From c76dae4aa51a47b9a2e885a65f7c9dc424ca4337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 8 Oct 2026 00:47:00 -0600 Subject: [PATCH 1/2] fix: emulator fidelity from the CPU, peripheral and integration audit - Intel HEX: handle extended segment/linear records and checksums; add TryLoadHex. - Cpu.Reset clears the I/O space up to RamStart and resets every peripheral. - Interrupt entry takes 4 cycles (5 on 22-bit-PC parts); the instruction after SEI/RETI always runs before a pending interrupt. Per-CPU interrupt hooks. - AvrRunner carries its cycle target across calls (Run, RunUntil, RunCycles). - USART keeps RXC unless RXEN goes 1 to 0; write-one-to-clear flags in TIFR, ADCSRA, SPSR, TWCR, WDTCSR and EECR. - ATmega2560 configs in the core with the right OCx pins, channel C, ICP, INT0-7 and PCINT0-2; fast PWM channel C sets its pin at BOTTOM. - ATtiny85: EEARH at 0x3F, AvrAttinyTimer1 ported from avr8js. - ADC Avcc/Aref settable; EEPROM write time scales with the CPU frequency. --- src/Avr8Sharp.Native/Exports.cs | 3 +- .../AvrTestSimulation.cs | 4 +- .../Boards/ATtiny13Simulation.cs | 1 + .../Boards/ATtiny85Simulation.cs | 71 +-- .../Boards/ATtinyX4Simulation.cs | 1 + .../Boards/ArduinoMegaSimulation.cs | 217 ++------ .../Boards/ArduinoUnoSimulation.cs | 1 + src/Avr8Sharp/Core/Cpu.cs | 115 +++- src/Avr8Sharp/Core/Decoders/SwitchDecoder.cs | 4 +- src/Avr8Sharp/Core/Interrupt.cs | 9 +- src/Avr8Sharp/Core/Opcodes.cs | 38 +- src/Avr8Sharp/Core/Peripherals/Adc.cs | 42 +- src/Avr8Sharp/Core/Peripherals/Clock.cs | 12 + src/Avr8Sharp/Core/Peripherals/Eeprom.cs | 49 +- src/Avr8Sharp/Core/Peripherals/Gpio.cs | 118 ++++ src/Avr8Sharp/Core/Peripherals/Spi.cs | 39 +- src/Avr8Sharp/Core/Peripherals/Timer.cs | 378 ++++++++++--- src/Avr8Sharp/Core/Peripherals/TimerAttiny.cs | 419 +++++++++++++++ src/Avr8Sharp/Core/Peripherals/Twi.cs | 23 +- src/Avr8Sharp/Core/Peripherals/Usart.cs | 72 ++- src/Avr8Sharp/Core/Peripherals/Usi.cs | 2 + src/Avr8Sharp/Core/Peripherals/Watchdog.cs | 18 +- src/Avr8Sharp/Core/Utils/Builder.cs | 9 +- src/Avr8Sharp/Core/Utils/IntelHex.cs | 119 ++++ src/Avr8Sharp/Core/Utils/Runner.cs | 142 +++-- .../ATtiny85PeripheralTests.cs | 143 +++++ .../ArduinoMegaPeripheralsTests.cs | 507 ++++++++++++++++++ tests/Avr8Sharp.Tests/AdcTests.cs | 93 ++++ tests/Avr8Sharp.Tests/CpuCoreAuditTests.cs | 377 +++++++++++++ tests/Avr8Sharp.Tests/EepromTests.cs | 61 +++ tests/Avr8Sharp.Tests/GpioTests.cs | 10 +- tests/Avr8Sharp.Tests/InterruptTests.cs | 32 +- tests/Avr8Sharp.Tests/ResetTests.cs | 192 +++++++ tests/Avr8Sharp.Tests/SpiTests.cs | 24 + tests/Avr8Sharp.Tests/TimerAttinyTests.cs | 483 +++++++++++++++++ tests/Avr8Sharp.Tests/TimerConfigTests.cs | 52 ++ tests/Avr8Sharp.Tests/TimerTests.cs | 32 +- tests/Avr8Sharp.Tests/TwiTests.cs | 14 +- tests/Avr8Sharp.Tests/UsartTests.cs | 33 +- tests/Avr8Sharp.Tests/WatchdogTests.cs | 16 + 40 files changed, 3548 insertions(+), 427 deletions(-) create mode 100644 src/Avr8Sharp/Core/Peripherals/TimerAttiny.cs create mode 100644 src/Avr8Sharp/Core/Utils/IntelHex.cs create mode 100644 tests/Avr8Sharp.TestKit.Samples/ATtiny85PeripheralTests.cs create mode 100644 tests/Avr8Sharp.TestKit.Samples/ArduinoMegaPeripheralsTests.cs create mode 100644 tests/Avr8Sharp.Tests/CpuCoreAuditTests.cs create mode 100644 tests/Avr8Sharp.Tests/ResetTests.cs create mode 100644 tests/Avr8Sharp.Tests/TimerAttinyTests.cs create mode 100644 tests/Avr8Sharp.Tests/TimerConfigTests.cs diff --git a/src/Avr8Sharp.Native/Exports.cs b/src/Avr8Sharp.Native/Exports.cs index 6004ad8..3e25b9b 100644 --- a/src/Avr8Sharp.Native/Exports.cs +++ b/src/Avr8Sharp.Native/Exports.cs @@ -108,7 +108,8 @@ public static IntPtr CreateAttiny85() var t = new ATtiny85Simulation(); var s = new NativeSession(t); s.Ports.Add(t.PortB); - s.Timers.AddRange([t.Timer0, t.Timer1]); + // Timer1 (AvrAttinyTimer1) resets itself through Cpu.OnPeripheralReset, so only Timer0 is tracked. + s.Timers.Add(t.Timer0); return Wrap(s); } diff --git a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs index b2b93e3..21297c2 100644 --- a/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs +++ b/src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs @@ -195,7 +195,7 @@ public AvrTestSimulation AddTimer(AvrTimerConfig config, out AvrTimer timer) public AvrTestSimulation AddEeprom(AvrEepromConfig config, out AvrEeprom eeprom, uint eepromSize = 1024) { var backend = new EepromMemoryBackend(eepromSize); - eeprom = new AvrEeprom(Runner.Cpu, backend, config); + eeprom = new AvrEeprom(Runner.Cpu, backend, config, Runner.Speed); return this; } @@ -203,7 +203,7 @@ public AvrTestSimulation AddEeprom(AvrEepromConfig config, out AvrEeprom eeprom, public AvrTestSimulation AddEeprom(AvrEepromConfig config, uint eepromSize = 1024) { var backend = new EepromMemoryBackend(eepromSize); - _ = new AvrEeprom(Runner.Cpu, backend, config); + _ = new AvrEeprom(Runner.Cpu, backend, config, Runner.Speed); return this; } diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs index 82ff01c..4ec6edf 100644 --- a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs +++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs @@ -29,6 +29,7 @@ public ATtiny13Simulation() : base(Flash, Sram) { WithFrequency(Frequency); Cpu.StackLowLimit = 0x60; + Cpu.RamStart = 0x60; AddGpio(PortBConfig, out var pb); PortB = pb; } diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs index 15e7225..046012e 100644 --- a/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs +++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs @@ -11,11 +11,9 @@ namespace Avr8Sharp.TestKit.Boards; /// if needed. /// /// -/// Timer 1 note: the ATtiny85 TC1 is an 8-bit timer with a single TCCR1 register -/// (no TCCRA/TCCRB split). This simulation maps TCCR1 (0x30) as the clock-select -/// register (CS13:CS10). Prescalers /1–/64 (CS10-CS12) are supported; /128 and above -/// (CS13=1) are not because the existing timer engine only reads the low 3 CS bits. -/// CTC and PWM modes (CTC1, PWM1A bits in TCCR1) are not emulated — Normal mode only. +/// Timer 1 note: the ATtiny85 TC1 is an 8-bit timer with its own register layout (TCCR1 with +/// CTC1, PWM1A, COM1A and CS13:CS10, plus GTCCR with PWM1B and COM1B, and OCR1C as TOP). It is +/// modelled by , not by the generic ATmega timer. /// /// /// @@ -70,54 +68,22 @@ public sealed class ATtiny85Simulation : AvrTestSimulation tov: 2, ocfa: 16, ocfb: 8, ocfc: 0, toie: 2, ociea: 16, ocieb: 8, ociec: 0); - // ── ATtiny85 Timer 1 config ─────────────────────────────────────────────── - // 8-bit TC1. Single control register TCCR1 at I/O 0x30 → data-mem 0x50. - // (Mmio.Data is indexed by data-memory address = I/O offset + 0x20.) - // TCCRA is mapped to address 0x00 (R0 — always 0 in well-behaved code → WGM = Normal mode). - // TCCRB is mapped to TCCR1 (data-mem 0x50) so CS10–CS12 select the prescaler. - // Prescalers for CS10:CS12 on ATtiny85 TC1: - // 1→/1, 2→/2, 3→/4, 4→/8, 5→/16, 6→/32, 7→/64 - // TIFR (0x58) and TIMSK (0x59) are shared with Timer0; MmioController hook-chaining - // allows both timers to handle their own bits in the same register. - // ATtiny85 datasheet interrupt vectors (word addresses): - // TIMER1_OVF = 0x004, TIMER1_COMPA = 0x003, TIMER1_COMPB = 0x009 - private static readonly AvrTimerConfig Tiny85Timer1Config = new( - bits: 8, - dividers: [0, 1, 2, 4, 8, 16, 32, 64], // CS10:CS12 → /1../64 - captureInterrupt: 0, - comparatorAInterrupt: 0x03, // TIMER1_COMPA vector - comparatorBInterrupt: 0x09, // TIMER1_COMPB vector - comparatorCInterrupt: 0, - overflowInterrupt: 0x04, // TIMER1_OVF vector - tccra: 0x00, // R0 — always 0 → forces Normal WGM mode - tccrb: 0x50, // TCCR1 — contains CS13:CS10 (I/O 0x30 → data-mem 0x50) - tccrc: 0x00, - tcnt: 0x4F, // TCNT1 (I/O 0x2F → data-mem 0x4F) - ocra: 0x4E, // OCR1A (I/O 0x2E → data-mem 0x4E) - ocrb: 0x4B, // OCR1B (I/O 0x2B → data-mem 0x4B) - ocrc: 0, - icr: 0, - timsk: 0x59, // shared TIMSK (Timer0 uses bits 0-2, Timer1 uses bits 4-6) - tifr: 0x58, // shared TIFR (Timer0 uses bits 0-2, Timer1 uses bits 4-6) - comparatorPortA: Tiny85PortBConfig.PORT, comparatorPinA: 1, // OC1A on PB1 - comparatorPortB: Tiny85PortBConfig.PORT, comparatorPinB: 4, // OC1B on PB4 - comparatorPortC: 0, comparatorPinC: 0, - externalClockPort: 0, externalClockPin: 0, - tov: 0x10, // TOV1 = TIFR bit 4 - ocfa: 0x40, // OCF1A = TIFR bit 6 - ocfb: 0x20, // OCF1B = TIFR bit 5 - ocfc: 0, - toie: 0x10, // TOIE1 = TIMSK bit 4 - ociea: 0x40, // OCIE1A = TIMSK bit 6 - ocieb: 0x20, // OCIE1B = TIMSK bit 5 - ociec: 0); + // ── ATtiny85 Timer 1 ────────────────────────────────────────────────────── + // TC1 is not the ATmega timer, so it has its own model (AvrAttinyTimer1): TCCR1 (I/O 0x30), + // GTCCR (0x2C), TCNT1 (0x2F), OCR1A/B/C (0x2E/0x2B/0x2D), CTC1, PWM1A/B, OCR1C as TOP and + // prescalers /1../16384. The data-space addresses are I/O + 0x20 and are the defaults of + // AvrAttinyTimer1Config. TIFR (0x58) and TIMSK (0x59) are shared with Timer0 through the + // MmioController hook chain. + private static readonly AvrAttinyTimer1Config Tiny85Timer1Config = new( + comparatorPort: Tiny85PortBConfig.PORT, comparatorPinA: 1, comparatorPinB: 4); // ── EEPROM ──────────────────────────────────────────────────────────────── - // ATtiny85: EECR=0x3C, EEDR=0x3D, EEARL=0x3E; no EEARH (9-bit address, 512 bytes) + // ATtiny85: EECR=0x3C, EEDR=0x3D, EEARL=0x3E, EEARH=0x3F (I/O 0x1C..0x1F + 0x20). + // The address is 9 bits wide, so EEARH only has bit 0; the backend ignores the other bits. // EE_RDY: vector 7, word address 0x06 private static readonly AvrEepromConfig Tiny85EepromConfig = new AvrEepromConfig( eepromReadyInterrupt: 0x06, - eecr: 0x3C, eedr: 0x3D, eearl: 0x3E, eearh: 0x00, + eecr: 0x3C, eedr: 0x3D, eearl: 0x3E, eearh: 0x3F, eraseCycles: 28800, writeCycles: 28800, atomicCycles: 54400); // ── GPIO ────────────────────────────────────────────────────────────────── @@ -132,10 +98,10 @@ public sealed class ATtiny85Simulation : AvrTestSimulation public AvrTimer Timer0 { get; } /// - /// Timer 1 — 8-bit TC1 (ATtiny85-specific). Normal mode only; prescalers /1–/64. - /// OC1A on PB1, OC1B on PB4. + /// Timer 1 — 8-bit TC1 (ATtiny85-specific): Normal, CTC1 and PWM1A/PWM1B modes with + /// OCR1C as TOP, prescalers /1 to /16384. OC1A on PB1, OC1B on PB4. /// - public AvrTimer Timer1 { get; } + public AvrAttinyTimer1 Timer1 { get; } /// ATtiny85 internal EEPROM — 512 bytes, volatile (in-memory backend). public AvrEeprom Eeprom { get; } @@ -146,10 +112,11 @@ public sealed class ATtiny85Simulation : AvrTestSimulation public ATtiny85Simulation() : base(Flash, Sram) { WithFrequency(Frequency); + Cpu.RamStart = 0x60; AddGpio(Tiny85PortBConfig, out var portB); PortB = portB; AddTimer(Tiny85Timer0Config, out var t0); Timer0 = t0; - AddTimer(Tiny85Timer1Config, out var t1); Timer1 = t1; + Timer1 = new AvrAttinyTimer1(Cpu, Tiny85Timer1Config); AddEeprom(Tiny85EepromConfig, out var eeprom, 512); Eeprom = eeprom; } } diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs index abdd47f..5e63ee8 100644 --- a/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs +++ b/src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs @@ -34,6 +34,7 @@ public ATtinyX4Simulation(int flash, int sram) : base(flash, sram) { WithFrequency(Frequency); Cpu.StackLowLimit = 0x60; // SRAM starts at 0x60 on ATtiny parts + Cpu.RamStart = 0x60; AddGpio(PortAConfig, out var pa); PortA = pa; AddGpio(PortBConfig, out var pb); PortB = pb; diff --git a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs index 101b6c0..864d4c0 100644 --- a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs +++ b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoMegaSimulation.cs @@ -32,160 +32,6 @@ public sealed class ArduinoMegaSimulation : AvrTestSimulation private const uint Frequency = 16_000_000; private const uint EepromSize = 4096; - // ── ATmega2560 Timer interrupt vectors (JMP/4-byte vectors) ────────────── - // ATmega2560 uses 4-byte JMP entries; vector N sits at byte address (N-1)×4. - // Interrupt addresses are WORD indices (byte_address / 2) because DoAvrInterrupt - // sets cpu.Pc = address directly and Pc is a word index into ProgramMemory[]. - - private static readonly AvrTimerConfig Mega2560Timer0Config = - AvrTimer.Timer0Config.CreateNew( - comparatorAInterrupt: 0x2A, // vector 22 — word addr (byte 0x54) - comparatorBInterrupt: 0x2C, // vector 23 — word addr (byte 0x58) - overflowInterrupt: 0x2E); // vector 24 — word addr (byte 0x5C) - - private static readonly AvrTimerConfig Mega2560Timer1Config = - AvrTimer.Timer1Config.CreateNew( - captureInterrupt: 0x20, // vector 17 — word addr (byte 0x40) - comparatorAInterrupt: 0x22, // vector 18 — word addr (byte 0x44) - comparatorBInterrupt: 0x24, // vector 19 — word addr (byte 0x48) - comparatorCInterrupt: 0x26, // vector 20 — word addr (byte 0x4C) - overflowInterrupt: 0x28); // vector 21 — word addr (byte 0x50) - - private static readonly AvrTimerConfig Mega2560Timer2Config = - AvrTimer.Timer2Config.CreateNew( - comparatorAInterrupt: 0x1A, // vector 14 — word addr (byte 0x34) - comparatorBInterrupt: 0x1C, // vector 15 — word addr (byte 0x38) - overflowInterrupt: 0x1E); // vector 16 — word addr (byte 0x3C) - - // Timer 3 — 16-bit, registers 0x90–0x9C, TIMSK3 0x71, TIFR3 0x38 - private static readonly AvrTimerConfig Mega2560Timer3Config = new AvrTimerConfig( - bits: 16, - dividers: AvrTimer.Timer01Dividers, - captureInterrupt: 0x3E, // vector 32 — word addr (byte 0x7C) - comparatorAInterrupt: 0x40, // vector 33 — word addr (byte 0x80) - comparatorBInterrupt: 0x42, // vector 34 — word addr (byte 0x84) - comparatorCInterrupt: 0x44, // vector 35 — word addr (byte 0x88) - overflowInterrupt: 0x46, // vector 36 — word addr (byte 0x8C) - tccra: 0x90, tccrb: 0x91, tccrc: 0x92, - tcnt: 0x94, ocra: 0x98, ocrb: 0x9A, ocrc: 0x9C, icr: 0x96, - timsk: 0x71, tifr: 0x38, - comparatorPortA: AvrIoPort.PortEConfig.PORT, comparatorPinA: 3, - comparatorPortB: AvrIoPort.PortEConfig.PORT, comparatorPinB: 4, - comparatorPortC: AvrIoPort.PortEConfig.PORT, comparatorPinC: 5, - externalClockPort: AvrIoPort.PortEConfig.PORT, externalClockPin: 6, - tov: 1, ocfa: 2, ocfb: 4, ocfc: 8, - toie: 1, ociea: 2, ocieb: 4, ociec: 8); - - // Timer 4 — 16-bit, registers 0xA0–0xAC, TIMSK4 0x72, TIFR4 0x39 - private static readonly AvrTimerConfig Mega2560Timer4Config = new AvrTimerConfig( - bits: 16, - dividers: AvrTimer.Timer01Dividers, - captureInterrupt: 0x52, // vector 42 — word addr (byte 0xA4) - comparatorAInterrupt: 0x54, // vector 43 — word addr (byte 0xA8) - comparatorBInterrupt: 0x56, // vector 44 — word addr (byte 0xAC) - comparatorCInterrupt: 0x58, // vector 45 — word addr (byte 0xB0) - overflowInterrupt: 0x5A, // vector 46 — word addr (byte 0xB4) - tccra: 0xA0, tccrb: 0xA1, tccrc: 0xA2, - tcnt: 0xA4, ocra: 0xA8, ocrb: 0xAA, ocrc: 0xAC, icr: 0xA6, - timsk: 0x72, tifr: 0x39, - comparatorPortA: AvrIoPort.PortHConfig.PORT, comparatorPinA: 3, - comparatorPortB: AvrIoPort.PortHConfig.PORT, comparatorPinB: 4, - comparatorPortC: AvrIoPort.PortHConfig.PORT, comparatorPinC: 5, - externalClockPort: AvrIoPort.PortHConfig.PORT, externalClockPin: 3, - tov: 1, ocfa: 2, ocfb: 4, ocfc: 8, - toie: 1, ociea: 2, ocieb: 4, ociec: 8); - - // Timer 5 — 16-bit, registers 0x120–0x12D, TIMSK5 0x73, TIFR5 0x3A - private static readonly AvrTimerConfig Mega2560Timer5Config = new AvrTimerConfig( - bits: 16, - dividers: AvrTimer.Timer01Dividers, - captureInterrupt: 0x5C, // vector 47 — word addr (byte 0xB8) - comparatorAInterrupt: 0x5E, // vector 48 — word addr (byte 0xBC) - comparatorBInterrupt: 0x60, // vector 49 — word addr (byte 0xC0) - comparatorCInterrupt: 0x62, // vector 50 — word addr (byte 0xC4) - overflowInterrupt: 0x64, // vector 51 — word addr (byte 0xC8) - tccra: 0x120, tccrb: 0x121, tccrc: 0x122, - tcnt: 0x124, icr: 0x126, - ocra: 0x128, ocrb: 0x12A, ocrc: 0x12C, - timsk: 0x73, tifr: 0x3A, - comparatorPortA: AvrIoPort.PortLConfig.PORT, comparatorPinA: 3, - comparatorPortB: AvrIoPort.PortLConfig.PORT, comparatorPinB: 4, - comparatorPortC: AvrIoPort.PortLConfig.PORT, comparatorPinC: 5, - externalClockPort: AvrIoPort.PortLConfig.PORT, externalClockPin: 2, - tov: 1, ocfa: 2, ocfb: 4, ocfc: 8, - toie: 1, ociea: 2, ocieb: 4, ociec: 8, - icf: 0x20, icie: 0x20); - - // USART0 — same I/O registers as ATmega328P (0xC0–0xC6) but different interrupt vectors - private static readonly AvrUsartConfig Mega2560Usart0Config = new AvrUsartConfig - { - RxCompleteInterrupt = 0x32, // Vector 26 — byte addr 0x0064 - DataRegisterEmptyInterrupt = 0x34, // Vector 27 - TxCompleteInterrupt = 0x36, // Vector 28 - UCSRA = 0xC0, UCSRB = 0xC1, UCSRC = 0xC2, - UBRRL = 0xC4, UBRRH = 0xC5, UDR = 0xC6, - }; - - // USART1 — registers 0xC8–0xCE - private static readonly AvrUsartConfig Mega2560Usart1Config = new AvrUsartConfig - { - RxCompleteInterrupt = 0x48, // Vector 37 - DataRegisterEmptyInterrupt = 0x4A, // Vector 38 - TxCompleteInterrupt = 0x4C, // Vector 39 - UCSRA = 0xC8, UCSRB = 0xC9, UCSRC = 0xCA, - UBRRL = 0xCC, UBRRH = 0xCD, UDR = 0xCE, - }; - - // USART2 — registers 0xD0–0xD6 - private static readonly AvrUsartConfig Mega2560Usart2Config = new AvrUsartConfig - { - RxCompleteInterrupt = 0x66, // Vector 52 — word addr (byte 0xCC) - DataRegisterEmptyInterrupt = 0x68, // Vector 53 — word addr (byte 0xD0) - TxCompleteInterrupt = 0x6A, // Vector 54 — word addr (byte 0xD4) - UCSRA = 0xD0, UCSRB = 0xD1, UCSRC = 0xD2, - UBRRL = 0xD4, UBRRH = 0xD5, UDR = 0xD6, - }; - - // USART3 — registers 0x130–0x136 - private static readonly AvrUsartConfig Mega2560Usart3Config = new AvrUsartConfig - { - RxCompleteInterrupt = 0x6C, // Vector 55 - DataRegisterEmptyInterrupt = 0x6E, // Vector 56 - TxCompleteInterrupt = 0x70, // Vector 57 - UCSRA = 0x130, UCSRB = 0x131, UCSRC = 0x132, - UBRRL = 0x134, UBRRH = 0x135, UDR = 0x136, - }; - - // EEPROM — same I/O register addresses as ATmega328P; interrupt vector differs - // ATmega2560 EE_READY: avr-libc vect_num 30 → word addr 30×2 = 0x3C (byte 0x78). - // (0x3A is the ADC vector, vect_num 29 — do not confuse the two.) - private static readonly AvrEepromConfig Mega2560EepromConfig = new AvrEepromConfig( - eepromReadyInterrupt: 0x3C, - 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; } @@ -262,43 +108,44 @@ public ArduinoMegaSimulation() : base(Flash, Sram) // ATmega2560 SRAM starts at 0x200 (extended I/O occupies 0x100-0x1FF); a // PUSH/CALL below it has overflowed the stack into the I/O/register space. Cpu.StackLowLimit = 0x200; - - AddGpio(AvrIoPort.PortAConfig, out var pA); PortA = pA; - AddGpio(AvrIoPort.PortBConfig, out var pB); PortB = pB; - AddGpio(AvrIoPort.PortCConfig, out var pC); PortC = pC; - AddGpio(AvrIoPort.PortDConfig, out var pD); PortD = pD; - AddGpio(AvrIoPort.PortEConfig, out var pE); PortE = pE; - AddGpio(AvrIoPort.PortFConfig, out var pF); PortF = pF; - AddGpio(AvrIoPort.PortGConfig, out var pG); PortG = pG; - AddGpio(AvrIoPort.PortHConfig, out var pH); PortH = pH; - AddGpio(AvrIoPort.PortJConfig, out var pJ); PortJ = pJ; - AddGpio(AvrIoPort.PortKConfig, out var pK); PortK = pK; - AddGpio(AvrIoPort.PortLConfig, out var pL); PortL = pL; - - AddTimer(Mega2560Timer0Config, out var t0); Timer0 = t0; - AddTimer(Mega2560Timer1Config, out var t1); Timer1 = t1; - AddTimer(Mega2560Timer2Config, out var t2); Timer2 = t2; - AddTimer(Mega2560Timer3Config, out var t3); Timer3 = t3; - AddTimer(Mega2560Timer4Config, out var t4); Timer4 = t4; - AddTimer(Mega2560Timer5Config, out var t5); Timer5 = t5; - - AddUsart(Mega2560Usart0Config, out var s0); Serial0 = s0; - AddUsart(Mega2560Usart1Config, out var s1); Serial1 = s1; - AddUsart(Mega2560Usart2Config, out var s2); Serial2 = s2; - AddUsart(Mega2560Usart3Config, out var s3); Serial3 = s3; - - AddEeprom(Mega2560EepromConfig, out var eeprom, EepromSize); Eeprom = eeprom; - - AddSpi(Mega2560SpiConfig, out var spi); Spi = spi; + Cpu.RamStart = 0x200; + + AddGpio(AvrIoPort.Mega2560PortAConfig, out var pA); PortA = pA; + AddGpio(AvrIoPort.Mega2560PortBConfig, out var pB); PortB = pB; + AddGpio(AvrIoPort.Mega2560PortCConfig, out var pC); PortC = pC; + AddGpio(AvrIoPort.Mega2560PortDConfig, out var pD); PortD = pD; + AddGpio(AvrIoPort.Mega2560PortEConfig, out var pE); PortE = pE; + AddGpio(AvrIoPort.Mega2560PortFConfig, out var pF); PortF = pF; + AddGpio(AvrIoPort.Mega2560PortGConfig, out var pG); PortG = pG; + AddGpio(AvrIoPort.Mega2560PortHConfig, out var pH); PortH = pH; + AddGpio(AvrIoPort.Mega2560PortJConfig, out var pJ); PortJ = pJ; + AddGpio(AvrIoPort.Mega2560PortKConfig, out var pK); PortK = pK; + AddGpio(AvrIoPort.Mega2560PortLConfig, out var pL); PortL = pL; + + AddTimer(AvrTimer.Mega2560Timer0Config, out var t0); Timer0 = t0; + AddTimer(AvrTimer.Mega2560Timer1Config, out var t1); Timer1 = t1; + AddTimer(AvrTimer.Mega2560Timer2Config, out var t2); Timer2 = t2; + AddTimer(AvrTimer.Mega2560Timer3Config, out var t3); Timer3 = t3; + AddTimer(AvrTimer.Mega2560Timer4Config, out var t4); Timer4 = t4; + AddTimer(AvrTimer.Mega2560Timer5Config, out var t5); Timer5 = t5; + + AddUsart(AvrUsart.Mega2560Usart0Config, out var s0); Serial0 = s0; + AddUsart(AvrUsart.Mega2560Usart1Config, out var s1); Serial1 = s1; + AddUsart(AvrUsart.Mega2560Usart2Config, out var s2); Serial2 = s2; + AddUsart(AvrUsart.Mega2560Usart3Config, out var s3); Serial3 = s3; + + AddEeprom(AvrEeprom.Mega2560EepromConfig, out var eeprom, EepromSize); Eeprom = eeprom; + + AddSpi(AvrSpi.Mega2560SpiConfig, out var spi); Spi = spi; SpiBus = new SpiDeviceStub(); spi.OnTransfer = SpiBus.Transfer; - AddTwi(Mega2560TwiConfig, out var twi); Twi = twi; + AddTwi(AvrTwi.Mega2560TwiConfig, out var twi); Twi = twi; TwiBus = new TwiDeviceStub(twi); twi.EventHandler = TwiBus; - AddAdc(AvrAdc.Atmega2560AdcConfig, out var adc); Adc = adc; + AddAdc(AvrAdc.Mega2560AdcConfig, out var adc); Adc = adc; - AddWatchdog(Mega2560WatchdogConfig, out var watchdog); Watchdog = watchdog; + AddWatchdog(AvrWatchdog.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 9894cfc..dc4ab4b 100644 --- a/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs +++ b/src/Avr8Sharp.TestKit.Core/Boards/ArduinoUnoSimulation.cs @@ -82,6 +82,7 @@ public ArduinoUnoSimulation() : base(Flash, Sram) // stack into the I/O/register space. Surface it instead of silently corrupting // peripheral state. Cpu.StackLowLimit = 0x100; + Cpu.RamStart = 0x100; AddGpio(AvrIoPort.PortBConfig, out var portB); PortB = portB; AddGpio(AvrIoPort.PortCConfig, out var portC); PortC = portC; diff --git a/src/Avr8Sharp/Core/Cpu.cs b/src/Avr8Sharp/Core/Cpu.cs index 751f69a..ed2a0d5 100644 --- a/src/Avr8Sharp/Core/Cpu.cs +++ b/src/Avr8Sharp/Core/Cpu.cs @@ -23,11 +23,31 @@ public class Cpu private ulong _nextEventCycle = ulong.MaxValue; private short _nextInterrupt = -1; private short _maxInterrupt = 0; + // Set by SEI/RETI/anything that raises I: the next Tick must not dispatch an interrupt + // (the instruction after it always runs first). It is paired with _nextEventCycle = 0 + // so the next Tick takes the existing slow path and consumes it; the hot path pays nothing. + private bool _interruptHold; #endregion #region Public Properties public Action OnWatchdogReset { get; set; } = () => { }; public event Action? OnPeripheralReset; + /// + /// Raised when this CPU dispatches an interrupt. Arguments: vector address, PC that was + /// pushed. Instance-level replacement for the process-global AvrInterrupt.OnInterruptDispatch. + /// + public event Action? OnInterruptDispatch; + /// + /// Raised when this CPU executes a BREAK instruction (0x9598). The argument is the word + /// address of the BREAK. Instance-level replacement for AvrInterrupt.OnBreakpoint. + /// + public event Action? OnBreakpoint; + /// + /// Raised when this CPU executes a SLEEP instruction (0x9588). The argument is the + /// SM2:SM1:SM0 sleep mode bits from SMCR (bits 3:1). Instance-level replacement for + /// AvrInterrupt.OnSleep. + /// + public event Action? OnSleep; public MmioController Mmio { get; } public ushort[] ProgramMemory { get; } public byte[] ProgBytes { get; } @@ -44,6 +64,18 @@ public ushort Sp { /// a board/simulation sets it to the chip's SRAM start to catch overflow. /// public int StackLowLimit { get; set; } = 0; + + private int _ramStart = -1; + /// + /// The chip's first SRAM address, i.e. the end of the register/I/O space that + /// returns to reset values. When not set it is 0x200 for cores + /// with a 22-bit PC (ATmega2560/2561) and 0x100 otherwise, never above the data size. + /// Independent of . Boards set it explicitly (ATtiny: 0x60). + /// + public int RamStart { + get => Math.Min (_ramStart >= 0 ? _ramStart : (Pc22Bits ? 0x200 : RegisterSpace), Mmio.Data.Length); + set => _ramStart = value; + } /// /// The core variant this CPU models. Instructions the variant does not have throw /// instead of executing; instructions @@ -113,9 +145,85 @@ private void RegisterSregHooks() }); } + #region Instance hooks + // Instance handlers run first, then the obsolete process-global ones. + #pragma warning disable CS0618 + internal void RaiseInterruptDispatch (int address, uint pc) + { + OnInterruptDispatch?.Invoke (address, pc); + AvrInterrupt.OnInterruptDispatch?.Invoke (address, pc); + } + + internal void RaiseBreakpoint () + { + OnBreakpoint?.Invoke (Pc); + AvrInterrupt.OnBreakpoint?.Invoke (Pc); + } + + internal void RaiseSleep () + { + var mode = (byte)((Mmio.Data[0x53] >> 1) & 0x07); + OnSleep?.Invoke (mode); + AvrInterrupt.OnSleep?.Invoke (mode); + } + #pragma warning restore CS0618 + #endregion + + /// + /// Makes the next skip interrupt dispatch, so the instruction after + /// SEI / RETI / a write that sets I always runs before a pending interrupt is served. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void HoldInterrupts () + { + _interruptHold = true; + _nextEventCycle = 0; + } + + /// + /// Data write issued by an instruction. Identical to except that a + /// write to SREG (0x5F) that sets I from 0 holds interrupts for one instruction. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteDataSreg (ushort address, byte value) + { + if (address != 95) { + Mmio.WriteData (address, value); + return; + } + var wasEnabled = (_ram[95] & 0x80) != 0; + Mmio.WriteData (address, value); + if (!wasEnabled && (value & 0x80) != 0) HoldInterrupts (); + } + + private readonly List _preservedOnReset = []; + + /// + /// Registers a data address that must not zero (a register that + /// survives a reset on silicon, such as MCUSR which holds the reset-cause flags). + /// + public void PreserveOnReset (int address) + { + if (!_preservedOnReset.Contains (address)) _preservedOnReset.Add (address); + } + + /// + /// Resets the CPU and every peripheral, as the RESET pin would. The I/O register space + /// (0x20 up to ) + /// returns to its reset value: zero here, then each peripheral subscribed to + /// resets its internal state and writes its non-zero + /// reset values. SRAM and the general registers r0-r31 are kept, as on silicon. The + /// zeroing writes the data array directly, so no MMIO write hook runs. + /// public void Reset () { - // Reset the CPU + var ioEnd = RamStart; + if (ioEnd > 0x20) { + var preserved = new byte[_preservedOnReset.Count]; + for (var i = 0; i < preserved.Length; i++) preserved[i] = Mmio.Data[_preservedOnReset[i]]; + Array.Clear (Mmio.Data, 0x20, ioEnd - 0x20); + for (var i = 0; i < preserved.Length; i++) Mmio.Data[_preservedOnReset[i]] = preserved[i]; + } Sp = (ushort)(Mmio.Data.Length - 1); Mmio.Data[95] = 0; _sregArith = 0; @@ -124,6 +232,7 @@ public void Reset () _pendingInterrupts[i] = null; } _nextInterrupt = -1; + _interruptHold = false; _clockHead = 0; _clockEventCount = 0; _nextEventCycle = ulong.MaxValue; @@ -296,6 +405,10 @@ public void Tick() if (Cycles >= _nextEventCycle) { ProcessClockEvents(); + if (_interruptHold) { + _interruptHold = false; + return; + } } if (!InterruptsEnabled || _nextInterrupt < 0) return; diff --git a/src/Avr8Sharp/Core/Decoders/SwitchDecoder.cs b/src/Avr8Sharp/Core/Decoders/SwitchDecoder.cs index a0b48ce..419f795 100644 --- a/src/Avr8Sharp/Core/Decoders/SwitchDecoder.cs +++ b/src/Avr8Sharp/Core/Decoders/SwitchDecoder.cs @@ -317,14 +317,14 @@ public void Decode(Cpu cpu) if (opcode == 0x9588) { /* SLEEP — invoke OnSleep callback with SM2:SM1:SM0 bits from SMCR */ - AvrInterrupt.OnSleep?.Invoke((byte)((cpu.Mmio.Data[0x53] >> 1) & 0x07)); + cpu.RaiseSleep(); break; } if (opcode == 0x9598) { /* BREAK - hardware breakpoint */ - AvrInterrupt.OnBreakpoint?.Invoke(cpu.Pc); + cpu.RaiseBreakpoint(); break; } diff --git a/src/Avr8Sharp/Core/Interrupt.cs b/src/Avr8Sharp/Core/Interrupt.cs index 17b8277..5a70cb7 100644 --- a/src/Avr8Sharp/Core/Interrupt.cs +++ b/src/Avr8Sharp/Core/Interrupt.cs @@ -2,12 +2,14 @@ namespace AVR8Sharp.Core; public static class AvrInterrupt { + [Obsolete ("Process-global; use Cpu.OnInterruptDispatch, which is per instance.")] public static Action? OnInterruptDispatch { get; set; } /// /// Called when a BREAK instruction (0x9598) is executed. /// The parameter is the word address of the BREAK instruction. /// + [Obsolete ("Process-global; use Cpu.OnBreakpoint, which is per instance.")] public static Action? OnBreakpoint { get; set; } /// @@ -15,11 +17,12 @@ public static class AvrInterrupt /// The parameter is the SM2:SM1:SM0 sleep mode bits from SMCR (bits 3:1). /// The host simulation can pause the CPU loop in response. /// + [Obsolete ("Process-global; use Cpu.OnSleep, which is per instance.")] public static Action? OnSleep { get; set; } public static void DoAvrInterrupt (Cpu cpu, int address) { - OnInterruptDispatch?.Invoke(address, cpu.Pc); + cpu.RaiseInterruptDispatch (address, cpu.Pc); var sp = cpu.Mmio.DataView.GetUint16(93, true); if (sp - (cpu.Pc22Bits ? 2 : 1) < cpu.StackLowLimit) throw new AvrStackOverflowException(cpu.Pc, sp - (cpu.Pc22Bits ? 2 : 1), cpu.StackLowLimit); @@ -31,7 +34,9 @@ public static void DoAvrInterrupt (Cpu cpu, int address) } cpu.Mmio.DataView.SetUint16(93, (ushort)(sp - (cpu.Pc22Bits ? 3 : 2)), true); cpu.Mmio.Data[95] &= 0x7f; - cpu.Cycles += 3; // 2 for PC push + 1 for vector fetch (AVR spec: 4 cycles total including current instruction) + // Interrupt response is 4 cycles with a 16-bit PC and 5 with a 22-bit PC (3-byte push). + // The interrupted instruction already charged its own cycles before Tick got here. + cpu.Cycles += cpu.Pc22Bits ? 5UL : 4UL; cpu.Pc = (uint)address; } } diff --git a/src/Avr8Sharp/Core/Opcodes.cs b/src/Avr8Sharp/Core/Opcodes.cs index 005b693..4dcad5b 100644 --- a/src/Avr8Sharp/Core/Opcodes.cs +++ b/src/Avr8Sharp/Core/Opcodes.cs @@ -136,7 +136,11 @@ public static void BSET(ref Cpu cpu, ref ushort opcode) { var bit = (opcode & 0x70) >> 4; if (bit < 6) cpu._sregArith |= (byte)(1 << bit); - else cpu.Mmio.Data[95] |= (byte)(1 << bit); + else + { + if (bit == 7 && (cpu.Mmio.Data[95] & 0x80) == 0) cpu.HoldInterrupts(); + cpu.Mmio.Data[95] |= (byte)(1 << bit); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -659,14 +663,13 @@ public static void NOP(ref Cpu cpu, ref ushort opcode) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void BREAK(ref Cpu cpu, ref ushort opcode) { - AvrInterrupt.OnBreakpoint?.Invoke(cpu.Pc); + cpu.RaiseBreakpoint(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SLEEP(ref Cpu cpu, ref ushort opcode) { - // Invoke OnSleep with SM2:SM1:SM0 bits from SMCR register (0x53), bits 3:1 - AvrInterrupt.OnSleep?.Invoke((byte)((cpu.Mmio.Data[0x53] >> 1) & 0x07)); + cpu.RaiseSleep(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -696,7 +699,7 @@ public static void SBR(ref Cpu cpu, ref ushort opcode) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void OUT(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData((ushort)(((opcode & 0xf) | ((opcode & 0x600) >> 5)) + 32), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg((ushort)(((opcode & 0xf) | ((opcode & 0x600) >> 5)) + 32), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -774,6 +777,7 @@ public static void RETI(ref Cpu cpu, ref ushort opcode) cpu.Cycles += cpu.Pc22Bits ? 4UL : 3UL; cpu.Mmio.Data[95] |= 0x80; // Enable interrupts + cpu.HoldInterrupts(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -899,7 +903,7 @@ public static void STS(ref Cpu cpu, ref ushort opcode) { var value = cpu.Mmio.Data[(opcode & 0x1f0) >> 4]; var addr = cpu.ProgramMemory[(int)(cpu.Pc + 1)]; - cpu.WriteData(addr, value); + cpu.WriteDataSreg(addr, value); cpu.Pc++; cpu.Cycles++; } @@ -907,7 +911,7 @@ public static void STS(ref Cpu cpu, ref ushort opcode) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void STX(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData(cpu.Mmio.DataView.GetUint16(26, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg(cpu.Mmio.DataView.GetUint16(26, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); cpu.Cycles++; } @@ -915,7 +919,7 @@ public static void STX(ref Cpu cpu, ref ushort opcode) public static void STX_INC(ref Cpu cpu, ref ushort opcode) { var x = cpu.Mmio.DataView.GetUint16(26, true); - cpu.WriteData(x, cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg(x, cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); cpu.Mmio.DataView.SetUint16(26, (ushort)(x + 1), true); cpu.Cycles++; } @@ -926,14 +930,14 @@ public static void STX_DEC(ref Cpu cpu, ref ushort opcode) var i = cpu.Mmio.Data[(opcode & 0x1f0) >> 4]; var x = cpu.Mmio.DataView.GetUint16(26, true) - 1; cpu.Mmio.DataView.SetUint16(26, (ushort)x, true); - cpu.WriteData((ushort)x, i); + cpu.WriteDataSreg((ushort)x, i); cpu.Cycles++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void STY(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData(cpu.Mmio.DataView.GetUint16(28, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg(cpu.Mmio.DataView.GetUint16(28, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); cpu.Cycles++; } @@ -942,7 +946,7 @@ public static void STY_INC(ref Cpu cpu, ref ushort opcode) { var i = cpu.Mmio.Data[(opcode & 0x1f0) >> 4]; var y = cpu.Mmio.DataView.GetUint16(28, true); - cpu.WriteData(y, i); + cpu.WriteDataSreg(y, i); cpu.Mmio.DataView.SetUint16(28, (ushort)(y + 1), true); cpu.Cycles++; } @@ -953,14 +957,14 @@ public static void STY_DEC(ref Cpu cpu, ref ushort opcode) var i = cpu.Mmio.Data[(opcode & 0x1f0) >> 4]; var y = cpu.Mmio.DataView.GetUint16(28, true) - 1; cpu.Mmio.DataView.SetUint16(28, (ushort)y, true); - cpu.WriteData((ushort)y, i); + cpu.WriteDataSreg((ushort)y, i); cpu.Cycles++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void STDY(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData( + cpu.WriteDataSreg( (ushort)(cpu.Mmio.DataView.GetUint16(28, true) + ((opcode & 7) | ((opcode & 0xc00) >> 7) | ((opcode & 0x2000) >> 8))), cpu.Mmio.Data[(opcode & 0x1f0) >> 4] @@ -971,7 +975,7 @@ public static void STDY(ref Cpu cpu, ref ushort opcode) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void STZ(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData(cpu.Mmio.DataView.GetUint16(30, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg(cpu.Mmio.DataView.GetUint16(30, true), cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); cpu.Cycles++; } @@ -979,7 +983,7 @@ public static void STZ(ref Cpu cpu, ref ushort opcode) public static void STZ_INC(ref Cpu cpu, ref ushort opcode) { var z = cpu.Mmio.DataView.GetUint16(30, true); - cpu.WriteData(z, cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); + cpu.WriteDataSreg(z, cpu.Mmio.Data[(opcode & 0x1f0) >> 4]); cpu.Mmio.DataView.SetUint16(30, (ushort)(z + 1), true); cpu.Cycles++; } @@ -990,14 +994,14 @@ public static void STZ_DEC(ref Cpu cpu, ref ushort opcode) var i = cpu.Mmio.Data[(opcode & 0x1f0) >> 4]; var z = cpu.Mmio.DataView.GetUint16(30, true) - 1; cpu.Mmio.DataView.SetUint16(30, (ushort)z, true); - cpu.WriteData((ushort)z, i); + cpu.WriteDataSreg((ushort)z, i); cpu.Cycles++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void STDZ(ref Cpu cpu, ref ushort opcode) { - cpu.WriteData( + cpu.WriteDataSreg( (ushort)(cpu.Mmio.DataView.GetUint16(30, true) + ((opcode & 7) | ((opcode & 0xc00) >> 7) | ((opcode & 0x2000) >> 8))), cpu.Mmio.Data[(opcode & 0x1f0) >> 4] diff --git a/src/Avr8Sharp/Core/Peripherals/Adc.cs b/src/Avr8Sharp/Core/Peripherals/Adc.cs index 07f2321..5c20c60 100644 --- a/src/Avr8Sharp/Core/Peripherals/Adc.cs +++ b/src/Avr8Sharp/Core/Peripherals/Adc.cs @@ -61,6 +61,9 @@ private static ADCMuxConfiguration BuildAtmega2560Channels() ] ); + /// Alias of following the Mega2560<Peripheral>Config naming scheme. + public static readonly AvrAdcConfig Mega2560AdcConfig = Atmega2560AdcConfig; + public static readonly AdcMuxInput FallbackMuxInput = new AdcMuxInput(type: AdcMuxInputType.Constant, voltage: 0); public static readonly AvrAdcConfig AdcConfig = new AvrAdcConfig( @@ -104,8 +107,26 @@ private static ADCMuxConfiguration BuildAtmega2560Channels() int _conversionCycles = 25; readonly AvrAdcConfig _config; readonly AvrInterruptConfig _adc; - readonly double avcc = 5.0; - readonly double aref = 5.0; + private double avcc = 5.0; + private double aref = 5.0; + + /// + /// The voltage on the AVCC pin, which is the reference when ADMUX selects AVCC (the + /// Arduino core's DEFAULT). 5 V unless the host says otherwise; a board fed from a + /// different supply, or a 3.3 V board, sets it so conversions scale like the chip's do. + /// + public double Avcc + { + get => avcc; + set { avcc = value; UpdateCaches(); } + } + + /// The voltage on the AREF pin, the reference when ADMUX selects EXTERNAL. + public double Aref + { + get => aref; + set { aref = value; UpdateCaches(); } + } private int _cachedSampleCycles; private double _cachedReferenceVoltage; @@ -222,9 +243,15 @@ public AvrAdc(Cpu cpu, AvrAdcConfig config) _conversionCycles = 25; } + // ADIF is cleared by writing a one and never stored (datasheet 24.9.2, so sbi on + // ADCSRA clears it); ADSC cannot be cleared by firmware while a conversion runs. + var written = value; + value = (byte)((value & ~ADIF) | (oldValue & ADIF)); + if (_converting) value |= ADSC; cpu.Mmio.Data[config.ADCSRA] = value; UpdateCaches(); cpu.UpdateInterruptEnable(_adc, value); + cpu.ClearInterruptByFlag(_adc, written); if (!_converting && (value & ADSC) != 0) { @@ -239,10 +266,19 @@ public AvrAdc(Cpu cpu, AvrAdcConfig config) return true; } - return false; + return true; }); UpdateCaches(); + + cpu.OnPeripheralReset += () => + { + // A conversion in flight lost its completion event with the clock events + _converting = false; + _pendingAdcResult = 0; + _conversionCycles = 25; + UpdateCaches(); + }; } /// diff --git a/src/Avr8Sharp/Core/Peripherals/Clock.cs b/src/Avr8Sharp/Core/Peripherals/Clock.cs index 4bff395..6561736 100644 --- a/src/Avr8Sharp/Core/Peripherals/Clock.cs +++ b/src/Avr8Sharp/Core/Peripherals/Clock.cs @@ -78,6 +78,18 @@ public AvrClock(Cpu cpu, uint baseFreqHz, AvrClockConfig clockConfig) return true; }); + + // CLKPR resets to the unprogrammed-CKDIV8 value (division factor 1) + cpu.OnPeripheralReset += () => + { + _clockEnabledCycles = 0; + if (_prescalerValue != 1) + { + _cyclesDelta = (int)(((double)cpu.Cycles + _cyclesDelta) * (_prescalerValue / 1.0) - + (double)cpu.Cycles); + _prescalerValue = 1; + } + }; } } diff --git a/src/Avr8Sharp/Core/Peripherals/Eeprom.cs b/src/Avr8Sharp/Core/Peripherals/Eeprom.cs index 932fcf3..492a97b 100644 --- a/src/Avr8Sharp/Core/Peripherals/Eeprom.cs +++ b/src/Avr8Sharp/Core/Peripherals/Eeprom.cs @@ -23,6 +23,28 @@ public class AvrEeprom atomicCycles: 54400 // 3.4 ms at 16 MHz (atomic erase+write, datasheet Table 7-1) ); + /// ATmega2560 EEPROM: same registers as the ATmega328P; EE_READY is vector 30 -> word 0x3C. + public static readonly AvrEepromConfig Mega2560EepromConfig = new AvrEepromConfig( + eepromReadyInterrupt: 0x3c, + eecr: 0x3f, + eedr: 0x40, + eearl: 0x41, + eearh: 0x42, + eraseCycles: 28800, + writeCycles: 28800, + atomicCycles: 54400 + ); + + /// + /// CPU frequency the cycle counts in are expressed at (16 MHz). + /// The EEPROM programming time is fixed in wall-clock time (datasheet: 3.4 ms erase+write, + /// 1.8 ms erase-only or write-only, self-timed), so the cycles scale with the CPU frequency. + /// + public const uint ReferenceFrequencyHz = 16_000_000; + + /// CPU frequency in Hz used to convert the configured times into cycles. + public uint FrequencyHz { get; set; } + private ulong _writeEnabledCycles = 0; private ulong _writeCompleteCycles = 0; readonly AvrEepromConfig _config; @@ -30,9 +52,11 @@ public class AvrEeprom readonly IEepromBackend _backend; readonly Cpu _cpu; - public AvrEeprom(Cpu cpu, IEepromBackend backend, AvrEepromConfig? config = null) + public AvrEeprom(Cpu cpu, IEepromBackend backend, AvrEepromConfig? config = null, + uint frequencyHz = ReferenceFrequencyHz) { _cpu = cpu; + FrequencyHz = frequencyHz == 0 ? ReferenceFrequencyHz : frequencyHz; _backend = backend; _config = config ?? EepromConfig; _eer = new AvrInterruptConfig( @@ -49,7 +73,10 @@ public AvrEeprom(Cpu cpu, IEepromBackend backend, AvrEepromConfig? config = null { var addr = (ushort)((cpu.Mmio.Data[_config.EEARH] << 8) | cpu.Mmio.Data[_config.EEARL]); - cpu.Mmio.Data[_config.EECR] = (byte)((cpu.Mmio.Data[_config.EECR] & ~EECR_WRITE_MASK) | (eecr & EECR_WRITE_MASK)); + // EEPE is set only by the write sequence below and cleared by hardware when the + // programming finishes; writing zero to it (e.g. toggling EERIE) must not clear it. + var keepEepe = (eecr & EEPE) == 0 ? cpu.Mmio.Data[_config.EECR] & EEPE : 0; + cpu.Mmio.Data[_config.EECR] = (byte)((cpu.Mmio.Data[_config.EECR] & ~EECR_WRITE_MASK) | (eecr & EECR_WRITE_MASK) | keepEepe); cpu.UpdateInterruptEnable(_eer, eecr); if ((eecr & EERE) != 0) @@ -80,7 +107,7 @@ public AvrEeprom(Cpu cpu, IEepromBackend backend, AvrEepromConfig? config = null // If EEMPE is zero, setting EEPE will have no effect. if (cpu.Cycles >= _writeEnabledCycles) { - cpu.Mmio.Data[_config.EECR] &= ~EEPE & 0xFF; + if (cpu.Cycles >= _writeCompleteCycles) cpu.Mmio.Data[_config.EECR] &= ~EEPE & 0xFF; return true; } @@ -116,6 +143,12 @@ public AvrEeprom(Cpu cpu, IEepromBackend backend, AvrEepromConfig? config = null if (doWrite) duration += (int)_config.WriteCycles; } + // Config cycles are at the 16 MHz reference; rescale to the actual CPU clock. + if (FrequencyHz != ReferenceFrequencyHz) + { + duration = (int)Math.Max(1L, (long)duration * FrequencyHz / ReferenceFrequencyHz); + } + _writeCompleteCycles = cpu.Cycles + (ulong)duration; cpu.Mmio.Data[_config.EECR] |= EEPE; @@ -169,19 +202,23 @@ public EepromMemoryBackend(uint size) } } + // On silicon the address bits above the EEPROM size are ignored (the address registers + // are wider than the array), so out-of-range addresses wrap instead of faulting. + private int Index(uint address) => (int)(address % (uint)_memory.Length); + public byte ReadMemory(uint address) { - return _memory[address]; + return _memory[Index(address)]; } public void WriteMemory(uint address, byte value) { - _memory[address] &= value; + _memory[Index(address)] &= value; } public void EraseMemory(uint address) { - _memory[address] = 0xFF; + _memory[Index(address)] = 0xFF; } } diff --git a/src/Avr8Sharp/Core/Peripherals/Gpio.cs b/src/Avr8Sharp/Core/Peripherals/Gpio.cs index dc3ec79..8f57010 100644 --- a/src/Avr8Sharp/Core/Peripherals/Gpio.cs +++ b/src/Avr8Sharp/Core/Peripherals/Gpio.cs @@ -138,6 +138,105 @@ public class AvrIoPort externalInterrupts: [] ); + // ── ATmega2560 ────────────────────────────────────────────────────────── + // Naming scheme for every Mega peripheral config in the core: Mega2560Config + // (Mega2560Timer3Config, Mega2560Usart1Config, Mega2560PortBConfig ...). Interrupt "address" + // values are word indices = avr-libc vector number x 2 (the ATmega2560 uses 4-byte JMP vectors). + // Checked against the ATmega2560 datasheet register summary and interrupt vector table. + + /// ATmega2560 INT0 (PD0): EICRA ISC00/01, vector 1 -> word 0x02. + public static readonly AvrExternalInterrupt Mega2560INT0 = new AvrExternalInterrupt (eicr: 0x69, eimsk: 0x3d, eifr: 0x3c, iscOffset: 0, index: 0, interrupt: 0x02); + /// ATmega2560 INT1 (PD1): EICRA ISC10/11, vector 2 -> word 0x04. + public static readonly AvrExternalInterrupt Mega2560INT1 = new AvrExternalInterrupt (eicr: 0x69, eimsk: 0x3d, eifr: 0x3c, iscOffset: 2, index: 1, interrupt: 0x04); + /// ATmega2560 INT2 (PD2): EICRA ISC20/21, vector 3 -> word 0x06. + public static readonly AvrExternalInterrupt Mega2560INT2 = new AvrExternalInterrupt (eicr: 0x69, eimsk: 0x3d, eifr: 0x3c, iscOffset: 4, index: 2, interrupt: 0x06); + /// ATmega2560 INT3 (PD3): EICRA ISC30/31, vector 4 -> word 0x08. + public static readonly AvrExternalInterrupt Mega2560INT3 = new AvrExternalInterrupt (eicr: 0x69, eimsk: 0x3d, eifr: 0x3c, iscOffset: 6, index: 3, interrupt: 0x08); + /// ATmega2560 INT4 (PE4): EICRB ISC40/41, vector 5 -> word 0x0A. + public static readonly AvrExternalInterrupt Mega2560INT4 = new AvrExternalInterrupt (eicr: 0x6a, eimsk: 0x3d, eifr: 0x3c, iscOffset: 0, index: 4, interrupt: 0x0a); + /// ATmega2560 INT5 (PE5): EICRB ISC50/51, vector 6 -> word 0x0C. + public static readonly AvrExternalInterrupt Mega2560INT5 = new AvrExternalInterrupt (eicr: 0x6a, eimsk: 0x3d, eifr: 0x3c, iscOffset: 2, index: 5, interrupt: 0x0c); + /// ATmega2560 INT6 (PE6): EICRB ISC60/61, vector 7 -> word 0x0E. + public static readonly AvrExternalInterrupt Mega2560INT6 = new AvrExternalInterrupt (eicr: 0x6a, eimsk: 0x3d, eifr: 0x3c, iscOffset: 4, index: 6, interrupt: 0x0e); + /// ATmega2560 INT7 (PE7): EICRB ISC70/71, vector 8 -> word 0x10. + public static readonly AvrExternalInterrupt Mega2560INT7 = new AvrExternalInterrupt (eicr: 0x6a, eimsk: 0x3d, eifr: 0x3c, iscOffset: 6, index: 7, interrupt: 0x10); + + /// PCINT0 group: PORTB (PCINT0-7), PCMSK0 0x6B, vector 9 -> word 0x12. + public static readonly AvrPinChangeInterrupt Mega2560PCINT0 = new AvrPinChangeInterrupt (pcie: 0, pcicr: 0x68, pcifr: 0x3b, pcmsk: 0x6b, pinChangeInterrupt: 0x12, mask: 0xFF, offset: 0); + /// PCINT1 group, PE0 half (PCINT8 = PCMSK1 bit 0), vector 10 -> word 0x14. + public static readonly AvrPinChangeInterrupt Mega2560PCINT1E = new AvrPinChangeInterrupt (pcie: 1, pcicr: 0x68, pcifr: 0x3b, pcmsk: 0x6c, pinChangeInterrupt: 0x14, mask: 0x01, offset: 0); + /// PCINT1 group, PJ0-6 half (PCINT9-15 = PCMSK1 bits 1-7), vector 10 -> word 0x14. + public static readonly AvrPinChangeInterrupt Mega2560PCINT1J = new AvrPinChangeInterrupt (pcie: 1, pcicr: 0x68, pcifr: 0x3b, pcmsk: 0x6c, pinChangeInterrupt: 0x14, mask: 0x7F, offset: 1); + /// PCINT2 group: PORTK (PCINT16-23), PCMSK2 0x6D, vector 11 -> word 0x16. + public static readonly AvrPinChangeInterrupt Mega2560PCINT2 = new AvrPinChangeInterrupt (pcie: 2, pcicr: 0x68, pcifr: 0x3b, pcmsk: 0x6d, pinChangeInterrupt: 0x16, mask: 0xFF, offset: 0); + + /// ATmega2560 Port A (digital 22-29). + public static readonly AvrPortConfig Mega2560PortAConfig = new AvrPortConfig ( + pin: 0x20, ddr: 0x21, port: 0x22, + externalInterrupts: [] + ); + + /// ATmega2560 Port B (PCINT0-7, OC0A/OC1C/OC1A/OC1B/OC2A, SPI). + public static readonly AvrPortConfig Mega2560PortBConfig = new AvrPortConfig ( + pin: 0x23, ddr: 0x24, port: 0x25, + pinChange: Mega2560PCINT0 + ); + + /// ATmega2560 Port C (digital 30-37). + public static readonly AvrPortConfig Mega2560PortCConfig = new AvrPortConfig ( + pin: 0x26, ddr: 0x27, port: 0x28, + externalInterrupts: [] + ); + + /// ATmega2560 Port D (INT0-3, T0/T1, ICP1). + public static readonly AvrPortConfig Mega2560PortDConfig = new AvrPortConfig ( + pin: 0x29, ddr: 0x2a, port: 0x2b, + externalInterrupts: [Mega2560INT0, Mega2560INT1, Mega2560INT2, Mega2560INT3] + ); + + /// ATmega2560 Port E (INT4-7, PCINT8, OC3A/B/C, T3, ICP3). + public static readonly AvrPortConfig Mega2560PortEConfig = new AvrPortConfig ( + pin: 0x2c, ddr: 0x2d, port: 0x2e, + pinChange: Mega2560PCINT1E, + externalInterrupts: [null, null, null, null, Mega2560INT4, Mega2560INT5, Mega2560INT6, Mega2560INT7] + ); + + /// ATmega2560 Port F (ADC0-7). + public static readonly AvrPortConfig Mega2560PortFConfig = new AvrPortConfig ( + pin: 0x2f, ddr: 0x30, port: 0x31, + externalInterrupts: [] + ); + + /// ATmega2560 Port G (OC0B). + public static readonly AvrPortConfig Mega2560PortGConfig = new AvrPortConfig ( + pin: 0x32, ddr: 0x33, port: 0x34, + externalInterrupts: [] + ); + + /// ATmega2560 Port H (OC4A/B/C, OC2B, T4). + public static readonly AvrPortConfig Mega2560PortHConfig = new AvrPortConfig ( + pin: 0x100, ddr: 0x101, port: 0x102, + externalInterrupts: [] + ); + + /// ATmega2560 Port J (PCINT9-15). + public static readonly AvrPortConfig Mega2560PortJConfig = new AvrPortConfig ( + pin: 0x103, ddr: 0x104, port: 0x105, + pinChange: Mega2560PCINT1J + ); + + /// ATmega2560 Port K (ADC8-15, PCINT16-23). + public static readonly AvrPortConfig Mega2560PortKConfig = new AvrPortConfig ( + pin: 0x106, ddr: 0x107, port: 0x108, + pinChange: Mega2560PCINT2 + ); + + /// ATmega2560 Port L (OC5A/B/C, T5, ICP4/5). + public static readonly AvrPortConfig Mega2560PortLConfig = new AvrPortConfig ( + pin: 0x109, ddr: 0x10a, port: 0x10b, + externalInterrupts: [] + ); + private readonly List _externalInts = []; public event Action? OnGpioChange; private readonly AvrInterruptConfig? _pcint; @@ -230,6 +329,25 @@ public AvrIoPort (Cpu cpu, AvrPortConfig portConfig) // Move here to be able to test the TimerOverridePin TimerOverridePin = DelegateTimerOverridePin; + + cpu.OnPeripheralReset += ResetState; + } + + /// + /// Returns the port to its reset state: every pin an input without pull-up, timer + /// overrides released. The register bytes were already zeroed by . + /// Listeners are notified so embedders see the pins go back to input. Pins held by + /// belong to the outside world and stay driven. + /// + private void ResetState () + { + _overrideMask = 0xff; + _overrideValue = 0; + foreach (var external in _externalInts) { + if (external != null) external.Constant = false; + } + WriteGpio (0, 0); + UpdatePinRegister (0); } private void AssignExternalInterrupts (AvrExternalInterrupt?[] externalInts) diff --git a/src/Avr8Sharp/Core/Peripherals/Spi.cs b/src/Avr8Sharp/Core/Peripherals/Spi.cs index 73a0d0c..aedd280 100644 --- a/src/Avr8Sharp/Core/Peripherals/Spi.cs +++ b/src/Avr8Sharp/Core/Peripherals/Spi.cs @@ -28,11 +28,21 @@ public class AvrSpi SPDR = 0x4e }; + /// ATmega2560 SPI: same registers as the ATmega328P; SPI STC is vector 24 -> word 0x30. + public static readonly AvrSpiConfig Mega2560SpiConfig = new AvrSpiConfig { + SpiInterrupt = 0x30, + + SPCR = 0x4c, + SPSR = 0x4d, + SPDR = 0x4e + }; + readonly Cpu _cpu; readonly AvrSpiConfig _config; readonly uint _freqHz; bool _transmissionActive = false; + bool _spsrReadWithSpif = false; byte _shiftRegister = 0; readonly AvrInterruptConfig _spi; @@ -120,6 +130,7 @@ public AvrSpi (Cpu cpu, AvrSpiConfig config, uint freqHz) } // Clear write collision / interrupt flags + _spsrReadWithSpif = false; _cpu.Mmio.Data[_config.SPSR] &= ~SPSR_WCOL & 0xFF; _cpu.ClearInterrupt (_spi); @@ -128,6 +139,22 @@ public AvrSpi (Cpu cpu, AvrSpiConfig config, uint freqHz) return true; }); + // SPIF is cleared by reading SPSR with SPIF set and then accessing SPDR (datasheet SPSR). + cpu.Mmio.RegisterRead(_config.SPSR, _ => { + var value = _cpu.Mmio.Data[_config.SPSR]; + _spsrReadWithSpif = (value & SPSR_SPIF) != 0; + return value; + }); + + cpu.Mmio.RegisterRead(_config.SPDR, _ => { + var value = _cpu.Mmio.Data[_config.SPDR]; + if (_spsrReadWithSpif) { + _spsrReadWithSpif = false; + _cpu.ClearInterrupt (_spi); + } + return value; + }); + cpu.Mmio.RegisterWrite(_config.SPCR, (value, _, _, _) => { _cpu.UpdateInterruptEnable (_spi, value); return false; @@ -135,10 +162,16 @@ public AvrSpi (Cpu cpu, AvrSpiConfig config, uint freqHz) cpu.Mmio.RegisterWrite(_config.SPSR, (value, _, _, _) => { - _cpu.Mmio.Data[_config.SPSR] = value; - _cpu.ClearInterruptByFlag(_spi, value); - return false; + // SPIF and WCOL are read-only; only SPI2X is writable (datasheet SPSR). + _cpu.Mmio.Data[_config.SPSR] = (byte)((_cpu.Mmio.Data[_config.SPSR] & ~SPSR_SPI2X) | (value & SPSR_SPI2X)); + return true; }); + + cpu.OnPeripheralReset += () => { + // Any transfer in flight lost its completion event with the clock events + _transmissionActive = false; + _shiftRegister = 0; + }; } public void CompleteTransfer (int receivedByte) diff --git a/src/Avr8Sharp/Core/Peripherals/Timer.cs b/src/Avr8Sharp/Core/Peripherals/Timer.cs index 51bf9ca..c7a0f6d 100644 --- a/src/Avr8Sharp/Core/Peripherals/Timer.cs +++ b/src/Avr8Sharp/Core/Peripherals/Timer.cs @@ -163,6 +163,176 @@ public class AvrTimer ociec: DefaultTimerBits.OCIEC ); + // ── ATmega2560 ────────────────────────────────────────────────────────── + // Naming: Mega2560TimerConfig. Interrupt "address" values are word indices = avr-libc vector + // number x 2. Output compare / T / ICP pins follow the ATmega2560 datasheet pin table. + + /// ATmega2560 Timer0: 8-bit, OC0A = PB7, OC0B = PG5, T0 = PD7. Vectors COMPA 0x2A, COMPB 0x2C, OVF 0x2E. + public static readonly AvrTimerConfig Mega2560Timer0Config = Timer0Config.CreateNew( + comparatorAInterrupt: 0x2a, + comparatorBInterrupt: 0x2c, + overflowInterrupt: 0x2e, + comparatorPortA: AvrIoPort.Mega2560PortBConfig.PORT, + comparatorPinA: 7, + comparatorPortB: AvrIoPort.Mega2560PortGConfig.PORT, + comparatorPinB: 5, + externalClockPort: AvrIoPort.Mega2560PortDConfig.PORT, + externalClockPin: 7); + + /// ATmega2560 Timer1: 16-bit with channel C (OCR1C 0x8C), OC1A = PB5, OC1B = PB6, OC1C = PB7, T1 = PD6, ICP1 = PD4. Vectors CAPT 0x20, COMPA 0x22, COMPB 0x24, COMPC 0x26, OVF 0x28. + public static readonly AvrTimerConfig Mega2560Timer1Config = Timer1Config.CreateNew( + captureInterrupt: 0x20, + comparatorAInterrupt: 0x22, + comparatorBInterrupt: 0x24, + comparatorCInterrupt: 0x26, + overflowInterrupt: 0x28, + ocrc: 0x8c, + ocfc: 0x08, + ociec: 0x08, + comparatorPortA: AvrIoPort.Mega2560PortBConfig.PORT, + comparatorPinA: 5, + comparatorPortB: AvrIoPort.Mega2560PortBConfig.PORT, + comparatorPinB: 6, + comparatorPortC: AvrIoPort.Mega2560PortBConfig.PORT, + comparatorPinC: 7, + externalClockPort: AvrIoPort.Mega2560PortDConfig.PORT, + externalClockPin: 6, + icpPort: AvrIoPort.Mega2560PortDConfig.PORT, + icpPin: 4); + + /// ATmega2560 Timer2: 8-bit async, OC2A = PB4, OC2B = PH6. Vectors COMPA 0x1A, COMPB 0x1C, OVF 0x1E. + public static readonly AvrTimerConfig Mega2560Timer2Config = Timer2Config.CreateNew( + comparatorAInterrupt: 0x1a, + comparatorBInterrupt: 0x1c, + overflowInterrupt: 0x1e, + comparatorPortA: AvrIoPort.Mega2560PortBConfig.PORT, + comparatorPinA: 4, + comparatorPortB: AvrIoPort.Mega2560PortHConfig.PORT, + comparatorPinB: 6); + + /// ATmega2560 Timer3: 16-bit, OC3A/B/C = PE3/PE4/PE5, T3 = PE6, ICP3 = PE7. Vectors CAPT 0x3E, COMPA 0x40, COMPB 0x42, COMPC 0x44, OVF 0x46. + public static readonly AvrTimerConfig Mega2560Timer3Config = new AvrTimerConfig( + bits: 16, + dividers: Timer01Dividers, + captureInterrupt: 0x3e, + comparatorAInterrupt: 0x40, + comparatorBInterrupt: 0x42, + comparatorCInterrupt: 0x44, + overflowInterrupt: 0x46, + tifr: 0x38, + ocra: 0x98, + ocrb: 0x9a, + ocrc: 0x9c, + icr: 0x96, + tcnt: 0x94, + tccra: 0x90, + tccrb: 0x91, + tccrc: 0x92, + timsk: 0x71, + comparatorPortA: AvrIoPort.Mega2560PortEConfig.PORT, + comparatorPinA: 3, + comparatorPortB: AvrIoPort.Mega2560PortEConfig.PORT, + comparatorPinB: 4, + comparatorPortC: AvrIoPort.Mega2560PortEConfig.PORT, + comparatorPinC: 5, + externalClockPort: AvrIoPort.Mega2560PortEConfig.PORT, + externalClockPin: 6, + icpPort: AvrIoPort.Mega2560PortEConfig.PORT, + icpPin: 7, + tov: DefaultTimerBits.TOV, + ocfa: DefaultTimerBits.OCFA, + ocfb: DefaultTimerBits.OCFB, + ocfc: 0x08, + icf: 0x20, + toie: DefaultTimerBits.TOIE, + ociea: DefaultTimerBits.OCIEA, + ocieb: DefaultTimerBits.OCIEB, + ociec: 0x08, + icie: 0x20 + ); + + /// ATmega2560 Timer4: 16-bit, OC4A/B/C = PH3/PH4/PH5, T4 = PH7, ICP4 = PL0. Vectors CAPT 0x52, COMPA 0x54, COMPB 0x56, COMPC 0x58, OVF 0x5A. + public static readonly AvrTimerConfig Mega2560Timer4Config = new AvrTimerConfig( + bits: 16, + dividers: Timer01Dividers, + captureInterrupt: 0x52, + comparatorAInterrupt: 0x54, + comparatorBInterrupt: 0x56, + comparatorCInterrupt: 0x58, + overflowInterrupt: 0x5a, + tifr: 0x39, + ocra: 0xa8, + ocrb: 0xaa, + ocrc: 0xac, + icr: 0xa6, + tcnt: 0xa4, + tccra: 0xa0, + tccrb: 0xa1, + tccrc: 0xa2, + timsk: 0x72, + comparatorPortA: AvrIoPort.Mega2560PortHConfig.PORT, + comparatorPinA: 3, + comparatorPortB: AvrIoPort.Mega2560PortHConfig.PORT, + comparatorPinB: 4, + comparatorPortC: AvrIoPort.Mega2560PortHConfig.PORT, + comparatorPinC: 5, + externalClockPort: AvrIoPort.Mega2560PortHConfig.PORT, + externalClockPin: 7, + icpPort: AvrIoPort.Mega2560PortLConfig.PORT, + icpPin: 0, + tov: DefaultTimerBits.TOV, + ocfa: DefaultTimerBits.OCFA, + ocfb: DefaultTimerBits.OCFB, + ocfc: 0x08, + icf: 0x20, + toie: DefaultTimerBits.TOIE, + ociea: DefaultTimerBits.OCIEA, + ocieb: DefaultTimerBits.OCIEB, + ociec: 0x08, + icie: 0x20 + ); + + /// ATmega2560 Timer5: 16-bit, OC5A/B/C = PL3/PL4/PL5, T5 = PL2, ICP5 = PL1. Vectors CAPT 0x5C, COMPA 0x5E, COMPB 0x60, COMPC 0x62, OVF 0x64. + public static readonly AvrTimerConfig Mega2560Timer5Config = new AvrTimerConfig( + bits: 16, + dividers: Timer01Dividers, + captureInterrupt: 0x5c, + comparatorAInterrupt: 0x5e, + comparatorBInterrupt: 0x60, + comparatorCInterrupt: 0x62, + overflowInterrupt: 0x64, + tifr: 0x3a, + ocra: 0x128, + ocrb: 0x12a, + ocrc: 0x12c, + icr: 0x126, + tcnt: 0x124, + tccra: 0x120, + tccrb: 0x121, + tccrc: 0x122, + timsk: 0x73, + comparatorPortA: AvrIoPort.Mega2560PortLConfig.PORT, + comparatorPinA: 3, + comparatorPortB: AvrIoPort.Mega2560PortLConfig.PORT, + comparatorPinB: 4, + comparatorPortC: AvrIoPort.Mega2560PortLConfig.PORT, + comparatorPinC: 5, + externalClockPort: AvrIoPort.Mega2560PortLConfig.PORT, + externalClockPin: 2, + icpPort: AvrIoPort.Mega2560PortLConfig.PORT, + icpPin: 1, + tov: DefaultTimerBits.TOV, + ocfa: DefaultTimerBits.OCFA, + ocfb: DefaultTimerBits.OCFB, + ocfc: 0x08, + icf: 0x20, + toie: DefaultTimerBits.TOIE, + ociea: DefaultTimerBits.OCIEA, + ocieb: DefaultTimerBits.OCIEB, + ociec: 0x08, + icie: 0x20 + ); + public static readonly WgmConfig[] WgmModes8Bit = [ new WgmConfig(mode: TimerMode.Normal, timerTopValue: 0xff, ocrUpdateMode: OcrUpdateMode.Immediate, @@ -435,6 +605,7 @@ public AvrTimer(Cpu cpu, AvrTimerConfig config) } _cpu.Mmio.Data[_config.TCCRB] = value; + TryAttachIcp(); _updateDivider = true; _cpu.ClearClockEvent(_countAction); _cpu.AddClockEvent(_countAction, 0); @@ -453,7 +624,8 @@ public AvrTimer(Cpu cpu, AvrTimerConfig config) cpu.Mmio.RegisterWrite(config.TIFR, (value, _, _, _) => { - _cpu.Mmio.Data[config.TIFR] = value; + // TIFR flags are cleared by writing 1; writing 0 leaves them untouched (datasheet: + // "cleared by writing a logic one"), so the written value is never stored. _cpu.ClearInterruptByFlag(_ovf, value); _cpu.ClearInterruptByFlag(_ocfa, value); _cpu.ClearInterruptByFlag(_ocfb, value); @@ -471,6 +643,49 @@ public AvrTimer(Cpu cpu, AvrTimerConfig config) if (_hasCaptureInterrupt) _cpu.UpdateInterruptEnable(_capt!, value); return false; }); + + cpu.OnPeripheralReset += ResetFromCpu; + TryAttachIcp(); + } + + /// + /// CPU reset: the timer stops (no clock source until TCCRnB is written again), counters + /// and compare buffers clear, and the waveform mode returns to Normal. Pin overrides are + /// released by the GPIO reset, so only the local bookkeeping is cleared here. + /// + private void ResetFromCpu() + { + Reset(); + _countingUp = true; + _highByteTemp = 0; + _compA = 0; + _compB = 0; + _compC = 0; + if (_externalClockPort != null) + { + _externalClockPort.ExternalClockListeners[_config.ExternalClockPin] = null; + } + + UpdateWgmConfig(); + } + + private bool _icpAttached; + + /// + /// Hooks the ICPn pin (when the config models it) so an edge selected by ICESn (TCCRnB bit 6) + /// triggers an input capture. Retried on TCCRB writes in case the port was added after the timer. + /// + private void TryAttachIcp() + { + if (_icpAttached || !_hasCaptureInterrupt || _config.IcpPort == 0) return; + var port = _cpu.GpioByPort.GetValueOrDefault(_config.IcpPort); + if (port == null) return; + port.ExternalClockListeners[_config.IcpPin] = level => + { + var risingEdgeSelected = (_cpu.Mmio.Data[_config.TCCRB] & 0x40) != 0; + if (level == risingEdgeSelected) TriggerCapture(); + }; + _icpAttached = true; } private byte ReadTcnt(ushort addr) @@ -686,6 +901,11 @@ public void Count(bool reschedule, bool external = false) { UpdateCompPin(_compB, 'B', true); } + + if (_hasOcrC && _compC != 0) + { + UpdateCompPin(_compC, 'C', true); + } } if (_ocrUpdateMode == OcrUpdateMode.Bottom && overflow) @@ -1131,6 +1351,10 @@ public class AvrTimerConfig public readonly ushort ExternalClockPort; public readonly byte ExternalClockPin; + // Input capture pin (ICPn) and its port. Optional: 0 if the pin is not modelled. + public readonly ushort IcpPort; + public readonly byte IcpPin; + public AvrTimerConfig( byte bits = 0, int[]? dividers = null, @@ -1166,7 +1390,9 @@ public AvrTimerConfig( ushort comparatorPortC = 0, byte comparatorPinC = 0, ushort externalClockPort = 0, - byte externalClockPin = 0 + byte externalClockPin = 0, + ushort icpPort = 0, + byte icpPin = 0 ) { Bits = bits; @@ -1204,81 +1430,91 @@ public AvrTimerConfig( ComparatorPinC = comparatorPinC; ExternalClockPort = externalClockPort; ExternalClockPin = externalClockPin; + IcpPort = icpPort; + IcpPin = icpPin; } - public AvrTimerConfig CreateNew(byte bits = 0, + /// + /// Creates a copy of this config replacing only the values that are passed. A parameter left + /// null keeps the source value, so an explicit 0 (for example pin 0 or bit 0) + /// is honoured. + /// + public AvrTimerConfig CreateNew(byte? bits = null, int[]? dividers = null, - byte captureInterrupt = 0, - byte comparatorAInterrupt = 0, - byte comparatorBInterrupt = 0, - byte comparatorCInterrupt = 0, - byte overflowInterrupt = 0, - ushort tifr = 0, - ushort ocra = 0, - ushort ocrb = 0, - ushort ocrc = 0, - ushort icr = 0, - ushort tcnt = 0, - ushort tccra = 0, - ushort tccrb = 0, - ushort tccrc = 0, - ushort timsk = 0, - byte tov = 0, - byte ocfa = 0, - byte ocfb = 0, - byte ocfc = 0, - byte icf = 0, - byte toie = 0, - byte ociea = 0, - byte ocieb = 0, - byte ociec = 0, - byte icie = 0, - ushort comparatorPortA = 0, - byte comparatorPinA = 0, - ushort comparatorPortB = 0, - byte comparatorPinB = 0, - ushort comparatorPortC = 0, - byte comparatorPinC = 0, - ushort externalClockPort = 0, - byte externalClockPin = 0) + byte? captureInterrupt = null, + byte? comparatorAInterrupt = null, + byte? comparatorBInterrupt = null, + byte? comparatorCInterrupt = null, + byte? overflowInterrupt = null, + ushort? tifr = null, + ushort? ocra = null, + ushort? ocrb = null, + ushort? ocrc = null, + ushort? icr = null, + ushort? tcnt = null, + ushort? tccra = null, + ushort? tccrb = null, + ushort? tccrc = null, + ushort? timsk = null, + byte? tov = null, + byte? ocfa = null, + byte? ocfb = null, + byte? ocfc = null, + byte? icf = null, + byte? toie = null, + byte? ociea = null, + byte? ocieb = null, + byte? ociec = null, + byte? icie = null, + ushort? comparatorPortA = null, + byte? comparatorPinA = null, + ushort? comparatorPortB = null, + byte? comparatorPinB = null, + ushort? comparatorPortC = null, + byte? comparatorPinC = null, + ushort? externalClockPort = null, + byte? externalClockPin = null, + ushort? icpPort = null, + byte? icpPin = null) { - // The create new function should be used to create a new instance of the AvrTimerConfig class reusing the same values and replacing only the ones that are different return new AvrTimerConfig( - bits: bits == 0 ? Bits : bits, + bits: bits ?? Bits, dividers: dividers ?? Dividers, - captureInterrupt: captureInterrupt == 0 ? CaptureInterrupt : captureInterrupt, - comparatorAInterrupt: comparatorAInterrupt == 0 ? ComparatorAInterrupt : comparatorAInterrupt, - comparatorBInterrupt: comparatorBInterrupt == 0 ? ComparatorBInterrupt : comparatorBInterrupt, - comparatorCInterrupt: comparatorCInterrupt == 0 ? ComparatorCInterrupt : comparatorCInterrupt, - overflowInterrupt: overflowInterrupt == 0 ? OverflowInterrupt : overflowInterrupt, - tifr: tifr == 0 ? TIFR : tifr, - ocra: ocra == 0 ? OCRA : ocra, - ocrb: ocrb == 0 ? OCRB : ocrb, - ocrc: ocrc == 0 ? OCRC : ocrc, - icr: icr == 0 ? ICR : icr, - tcnt: tcnt == 0 ? TCNT : tcnt, - tccra: tccra == 0 ? TCCRA : tccra, - tccrb: tccrb == 0 ? TCCRB : tccrb, - tccrc: tccrc == 0 ? TCCRC : tccrc, - timsk: timsk == 0 ? TIMSK : timsk, - tov: tov == 0 ? TOV : tov, - ocfa: ocfa == 0 ? OCFA : ocfa, - ocfb: ocfb == 0 ? OCFB : ocfb, - ocfc: ocfc == 0 ? OCFC : ocfc, - icf: icf == 0 ? ICF : icf, - toie: toie == 0 ? TOIE : toie, - ociea: ociea == 0 ? OCIEA : ociea, - ocieb: ocieb == 0 ? OCIEB : ocieb, - ociec: ociec == 0 ? OCIEC : ociec, - icie: icie == 0 ? ICIE : icie, - comparatorPortA: comparatorPortA == 0 ? ComparatorPortA : comparatorPortA, - comparatorPinA: comparatorPinA == 0 ? ComparatorPinA : comparatorPinA, - comparatorPortB: comparatorPortB == 0 ? ComparatorPortB : comparatorPortB, - comparatorPinB: comparatorPinB == 0 ? ComparatorPinB : comparatorPinB, - comparatorPortC: comparatorPortC == 0 ? ComparatorPortC : comparatorPortC, - comparatorPinC: comparatorPinC == 0 ? ComparatorPinC : comparatorPinC, - externalClockPort: externalClockPort == 0 ? ExternalClockPort : externalClockPort, - externalClockPin: externalClockPin == 0 ? ExternalClockPin : externalClockPin + captureInterrupt: captureInterrupt ?? CaptureInterrupt, + comparatorAInterrupt: comparatorAInterrupt ?? ComparatorAInterrupt, + comparatorBInterrupt: comparatorBInterrupt ?? ComparatorBInterrupt, + comparatorCInterrupt: comparatorCInterrupt ?? ComparatorCInterrupt, + overflowInterrupt: overflowInterrupt ?? OverflowInterrupt, + tifr: tifr ?? TIFR, + ocra: ocra ?? OCRA, + ocrb: ocrb ?? OCRB, + ocrc: ocrc ?? OCRC, + icr: icr ?? ICR, + tcnt: tcnt ?? TCNT, + tccra: tccra ?? TCCRA, + tccrb: tccrb ?? TCCRB, + tccrc: tccrc ?? TCCRC, + timsk: timsk ?? TIMSK, + tov: tov ?? TOV, + ocfa: ocfa ?? OCFA, + ocfb: ocfb ?? OCFB, + ocfc: ocfc ?? OCFC, + icf: icf ?? ICF, + toie: toie ?? TOIE, + ociea: ociea ?? OCIEA, + ocieb: ocieb ?? OCIEB, + ociec: ociec ?? OCIEC, + icie: icie ?? ICIE, + comparatorPortA: comparatorPortA ?? ComparatorPortA, + comparatorPinA: comparatorPinA ?? ComparatorPinA, + comparatorPortB: comparatorPortB ?? ComparatorPortB, + comparatorPinB: comparatorPinB ?? ComparatorPinB, + comparatorPortC: comparatorPortC ?? ComparatorPortC, + comparatorPinC: comparatorPinC ?? ComparatorPinC, + externalClockPort: externalClockPort ?? ExternalClockPort, + externalClockPin: externalClockPin ?? ExternalClockPin, + icpPort: icpPort ?? IcpPort, + icpPin: icpPin ?? IcpPin ); } } diff --git a/src/Avr8Sharp/Core/Peripherals/TimerAttiny.cs b/src/Avr8Sharp/Core/Peripherals/TimerAttiny.cs new file mode 100644 index 0000000..1798954 --- /dev/null +++ b/src/Avr8Sharp/Core/Peripherals/TimerAttiny.cs @@ -0,0 +1,419 @@ +using AVR8Sharp.Core; + +namespace AVR8Sharp.Core.Peripherals; + +/// +/// Register map and wiring of the ATtiny25/45/85 Timer/Counter1. The defaults describe the +/// ATtiny85 (data-space addresses, that is I/O address + 0x20). +/// +public class AvrAttinyTimer1Config( + ushort tccr1 = 0x50, + ushort gtccr = 0x4c, + ushort tcnt1 = 0x4f, + ushort ocr1a = 0x4e, + ushort ocr1b = 0x4b, + ushort ocr1c = 0x4d, + ushort tifr = 0x58, + ushort timsk = 0x59, + byte overflowInterrupt = 0x04, + byte comparatorAInterrupt = 0x03, + byte comparatorBInterrupt = 0x09, + byte tov1 = 1 << 2, + byte ocf1a = 1 << 6, + byte ocf1b = 1 << 5, + byte toie1 = 1 << 2, + byte ocie1a = 1 << 6, + byte ocie1b = 1 << 5, + ushort comparatorPort = 0x38, + byte comparatorPinA = 1, + byte comparatorPinB = 4, + int[]? dividers = null) +{ + public readonly ushort TCCR1 = tccr1; + public readonly ushort GTCCR = gtccr; + public readonly ushort TCNT1 = tcnt1; + public readonly ushort OCR1A = ocr1a; + public readonly ushort OCR1B = ocr1b; + public readonly ushort OCR1C = ocr1c; + public readonly ushort TIFR = tifr; + public readonly ushort TIMSK = timsk; + + public readonly byte OverflowInterrupt = overflowInterrupt; + public readonly byte ComparatorAInterrupt = comparatorAInterrupt; + public readonly byte ComparatorBInterrupt = comparatorBInterrupt; + + // TIFR bits + public readonly byte TOV1 = tov1; + public readonly byte OCF1A = ocf1a; + public readonly byte OCF1B = ocf1b; + + // TIMSK bits + public readonly byte TOIE1 = toie1; + public readonly byte OCIE1A = ocie1a; + public readonly byte OCIE1B = ocie1b; + + /// PORT register of the port that carries OC1A / OC1B (PORTB on the ATtiny85). + public readonly ushort ComparatorPort = comparatorPort; + public readonly byte ComparatorPinA = comparatorPinA; + public readonly byte ComparatorPinB = comparatorPinB; + + /// CS13:CS10 to prescaler: 0 stops the timer, n selects 2^(n-1). + public readonly int[] Dividers = dividers ?? DefaultDividers; + + public static readonly int[] DefaultDividers = + [ + 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, + ]; +} + +/// +/// ATtiny25/45/85 Timer/Counter1 (8-bit, with OCR1C as programmable TOP). +/// +/// This chip's TC1 is not the ATmega timer: a single TCCR1 (CTC1, PWM1A, COM1A, CS13:0), +/// a GTCCR holding PWM1B and COM1B, a third compare register OCR1C, and prescalers up to +/// /16384. Ported from avr8js timer-attiny.ts (commit 82f9fa5). Differences from the +/// upstream port: +/// +/// PWM mode is single slope (count 0..OCR1C, reset after the OCR1C match), as in the +/// datasheet, instead of phase correct. +/// OCR1C resets to 0xFF. +/// A TCNT1 above TOP counts up to 0xFF before wrapping, like the hardware does. +/// TIFR writes only touch the TC1 flags, so the register can be shared with Timer0. +/// +/// The complementary outputs of COM1x = 01 in PWM mode (PB0 and PB3) are not driven. +/// +/// +public class AvrAttinyTimer1 +{ + // TCCR1 bits + private const int CTC1 = 1 << 7; + private const int PWM1A = 1 << 6; + private const int CS_MASK = 0x0f; + + // GTCCR bits + private const int PWM1B = 1 << 6; + private const int FOC1B = 1 << 3; + private const int FOC1A = 1 << 2; + private const int PSR1 = 1 << 1; + private const int PSR0 = 1 << 0; + + private readonly Cpu _cpu; + private readonly AvrAttinyTimer1Config _config; + private readonly Action _countAction; + + private ulong _lastCycle; + private int _tcnt; + private int _tcntNext; + private bool _tcntUpdated; + private byte _ocrA; + private byte _ocrB; + private byte _ocrC = 0xff; + private int _divider; + private bool _updateDivider; + + private readonly AvrInterruptConfig _ovf; + private readonly AvrInterruptConfig _ocfa; + private readonly AvrInterruptConfig _ocfb; + + public AvrAttinyTimer1(Cpu cpu, AvrAttinyTimer1Config? config = null) + { + _cpu = cpu; + _config = config ?? new AvrAttinyTimer1Config(); + _countAction = () => Count(true); + + _ovf = new AvrInterruptConfig( + address: _config.OverflowInterrupt, + enableRegister: _config.TIMSK, + enableMask: _config.TOIE1, + flagRegister: _config.TIFR, + flagMask: _config.TOV1); + _ocfa = new AvrInterruptConfig( + address: _config.ComparatorAInterrupt, + enableRegister: _config.TIMSK, + enableMask: _config.OCIE1A, + flagRegister: _config.TIFR, + flagMask: _config.OCF1A); + _ocfb = new AvrInterruptConfig( + address: _config.ComparatorBInterrupt, + enableRegister: _config.TIMSK, + enableMask: _config.OCIE1B, + flagRegister: _config.TIFR, + flagMask: _config.OCF1B); + + cpu.Mmio.Data[_config.OCR1C] = 0xff; + cpu.OnPeripheralReset += Reset; + + cpu.Mmio.RegisterRead(_config.TCNT1, _ => + { + Count(false); + return _cpu.Mmio.Data[_config.TCNT1] = (byte)_tcnt; + }); + + cpu.Mmio.RegisterWrite(_config.TCNT1, (value, _, _, _) => + { + _tcntNext = value; + _tcntUpdated = true; + _cpu.UpdateClockEvent(_countAction, 0); + if (_divider != 0) + { + TimerUpdated(_tcntNext, _tcntNext, false); + } + + return false; + }); + + cpu.Mmio.RegisterWrite(_config.OCR1A, (value, _, _, _) => { _ocrA = value; return false; }); + cpu.Mmio.RegisterWrite(_config.OCR1B, (value, _, _, _) => { _ocrB = value; return false; }); + cpu.Mmio.RegisterWrite(_config.OCR1C, (value, _, _, _) => { _ocrC = value; return false; }); + + cpu.Mmio.RegisterWrite(_config.TCCR1, (value, _, _, _) => + { + // Bring the counter up to date with the old mode before the new one applies. + Count(false); + _cpu.Mmio.Data[_config.TCCR1] = value; + _updateDivider = true; + _cpu.ClearClockEvent(_countAction); + _cpu.AddClockEvent(_countAction, 0); + UpdateCompConfig(); + return true; + }); + + cpu.Mmio.RegisterWrite(_config.GTCCR, (value, _, _, _) => + { + Count(false); + if ((value & PSR1) != 0) + { + _lastCycle = _cpu.Cycles; + } + + // FOC1x and PSRx are strobes and always read as zero. + _cpu.Mmio.Data[_config.GTCCR] = (byte)(value & ~(FOC1A | FOC1B | PSR1 | PSR0)); + UpdateCompConfig(); + + if ((value & FOC1A) != 0) ForceCompare(true); + if ((value & FOC1B) != 0) ForceCompare(false); + return true; + }); + + // TIFR and TIMSK are shared with Timer0: only touch the TC1 bits. + cpu.Mmio.RegisterWrite(_config.TIFR, (value, oldValue, _, _) => + { + // A hook that ran before this one may have overwritten the whole register. + var mine = _config.TOV1 | _config.OCF1A | _config.OCF1B; + _cpu.Mmio.Data[_config.TIFR] = (byte)((_cpu.Mmio.Data[_config.TIFR] & ~mine) | (oldValue & mine)); + _cpu.ClearInterruptByFlag(_ovf, value); + _cpu.ClearInterruptByFlag(_ocfa, value); + _cpu.ClearInterruptByFlag(_ocfb, value); + return true; + }); + + cpu.Mmio.RegisterWrite(_config.TIMSK, (value, _, _, _) => + { + _cpu.UpdateInterruptEnable(_ovf, value); + _cpu.UpdateInterruptEnable(_ocfa, value); + _cpu.UpdateInterruptEnable(_ocfb, value); + return false; + }); + } + + private byte Tccr1 => _cpu.Mmio.Data[_config.TCCR1]; + private byte Gtccr => _cpu.Mmio.Data[_config.GTCCR]; + private int CS => Tccr1 & CS_MASK; + private bool CtcMode => (Tccr1 & CTC1) != 0; + private bool PwmA => (Tccr1 & PWM1A) != 0; + private bool PwmB => (Gtccr & PWM1B) != 0; + private int ComA => (Tccr1 >> 4) & 0x3; + private int ComB => (Gtccr >> 4) & 0x3; + + /// TOP is OCR1C in CTC and PWM modes, 0xFF otherwise. + private int Top => CtcMode || PwmA || PwmB ? _ocrC : 0xff; + + public int DebugTCNT => _tcnt; + + public void Reset() + { + _divider = 0; + _lastCycle = 0; + _tcnt = 0; + _tcntNext = 0; + _tcntUpdated = false; + _updateDivider = true; + _ocrA = 0; + _ocrB = 0; + _ocrC = 0xff; + _cpu.Mmio.Data[_config.TCCR1] = 0; + _cpu.Mmio.Data[_config.GTCCR] = 0; + _cpu.Mmio.Data[_config.TCNT1] = 0; + _cpu.Mmio.Data[_config.OCR1A] = 0; + _cpu.Mmio.Data[_config.OCR1B] = 0; + _cpu.Mmio.Data[_config.OCR1C] = 0xff; + } + + public void Count(bool reschedule) + { + var delta = _cpu.Cycles - _lastCycle; + + if (_divider != 0 && delta >= (ulong)_divider) + { + var counterDelta = delta / (ulong)_divider; + _lastCycle += counterDelta * (ulong)_divider; + var prev = _tcnt; + var top = Top; + var overflow = false; + int newVal; + + if (prev > top && counterDelta < (ulong)(0x100 - prev)) + { + // Past TOP (TCNT1 written above OCR1C): the counter only wraps at 0xFF. + newVal = prev + (int)counterDelta; + } + else + { + var start = (ulong)prev; + var steps = counterDelta; + if (prev > top) + { + steps -= (ulong)(0x100 - prev); + start = 0; + overflow = true; + } + + var total = start + steps; + if (total > (ulong)top) overflow = true; + newVal = (int)(total % (ulong)(top + 1)); + } + + if (!_tcntUpdated) + { + _tcnt = newVal; + TimerUpdated(newVal, prev, overflow); + } + + if (overflow) + { + _cpu.SetInterruptFlag(_ovf); + } + } + + if (_tcntUpdated) + { + _tcnt = _tcntNext; + _tcntUpdated = false; + } + + if (_updateDivider) + { + var newDivider = _config.Dividers.Length > CS ? _config.Dividers[CS] : 0; + _lastCycle = newDivider != 0 ? _cpu.Cycles : 0; + _updateDivider = false; + _divider = newDivider; + if (newDivider != 0) + { + _cpu.AddClockEvent(_countAction, (int)(_lastCycle + (ulong)newDivider - _cpu.Cycles)); + } + + return; + } + + if (reschedule && _divider != 0) + { + _cpu.AddClockEvent(_countAction, (int)(_lastCycle + (ulong)_divider - _cpu.Cycles)); + } + } + + private void TimerUpdated(int value, int prev, bool wrapped) + { + var top = Top; + UpdateChannel(true, value, prev, wrapped, top); + UpdateChannel(false, value, prev, wrapped, top); + } + + private void UpdateChannel(bool channelA, int value, int prev, bool wrapped, int top) + { + var ocr = channelA ? _ocrA : _ocrB; + var com = channelA ? ComA : ComB; + var pwm = channelA ? PwmA : PwmB; + var reached = ocr <= top && CompareReached(value, prev, ocr); + + if (reached) + { + _cpu.SetInterruptFlag(channelA ? _ocfa : _ocfb); + } + + if (com == 0) return; + + if (!pwm) + { + if (reached) DriveCompareMatch(channelA, com); + return; + } + + // Single slope PWM: the output takes its "bottom" level when the counter restarts and + // flips to the "match" level at OCR1x. COM1x1:0 = 11 is the inverting mode. + var inverting = com == 3; + if (wrapped) + { + var matchedAfterWrap = ocr <= top && value >= ocr; + SetPin(channelA, matchedAfterWrap != inverting ? PinOverrideMode.Clear : PinOverrideMode.Set); + } + else if (reached) + { + SetPin(channelA, inverting ? PinOverrideMode.Set : PinOverrideMode.Clear); + } + } + + private static bool CompareReached(int value, int prev, int ocr) + { + var overflow = prev > value; + return (prev < ocr || overflow) && value >= ocr || prev < ocr && overflow; + } + + private void ForceCompare(bool channelA) + { + var com = channelA ? ComA : ComB; + var pwm = channelA ? PwmA : PwmB; + if (!pwm && com != 0) + { + DriveCompareMatch(channelA, com); + } + } + + private void DriveCompareMatch(bool channelA, int com) + { + var mode = com switch + { + 1 => PinOverrideMode.Toggle, + 2 => PinOverrideMode.Clear, + 3 => PinOverrideMode.Set, + _ => PinOverrideMode.None, + }; + if (mode != PinOverrideMode.None) SetPin(channelA, mode); + } + + private void SetPin(bool channelA, PinOverrideMode mode) + { + if (_cpu.GpioByPort.TryGetValue(_config.ComparatorPort, out var port)) + { + port.TimerOverridePin(channelA ? _config.ComparatorPinA : _config.ComparatorPinB, mode); + } + } + + private void UpdateCompConfig() + { + if (!_cpu.GpioByPort.TryGetValue(_config.ComparatorPort, out var port)) return; + port.TimerOverridePin(_config.ComparatorPinA, ComA != 0 ? PinOverrideMode.Enable : PinOverrideMode.None); + port.TimerOverridePin(_config.ComparatorPinB, ComB != 0 ? PinOverrideMode.Enable : PinOverrideMode.None); + ApplyPwmLevel(true); + ApplyPwmLevel(false); + } + + /// Puts a freshly enabled PWM output at the level the waveform has at the current count. + private void ApplyPwmLevel(bool channelA) + { + var com = channelA ? ComA : ComB; + if (com == 0 || !(channelA ? PwmA : PwmB)) return; + var ocr = channelA ? _ocrA : _ocrB; + var matched = ocr <= Top && _tcnt >= ocr; + SetPin(channelA, matched != (com == 3) ? PinOverrideMode.Clear : PinOverrideMode.Set); + } +} diff --git a/src/Avr8Sharp/Core/Peripherals/Twi.cs b/src/Avr8Sharp/Core/Peripherals/Twi.cs index d3e1c57..38c283e 100644 --- a/src/Avr8Sharp/Core/Peripherals/Twi.cs +++ b/src/Avr8Sharp/Core/Peripherals/Twi.cs @@ -57,6 +57,19 @@ public class AvrTwi TWAMR = 0xbd }; + /// ATmega2560 TWI: same registers as the ATmega328P; TWI is vector 39 -> word 0x4E. + public static readonly AvrTwiConfig Mega2560TwiConfig = new() + { + TwiInterrupt = 0x4e, + + TWBR = 0xb8, + TWSR = 0xb9, + TWAR = 0xba, + TWDR = 0xbb, + TWCR = 0xbc, + TWAMR = 0xbd + }; + private readonly Cpu _cpu; private readonly AvrTwiConfig _config; private readonly uint _freqHz; @@ -161,6 +174,11 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz) _deferred = null; _busy = false; _eventWireCycles = 0; + // TWSR resets to idle (TWS = 0xF8) and TWDR to 0xFF (datasheet register summary). + // TWAR is left at 0, which this model reads as "not a slave". + _cpu.Mmio.Data[_config.TWSR] = STATUS_TWI_IDLE; + _cpu.Mmio.Data[_config.TWDR] = 0xff; + SlaveAddressChanged?.Invoke(0); }; cpu.Mmio.RegisterWrite(_config.TWAR, (value, _, _, _) => @@ -172,7 +190,8 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz) cpu.Mmio.RegisterWrite(_config.TWCR, (value, _, _, _) => { - _cpu.Mmio.Data[_config.TWCR] = value; + // TWINT is cleared by writing a one; writing zero must not drop a pending flag. + _cpu.Mmio.Data[_config.TWCR] = (byte)((value & ~TWCR_TWINT) | (_cpu.Mmio.Data[_config.TWCR] & TWCR_TWINT)); var clearInt = (value & TWCR_TWINT) != 0; _cpu.ClearInterruptByFlag(_twi, value); _cpu.UpdateInterruptEnable(_twi, value); @@ -211,7 +230,7 @@ public AvrTwi(Cpu cpu, AvrTwiConfig config, uint freqHz) return true; } - return false; + return true; }); } diff --git a/src/Avr8Sharp/Core/Peripherals/Usart.cs b/src/Avr8Sharp/Core/Peripherals/Usart.cs index 788f942..600818e 100644 --- a/src/Avr8Sharp/Core/Peripherals/Usart.cs +++ b/src/Avr8Sharp/Core/Peripherals/Usart.cs @@ -50,6 +50,65 @@ public class AvrUsart UDR = 0xc6, }; + // ── ATmega2560 ────────────────────────────────────────────────────────── + // Naming: Mega2560UsartConfig. Interrupt values are word indices (avr-libc vector number x 2). + + /// ATmega2560 USART0: registers 0xC0-0xC6; RX 0x32, UDRE 0x34, TX 0x36. + public static readonly AvrUsartConfig Mega2560Usart0Config = new AvrUsartConfig + { + RxCompleteInterrupt = 0x32, + DataRegisterEmptyInterrupt = 0x34, + TxCompleteInterrupt = 0x36, + UCSRA = 0xc0, + UCSRB = 0xc1, + UCSRC = 0xc2, + UBRRL = 0xc4, + UBRRH = 0xc5, + UDR = 0xc6, + }; + + /// ATmega2560 USART1: registers 0xC8-0xCE; RX 0x48, UDRE 0x4A, TX 0x4C. + public static readonly AvrUsartConfig Mega2560Usart1Config = new AvrUsartConfig + { + RxCompleteInterrupt = 0x48, + DataRegisterEmptyInterrupt = 0x4a, + TxCompleteInterrupt = 0x4c, + UCSRA = 0xc8, + UCSRB = 0xc9, + UCSRC = 0xca, + UBRRL = 0xcc, + UBRRH = 0xcd, + UDR = 0xce, + }; + + /// ATmega2560 USART2: registers 0xD0-0xD6; RX 0x66, UDRE 0x68, TX 0x6A. + public static readonly AvrUsartConfig Mega2560Usart2Config = new AvrUsartConfig + { + RxCompleteInterrupt = 0x66, + DataRegisterEmptyInterrupt = 0x68, + TxCompleteInterrupt = 0x6a, + UCSRA = 0xd0, + UCSRB = 0xd1, + UCSRC = 0xd2, + UBRRL = 0xd4, + UBRRH = 0xd5, + UDR = 0xd6, + }; + + /// ATmega2560 USART3: registers 0x130-0x136; RX 0x6C, UDRE 0x6E, TX 0x70. + public static readonly AvrUsartConfig Mega2560Usart3Config = new AvrUsartConfig + { + RxCompleteInterrupt = 0x6c, + DataRegisterEmptyInterrupt = 0x6e, + TxCompleteInterrupt = 0x70, + UCSRA = 0x130, + UCSRB = 0x131, + UCSRC = 0x132, + UBRRL = 0x134, + UBRRH = 0x135, + UDR = 0x136, + }; + public static Dictionary RxMasks { get; } = new Dictionary { { 5, 0x1f }, @@ -222,6 +281,11 @@ public AvrUsart(Cpu cpu, AvrUsartConfig config, uint freqHz) Reset(); UpdateCalculatedValues(); + cpu.OnPeripheralReset += () => + { + Reset(); + UpdateCalculatedValues(); + }; cpu.Mmio.RegisterWrite(_config.UCSRA, (value, oldValue, _, _) => { @@ -251,8 +315,11 @@ public AvrUsart(Cpu cpu, AvrUsartConfig config, uint freqHz) _cpu.UpdateInterruptEnable(_rxc, value); _cpu.UpdateInterruptEnable(_udre, value); _cpu.UpdateInterruptEnable(_txc, value); - if ((value & UCSRB_RXEN) != 0 && (oldValue & UCSRB_RXEN) != 0) + // Disabling the receiver (RXEN 1->0) flushes the receive buffer and RXC + // (datasheet 19.7.3). Other UCSRB writes (e.g. toggling UDRIE) must keep a pending RXC. + if ((value & UCSRB_RXEN) == 0 && (oldValue & UCSRB_RXEN) != 0) { + _rxBuffer = 0; _cpu.ClearInterrupt(_rxc); } @@ -348,6 +415,9 @@ public void Reset() _cpu.Mmio.Data[_config.UCSRC] = UCSRC_UCSZ1 | UCSRC_UCSZ0; // default: 8 bits per byte _rxBusyValue = false; _rxBuffer = 0; + _incomingRxBuffer = 0; + _incomingFrameError = false; + _incomingParityError = false; _lineBuffer.Clear(); } diff --git a/src/Avr8Sharp/Core/Peripherals/Usi.cs b/src/Avr8Sharp/Core/Peripherals/Usi.cs index a174ca1..27d3bcb 100644 --- a/src/Avr8Sharp/Core/Peripherals/Usi.cs +++ b/src/Avr8Sharp/Core/Peripherals/Usi.cs @@ -71,6 +71,8 @@ public AvrUsi(Cpu cpu, AvrIoPort port, int portPin, int dataPin, int clockPin) _cpu.Mmio.RegisterWrite(USISR, DelegateWriteHookUsisr); _cpu.Mmio.RegisterWrite(USICR, DelegateWriteHookUsicr); + + _cpu.OnPeripheralReset += () => _port.OpenCollector = 0; } private void DelegatePortListener(byte value, byte oldValue) diff --git a/src/Avr8Sharp/Core/Peripherals/Watchdog.cs b/src/Avr8Sharp/Core/Peripherals/Watchdog.cs index 7d4edcf..51861b7 100644 --- a/src/Avr8Sharp/Core/Peripherals/Watchdog.cs +++ b/src/Avr8Sharp/Core/Peripherals/Watchdog.cs @@ -30,6 +30,15 @@ public class AvrWatchdog WDTCSR = 0x60 }; + /// ATmega2560 watchdog: MCUSR 0x54 and WDTCSR 0x60 as on the ATmega328P; WDT is vector 12 -> word 0x18. + public static readonly AvrWatchdogConfig Mega2560WatchdogConfig = new AvrWatchdogConfig + { + WatchdogInterrupt = 0x18, + + MCUSR = 0x54, + WDTCSR = 0x60 + }; + readonly long _clockFrequency = 128_000; private readonly Cpu _cpu; @@ -91,13 +100,20 @@ public AvrWatchdog(Cpu cpu, AvrWatchdogConfig config, AvrClock clock) // 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.PreserveOnReset(config.MCUSR); _cpu.OnPeripheralReset += () => { + // WDTCSR was zeroed by Cpu.Reset: the watchdog is off and its timer event is gone + _enabledValue = false; + _scheduled = false; if (!_watchdogReset) _cpu.Mmio.Data[config.MCUSR] |= MCUSR_EXTRF; }; _cpu.Mmio.RegisterWrite(config.WDTCSR, (value, oldValue, _, _) => { + // WDIF is cleared by writing a one (at the end of this hook); writing zero keeps it. + var written = value; + value = (byte)((value & ~WDTCSR_WDIF) | (oldValue & WDTCSR_WDIF)); if ((value & WDTCSR_WDCE) != 0 && (value & WDTCSR_WDE) != 0) { _changeEnabledCycles = _cpu.Cycles + 4; @@ -127,7 +143,7 @@ public AvrWatchdog(Cpu cpu, AvrWatchdogConfig config, AvrClock clock) _scheduled = false; } - _cpu.ClearInterruptByFlag(_watchdog, value); + _cpu.ClearInterruptByFlag(_watchdog, written); return true; }); } diff --git a/src/Avr8Sharp/Core/Utils/Builder.cs b/src/Avr8Sharp/Core/Utils/Builder.cs index f06f3b9..16f3b23 100644 --- a/src/Avr8Sharp/Core/Utils/Builder.cs +++ b/src/Avr8Sharp/Core/Utils/Builder.cs @@ -11,6 +11,13 @@ public static AvrBuilder Create (int flashSize = 0x8000, int ramSize = 8192) return new AvrBuilder (new AvrRunner (new byte[flashSize], ramSize), flashSize); } + /// Sets the chip's first SRAM address (see ). + public AvrBuilder WithRamStart (int ramStart) + { + @base.Cpu.RamStart = ramStart; + return this; + } + public AvrBuilder SetSpeed(uint speed) { @base.SetSpeed (speed); @@ -67,7 +74,7 @@ public AvrBuilder AddTwi(AvrTwiConfig config, out AvrTwi twim) public AvrBuilder AddEeprom(AvrEepromConfig config, IEepromBackend backend, out AvrEeprom eeprom) { - eeprom = new AvrEeprom (@base.Cpu, backend, config); + eeprom = new AvrEeprom (@base.Cpu, backend, config, @base.Speed); return this; } diff --git a/src/Avr8Sharp/Core/Utils/IntelHex.cs b/src/Avr8Sharp/Core/Utils/IntelHex.cs new file mode 100644 index 0000000..32db121 --- /dev/null +++ b/src/Avr8Sharp/Core/Utils/IntelHex.cs @@ -0,0 +1,119 @@ +namespace AVR8Sharp.Core.Utils; + +/// +/// Result of parsing an Intel HEX image. is the highest byte address +/// the image writes (inclusive, 0 when it writes nothing), the number of +/// data bytes it carries, and every problem found (empty when the image is clean). +/// +public sealed record HexInfo (int MaxAddress, int ByteCount, IReadOnlyList Errors); + +/// +/// Intel HEX parser shared by and everything built on it. Handles data +/// (00), EOF (01), extended segment address (02) and extended linear address (04) records, +/// ignores start-address records (03, 05) and verifies every record checksum. It never throws on +/// malformed input; problems are reported in . +/// +public static class IntelHex +{ + /// + /// Parses into . Bytes that fall outside + /// the target are dropped and reported as an error. When is false a + /// record with a bad checksum is still applied (it is reported either way). + /// + public static HexInfo Parse (string source, byte[] target, bool strict = true) + { + var errors = new List (); + long maxAddress = -1; + var byteCount = 0; + long baseAddress = 0; + var lineNo = 0; + var overflowReported = false; + Span record = stackalloc byte[5 + 255]; + + foreach (var rawLine in source.AsSpan ().EnumerateLines ()) { + lineNo++; + var line = rawLine.Trim (); + if (line.Length == 0) continue; + if (line[0] != ':') { + errors.Add ($"line {lineNo}: missing ':' record mark"); + continue; + } + var hexChars = line.Length - 1; + if (hexChars < 10 || (hexChars & 1) != 0 || hexChars / 2 > record.Length) { + errors.Add ($"line {lineNo}: bad record length"); + continue; + } + var total = hexChars / 2; + var bad = false; + for (var i = 0; i < total; i++) { + var hi = Digit (line[1 + i * 2]); + var lo = Digit (line[2 + i * 2]); + if (hi < 0 || lo < 0) { + bad = true; + break; + } + record[i] = (byte)(hi << 4 | lo); + } + if (bad) { + errors.Add ($"line {lineNo}: non-hex character"); + continue; + } + var count = record[0]; + if (total != 5 + count) { + errors.Add ($"line {lineNo}: length field {count} does not match record size"); + continue; + } + var sum = 0; + for (var i = 0; i < total; i++) sum += record[i]; + if ((sum & 0xff) != 0) { + errors.Add ($"line {lineNo}: checksum mismatch"); + if (strict) continue; + } + var address = record[1] << 8 | record[2]; + var type = record[3]; + var data = record.Slice (4, count); + + switch (type) { + case 0x00: + for (var i = 0; i < count; i++) { + var addr = baseAddress + address + i; + if (addr > maxAddress) maxAddress = addr; + byteCount++; + if (addr < target.Length) { + target[addr] = data[i]; + } else if (!overflowReported) { + errors.Add ($"line {lineNo}: address 0x{addr:X} is beyond the {target.Length}-byte flash"); + overflowReported = true; + } + } + break; + case 0x01: + return Finish (); + case 0x02: + if (count != 2) errors.Add ($"line {lineNo}: extended segment record needs 2 data bytes"); + else baseAddress = (long)(data[0] << 8 | data[1]) << 4; + break; + case 0x04: + if (count != 2) errors.Add ($"line {lineNo}: extended linear record needs 2 data bytes"); + else baseAddress = (long)(data[0] << 8 | data[1]) << 16; + break; + case 0x03: + case 0x05: + break; + default: + errors.Add ($"line {lineNo}: unknown record type 0x{type:X2}"); + break; + } + } + return Finish (); + + HexInfo Finish () => new (maxAddress < 0 ? 0 : (int)Math.Min (maxAddress, int.MaxValue), byteCount, errors); + } + + static int Digit (char c) => c switch { + >= '0' and <= '9' => c - '0', + >= 'A' and <= 'F' => c - 'A' + 10, + >= 'a' and <= 'f' => c - 'a' + 10, + _ => -1, + }; +} diff --git a/src/Avr8Sharp/Core/Utils/Runner.cs b/src/Avr8Sharp/Core/Utils/Runner.cs index 2b172a4..2cf1e39 100644 --- a/src/Avr8Sharp/Core/Utils/Runner.cs +++ b/src/Avr8Sharp/Core/Utils/Runner.cs @@ -1,4 +1,3 @@ -using System.Globalization; using System.Runtime.CompilerServices; using AVR8Sharp.Core; using AVR8Sharp.Core.Decoders; @@ -44,84 +43,131 @@ public void LoadProgram (ushort[] program) Cpu.LoadProgram (program); } + /// + /// Loads an Intel HEX image into flash (data, extended segment/linear address and EOF + /// records). Lenient: malformed lines are skipped, bytes beyond the flash are dropped and + /// checksums are not enforced. Use to find out whether the image was clean. + /// public void LoadHex (string source) { - var flashSize = Cpu.ProgBytes.Length; - var target = new byte[flashSize]; - - foreach (var line in source.AsSpan().EnumerateLines()) - { - var trimmedLine = line.Trim(); - - if (trimmedLine.Length < 11 || trimmedLine[0] != ':') continue; + var target = new byte[Cpu.ProgBytes.Length]; + IntelHex.Parse (source, target, strict: false); + Cpu.LoadProgram (target); + } - if (trimmedLine.Slice(7, 2) is not "00") continue; + /// + /// Loads an Intel HEX image into flash only if it is clean and fits in + /// Cpu.ProgBytes.Length bytes. Returns false, leaving flash untouched, when a record is + /// malformed, a checksum fails or the image reaches past the end of flash; the reasons are in + /// . + /// + public bool TryLoadHex (string hex, out HexInfo info) + { + var target = new byte[Cpu.ProgBytes.Length]; + info = IntelHex.Parse (hex, target); + if (info.Errors.Count > 0) return false; + Cpu.LoadProgram (target); + return true; + } - var bytes = int.Parse(trimmedLine.Slice(1, 2), NumberStyles.HexNumber); + private ulong _runTarget; + private ulong _runEnd; + + /// + /// Runs whole instructions until more cycles of simulated time + /// have elapsed, carrying the overshoot: the target advances from the previous target, not from + /// where the core stopped, so repeated small budgets do not drift (a 16-cycle budget spent on + /// 2-cycle instructions no longer runs ~2 % fast). A core advanced by other means (or reset) + /// re-bases the target on its cycle counter. Returns the cycles actually consumed. + /// + public long Run (long cycleBudget) + { + if (Cpu.Cycles != _runEnd) _runTarget = Cpu.Cycles; + _runTarget += (ulong)Math.Max (0, cycleBudget); + return RunToTarget (); + } - if (trimmedLine.Length < 11 + (bytes * 2)) continue; + /// Same as ; the name used by the SiliconTwin branch. + public void RunCycles (long cycles) => Run (cycles); - var addr = int.Parse(trimmedLine.Slice(3, 4), NumberStyles.HexNumber); + /// + /// Runs until the cycle counter reaches (to within one + /// instruction) and returns the cycles consumed. A later continues from + /// that target, so the overshoot is carried. + /// + public long RunUntil (ulong absoluteCycle) + { + if (Cpu.Cycles != _runEnd) _runTarget = Cpu.Cycles; + if (absoluteCycle > _runTarget) _runTarget = absoluteCycle; + return RunToTarget (); + } - for (var i = 0; i < bytes; i++) - { - if (addr + i < target.Length) - { - target[addr + i] = byte.Parse(trimmedLine.Slice(9 + i * 2, 2), NumberStyles.HexNumber); - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private long RunToTarget () + { + var start = Cpu.Cycles; + switch (_activeDecoder) { + case DecoderType.Switch: + RunLoop (ref _switchDecoder, _runTarget); + break; + case DecoderType.Lut: + RunLoop (ref _lutDecoder, _runTarget); + break; + case DecoderType.NativeLut: + RunLoop (ref _nativeLutDecoder, _runTarget); + break; + default: + throw new NotImplementedException ("Decoder type not implemented"); } + _runEnd = Cpu.Cycles; + return (long)(Cpu.Cycles - start); + } - Cpu.LoadProgram (target); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RunLoop (ref TDecoder decoder, ulong target) where TDecoder : struct, IInstructionDecoder + { + var cpu = Cpu; + while (cpu.Cycles < target) { + decoder.Decode (cpu); + cpu.Tick (); + } } public void Execute (ref TDecoder decoder, Action callback) where TDecoder : struct, IInstructionDecoder { - var cyclesToRun = Cpu.Cycles + (ulong)_workUnitCycles; - while (Cpu.Cycles < cyclesToRun) { - decoder.Decode (Cpu); - Cpu.Tick (); - } + ExecuteInternal (ref decoder); callback.Invoke (Cpu); } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ExecuteInternal (ref TDecoder decoder) where TDecoder : struct, IInstructionDecoder { - var cpu = Cpu; - var cyclesToRun = cpu.Cycles + (ulong)_workUnitCycles; - while (cpu.Cycles < cyclesToRun) { - decoder.Decode (cpu); - cpu.Tick (); - } + if (Cpu.Cycles != _runEnd) _runTarget = Cpu.Cycles; + _runTarget += (ulong)_workUnitCycles; + RunLoop (ref decoder, _runTarget); + _runEnd = Cpu.Cycles; } public void ExecuteProfiling(ProfilingDecoder decoder) { var cpu = Cpu; - var cyclesToRun = cpu.Cycles + (ulong)_workUnitCycles; - while (cpu.Cycles < cyclesToRun) + if (cpu.Cycles != _runEnd) _runTarget = cpu.Cycles; + _runTarget += (ulong)_workUnitCycles; + while (cpu.Cycles < _runTarget) { decoder.Decode(cpu); cpu.Tick(); } + _runEnd = cpu.Cycles; } + /// + /// Runs one work unit (). Cumulative like , + /// so the per-call overshoot is carried instead of accumulating as clock drift. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Execute() { - switch(_activeDecoder) { - case DecoderType.Switch: - ExecuteInternal (ref _switchDecoder); - break; - case DecoderType.Lut: - ExecuteInternal (ref _lutDecoder); - break; - case DecoderType.NativeLut: - ExecuteInternal (ref _nativeLutDecoder); - break; - default: - throw new NotImplementedException("Decoder type not implemented"); - } + Run (_workUnitCycles); } } diff --git a/tests/Avr8Sharp.TestKit.Samples/ATtiny85PeripheralTests.cs b/tests/Avr8Sharp.TestKit.Samples/ATtiny85PeripheralTests.cs new file mode 100644 index 0000000..66e9f6f --- /dev/null +++ b/tests/Avr8Sharp.TestKit.Samples/ATtiny85PeripheralTests.cs @@ -0,0 +1,143 @@ +using Avr8Sharp.TestKit; +using Avr8Sharp.TestKit.Boards; + +namespace Avr8Sharp.TestKit.Samples; + +/// +/// ATtiny85 peripheral wiring: EEPROM address registers, Timer1 and INT0. +/// +[TestFixture] +public class ATtiny85PeripheralTests +{ + private const int EECR = 0x3C; + private const int EEDR = 0x3D; + private const int TIFR = 0x58; + private const int GIFR = 0x5A; + private const int TCNT1 = 0x4F; + + [Test] + public void Eeprom_WriteWithInterruptsEnabled_DoesNotReadR0AsAddressHigh() + { + // avr-libc's eeprom_write_byte saves SREG in r0 before the timed sequence. With + // interrupts enabled r0 is 0x80, which must not leak into the EEPROM address. + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + sei + in r0, 0x3F ; r0 = SREG = 0x80 + ldi r16, 0x00 + out 0x1F, r16 ; EEARH + ldi r16, 0x05 + out 0x1E, r16 ; EEARL = 5 + ldi r16, 0x42 + out 0x1D, r16 ; EEDR + ldi r16, 0x04 + out 0x1C, r16 ; EEMPE + ldi r16, 0x02 + out 0x1C, r16 ; EEPE + done: rjmp done + "); + + Assert.DoesNotThrow(() => tiny.RunInstructions(20)); + tiny.RunMilliseconds(10); + + tiny.Cpu.WriteData(EECR, 0x01); // EERE + Assert.That(tiny.Data[EEDR], Is.EqualTo(0x42)); + } + + [Test] + public void Eeprom_AddressAboveTheArray_WrapsInsteadOfThrowing() + { + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + ldi r16, 0x82 + out 0x1F, r16 ; EEARH with unused high bits set + ldi r16, 0x05 + out 0x1E, r16 + ldi r16, 0x01 + out 0x1C, r16 ; EERE + break + "); + + Assert.DoesNotThrow(() => tiny.RunToBreak()); + } + + [Test] + public void Timer1_TccrWrite_DoesNotFreezePb1High() + { + // TCCR1 used to be mapped as TCCRB with TCCRA at address 0, so r0 was read as COM1A. + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + ldi r16, 0xFF + mov r0, r16 + ldi r16, 0x02 + out 0x17, r16 ; DDRB: PB1 output + ldi r16, 0x01 + out 0x30, r16 ; TCCR1 = CS10 + done: rjmp done + "); + + tiny.RunInstructions(20); + tiny.RunMicroseconds(100); + + tiny.PortB.Should().HavePinLow(1); + Assert.That(tiny.Cpu.ReadData(TCNT1), Is.Not.Zero); + } + + [Test] + public void Timer1_CtcMode_SetsTov1AtOcr1cAndNotTheTimer0Bit() + { + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + ldi r16, 99 + out 0x2D, r16 ; OCR1C = 99 + ldi r16, 0x81 + out 0x30, r16 ; TCCR1 = CTC1 | CS10 + done: rjmp done + "); + + tiny.RunInstructions(20); + tiny.RunCycles(150); + + Assert.Multiple(() => + { + Assert.That(tiny.Data[TIFR] & 0x04, Is.EqualTo(0x04), "TOV1"); + Assert.That(tiny.Data[TIFR] & 0x10, Is.Zero, "OCF0A must stay clear"); + }); + } + + [Test] + public void Timer1_Prescaler16384_IsSupported() + { + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + ldi r16, 0x0F + out 0x30, r16 ; CS1 = 15 -> /16384 + done: rjmp done + "); + + tiny.RunInstructions(20); + tiny.RunCycles(16384 * 3 + 16); + + Assert.That(tiny.Cpu.ReadData(TCNT1), Is.GreaterThanOrEqualTo(2)); + } + + [Test] + public void Int0_FallingEdgeOnPb2_SetsIntf0() + { + var tiny = new ATtiny85Simulation(); + tiny.WithAsm(@" + ldi r16, 0x40 + out 0x3B, r16 ; GIMSK: INT0 + ldi r16, 0x02 + out 0x35, r16 ; MCUCR: ISC01 = falling edge + break + "); + + tiny.RunToBreak(); + tiny.PortB.SetPinValue(2, true); + Assert.That(tiny.Data[GIFR] & 0x40, Is.Zero); + tiny.PortB.SetPinValue(2, false); + + Assert.That(tiny.Data[GIFR] & 0x40, Is.EqualTo(0x40)); + } +} diff --git a/tests/Avr8Sharp.TestKit.Samples/ArduinoMegaPeripheralsTests.cs b/tests/Avr8Sharp.TestKit.Samples/ArduinoMegaPeripheralsTests.cs new file mode 100644 index 0000000..9ef252f --- /dev/null +++ b/tests/Avr8Sharp.TestKit.Samples/ArduinoMegaPeripheralsTests.cs @@ -0,0 +1,507 @@ +using AVR8Sharp.Core.Peripherals; +using Avr8Sharp.TestKit.Boards; + +namespace Avr8Sharp.TestKit.Samples; + +/// +/// ATmega2560 peripheral wiring (compare-output pins, external/pin-change interrupts, SPI/USART +/// vectors, input capture) driven by small assembled programs. Interrupt vector "word" values are +/// avr-libc vector number x 2; .org takes byte addresses, so a vector at word W sits at byte 2*W. +/// +[TestFixture] +public class ArduinoMegaPeripheralsTests +{ + // Reset jumps to main (placed past the 57-entry vector table); the handler body is + // appended at the vector under test and just records a marker in r24 then breaks. + private static string Program(string main, int vectorWord, string handler = "ldi r24, 0xA5\nbreak") + { + return $@" + jmp main + .org {vectorWord * 2} + {handler} + .org 0x200 + main: + ldi r16, 0x21 + out 0x3E, r16 ; SPH = RAMEND high + ldi r16, 0xFF + out 0x3D, r16 ; SPL + {main} + "; + } + + private static List RecordPin(AvrIoPort port, int bit) + { + var levels = new List(); + port.AddListener((value, _) => levels.Add((value & (1 << bit)) != 0)); + return levels; + } + + private static void AssertToggles(List levels) + { + Assert.That(levels, Does.Contain(true), "pin never went high"); + Assert.That(levels, Does.Contain(false), "pin never went low"); + } + + // ── Compare-output pins ─────────────────────────────────────────────────── + + [Test] + public void Timer0_FastPwm_DrivesPB7_ForOC0A() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x80 + out 0x04, r16 ; DDRB.7 output + ldi r16, 0x40 + sts 0x0047, r16 ; OCR0A + ldi r16, 0x83 + out 0x24, r16 ; TCCR0A = COM0A1 | WGM01 | WGM00 + ldi r16, 0x01 + out 0x25, r16 ; TCCR0B = clk/1 + loop: + rjmp loop + "); + var pb7 = RecordPin(mega.PortB, 7); + var pd6 = RecordPin(mega.PortD, 6); + + mega.RunCycles(2000); + + AssertToggles(pb7); + Assert.That(pd6, Is.Empty, "OC0A must not use the ATmega328P pin (PD6)"); + } + + [Test] + public void Timer0_FastPwm_DrivesPG5_ForOC0B() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x20 + sts 0x0033, r16 ; DDRG.5 output + ldi r16, 0x40 + sts 0x0048, r16 ; OCR0B + ldi r16, 0x23 + out 0x24, r16 ; TCCR0A = COM0B1 | WGM01 | WGM00 + ldi r16, 0x01 + out 0x25, r16 + loop: + rjmp loop + "); + var pg5 = RecordPin(mega.PortG, 5); + + mega.RunCycles(2000); + + AssertToggles(pg5); + } + + [Test] + public void Timer1_ChannelC_DrivesPB7_ForOC1C() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x80 + out 0x04, r16 ; DDRB.7 output + ldi r16, 0x40 + sts 0x008C, r16 ; OCR1CL + ldi r16, 0x09 + sts 0x0080, r16 ; TCCR1A = COM1C1 | WGM10 + ldi r16, 0x09 + sts 0x0081, r16 ; TCCR1B = WGM12 | clk/1 (8-bit fast PWM) + loop: + rjmp loop + "); + var pb7 = RecordPin(mega.PortB, 7); + + mega.RunCycles(2000); + + AssertToggles(pb7); + } + + [Test] + public void Timer1_ChannelC_CompareMatchSetsOcf1c() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x10 + sts 0x008C, r16 ; OCR1CL = 16 + ldi r16, 0x01 + sts 0x0081, r16 ; TCCR1B = clk/1, normal mode + loop: + rjmp loop + "); + + mega.RunCycles(200); + + Assert.That(mega.Cpu.ReadData(0x36) & 0x08, Is.EqualTo(0x08), "TIFR1.OCF1C"); + } + + [Test] + public void Timer2_FastPwm_DrivesPH6_ForOC2B() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x40 + sts 0x0101, r16 ; DDRH.6 output + ldi r16, 0x40 + sts 0x00B4, r16 ; OCR2B + ldi r16, 0x23 + sts 0x00B0, r16 ; TCCR2A = COM2B1 | WGM21 | WGM20 + ldi r16, 0x01 + sts 0x00B1, r16 ; TCCR2B = clk/1 + loop: + rjmp loop + "); + var ph6 = RecordPin(mega.PortH, 6); + var pd3 = RecordPin(mega.PortD, 3); + + mega.RunCycles(2000); + + AssertToggles(ph6); + Assert.That(pd3, Is.Empty, "OC2B must not use the ATmega328P pin (PD3)"); + } + + [Test] + public void Timer2_FastPwm_DrivesPB4_ForOC2A() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x10 + out 0x04, r16 ; DDRB.4 output + ldi r16, 0x40 + sts 0x00B3, r16 ; OCR2A + ldi r16, 0x83 + sts 0x00B0, r16 ; TCCR2A = COM2A1 | WGM21 | WGM20 + ldi r16, 0x01 + sts 0x00B1, r16 + loop: + rjmp loop + "); + var pb4 = RecordPin(mega.PortB, 4); + + mega.RunCycles(2000); + + AssertToggles(pb4); + } + + [Test] + public void Timer3_ChannelC_DrivesPE5() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x20 + out 0x0D, r16 ; DDRE.5 output + ldi r16, 0x40 + sts 0x009C, r16 ; OCR3CL + ldi r16, 0x09 + sts 0x0090, r16 ; TCCR3A = COM3C1 | WGM30 + ldi r16, 0x09 + sts 0x0091, r16 ; TCCR3B = WGM32 | clk/1 + loop: + rjmp loop + "); + var pe5 = RecordPin(mega.PortE, 5); + + mega.RunCycles(2000); + + AssertToggles(pe5); + } + + // ── External interrupts (INT0-7) ────────────────────────────────────────── + + [Test] + public void Int4_FallingEdge_SetsEifrBit4() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x02 + sts 0x006A, r16 ; EICRB: ISC41 = falling edge on INT4 + ldi r16, 0x10 + out 0x1D, r16 ; EIMSK = INT4 (interrupts left globally disabled) + loop: + rjmp loop + "); + mega.PortE.SetPinValue(4, true); + mega.RunInstructions(20); + Assert.That(mega.Cpu.ReadData(0x3C) & 0x10, Is.Zero); + + mega.PortE.SetPinValue(4, false); + + Assert.That(mega.Cpu.ReadData(0x3C) & 0x10, Is.EqualTo(0x10), "EIFR.INTF4"); + } + + [Test] + public void Int4_FallingEdge_JumpsToVector0x0A() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x02 + sts 0x006A, r16 ; EICRB: INT4 falling edge + ldi r16, 0x10 + out 0x1D, r16 ; EIMSK + sei + loop: + rjmp loop + ", 0x0A)); + mega.PortE.SetPinValue(4, true); + mega.RunInstructions(50); + + mega.PortE.SetPinValue(4, false); + mega.RunToBreak(); + + Assert.That(mega.Cpu.ReadData(24), Is.EqualTo(0xA5)); + Assert.That(mega.Cpu.Pc, Is.InRange(0x0Au, 0x0Du)); + } + + [Test] + public void Int0_FallingEdgeOnPD0_JumpsToVector0x02() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x02 + sts 0x0069, r16 ; EICRA: INT0 falling edge + ldi r16, 0x01 + out 0x1D, r16 ; EIMSK + sei + loop: + rjmp loop + ", 0x02)); + mega.PortD.SetPinValue(0, true); + mega.RunInstructions(50); + + mega.PortD.SetPinValue(0, false); + mega.RunToBreak(); + + Assert.That(mega.Cpu.ReadData(24), Is.EqualTo(0xA5)); + Assert.That(mega.Cpu.Pc, Is.InRange(0x02u, 0x05u)); + } + + [Test] + public void Int7_RisingEdgeOnPE7_UsesEicrbIsc7() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0xC0 + sts 0x006A, r16 ; EICRB: ISC71:70 = rising edge + ldi r16, 0x80 + out 0x1D, r16 ; EIMSK = INT7 + sei + loop: + rjmp loop + ", 0x10)); + mega.PortE.SetPinValue(7, false); + mega.RunInstructions(50); + + mega.PortE.SetPinValue(7, true); + mega.RunToBreak(); + + Assert.That(mega.Cpu.Pc, Is.InRange(0x10u, 0x13u)); + } + + // ── Pin-change interrupts ───────────────────────────────────────────────── + + [Test] + public void Pcint2_OnPK0_JumpsToVector0x16() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x01 + sts 0x006D, r16 ; PCMSK2 = PCINT16 (PK0) + ldi r16, 0x04 + sts 0x0068, r16 ; PCICR = PCIE2 + sei + loop: + rjmp loop + ", 0x16)); + mega.PortK.SetPinValue(0, false); + mega.RunInstructions(50); + + mega.PortK.SetPinValue(0, true); + mega.RunToBreak(); + + Assert.That(mega.Cpu.ReadData(24), Is.EqualTo(0xA5)); + Assert.That(mega.Cpu.Pc, Is.InRange(0x16u, 0x19u)); + } + + [Test] + public void Pcint0_OnPB4_JumpsToVector0x12() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x10 + sts 0x006B, r16 ; PCMSK0 = PCINT4 (PB4) + ldi r16, 0x01 + sts 0x0068, r16 ; PCICR = PCIE0 + sei + loop: + rjmp loop + ", 0x12)); + mega.PortB.SetPinValue(4, false); + mega.RunInstructions(50); + + mega.PortB.SetPinValue(4, true); + mega.RunToBreak(); + + Assert.That(mega.Cpu.Pc, Is.InRange(0x12u, 0x15u)); + } + + [Test] + public void Pcint1_PE0IsBit0_AndPJ0IsBit1_OfPcmsk1() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x02 + sts 0x006C, r16 ; PCMSK1 = PCINT9 (PJ0) only + ldi r16, 0x02 + sts 0x0068, r16 ; PCICR = PCIE1 + loop: + rjmp loop + ", 0x14)); + mega.RunInstructions(20); + + mega.PortE.SetPinValue(0, true); // PCINT8, not enabled in PCMSK1 + Assert.That(mega.Cpu.ReadData(0x3B) & 0x02, Is.Zero, "PE0 is masked out"); + + mega.PortJ.SetPinValue(0, true); // PCINT9, enabled + Assert.That(mega.Cpu.ReadData(0x3B) & 0x02, Is.EqualTo(0x02), "PCIFR.PCIF1 from PJ0"); + } + + [Test] + public void Pcint1_OnPE0_SetsPcif1_WhenEnabledInBit0() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x01 + sts 0x006C, r16 ; PCMSK1 = PCINT8 (PE0) + ldi r16, 0x02 + sts 0x0068, r16 + loop: + rjmp loop + "); + mega.RunInstructions(20); + + mega.PortE.SetPinValue(0, true); + + Assert.That(mega.Cpu.ReadData(0x3B) & 0x02, Is.EqualTo(0x02)); + } + + // ── SPI / USART ─────────────────────────────────────────────────────────── + + [Test] + public void Spi_TransferComplete_JumpsToVector0x30() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0xD0 + out 0x2C, r16 ; SPCR = SPIE | SPE | MSTR + sei + ldi r16, 0x55 + out 0x2E, r16 ; SPDR: start transfer + loop: + rjmp loop + ", 0x30)); + + mega.RunToBreak(); + + Assert.That(mega.Cpu.ReadData(24), Is.EqualTo(0xA5)); + Assert.That(mega.Cpu.Pc, Is.InRange(0x30u, 0x33u)); + } + + [Test] + public void Usart1_TransmitsByte_OnSerial1Only() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(@" + ldi r16, 0x08 + sts 0x00C9, r16 ; UCSR1B = TXEN1 + ldi r16, 0x06 + sts 0x00CA, r16 ; UCSR1C = 8N1 + ldi r16, 103 + sts 0x00CC, r16 ; UBRR1L (9600 baud @16 MHz) + ldi r16, 0x4B ; 'K' + sts 0x00CE, r16 ; UDR1 + break + "); + + mega.RunToBreak(); + mega.RunMilliseconds(5); + + Assert.That(mega.Serial1.Text, Is.EqualTo("K")); + Assert.That(mega.Serial0.Text, Is.Empty); + } + + [Test] + public void Usart1_DataRegisterEmpty_JumpsToVector0x4A() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x20 + sts 0x00C9, r16 ; UCSR1B = UDRIE1 + sei + loop: + rjmp loop + ", 0x4A)); + + mega.RunToBreak(); + + Assert.That(mega.Cpu.Pc, Is.InRange(0x4Au, 0x4Du)); + } + + // ── Input capture ───────────────────────────────────────────────────────── + + [Test] + public void Timer3_TriggerCapture_SetsIcf3_AndJumpsToVector0x3E() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm(Program(@" + ldi r16, 0x01 + sts 0x0091, r16 ; TCCR3B = clk/1 + ldi r16, 0x20 + sts 0x0071, r16 ; TIMSK3 = ICIE3 + sei + loop: + rjmp loop + ", 0x3E)); + mega.RunInstructions(50); + + mega.Timer3.TriggerCapture(); + mega.RunToBreak(); + + Assert.That(mega.Cpu.Pc, Is.InRange(0x3Eu, 0x41u)); + } + + [Test] + public void Timer3_TriggerCapture_SetsIcf3InTifr3() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm("ldi r16, 0x01\nsts 0x0091, r16\nloop:\nrjmp loop"); + mega.RunInstructions(20); + + mega.Timer3.TriggerCapture(); + + Assert.That(mega.Cpu.ReadData(0x38) & 0x20, Is.EqualTo(0x20)); + } + + [Test] + public void Timer4_InputCaptureFlag_IsConfigured() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm("ldi r16, 0x01\nsts 0x00A1, r16\nloop:\nrjmp loop"); + mega.RunInstructions(20); + + mega.Timer4.TriggerCapture(); + + Assert.That(mega.Cpu.ReadData(0x39) & 0x20, Is.EqualTo(0x20), "TIFR4.ICF4"); + } + + [Test] + public void Timer3_IcpPin_PE7_RisingEdgeCaptures() + { + var mega = new ArduinoMegaSimulation(); + mega.WithAsm("ldi r16, 0x41\nsts 0x0091, r16\nloop:\nrjmp loop"); // ICES3 = rising, clk/1 + mega.PortE.SetPinValue(7, false); + mega.RunInstructions(20); + Assert.That(mega.Cpu.ReadData(0x38) & 0x20, Is.Zero); + + mega.PortE.SetPinValue(7, true); + + Assert.That(mega.Cpu.ReadData(0x38) & 0x20, Is.EqualTo(0x20), "TIFR3.ICF3 from ICP3 = PE7"); + } +} diff --git a/tests/Avr8Sharp.Tests/AdcTests.cs b/tests/Avr8Sharp.Tests/AdcTests.cs index ec0d9fc..15d4aa2 100644 --- a/tests/Avr8Sharp.Tests/AdcTests.cs +++ b/tests/Avr8Sharp.Tests/AdcTests.cs @@ -8,6 +8,7 @@ public class Adc : AvrTestBase { const int ADMUX = 0x7c; const int REFS0 = 1 << 6; + const int REFS1 = 1 << 7; const int ADCSRA = 0x7a; const int ADEN = 1 << 7; @@ -271,4 +272,96 @@ public void ReferenceSelection_FollowsDatasheetOrder () Assert.That (_adc.ReferenceVoltageType, Is.EqualTo (AdcReference.Internal1V1), "REFS = 11 (analogReference(INTERNAL))"); Assert.That (_adc.ReferenceVoltage, Is.EqualTo (1.1)); } + + + [Test(Description = "The AVCC reference follows the supply the host reports, so a 3.3 V board converts full-scale at 3.3 V")] + public void Conversion_Scales_With_Avcc () + { + var program = new AsmProgram (@$" + _REPLACE ADMUX, {ADMUX} + _REPLACE ADCSRA, {ADCSRA} + _REPLACE ADCH, {ADCH} + _REPLACE ADCL, {ADCL} + ldi r24, {REFS0} + sts ADMUX, r24 + ldi r24, {ADEN | ADSC | ADPS0 | ADPS1 | ADPS2} + sts ADCSRA, r24 + waitComplete: + lds r24, {ADCSRA} + andi r24, {ADSC} + brne waitComplete + lds r16, {ADCL} + lds r17, {ADCH} + break +").Compile(); + Cpu.LoadProgram(program.Program); + var runner = new TestProgramRunner (Cpu); + + _adc.Avcc = 3.3; + _adc.ChannelValues[0] = 1.65; // half of AVCC: 512 + + runner.RunInstructions (16); + Cpu.Cycles += 128 * 25; + Cpu.Tick (); + runner.RunInstructions (5); + + var result = (Cpu.Mmio.Data[R17] << 8) | Cpu.Mmio.Data[R16]; + Assert.That(result, Is.EqualTo(512)); + } + + [Test(Description = "REFS1:0 = 11 is the internal 1.1 V bandgap on the ATmega328P (the Arduino core's INTERNAL)")] + public void Conversion_Against_The_Internal_Bandgap () + { + var program = new AsmProgram (@$" + _REPLACE ADMUX, {ADMUX} + _REPLACE ADCSRA, {ADCSRA} + _REPLACE ADCH, {ADCH} + _REPLACE ADCL, {ADCL} + ldi r24, {REFS1 | REFS0} + sts ADMUX, r24 + ldi r24, {ADEN | ADSC | ADPS0 | ADPS1 | ADPS2} + sts ADCSRA, r24 + waitComplete: + lds r24, {ADCSRA} + andi r24, {ADSC} + brne waitComplete + lds r16, {ADCL} + lds r17, {ADCH} + break +").Compile(); + Cpu.LoadProgram(program.Program); + var runner = new TestProgramRunner (Cpu); + + _adc.ChannelValues[0] = 0.55; // half of 1.1 V: 512 + + runner.RunInstructions (16); + Cpu.Cycles += 128 * 25; + Cpu.Tick (); + runner.RunInstructions (5); + + var result = (Cpu.Mmio.Data[R17] << 8) | Cpu.Mmio.Data[R16]; + Assert.That(result, Is.EqualTo(512)); + } + + [Test(Description = "ADIF is cleared by writing a one and a write of zero keeps it (datasheet 24.9.2)")] + public void Adif_Is_Write_One_To_Clear () + { + Cpu.Mmio.Data[ADCSRA] = ADEN | ADIF; + Cpu.WriteData(ADCSRA, ADEN | ADPS0); + Assert.That(Cpu.ReadData(ADCSRA) & ADIF, Is.EqualTo(ADIF), "writing 0 to ADIF must not clear it"); + + Cpu.WriteData(ADCSRA, ADEN | ADIF); + Assert.That(Cpu.ReadData(ADCSRA) & ADIF, Is.Zero, "writing 1 to ADIF clears it"); + } + + [Test(Description = "A write cannot set ADIF, and ADSC cannot be cleared while a conversion runs")] + public void Adif_Cannot_Be_Set_And_Adsc_Sticks_While_Converting () + { + Cpu.WriteData(ADCSRA, ADEN | ADIF); + Assert.That(Cpu.ReadData(ADCSRA) & ADIF, Is.Zero, "ADIF is not a stored bit"); + + Cpu.WriteData(ADCSRA, ADEN | ADSC | ADPS0 | ADPS1 | ADPS2); + Cpu.WriteData(ADCSRA, ADEN | ADPS0 | ADPS1 | ADPS2); + Assert.That(Cpu.ReadData(ADCSRA) & ADSC, Is.EqualTo(ADSC), "ADSC stays set while converting"); + } } diff --git a/tests/Avr8Sharp.Tests/CpuCoreAuditTests.cs b/tests/Avr8Sharp.Tests/CpuCoreAuditTests.cs new file mode 100644 index 0000000..e62f39c --- /dev/null +++ b/tests/Avr8Sharp.Tests/CpuCoreAuditTests.cs @@ -0,0 +1,377 @@ +using System.Text; +using AvrCpu = AVR8Sharp.Core.Cpu; +using AVR8Sharp.Core; +using AVR8Sharp.Core.Decoders; +using AVR8Sharp.Core.Utils; + +namespace Avr8Sharp.Tests; + +/// +/// Regression tests for the CPU-core audit: Intel HEX loading, the one-instruction rule after +/// SEI/RETI, per-CPU instance hooks and the drift-free runner. +/// +[TestFixture] +public class CpuCoreAuditTests +{ + private const ushort Sei = 0x9478; + private const ushort Reti = 0x9518; + private const ushort Nop = 0x0000; + private const ushort Break = 0x9598; + private const ushort SleepOp = 0x9588; + private const ushort IncR16 = 0x9503; + private const ushort IncR17 = 0x9513; + + #region Intel HEX + + private static string Rec (int type, int addr, params byte[] data) + { + var bytes = new List { (byte)data.Length, (byte)(addr >> 8), (byte)addr, (byte)type }; + bytes.AddRange (data); + var sum = bytes.Sum (b => b); + bytes.Add ((byte)(-sum & 0xff)); + var sb = new StringBuilder (":"); + foreach (var b in bytes) sb.Append (b.ToString ("X2")); + return sb.ToString (); + } + + private static AvrRunner HexRunner (int flashBytes) + { + var runner = new AvrRunner (new byte[flashBytes], 2048); + for (var i = 0; i < flashBytes; i++) runner.Cpu.ProgBytes[i] = 0; + return runner; + } + + [Test (Description = "Extended linear address (04) places data above 64 KB instead of wrapping onto flash start")] + public void Hex_ExtendedLinearAddress () + { + var runner = HexRunner (0x40000); + runner.Cpu.SetProgramByte (0, 0xAA); + var hex = string.Join ("\n", + Rec (0, 0x0000, 0x11, 0x22), + Rec (4, 0, 0x00, 0x02), + Rec (0, 0x0010, 0x33, 0x44), + Rec (1, 0)); + + Assert.That (runner.TryLoadHex (hex, out var info), Is.True, string.Join (";", info.Errors)); + Assert.Multiple (() => { + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (0x11)); + Assert.That (runner.Cpu.ProgBytes[0x20010], Is.EqualTo (0x33)); + Assert.That (runner.Cpu.ProgBytes[0x20011], Is.EqualTo (0x44)); + Assert.That (info.MaxAddress, Is.EqualTo (0x20011)); + Assert.That (info.ByteCount, Is.EqualTo (4)); + }); + } + + [Test (Description = "void LoadHex honours extended linear addresses too (mega sketches > 64 KB)")] + public void Hex_LoadHex_ExtendedLinear () + { + var runner = HexRunner (0x40000); + runner.LoadHex (string.Join ("\n", Rec (0, 0, 0x11), Rec (4, 0, 0x00, 0x01), Rec (0, 0, 0x77), Rec (1, 0))); + Assert.Multiple (() => { + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (0x11)); + Assert.That (runner.Cpu.ProgBytes[0x10000], Is.EqualTo (0x77)); + }); + } + + [Test (Description = "Extended segment address (02) shifts the base by 4 bits")] + public void Hex_ExtendedSegmentAddress () + { + var runner = HexRunner (0x40000); + var hex = string.Join ("\n", Rec (2, 0, 0x20, 0x00), Rec (0, 0x0004, 0x5A), Rec (1, 0)); + Assert.That (runner.TryLoadHex (hex, out var info), Is.True, string.Join (";", info.Errors)); + Assert.That (runner.Cpu.ProgBytes[0x20004], Is.EqualTo (0x5A)); + } + + [Test (Description = "Start-address records (03, 05) are ignored and EOF stops parsing")] + public void Hex_StartRecordsIgnored_EofStops () + { + var runner = HexRunner (0x100); + var hex = string.Join ("\n", + Rec (3, 0, 0, 0, 0, 0), + Rec (5, 0, 0, 0, 0, 0), + Rec (0, 0, 0x01), + Rec (1, 0), + Rec (0, 1, 0x02)); + Assert.That (runner.TryLoadHex (hex, out var info), Is.True, string.Join (";", info.Errors)); + Assert.Multiple (() => { + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (1)); + Assert.That (runner.Cpu.ProgBytes[1], Is.EqualTo (0), "data after EOF must not load"); + }); + } + + [Test (Description = "A bad checksum makes TryLoadHex fail and leaves flash untouched")] + public void Hex_BadChecksum_FlashUntouched () + { + var runner = HexRunner (0x100); + runner.Cpu.SetProgramByte (0, 0xEE); + var bad = Rec (0, 0, 0x01, 0x02); + bad = bad[..^2] + (bad.EndsWith ("00") ? "01" : "00"); + + Assert.That (runner.TryLoadHex (bad + "\n" + Rec (1, 0), out var info), Is.False); + Assert.Multiple (() => { + Assert.That (info.Errors, Is.Not.Empty); + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (0xEE)); + }); + } + + [Test (Description = "An image larger than flash is rejected without touching flash, with MaxAddress reported")] + public void Hex_TooBig_FlashUntouched () + { + var runner = HexRunner (0x100); + runner.Cpu.SetProgramByte (0, 0xEE); + var hex = string.Join ("\n", Rec (0, 0, 0x01), Rec (4, 0, 0x00, 0x01), Rec (0, 0, 0x02), Rec (1, 0)); + + Assert.That (runner.TryLoadHex (hex, out var info), Is.False); + Assert.Multiple (() => { + Assert.That (info.MaxAddress, Is.EqualTo (0x10000)); + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (0xEE)); + }); + } + + [Test (Description = "Malformed lines never throw; the valid records around them still load with LoadHex")] + public void Hex_Malformed_DoesNotThrow () + { + var runner = HexRunner (0x100); + var hex = string.Join ("\n", + ":", + ":ZZ", + ":10000000", + "garbage", + ":0100000G00", + Rec (0, 0, 0x42), + ":FF0000", + Rec (1, 0)); + + Assert.DoesNotThrow (() => runner.LoadHex (hex)); + Assert.That (runner.Cpu.ProgBytes[0], Is.EqualTo (0x42)); + Assert.That (runner.TryLoadHex (hex, out var info), Is.False); + Assert.That (info.Errors.Count, Is.GreaterThanOrEqualTo (5)); + } + + [Test (Description = "A clean image loads through TryLoadHex and the real blink sketch parses")] + public void Hex_Clean_Loads () + { + var runner = HexRunner (0x100); + Assert.That (runner.TryLoadHex (Rec (0, 0x10, 0xDE, 0xAD) + "\n" + Rec (1, 0), out var info), Is.True); + Assert.Multiple (() => { + Assert.That (info.Errors, Is.Empty); + Assert.That (info.ByteCount, Is.EqualTo (2)); + Assert.That (runner.Cpu.ProgBytes[0x10], Is.EqualTo (0xDE)); + }); + } + + #endregion + + #region One instruction after SEI / RETI + + private static AvrCpu MakeCpu (params ushort[] words) + { + var program = new ushort[0x100]; + words.CopyTo (program, 0); + return new AvrCpu (program); + } + + private static void Step (ref TDecoder decoder, AvrCpu cpu) where TDecoder : struct, IInstructionDecoder + { + decoder.Decode (cpu); + cpu.Tick (); + } + + private static void ForEachDecoder (Action> body) + { + var sw = new SwitchDecoder (); + body (cpu => Step (ref sw, cpu)); + var lut = new LutDecoder (); + body (cpu => Step (ref lut, cpu)); + var native = new NativeLutDecoder (); + body (cpu => Step (ref native, cpu)); + var counting = new CountingDecoder (new ExecutionCounts (0x100)); + body (cpu => Step (ref counting, cpu)); + } + + private static readonly AvrInterruptConfig Irq = new (4, 0x60, 1, 0x61, 1); + + [Test (Description = "SEI with a pending IRQ: the following instruction runs before the interrupt is taken")] + public void Sei_RunsNextInstructionBeforePendingIrq () + { + ForEachDecoder (step => { + var cpu = MakeCpu (Sei, IncR16, Nop, Nop, Reti); + cpu.QueueInterrupt (new AvrInterruptConfig (4, 0x60, 1, 0x61, 1)); + + step (cpu); // SEI + Assert.That (cpu.Pc, Is.EqualTo (1u), "interrupt must not be taken right after SEI"); + step (cpu); // INC r16, then the interrupt is taken + Assert.Multiple (() => { + Assert.That (cpu.Mmio.Data[16], Is.EqualTo (1), "the instruction after SEI must execute"); + Assert.That (cpu.Pc, Is.EqualTo (4u), "interrupt taken after that instruction"); + }); + }); + } + + [Test (Description = "OUT SREG that sets I also delays a pending IRQ by one instruction")] + public void OutSreg_RunsNextInstructionBeforePendingIrq () + { + ForEachDecoder (step => { + var cpu = MakeCpu (0xE800 /* ldi r16,0x80 */, 0xBF0F /* out SREG,r16 */, IncR17, Nop, Reti); + cpu.QueueInterrupt (new AvrInterruptConfig (4, 0x60, 1, 0x61, 1)); + + step (cpu); + step (cpu); // OUT SREG + Assert.That (cpu.Pc, Is.EqualTo (2u)); + step (cpu); // INC r17 + Assert.Multiple (() => { + Assert.That (cpu.Mmio.Data[17], Is.EqualTo (1)); + Assert.That (cpu.Pc, Is.EqualTo (4u)); + }); + }); + } + + [Test (Description = "Host writes to SREG (not an instruction) still dispatch on the very next Tick")] + public void HostSregWrite_DispatchesImmediately () + { + var cpu = MakeCpu (Nop, Nop, Nop, Nop, Reti); + cpu.QueueInterrupt (new AvrInterruptConfig (4, 0x60, 1, 0x61, 1)); + cpu.WriteData (95, 0x80); + cpu.Tick (); + Assert.That (cpu.Pc, Is.EqualTo (4u)); + } + + [Test (Description = "An always-pending IRQ with RETI lets exactly one main-line instruction run between ISRs")] + public void Reti_AlwaysPendingIrq_OneMainInstructionPerIsr () + { + ForEachDecoder (step => { + var cpu = MakeCpu (Sei, IncR16, IncR17, 0xCFFD /* rjmp .-3 */, Reti); + cpu.QueueInterrupt (new AvrInterruptConfig (4, 0x60, 1, 0x61, 1, constant: true)); + var pushed = new List (); + cpu.OnInterruptDispatch += (_, pc) => pushed.Add (pc); + + for (var i = 0; i < 60 && pushed.Count < 9; i++) step (cpu); + + // SEI, [inc r16] ISR, [inc r17] ISR, [rjmp] ISR, [inc r16] ISR ... + Assert.That (pushed, Is.EqualTo (new uint[] { 2, 3, 1, 2, 3, 1, 2, 3, 1 })); + }); + } + + [Test (Description = "Interrupt entry is 4 cycles after the interrupted instruction on a 16-bit PC part")] + public void InterruptEntry_FourCyclesAfterInstruction () + { + var cpu = MakeCpu (Nop, Nop, Nop, Nop, Reti); + cpu.QueueInterrupt (new AvrInterruptConfig (4, 0x60, 1, 0x61, 1)); + cpu.WriteData (95, 0x80); + var decoder = new SwitchDecoder (); + Step (ref decoder, cpu); + Assert.That (cpu.Cycles, Is.EqualTo (1 + 4)); + } + + #endregion + + #region Per-CPU hooks + + [Test (Description = "Instance hooks fire only for their own CPU; the obsolete statics still fire as fallbacks")] + public void InstanceHooks_DoNotCrossFire () + { + ForEachDecoder (step => { + var a = MakeCpu (Break, SleepOp); + var b = MakeCpu (Break, SleepOp); + var aBreaks = 0; var bBreaks = 0; var aSleeps = 0; var bSleeps = 0; + a.OnBreakpoint += _ => aBreaks++; + b.OnBreakpoint += _ => bBreaks++; + a.OnSleep += _ => aSleeps++; + b.OnSleep += _ => bSleeps++; + var globalBreaks = 0; var globalSleeps = 0; +#pragma warning disable CS0618 + AvrInterrupt.OnBreakpoint = _ => globalBreaks++; + AvrInterrupt.OnSleep = _ => globalSleeps++; + try { + step (a); step (a); // only CPU a runs + Assert.Multiple (() => { + Assert.That (aBreaks, Is.EqualTo (1)); + Assert.That (aSleeps, Is.EqualTo (1)); + Assert.That (bBreaks, Is.Zero); + Assert.That (bSleeps, Is.Zero); + Assert.That (globalBreaks, Is.EqualTo (1), "obsolete static fallback still called"); + Assert.That (globalSleeps, Is.EqualTo (1)); + }); + } finally { + AvrInterrupt.OnBreakpoint = null; + AvrInterrupt.OnSleep = null; + } +#pragma warning restore CS0618 + }); + } + + [Test (Description = "OnInterruptDispatch is per CPU and reports vector and pushed PC")] + public void InstanceHook_InterruptDispatch () + { + var a = MakeCpu (Nop, Nop, Nop, Nop, Reti); + var b = MakeCpu (Nop, Nop, Nop, Nop, Reti); + var seen = new List<(int, uint)> (); + var bSeen = 0; + a.OnInterruptDispatch += (addr, pc) => seen.Add ((addr, pc)); + b.OnInterruptDispatch += (_, _) => bSeen++; + a.QueueInterrupt (Irq); + a.WriteData (95, 0x80); + a.Tick (); + Assert.Multiple (() => { + Assert.That (seen, Is.EqualTo (new[] { (4, 0u) })); + Assert.That (bSeen, Is.Zero); + }); + } + + #endregion + + #region Runner + + private static AvrRunner SpinRunner () + { + var runner = new AvrRunner (new byte[0x200], 2048); + runner.LoadProgram (new ushort[] { 0xCFFF }); // rjmp . (2 cycles per instruction) + return runner; + } + + [Test (Description = "Execute with a small work unit does not drift: 1000 x 15 cycles is 15000, not 16000")] + public void Execute_DoesNotDrift () + { + var runner = SpinRunner (); + runner.SetWorkUnitCycles (15); + for (var i = 0; i < 1000; i++) runner.Execute (); + Assert.That (runner.Cpu.Cycles, Is.InRange (15000UL, 15001UL)); + } + + [Test (Description = "Run returns the cycles consumed and the sum tracks the cumulative budget")] + public void Run_ReturnsConsumed_AndCarries () + { + var runner = SpinRunner (); + long total = 0; + for (var i = 0; i < 500; i++) { + var used = runner.Run (15); + Assert.That (used, Is.InRange (14L, 16L)); + total += used; + } + Assert.That (total, Is.EqualTo ((long)runner.Cpu.Cycles)); + Assert.That (total, Is.InRange (7500L, 7501L)); + } + + [Test (Description = "RunUntil stops at the absolute cycle and a later Run continues from that target")] + public void RunUntil_ThenRun_Cumulative () + { + var runner = SpinRunner (); + var used = runner.RunUntil (101); + Assert.That (runner.Cpu.Cycles, Is.InRange (101UL, 102UL)); + Assert.That (used, Is.EqualTo ((long)runner.Cpu.Cycles)); + runner.Run (99); + Assert.That (runner.Cpu.Cycles, Is.InRange (200UL, 201UL)); + } + + [Test (Description = "A core advanced by other means re-bases the carried target")] + public void Run_RebasesAfterExternalAdvance () + { + var runner = SpinRunner (); + runner.Run (10); + runner.Cpu.Cycles += 1000; + var used = runner.Run (10); + Assert.That (used, Is.InRange (10L, 11L)); + } + + #endregion +} diff --git a/tests/Avr8Sharp.Tests/EepromTests.cs b/tests/Avr8Sharp.Tests/EepromTests.cs index bbfe264..e30c031 100644 --- a/tests/Avr8Sharp.Tests/EepromTests.cs +++ b/tests/Avr8Sharp.Tests/EepromTests.cs @@ -503,4 +503,65 @@ public void WriteEnableWindow_ResetInvalidatesWindow() }); } } + + [TestFixture] + public class Timing : AvrTestBase + { + private EepromMemoryBackend _backend; + + protected override void SetupPeripherals() + { + _backend = new EepromMemoryBackend(1024); + _ = new AvrEeprom(Cpu, _backend, null, 8_000_000); + } + + private void StartWrite(int eecrValue) + { + Cpu.WriteData(EEDR, 0x55); + Cpu.WriteData(EEARL, 15); + Cpu.WriteData(EEARH, 0); + Cpu.WriteData(EECR, EEMPE); + Cpu.WriteData(EECR, (byte)eecrValue); + } + + [Test(Description = "At 8 MHz the atomic erase+write takes 3.4 ms = 27200 cycles, not the 16 MHz count")] + public void AtomicWriteScalesWithFrequency() + { + var start = Cpu.Cycles; + StartWrite(EEPE); + Cpu.Cycles = start + 27199; + Cpu.Tick(); + Assert.That(Cpu.Mmio.Data[EECR] & EEPE, Is.EqualTo(EEPE), "still programming"); + + Cpu.Cycles = start + 27200; + Cpu.Tick(); + Assert.Multiple(() => + { + Assert.That(Cpu.Mmio.Data[EECR] & EEPE, Is.Zero); + Assert.That(_backend.ReadMemory(15), Is.EqualTo(0x55)); + }); + } + + [Test(Description = "Erase-only is 1.8 ms = 14400 cycles at 8 MHz")] + public void EraseOnlyScalesWithFrequency() + { + var start = Cpu.Cycles; + StartWrite(EEPE | EEPM0); + Cpu.Cycles = start + 14399; + Cpu.Tick(); + Assert.That(Cpu.Mmio.Data[EECR] & EEPE, Is.EqualTo(EEPE)); + + Cpu.Cycles = start + 14400; + Cpu.Tick(); + Assert.That(Cpu.Mmio.Data[EECR] & EEPE, Is.Zero); + } + + [Test(Description = "Writing EECR without EEPE (e.g. toggling EERIE) must not clear EEPE while programming")] + public void EepeSurvivesEecrWrite() + { + StartWrite(EEPE); + Cpu.WriteData(EECR, EERIE); + Assert.That(Cpu.Mmio.Data[EECR] & EEPE, Is.EqualTo(EEPE)); + } + } } diff --git a/tests/Avr8Sharp.Tests/GpioTests.cs b/tests/Avr8Sharp.Tests/GpioTests.cs index 5f2c64b..910df6d 100644 --- a/tests/Avr8Sharp.Tests/GpioTests.cs +++ b/tests/Avr8Sharp.Tests/GpioTests.cs @@ -400,7 +400,7 @@ public void Int0RisingEdge () Assert.Multiple (() => { Assert.That (Cpu.Pc, Is.EqualTo (PC_INT_INT0)); - Assert.That (Cpu.Cycles, Is.EqualTo (3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That (Cpu.Cycles, Is.EqualTo (4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That (Cpu.Mmio.Data[EIFR], Is.EqualTo (0)); }); @@ -425,7 +425,7 @@ public void Int0FallingEdge () Assert.Multiple (() => { Assert.That (Cpu.Pc, Is.EqualTo (PC_INT_INT0)); - Assert.That (Cpu.Cycles, Is.EqualTo (3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That (Cpu.Cycles, Is.EqualTo (4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That (Cpu.Mmio.Data[EIFR], Is.EqualTo (0)); }); } @@ -467,7 +467,7 @@ public void Int0StickyLow () Cpu.Tick (); Assert.Multiple (() => { Assert.That (Cpu.Pc, Is.EqualTo (PC_INT_INT0)); - Assert.That (Cpu.Cycles, Is.EqualTo (3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That (Cpu.Cycles, Is.EqualTo (4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) }); // Flag shouldn't be cleared, as the interrupt is sticky @@ -503,7 +503,7 @@ public void PinChangeHigh () Assert.Multiple (() => { Assert.That (Cpu.Pc, Is.EqualTo (PC_INT_PCINT0)); - Assert.That (Cpu.Cycles, Is.EqualTo(3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That (Cpu.Cycles, Is.EqualTo(4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (0)); }); } @@ -524,7 +524,7 @@ public void PinChangeLow () Assert.Multiple (() => { Assert.That (Cpu.Pc, Is.EqualTo (PC_INT_PCINT0)); - Assert.That (Cpu.Cycles, Is.EqualTo(3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That (Cpu.Cycles, Is.EqualTo(4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (0)); }); } diff --git a/tests/Avr8Sharp.Tests/InterruptTests.cs b/tests/Avr8Sharp.Tests/InterruptTests.cs index 4f879e8..4dca476 100644 --- a/tests/Avr8Sharp.Tests/InterruptTests.cs +++ b/tests/Avr8Sharp.Tests/InterruptTests.cs @@ -24,7 +24,7 @@ public void Interrupt_Handler () Assert.Multiple(() => { - Assert.That(Cpu.Cycles, Is.EqualTo(3)); // 2 for PC push + 1 for vector fetch (AVR spec: 4 cycles total) + Assert.That(Cpu.Cycles, Is.EqualTo(4)); // datasheet: interrupt response is 4 cycles with a 16-bit PC Assert.That(Cpu.Pc, Is.EqualTo(5)); Assert.That(Cpu.Mmio.Data[93], Is.EqualTo(0x7E)); // SP <- 0x7E Assert.That(Cpu.Mmio.Data[0x80], Is.EqualTo(0x20)); // Return address low byte @@ -47,7 +47,7 @@ public void AVRJS_Issue_58 () Assert.Multiple(() => { - Assert.That(Cpu.Cycles, Is.EqualTo(3)); // 2 for PC push + 1 for vector fetch (AVR spec: 4 cycles total) + Assert.That(Cpu.Cycles, Is.EqualTo(5)); // datasheet: interrupt response is 5 cycles with a 22-bit PC Assert.That(Cpu.Pc, Is.EqualTo(5)); Assert.That(Cpu.Mmio.Data[93], Is.EqualTo(0x7D)); // SP <- 0x7D Assert.That(Cpu.Mmio.Data[0x80], Is.EqualTo(0x20)); // Return address low byte @@ -57,19 +57,31 @@ public void AVRJS_Issue_58 () }); } - [Test(Description = "Interrupt latency must be exactly 3 cycles from DoAvrInterrupt (4 total incl. executing instruction)")] - public void InterruptLatency_ThreeCycles () + [Test(Description = "Interrupt response is 4 cycles on a 16-bit PC part, on top of the interrupted instruction")] + public void InterruptLatency_FourCycles () { - // AVR spec: after a pending interrupt is recognized, the current instruction - // finishes (1 cycle), then the PC is pushed onto the stack (2 cycles), - // then the vector address is fetched (1 cycle) = 4 cycles total. - // DoAvrInterrupt is called *after* the instruction cycle, so it must add exactly 3. + // Datasheet: "the interrupt execution response for all the enabled AVR interrupts is + // minimum four clock cycles" (PC push + vector). DoAvrInterrupt runs after the interrupted + // instruction already charged its own cycles, so it adds exactly 4. + var cpu = new AVR8Sharp.Core.Cpu(new ushort[0x8000]); + cpu.Mmio.Data[93] = 0x80; + cpu.Mmio.Data[95] = 0b10000001; // I flag + C flag + + var cyclesBefore = cpu.Cycles; + AvrInterrupt.DoAvrInterrupt(cpu, 10); + Assert.That(cpu.Cycles - cyclesBefore, Is.EqualTo(4)); + } + + [Test(Description = "Interrupt response is 5 cycles on a 22-bit PC part (3-byte PC push)")] + public void InterruptLatency_FiveCyclesOn22BitPc () + { + Assert.That(Cpu.Pc22Bits, Is.True); Cpu.Mmio.Data[93] = 0x80; - Cpu.Mmio.Data[95] = 0b10000001; // I flag + C flag + Cpu.Mmio.Data[95] = 0b10000001; var cyclesBefore = Cpu.Cycles; AvrInterrupt.DoAvrInterrupt(Cpu, 10); - Assert.That(Cpu.Cycles - cyclesBefore, Is.EqualTo(3)); + Assert.That(Cpu.Cycles - cyclesBefore, Is.EqualTo(5)); } [Test(Description = "SREG is cleared to zero on CPU Reset")] diff --git a/tests/Avr8Sharp.Tests/ResetTests.cs b/tests/Avr8Sharp.Tests/ResetTests.cs new file mode 100644 index 0000000..e03c9af --- /dev/null +++ b/tests/Avr8Sharp.Tests/ResetTests.cs @@ -0,0 +1,192 @@ +using AVR8Sharp.Core.Peripherals; +using Avr8Sharp.Tests.Utils; + +namespace Avr8Sharp.Tests; + +[TestFixture] +public class Reset : AvrTestBase +{ + const int FREQ_16MHZ = 16_000_000; + + const int DDRB = 0x24; + const int PORTB = 0x25; + const int TIMSK0 = 0x6e; + const int TCCR0B = 0x45; + const int TCNT0 = 0x46; + const int CS00 = 1; + const int UCSR0A = 0xc0; + const int UCSR0B = 0xc1; + const int UCSR0C = 0xc2; + const int UBRR0L = 0xc4; + const int UDR0 = 0xc6; + const int TXEN = 8; + const int RXEN = 16; + const int UDRE = 0x20; + const int MCUSR = 0x54; + const int WDRF = 1 << 3; + const int EXTRF = 1 << 1; + const int WDTCSR = 0x60; + const int WDCE = 1 << 4; + const int WDE = 1 << 3; + + private AvrIoPort _portB; + private AvrTimer _timer0; + private AvrUsart _usart; + private AvrWatchdog _watchdog; + + protected override void SetupPeripherals() + { + _portB = new AvrIoPort(Cpu, AvrIoPort.PortBConfig); + _timer0 = new AvrTimer(Cpu, AvrTimer.Timer0Config); + _usart = new AvrUsart(Cpu, AvrUsart.Usart0Config, FREQ_16MHZ); + _watchdog = new AvrWatchdog(Cpu, AvrWatchdog.WatchdogConfig, Clock); + } + + private void DirtyPeripherals() + { + Cpu.WriteData(DDRB, 0xff); + Cpu.WriteData(PORTB, 0x20); + Cpu.WriteData(UCSR0B, TXEN | RXEN); + Cpu.WriteData(UBRR0L, 103); + Cpu.WriteData(TIMSK0, 1); + Cpu.WriteData(TCCR0B, CS00); + Cpu.WriteData(UDR0, 0x41); + } + + [Test(Description = "Reset returns the I/O registers to their reset values")] + public void RegistersReturnToResetValues() + { + DirtyPeripherals(); + + Cpu.Reset(); + + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(DDRB), Is.EqualTo(0)); + Assert.That(Cpu.ReadData(PORTB), Is.EqualTo(0)); + Assert.That(Cpu.ReadData(TCCR0B), Is.EqualTo(0)); + Assert.That(Cpu.ReadData(TIMSK0), Is.EqualTo(0)); + Assert.That(Cpu.ReadData(UCSR0A), Is.EqualTo(UDRE)); + Assert.That(Cpu.ReadData(UCSR0B), Is.EqualTo(0)); + Assert.That(Cpu.ReadData(UCSR0C), Is.EqualTo(0x06)); + Assert.That(Cpu.ReadData(UBRR0L), Is.EqualTo(0)); + Assert.That(_portB.GetPinState(5), Is.EqualTo(PinState.Input)); + Assert.That(Cpu.Sp, Is.EqualTo(Cpu.Mmio.Data.Length - 1)); + }); + } + + [Test(Description = "Reset keeps SRAM and the general registers")] + public void SramAndRegistersSurvive() + { + Cpu.WriteData(0x300, 0x5a); + Cpu.WriteData(R20, 0x77); + + Cpu.Reset(); + + Assert.That(Cpu.ReadData(0x300), Is.EqualTo(0x5a)); + Assert.That(Cpu.ReadData(R20), Is.EqualTo(0x77)); + } + + [Test(Description = "Reset notifies port listeners that the pins went back to input")] + public void PortListenerFiresOnReset() + { + Cpu.WriteData(DDRB, 0xff); + Cpu.WriteData(PORTB, 0x20); + var calls = new List<(byte Value, byte Old)>(); + _portB.AddListener((value, old) => calls.Add((value, old))); + + Cpu.Reset(); + + Assert.That(calls, Has.Count.EqualTo(1)); + Assert.That(calls[0], Is.EqualTo(((byte)0, (byte)0x20))); + } + + [Test(Description = "A timer stops counting after a reset")] + public void TimerStopsCounting() + { + Cpu.WriteData(TCCR0B, CS00); + Cpu.Cycles = 1; + Cpu.Tick(); + Cpu.Cycles = 10; + Cpu.Tick(); + Assert.That(Cpu.ReadData(TCNT0), Is.GreaterThan(0)); + + Cpu.Reset(); + Cpu.Cycles += 100; + Cpu.Tick(); + Cpu.Cycles += 100; + Cpu.Tick(); + + Assert.That(Cpu.ReadData(TCNT0), Is.EqualTo(0)); + } + + [Test(Description = "Serial transmission works again after a reset with TXEN left set by the old firmware")] + public void UsartTransmitsAfterReset() + { + DirtyPeripherals(); + Cpu.Reset(); + + var sent = new List(); + _usart.OnByteTransmit = b => sent.Add(b); + + Cpu.WriteData(UCSR0B, TXEN); + Assert.That(Cpu.ReadData(UCSR0A) & UDRE, Is.EqualTo(UDRE)); + Cpu.WriteData(UDR0, 0x42); + + Assert.That(sent, Is.EqualTo(new byte[] { 0x42 })); + + // The transmit-complete event brings UDRE back + Cpu.Cycles += (ulong)_usart.CyclesPerChar + 1; + Cpu.Tick(); + Assert.That(Cpu.ReadData(UCSR0A) & UDRE, Is.EqualTo(UDRE)); + } + + [Test(Description = "A manual reset sets EXTRF and keeps the other MCUSR flags")] + public void ManualResetSetsExtrf() + { + Cpu.Reset(); + + Assert.That(Cpu.ReadData(MCUSR) & EXTRF, Is.EqualTo(EXTRF)); + Assert.That(Cpu.ReadData(MCUSR) & 1, Is.EqualTo(1), "PORF survives the reset"); + } + + [Test(Description = "A watchdog reset sets WDRF, not EXTRF, and leaves the watchdog disabled")] + public void WatchdogResetKeepsMcusrAndClearsWatchdog() + { + Cpu.WriteData(MCUSR, 0); + Cpu.WriteData(WDTCSR, WDCE | WDE); + Cpu.WriteData(WDTCSR, WDE); + DirtyPeripherals(); + + Cpu.Cycles += 16000 * 20; + Cpu.Tick(); + + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(MCUSR) & WDRF, Is.EqualTo(WDRF)); + Assert.That(Cpu.ReadData(MCUSR) & EXTRF, Is.EqualTo(0)); + Assert.That(Cpu.ReadData(DDRB), Is.EqualTo(0)); + Assert.That(_watchdog.Enabled, Is.False); + }); + } + + [Test(Description = "A Mega built like CircuitMirror clears the extended I/O (PORTH) on reset")] + public void MegaExtendedIoIsCleared() + { + var runner = AVR8Sharp.Core.AvrBuilder.Create(0x40000, 0x2100) + .AddGpioPort(AvrIoPort.PortHConfig, out var portH) + .Build(); + runner.Cpu.WriteData(AvrIoPort.PortHConfig.DDR, 0xff); + runner.Cpu.WriteData(AvrIoPort.PortHConfig.PORT, 0x10); + + runner.Cpu.Reset(); + + Assert.Multiple(() => + { + Assert.That(runner.Cpu.RamStart, Is.EqualTo(0x200)); + Assert.That(runner.Cpu.ReadData(AvrIoPort.PortHConfig.DDR), Is.EqualTo(0)); + Assert.That(runner.Cpu.ReadData(AvrIoPort.PortHConfig.PORT), Is.EqualTo(0)); + Assert.That(portH.GetPinState(4), Is.EqualTo(PinState.Input)); + }); + } +} diff --git a/tests/Avr8Sharp.Tests/SpiTests.cs b/tests/Avr8Sharp.Tests/SpiTests.cs index 6d61024..5d0bc76 100644 --- a/tests/Avr8Sharp.Tests/SpiTests.cs +++ b/tests/Avr8Sharp.Tests/SpiTests.cs @@ -361,4 +361,28 @@ public void DoubleBuffering () Cpu.Tick(); Assert.That(Cpu.ReadData(SPDR), Is.EqualTo(0x88)); } + + + [Test (Description = "SPIF and WCOL are read-only; only SPI2X is writable in SPSR")] + public void SpsrOnlySpi2xWritable () + { + Cpu.Mmio.Data[SPSR] = SPIF | WCOL; + Cpu.WriteData(SPSR, SPI2X); + Assert.That(Cpu.Mmio.Data[SPSR], Is.EqualTo(SPIF | WCOL | SPI2X)); + + Cpu.WriteData(SPSR, 0); + Assert.That(Cpu.Mmio.Data[SPSR], Is.EqualTo(SPIF | WCOL)); + } + + [Test (Description = "SPIF is cleared by reading SPSR with SPIF set and then accessing SPDR")] + public void SpifClearedBySpsrThenSpdrRead () + { + Cpu.Mmio.Data[SPSR] = SPIF; + Cpu.ReadData(SPDR); + Assert.That(Cpu.Mmio.Data[SPSR] & SPIF, Is.EqualTo(SPIF), "SPDR read alone does not clear SPIF"); + + Cpu.ReadData(SPSR); + Cpu.ReadData(SPDR); + Assert.That(Cpu.Mmio.Data[SPSR] & SPIF, Is.Zero); + } } diff --git a/tests/Avr8Sharp.Tests/TimerAttinyTests.cs b/tests/Avr8Sharp.Tests/TimerAttinyTests.cs new file mode 100644 index 0000000..c0af587 --- /dev/null +++ b/tests/Avr8Sharp.Tests/TimerAttinyTests.cs @@ -0,0 +1,483 @@ +using AVR8Sharp.Core.Peripherals; +using Avr8Sharp.Tests.Utils; + +namespace Avr8Sharp.Tests; + +/// +/// ATtiny25/45/85 Timer/Counter1. The first group is ported from avr8js timer-attiny.spec.ts +/// (commit 82f9fa5); the rest covers the register layout and the PWM and output-compare +/// behaviour that the generic ATmega timer cannot model. +/// +[TestFixture] +public class TimerAttiny : AvrTestBase +{ + const int PINB = 0x36; + const int DDRB = 0x37; + const int PORTB = 0x38; + + const int TCCR1 = 0x50; + const int GTCCR = 0x4c; + const int TCNT1 = 0x4f; + const int OCR1A = 0x4e; + const int OCR1B = 0x4b; + const int OCR1C = 0x4d; + const int TIFR = 0x58; + const int TIMSK = 0x59; + + const int TOV1 = 1 << 2; + const int OCF1A = 1 << 6; + const int OCF1B = 1 << 5; + const int OCIE1A = 1 << 6; + const int TOIE1 = 1 << 2; + + const int CTC1 = 1 << 7; + const int PWM1A = 1 << 6; + const int COM1A1 = 1 << 5; + const int COM1A0 = 1 << 4; + const int CS10 = 1; + const int CS13 = 1 << 3; + + const int PWM1B = 1 << 6; + const int COM1B1 = 1 << 5; + const int FOC1A = 1 << 2; + const int PSR1 = 1 << 1; + + const int PB1 = 1; + const int PB4 = 4; + + private AvrIoPort _portB = null!; + private AvrAttinyTimer1 _timer = null!; + + protected override int FlashByteCount => 0x1000; + + protected override void SetupPeripherals() + { + _portB = new AvrIoPort(Cpu, new AvrPortConfig(pin: PINB, ddr: DDRB, port: PORTB)); + _timer = new AvrAttinyTimer1(Cpu); + } + + private void Start(ulong atCycle = 1) + { + Cpu.Cycles = atCycle; + Cpu.Tick(); + } + + // Advances one cycle at a time, like the CPU loop does, so that no compare match is skipped. + private void RunTo(ulong cycle) + { + while (Cpu.Cycles < cycle) + { + Cpu.Cycles++; + Cpu.Tick(); + } + } + + // ── Ported from avr8js ──────────────────────────────────────────────────── + + [Test(Description = "Should update timer every tick when prescaler is 1 (CS=1)")] + public void Prescaler1() + { + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(2); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(1)); + } + + [Test(Description = "Should update timer every 128 ticks when prescaler is 128 (CS=8)")] + public void Prescaler128() + { + Cpu.WriteData(TCCR1, CS13); + Start(1); + RunTo(1 + 128); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(1)); + } + + [Test(Description = "Should not update timer when disabled (CS=0)")] + public void Disabled() + { + Cpu.WriteData(TCCR1, 0); + Start(1); + RunTo(100000); + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + } + + [Test(Description = "Should clear timer on OCR1C match when CTC1 is set")] + public void CtcClearsOnOcr1c() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Cpu.WriteData(TCNT1, 8); + Start(1); + RunTo(1 + 3); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(1)); + } + + [Test(Description = "Should set TOV1 when timer overflows past OCR1C")] + public void CtcSetsTov1() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(TCNT1, 9); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Start(1); + RunTo(2); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + }); + } + + [Test(Description = "Should set OCF1A when timer matches OCR1A")] + public void CtcSetsOcf1a() + { + Cpu.WriteData(OCR1C, 249); + Cpu.WriteData(OCR1A, 5); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Cpu.WriteData(TCNT1, 4); + Start(1); + RunTo(3); + Assert.That(Cpu.Mmio.Data[TIFR] & OCF1A, Is.EqualTo(OCF1A)); + } + + [Test(Description = "Should set OCF1B when timer matches OCR1B")] + public void CtcSetsOcf1b() + { + Cpu.WriteData(OCR1C, 249); + Cpu.WriteData(OCR1B, 10); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Cpu.WriteData(TCNT1, 9); + Start(1); + RunTo(3); + Assert.That(Cpu.Mmio.Data[TIFR] & OCF1B, Is.EqualTo(OCF1B)); + } + + [Test(Description = "Should fire the COMPA interrupt when enabled")] + public void CompAInterrupt() + { + Cpu.WriteData(OCR1C, 249); + Cpu.WriteData(OCR1A, 0); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Cpu.WriteData(TCNT1, 248); + Cpu.WriteData(TIMSK, OCIE1A); + Cpu.Mmio.Data[SREG] = 0x80; + Start(1); + RunTo(3); + Assert.That(Cpu.Pc, Is.EqualTo(0x03)); + } + + [Test(Description = "Should overflow after a full period with prescaler 128")] + public void FullPeriodPrescaler128() + { + Cpu.WriteData(TCCR1, CTC1 | CS13); + Cpu.WriteData(OCR1C, 249); + Cpu.WriteData(TIMSK, OCIE1A); + Cpu.Mmio.Data[SREG] = 0x80; + + // Full timer period: 250 * 128 = 32000 cycles + Start(1); + RunTo(32001); + + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Not.Zero); + } + + [Test(Description = "Should clear TOV1 by writing 1 to TIFR")] + public void ClearTov1() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(TCNT1, 9); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Start(1); + RunTo(2); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + Cpu.WriteData(TIFR, TOV1); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Zero); + } + + // ── Register layout and normal mode ─────────────────────────────────────── + + [Test(Description = "OCR1C resets to 0xFF")] + public void Ocr1cResetValue() + { + Assert.That(Cpu.ReadData(OCR1C), Is.EqualTo(0xff)); + } + + [Test(Description = "Normal mode overflows from 0xFF and ignores OCR1C")] + public void NormalModeOverflow() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(TCNT1, 0xff); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(2); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + }); + } + + [Test(Description = "Overflow interrupt uses TOV1/TOIE1 at bit 2 and vector 4")] + public void OverflowInterrupt() + { + Cpu.WriteData(TIMSK, TOIE1); + Cpu.Mmio.Data[SREG] = 0x80; + Cpu.WriteData(TCNT1, 0xff); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(2); + Cpu.Tick(); + Assert.Multiple(() => + { + Assert.That(Cpu.Pc, Is.EqualTo(0x04)); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Zero); + }); + } + + [Test(Description = "TOIE1 is TIMSK bit 2: bit 4 (OCIE0A) does not enable the Timer1 overflow")] + public void Bit4DoesNotEnableOverflow() + { + Cpu.WriteData(TIMSK, 1 << 4); + Cpu.Mmio.Data[SREG] = 0x80; + Cpu.WriteData(TCNT1, 0xff); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(2); + Cpu.Tick(); + Assert.That(Cpu.Pc, Is.Not.EqualTo(0x04)); + } + + [Test(Description = "Prescalers above /64 follow 2^(CS-1) up to /16384")] + public void PrescalerTable() + { + for (var cs = 1; cs <= 15; cs++) + { + Cpu.Reset(); + Cpu.Cycles = 0; + Cpu.WriteData(TCCR1, (byte)cs); + Start(1); + RunTo(1 + (1UL << (cs - 1)) * 3); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(3), $"CS1={cs}"); + } + } + + [Test(Description = "PSR1 restarts the prescaler without leaving a bit set in GTCCR")] + public void Psr1ResetsPrescaler() + { + Cpu.WriteData(TCCR1, CS13); // /128 + Start(1); + RunTo(100); + Cpu.WriteData(GTCCR, PSR1); + RunTo(100 + 127); + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + RunTo(100 + 128); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(1)); + Assert.That(Cpu.Mmio.Data[GTCCR] & PSR1, Is.Zero); + }); + } + + [Test(Description = "A TCNT1 above OCR1C counts up to 0xFF before wrapping")] + public void CounterAboveTop() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(TCNT1, 250); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Start(1); + RunTo(1 + 5); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(255)); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Zero); + }); + RunTo(1 + 6); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + }); + } + + [Test(Description = "Clearing a flag with TIFR leaves the other TC1 flags and foreign bits alone")] + public void TifrClearsOnlyWrittenFlags() + { + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(OCR1A, 0); + Cpu.WriteData(TCNT1, 9); + Cpu.WriteData(TCCR1, CTC1 | CS10); + Start(1); + RunTo(2); + Cpu.Mmio.Data[TIFR] |= 1 << 1; // TOV0, owned by Timer0 + Cpu.WriteData(TIFR, TOV1); + Assert.Multiple(() => + { + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Zero); + Assert.That(Cpu.Mmio.Data[TIFR] & OCF1A, Is.EqualTo(OCF1A)); + Assert.That(Cpu.Mmio.Data[TIFR] & (1 << 1), Is.EqualTo(1 << 1)); + }); + } + + [Test(Description = "Sharing TIFR with an ATmega-style Timer0 does not lose TC1 flags")] + public void SharedTifrWithTimer0() + { + // Timer0 first, like ATtiny85Simulation mounts them. + Cpu = new AVR8Sharp.Core.Cpu(new ushort[0x1000], SramByteCount); + var timer0 = new AvrTimer(Cpu, AvrTimer.Timer0Config.CreateNew( + tov: 2, ocfa: 16, ocfb: 8, toie: 2, ociea: 16, ocieb: 8, + tccra: 0x4a, tccrb: 0x53, tcnt: 0x52, ocra: 0x49, ocrb: 0x48, + tifr: TIFR, timsk: TIMSK)); + _timer = new AvrAttinyTimer1(Cpu); + Cpu.WriteData(TCNT1, 0xff); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(2); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + Cpu.WriteData(TIFR, 1 << 1); // clear TOV0 only + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + } + + // ── Output compare ──────────────────────────────────────────────────────── + + [Test(Description = "COM1A = 10 clears OC1A (PB1) on compare match in normal mode")] + public void CompareMatchClearsPin() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(PORTB, 1 << PB1); + Cpu.WriteData(OCR1A, 5); + Cpu.WriteData(TCCR1, COM1A1 | CS10); + Start(1); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.High)); + RunTo(1 + 5); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.Low)); + } + + [Test(Description = "COM1A = 01 toggles OC1A on compare match")] + public void CompareMatchTogglesPin() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(OCR1C, 9); + Cpu.WriteData(OCR1A, 3); + Cpu.WriteData(TCCR1, CTC1 | COM1A0 | CS10); + Start(1); + var first = _portB.GetPinState(PB1); + RunTo(1 + 3); + var second = _portB.GetPinState(PB1); + RunTo(1 + 13); + var third = _portB.GetPinState(PB1); + Assert.Multiple(() => + { + Assert.That(second, Is.Not.EqualTo(first)); + Assert.That(third, Is.EqualTo(first)); + }); + } + + [Test(Description = "COM1A = 00 leaves PB1 under PORTB control (TCCR1 writes do not read r0 as the mode)")] + public void NoCompareOutputWithoutCom() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(PORTB, 0); + Cpu.Mmio.Data[0] = 0xff; // r0, what a missing TCCRA mapping used to be read from + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(300); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.Low)); + } + + [Test(Description = "FOC1A forces a compare match on OC1A in non-PWM mode and reads back as zero")] + public void ForceOutputCompare() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(TCCR1, COM1A1 | COM1A0 | CS10); // set on match + Cpu.WriteData(GTCCR, FOC1A); + Assert.Multiple(() => + { + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.High)); + Assert.That(Cpu.Mmio.Data[GTCCR] & FOC1A, Is.Zero); + }); + } + + [Test(Description = "COM1B in GTCCR drives OC1B (PB4) on compare match")] + public void CompareMatchPinB() + { + Cpu.WriteData(DDRB, 1 << PB4); + Cpu.WriteData(PORTB, 1 << PB4); + Cpu.WriteData(OCR1B, 4); + Cpu.WriteData(GTCCR, COM1B1); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(1 + 4); + Assert.That(_portB.GetPinState(PB4), Is.EqualTo(PinState.Low)); + } + + // ── PWM ─────────────────────────────────────────────────────────────────── + + [Test(Description = "PWM1A with COM1A = 10 sets OC1A at the period start and clears it at OCR1A")] + public void PwmA() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(OCR1C, 99); + Cpu.WriteData(OCR1A, 25); + Cpu.WriteData(TCCR1, PWM1A | COM1A1 | CS10); + Start(1); + RunTo(1 + 10); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.High), "before the match"); + RunTo(1 + 25); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.Low), "after the match"); + RunTo(1 + 100 + 5); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.High), "next period"); + } + + [Test(Description = "PWM1A counts 0..OCR1C (single slope) and sets TOV1 at the wrap")] + public void PwmSingleSlope() + { + Cpu.WriteData(OCR1C, 99); + Cpu.WriteData(TCCR1, PWM1A | CS10); + Start(1); + RunTo(1 + 99); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(99)); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.Zero); + RunTo(1 + 100); + Assert.Multiple(() => + { + Assert.That(Cpu.ReadData(TCNT1), Is.Zero); + Assert.That(Cpu.Mmio.Data[TIFR] & TOV1, Is.EqualTo(TOV1)); + }); + } + + [Test(Description = "COM1A = 11 inverts the PWM output")] + public void PwmInverting() + { + Cpu.WriteData(DDRB, 1 << PB1); + Cpu.WriteData(OCR1C, 99); + Cpu.WriteData(OCR1A, 25); + Cpu.WriteData(TCCR1, PWM1A | COM1A1 | COM1A0 | CS10); + Start(1); + RunTo(1 + 10); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.Low)); + RunTo(1 + 25); + Assert.That(_portB.GetPinState(PB1), Is.EqualTo(PinState.High)); + } + + [Test(Description = "PWM1B (GTCCR) drives OC1B (PB4) with OCR1B and OCR1C as TOP")] + public void PwmB() + { + Cpu.WriteData(DDRB, 1 << PB4); + Cpu.WriteData(OCR1C, 99); + Cpu.WriteData(OCR1B, 40); + Cpu.WriteData(GTCCR, PWM1B | COM1B1); + Cpu.WriteData(TCCR1, CS10); + Start(1); + RunTo(1 + 10); + Assert.That(_portB.GetPinState(PB4), Is.EqualTo(PinState.High)); + RunTo(1 + 40); + Assert.That(_portB.GetPinState(PB4), Is.EqualTo(PinState.Low)); + } + + [Test(Description = "A TCNT1 write on a stopped timer is visible when it is read back")] + public void TcntWrite() + { + Cpu.WriteData(TCNT1, 77); + Cpu.Tick(); + Assert.That(Cpu.ReadData(TCNT1), Is.EqualTo(77)); + } +} diff --git a/tests/Avr8Sharp.Tests/TimerConfigTests.cs b/tests/Avr8Sharp.Tests/TimerConfigTests.cs new file mode 100644 index 0000000..3503c11 --- /dev/null +++ b/tests/Avr8Sharp.Tests/TimerConfigTests.cs @@ -0,0 +1,52 @@ +using AVR8Sharp.Core.Peripherals; + +namespace Avr8Sharp.Tests; + +[TestFixture] +public class TimerConfigTests +{ + [Test] + public void CreateNew_KeepsSourceValues_WhenNothingIsPassed() + { + var copy = AvrTimer.Timer1Config.CreateNew(); + + Assert.That(copy.ComparatorPinA, Is.EqualTo(AvrTimer.Timer1Config.ComparatorPinA)); + Assert.That(copy.ComparatorPortB, Is.EqualTo(AvrTimer.Timer1Config.ComparatorPortB)); + Assert.That(copy.ICF, Is.EqualTo(AvrTimer.Timer1Config.ICF)); + Assert.That(copy.TCNT, Is.EqualTo(AvrTimer.Timer1Config.TCNT)); + } + + [Test] + public void CreateNew_HonoursExplicitZero_ForPinAndBit() + { + var copy = AvrTimer.Timer1Config.CreateNew(comparatorPinA: 0, externalClockPin: 0, tov: 0); + + Assert.That(AvrTimer.Timer1Config.ComparatorPinA, Is.Not.Zero); + Assert.That(copy.ComparatorPinA, Is.Zero); + Assert.That(copy.ExternalClockPin, Is.Zero); + Assert.That(copy.TOV, Is.Zero); + } + + [Test] + public void Mega2560_TimerPins_FollowTheDatasheet() + { + var portB = AvrIoPort.Mega2560PortBConfig.PORT; + + Assert.That(AvrTimer.Mega2560Timer0Config.ComparatorPortA, Is.EqualTo(portB)); + Assert.That(AvrTimer.Mega2560Timer0Config.ComparatorPinA, Is.EqualTo(7)); + Assert.That(AvrTimer.Mega2560Timer0Config.ComparatorPortB, Is.EqualTo(AvrIoPort.Mega2560PortGConfig.PORT)); + Assert.That(AvrTimer.Mega2560Timer0Config.ComparatorPinB, Is.EqualTo(5)); + Assert.That(AvrTimer.Mega2560Timer1Config.ComparatorPinC, Is.EqualTo(7)); + Assert.That(AvrTimer.Mega2560Timer1Config.OCRC, Is.EqualTo(0x8c)); + Assert.That(AvrTimer.Mega2560Timer2Config.ComparatorPortB, Is.EqualTo(AvrIoPort.Mega2560PortHConfig.PORT)); + Assert.That(AvrTimer.Mega2560Timer2Config.ComparatorPinB, Is.EqualTo(6)); + Assert.That(AvrTimer.Mega2560Timer4Config.ExternalClockPin, Is.EqualTo(7)); + Assert.That(AvrTimer.Mega2560Timer3Config.IcpPin, Is.EqualTo(7)); + Assert.That(AvrTimer.Mega2560Timer4Config.IcpPort, Is.EqualTo(AvrIoPort.Mega2560PortLConfig.PORT)); + foreach (var t in new[] { AvrTimer.Mega2560Timer1Config, AvrTimer.Mega2560Timer3Config, AvrTimer.Mega2560Timer4Config, AvrTimer.Mega2560Timer5Config }) + { + Assert.That(t.ICF, Is.EqualTo(0x20)); + Assert.That(t.ICIE, Is.EqualTo(0x20)); + } + } +} diff --git a/tests/Avr8Sharp.Tests/TimerTests.cs b/tests/Avr8Sharp.Tests/TimerTests.cs index cb9e0de..03aa6fb 100644 --- a/tests/Avr8Sharp.Tests/TimerTests.cs +++ b/tests/Avr8Sharp.Tests/TimerTests.cs @@ -204,10 +204,10 @@ public void Timer0OverflowInterrupt () var tcnt = Cpu.ReadData(TCNT0); Assert.Multiple(() => { - Assert.That(tcnt, Is.EqualTo(3)); // TCNT = 3 (one tick + 3 interrupt-dispatch cycles, AVR spec) + Assert.That(tcnt, Is.EqualTo(4)); // TCNT = 4 (one tick + 4 interrupt-dispatch cycles, AVR spec) Assert.That(Cpu.ReadData(TIFR0) & TOV0, Is.Zero); Assert.That(Cpu.Pc, Is.EqualTo(0x20)); - Assert.That(Cpu.Cycles, Is.EqualTo(5)); // cycles: 2 (tick) + 3 (DoAvrInterrupt) = 5 + Assert.That(Cpu.Cycles, Is.EqualTo(6)); // cycles: 2 (tick) + 4 (DoAvrInterrupt) = 6 }); } @@ -230,10 +230,10 @@ public void Timer0Override () var tcnt = Cpu.ReadData(TCNT0); Assert.Multiple(() => { - Assert.That(tcnt, Is.EqualTo(3)); // TCNT = 3 (one tick + 3 interrupt-dispatch cycles, AVR spec) + Assert.That(tcnt, Is.EqualTo(4)); // TCNT = 4 (one tick + 4 interrupt-dispatch cycles, AVR spec) Assert.That(Cpu.ReadData(TIFR0) & 2, Is.Zero); Assert.That(Cpu.Pc, Is.EqualTo(0x20)); - Assert.That(Cpu.Cycles, Is.EqualTo(5)); // cycles: 2 (tick) + 3 (DoAvrInterrupt) = 5 + Assert.That(Cpu.Cycles, Is.EqualTo(6)); // cycles: 2 (tick) + 4 (DoAvrInterrupt) = 6 }); } @@ -465,10 +465,10 @@ public void Timer0OutputCompareMatchInterruptA () var tcnt = Cpu.ReadData(TCNT0); Assert.Multiple(() => { - Assert.That(tcnt, Is.EqualTo(0x24)); // TCNT = 0x24 (one tick + 3 interrupt-dispatch cycles, AVR spec) + Assert.That(tcnt, Is.EqualTo(0x25)); // TCNT = 0x25 (one tick + 4 interrupt-dispatch cycles, AVR spec) Assert.That(Cpu.ReadData(TIFR0) & OCF0A, Is.Zero); Assert.That(Cpu.Pc, Is.EqualTo(0x1c)); - Assert.That(Cpu.Cycles, Is.EqualTo(5)); // cycles: 2 (tick) + 3 (DoAvrInterrupt) = 5 + Assert.That(Cpu.Cycles, Is.EqualTo(6)); // cycles: 2 (tick) + 4 (DoAvrInterrupt) = 6 }); } @@ -507,10 +507,10 @@ public void Timer0OutputCompareMatchInterruptB () var tcnt = Cpu.ReadData(TCNT0); Assert.Multiple(() => { - Assert.That(tcnt, Is.EqualTo(0x24)); // TCNT = 0x24 (one tick + 3 interrupt-dispatch cycles, AVR spec) + Assert.That(tcnt, Is.EqualTo(0x25)); // TCNT = 0x25 (one tick + 4 interrupt-dispatch cycles, AVR spec) Assert.That(Cpu.ReadData(TIFR0) & OCF0B, Is.Zero); Assert.That(Cpu.Pc, Is.EqualTo(0x1e)); - Assert.That(Cpu.Cycles, Is.EqualTo(5)); // cycles: 2 (tick) + 3 (DoAvrInterrupt) = 5 + Assert.That(Cpu.Cycles, Is.EqualTo(6)); // cycles: 2 (tick) + 4 (DoAvrInterrupt) = 6 }); } @@ -1273,10 +1273,10 @@ public void Timer1OverflowInterrupt () Cpu.ReadData(TCNT1); // Refresh TCNT1 Assert.Multiple(() => { - Assert.That(Cpu.Mmio.DataView.GetUint16(TCNT1, true), Is.EqualTo(3)); // TCNT = 3 (3 interrupt-dispatch cycles) + Assert.That(Cpu.Mmio.DataView.GetUint16(TCNT1, true), Is.EqualTo(4)); // TCNT = 4 (4 interrupt-dispatch cycles) Assert.That(Cpu.ReadData(TIFR1) & TOV1, Is.Zero); Assert.That(Cpu.Pc, Is.EqualTo(0x1a)); - Assert.That(Cpu.Cycles, Is.EqualTo(6)); // cycles: 3 (pre-interrupt) + 3 (DoAvrInterrupt) = 6 + Assert.That(Cpu.Cycles, Is.EqualTo(7)); // cycles: 3 (pre-interrupt) + 4 (DoAvrInterrupt) = 7 }); } @@ -2051,4 +2051,16 @@ public void Timer5_InputCapture () }); } } + + + [Test (Description = "TIFR is write-one-to-clear: writing zero keeps flags, writing one clears only that flag")] + public void TifrWriteOneToClear () + { + Cpu.Mmio.Data[TIFR0] = TOV0 | OCF0A; + Cpu.WriteData(TIFR0, 0); + Assert.That(Cpu.Mmio.Data[TIFR0], Is.EqualTo(TOV0 | OCF0A)); + + Cpu.WriteData(TIFR0, OCF0A); + Assert.That(Cpu.Mmio.Data[TIFR0], Is.EqualTo(TOV0)); + } } diff --git a/tests/Avr8Sharp.Tests/TwiTests.cs b/tests/Avr8Sharp.Tests/TwiTests.cs index a0f723b..e9bb0c6 100644 --- a/tests/Avr8Sharp.Tests/TwiTests.cs +++ b/tests/Avr8Sharp.Tests/TwiTests.cs @@ -66,7 +66,7 @@ public void ShouldTriggerInterruptWhenTwintIsSet() Assert.Multiple(() => { Assert.That(Cpu.Pc, Is.EqualTo(0x30)); // 2-wire Serial Interface Vector - Assert.That(Cpu.Cycles, Is.EqualTo(3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That(Cpu.Cycles, Is.EqualTo(4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That(Cpu.ReadData(TWCR) & TWINT, Is.EqualTo(0)); }); } @@ -1117,4 +1117,14 @@ public void SlaveAddressMatch_JumpsToInterruptOnlyWhenEnabled() }); } } -} \ No newline at end of file + + + [Test] + public void TwcrWriteOfZeroKeepsTwint() + { + Cpu.Mmio.Data[TWCR] = TWINT | TWEN; + Cpu.WriteData(TWCR, TWEN | TWIE); + + Assert.That(Cpu.Mmio.Data[TWCR] & TWINT, Is.EqualTo(TWINT)); + } +} diff --git a/tests/Avr8Sharp.Tests/UsartTests.cs b/tests/Avr8Sharp.Tests/UsartTests.cs index a5433b9..f1d18b1 100644 --- a/tests/Avr8Sharp.Tests/UsartTests.cs +++ b/tests/Avr8Sharp.Tests/UsartTests.cs @@ -299,7 +299,7 @@ public void DataRegisterEmptyInterrupt() Assert.Multiple(() => { Assert.That(Cpu.Pc, Is.EqualTo(PC_INT_UDRE)); - Assert.That(Cpu.Cycles, Is.EqualTo(3)); // 3 cycles from DoAvrInterrupt (4 total incl. instruction) + Assert.That(Cpu.Cycles, Is.EqualTo(4)); // 4 cycles from DoAvrInterrupt (interrupt response, excludes the interrupted instruction) Assert.That((Cpu.Mmio.Data[UCSR0A] & UDRE), Is.EqualTo(0)); }); } @@ -315,7 +315,7 @@ public void TxCompleteInterrupt () Assert.Multiple(() => { Assert.That(Cpu.Pc, Is.EqualTo(PC_INT_TXC)); - Assert.That(Cpu.Cycles, Is.EqualTo(1_000_000 + 3)); // 3 cycles from DoAvrInterrupt + Assert.That(Cpu.Cycles, Is.EqualTo(1_000_000 + 4)); // 4 cycles from DoAvrInterrupt Assert.That((Cpu.Mmio.Data[UCSR0A] & TXC), Is.EqualTo(0)); }); } @@ -741,4 +741,33 @@ public void SecondInitLeavesTheTransmitterReady () "the transmitter must still be ready: a firmware polling UDRE would spin forever"); } } + + + [Test(Description = "A UCSRB write that keeps RXEN set (e.g. toggling UDRIE) must not drop a received byte")] + public void UcsrbWriteKeepsPendingRxc() + { + Cpu.WriteData(UCSR0B, RXEN | TXEN); + _usart.WriteByte(0x42, immediate: true); + Cpu.WriteData(UCSR0B, RXEN | TXEN | UDRIE); + Cpu.WriteData(UCSR0B, RXEN | TXEN); + + Assert.Multiple(() => + { + Assert.That(Cpu.Mmio.Data[UCSR0A] & RXC, Is.EqualTo(RXC), "RXC must survive"); + Assert.That(Cpu.ReadData(UDR0), Is.EqualTo(0x42)); + }); + } + + [Test(Description = "Disabling the receiver (RXEN 1->0) flushes the receive buffer and RXC (datasheet 19.7.3)")] + public void DisablingReceiverFlushesBuffer() + { + Cpu.WriteData(UCSR0B, RXEN); + _usart.WriteByte(0x42, immediate: true); + Cpu.WriteData(UCSR0B, 0); + + Assert.That(Cpu.Mmio.Data[UCSR0A] & RXC, Is.Zero); + + Cpu.WriteData(UCSR0B, RXEN); + Assert.That(Cpu.ReadData(UDR0), Is.Zero, "the stale byte must be gone"); + } } diff --git a/tests/Avr8Sharp.Tests/WatchdogTests.cs b/tests/Avr8Sharp.Tests/WatchdogTests.cs index 9a2f0fa..25ead61 100644 --- a/tests/Avr8Sharp.Tests/WatchdogTests.cs +++ b/tests/Avr8Sharp.Tests/WatchdogTests.cs @@ -156,6 +156,7 @@ public void InterruptOnTimeout () nop sei + nop break ").Compile(); @@ -172,6 +173,9 @@ public void InterruptOnTimeout () // Now we skip an extra 8ms. Watchdog should fire and jump to the interrupt handler Cpu.Cycles += 16000 * 8; + runner.RunInstructions (1); // sei + // The instruction after SEI always runs before a pending interrupt is served + Assert.That (Cpu.Pc, Is.Not.EqualTo(INT_WDT)); runner.RunInstructions (1); Assert.That (Cpu.Pc, Is.EqualTo(INT_WDT)); @@ -303,4 +307,16 @@ public void RescheduleOnPrescalerShortened() Assert.That(Cpu.ReadData(MCUSR) & WDRF, Is.EqualTo(WDRF), "WDRF flag must be set"); }); } + + + [Test (Description = "WDIF is write-one-to-clear: writing zero keeps it")] + public void WdifWriteOneToClear () + { + Cpu.Mmio.Data[WDTCSR] = 0x80 | WDIE; + Cpu.WriteData(WDTCSR, WDIE); + Assert.That(Cpu.Mmio.Data[WDTCSR] & 0x80, Is.EqualTo(0x80)); + + Cpu.WriteData(WDTCSR, WDIE | 0x80); + Assert.That(Cpu.Mmio.Data[WDTCSR] & 0x80, Is.Zero); + } } From f3924d1c7ef1ac81fd0e3f7915f5662a8c9d6526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 8 Oct 2026 00:47:00 -0600 Subject: [PATCH 2/2] docs(changelog): emulator fidelity fixes --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2173ad7..e5549ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ All notable changes to this project will be documented in this file. --- +## [Unreleased] + +Fidelity fixes from an audit of the CPU, the peripherals and the iCircuit integration. +Several change cycle counts or register values that a test may have pinned; each entry says so. + +### Bug Fixes + +- **Intel HEX above 64 KB.** `AvrRunner.LoadHex` ignored the extended segment (02) and + extended linear (04) records, so a Mega sketch larger than 64 KB wrote its upper half over + the start of flash, vectors included. A shared `IntelHex` parser now handles 00/01/02/04, + skips 03/05 and verifies checksums. `LoadHex` stays lenient; the new + `TryLoadHex(hex, out HexInfo)` is strict and leaves flash untouched when the image has + errors or does not fit. +- **`Cpu.Reset()` is a real reset.** It only reset SP, SREG and PC, so I/O registers and + peripheral state survived: after a firmware reload `Serial.write` could hang (TXEN was still + set, so UDRE never came back) and pins stayed outputs. The I/O space from 0x20 to the new + `Cpu.RamStart` now returns to its reset values (UCSRnA 0x20, UCSRnC 0x06, TWSR 0xF8, the rest + 0), every peripheral resets its internal state, and port listeners see the pins go back to + input. SRAM and r0-r31 are kept, as on the chip. `RamStart` defaults to 0x200 on 22-bit-PC + parts and 0x100 otherwise; `AvrBuilder.WithRamStart` and the TestKit boards set it. +- **Interrupt response time.** Entry cost 3 cycles; the datasheet gives 4, and 5 on parts with + a 22-bit PC. Every ISR timing moves by one cycle (two on the Mega). +- **One instruction after SEI and RETI.** A pending interrupt was served right after `sei`, + `reti` or a write that set I, so `sei; sleep` raced and an always-pending interrupt starved + the main loop. The next instruction now always runs first, in every decoder. +- **USART loses a received byte while transmitting.** Any UCSRnB write with RXEN set cleared + RXC; Arduino's HardwareSerial toggles UDRIE on every byte it sends. RXC and the receive + buffer are now flushed only when RXEN goes from 1 to 0. Inherited from avr8js. +- **Write-one-to-clear flags.** TIFRn stored the written value, so clearing one flag wiped + the others; ADIF was stored instead of cleared by a written one; SPIF and WCOL were + writable, and SPIF did not clear on SPSR-then-SPDR. TWINT, WDIF and EEPE were lost on + writes that left them at 0. All now follow the datasheet. +- **ATmega2560 configs.** Timer0/1/2 drove the 328P compare-output pins, so `analogWrite` on + Mega pins 4, 9, 10, 11, 12 and 13 hit the wrong pin or none. Timer1 had no channel C, and + Timer3/4 had no input capture. INT0-7 did not exist and the pin-change interrupts went to + 328P vectors, so `attachInterrupt` and SoftwareSerial RX were dead on the Mega. +- **Channel C in fast PWM** never set its pin at BOTTOM, so OCnC PWM did not toggle. +- **ATtiny85 EEPROM.** EEARH was mapped to address 0x00 (r0), so `eeprom_write_byte` with + interrupts enabled threw. It is now 0x3F, and the memory backend wraps addresses beyond the + EEPROM size, as the chip does. +- **ATtiny85 Timer1** used the generic ATmega timer: TOV1 on the wrong bit, TCCR1 read r0 as + TCCRA (PB1 froze high), no CTC1, PWM1A/B, OCR1C or prescalers above /64. + `ATtiny85Simulation.Timer1` is now an `AvrAttinyTimer1`, ported from avr8js `timer-attiny.ts`. +- **EEPROM write time** was fixed in 16 MHz cycles; it now scales with the CPU frequency + (3.4 ms erase and write, 1.8 ms either alone). + +### New + +- `Mega2560Config` in the core: `AvrTimer.Mega2560Timer0Config` to `Timer5Config`, + `AvrUsart.Mega2560Usart0Config` to `Usart3Config`, `AvrSpi`, `AvrTwi`, `AvrEeprom`, + `AvrWatchdog` and `AvrAdc.Mega2560*Config`, and `AvrIoPort.Mega2560PortAConfig` to + `PortLConfig` with INT0-7 and PCINT0-2. `ArduinoMegaSimulation` uses them. +- Timers sample their ICPn pin when the config names one (Mega Timer1/3/4/5), on the edge + selected by ICESn. +- `AvrTimerConfig.CreateNew` takes nullable arguments, so an explicit 0 can be set. +- `AvrAdc.Avcc` and `Aref` are settable (5 V by default). +- `Cpu.OnInterruptDispatch`, `OnBreakpoint` and `OnSleep` are per-CPU events; the static + `AvrInterrupt` hooks still work but are obsolete, because they fire for every CPU in the + process. +- `AvrRunner.Run(budget)`, `RunUntil(cycle)` and `RunCycles(budget)` carry the target across + calls, so stepping in small slices no longer drifts. `Execute()` uses the same target. + +--- + ## [v1.1.0] - 2026-10-07 ### Bug Fixes