From 4dc7625ea645fe0ebba6c16097228dbc0d12a1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:22 -0600 Subject: [PATCH 1/7] fix(cpu): RCALL and RET keep the full PC 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. The 328P hid it because its flash length divides 0x10000. RET/RETI subtracted 1 from the low 16 bits before OR-ing in the high byte, so a return address whose low word is zero lost its high byte. Both now do the arithmetic on the full address. --- src/Avr8Sharp/Core/Opcodes.cs | 14 +- .../LargeFlashControlFlowTests.cs | 135 ++++++++++++++++++ 2 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 tests/Avr8Sharp.Tests/LargeFlashControlFlowTests.cs 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/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)); + } +} From d577a55c3393b3c66b69cc5c46eca5ed6c04d189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:22 -0600 Subject: [PATCH 2/7] fix(gpio): PINx reads the internal pull-up on a floating input (#22) An input pin whose PORT bit is set reads 1 while nothing drives it. A pin counts as driven from the first SetPinValue until the new ReleasePin, so an embedder that injects every input on every step sees no change. Enabling the pull-up on a floating pin now raises the pin-change edge the chip does. Gpio.PortWrite and Gpio.PinToggle asserted a floating pull-up reading 0 and now expect the silicon value. --- src/Avr8Sharp/Core/Peripherals/Gpio.cs | 21 +++++++- tests/Avr8Sharp.Tests/GpioTests.cs | 75 +++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) 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/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 { From 2854970f241ffe22f10126886b1adb78245da206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:22 -0600 Subject: [PATCH 3/7] fix(twi): bus events take their wire time at the programmed bit rate (#24) START and STOP take one SCL period, an address or data byte nine, at 16 + 2 * TWBR * prescaler CPU cycles per period, before TWINT is raised. The time counts from the dispatch to the ITwiEventHandler, so a handler that completes later adds no extra delay. EmulateBusTiming = false restores the next-cycle completion. --- src/Avr8Sharp/Core/Peripherals/Twi.cs | 128 ++++++++++++--- tests/Avr8Sharp.Tests/TwiTests.cs | 223 +++++++++++++++++++++++--- 2 files changed, 309 insertions(+), 42 deletions(-) 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/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 { From 8b5903251e470c78017379dfe9f2652b288e9a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:23 -0600 Subject: [PATCH 4/7] fix(adc): ATmega328P reference selection follows the datasheet order REFS1:0 is 00 AREF, 01 AVCC, 10 reserved, 11 internal 1.1 V (table 28-3). The table was the ATmega8's, so analogReference(INTERNAL) converted against 2.56 V. Also adds Atmega2560AdcConfig (16 channels, MUX5 selects ADC8-15); MUX5 is ignored on 8-channel configs. --- src/Avr8Sharp/Core/Peripherals/Adc.cs | 58 +++++++++++++++++++++++---- tests/Avr8Sharp.Tests/AdcTests.cs | 12 ++++++ 2 files changed, 63 insertions(+), 7 deletions(-) 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/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)); + } } From 411856ad50c3e7fb88f33a8ce4fce688cb16b197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:32 -0600 Subject: [PATCH 5/7] perf(testkit): RunUntilSerial evaluates its predicate only on new output (#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 in the output. SerialProbe.Text is cached until the next byte, and the text predicates run only when the probe's version changes. The core was checked across the 2^31 and 2^32 cycle boundaries and was not the cause; CycleBoundaryTests pins clock events and Timer0 there. --- .../AvrTestSimulation.cs | 25 ++++- .../Probes/SerialProbe.cs | 20 +++- .../ArduinoUnoSamples.cs | 29 ++++++ tests/Avr8Sharp.Tests/CycleBoundaryTests.cs | 95 +++++++++++++++++++ 4 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 tests/Avr8Sharp.Tests/CycleBoundaryTests.cs diff --git a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs index be6a97e..12060a4 100644 --- a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs +++ b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs @@ -335,6 +335,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 +364,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 +374,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/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/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.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); + } +} From 626415aefdaa3cd88a546218c23097967f9bcd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:39 -0600 Subject: [PATCH 6/7] feat(testkit): full Uno and Mega presets, MCUSR reset flags, strict mode (#26, #27) ArduinoUnoSimulation and ArduinoMegaSimulation mount SPI, TWI, ADC and the watchdog. The SPI and TWI device stubs move from the native layer into TestKit.Core (Avr8Sharp.TestKit.Probes) so the boards and the C ABI share one wiring. AvrWatchdog owns MCUSR: PORF on power-on, EXTRF on any other CPU reset, WDRF on a watchdog reset, and a flag clears only on a written zero. UnmountedAccess (Ignore by default, Warn, Throw) reports a read or write of PINx/DDRx/PORTx of a port that was never added; its hooks are only installed when the mode leaves Ignore. C ABI: a8s_set_strict, a8s_read_warnings, a8s_gpio_release_pin. Python: Simulation.with_strict, Simulation.warnings, Port.release, and adc/spi/twi on ArduinoMega. --- bindings/python/src/avr8sharp/_native.py | 6 +- bindings/python/src/avr8sharp/simulation.py | 47 ++- bindings/python/tests/test_pin_injection.py | 14 + .../python/tests/test_strict_and_boards.py | 59 +++ src/Avr8Sharp.Native/Exports.cs | 64 +++- src/Avr8Sharp.Native/NativeSession.cs | 5 +- .../AvrTestSimulation.cs | 104 +++++- .../Boards/ATtiny13Simulation.cs | 3 + .../Boards/ATtiny85Simulation.cs | 3 + .../Boards/ATtinyX4Simulation.cs | 3 + .../Boards/ArduinoMegaSimulation.cs | 51 +++ .../Boards/ArduinoUnoSimulation.cs | 37 +- .../Probes}/DeviceStubs.cs | 6 +- .../UnmountedIoAccess.cs | 24 ++ src/Avr8Sharp/Core/Peripherals/Watchdog.cs | 35 +- .../BoardPeripheralsTests.cs | 350 ++++++++++++++++++ tests/Avr8Sharp.Tests/WatchdogTests.cs | 6 +- 17 files changed, 775 insertions(+), 42 deletions(-) create mode 100644 bindings/python/tests/test_strict_and_boards.py rename src/{Avr8Sharp.Native => Avr8Sharp.TestKit.Core/Probes}/DeviceStubs.cs (94%) create mode 100644 src/Avr8Sharp.TestKit.Core/UnmountedIoAccess.cs create mode 100644 tests/Avr8Sharp.TestKit.Samples/BoardPeripheralsTests.cs 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 12060a4..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 ──────────────────────────────────────────────────────────── /// 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/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/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/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/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"); }); } } From 962573382071740b0d6cbfe95760c035209251b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 7 Oct 2026 19:24:39 -0600 Subject: [PATCH 7/7] docs(changelog): release notes for issues #21 to #27 --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) 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`