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"); }); } }