diff --git a/CHANGELOG.md b/CHANGELOG.md
index b02c21d..9140683 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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`
diff --git a/bindings/python/src/avr8sharp/_native.py b/bindings/python/src/avr8sharp/_native.py
index 81df546..b5db097 100644
--- a/bindings/python/src/avr8sharp/_native.py
+++ b/bindings/python/src/avr8sharp/_native.py
@@ -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),
@@ -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),
diff --git a/bindings/python/src/avr8sharp/simulation.py b/bindings/python/src/avr8sharp/simulation.py
index 697674b..3986038 100644
--- a/bindings/python/src/avr8sharp/simulation.py
+++ b/bindings/python/src/avr8sharp/simulation.py
@@ -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)."""
@@ -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":
@@ -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:
@@ -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"
@@ -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):
diff --git a/bindings/python/tests/test_pin_injection.py b/bindings/python/tests/test_pin_injection.py
index 5dfc8c5..8791c7b 100644
--- a/bindings/python/tests/test_pin_injection.py
+++ b/bindings/python/tests/test_pin_injection.py
@@ -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
@@ -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()
diff --git a/bindings/python/tests/test_strict_and_boards.py b/bindings/python/tests/test_strict_and_boards.py
new file mode 100644
index 0000000..144cb49
--- /dev/null
+++ b/bindings/python/tests/test_strict_and_boards.py
@@ -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()
diff --git a/src/Avr8Sharp.Native/Exports.cs b/src/Avr8Sharp.Native/Exports.cs
index 7898eba..6004ad8 100644
--- a/src/Avr8Sharp.Native/Exports.cs
+++ b/src/Avr8Sharp.Native/Exports.cs
@@ -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);
}
@@ -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);
}
@@ -385,6 +392,18 @@ public static int GpioSetPin(IntPtr h, int portIdx, int pin, int value)
s.Ports[portIdx].SetPinValue((byte)pin, value != 0);
});
+ /// Stops driving on port , undoing
+ /// a8s_gpio_set_pin: 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.
+ [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")]
@@ -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 ───────────────────────────────────────────────────────────
+
+ /// 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).
+ [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;
+ });
+
+ /// Copies the recorded unmounted-access warnings (UTF-8, one per line) into the caller
+ /// buffer; returns the full length.
+ [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 ────────────────────────────────────────────────────────────────
/// Copies the last error message (UTF-8) into the caller buffer; returns full length.
@@ -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) ───────────────────
-
- /// Wires ADC, an SPI bus stub, and a single-address I²C slave stub onto an
- /// ATmega328P-style simulation, using the 328P peripheral configs.
- 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) ─────
/// Sets the analog voltage (volts) presented on ADC ,
/// so a firmware ADC read on that channel returns the corresponding value.
diff --git a/src/Avr8Sharp.Native/NativeSession.cs b/src/Avr8Sharp.Native/NativeSession.cs
index f2ee670..73fc23b 100644
--- a/src/Avr8Sharp.Native/NativeSession.cs
+++ b/src/Avr8Sharp.Native/NativeSession.cs
@@ -17,7 +17,7 @@ internal sealed class NativeSession(AvrTestSimulation sim)
public readonly List Serials = new();
public readonly List Timers = new();
- /// ADC peripheral (ATmega328P-family sessions only); null otherwise.
+ /// ADC peripheral (Uno and Mega board sessions only); null otherwise.
public AvrAdc? Adc;
/// SPI master-bus stub: captures MOSI, replays a canned response queue.
@@ -26,6 +26,9 @@ internal sealed class NativeSession(AvrTestSimulation sim)
/// I²C single-address slave stub: ACKs its address, logs writes, replays reads.
public TwiDeviceStub? Twi;
+ /// Watchdog (owns MCUSR); present on the Uno and Mega board sessions.
+ public AvrWatchdog? Watchdog;
+
/// Power-on snapshot of the Data array, captured by a8s_snapshot.
public byte[]? Snapshot;
diff --git a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs
index be6a97e..b2b93e3 100644
--- a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs
@@ -42,6 +42,57 @@ public class AvrTestSimulation
private LutDecoder _decoder;
+ private AvrClock? _clock;
+
+ // PORT register addresses of the GPIO ports mounted through AddGpio.
+ private readonly HashSet _mountedPorts = new();
+ private UnmountedAccessMode _unmountedAccess = UnmountedAccessMode.Ignore;
+ private bool _unmountedHooksInstalled;
+ private readonly List _warnings = new();
+
+ // Low-I/O ports whose registers never overlap another peripheral on the ATmega parts.
+ private static readonly (char Letter, AvrPortConfig Config)[] KnownPorts =
+ [
+ ('A', AvrIoPort.PortAConfig), ('B', AvrIoPort.PortBConfig), ('C', AvrIoPort.PortCConfig),
+ ('D', AvrIoPort.PortDConfig), ('E', AvrIoPort.PortEConfig), ('F', AvrIoPort.PortFConfig),
+ ('G', AvrIoPort.PortGConfig),
+ ];
+
+ ///
+ /// Whether checks the GPIO register block of the ATmega ports. Boards
+ /// whose low I/O space holds other peripherals (ATtiny) turn it off, since an address in that
+ /// range is not necessarily a missing port there.
+ ///
+ protected virtual bool ChecksUnmountedPorts => true;
+
+ ///
+ /// What happens when firmware reads or writes PIN/DDR/PORT of a port that was never mounted with
+ /// . Defaults to , which leaves the core
+ /// untouched; the checking hooks are only installed once the mode is changed.
+ ///
+ public UnmountedAccessMode UnmountedAccess
+ {
+ get => _unmountedAccess;
+ set
+ {
+ _unmountedAccess = value;
+ if (value != UnmountedAccessMode.Ignore) InstallUnmountedHooks();
+ }
+ }
+
+ /// Messages recorded in mode, one per distinct register and access kind.
+ public IReadOnlyList Warnings => _warnings;
+
+ ///
+ /// Strict mode: accessing a register of an unmounted GPIO port throws
+ /// naming the register and address. Pass false to turn it off.
+ ///
+ public AvrTestSimulation WithStrict(bool strict = true)
+ {
+ UnmountedAccess = strict ? UnmountedAccessMode.Throw : UnmountedAccessMode.Ignore;
+ return this;
+ }
+
public byte[] Data => Runner.Cpu.Mmio.Data;
public AvrMemoryView Memory => new(Runner.Cpu.Mmio.Data);
@@ -91,8 +142,8 @@ public AvrTestSimulation WithProgram(byte[] bytes)
}
///
- /// Resets the CPU to its power-on state: PC=0, SP=RAMEND, SREG=0, pending interrupts cleared.
- /// Does not clear the loaded program.
+ /// Resets the CPU as the RESET pin would: PC=0, SP=RAMEND, SREG=0, pending interrupts cleared.
+ /// Does not clear the loaded program. With a watchdog mounted, MCUSR keeps its flags and EXTRF is set.
/// Returns this for chaining.
///
public AvrTestSimulation Reset()
@@ -107,6 +158,7 @@ public AvrTestSimulation Reset()
public AvrTestSimulation AddGpio(AvrPortConfig config, out AvrIoPort port)
{
port = new AvrIoPort(Runner.Cpu, config);
+ _mountedPorts.Add(config.PORT);
return this;
}
@@ -176,6 +228,54 @@ public AvrTestSimulation AddAdc(AvrAdcConfig config, out AvrAdc adc)
return this;
}
+ ///
+ /// Adds the watchdog together with its system clock. The peripheral owns MCUSR, so MCUSR reads
+ /// PORF after this call and a CPU reset sets EXTRF (a watchdog reset sets WDRF).
+ ///
+ public AvrTestSimulation AddWatchdog(AvrWatchdogConfig config, out AvrWatchdog watchdog)
+ {
+ _clock ??= new AvrClock(Runner.Cpu, Runner.Speed, AvrClock.ClockConfig);
+ watchdog = new AvrWatchdog(Runner.Cpu, config, _clock);
+ return this;
+ }
+
+ private void InstallUnmountedHooks()
+ {
+ if (_unmountedHooksInstalled || !ChecksUnmountedPorts) return;
+ _unmountedHooksInstalled = true;
+ var mmio = Runner.Cpu.Mmio;
+ foreach (var (letter, config) in KnownPorts)
+ {
+ if (_mountedPorts.Contains(config.PORT)) continue;
+ foreach (var (reg, address) in new[] { ("PIN", config.PIN), ("DDR", config.DDR), ("PORT", config.PORT) })
+ {
+ var name = $"{reg}{letter}";
+ var portAddress = config.PORT;
+ var portLetter = letter;
+ // A port mounted later replaces the read hook and is skipped by the write hook.
+ mmio.RegisterRead(address, a =>
+ {
+ CheckUnmounted(portAddress, name, a, false, portLetter);
+ return mmio.Data[a];
+ });
+ mmio.RegisterWrite(address, (_, _, a, _) =>
+ {
+ CheckUnmounted(portAddress, name, a, true, portLetter);
+ return false;
+ });
+ }
+ }
+ }
+
+ private void CheckUnmounted(ushort portAddress, string name, ushort address, bool isWrite, char letter)
+ {
+ if (_unmountedAccess == UnmountedAccessMode.Ignore || _mountedPorts.Contains(portAddress)) return;
+ var message = $"{name} (0x{address:X2}) {(isWrite ? "written" : "read")} but port {letter} is not mounted (use AddGpio)";
+ if (_unmountedAccess == UnmountedAccessMode.Throw)
+ throw new UnmountedIoAccessException(message, address, isWrite);
+ if (!_warnings.Contains(message)) _warnings.Add(message);
+ }
+
// ── Execution ────────────────────────────────────────────────────────────
///
@@ -335,6 +435,27 @@ public AvrTestSimulation RunUntilMs(
$"({Runner.Cpu.Cycles} cycles elapsed).");
}
+ ///
+ /// Wraps a text predicate so it is evaluated only when the captured output changed
+ /// since the last call. The step loop polls the predicate once per instruction;
+ /// rebuilding and scanning the whole text each time made long runs quadratic.
+ ///
+ private static Func OnSerialChange(
+ Probes.SerialProbe serial, Func predicate)
+ {
+ var version = -1;
+ var result = false;
+ return _ =>
+ {
+ if (serial.Version != version)
+ {
+ version = serial.Version;
+ result = predicate(serial.Text);
+ }
+ return result;
+ };
+ }
+
///
/// Runs until the captured text satisfies ,
/// or until of simulated time elapses.
@@ -343,7 +464,7 @@ public AvrTestSimulation RunUntilSerial(
Probes.SerialProbe serial,
Func predicate,
double maxMs = 2000)
- => RunUntilMs(_ => predicate(serial.Text), maxMs);
+ => RunUntilMs(OnSerialChange(serial, predicate), maxMs);
///
/// Runs until contains the given as a substring,
@@ -353,7 +474,7 @@ public AvrTestSimulation RunUntilSerial(
Probes.SerialProbe serial,
string text,
double maxMs = 2000)
- => RunUntilMs(_ => serial.Text.Contains(text), maxMs);
+ => RunUntilMs(OnSerialChange(serial, t => t.Contains(text)), maxMs);
///
/// Runs until has received at least bytes,
diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs
index 634e40f..82ff01c 100644
--- a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs
@@ -22,6 +22,9 @@ public sealed class ATtiny13Simulation : AvrTestSimulation
/// Port B — the only GPIO port (PB0–PB5).
public AvrIoPort PortB { get; }
+ // The low I/O space holds ADC/USI/timer registers here, so a missing ATmega port is not detectable.
+ protected override bool ChecksUnmountedPorts => false;
+
public ATtiny13Simulation() : base(Flash, Sram)
{
WithFrequency(Frequency);
diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs
index 35933bf..15e7225 100644
--- a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs
@@ -140,6 +140,9 @@ public sealed class ATtiny85Simulation : AvrTestSimulation
/// ATtiny85 internal EEPROM — 512 bytes, volatile (in-memory backend).
public AvrEeprom Eeprom { get; }
+ // The low I/O space holds ADC/USI/timer registers here, so a missing ATmega port is not detectable.
+ protected override bool ChecksUnmountedPorts => false;
+
public ATtiny85Simulation() : base(Flash, Sram)
{
WithFrequency(Frequency);
diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs
index 1dd28e8..abdd47f 100644
--- a/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs
@@ -27,6 +27,9 @@ public sealed class ATtinyX4Simulation : AvrTestSimulation
/// Port B — PB0–PB3.
public AvrIoPort PortB { get; }
+ // The low I/O space holds ADC/USI/timer registers here, so a missing ATmega port is not detectable.
+ protected override bool ChecksUnmountedPorts => false;
+
public ATtinyX4Simulation(int flash, int sram) : base(flash, sram)
{
WithFrequency(Frequency);
diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs
index 21c98e5..101b6c0 100644
--- a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs
@@ -164,6 +164,28 @@ public sealed class ArduinoMegaSimulation : AvrTestSimulation
eecr: 0x3F, eedr: 0x40, eearl: 0x41, eearh: 0x42,
eraseCycles: 28800, writeCycles: 28800, atomicCycles: 54400);
+ // SPI — same I/O registers as ATmega328P (0x4C-0x4E); SPI STC is avr-libc vector 24 -> word 0x30
+ private static readonly AvrSpiConfig Mega2560SpiConfig = new AvrSpiConfig
+ {
+ SpiInterrupt = 0x30,
+ SPCR = 0x4c, SPSR = 0x4d, SPDR = 0x4e,
+ };
+
+ // TWI — same I/O registers as ATmega328P (0xB8-0xBD); TWI is avr-libc vector 39 -> word 0x4E
+ private static readonly AvrTwiConfig Mega2560TwiConfig = new()
+ {
+ TwiInterrupt = 0x4e,
+ TWBR = 0xb8, TWSR = 0xb9, TWAR = 0xba, TWDR = 0xbb, TWCR = 0xbc, TWAMR = 0xbd,
+ };
+
+ // Watchdog — MCUSR 0x54 and WDTCSR 0x60 as on the ATmega328P; WDT is avr-libc vector 12 -> word 0x18
+ private static readonly AvrWatchdogConfig Mega2560WatchdogConfig = new AvrWatchdogConfig
+ {
+ WatchdogInterrupt = 0x18,
+ MCUSR = 0x54,
+ WDTCSR = 0x60,
+ };
+
// ── GPIO ports ────────────────────────────────────────────────────────────
/// Port A — digital pins 22–29.
public AvrIoPort PortA { get; }
@@ -216,6 +238,23 @@ public sealed class ArduinoMegaSimulation : AvrTestSimulation
/// ATmega2560 internal EEPROM — 4096 bytes, volatile (in-memory backend).
public AvrEeprom Eeprom { get; }
+ // ── SPI / TWI / ADC / watchdog ────────────────────────────────────────────
+ /// SPI peripheral (MOSI = PB2, MISO = PB3, SCK = PB1). Transfers are answered by .
+ public AvrSpi Spi { get; }
+ /// Scripted SPI slave: records MOSI bytes, answers with queued responses or 0xFF.
+ public SpiDeviceStub SpiBus { get; }
+ /// TWI (I²C) peripheral (SCL = PD0, SDA = PD1). Transactions are answered by .
+ public AvrTwi Twi { get; }
+ /// Scripted I²C slave; NACKs every address until one is added to .
+ public TwiDeviceStub TwiBus { get; }
+ ///
+ /// 10-bit ADC with 16 single-ended channels (A0-A15; channels 8-15 via MUX5), the 1.1 V bandgap
+ /// and GND. Differential inputs are not modelled. Channels read 0 V until set via .
+ ///
+ public AvrAdc Adc { get; }
+ /// Watchdog timer. It also owns MCUSR, which reads PORF (0x01) after construction.
+ public AvrWatchdog Watchdog { get; }
+
public ArduinoMegaSimulation() : base(Flash, Sram)
{
WithFrequency(Frequency);
@@ -249,5 +288,17 @@ public ArduinoMegaSimulation() : base(Flash, Sram)
AddUsart(Mega2560Usart3Config, out var s3); Serial3 = s3;
AddEeprom(Mega2560EepromConfig, out var eeprom, EepromSize); Eeprom = eeprom;
+
+ AddSpi(Mega2560SpiConfig, out var spi); Spi = spi;
+ SpiBus = new SpiDeviceStub();
+ spi.OnTransfer = SpiBus.Transfer;
+
+ AddTwi(Mega2560TwiConfig, out var twi); Twi = twi;
+ TwiBus = new TwiDeviceStub(twi);
+ twi.EventHandler = TwiBus;
+
+ AddAdc(AvrAdc.Atmega2560AdcConfig, out var adc); Adc = adc;
+
+ AddWatchdog(Mega2560WatchdogConfig, out var watchdog); Watchdog = watchdog;
}
}
diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs
index 7ef2e8a..9894cfc 100644
--- a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs
+++ b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs
@@ -7,8 +7,9 @@ namespace Avr8Sharp.TestKit.Boards;
/// Pre-configured simulation for the Arduino Uno (ATmega328P).
///
/// All standard peripherals are created automatically:
-/// three GPIO ports (B, C, D), three timers (0, 1, 2), and USART0.
-/// Serial output is captured in .
+/// three GPIO ports (B, C, D), three timers (0, 1, 2), USART0, EEPROM, SPI, TWI, ADC and the watchdog.
+/// Serial output is captured in . SPI and TWI are wired to scripted slaves
+/// (, ) so transfers always complete.
///
///
///
@@ -53,6 +54,26 @@ public sealed class ArduinoUnoSimulation : AvrTestSimulation
/// ATmega328P internal EEPROM — 1024 bytes, volatile (in-memory backend).
public AvrEeprom Eeprom { get; }
+ // ── SPI / TWI / ADC / watchdog ────────────────────────────────────────────
+ /// SPI peripheral (SPCR/SPSR/SPDR at 0x4C-0x4E). Transfers are answered by .
+ public AvrSpi Spi { get; }
+ ///
+ /// Scripted SPI slave: records every MOSI byte and answers with ,
+ /// or 0xFF (idle MISO) once the queue is empty, so a transfer always completes.
+ ///
+ public SpiDeviceStub SpiBus { get; }
+ /// TWI (I²C) peripheral (TWBR..TWAMR at 0xB8-0xBD). Transactions are answered by .
+ public AvrTwi Twi { get; }
+ ///
+ /// Scripted I²C slave. With no address in every SLA+W/R is
+ /// NACKed, as on a bus with pull-ups and no device; add addresses to make it ACK.
+ ///
+ public TwiDeviceStub TwiBus { get; }
+ /// 10-bit ADC. Every channel reads 0 V until set through .
+ public AvrAdc Adc { get; }
+ /// Watchdog timer. It also owns MCUSR, which reads PORF (0x01) after construction.
+ public AvrWatchdog Watchdog { get; }
+
public ArduinoUnoSimulation() : base(Flash, Sram)
{
WithFrequency(Frequency);
@@ -73,5 +94,17 @@ public ArduinoUnoSimulation() : base(Flash, Sram)
AddUsart(AvrUsart.Usart0Config, out var serial); Serial = serial;
AddEeprom(AvrEeprom.EepromConfig, out var eeprom); Eeprom = eeprom;
+
+ AddSpi(AvrSpi.SpiConfig, out var spi); Spi = spi;
+ SpiBus = new SpiDeviceStub();
+ spi.OnTransfer = SpiBus.Transfer;
+
+ AddTwi(AvrTwi.TwiConfig, out var twi); Twi = twi;
+ TwiBus = new TwiDeviceStub(twi);
+ twi.EventHandler = TwiBus;
+
+ AddAdc(AvrAdc.AdcConfig, out var adc); Adc = adc;
+
+ AddWatchdog(AvrWatchdog.WatchdogConfig, out var watchdog); Watchdog = watchdog;
}
}
diff --git a/src/Avr8Sharp.Native/DeviceStubs.cs b/src/Avr8Sharp.TestKit.Core/Probes/DeviceStubs.cs
similarity index 94%
rename from src/Avr8Sharp.Native/DeviceStubs.cs
rename to src/Avr8Sharp.TestKit.Core/Probes/DeviceStubs.cs
index eed12c6..9db2459 100644
--- a/src/Avr8Sharp.Native/DeviceStubs.cs
+++ b/src/Avr8Sharp.TestKit.Core/Probes/DeviceStubs.cs
@@ -1,13 +1,13 @@
using AVR8Sharp.Core.Peripherals;
-namespace Avr8Sharp.Native;
+namespace Avr8Sharp.TestKit.Probes;
///
/// A canned SPI slave on the bus. Captures every byte the firmware clocks out (MOSI) and replays
/// a pre-loaded response queue back (MISO) — the standard way to test SPI sensor/display drivers
/// without modelling a real device. Returns 0xFF once the queue is drained (idle MISO line).
///
-internal sealed class SpiDeviceStub
+public sealed class SpiDeviceStub
{
public readonly List Mosi = new();
// Settable so a session can point both bus stubs at ONE queue -- the bench
@@ -27,7 +27,7 @@ public int Transfer(byte outgoing)
/// (0xFF when drained). The response queue is shared across addresses, the way a scripted
/// bench with one responder per pin would be wired.
///
-internal sealed class TwiDeviceStub(AvrTwi twi) : ITwiEventHandler
+public sealed class TwiDeviceStub(AvrTwi twi) : ITwiEventHandler
{
/// 7-bit slave addresses this device answers to.
public readonly HashSet Addresses = new();
diff --git a/src/Avr8Sharp.TestKit.Core/Probes/SerialProbe.cs b/src/Avr8Sharp.TestKit.Core/Probes/SerialProbe.cs
index 32f0b83..2dfc315 100644
--- a/src/Avr8Sharp.TestKit.Core/Probes/SerialProbe.cs
+++ b/src/Avr8Sharp.TestKit.Core/Probes/SerialProbe.cs
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
+using System.Runtime.InteropServices;
using System.Text;
using AVR8Sharp.Core.Peripherals;
@@ -13,6 +14,7 @@ public class SerialProbe
private readonly List _rawBytes = new();
private readonly AvrUsart _usart;
private ReadOnlyCollection? _linesCache;
+ private string? _textCache;
internal SerialProbe(AvrUsart usart)
{
@@ -21,11 +23,23 @@ internal SerialProbe(AvrUsart usart)
{
_rawBytes.Add(b);
_linesCache = null;
+ _textCache = null;
+ Version++;
};
}
- /// All characters received so far as a single string (Latin-1 encoded).
- public string Text => Encoding.Latin1.GetString(_rawBytes.ToArray());
+ ///
+ /// Bumped on every received byte and on . Lets run helpers skip
+ /// re-evaluating a text predicate while the captured output is unchanged.
+ ///
+ internal int Version { get; private set; }
+
+ ///
+ /// All characters received so far as a single string (Latin-1 encoded).
+ /// The string is cached until the next byte arrives, so polling it between
+ /// instructions does not allocate.
+ ///
+ public string Text => _textCache ??= Encoding.Latin1.GetString(CollectionsMarshal.AsSpan(_rawBytes));
/// All received bytes as a raw byte array (useful for binary-protocol tests).
public byte[] Bytes => _rawBytes.ToArray();
@@ -45,6 +59,8 @@ public void Clear()
{
_rawBytes.Clear();
_linesCache = null;
+ _textCache = null;
+ Version++;
}
///
diff --git a/src/Avr8Sharp.TestKit.Core/UnmountedIoAccess.cs b/src/Avr8Sharp.TestKit.Core/UnmountedIoAccess.cs
new file mode 100644
index 0000000..88b6745
--- /dev/null
+++ b/src/Avr8Sharp.TestKit.Core/UnmountedIoAccess.cs
@@ -0,0 +1,24 @@
+namespace Avr8Sharp.TestKit;
+
+/// What does when firmware touches the registers of a GPIO port that was never mounted.
+public enum UnmountedAccessMode
+{
+ /// Default. The access reads as plain memory (0 until written), exactly like the core.
+ Ignore = 0,
+
+ /// The first access to each register is recorded in ; execution continues.
+ Warn = 1,
+
+ /// The access throws .
+ Throw = 2,
+}
+
+/// Thrown in mode when firmware accesses a register of an unmounted peripheral.
+public sealed class UnmountedIoAccessException(string message, ushort address, bool isWrite) : InvalidOperationException(message)
+{
+ /// Data-space address that was accessed.
+ public ushort Address { get; } = address;
+
+ /// true for a write, false for a read.
+ public bool IsWrite { get; } = isWrite;
+}
diff --git a/src/Avr8Sharp/Core/Opcodes.cs b/src/Avr8Sharp/Core/Opcodes.cs
index 9309b4e..005b693 100644
--- a/src/Avr8Sharp/Core/Opcodes.cs
+++ b/src/Avr8Sharp/Core/Opcodes.cs
@@ -735,7 +735,7 @@ public static void RCALL(ref Cpu cpu, ref ushort opcode)
}
cpu.Mmio.DataView.SetUint16(93, (ushort)(sp - (cpu.Pc22Bits ? 3 : 2)), true);
- cpu.Pc += (ushort)k;
+ cpu.Pc = (uint)((int)cpu.Pc + k);
cpu.Cycles += cpu.Pc22Bits ? 3UL : 2UL;
}
@@ -746,12 +746,14 @@ public static void RET(ref Cpu cpu, ref ushort opcode)
if (i >= cpu.Mmio.Data.Length)
throw new AvrStackUnderflowException(cpu.Pc, i - (cpu.Pc22Bits ? 3 : 2));
cpu.Mmio.DataView.SetUint16(93, (ushort)i, true);
- cpu.Pc = (uint)((cpu.Mmio.Data[i - 1] << 8) + cpu.Mmio.Data[i] - 1);
+ var target = (cpu.Mmio.Data[i - 1] << 8) + cpu.Mmio.Data[i];
if (cpu.Pc22Bits)
{
- cpu.Pc |= (uint)(cpu.Mmio.Data[i - 2] << 16);
+ target |= cpu.Mmio.Data[i - 2] << 16;
}
+ cpu.Pc = (uint)(target - 1);
+
cpu.Cycles += cpu.Pc22Bits ? 4UL : 3UL;
}
@@ -762,12 +764,14 @@ public static void RETI(ref Cpu cpu, ref ushort opcode)
if (i >= cpu.Mmio.Data.Length)
throw new AvrStackUnderflowException(cpu.Pc, i - (cpu.Pc22Bits ? 3 : 2));
cpu.Mmio.DataView.SetUint16(93, (ushort)i, true);
- cpu.Pc = (uint)((cpu.Mmio.Data[i - 1] << 8) + cpu.Mmio.Data[i] - 1);
+ var target = (cpu.Mmio.Data[i - 1] << 8) + cpu.Mmio.Data[i];
if (cpu.Pc22Bits)
{
- cpu.Pc |= (uint)(cpu.Mmio.Data[i - 2] << 16);
+ target |= cpu.Mmio.Data[i - 2] << 16;
}
+ cpu.Pc = (uint)(target - 1);
+
cpu.Cycles += cpu.Pc22Bits ? 4UL : 3UL;
cpu.Mmio.Data[95] |= 0x80; // Enable interrupts
}
diff --git a/src/Avr8Sharp/Core/Peripherals/Adc.cs b/src/Avr8Sharp/Core/Peripherals/Adc.cs
index d739370..07f2321 100644
--- a/src/Avr8Sharp/Core/Peripherals/Adc.cs
+++ b/src/Avr8Sharp/Core/Peripherals/Adc.cs
@@ -20,6 +20,47 @@ public class AvrAdc
{ 15, new AdcMuxInput(type: AdcMuxInputType.Constant, voltage: 0) },
};
+ ///
+ /// ATmega2560 mux: single-ended ADC0..ADC7 (MUX5=0, MUX 0..7) and ADC8..ADC15 (MUX5=1,
+ /// MUX 0..7, index 0x20..0x27), 1.1 V bandgap (0x1E) and GND (0x1F). Differential inputs are
+ /// not modelled and read as 0 V.
+ ///
+ public static ADCMuxConfiguration Atmega2560Channels { get; } = BuildAtmega2560Channels();
+
+ private static ADCMuxConfiguration BuildAtmega2560Channels()
+ {
+ var map = new ADCMuxConfiguration();
+ for (var i = 0; i < 8; i++)
+ {
+ map[i] = new AdcMuxInput(type: AdcMuxInputType.SingleEnded, channel: i);
+ map[0x20 + i] = new AdcMuxInput(type: AdcMuxInputType.SingleEnded, channel: 8 + i);
+ }
+ map[0x1e] = new AdcMuxInput(type: AdcMuxInputType.Constant, voltage: 1.1);
+ map[0x1f] = new AdcMuxInput(type: AdcMuxInputType.Constant, voltage: 0);
+ return map;
+ }
+
+ /// ATmega2560 ADC: same register block as the ATmega328P, 16 channels, ADC vector 29.
+ public static readonly AvrAdcConfig Atmega2560AdcConfig = new AvrAdcConfig(
+ admux: 0x7c,
+ adcsra: 0x7a,
+ adcsrb: 0x7b,
+ adcl: 0x78,
+ adch: 0x79,
+ didr0: 0x7e,
+ adcInterrupt: 0x3a,
+ numChannels: 16,
+ muxInputMask: 0x1f,
+ muxChannels: Atmega2560Channels,
+ adcReferences:
+ [
+ AdcReference.AREF,
+ AdcReference.AVCC,
+ AdcReference.Internal1V1,
+ AdcReference.Internal2V56
+ ]
+ );
+
public static readonly AdcMuxInput FallbackMuxInput = new AdcMuxInput(type: AdcMuxInputType.Constant, voltage: 0);
public static readonly AvrAdcConfig AdcConfig = new AvrAdcConfig(
@@ -33,12 +74,13 @@ public class AvrAdc
numChannels: 8,
muxInputMask: 0xf,
muxChannels: Atmega328Channels,
+ // REFS1:0 = 00 AREF, 01 AVCC, 10 reserved, 11 internal 1.1 V (datasheet table 28-3)
adcReferences:
[
- AdcReference.AVCC,
AdcReference.AREF,
- AdcReference.Internal1V1,
- AdcReference.Internal2V56
+ AdcReference.AVCC,
+ null,
+ AdcReference.Internal1V1
]
);
@@ -67,7 +109,7 @@ public class AvrAdc
private int _cachedSampleCycles;
private double _cachedReferenceVoltage;
- private readonly AdcMuxInput[] _muxArray = new AdcMuxInput[32];
+ private readonly AdcMuxInput[] _muxArray = new AdcMuxInput[64];
private int _pendingAdcResult;
private readonly Action _completeAdcAction;
@@ -160,7 +202,7 @@ public AvrAdc(Cpu cpu, AvrAdcConfig config)
);
ChannelValues = new double[config.NumChannels];
- for (var i = 0; i < 32; i++)
+ for (var i = 0; i < _muxArray.Length; i++)
{
_muxArray[i] = config.MuxChannels.GetValueOrDefault(i, FallbackMuxInput);
}
@@ -213,12 +255,14 @@ private void StartConversion()
_converting = true;
_cpu.Mmio.Data[_config.ADCSRA] |= ADSC;
var channel = _cpu.Mmio.Data[_config.ADMUX] & MUX_MASK;
- if ((_cpu.Mmio.Data[_config.ADCSRB] & MUX5) != 0)
+ // MUX5 only selects an input on chips with more than 8 channels (ATmega2560); on the
+ // ATmega328P that ADCSRB bit is reserved.
+ if (_config.NumChannels > 8 && (_cpu.Mmio.Data[_config.ADCSRB] & MUX5) != 0)
{
channel |= 0x20;
}
- OnADCRead(_muxArray[channel & 0x1F]);
+ OnADCRead(_muxArray[channel & 0x3F]);
}
///
diff --git a/src/Avr8Sharp/Core/Peripherals/Gpio.cs b/src/Avr8Sharp/Core/Peripherals/Gpio.cs
index e321329..dc3ec79 100644
--- a/src/Avr8Sharp/Core/Peripherals/Gpio.cs
+++ b/src/Avr8Sharp/Core/Peripherals/Gpio.cs
@@ -144,6 +144,7 @@ public class AvrIoPort
private readonly Cpu _cpu;
private readonly AvrPortConfig _portConfig;
private int _pinValue;
+ private int _driven;
private byte _overrideMask = 0xff;
private byte _overrideValue = 0;
private byte _lastValue = 0;
@@ -332,7 +333,8 @@ public PinState GetPinState (byte index)
///
/// Sets the input value for the given pin. This is the value that
- /// will be returned when reading from the PIN register.
+ /// will be returned when reading from the PIN register. The pin counts as
+ /// externally driven until is called.
///
/// Pin index to set from 0 to 7
/// The value to set
@@ -343,12 +345,27 @@ public void SetPinValue (byte index, bool value)
if (value) {
_pinValue |= bitMask;
}
+ _driven |= bitMask;
+ UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]);
+ }
+
+ ///
+ /// Releases the given pin back to floating, undoing .
+ /// While an input pin floats, the PIN register reads the internal pull-up
+ /// (the PORT bit): 1 with the pull-up enabled, 0 without it.
+ ///
+ /// Pin index to release from 0 to 7
+ public void ReleasePin (byte index)
+ {
+ var bitMask = 1 << index;
+ _driven &= ~bitMask;
UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]);
}
private void UpdatePinRegister (byte ddr)
{
- var newPin = (byte)(((_pinValue & ~ddr) | (_lastValue & ddr)) & 0xff);
+ var pulled = _cpu.Mmio.Data[_portConfig.PORT] & ~_driven;
+ var newPin = (byte)(((((_pinValue & _driven) | pulled) & ~ddr) | (_lastValue & ddr)) & 0xff);
_cpu.Mmio.Data[_portConfig.PIN] = newPin;
if (_lastPin == newPin) return;
for (var index = 0; index < 8; index++)
diff --git a/src/Avr8Sharp/Core/Peripherals/Twi.cs b/src/Avr8Sharp/Core/Peripherals/Twi.cs
index 70ea1ca..d3e1c57 100644
--- a/src/Avr8Sharp/Core/Peripherals/Twi.cs
+++ b/src/Avr8Sharp/Core/Peripherals/Twi.cs
@@ -65,8 +65,40 @@ public class AvrTwi
private bool _busy = false;
+ // Wire timing of the event being processed. The event "starts" when it is dispatched
+ // to the handler; TWINT is raised no earlier than _eventStartCycle + _eventWireCycles.
+ private ulong _eventStartCycle;
+ private ulong _eventWireCycles;
+ // Incremented whenever a deferred completion is scheduled or invalidated (reset), so a
+ // stale clock event can never touch the state of a later event.
+ private int _sequence;
+ private Action? _deferred;
+
+ // Bus time of each event, in SCL periods. START and STOP are one period each (the
+ // setup/hold times of the datasheet SCL timing are about half a period apiece). An
+ // address or data byte is 8 bits plus the ACK/NACK bit, so 9 SCL periods.
+ const int START_SCL_PERIODS = 1;
+ const int STOP_SCL_PERIODS = 1;
+ const int BYTE_SCL_PERIODS = 9;
+
public ITwiEventHandler EventHandler { get; set; }
+ ///
+ /// When true (default), each bus event (START, address, data byte, STOP) takes its wire
+ /// time at before TWINT is raised, as on silicon. The time is
+ /// counted from the moment the event is dispatched to , so a
+ /// handler that completes late (for example from an external circuit solver) is not
+ /// delayed any further once the wire time has already elapsed. Set to false to complete
+ /// every event as soon as the handler reports it.
+ ///
+ public bool EmulateBusTiming { get; set; } = true;
+
+ ///
+ /// CPU cycles of one SCL period: CPU clock / SCL frequency = 16 + 2 * TWBR * prescaler
+ /// (ATmega328P datasheet, "Bit Rate Generator Unit").
+ ///
+ public int SclPeriodCycles => 16 + 2 * _cpu.Mmio.Data[_config.TWBR] * Prescaler;
+
public int Prescaler
{
get
@@ -122,6 +154,15 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz)
UpdateStatus(STATUS_TWI_IDLE);
+ cpu.OnPeripheralReset += () =>
+ {
+ // Cpu.Reset already dropped the pending clock events
+ _sequence++;
+ _deferred = null;
+ _busy = false;
+ _eventWireCycles = 0;
+ };
+
cpu.Mmio.RegisterWrite(_config.TWAR, (value, _, _, _) =>
{
_cpu.Mmio.Data[_config.TWAR] = value;
@@ -142,27 +183,27 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz)
{
if ((value & TWCR_TWSTA) != 0)
{
- _busy = true;
+ BeginEvent(START_SCL_PERIODS);
EventHandler.Start(Status != STATUS_TWI_IDLE);
}
else if ((value & TWCR_TWSTO) != 0)
{
- _busy = true;
+ BeginEvent(STOP_SCL_PERIODS);
EventHandler.Stop();
}
else if (Status == STATUS_START || Status == STATUS_REPEATED_START)
{
- _busy = true;
+ BeginEvent(BYTE_SCL_PERIODS);
EventHandler.ConnectToSlave((byte)(twdrValue >> 1), (twdrValue & 0x1) == 0);
}
else if (Status == STATUS_SLAW_ACK || Status == STATUS_DATA_SENT_ACK)
{
- _busy = true;
+ BeginEvent(BYTE_SCL_PERIODS);
EventHandler.WriteByte(twdrValue);
}
else if (Status == STATUS_SLAR_ACK || Status == STATUS_DATA_RECEIVED_ACK)
{
- _busy = true;
+ BeginEvent(BYTE_SCL_PERIODS);
var ack = (value & TWCR_TWEA) != 0;
EventHandler.ReadByte(ack);
}
@@ -174,17 +215,53 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz)
});
}
+ private void BeginEvent(int sclPeriods)
+ {
+ _busy = true;
+ _eventStartCycle = _cpu.Cycles;
+ _eventWireCycles = EmulateBusTiming ? (ulong)sclPeriods * (ulong)SclPeriodCycles : 0;
+ }
+
+ ///
+ /// Runs now if the wire time of the current event has elapsed
+ /// (or the bus is not busy with a dispatched event), otherwise at the end of the wire time.
+ ///
+ private void Finish(Action apply)
+ {
+ var remaining = _busy ? (long)(_eventStartCycle + _eventWireCycles) - (long)_cpu.Cycles : 0;
+ if (remaining <= 0)
+ {
+ _sequence++;
+ _deferred = null;
+ _busy = false;
+ apply();
+ return;
+ }
+
+ var sequence = ++_sequence;
+ _deferred = apply;
+ _cpu.AddClockEvent(() =>
+ {
+ if (sequence != _sequence || _deferred == null) return;
+ var action = _deferred;
+ _deferred = null;
+ _busy = false;
+ action();
+ }, (int)remaining);
+ }
+
public void CompleteStart()
{
- _busy = false;
- UpdateStatus(Status == STATUS_TWI_IDLE ? STATUS_START : STATUS_REPEATED_START);
+ Finish(() => UpdateStatus(Status == STATUS_TWI_IDLE ? STATUS_START : STATUS_REPEATED_START));
}
public void CompleteStop()
{
- _busy = false;
- _cpu.Mmio.Data[_config.TWCR] &= ~TWCR_TWSTO & 0xff;
- UpdateStatus(STATUS_TWI_IDLE);
+ Finish(() =>
+ {
+ _cpu.Mmio.Data[_config.TWCR] &= ~TWCR_TWSTO & 0xff;
+ UpdateStatus(STATUS_TWI_IDLE);
+ });
}
///
@@ -197,29 +274,32 @@ public void CompleteStop()
public void CompleteConnect(bool ack)
{
- _busy = false;
- if ((_cpu.Mmio.Data[_config.TWDR] & 0x1) != 0)
- {
- UpdateStatus(ack ? STATUS_SLAR_ACK : STATUS_SLAR_NACK);
- }
- else
+ Finish(() =>
{
- UpdateStatus(ack ? STATUS_SLAW_ACK : STATUS_SLAW_NACK);
- }
+ if ((_cpu.Mmio.Data[_config.TWDR] & 0x1) != 0)
+ {
+ UpdateStatus(ack ? STATUS_SLAR_ACK : STATUS_SLAR_NACK);
+ }
+ else
+ {
+ UpdateStatus(ack ? STATUS_SLAW_ACK : STATUS_SLAW_NACK);
+ }
+ });
}
public void CompleteWrite(bool ack)
{
- _busy = false;
- UpdateStatus(ack ? STATUS_DATA_SENT_ACK : STATUS_DATA_SENT_NACK);
+ Finish(() => UpdateStatus(ack ? STATUS_DATA_SENT_ACK : STATUS_DATA_SENT_NACK));
}
public void CompleteRead(byte data)
{
- _busy = false;
- var ack = (_cpu.Mmio.Data[_config.TWCR] & TWCR_TWEA) != 0;
- _cpu.Mmio.Data[_config.TWDR] = data;
- UpdateStatus(ack ? STATUS_DATA_RECEIVED_ACK : STATUS_DATA_RECEIVED_NACK);
+ Finish(() =>
+ {
+ var ack = (_cpu.Mmio.Data[_config.TWCR] & TWCR_TWEA) != 0;
+ _cpu.Mmio.Data[_config.TWDR] = data;
+ UpdateStatus(ack ? STATUS_DATA_RECEIVED_ACK : STATUS_DATA_RECEIVED_NACK);
+ });
}
///
diff --git a/src/Avr8Sharp/Core/Peripherals/Watchdog.cs b/src/Avr8Sharp/Core/Peripherals/Watchdog.cs
index 8ca785a..7d4edcf 100644
--- a/src/Avr8Sharp/Core/Peripherals/Watchdog.cs
+++ b/src/Avr8Sharp/Core/Peripherals/Watchdog.cs
@@ -5,7 +5,10 @@ namespace AVR8Sharp.Core.Peripherals;
public class AvrWatchdog
{
// Register Bits
+ const int MCUSR_PORF = 0x1; // Power-on Reset Flag
+ const int MCUSR_EXTRF = 0x2; // External Reset Flag
const int MCUSR_WDRF = 0x8; // Watchdog System Reset Flag
+ const int MCUSR_FLAGS_MASK = 0x0f; // PORF | EXTRF | BORF | WDRF; bits 7:4 are reserved
const int WDTCSR_WDIF = 0x80; // Watchdog Interrupt Flag
const int WDTCSR_WDIE = 0x40; // Watchdog Interrupt Enable
@@ -37,6 +40,7 @@ public class AvrWatchdog
private ulong _watchdogTimeout = 0;
private bool _enabledValue = false;
private bool _scheduled = false;
+ private bool _watchdogReset = false;
private readonly AvrInterruptConfig _watchdog;
@@ -74,6 +78,24 @@ public AvrWatchdog(Cpu cpu, AvrWatchdogConfig config, AvrClock clock)
_cpu.OnWatchdogReset = ResetWatchdog;
+ // MCUSR is owned here because the watchdog is the peripheral that reports reset causes.
+ // Constructing the peripheral models the power-on of the chip, so PORF starts set.
+ PowerOnReset();
+
+ // A flag is cleared by writing a logic zero to it; writing a one never sets it.
+ _cpu.Mmio.RegisterWrite(config.MCUSR, (value, oldValue, _, _) =>
+ {
+ _cpu.Mmio.Data[config.MCUSR] = (byte)(oldValue & value & MCUSR_FLAGS_MASK);
+ return true;
+ });
+
+ // Any CPU reset that is not the watchdog's own is seen by the chip as an external
+ // reset (RESET pin), which sets EXTRF. MCUSR itself survives the reset, as on silicon.
+ _cpu.OnPeripheralReset += () =>
+ {
+ if (!_watchdogReset) _cpu.Mmio.Data[config.MCUSR] |= MCUSR_EXTRF;
+ };
+
_cpu.Mmio.RegisterWrite(config.WDTCSR, (value, oldValue, _, _) =>
{
if ((value & WDTCSR_WDCE) != 0 && (value & WDTCSR_WDE) != 0)
@@ -110,6 +132,15 @@ public AvrWatchdog(Cpu cpu, AvrWatchdogConfig config, AvrClock clock)
});
}
+ ///
+ /// Puts MCUSR in its power-on state: only PORF set. Does not reset the CPU. The constructor
+ /// already does this; call it again to rewind a restored machine to a fresh power-on.
+ ///
+ public void PowerOnReset()
+ {
+ _cpu.Mmio.Data[_config.MCUSR] = MCUSR_PORF;
+ }
+
private void ResetWatchdog()
{
var cycles = (int)Math.Floor((_clock.Frequency / _clockFrequency) * Prescaler);
@@ -135,7 +166,9 @@ private void CheckWatchdog()
}
else
{
- _cpu.Reset();
+ _watchdogReset = true;
+ try { _cpu.Reset(); }
+ finally { _watchdogReset = false; }
_scheduled = false;
_cpu.Mmio.Data[_config.MCUSR] |= MCUSR_WDRF;
return;
diff --git a/tests/Avr8Sharp.TestKit.Samples/ArduinoUnoSamples.cs b/tests/Avr8Sharp.TestKit.Samples/ArduinoUnoSamples.cs
index 9dfbf62..1de2e85 100644
--- a/tests/Avr8Sharp.TestKit.Samples/ArduinoUnoSamples.cs
+++ b/tests/Avr8Sharp.TestKit.Samples/ArduinoUnoSamples.cs
@@ -162,4 +162,33 @@ public void Serial_ShouldPrintStartupMessage()
uno.Serial.Should().Contain("Hello from Uno!");
uno.Serial.Should().ContainLine("Hello from Uno!");
}
+
+ ///
+ /// Regression for issue #25: RunUntilSerial polls its predicate once per
+ /// instruction and used to rebuild the whole captured text each time, so long runs
+ /// slowed down in proportion to the output. The predicate must now run only when
+ /// the captured output changes.
+ ///
+ [Test]
+ public void RunUntilSerial_ShouldEvaluatePredicateOnlyWhenOutputChanges()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(@"
+ ldi r16, 8
+ sts 0xc1, r16 ; UCSR0B = TXEN0
+ ldi r21, 0x78
+ sts 0xc6, r21 ; UDR0 = 'x'
+ loop:
+ inc r17
+ rjmp loop
+ ");
+
+ var calls = 0;
+ Assert.Throws(() => uno.RunUntilSerial(uno.Serial, t => { calls++; return t == "never"; }, 5));
+
+ // 5 ms is 80000 cycles, about 40000 instructions. One evaluation per distinct text
+ // (empty, then "x") is enough.
+ Assert.That(uno.Serial.ByteCount, Is.EqualTo(1));
+ Assert.That(calls, Is.LessThanOrEqualTo(2));
+ }
}
diff --git a/tests/Avr8Sharp.TestKit.Samples/BoardPeripheralsTests.cs b/tests/Avr8Sharp.TestKit.Samples/BoardPeripheralsTests.cs
new file mode 100644
index 0000000..353c519
--- /dev/null
+++ b/tests/Avr8Sharp.TestKit.Samples/BoardPeripheralsTests.cs
@@ -0,0 +1,350 @@
+using Avr8Sharp.TestKit;
+using Avr8Sharp.TestKit.Boards;
+using AVR8Sharp.Core.Peripherals;
+
+namespace Avr8Sharp.TestKit.Samples;
+
+///
+/// The Uno and Mega presets mount SPI, TWI, ADC and the watchdog, with scripted bus slaves so
+/// transfers complete, and MCUSR reports PORF after power-on.
+///
+[TestFixture]
+public class BoardPeripheralsTests
+{
+ private const int McusrAddress = 0x54;
+ private const byte Porf = 0x01;
+ private const byte Extrf = 0x02;
+ private const byte Wdrf = 0x08;
+
+ // SPI master: send 0xAA, wait for SPIF, read the byte clocked back into r18.
+ private const string SpiTransfer = @"
+ ldi r16, 0x50 ; SPE | MSTR
+ out 0x2C, r16 ; SPCR
+ ldi r16, 0xAA
+ out 0x2E, r16 ; SPDR
+ wait:
+ in r17, 0x2D ; SPSR
+ sbrs r17, 7 ; SPIF
+ rjmp wait
+ in r18, 0x2E ; SPDR
+ break
+ ";
+
+ // TWI master: START, then SLA+W for address 0x50; r18 = TWSR status after the address phase.
+ private const string TwiAddress = @"
+ ldi r16, 0xA4 ; TWINT | TWSTA | TWEN
+ sts 0x00BC, r16 ; TWCR
+ w1:
+ lds r17, 0x00BC
+ sbrs r17, 7
+ rjmp w1
+ ldi r16, 0xA0 ; 0x50 << 1 | W
+ sts 0x00BB, r16 ; TWDR
+ ldi r16, 0x84 ; TWINT | TWEN
+ sts 0x00BC, r16
+ w2:
+ lds r17, 0x00BC
+ sbrs r17, 7
+ rjmp w2
+ lds r18, 0x00B9 ; TWSR
+ andi r18, 0xF8
+ break
+ ";
+
+ // ADC: single conversion on ADC0 (AVCC reference), result in r18 (low) / r19 (high).
+ private const string AdcConvert = @"
+ ldi r16, 0x40 ; REFS0, MUX = 0
+ sts 0x007C, r16 ; ADMUX
+ ldi r16, 0xC7 ; ADEN | ADSC | prescaler 128
+ sts 0x007A, r16 ; ADCSRA
+ wait:
+ lds r17, 0x007A
+ sbrc r17, 6 ; ADSC clears when done
+ rjmp wait
+ lds r18, 0x0078 ; ADCL
+ lds r19, 0x0079 ; ADCH
+ break
+ ";
+
+ // Watchdog in reset mode with the shortest (16 ms) timeout, then spin.
+ private const string WatchdogArm = @"
+ ldi r16, 0x18 ; WDCE | WDE
+ sts 0x0060, r16 ; WDTCSR
+ ldi r16, 0x08 ; WDE, WDP = 0
+ sts 0x0060, r16
+ spin:
+ rjmp spin
+ ";
+
+ // ── Uno ───────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void Uno_Spi_TransferCompletesWithIdleMisoByDefault()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(SpiTransfer);
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18), Is.EqualTo(0xFF));
+ Assert.That(uno.SpiBus.Mosi, Is.EqualTo(new byte[] { 0xAA }));
+ }
+
+ [Test]
+ public void Uno_Spi_ReturnsQueuedResponse()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(SpiTransfer);
+ uno.SpiBus.Responses.Enqueue(0x42);
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18), Is.EqualTo(0x42));
+ }
+
+ [Test]
+ public void Uno_Twi_NoSlaveNacksTheAddress()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(TwiAddress);
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18), Is.EqualTo(0x20), "TW_MT_SLA_NACK");
+ }
+
+ [Test]
+ public void Uno_Twi_ConfiguredSlaveAcks()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(TwiAddress);
+ uno.TwiBus.Addresses.Add(0x50);
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18), Is.EqualTo(0x18), "TW_MT_SLA_ACK");
+ }
+
+ [Test]
+ public void Uno_Adc_ConversionFinishesAtZeroVoltsByDefault()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(AdcConvert);
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18) | (uno.Cpu.ReadData(19) << 8), Is.Zero);
+ }
+
+ [Test]
+ public void Uno_Adc_ReadsInjectedVoltage()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(AdcConvert);
+ uno.Adc.ChannelValues[0] = 2.5; // half of AVCC (5 V) -> 512
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(18) | (uno.Cpu.ReadData(19) << 8), Is.EqualTo(512));
+ }
+
+ [Test]
+ public void Uno_Watchdog_ResetsTheChipAndSetsWdrf()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(WatchdogArm);
+ uno.RunMilliseconds(40);
+
+ Assert.That(uno.Cpu.ReadData(McusrAddress) & Wdrf, Is.EqualTo(Wdrf));
+ }
+
+ // ── MCUSR ─────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void Uno_Mcusr_IsPorfAfterCreation()
+ {
+ var uno = new ArduinoUnoSimulation();
+
+ Assert.That(uno.Cpu.ReadData(McusrAddress), Is.EqualTo(Porf));
+ }
+
+ [Test]
+ public void Mcusr_WritingZeroClearsAndWritingOneNeverSets()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(@"
+ ldi r16, 0xFF
+ out 0x34, r16 ; MCUSR: writing ones leaves PORF as it was
+ in r17, 0x34
+ clr r16
+ out 0x34, r16 ; writing a zero clears the flag
+ in r18, 0x34
+ ldi r16, 0xFF
+ out 0x34, r16 ; writing ones cannot set it again
+ in r19, 0x34
+ break
+ ");
+ uno.RunToBreak();
+
+ Assert.That(uno.Cpu.ReadData(17), Is.EqualTo(Porf));
+ Assert.That(uno.Cpu.ReadData(18), Is.Zero);
+ Assert.That(uno.Cpu.ReadData(19), Is.Zero);
+ }
+
+ [Test]
+ public void Mcusr_ExternalResetSetsExtrfAndKeepsPorf()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.Reset();
+
+ Assert.That(uno.Cpu.ReadData(McusrAddress), Is.EqualTo(Porf | Extrf));
+ }
+
+ [Test]
+ public void Mcusr_WatchdogResetSetsWdrfNotExtrf()
+ {
+ var uno = new ArduinoUnoSimulation();
+ uno.WithAsm(WatchdogArm);
+ uno.RunMilliseconds(40);
+
+ Assert.That(uno.Cpu.ReadData(McusrAddress), Is.EqualTo(Porf | Wdrf));
+ }
+
+ [Test]
+ public void AvrRunnerMachineWithWatchdogGetsPorfOnConstruction()
+ {
+ var runner = new AVR8Sharp.Core.Utils.AvrRunner(new byte[0x8000], 2048);
+ var clock = new AvrClock(runner.Cpu, 16_000_000, AvrClock.ClockConfig);
+ _ = new AvrWatchdog(runner.Cpu, AvrWatchdog.WatchdogConfig, clock);
+
+ Assert.That(runner.Cpu.ReadData(McusrAddress), Is.EqualTo(Porf));
+ }
+
+ // ── Mega ──────────────────────────────────────────────────────────────────
+
+ [Test]
+ public void Mega_Mcusr_IsPorfAfterCreation()
+ {
+ Assert.That(new ArduinoMegaSimulation().Cpu.ReadData(McusrAddress), Is.EqualTo(Porf));
+ }
+
+ [Test]
+ public void Mega_Spi_And_Twi_Complete()
+ {
+ var spi = new ArduinoMegaSimulation();
+ spi.WithAsm(SpiTransfer);
+ spi.SpiBus.Responses.Enqueue(0x5A);
+ spi.RunToBreak();
+ Assert.That(spi.Cpu.ReadData(18), Is.EqualTo(0x5A));
+
+ var twi = new ArduinoMegaSimulation();
+ twi.WithAsm(TwiAddress);
+ twi.RunToBreak();
+ Assert.That(twi.Cpu.ReadData(18), Is.EqualTo(0x20));
+ }
+
+ [Test]
+ public void Mega_Adc_ReadsHighChannelThroughMux5()
+ {
+ var mega = new ArduinoMegaSimulation();
+ mega.WithAsm(@"
+ ldi r16, 0x08 ; MUX5
+ sts 0x007B, r16 ; ADCSRB
+ ldi r16, 0x41 ; REFS0, MUX = 1 -> ADC9
+ sts 0x007C, r16 ; ADMUX
+ ldi r16, 0xC7
+ sts 0x007A, r16 ; ADCSRA
+ wait:
+ lds r17, 0x007A
+ sbrc r17, 6
+ rjmp wait
+ lds r18, 0x0078
+ lds r19, 0x0079
+ break
+ ");
+ mega.Adc.ChannelValues[9] = 2.5;
+ mega.Adc.ChannelValues[1] = 5.0; // must not be picked: MUX5 selects ADC9, not ADC1
+ mega.RunToBreak();
+
+ Assert.That(mega.Cpu.ReadData(18) | (mega.Cpu.ReadData(19) << 8), Is.EqualTo(512));
+ }
+
+ [Test]
+ public void Mega_Watchdog_SetsWdrf()
+ {
+ var mega = new ArduinoMegaSimulation();
+ mega.WithAsm(WatchdogArm);
+ mega.RunMilliseconds(40);
+
+ Assert.That(mega.Cpu.ReadData(McusrAddress) & Wdrf, Is.EqualTo(Wdrf));
+ }
+
+ // ── Strict mode (unmounted ports) ─────────────────────────────────────────
+
+ private const string ReadPind = "in r16, 0x09\nbreak\n";
+
+ [Test]
+ public void Strict_Off_ReadingUnmountedPinReadsZero()
+ {
+ var sim = AvrTestSimulation.Create().WithAsm(ReadPind);
+
+ Assert.DoesNotThrow(() => sim.RunToBreak());
+ Assert.That(sim.Cpu.ReadData(16), Is.Zero);
+ }
+
+ [Test]
+ public void Strict_On_ReadingUnmountedPinThrowsNamingRegisterAndAddress()
+ {
+ var sim = AvrTestSimulation.Create().WithStrict().WithAsm(ReadPind);
+
+ var ex = Assert.Throws(() => sim.RunToBreak())!;
+ Assert.That(ex.Message, Does.Contain("PIND").And.Contain("0x29").And.Contain("port D"));
+ Assert.That(ex.Address, Is.EqualTo(0x29));
+ Assert.That(ex.IsWrite, Is.False);
+ }
+
+ [Test]
+ public void Strict_On_WritingUnmountedDdrThrows()
+ {
+ var sim = AvrTestSimulation.Create().WithStrict().WithAsm("ldi r16, 1\nout 0x0A, r16\nbreak\n");
+
+ var ex = Assert.Throws(() => sim.RunToBreak())!;
+ Assert.That(ex.Message, Does.Contain("DDRD").And.Contain("written"));
+ }
+
+ [Test]
+ public void Strict_On_MountedPortDoesNotThrow_WhetherMountedBeforeOrAfter()
+ {
+ var before = AvrTestSimulation.Create()
+ .AddGpio(AvrIoPort.PortDConfig, out _).WithStrict().WithAsm(ReadPind);
+ Assert.DoesNotThrow(() => before.RunToBreak());
+
+ var after = AvrTestSimulation.Create().WithStrict();
+ after.AddGpio(AvrIoPort.PortDConfig, out var portD).WithAsm(ReadPind);
+ Assert.DoesNotThrow(() => after.RunToBreak());
+ Assert.That(portD, Is.Not.Null);
+ }
+
+ [Test]
+ public void Strict_Warn_RecordsOneMessagePerRegisterAndKeepsRunning()
+ {
+ var sim = AvrTestSimulation.Create();
+ sim.UnmountedAccess = UnmountedAccessMode.Warn;
+ sim.WithAsm("in r16, 0x09\nin r16, 0x09\nbreak\n");
+
+ Assert.DoesNotThrow(() => sim.RunToBreak());
+ Assert.That(sim.Warnings, Has.Count.EqualTo(1));
+ Assert.That(sim.Warnings[0], Does.Contain("PIND"));
+ }
+
+ [Test]
+ public void Strict_OnUno_AllChipPortsAreMounted_NothingThrows()
+ {
+ var uno = new ArduinoUnoSimulation().WithStrict().WithAsm(
+ "in r16, 0x03\nin r16, 0x06\nin r16, 0x09\nldi r16, 1\nout 0x04, r16\nbreak\n");
+
+ Assert.DoesNotThrow(() => uno.RunToBreak());
+ }
+
+ [Test]
+ public void Strict_OnAttiny85_DoesNotFlagItsOwnLowIoRegisters()
+ {
+ var tiny = new ATtiny85Simulation().WithStrict().WithAsm("in r16, 0x06\nbreak\n"); // ADCSRA lives at 0x26
+
+ Assert.DoesNotThrow(() => tiny.RunToBreak());
+ }
+}
diff --git a/tests/Avr8Sharp.Tests/AdcTests.cs b/tests/Avr8Sharp.Tests/AdcTests.cs
index ce18b6a..ec0d9fc 100644
--- a/tests/Avr8Sharp.Tests/AdcTests.cs
+++ b/tests/Avr8Sharp.Tests/AdcTests.cs
@@ -259,4 +259,16 @@ public void TemperatureSensor_Configurable ()
Assert.That (result, Is.EqualTo (81),
"ADC result must reflect the configured TemperatureVoltage");
}
+
+ [Test (Description = "ATmega328P REFS1:0 selects AREF, AVCC, reserved, internal 1.1 V (datasheet table 28-3)")]
+ public void ReferenceSelection_FollowsDatasheetOrder ()
+ {
+ Cpu.Mmio.Data[ADMUX] = 0x00;
+ Assert.That (_adc.ReferenceVoltageType, Is.EqualTo (AdcReference.AREF), "REFS = 00");
+ Cpu.Mmio.Data[ADMUX] = REFS0;
+ Assert.That (_adc.ReferenceVoltageType, Is.EqualTo (AdcReference.AVCC), "REFS = 01");
+ Cpu.Mmio.Data[ADMUX] = 0xc0;
+ Assert.That (_adc.ReferenceVoltageType, Is.EqualTo (AdcReference.Internal1V1), "REFS = 11 (analogReference(INTERNAL))");
+ Assert.That (_adc.ReferenceVoltage, Is.EqualTo (1.1));
+ }
}
diff --git a/tests/Avr8Sharp.Tests/CycleBoundaryTests.cs b/tests/Avr8Sharp.Tests/CycleBoundaryTests.cs
new file mode 100644
index 0000000..ef055d3
--- /dev/null
+++ b/tests/Avr8Sharp.Tests/CycleBoundaryTests.cs
@@ -0,0 +1,95 @@
+using AVR8Sharp.Core.Peripherals;
+using Avr8Sharp.Tests.Utils;
+
+namespace Avr8Sharp.Tests;
+
+///
+/// Regression tests for issue #25 (slowdown past ~130 s at 16 MHz). The suspect was a
+/// 32-bit wrap of the cycle counter at 2^31 (~134 s) or 2^32 (~268 s). These tests park
+/// Cpu.Cycles just below each boundary and check that clock events and the timer
+/// keep firing at the right cycle and rate while crossing it.
+///
+[TestFixture]
+public class CycleBoundary : AvrTestBase
+{
+ const int TCCR0B = 0x45;
+ const int TCNT0 = 0x46;
+ const int CS01 = 2;
+ const int CS00 = 1;
+
+ private AvrTimer _timer0;
+
+ protected override void SetupPeripherals()
+ {
+ _timer0 = new AvrTimer (Cpu, AvrTimer.Timer0Config);
+ }
+
+ private static readonly ulong[] Boundaries = [1UL << 31, 1UL << 32];
+
+ [Test (Description = "A clock event scheduled across a 32-bit boundary fires exactly at its cycle")]
+ public void ClockEventFiresAtExactCycleAcrossBoundary ([ValueSource (nameof (Boundaries))] ulong boundary)
+ {
+ Cpu.Cycles = boundary - 5;
+ ulong firedAt = 0;
+ var fired = 0;
+ Cpu.AddClockEvent (() => { fired++; firedAt = Cpu.Cycles; }, 10);
+
+ for (var i = 0; i < 20; i++) {
+ Cpu.Cycles++;
+ Cpu.Tick ();
+ }
+
+ Assert.That (fired, Is.EqualTo (1));
+ Assert.That (firedAt, Is.EqualTo (boundary + 5));
+ }
+
+ [Test (Description = "Events keep their order when the queue spans a 32-bit boundary")]
+ public void ClockEventsKeepOrderAcrossBoundary ([ValueSource (nameof (Boundaries))] ulong boundary)
+ {
+ Cpu.Cycles = boundary - 3;
+ var order = new List ();
+ Cpu.AddClockEvent (() => order.Add (3), 8);
+ Cpu.AddClockEvent (() => order.Add (1), 2);
+ Cpu.AddClockEvent (() => order.Add (2), 4);
+
+ for (var i = 0; i < 10; i++) {
+ Cpu.Cycles++;
+ Cpu.Tick ();
+ }
+
+ Assert.That (order, Is.EqualTo (new[] { 1, 2, 3 }));
+ }
+
+ [Test (Description = "Timer0 at prescaler 64 counts at the same rate across a 32-bit boundary as from zero")]
+ public void TimerRateIsStableAcrossBoundary ([ValueSource (nameof (Boundaries))] ulong boundary)
+ {
+ var baseline = RunTimer (1000);
+ var crossing = RunTimer (boundary - 1000);
+
+ Assert.That (baseline.Overflows, Is.EqualTo (3)); // 1000 timer ticks, 256 counts per overflow
+ Assert.That (crossing, Is.EqualTo (baseline));
+ }
+
+ // Runs 1000 timer ticks (prescaler 64) starting with the cycle counter at startCycles
+ private (int Overflows, byte Tcnt) RunTimer (ulong startCycles)
+ {
+ Setup ();
+ Cpu.Cycles = startCycles;
+ Cpu.WriteData (TCCR0B, CS01 | CS00);
+ Cpu.Cycles++;
+ Cpu.Tick ();
+
+ var overflows = 0;
+ var lastTcnt = Cpu.ReadData (TCNT0);
+ var begin = Cpu.Cycles;
+ while (Cpu.Cycles - begin < 64 * 1000) {
+ Cpu.Cycles++;
+ Cpu.Tick ();
+ var tcnt = Cpu.ReadData (TCNT0);
+ if (tcnt < lastTcnt) overflows++;
+ lastTcnt = tcnt;
+ }
+
+ return (overflows, lastTcnt);
+ }
+}
diff --git a/tests/Avr8Sharp.Tests/GpioTests.cs b/tests/Avr8Sharp.Tests/GpioTests.cs
index 9abc7ef..5f2c64b 100644
--- a/tests/Avr8Sharp.Tests/GpioTests.cs
+++ b/tests/Avr8Sharp.Tests/GpioTests.cs
@@ -64,7 +64,8 @@ public void PortWrite ()
Cpu.WriteData (PORTB, 0x55);
- Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x5));
+ // PINB.7:4 are floating inputs with the pull-up on, so they read the PORT bits (0x50)
+ Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x55));
}
[Test (Description = "Should invoke the listeners when DDR changes (issue #28)")]
@@ -109,7 +110,8 @@ public void PinToggle ()
Assert.Multiple(() =>
{
- Assert.That(Cpu.Mmio.Data[PINB], Is.EqualTo(0x4));
+ // PINB.7:4 are floating inputs with the pull-up on, so they read the PORT bits (0x50)
+ Assert.That(Cpu.Mmio.Data[PINB], Is.EqualTo(0x54));
Assert.That(calledCorrectly, Is.True);
});
}
@@ -304,6 +306,75 @@ public void SetPinValueInput ()
}
}
+ [TestFixture]
+ public class PinPullUp : AvrTestBase
+ {
+ private AvrIoPort portB;
+
+ protected override void SetupPeripherals()
+ {
+ portB = new AvrIoPort (Cpu, AvrIoPort.PortBConfig);
+ }
+
+ [Test (Description = "A floating input with the pull-up enabled should read 1")]
+ public void FloatingPullUpReadsHigh ()
+ {
+ Cpu.WriteData (DDRB, 0);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0));
+
+ Cpu.WriteData (PORTB, 1 << PB4);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (1 << PB4));
+
+ Cpu.WriteData (PORTB, 0);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0));
+ }
+
+ [Test (Description = "A driven input should keep reading the injected level whatever PORT says")]
+ public void DrivenPinIgnoresPullUp ()
+ {
+ Cpu.WriteData (DDRB, 0);
+ Cpu.WriteData (PORTB, 1 << PB4);
+ portB.SetPinValue (PB4, false);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0));
+
+ portB.ReleasePin (PB4);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (1 << PB4));
+ }
+
+ [Test (Description = "Releasing a pin without the pull-up should read 0")]
+ public void ReleasedPinWithoutPullUpReadsLow ()
+ {
+ Cpu.WriteData (DDRB, 0);
+ portB.SetPinValue (PB4, true);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (1 << PB4));
+
+ portB.ReleasePin (PB4);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0));
+ }
+
+ [Test (Description = "Switching a floating pulled-up pin from output low to input should read 1")]
+ public void DdrChangeToInputReadsPullUp ()
+ {
+ Cpu.WriteData (PORTB, 1 << PB4);
+ Cpu.WriteData (DDRB, 1 << PB4);
+ Cpu.WriteData (PORTB, 0);
+ Cpu.WriteData (PORTB, 1 << PB4);
+ Cpu.WriteData (DDRB, 0);
+ Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (1 << PB4));
+ }
+
+ [Test (Description = "Enabling the pull-up on a floating pin should raise a pin change interrupt")]
+ public void EnablingPullUpRaisesPinChange ()
+ {
+ Cpu.WriteData (PCICR, 1 << PCIE0);
+ Cpu.WriteData (PCMSK0, 1 << PB4);
+ Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (0));
+
+ Cpu.WriteData (PORTB, 1 << PB4);
+ Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (1 << PCIE0));
+ }
+ }
+
[TestFixture]
public class ExternalInterrupts : AvrTestBase
{
diff --git a/tests/Avr8Sharp.Tests/LargeFlashControlFlowTests.cs b/tests/Avr8Sharp.Tests/LargeFlashControlFlowTests.cs
new file mode 100644
index 0000000..185e1b0
--- /dev/null
+++ b/tests/Avr8Sharp.Tests/LargeFlashControlFlowTests.cs
@@ -0,0 +1,135 @@
+using AvrCpu = AVR8Sharp.Core.Cpu;
+using AVR8Sharp.Core.Decoders;
+
+namespace Avr8Sharp.Tests;
+
+///
+/// RCALL/RET and friends on parts whose program counter is wider than 16 bits
+/// (ATmega2560-sized flash), where a truncated displacement leaves a phantom bit 16 in Pc.
+///
+[TestFixture]
+public class LargeFlashControlFlow
+{
+ private const ushort RET = 0x9508;
+ private const int Words256K = 0x20000; // 128K words = 256 KB, ATmega2560
+ private const int Words130K = 0x10100; // just over 64K words, still a 22-bit PC
+
+ private static ushort Rcall (int k) => (ushort)(0xd000 | (k & 0xfff));
+ private static ushort Rjmp (int k) => (ushort)(0xc000 | (k & 0xfff));
+
+ private static void Step (AvrCpu cpu, bool lut)
+ {
+ if (lut) new LutDecoder ().Decode (cpu);
+ else new SwitchDecoder ().Decode (cpu);
+ }
+
+ private static AvrCpu NewCpu (int words)
+ {
+ var cpu = new AvrCpu (new ushort[words]);
+ Assert.That (cpu.Pc22Bits, Is.True);
+ return cpu;
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void BackwardRcallThenRetReturnsToCaller (bool lut)
+ {
+ var cpu = NewCpu (Words256K);
+ cpu.ProgramMemory[0x20] = RET;
+ cpu.ProgramMemory[0x30] = Rcall (-0x11); // target 0x30 + 1 - 0x11 = 0x20
+ cpu.Pc = 0x30;
+ var sp = cpu.Sp;
+
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x20u));
+ Assert.That (cpu.Sp, Is.EqualTo ((ushort)(sp - 3)));
+
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x31u));
+ Assert.That (cpu.Sp, Is.EqualTo (sp));
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void ForwardRcallThenRetReturnsToCaller (bool lut)
+ {
+ var cpu = NewCpu (Words256K);
+ cpu.ProgramMemory[0x30] = Rcall (0x0f); // target 0x40
+ cpu.ProgramMemory[0x40] = RET;
+ cpu.Pc = 0x30;
+
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x40u));
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x31u));
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void BackwardRcallAboveTheFirst64KWords (bool lut)
+ {
+ var cpu = NewCpu (Words256K);
+ cpu.ProgramMemory[0x12000] = RET;
+ cpu.ProgramMemory[0x12010] = Rcall (-0x11);
+ cpu.Pc = 0x12010;
+
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x12000u));
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x12011u));
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void RcallNearTheTopOf128KWordFlash (bool lut)
+ {
+ var cpu = NewCpu (Words256K);
+ cpu.ProgramMemory[0x1ffe0] = RET;
+ cpu.ProgramMemory[0x1fff0] = Rcall (-0x11); // backward to 0x1ffe0
+ cpu.ProgramMemory[0x1fff2] = Rcall (0x03); // forward to 0x1fff6
+ cpu.ProgramMemory[0x1fff6] = RET;
+
+ cpu.Pc = 0x1fff0;
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x1ffe0u));
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x1fff1u));
+
+ cpu.Pc = 0x1fff2;
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x1fff6u));
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x1fff3u));
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void RetToAReturnAddressWithZeroLow16Bits (bool lut)
+ {
+ var cpu = NewCpu (Words130K);
+ cpu.ProgramMemory[0xffff] = Rcall (0x04); // return address 0x10000, target 0x10004
+ cpu.ProgramMemory[0x10004] = RET;
+ cpu.Pc = 0xffff;
+
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x10004u));
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x10000u));
+ }
+
+ [TestCase (false)]
+ [TestCase (true)]
+ public void BackwardRjmpAndBranchStayInRange (bool lut)
+ {
+ var cpu = NewCpu (Words256K);
+ cpu.ProgramMemory[0x14000] = Rjmp (-0x11);
+ cpu.Pc = 0x14000;
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x13ff0u));
+
+ cpu.ProgramMemory[0x14100] = 0xf7e9; // brne .-6 (Z clear)
+ cpu.Pc = 0x14100;
+ Step (cpu, lut);
+ Assert.That (cpu.Pc, Is.EqualTo (0x140feu));
+ }
+}
diff --git a/tests/Avr8Sharp.Tests/TwiTests.cs b/tests/Avr8Sharp.Tests/TwiTests.cs
index f34d98f..a0f723b 100644
--- a/tests/Avr8Sharp.Tests/TwiTests.cs
+++ b/tests/Avr8Sharp.Tests/TwiTests.cs
@@ -110,6 +110,13 @@ protected override void SetupPeripherals()
_twi.EventHandler = _mockTwiEventHandler.Object;
}
+ // Lets the wire time of the event in flight elapse (TWBR=0 makes a byte 144 cycles)
+ private void AdvanceBus()
+ {
+ Cpu.Cycles += 200;
+ Cpu.Tick();
+ }
+
[Test]
public void ShouldCallStartEventWhenTwstaIsSet()
{
@@ -131,6 +138,7 @@ public void ShouldConnectSuccessfullyInCaseOfRepeatedStart()
Cpu.Tick();
_mockTwiEventHandler.Verify(t => t.Start(false), Times.Once);
+ AdvanceBus();
// Repeated start
Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
@@ -138,6 +146,7 @@ public void ShouldConnectSuccessfullyInCaseOfRepeatedStart()
Cpu.Tick();
_mockTwiEventHandler.Verify(t => t.Start(true), Times.Once);
+ AdvanceBus();
// Now try to connect...
Cpu.WriteData(TWDR, 0x80); // Address 0x40, write mode
@@ -155,6 +164,7 @@ public void ShouldSendStopConditionWhenTwstoIsSet()
Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
Cpu.Cycles++;
Cpu.Tick();
+ AdvanceBus();
// Send Stop
Cpu.WriteData(TWCR, TWINT | TWSTO | TWEN);
@@ -293,28 +303,28 @@ breq wait_for_twint
runner.RunInstructions(4);
_mockTwiEventHandler.Verify(t => t.Start(false), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 2: wait for slave connect in write mode
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.ConnectToSlave(0x22, true), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 3: wait for first data byte
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.WriteByte(0x55), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 4: wait for stop condition
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.Stop(), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 5: wait for the assembly code to indicate success by settings r17 to 0x42
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
Assert.That(Cpu.ReadData(R17), Is.EqualTo(0x42));
}
@@ -464,38 +474,215 @@ breq wait_for_twint
runner.RunInstructions(4);
_mockTwiEventHandler.Verify(t => t.Start(false), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 2: wait for slave connect in read mode
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.ConnectToSlave(0x50, false), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 3: send the first byte to the master, expect ack
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.ReadByte(true), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 4: send the second byte to the master, expect nack
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.ReadByte(false), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 5: wait for stop condition
- runner.RunInstructions(24);
+ runner.RunInstructions(100);
_mockTwiEventHandler.Verify(t => t.Stop(), Times.Once);
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
// Step 6: wait for the assembly code to indicate success by settings r17 to 0x42
- runner.RunInstructions(16);
+ runner.RunInstructions(100);
Assert.That(Cpu.Mmio.Data[R17], Is.EqualTo(0x42));
}
}
+ [TestFixture]
+ public class BusTiming : AvrTestBase
+ {
+ private AvrTwi _twi;
+ private DeferredHandler _handler;
+
+ // Handler that only records the events; the test completes them when it wants.
+ private class DeferredHandler(Func twi) : ITwiEventHandler
+ {
+ public int Starts;
+ public bool CompleteStartInline = true;
+ public void Start(bool repeated) { Starts++; if (CompleteStartInline) twi().CompleteStart(); }
+ public void Stop() { }
+ public void ConnectToSlave(byte address, bool write) { }
+ public void WriteByte(byte data) { }
+ public void ReadByte(bool ack) { }
+ }
+
+ private class AckingHandler(Func twi) : ITwiEventHandler
+ {
+ public void Start(bool repeated) => twi().CompleteStart();
+ public void Stop() => twi().CompleteStop();
+ public void ConnectToSlave(byte address, bool write) => twi().CompleteConnect(true);
+ public void WriteByte(byte data) => twi().CompleteWrite(true);
+ public void ReadByte(bool ack) => twi().CompleteRead(0x66);
+ }
+
+ protected override void SetupPeripherals()
+ {
+ _twi = new AvrTwi(Cpu, AvrTwi.TwiConfig, FREQ_16MHZ);
+ _handler = new DeferredHandler(() => _twi);
+ _twi.EventHandler = _handler;
+ }
+
+ private bool TwintSet => (Cpu.ReadData(TWCR) & TWINT) != 0;
+
+ private void Run(int cycles)
+ {
+ for (var i = 0; i < cycles; i++)
+ {
+ Cpu.Cycles++;
+ Cpu.Tick();
+ }
+ }
+
+ // Cycles from the moment the TWCR write happens until TWINT reads back as set
+ private int CyclesUntilTwint(int twcr)
+ {
+ var start = Cpu.Cycles;
+ Cpu.WriteData(TWCR, (byte)twcr);
+ var guard = 0;
+ while (!TwintSet && guard++ < 1_000_000)
+ {
+ Cpu.Cycles++;
+ Cpu.Tick();
+ }
+ return (int)(Cpu.Cycles - start);
+ }
+
+ [TestCase(72, 0, 161)] // 100 kHz: period 160 cycles + 1 dispatch cycle
+ [TestCase(12, 0, 41)] // 400 kHz: period 40 cycles + 1
+ [TestCase(12, 1, 113)] // TWPS=1 (x4): period 16 + 2*12*4 = 112 cycles + 1
+ [TestCase(2, 3, 273)] // TWPS=3 (x64): period 16 + 2*2*64 = 272 cycles + 1
+ public void StartTakesOneSclPeriod(int twbr, int twps, int expected)
+ {
+ Cpu.WriteData(TWBR, (byte)twbr);
+ Cpu.WriteData(TWSR, (byte)(0xf8 | twps));
+
+ Assert.That(CyclesUntilTwint(TWINT | TWSTA | TWEN), Is.EqualTo(expected));
+ Assert.That(_twi.Status, Is.EqualTo(0x08));
+ }
+
+ [Test]
+ public void ByteWriteTakesNineSclPeriods()
+ {
+ Cpu.WriteData(TWBR, 72);
+ _twi.EventHandler = new AckingHandler(() => _twi);
+ CyclesUntilTwint(TWINT | TWSTA | TWEN);
+
+ Cpu.WriteData(TWDR, 0x44);
+ Assert.That(CyclesUntilTwint(TWINT | TWEN), Is.EqualTo(1 + 9 * 160));
+ Assert.That(_twi.Status, Is.EqualTo(0x18));
+
+ Cpu.WriteData(TWDR, 0x55);
+ Assert.That(CyclesUntilTwint(TWINT | TWEN), Is.EqualTo(1 + 9 * 160));
+ Assert.That(_twi.Status, Is.EqualTo(0x28));
+
+ Assert.That(CyclesUntilTwint(TWINT | TWEN | TWSTO), Is.EqualTo(1 + 160));
+ Assert.That(_twi.Status, Is.EqualTo(0xf8));
+ }
+
+ [Test]
+ public void ByteReadTakesNineSclPeriods()
+ {
+ Cpu.WriteData(TWBR, 12);
+ _twi.EventHandler = new AckingHandler(() => _twi);
+ CyclesUntilTwint(TWINT | TWSTA | TWEN);
+ Cpu.WriteData(TWDR, 0xa1);
+ CyclesUntilTwint(TWINT | TWEN);
+
+ Assert.That(CyclesUntilTwint(TWINT | TWEN | TWEA), Is.EqualTo(1 + 9 * 40));
+ Assert.That(_twi.Status, Is.EqualTo(0x50));
+ Assert.That(Cpu.ReadData(TWDR), Is.EqualTo(0x66));
+ }
+
+ [Test]
+ public void LateCompletionGetsNoExtraDelay()
+ {
+ Cpu.WriteData(TWBR, 12); // start wire time: 40 cycles
+ _handler.CompleteStartInline = false;
+ Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
+ Run(1);
+ Assert.That(_handler.Starts, Is.EqualTo(1));
+
+ Run(5000); // the external model takes much longer than the wire time
+ Assert.That(TwintSet, Is.False);
+
+ _twi.CompleteStart();
+ Assert.That(TwintSet, Is.True);
+ Assert.That(_twi.Status, Is.EqualTo(0x08));
+ }
+
+ [Test]
+ public void EarlyCompletionIsHeldUntilWireTimeElapses()
+ {
+ Cpu.WriteData(TWBR, 12);
+ _handler.CompleteStartInline = false;
+ Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
+ Run(1); // dispatched here
+ Run(10);
+ _twi.CompleteStart(); // 10 cycles in, wire time is 40
+
+ Assert.That(TwintSet, Is.False);
+ Run(29);
+ Assert.That(TwintSet, Is.False);
+ Run(1);
+ Assert.That(TwintSet, Is.True);
+ }
+
+ [Test]
+ public void TwcrWriteWhileBusyIsIgnored()
+ {
+ Cpu.WriteData(TWBR, 12);
+ Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
+ Run(1);
+ Run(10); // event in flight
+
+ Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
+ Run(100);
+
+ Assert.That(_handler.Starts, Is.EqualTo(1));
+ Assert.That(TwintSet, Is.True);
+ }
+
+ [Test]
+ public void ResetDropsDeferredCompletion()
+ {
+ Cpu.WriteData(TWBR, 72);
+ Cpu.WriteData(TWCR, TWINT | TWSTA | TWEN);
+ Run(10);
+ Cpu.Reset();
+ Run(1000);
+
+ Assert.That(TwintSet, Is.False);
+ Assert.That(_twi.Status, Is.EqualTo(0xf8));
+ }
+
+ [Test]
+ public void EmulateBusTimingFalseCompletesOnNextCycle()
+ {
+ Cpu.WriteData(TWBR, 72);
+ _twi.EmulateBusTiming = false;
+
+ Assert.That(CyclesUntilTwint(TWINT | TWSTA | TWEN), Is.EqualTo(1));
+ }
+ }
+
[TestFixture]
public class SlaveMode : AvrTestBase
{
diff --git a/tests/Avr8Sharp.Tests/WatchdogTests.cs b/tests/Avr8Sharp.Tests/WatchdogTests.cs
index 1ccce03..9a2f0fa 100644
--- a/tests/Avr8Sharp.Tests/WatchdogTests.cs
+++ b/tests/Avr8Sharp.Tests/WatchdogTests.cs
@@ -96,7 +96,7 @@ public void ResetOnTimeout()
Assert.Multiple(() =>
{
Assert.That(Cpu.Pc, Is.EqualTo(0));
- Assert.That(Cpu.ReadData(MCUSR), Is.EqualTo(WDRF));
+ Assert.That(Cpu.ReadData(MCUSR) & WDRF, Is.EqualTo(WDRF));
});
}
@@ -262,7 +262,7 @@ public void NoDuplicateEventsOnConsecutiveWrites()
Assert.Multiple(() =>
{
Assert.That(Cpu.Pc, Is.EqualTo(0), "CPU should reset on timeout");
- Assert.That(Cpu.ReadData(MCUSR), Is.EqualTo(WDRF), "WDRF flag must be set on Watchdog reset");
+ Assert.That(Cpu.ReadData(MCUSR) & WDRF, Is.EqualTo(WDRF), "WDRF flag must be set on Watchdog reset");
});
}
@@ -300,7 +300,7 @@ public void RescheduleOnPrescalerShortened()
Assert.Multiple(() =>
{
Assert.That(Cpu.Pc, Is.EqualTo(0), "CPU should have reset because 20ms > new 16ms timeout");
- Assert.That(Cpu.ReadData(MCUSR), Is.EqualTo(WDRF), "WDRF flag must be set");
+ Assert.That(Cpu.ReadData(MCUSR) & WDRF, Is.EqualTo(WDRF), "WDRF flag must be set");
});
}
}