Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file.

---

## [Unreleased]

### Bug Fixes

Two of these change what an existing test reads (the pull-up and the TWI timing), so a suite
that pinned the old values will need updating.

- **Backward RCALL on 22-bit-PC parts** (#21). `RCALL` added its signed offset as an unsigned
16-bit value, so on an ATmega2560 every backward `rcall` landed 0x10000 words too high and
the firmware never came back. The ATmega328P hid it because its flash length divides
0x10000. `RET`/`RETI` had a second bug on the same parts: a return address whose low 16
bits are zero (a call at word 0xFFFF) lost its high byte. Both now do the arithmetic on the
full address.
- **PINx reads the internal pull-up** (#22). An input pin whose PORT bit is set reads 1 while
nothing drives it, as the datasheet says, and enabling the pull-up on a floating pin raises
the same pin-change edge as on the chip. A pin is driven from the first `SetPinValue` until
the new `AvrIoPort.ReleasePin` lets it float again, so an embedder that injects every input
on every step sees no change. Tests that read a floating pull-up as 0 now read 1.
- **TWI transfers take their wire time** (#24). START and STOP take one SCL period and an
address or data byte nine (8 bits plus ACK), at `16 + 2 * TWBR * prescaler` CPU cycles per
period, before TWINT is raised. The time counts from the moment the event reaches the
`ITwiEventHandler`, so a handler that completes later than that adds no extra delay.
`AvrTwi.EmulateBusTiming = false` brings back the old next-cycle completion.
- **ADC reference selection on the ATmega328P.** REFS1:0 mapped to AVCC, AREF, 1.1 V, 2.56 V;
the datasheet order is AREF, AVCC, reserved, 1.1 V, so `analogReference(INTERNAL)`
converted against 2.56 V instead of 1.1 V.
- **`RunUntilSerial` no longer slows down as output accumulates** (#25). The predicate ran on
every instruction against a string rebuilt from all the bytes received so far, so long runs
that print were quadratic. The text is now cached and the predicate runs only when a byte
arrives. The core was checked across the 2^31 and 2^32 cycle boundaries and was not the
cause; tests now cover both.
- **MCUSR reports the reset cause** (#27). `AvrWatchdog` owns MCUSR: it starts with PORF set,
a CPU reset other than the watchdog's sets EXTRF, a watchdog reset sets WDRF, and firmware
clears a flag by writing a zero to it (writing a one never sets it).

### New: complete Uno and Mega presets (#27)

- `ArduinoUnoSimulation` and `ArduinoMegaSimulation` mount SPI, TWI, ADC and the watchdog,
exposed as `Spi`/`SpiBus`, `Twi`/`TwiBus`, `Adc` and `Watchdog`. With no device configured a
SPI transfer reads 0xFF, an I2C address is NACKed, and every ADC input reads 0 V. The
`SpiDeviceStub` and `TwiDeviceStub` behind them are now public, in `Avr8Sharp.TestKit.Probes`.
- `AvrAdc.Atmega2560AdcConfig` covers the 16 channels of the ATmega2560 (MUX5 selects ADC8 to
ADC15) with its reference order. MUX5 is ignored on 8-channel configs, as on the 328P.

### New: strict mode for unmounted ports (#26)

- **TestKit**: `UnmountedAccess` (`Ignore` by default, `Warn`, `Throw`) and the `WithStrict()`
shortcut. A read or write of PINx, DDRx or PORTx of a port that was never added throws
`UnmountedIoAccessException` ("PIND (0x29) read but port D is not mounted") or adds one
entry per register to `Warnings`. The hooks are installed only when the mode leaves
`Ignore`, so the default run costs nothing.
- **C ABI**: `a8s_set_strict`, `a8s_read_warnings` and `a8s_gpio_release_pin`.
- **Python**: `Simulation.with_strict(True | "warn" | False)`, `Simulation.warnings`,
`Port.release(pin)`, and `.adc`/`.spi`/`.twi` on `ArduinoMega`.

---

## [v2.0.0-beta1] — unreleased

### Renamed: the TestKit is now `SiliconTwin.AVR`
Expand Down
6 changes: 5 additions & 1 deletion bindings/python/src/avr8sharp/_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
# gpio observation and injection
"a8s_gpio_pin": ([c_void_p, c_int, c_int], c_int),
"a8s_gpio_set_pin": ([c_void_p, c_int, c_int, c_int], c_int),
# analog / bus peripherals (ATmega328P-family sessions)
"a8s_gpio_release_pin": ([c_void_p, c_int, c_int], c_int),
# analog / bus peripherals (Uno and Mega sessions)
"a8s_adc_set_channel": ([c_void_p, c_int, c_double], c_int),
"a8s_spi_queue_response": ([c_void_p, c_byte], c_int),
"a8s_spi_read_mosi": ([c_void_p, c_char_p, c_int], c_int),
Expand All @@ -75,6 +76,9 @@
"a8s_twi_read_writes": ([c_void_p, c_char_p, c_int], c_int),
"a8s_twi_read_events": ([c_void_p, c_char_p, c_int], c_int),
"a8s_twi_read_reads": ([c_void_p, c_char_p, c_int], c_int),
# strict mode (unmounted GPIO ports)
"a8s_set_strict": ([c_void_p, c_int], c_int),
"a8s_read_warnings": ([c_void_p, c_char_p, c_int], c_int),
# cpu / memory
"a8s_cpu_pc": ([c_void_p], c_uint),
"a8s_cpu_sp": ([c_void_p], c_uint),
Expand Down
47 changes: 38 additions & 9 deletions bindings/python/src/avr8sharp/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ def set_high(self, pin: int) -> None:
def set_low(self, pin: int) -> None:
self.set(pin, False)

def release(self, pin: int) -> None:
"""Stops driving `pin`, undoing set(): the line floats again, so an input pin whose PORT bit
is set reads 1 through the internal pull-up and any other floating input reads 0."""
lib = self._sim._lib
_check(lib, self._sim._h, lib.a8s_gpio_release_pin(self._sim._h, self._index, pin), "gpio_release_pin")


def _read_buffer(fn, sim: "Simulation", *args) -> bytes:
"""Two-pass read of a native length-prefixed byte buffer (call with cap, then size exactly)."""
Expand Down Expand Up @@ -357,6 +363,32 @@ def add_timer(self, which: int) -> int:
_check(self._lib, self._h, idx, "add_timer")
return idx

# ── strict mode ───────────────────────────────────────────────────────────

def with_strict(self, strict: bool | str = True) -> "Simulation":
"""Make firmware access to the PIN/DDR/PORT registers of a GPIO port that was never
mounted visible instead of silently reading 0. ``True`` raises (the failing run call
reports e.g. ``PIND (0x29) read but port D is not mounted``), ``"warn"`` records one
message per register in :attr:`warnings`, ``False`` restores the default."""
mode = {False: 0, "warn": 1, True: 2}.get(strict)
if mode is None:
raise ValueError("strict must be True, False or 'warn'")
_check(self._lib, self._h, self._lib.a8s_set_strict(self._h, mode), "set_strict")
return self

@property
def warnings(self) -> list[str]:
"""Messages recorded while ``with_strict("warn")`` is active."""
raw = _read_buffer(self._lib.a8s_read_warnings, self)
return raw.decode("utf-8", "replace").split("\n") if raw else []

def share_bus_responses(self) -> None:
"""Point the SPI and TWI stubs at ONE response queue: a byte queued through
either stub answers whichever bus the firmware reads next -- the bench wiring
of a single scripted responder feeding both buses."""
_check(self._lib, self._h, self._lib.a8s_bus_share_responses(self._h),
"bus_share_responses")

# ── reset / snapshot ──────────────────────────────────────────────────────

def reset(self) -> "Simulation":
Expand Down Expand Up @@ -396,7 +428,7 @@ def __del__(self) -> None:


class ArduinoUno(Simulation):
"""ATmega328P: ports B/C/D, timers 0/1/2, USART0, plus ADC and SPI/I²C device stubs.
"""ATmega328P: ports B/C/D, timers 0/1/2, USART0, plus ADC, watchdog and SPI/I²C device stubs.
16 MHz, 32 KB flash, 2 KB SRAM."""

def __init__(self) -> None:
Expand All @@ -409,16 +441,10 @@ def __init__(self) -> None:
self.spi = Spi(self)
self.twi = Twi(self)

def share_bus_responses(self) -> None:
"""Point the SPI and TWI stubs at ONE response queue: a byte queued through
either stub answers whichever bus the firmware reads next -- the bench wiring
of a single scripted responder feeding both buses."""
_check(self._lib, self._h, self._lib.a8s_bus_share_responses(self._h),
"bus_share_responses")


class ArduinoMega(Simulation):
"""ATmega2560: ports A..L, timers 0..5, USART0..3."""
"""ATmega2560: ports A..L, timers 0..5, USART0..3, plus ADC (16 channels) and SPI/I²C
device stubs."""

_PORT_LETTERS = "ABCDEFGHJKL"

Expand All @@ -431,6 +457,9 @@ def __init__(self) -> None:
self.serial1 = Serial(self, 1)
self.serial2 = Serial(self, 2)
self.serial3 = Serial(self, 3)
self.adc = Adc(self)
self.spi = Spi(self)
self.twi = Twi(self)


class ATtiny85(Simulation):
Expand Down
14 changes: 14 additions & 0 deletions bindings/python/tests/test_pin_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
PORTB_OUT = 0x25 # data address of PORTB (I/O address 0x05)
DDRB = 0x24 # data address of DDRB (I/O address 0x04)
PIND = 0x29
PORTD = 0x2b
UDR0 = 0xC6
UCSRA0 = 0xC0
UCSRA_RXC = 0x80
Expand Down Expand Up @@ -55,6 +56,19 @@ def test_set_pin_has_no_effect_on_an_output_pin():
uno.close()


def test_release_returns_an_input_pin_to_its_pull_up():
sim = a.Simulation.create()
port_d = sim.add_gpio(2)

port_d.set_low(2)
sim.cpu.write(PORTD, 0x04) # pull-up on, but the line is driven low
assert not (sim.cpu.read(PIND) & 0x04)

port_d.release(2)
assert sim.cpu.read(PIND) & 0x04 # floating, the pull-up wins
sim.close()


def test_serial_inject_arrives_in_udr_after_settling_time():
sim = a.Simulation.create()
serial = sim.add_usart0()
Expand Down
59 changes: 59 additions & 0 deletions bindings/python/tests/test_strict_and_boards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Strict mode for unmounted GPIO ports, and the SPI/TWI/ADC/watchdog peripherals of the board presets."""

from __future__ import annotations

import pytest

import avr8sharp as a

MCUSR = 0x54
READ_PIND = "in r16, 0x09\nbreak\n"


def test_unmounted_port_reads_zero_by_default():
sim = a.Simulation.create()
sim.with_asm(READ_PIND).run_to_break()
assert sim.cpu.reg(16) == 0
sim.close()


def test_strict_raises_naming_register_and_address():
sim = a.Simulation.create().with_strict()
sim.with_asm(READ_PIND)
with pytest.raises(a.Avr8SharpError, match=r"PIND \(0x29\) read but port D is not mounted"):
sim.run_to_break()
sim.close()


def test_strict_allows_a_mounted_port():
sim = a.Simulation.create().with_strict()
sim.add_gpio(2)
sim.with_asm(READ_PIND).run_to_break()
sim.close()


def test_strict_warn_records_without_raising():
sim = a.Simulation.create().with_strict("warn")
sim.with_asm(READ_PIND + "").run_to_break()
assert len(sim.warnings) == 1 and "PIND" in sim.warnings[0]
sim.close()


def test_uno_mcusr_is_porf_and_adc_spi_twi_exist():
uno = a.ArduinoUno()
assert uno.cpu.read(MCUSR) == 0x01
uno.with_asm(
"ldi r16, 0x50\nout 0x2C, r16\nldi r16, 0xAA\nout 0x2E, r16\n"
"w: in r17, 0x2D\nsbrs r17, 7\nrjmp w\nin r18, 0x2E\nbreak\n"
)
uno.spi.queue_response(0x42)
uno.run_to_break()
assert uno.cpu.reg(18) == 0x42
uno.close()


def test_mega_has_the_bus_peripherals():
mega = a.ArduinoMega()
assert mega.cpu.read(MCUSR) == 0x01
mega.adc.set_channel(9, 2.5)
mega.close()
64 changes: 44 additions & 20 deletions src/Avr8Sharp.Native/Exports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ public static IntPtr CreateUno()
s.Ports.AddRange([uno.PortB, uno.PortC, uno.PortD]);
s.Timers.AddRange([uno.Timer0, uno.Timer1, uno.Timer2]);
s.Serials.Add(uno.Serial);
WireAtmega328Analog(uno, s);
s.Adc = uno.Adc;
s.Spi = uno.SpiBus;
s.Twi = uno.TwiBus;
s.Watchdog = uno.Watchdog;
return Wrap(s);
}

Expand All @@ -92,6 +95,10 @@ public static IntPtr CreateMega()
m.PortG, m.PortH, m.PortJ, m.PortK, m.PortL]);
s.Timers.AddRange([m.Timer0, m.Timer1, m.Timer2, m.Timer3, m.Timer4, m.Timer5]);
s.Serials.AddRange([m.Serial0, m.Serial1, m.Serial2, m.Serial3]);
s.Adc = m.Adc;
s.Spi = m.SpiBus;
s.Twi = m.TwiBus;
s.Watchdog = m.Watchdog;
return Wrap(s);
}

Expand Down Expand Up @@ -385,6 +392,18 @@ public static int GpioSetPin(IntPtr h, int portIdx, int pin, int value)
s.Ports[portIdx].SetPinValue((byte)pin, value != 0);
});

/// <summary>Stops driving <paramref name="pin"/> on port <paramref name="portIdx"/>, undoing
/// <c>a8s_gpio_set_pin</c>: the line floats again, so an input pin with its PORT bit set reads
/// 1 through the internal pull-up and any other floating input reads 0.</summary>
[UnmanagedCallersOnly(EntryPoint = "a8s_gpio_release_pin")]
public static int GpioReleasePin(IntPtr h, int portIdx, int pin)
=> Run(h, s =>
{
if (portIdx < 0 || portIdx >= s.Ports.Count)
throw new ArgumentOutOfRangeException(nameof(portIdx));
s.Ports[portIdx].ReleasePin((byte)pin);
});

// ── CPU / memory observation ──────────────────────────────────────────────

[UnmanagedCallersOnly(EntryPoint = "a8s_cpu_pc")]
Expand Down Expand Up @@ -434,12 +453,35 @@ public static int Restore(IntPtr h)
Array.Copy(s.Snapshot, s.Sim.Data, s.Snapshot.Length);
foreach (var t in s.Timers) t.Reset();
s.Sim.Cpu.Reset();
// The reset above would flag EXTRF; a restore rewinds to the power-on state instead.
s.Watchdog?.PowerOnReset();
s.Sim.Cpu.Cycles = 0;
foreach (var probe in s.Serials) probe.Clear();
if (s.Spi is not null) { s.Spi.Mosi.Clear(); s.Spi.Responses.Clear(); }
if (s.Twi is not null) { s.Twi.Writes.Clear(); s.Twi.Responses.Clear(); }
});

// ── Strict mode ───────────────────────────────────────────────────────────

/// <summary>Sets how access to an unmounted GPIO port is treated: 0 = ignore (default),
/// 1 = record a warning, 2 = throw (the failing run call then reports the message).</summary>
[UnmanagedCallersOnly(EntryPoint = "a8s_set_strict")]
public static int SetStrict(IntPtr h, int mode)
=> Run(h, s =>
{
if (mode < 0 || mode > 2) throw new ArgumentOutOfRangeException(nameof(mode));
s.Sim.UnmountedAccess = (UnmountedAccessMode)mode;
});

/// <summary>Copies the recorded unmounted-access warnings (UTF-8, one per line) into the caller
/// buffer; returns the full length.</summary>
[UnmanagedCallersOnly(EntryPoint = "a8s_read_warnings")]
public static int ReadWarnings(IntPtr h, byte* outBuf, int cap)
{
if (Get(h) is not { } s) return ErrBadHandle;
return CopyOut(Encoding.UTF8.GetBytes(string.Join("\n", s.Sim.Warnings)), outBuf, cap);
}

// ── Errors ────────────────────────────────────────────────────────────────

/// <summary>Copies the last error message (UTF-8) into the caller buffer; returns full length.</summary>
Expand All @@ -450,25 +492,7 @@ public static int LastError(IntPtr h, byte* outBuf, int cap)
return CopyOut(Encoding.UTF8.GetBytes(s.LastError), outBuf, cap);
}

// ── Analog/bus peripherals (ATmega328P-family sessions) ───────────────────

/// <summary>Wires ADC, an SPI bus stub, and a single-address I²C slave stub onto an
/// ATmega328P-style simulation, using the 328P peripheral configs.</summary>
private static void WireAtmega328Analog(AvrTestSimulation sim, NativeSession s)
{
sim.AddAdc(AvrAdc.AdcConfig, out var adc);
s.Adc = adc;

sim.AddSpi(AvrSpi.SpiConfig, out var spi);
var spiStub = new SpiDeviceStub();
spi.OnTransfer = spiStub.Transfer;
s.Spi = spiStub;

sim.AddTwi(AvrTwi.TwiConfig, out var twi);
var twiStub = new TwiDeviceStub(twi);
twi.EventHandler = twiStub;
s.Twi = twiStub;
}
// ── Analog/bus peripherals (ATmega328P and ATmega2560 board sessions) ─────

/// <summary>Sets the analog voltage (volts) presented on ADC <paramref name="channel"/>,
/// so a firmware ADC read on that channel returns the corresponding value.</summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Avr8Sharp.Native/NativeSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal sealed class NativeSession(AvrTestSimulation sim)
public readonly List<SerialProbe> Serials = new();
public readonly List<AvrTimer> Timers = new();

/// <summary>ADC peripheral (ATmega328P-family sessions only); null otherwise.</summary>
/// <summary>ADC peripheral (Uno and Mega board sessions only); null otherwise.</summary>
public AvrAdc? Adc;

/// <summary>SPI master-bus stub: captures MOSI, replays a canned response queue.</summary>
Expand All @@ -26,6 +26,9 @@ internal sealed class NativeSession(AvrTestSimulation sim)
/// <summary>I²C single-address slave stub: ACKs its address, logs writes, replays reads.</summary>
public TwiDeviceStub? Twi;

/// <summary>Watchdog (owns MCUSR); present on the Uno and Mega board sessions.</summary>
public AvrWatchdog? Watchdog;

/// <summary>Power-on snapshot of the Data array, captured by <c>a8s_snapshot</c>.</summary>
public byte[]? Snapshot;

Expand Down
Loading
Loading