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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

- `Mega2560<Peripheral>Config` 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
Expand Down
3 changes: 2 additions & 1 deletion src/Avr8Sharp.Native/Exports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Avr8Sharp.TestKit.Core/AvrTestSimulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,15 @@ 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;
}

/// <summary>Adds an EEPROM peripheral (no out handle).</summary>
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;
}

Expand Down
1 change: 1 addition & 0 deletions src/Avr8Sharp.TestKit.Core/Boards/ATtiny13Simulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public ATtiny13Simulation() : base(Flash, Sram)
{
WithFrequency(Frequency);
Cpu.StackLowLimit = 0x60;
Cpu.RamStart = 0x60;

AddGpio(PortBConfig, out var pb); PortB = pb;
}
Expand Down
71 changes: 19 additions & 52 deletions src/Avr8Sharp.TestKit.Core/Boards/ATtiny85Simulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ namespace Avr8Sharp.TestKit.Boards;
/// <see cref="AvrTestSimulation.AddUsart"/> if needed.
/// </para>
/// <para>
/// 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 <see cref="AvrAttinyTimer1"/>, not by the generic ATmega timer.
/// </para>
/// </summary>
/// <example>
Expand Down Expand Up @@ -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 ──────────────────────────────────────────────────────────────────
Expand All @@ -132,10 +98,10 @@ public sealed class ATtiny85Simulation : AvrTestSimulation
public AvrTimer Timer0 { get; }

/// <summary>
/// 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.
/// </summary>
public AvrTimer Timer1 { get; }
public AvrAttinyTimer1 Timer1 { get; }

/// <summary>ATtiny85 internal EEPROM — 512 bytes, volatile (in-memory backend).</summary>
public AvrEeprom Eeprom { get; }
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/Avr8Sharp.TestKit.Core/Boards/ATtinyX4Simulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading