From 3b6d26e05ca936fc9654e97910e25a363b9cc745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Thu, 8 Oct 2026 01:27:49 -0600 Subject: [PATCH] feat: implement SiliconTwin.Abstractions (AvrMcu, batch GPIO, pin events, ADC pull, PWM info) AvrBuilder.BuildMcu returns an AvrMcu over everything the builder mounted: IMcu (cycle-budget Run, Reset, Load), one IPinBank per port with batch SetInputs/ReleaseInputs and cycle-stamped PinChanged events, IPwmSource from the timer state, IAdcInput with a pull callback, and byte-level UART, SPI and I2C ports. The pieces underneath are public on their own: AvrIoPort.SetInputs, ReleaseInputs, ApplyInputs, OutputEnableMask, OutputLevels, PullUpMask, PinChanged; AvrAdc.ReadChannelVolts; AvrTimer.TryGetPwm; AvrClock.Changed. Exposed over the C ABI and Python. The core is now trimmable and AOT compatible, with a Native AOT smoke test in CI. --- .github/workflows/ci.yml | 9 + CHANGELOG.md | 32 ++ bindings/python/src/avr8sharp/_native.py | 3 + bindings/python/src/avr8sharp/simulation.py | 17 + bindings/python/tests/test_pin_injection.py | 28 ++ src/Avr8Sharp.Native/Exports.cs | 33 ++ src/Avr8Sharp/Avr8Sharp.csproj | 6 + src/Avr8Sharp/Core/Abstractions/AvrMcu.cs | 442 ++++++++++++++++++ src/Avr8Sharp/Core/Peripherals/Adc.cs | 19 +- src/Avr8Sharp/Core/Peripherals/Clock.cs | 5 + src/Avr8Sharp/Core/Peripherals/Gpio.cs | 121 ++++- src/Avr8Sharp/Core/Peripherals/Timer.cs | 48 ++ src/Avr8Sharp/Core/Utils/Builder.cs | 35 ++ .../Avr8Sharp.AotSmoke.csproj | 19 + tests/Avr8Sharp.AotSmoke/Program.cs | 57 +++ tests/Avr8Sharp.Tests/AdcPullTests.cs | 118 +++++ tests/Avr8Sharp.Tests/AvrMcuContractTests.cs | 439 +++++++++++++++++ tests/Avr8Sharp.Tests/GpioBatchInputTests.cs | 263 +++++++++++ 18 files changed, 1686 insertions(+), 8 deletions(-) create mode 100644 src/Avr8Sharp/Core/Abstractions/AvrMcu.cs create mode 100644 tests/Avr8Sharp.AotSmoke/Avr8Sharp.AotSmoke.csproj create mode 100644 tests/Avr8Sharp.AotSmoke/Program.cs create mode 100644 tests/Avr8Sharp.Tests/AdcPullTests.cs create mode 100644 tests/Avr8Sharp.Tests/AvrMcuContractTests.cs create mode 100644 tests/Avr8Sharp.Tests/GpioBatchInputTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c62b1e..d6bbde6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,3 +41,12 @@ jobs: -c Release --no-build --logger "console;verbosity=normal" + + # ── Native AOT smoke ────────────────────────────────────────────────────── + # Publishes the core with Native AOT and runs a blink program: fails if the + # core stops being AOT/trim compatible (IL2xxx/IL3xxx warnings are surfaced too). + - name: AOT smoke + run: | + sudo apt-get update && sudo apt-get install -y clang zlib1g-dev + dotnet publish tests/Avr8Sharp.AotSmoke -r linux-x64 -c Release -o aot-smoke + ./aot-smoke/Avr8Sharp.AotSmoke diff --git a/CHANGELOG.md b/CHANGELOG.md index e5549ce..51f96f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,38 @@ Several change cycle counts or register values that a test may have pinned; each - `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. +### New: the SiliconTwin.Abstractions contract + +The core implements [`SiliconTwin.Abstractions`](https://github.com/silicon-twin/abstractions), +the shared embedder contract of the Silicon Twin emulators (MIT, no dependencies), so a host +drives an AVR the same way it drives an RP2040. + +- `AvrBuilder.BuildMcu(name)` returns an `AvrMcu` (`IMcu`, `IGpio`, `IPwmSource`, `IAdcInput`, + `IPeripheralMap`) over everything the builder mounted. `runner.AsMcu(...)` covers machines + built by hand. +- `IMcu`: `Run(budget)`/`RunUntil` without drift, `Reset(Soft | Power)`, `Load` of Intel HEX + (strict) or a raw binary. +- One `IPinBank` per port: `SetInputs(mask, levels)`, `ReleaseInputs(mask)`, + `OutputEnableMask`, `OutputLevels`, `PullUpMask`, and `PinChanged` with the cycle of the + write and a `WatchMask`. It costs one null check when nobody subscribes. +- `IPwmSource.TryGetPwm`: frequency and duty from the timer's clock select, waveform mode, TOP, + OCR and COM bits (fast, phase-correct and phase-and-frequency-correct, channels A to C). +- `IAdcInput`: `ReadChannelVolts`, a callback the ADC calls only when a conversion samples its + input, instead of pushing `ChannelValues` every step. +- UART (`TxByte`, `TryInjectRx`), SPI (`Transfer`, idle bus reads 0xFF) and I2C targets attached + by address. + +Underneath, also usable directly: + +- `AvrIoPort.SetInputs`, `ReleaseInputs`, `ApplyInputs` (full state in one PIN recompute), + `OutputEnableMask`, `OutputLevels`, `PullUpMask`, `PinChanged` and `WatchMask`. + `SetPinValue` and `ReleasePin` no longer recompute PIN when nothing changes. +- `AvrAdc.ReadChannelVolts`, `AvrTimer.TryGetPwm`, `AvrClock.Changed`, `AvrBuilder.AddClock`. +- C ABI `a8s_gpio_set_inputs`, `a8s_gpio_release_inputs`, `a8s_gpio_apply_inputs`; Python + `Port.set_inputs`, `release_inputs`, `apply_inputs`. +- The core declares `IsTrimmable` and `IsAotCompatible`, and CI publishes and runs a Native AOT + smoke test. + --- ## [v1.1.0] - 2026-10-07 diff --git a/bindings/python/src/avr8sharp/_native.py b/bindings/python/src/avr8sharp/_native.py index b5db097..eb9f6b4 100644 --- a/bindings/python/src/avr8sharp/_native.py +++ b/bindings/python/src/avr8sharp/_native.py @@ -66,6 +66,9 @@ "a8s_gpio_pin": ([c_void_p, c_int, c_int], c_int), "a8s_gpio_set_pin": ([c_void_p, c_int, c_int, c_int], c_int), "a8s_gpio_release_pin": ([c_void_p, c_int, c_int], c_int), + "a8s_gpio_set_inputs": ([c_void_p, c_int, c_int, c_int], c_int), + "a8s_gpio_release_inputs": ([c_void_p, c_int, c_int], c_int), + "a8s_gpio_apply_inputs": ([c_void_p, c_int, c_int, c_int], c_int), # analog / bus peripherals (Uno and Mega sessions) "a8s_adc_set_channel": ([c_void_p, c_int, c_double], c_int), "a8s_spi_queue_response": ([c_void_p, c_byte], c_int), diff --git a/bindings/python/src/avr8sharp/simulation.py b/bindings/python/src/avr8sharp/simulation.py index 3986038..69e3f87 100644 --- a/bindings/python/src/avr8sharp/simulation.py +++ b/bindings/python/src/avr8sharp/simulation.py @@ -114,6 +114,23 @@ def release(self, pin: int) -> None: lib = self._sim._lib _check(lib, self._sim._h, lib.a8s_gpio_release_pin(self._sim._h, self._index, pin), "gpio_release_pin") + def set_inputs(self, mask: int, levels: int) -> None: + """Drives only the pins in `mask` to the matching bits of `levels` in one call; the other + pins keep whatever they had (driven or released).""" + lib = self._sim._lib + _check(lib, self._sim._h, lib.a8s_gpio_set_inputs(self._sim._h, self._index, mask & 0xFF, levels & 0xFF), "gpio_set_inputs") + + def release_inputs(self, mask: int) -> None: + """Stops driving the pins in `mask` in one call, undoing set_inputs().""" + lib = self._sim._lib + _check(lib, self._sim._h, lib.a8s_gpio_release_inputs(self._sim._h, self._index, mask & 0xFF), "gpio_release_inputs") + + def apply_inputs(self, driven_mask: int, levels: int) -> None: + """Sets the whole external state in one call: pins in `driven_mask` are driven to `levels`, + every other pin is released.""" + lib = self._sim._lib + _check(lib, self._sim._h, lib.a8s_gpio_apply_inputs(self._sim._h, self._index, driven_mask & 0xFF, levels & 0xFF), "gpio_apply_inputs") + def _read_buffer(fn, sim: "Simulation", *args) -> bytes: """Two-pass read of a native length-prefixed byte buffer (call with cap, then size exactly).""" diff --git a/bindings/python/tests/test_pin_injection.py b/bindings/python/tests/test_pin_injection.py index 8791c7b..b5804e3 100644 --- a/bindings/python/tests/test_pin_injection.py +++ b/bindings/python/tests/test_pin_injection.py @@ -162,3 +162,31 @@ def test_respond_to_rise_reports_the_wait_before_the_trigger(): assert 9 <= waited <= 12 uno.close() + + +def test_set_inputs_drives_only_the_mask_and_release_inputs_undoes_it(): + sim = a.Simulation.create() + port_d = sim.add_gpio(2) + sim.cpu.write(PORTD, 0x03) # pull-ups on PD0, PD1 + + port_d.set_inputs(0xFF, 0x84) + assert sim.cpu.read(PIND) == 0x84 + + port_d.set_inputs(0x01, 0x01) # PD1..PD7 untouched + assert sim.cpu.read(PIND) == 0x85 + + port_d.release_inputs(0xFC) # PD0, PD1 stay driven + assert sim.cpu.read(PIND) == 0x01 + port_d.release_inputs(0x03) + assert sim.cpu.read(PIND) == 0x03 # pull-ups + + +def test_apply_inputs_sets_the_full_state(): + sim = a.Simulation.create() + port_d = sim.add_gpio(2) + sim.cpu.write(PORTD, 0x03) + port_d.apply_inputs(0xFF, 0x00) + assert sim.cpu.read(PIND) == 0x00 + port_d.apply_inputs(0xFC, 0xFC) + assert sim.cpu.read(PIND) == 0xFF # PD0, PD1 released back to their pull-ups + sim.close() diff --git a/src/Avr8Sharp.Native/Exports.cs b/src/Avr8Sharp.Native/Exports.cs index 3e25b9b..4b09e59 100644 --- a/src/Avr8Sharp.Native/Exports.cs +++ b/src/Avr8Sharp.Native/Exports.cs @@ -393,6 +393,39 @@ public static int GpioSetPin(IntPtr h, int portIdx, int pin, int value) s.Ports[portIdx].SetPinValue((byte)pin, value != 0); }); + /// Drives only the pins in of port to the + /// matching bits of ; other pins are left as they were. + [UnmanagedCallersOnly(EntryPoint = "a8s_gpio_set_inputs")] + public static int GpioSetInputs(IntPtr h, int portIdx, int mask, int levels) + => Run(h, s => + { + if (portIdx < 0 || portIdx >= s.Ports.Count) + throw new ArgumentOutOfRangeException(nameof(portIdx)); + s.Ports[portIdx].SetInputs((byte)mask, (byte)levels); + }); + + /// Stops driving the pins in of port + /// (see a8s_gpio_release_pin), in one call. + [UnmanagedCallersOnly(EntryPoint = "a8s_gpio_release_inputs")] + public static int GpioReleaseInputs(IntPtr h, int portIdx, int mask) + => Run(h, s => + { + if (portIdx < 0 || portIdx >= s.Ports.Count) + throw new ArgumentOutOfRangeException(nameof(portIdx)); + s.Ports[portIdx].ReleaseInputs((byte)mask); + }); + + /// Sets the whole external state of port : pins in + /// are driven to , every other pin is released. + [UnmanagedCallersOnly(EntryPoint = "a8s_gpio_apply_inputs")] + public static int GpioApplyInputs(IntPtr h, int portIdx, int drivenMask, int levels) + => Run(h, s => + { + if (portIdx < 0 || portIdx >= s.Ports.Count) + throw new ArgumentOutOfRangeException(nameof(portIdx)); + s.Ports[portIdx].ApplyInputs((byte)drivenMask, (byte)levels); + }); + /// Stops driving on port , undoing /// a8s_gpio_set_pin: the line floats again, so an input pin with its PORT bit set reads /// 1 through the internal pull-up and any other floating input reads 0. diff --git a/src/Avr8Sharp/Avr8Sharp.csproj b/src/Avr8Sharp/Avr8Sharp.csproj index 5516402..1e82956 100644 --- a/src/Avr8Sharp/Avr8Sharp.csproj +++ b/src/Avr8Sharp/Avr8Sharp.csproj @@ -5,6 +5,8 @@ enable enable true + true + true Avr8Sharp @@ -25,6 +27,10 @@ default + + + + diff --git a/src/Avr8Sharp/Core/Abstractions/AvrMcu.cs b/src/Avr8Sharp/Core/Abstractions/AvrMcu.cs new file mode 100644 index 0000000..b145a76 --- /dev/null +++ b/src/Avr8Sharp/Core/Abstractions/AvrMcu.cs @@ -0,0 +1,442 @@ +using System.Text; +using AVR8Sharp.Core.Peripherals; +using AVR8Sharp.Core.Utils; +using SiliconTwin.Abstractions; + +namespace AVR8Sharp.Core.Abstractions; + +/// +/// Thin adapter that exposes an and its peripherals through the +/// SiliconTwin contracts (, , , +/// , ). It holds no emulation state of its own +/// and does not change how the core behaves. +/// +/// +/// Peripherals are passed in because hands them back through out +/// parameters and the core keeps no registry of them (GPIO ports are the exception: they register +/// themselves in and are discovered automatically). +/// +/// var runner = AvrBuilder.Create() +/// .AddGpioPort(AvrIoPort.PortBConfig, out _) +/// .AddUsart(AvrUsart.Usart0Config, out var usart) +/// .Build(); +/// var mcu = runner.AsMcu("atmega328p", usarts: [usart]); +/// +/// +public sealed class AvrMcu : IMcu, IGpio, IPwmSource, IAdcInput, IPeripheralMap +{ + readonly AvrRunner _runner; + readonly AvrClock? _clock; + readonly AvrWatchdog? _watchdog; + readonly AvrAdc? _adc; + readonly AvrTimer[] _timers; + readonly AvrPinBank[] _banks; + readonly AvrUartPort[] _uarts; + readonly AvrSpiPort[] _spis; + readonly AvrI2cPort[] _i2cs; + Func? _adcCallback; + Action? _clockChanged; + + /// Value of at the last power reset; counts from there. + internal ulong CycleBase { get; private set; } + + /// The machine, normally from . + /// Device name reported by . + /// Clock prescaler peripheral, if mounted: makes follow CLKPR. + /// Watchdog, if mounted: a power reset restores its power-on MCUSR. + /// ADC, if mounted. + /// Timers, used by . + /// USARTs, in order: index 0 is UART0. + /// SPI controllers, in order. + /// TWI controllers, in order. Each one gets its replaced. + public AvrMcu (AvrRunner runner, string name = "avr", AvrClock? clock = null, AvrWatchdog? watchdog = null, + AvrAdc? adc = null, IEnumerable? timers = null, IEnumerable? usarts = null, + IEnumerable? spis = null, IEnumerable? twis = null) + { + ArgumentNullException.ThrowIfNull (runner); + _runner = runner; + Name = name; + _clock = clock; + _watchdog = watchdog; + _adc = adc; + _timers = timers?.ToArray () ?? []; + + // Chip order (PORTA, PORTB, ...) regardless of the order the ports were mounted in + _banks = runner.Cpu.GpioPorts.OrderBy (p => p.Config.PORT).Select (p => new AvrPinBank (this, p)).ToArray (); + _uarts = (usarts ?? []).Select ((u, i) => new AvrUartPort (i, u)).ToArray (); + _spis = (spis ?? []).Select ((s, i) => new AvrSpiPort (i, s)).ToArray (); + _i2cs = (twis ?? []).Select ((t, i) => new AvrI2cPort (i, t)).ToArray (); + + if (_clock != null) _clock.Changed += () => _clockChanged?.Invoke (); + } + + /// The wrapped machine. + public AvrRunner Runner => _runner; + + /// The wrapped CPU. + public Cpu Cpu => _runner.Cpu; + + public string Name { get; } + + public long Cycles => (long)(_runner.Cpu.Cycles - CycleBase); + + public uint ClockHz => _clock?.Frequency ?? _runner.Speed; + + /// Raised when CLKPR changes the clock. Never raised without an . + public event Action? ClockChanged { + add => _clockChanged += value; + remove => _clockChanged -= value; + } + + public long Run (long cycleBudget) => _runner.Run (cycleBudget); + + public long RunUntil (long absoluteCycle) + { + if (absoluteCycle <= Cycles) return 0; + return _runner.RunUntil ((ulong)absoluteCycle + CycleBase); + } + + /// + /// Soft: (registers, peripherals and pending events; SRAM and + /// flash are kept, keeps counting). Power: the same, after clearing the + /// general registers and SRAM, then MCUSR gets its power-on value (if a watchdog is mounted) + /// and restarts from zero. Flash is kept. + /// + public void Reset (ResetKind kind = ResetKind.Soft) + { + var cpu = _runner.Cpu; + if (kind == ResetKind.Power) { + Array.Clear (cpu.Mmio.Data, 0, 32); + Array.Clear (cpu.Mmio.Data, cpu.RamStart, cpu.Mmio.Data.Length - cpu.RamStart); + } + cpu.Reset (); + if (kind == ResetKind.Power) { + _watchdog?.PowerOnReset (); + CycleBase = cpu.Cycles; + } + } + + /// + /// Loads Intel HEX (strict: malformed records, bad checksums or an image past the end of flash + /// throw and leave flash untouched) or a raw binary at + /// (bytes outside the image are kept). Other formats throw + /// . + /// + public void Load (in FirmwareImage image) + { + switch (image.Format) { + case FirmwareFormat.IntelHex: + if (!_runner.TryLoadHex (Encoding.ASCII.GetString (image.Data.Span), out var info)) + throw new FormatException ("Invalid Intel HEX: " + string.Join ("; ", info.Errors)); + break; + case FirmwareFormat.Bin: + var flash = _runner.Cpu.ProgBytes; + if ((long)image.Offset + image.Data.Length > flash.Length) + throw new ArgumentOutOfRangeException (nameof (image), "The image does not fit in flash."); + var merged = (byte[])flash.Clone (); + image.Data.Span.CopyTo (merged.AsSpan ((int)image.Offset)); + _runner.Cpu.LoadProgram (merged); + break; + default: + throw new NotSupportedException ($"Firmware format {image.Format} is not supported by AVR."); + } + } + + public IReadOnlyList Banks => _banks; + + public IReadOnlyList Uarts => _uarts; + public IReadOnlyList Spis => _spis; + public IReadOnlyList I2cs => _i2cs; + + /// Reference voltage currently selected by ADMUX; 0 when no ADC is mounted. + public double ReferenceVolts => _adc?.ReferenceVoltage ?? 0; + + /// Maps to . Ignored when no ADC is mounted. + public Func? ReadChannelVolts { + get => _adcCallback; + set { + _adcCallback = value; + if (_adc != null) _adc.ReadChannelVolts = value; + } + } + + /// + /// PWM of timer compare outputs (fast PWM and phase correct, non-inverting or inverting), from the + /// current timer registers. Returns false for pins that no mounted timer's OCnx output uses. + /// + public bool TryGetPwm (int bank, int pin, out PwmInfo info) + { + info = default; + if ((uint)bank >= (uint)_banks.Length || (uint)pin >= 8) return false; + var portAddress = _banks[bank].Port.Config.PORT; + foreach (var timer in _timers) { + var c = timer.Config; + for (var ch = 0; ch < 3; ch++) { + var (port, cpin) = ch switch { + 0 => (c.ComparatorPortA, c.ComparatorPinA), + 1 => (c.ComparatorPortB, c.ComparatorPinB), + _ => (c.ComparatorPortC, c.ComparatorPinC), + }; + if (port != portAddress || cpin != pin) continue; + if (!timer.TryGetPwm (ch, ClockHz, out var enabled, out var freq, out var duty)) continue; + info = new PwmInfo (enabled, freq, duty); + return true; + } + } + return false; + } + + public void Dispose () + { + } +} + +/// Factory extensions to wrap a built machine as an . +public static class AvrRunnerExtensions +{ + /// Wraps the runner as an ; see its constructor for the parameters. + public static AvrMcu AsMcu (this AvrRunner runner, string name = "avr", AvrClock? clock = null, + AvrWatchdog? watchdog = null, AvrAdc? adc = null, IEnumerable? timers = null, + IEnumerable? usarts = null, IEnumerable? spis = null, IEnumerable? twis = null) + => new (runner, name, clock, watchdog, adc, timers, usarts, spis, twis); +} + +/// One as an of 8 pins. +public sealed class AvrPinBank : IPinBank +{ + readonly AvrMcu _mcu; + readonly AvrPinChangeHandler _forward; + PinChangeHandler? _changed; + + internal AvrPinBank (AvrMcu mcu, AvrIoPort port) + { + _mcu = mcu; + Port = port; + Name = "PORT" + PortLetter (port.Config.PORT); + _forward = (changed, enable, levels, cycle) => { + var e = new PinChange (changed, enable, levels, (long)(cycle - _mcu.CycleBase)); + _changed?.Invoke (in e); + }; + } + + /// The wrapped port. + public AvrIoPort Port { get; } + + public string Name { get; } + public int PinCount => 8; + public ulong OutputEnableMask => Port.OutputEnableMask; + public ulong OutputLevels => Port.OutputLevels; + public ulong PullUpMask => Port.PullUpMask; + public ulong PullDownMask => 0; + + public void SetInputs (ulong mask, ulong levels) => Port.SetInputs ((byte)mask, (byte)levels); + public void ReleaseInputs (ulong mask) => Port.ReleaseInputs ((byte)mask); + + public event PinChangeHandler PinChanged { + add { + if (_changed == null) Port.PinChanged += _forward; + _changed += value; + } + remove { + _changed -= value; + if (_changed == null) Port.PinChanged -= _forward; + } + } + + public ulong WatchMask { + get => Port.WatchMask; + set => Port.WatchMask = (byte)value; + } + + static string PortLetter (ushort portAddress) => portAddress switch { + 0x22 => "A", 0x25 => "B", 0x28 => "C", 0x2b => "D", 0x2e => "E", 0x31 => "F", 0x34 => "G", + 0x102 => "H", 0x105 => "J", 0x108 => "K", 0x10b => "L", + _ => "@" + portAddress.ToString ("x"), + }; +} + +/// One as an . +public sealed class AvrUartPort : IUartPort +{ + readonly AvrUsart _usart; + readonly Action _forward; + Action? _previous; + Action? _tx; + + internal AvrUartPort (int index, AvrUsart usart) + { + Index = index; + _usart = usart; + _forward = value => { + _previous?.Invoke (value); + _tx?.Invoke (value); + }; + } + + public int Index { get; } + + public uint BaudRate => _usart.TxEnable || _usart.RxEnable ? (uint)_usart.BaudRate : 0; + + /// + /// Raised for every transmitted byte. Wraps : a handler + /// already set there keeps being called. + /// + public event Action? TxByte { + add { + if (_tx == null) { + _previous = _usart.OnByteTransmit; + _usart.OnByteTransmit = _forward; + } + _tx += value; + } + remove { + _tx -= value; + if (_tx == null && ReferenceEquals (_usart.OnByteTransmit, _forward)) { + _usart.OnByteTransmit = _previous; + _previous = null; + } + } + } + + /// + /// Delivers a byte through (it arrives after one character + /// time). False when the receiver is disabled or still busy with the previous byte. + /// + public bool TryInjectRx (byte value) => _usart.WriteByte (value); +} + +/// One (master mode) as an . +public sealed class AvrSpiPort : ISpiPort +{ + readonly AvrSpi _spi; + Func? _transfer; + + internal AvrSpiPort (int index, AvrSpi spi) + { + Index = index; + _spi = spi; + _spi.OnTransfer = _ => 0xff; + } + + public int Index { get; } + public int FrameBits => 8; + + /// Maps to ; null makes the bus read 0xFF. + public Func? Transfer { + get => _transfer; + set { + _transfer = value; + _spi.OnTransfer = value == null ? _ => 0xff : mosi => (int)(value (mosi) & 0xff); + } + } +} + +/// +/// One as an . Installs its own +/// that dispatches the firmware's master transactions to the +/// attached targets by address (no target at the address: NACK). +/// +/// +/// Slave side: , and +/// map to the TWI slave simulation. throws +/// because does not model the slave-transmit byte sequence. +/// +public sealed class AvrI2cPort : II2cPort +{ + const int StatusSlaveDataRxAck = 0x80; + + readonly AvrTwi _twi; + readonly Dictionary _targets = new (); + II2cTarget? _active; + Action? _slaveAddressChanged; + + internal AvrI2cPort (int index, AvrTwi twi) + { + Index = index; + _twi = twi; + _twi.EventHandler = new Handler (this); + } + + public int Index { get; } + + public void AttachTarget (byte addr7, II2cTarget target) + { + ArgumentNullException.ThrowIfNull (target); + _targets[addr7] = target; + } + + public void DetachTarget (byte addr7) => _targets.Remove (addr7); + + public byte SlaveAddress => _twi.SlaveAddress; + + public event Action? SlaveAddressChanged { + add { + if (_slaveAddressChanged == null) _twi.SlaveAddressChanged += Forward; + _slaveAddressChanged += value; + } + remove { + _slaveAddressChanged -= value; + if (_slaveAddressChanged == null) _twi.SlaveAddressChanged -= Forward; + } + } + + void Forward (byte address) => _slaveAddressChanged?.Invoke (address); + + public bool BeginMasterTransfer (byte addr7, bool write) => _twi.SimulateIncomingAddress (addr7, write); + + /// False when the firmware answered with NACK (TWEA clear) instead of status 0x80. + public bool MasterWrite (byte data) + { + _twi.SimulateIncomingData (data); + return _twi.Status == StatusSlaveDataRxAck; + } + + public bool TryMasterRead (out byte data) + => throw new NotSupportedException ("AvrTwi does not model slave-transmit sequencing."); + + public void MasterStop () => _twi.SimulateIncomingStop (); + + sealed class Handler (AvrI2cPort port) : ITwiEventHandler + { + public void Start (bool repeated) + { + if (repeated) EndTransaction (); + port._twi.CompleteStart (); + } + + public void Stop () + { + EndTransaction (); + port._twi.CompleteStop (); + } + + public void ConnectToSlave (byte address, bool write) + { + EndTransaction (); + if (port._targets.TryGetValue (address, out var target) && target.Probe (address, write)) { + port._active = target; + port._twi.CompleteConnect (true); + } else { + port._twi.CompleteConnect (false); + } + } + + public void WriteByte (byte data) + { + if (port._active == null) { + port._twi.CompleteWrite (false); + return; + } + port._active.Write (data); + port._twi.CompleteWrite (true); + } + + public void ReadByte (bool ack) => port._twi.CompleteRead (port._active?.Read () ?? 0xff); + + void EndTransaction () + { + port._active?.Stop (); + port._active = null; + } + } +} diff --git a/src/Avr8Sharp/Core/Peripherals/Adc.cs b/src/Avr8Sharp/Core/Peripherals/Adc.cs index 5c20c60..30ba529 100644 --- a/src/Avr8Sharp/Core/Peripherals/Adc.cs +++ b/src/Avr8Sharp/Core/Peripherals/Adc.cs @@ -203,6 +203,15 @@ public double ReferenceVoltage public double[] ChannelValues { get; } + /// + /// Optional pull hook: when set, a conversion asks it for the input voltage at the instant it + /// samples (the same instant is read), instead of reading + /// . The argument is the single-ended input number (ADC0..ADC15 as + /// in ); differential modes call it once per input. When null the + /// array is used. + /// + public Func? ReadChannelVolts; + /// /// Voltage returned by the internal temperature sensor channel (mux input 8 on ATmega328P). /// Defaults to ~0.378 V which corresponds to approximately 25 °C. @@ -320,13 +329,19 @@ public void Trigger(AdcTriggerSource source) StartConversion(); } + private double SampleChannel(int channel) + { + var read = ReadChannelVolts; + return read != null ? read(channel) : ChannelValues[channel]; + } + public void OnADCRead(AdcMuxInput input) { var voltage = input.Type switch { AdcMuxInputType.Constant => input.Voltage, - AdcMuxInputType.SingleEnded => ChannelValues[input.Channel], - AdcMuxInputType.Differential => input.Gain * (ChannelValues[input.PositiveChannel] - ChannelValues[input.NegativeChannel]), + AdcMuxInputType.SingleEnded => SampleChannel(input.Channel), + AdcMuxInputType.Differential => input.Gain * (SampleChannel(input.PositiveChannel) - SampleChannel(input.NegativeChannel)), AdcMuxInputType.Temperature => TemperatureVoltage, _ => 0.0 }; diff --git a/src/Avr8Sharp/Core/Peripherals/Clock.cs b/src/Avr8Sharp/Core/Peripherals/Clock.cs index 6561736..7c15064 100644 --- a/src/Avr8Sharp/Core/Peripherals/Clock.cs +++ b/src/Avr8Sharp/Core/Peripherals/Clock.cs @@ -32,6 +32,9 @@ public uint Frequency get { return (uint)(_baseFreqHz / (_prescalerValue != 0 ? _prescalerValue : 1)); } } + /// Raised after a CLKPR write or a reset changes . + public event Action? Changed; + public int Prescaler { get { return _prescalerValue; } @@ -73,6 +76,7 @@ public AvrClock(Cpu cpu, uint baseFreqHz, AvrClockConfig clockConfig) { _cyclesDelta = (int)(((double)cpu.Cycles + _cyclesDelta) * (oldPrescaler / (double)_prescalerValue) - (double)cpu.Cycles); + Changed?.Invoke(); } } @@ -88,6 +92,7 @@ public AvrClock(Cpu cpu, uint baseFreqHz, AvrClockConfig clockConfig) _cyclesDelta = (int)(((double)cpu.Cycles + _cyclesDelta) * (_prescalerValue / 1.0) - (double)cpu.Cycles); _prescalerValue = 1; + Changed?.Invoke(); } }; } diff --git a/src/Avr8Sharp/Core/Peripherals/Gpio.cs b/src/Avr8Sharp/Core/Peripherals/Gpio.cs index 8f57010..5b7d8c3 100644 --- a/src/Avr8Sharp/Core/Peripherals/Gpio.cs +++ b/src/Avr8Sharp/Core/Peripherals/Gpio.cs @@ -459,11 +459,11 @@ public PinState GetPinState (byte index) public void SetPinValue (byte index, bool value) { var bitMask = 1 << index; - _pinValue &= ~bitMask; - if (value) { - _pinValue |= bitMask; - } - _driven |= bitMask; + var newPinValue = value ? _pinValue | bitMask : _pinValue & ~bitMask; + var newDriven = _driven | bitMask; + if (newPinValue == _pinValue && newDriven == _driven) return; + _pinValue = newPinValue; + _driven = newDriven; UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]); } @@ -476,10 +476,105 @@ public void SetPinValue (byte index, bool value) public void ReleasePin (byte index) { var bitMask = 1 << index; + if ((_driven & bitMask) == 0) return; _driven &= ~bitMask; UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]); } - + + /// + /// Drives only the pins in from outside to the matching bits of + /// ; the other pins keep whatever they had (driven or released). + /// Equivalent to for each bit of the mask, but recomputes PIN once + /// and returns early when nothing changed. + /// + /// Pins to drive from outside + /// Levels of the pins in the mask (other bits are ignored) + public void SetInputs (byte mask, byte levels) + { + var newDriven = _driven | mask; + var newPinValue = (_pinValue & ~mask) | (levels & mask); + if (newDriven == _driven && newPinValue == _pinValue) return; + _driven = newDriven; + _pinValue = newPinValue; + UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]); + } + + /// + /// Stops driving the pins in from outside, as + /// does for one pin, with a single PIN recompute and an early return when none was driven. + /// + public void ReleaseInputs (byte mask) + { + if ((_driven & mask) == 0) return; + _driven &= ~mask; + UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]); + } + + /// + /// Sets the full external state in one call: pins in are driven + /// to and every other pin is released. Same result as + /// SetInputs(drivenMask, levels) followed by ReleaseInputs(~drivenMask), with one + /// PIN recompute. + /// + public void ApplyInputs (byte drivenMask, byte levels) + { + var newPinValue = levels & drivenMask; + if (drivenMask == _driven && newPinValue == (_pinValue & drivenMask)) return; + _driven = drivenMask; + _pinValue = (_pinValue & ~drivenMask) | newPinValue; + UpdatePinRegister (_cpu.Mmio.Data[_portConfig.DDR]); + } + + /// + /// Pins with the internal pull-up in effect, exactly the pins reports + /// as : inputs with the PORT bit set, plus open-collector + /// outputs that are released (high) with the PORT bit set. + /// + public byte PullUpMask { + get { + var port = _cpu.Mmio.Data[_portConfig.PORT]; + var released = _cpu.Mmio.Data[_portConfig.DDR] & OpenCollector & _lastValue; + return (byte)(port & (~_cpu.Mmio.Data[_portConfig.DDR] | released)); + } + } + + /// The register layout this port was built with. + public AvrPortConfig Config => _portConfig; + + private AvrPinChangeHandler? _pinChanged; + + /// + /// Raised when the effective pad level or output enable of a pin in + /// changes on the chip side (after timer overrides), with at that + /// instant. Costs nothing while nobody subscribes. A change of + /// alone is not reported until the next write that moves a pin. + /// + public event AvrPinChangeHandler? PinChanged { + add { _pinChanged += value; } + remove { _pinChanged -= value; } + } + + /// Pins that may raise . All pins by default. + public byte WatchMask { get; set; } = 0xff; + + /// + /// Pins the chip drives (as or in + /// ): DDR bits, minus open-collector pins that are in their high + /// (released) state. Timer overrides only change the level, never the direction. + /// + public byte OutputEnableMask { + get { + var ddr = _cpu.Mmio.Data[_portConfig.DDR]; + return (byte)(ddr & ~(OpenCollector & _lastValue)); + } + } + + /// + /// Driven level of the bits in (timer overrides included). + /// Bits outside the mask read 0. + /// + public byte OutputLevels => (byte)(_lastValue & OutputEnableMask); + private void UpdatePinRegister (byte ddr) { var pulled = _cpu.Mmio.Data[_portConfig.PORT] & ~_driven; @@ -629,9 +724,20 @@ private void WriteGpio (byte value, byte ddr) var newValue = (byte)((((value & _overrideMask) | _overrideValue) & ddr) | (value & ~ddr)); var prevValue = _lastValue; if (newValue == prevValue && ddr == _lastDdr) return; + var pinChanged = _pinChanged; + if (pinChanged == null) { + _lastValue = newValue; + _lastDdr = ddr; + OnGpioChange?.Invoke(newValue, prevValue); + return; + } + var oldEnable = (byte)(_lastDdr & ~(OpenCollector & prevValue)); _lastValue = newValue; _lastDdr = ddr; + var newEnable = (byte)(ddr & ~(OpenCollector & newValue)); + var changed = (byte)(((oldEnable ^ newEnable) | ((prevValue ^ newValue) & oldEnable & newEnable)) & WatchMask); OnGpioChange?.Invoke(newValue, prevValue); + if (changed != 0) pinChanged (changed, newEnable, (byte)(newValue & newEnable), _cpu.Cycles); } } @@ -668,6 +774,9 @@ public class AvrPortConfig (ushort pin, ushort ddr, ushort port, AvrPinChangeInt public readonly AvrExternalInterrupt?[]? ExternalInterrupts = externalInterrupts; } +/// Chip-side pin change: pins that changed, the pins the chip drives and their levels afterwards. +public delegate void AvrPinChangeHandler (byte changed, byte outputEnable, byte levels, ulong cycle); + public enum PinState { Low = 0, diff --git a/src/Avr8Sharp/Core/Peripherals/Timer.cs b/src/Avr8Sharp/Core/Peripherals/Timer.cs index c7a0f6d..2bce225 100644 --- a/src/Avr8Sharp/Core/Peripherals/Timer.cs +++ b/src/Avr8Sharp/Core/Peripherals/Timer.cs @@ -491,6 +491,54 @@ public int OcrMask } } + /// The register layout this timer was built with. + public AvrTimerConfig Config => _config; + + /// + /// Describes the PWM waveform on compare channel (0 = A, 1 = B, 2 = C) + /// from the current registers: fast PWM and phase correct / phase and frequency correct, with + /// COMnx = 2 (non-inverting) or 3 (inverting). Returns false when the channel does not exist. + /// is false when the channel is not producing PWM (other mode, + /// output disconnected, toggle mode, or clock stopped). Fast PWM high time is OCR + 1 counts, + /// phase correct is OCR counts out of TOP, as the waveform generator does. + /// + public bool TryGetPwm(int channel, uint clockHz, out bool enabled, out double frequencyHz, out double duty) + { + enabled = false; + frequencyHz = 0; + duty = 0; + int ocr; + byte comp; + switch (channel) { + case 0: ocr = _ocrA; comp = _compA; break; + case 1: ocr = _ocrB; comp = _compB; break; + case 2 when _hasOcrC: ocr = _ocrC; comp = _compC; break; + default: return false; + } + var divider = _config.Dividers != null && CS < _config.Dividers.Length ? _config.Dividers[CS] : 0; + var top = TOP; + // With TOP = OCRnA, channel A only toggles; it has no duty of its own + if (comp < 2 || divider <= 0 || top <= 0 || (channel == 0 && _topValue == TOP_OCRA)) return true; + double period, high; + switch (_timerMode) { + case TimerMode.FastPWM: + period = top + 1; + high = Math.Min(ocr, top) + 1; + break; + case TimerMode.PWMPhaseCorrect: + case TimerMode.PWMPhaseFrequencyCorrect: + period = 2.0 * top; + high = 2.0 * Math.Min(ocr, top); + break; + default: + return true; + } + enabled = true; + frequencyHz = clockHz / (divider * period); + duty = comp == 3 ? 1.0 - high / period : high / period; + return true; + } + public int DebugTCNT { get { return _tcnt; } diff --git a/src/Avr8Sharp/Core/Utils/Builder.cs b/src/Avr8Sharp/Core/Utils/Builder.cs index 16f3b23..f5e88f9 100644 --- a/src/Avr8Sharp/Core/Utils/Builder.cs +++ b/src/Avr8Sharp/Core/Utils/Builder.cs @@ -1,3 +1,4 @@ +using AVR8Sharp.Core.Abstractions; using AVR8Sharp.Core.Peripherals; using AVR8Sharp.Core.Utils; @@ -6,6 +7,16 @@ namespace AVR8Sharp.Core; public class AvrBuilder (AvrRunner @base, int flashSize = 0x8000) { readonly AvrRunner @base = @base; + + // What the Add* methods mounted, so BuildMcu can wrap the machine without being told again + readonly List _timers = []; + readonly List _usarts = []; + readonly List _spis = []; + readonly List _twis = []; + AvrAdc? _adc; + AvrClock? _clock; + AvrWatchdog? _watchdog; + public static AvrBuilder Create (int flashSize = 0x8000, int ramSize = 8192) { return new AvrBuilder (new AvrRunner (new byte[flashSize], ramSize), flashSize); @@ -45,12 +56,14 @@ public AvrBuilder AddGpioPort(AvrPortConfig config, out AvrIoPort port) public AvrBuilder AddTimer(AvrTimerConfig config, out AvrTimer timer) { timer = new AvrTimer (@base.Cpu, config); + _timers.Add (timer); return this; } public AvrBuilder AddUsart(AvrUsartConfig config, out AvrUsart usart) { usart = new AvrUsart (@base.Cpu, config, @base.Speed); + _usarts.Add (usart); return this; } @@ -63,12 +76,14 @@ public AvrBuilder AddUsi(AvrIoPort portObj, int portPin, int dataPin, int clockP public AvrBuilder AddSpi(AvrSpiConfig config, out AvrSpi spi) { spi = new AvrSpi (@base.Cpu, config, @base.Speed); + _spis.Add (spi); return this; } public AvrBuilder AddTwi(AvrTwiConfig config, out AvrTwi twim) { twim = new AvrTwi (@base.Cpu, config, @base.Speed); + _twis.Add (twim); return this; } @@ -81,12 +96,22 @@ public AvrBuilder AddEeprom(AvrEepromConfig config, IEepromBackend backend, out public AvrBuilder AddAdc(AvrAdcConfig config, out AvrAdc adc) { adc = new AvrAdc (@base.Cpu, config); + _adc = adc; return this; } + public AvrBuilder AddClock(AvrClockConfig config, out AvrClock clock) + { + clock = new AvrClock (@base.Cpu, @base.Speed, config); + _clock = clock; + return this; + } + public AvrBuilder AddWatchdog(AvrWatchdogConfig config, AvrClock clock, out AvrWatchdog watchdog) { watchdog = new AvrWatchdog (@base.Cpu, config, clock); + _watchdog = watchdog; + _clock ??= clock; return this; } @@ -108,6 +133,16 @@ public AvrBuilder UseSwitchDecoder() return this; } + /// + /// Builds the machine and wraps it as an (SiliconTwin contracts) with + /// every peripheral mounted through this builder. USARTs, SPIs and TWIs are indexed in the + /// order they were added. + /// + public AvrMcu BuildMcu(string name = "avr") + { + return new AvrMcu (@base, name, _clock, _watchdog, _adc, _timers, _usarts, _spis, _twis); + } + public AvrRunner Build() { return @base; diff --git a/tests/Avr8Sharp.AotSmoke/Avr8Sharp.AotSmoke.csproj b/tests/Avr8Sharp.AotSmoke/Avr8Sharp.AotSmoke.csproj new file mode 100644 index 0000000..5f6296a --- /dev/null +++ b/tests/Avr8Sharp.AotSmoke/Avr8Sharp.AotSmoke.csproj @@ -0,0 +1,19 @@ + + + + + Exe + net10.0 + enable + enable + false + true + true + + + + + + + diff --git a/tests/Avr8Sharp.AotSmoke/Program.cs b/tests/Avr8Sharp.AotSmoke/Program.cs new file mode 100644 index 0000000..676e51d --- /dev/null +++ b/tests/Avr8Sharp.AotSmoke/Program.cs @@ -0,0 +1,57 @@ +using AVR8Sharp.Core; +using AVR8Sharp.Core.Abstractions; +using AVR8Sharp.Core.Peripherals; +using AVR8Sharp.Core.Utils; +using SiliconTwin.Abstractions; + +// Native AOT smoke check: assemble a blink program, run a few thousand cycles through the +// real decoder, and require PORTB to have toggled. Exit code 0 on success. + +const string source = """ + sbi 0x04, 0 ; DDRB.0 output +loop: + sbi 0x05, 0 ; PORTB.0 high + cbi 0x05, 0 ; PORTB.0 low + rjmp loop +"""; + +var assembler = new AvrAssembler(); +var program = assembler.Assemble(source); +if (assembler.Errors.Count > 0) { + Console.Error.WriteLine("assembly failed: " + string.Join(", ", assembler.Errors)); + return 2; +} + +// Everything below goes through the SiliconTwin interfaces +IMcu mcu = AvrBuilder.Create(0x8000, 2048) + .AddGpioPort(AvrIoPort.PortBConfig, out _) + .AddTimer(AvrTimer.Timer0Config, out _) + .AddUsart(AvrUsart.Usart0Config, out _) + .AddAdc(AvrAdc.AdcConfig, out _) + .BuildMcu("atmega328p"); +mcu.Load(FirmwareImage.FromBin(program)); +mcu.Reset(ResetKind.Power); + +var portB = ((IGpio)mcu).Banks[0]; +var toggles = 0; +var cycleOk = true; +portB.PinChanged += (in PinChange e) => { + if (e.Affects(1) && e.IsDriven(0)) toggles++; + if (e.Cycle != mcu.Cycles) cycleOk = false; +}; + +var consumed = 0L; +for (var i = 0; i < 50; i++) consumed += mcu.Run(100); + +((IAdcInput)mcu).ReadChannelVolts = _ => 2.5; +var pwmOk = !((IPwmSource)mcu).TryGetPwm(0, 3, out _); +var uartTx = 0; +((IPeripheralMap)mcu).Uarts[0].TxByte += _ => uartTx++; + +Console.WriteLine($"cycles={mcu.Cycles} consumed={consumed} toggles={toggles} outputEnableMask=0x{portB.OutputEnableMask:x2}"); +if (toggles < 100 || portB.OutputEnableMask != 0x01 || !cycleOk || !pwmOk || mcu.Cycles != consumed) { + Console.Error.WriteLine("FAIL: PORTB did not toggle as expected"); + return 1; +} +Console.WriteLine("OK"); +return 0; diff --git a/tests/Avr8Sharp.Tests/AdcPullTests.cs b/tests/Avr8Sharp.Tests/AdcPullTests.cs new file mode 100644 index 0000000..5b62654 --- /dev/null +++ b/tests/Avr8Sharp.Tests/AdcPullTests.cs @@ -0,0 +1,118 @@ +using AVR8Sharp.Core.Peripherals; +using Avr8Sharp.Tests.Utils; + +namespace Avr8Sharp.Tests; + +[TestFixture] +public class AdcPull : AvrTestBase +{ + const int ADMUX = 0x7c; + const int ADCSRA = 0x7a; + const int ADCSRB = 0x7b; + const int ADCH = 0x79; + const int ADCL = 0x78; + const int REFS0 = 1 << 6; + const int ADEN = 1 << 7; + const int ADSC = 1 << 6; + const int PRESCALER_128 = 7; + + AvrAdc _adc; + + protected override void SetupPeripherals () => _adc = new AvrAdc (Cpu, AvrAdc.AdcConfig); + + void Convert (int admux, int adcsrb = 0) + { + Cpu.WriteData (ADMUX, (byte)admux); + Cpu.WriteData (ADCSRB, (byte)adcsrb); + Cpu.WriteData (ADCSRA, ADEN | ADSC | PRESCALER_128); + Cpu.Cycles += 128 * 26; + Cpu.Tick (); + } + + int Result => Cpu.Mmio.Data[ADCL] | (Cpu.Mmio.Data[ADCH] << 8); + + [Test] + public void CalledOncePerConversionWithTheChannel () + { + var calls = new List (); + _adc.ReadChannelVolts = ch => { calls.Add (ch); return 2.56; }; + _adc.ChannelValues[3] = 1.0; // ignored while the hook is set + + Convert (REFS0 | 3); + Assert.That (calls, Is.EqualTo (new[] { 3 })); + Assert.That (Result, Is.EqualTo (524)); + + Convert (REFS0 | 5); + Assert.That (calls, Is.EqualTo (new[] { 3, 5 })); + } + + [Test] + public void NotCalledWithoutConversions () + { + var calls = 0; + _adc.ReadChannelVolts = _ => { calls++; return 1; }; + Cpu.WriteData (ADMUX, REFS0 | 2); + Cpu.WriteData (ADCSRA, ADEN | PRESCALER_128); // enabled, no start + Cpu.Cycles += 100000; + Cpu.Tick (); + Assert.That (calls, Is.EqualTo (0)); + } + + [Test] + public void ConstantAndTemperatureInputsDoNotPull () + { + var calls = 0; + _adc.ReadChannelVolts = _ => { calls++; return 1; }; + Convert (REFS0 | 8); // temperature + Convert (REFS0 | 14); // 1.1 V bandgap + Assert.That (calls, Is.EqualTo (0)); + } + + [Test] + public void FallsBackToChannelValuesWhenNull () + { + _adc.ChannelValues[1] = 2.56; + Convert (REFS0 | 1); + Assert.That (Result, Is.EqualTo (524)); + + _adc.ReadChannelVolts = _ => 5.0; + Convert (REFS0 | 1); + Assert.That (Result, Is.EqualTo (1023)); + + _adc.ReadChannelVolts = null; + Convert (REFS0 | 1); + Assert.That (Result, Is.EqualTo (524)); + } +} + +[TestFixture] +public class AdcPullMega : AvrTestBase +{ + const int ADMUX = 0x7c; + const int ADCSRA = 0x7a; + const int ADCSRB = 0x7b; + const int ADCH = 0x79; + const int ADCL = 0x78; + const int MUX5 = 0x8; + + AvrAdc _adc; + + protected override void SetupPeripherals () => _adc = new AvrAdc (Cpu, AvrAdc.Atmega2560AdcConfig); + + [TestCase (0, 0, 0)] + [TestCase (7, 0, 7)] + [TestCase (0, MUX5, 8)] + [TestCase (7, MUX5, 15)] + public void MuxChannelsReachTheHook (int mux, int adcsrb, int expected) + { + var calls = new List (); + _adc.ReadChannelVolts = ch => { calls.Add (ch); return 2.56; }; + Cpu.WriteData (ADMUX, (byte)(0x40 | mux)); + Cpu.WriteData (ADCSRB, (byte)adcsrb); + Cpu.WriteData (ADCSRA, 0x80 | 0x40 | 7); + Cpu.Cycles += 128 * 26; + Cpu.Tick (); + Assert.That (calls, Is.EqualTo (new[] { expected })); + Assert.That (Cpu.Mmio.Data[ADCL] | (Cpu.Mmio.Data[ADCH] << 8), Is.EqualTo (524)); + } +} diff --git a/tests/Avr8Sharp.Tests/AvrMcuContractTests.cs b/tests/Avr8Sharp.Tests/AvrMcuContractTests.cs new file mode 100644 index 0000000..18e7a35 --- /dev/null +++ b/tests/Avr8Sharp.Tests/AvrMcuContractTests.cs @@ -0,0 +1,439 @@ +using System.Text; +using AVR8Sharp.Core; +using AVR8Sharp.Core.Abstractions; +using AVR8Sharp.Core.Peripherals; +using AVR8Sharp.Core.Utils; +using SiliconTwin.Abstractions; + +namespace Avr8Sharp.Tests; + +/// Drives an Uno-like machine only through the SiliconTwin interfaces. +[TestFixture] +public class AvrMcuContract +{ + const int MCUSR = 0x54; + const int CLKPR = 0x61; + const int UCSR0B = 0xc1; + const int UDR0 = 0xc6; + const int UCSR0A = 0xc0; + const int SPCR = 0x4c; + const int SPDR = 0x4e; + const int TWBR = 0xb8; + const int TWSR = 0xb9; + const int TWDR = 0xbb; + const int TWCR = 0xbc; + const int ADMUX = 0x7c; + const int ADCSRA = 0x7a; + const int ADCL = 0x78; + const int ADCH = 0x79; + const int TCCR0A = 0x44; + const int TCCR0B = 0x45; + const int OCR0A = 0x47; + + // sbi DDRB,0 ; loop: sbi PORTB,0 ; cbi PORTB,0 ; rjmp loop + const string Blink = "sbi 0x04, 0\nloop:\nsbi 0x05, 0\ncbi 0x05, 0\nrjmp loop\n"; + + AvrMcu _mcu = null!; + IMcu Mcu => _mcu; + AVR8Sharp.Core.Cpu Cpu => _mcu.Cpu; + IPinBank PortB => ((IGpio)_mcu).Banks[0]; + IPinBank PortD => ((IGpio)_mcu).Banks[2]; + + [SetUp] + public void SetUp () => _mcu = BuildUno (); + + [TearDown] + public void TearDown () => _mcu.Dispose (); + + static AvrMcu BuildUno () + { + var builder = AvrBuilder.Create (0x8000, 2048) + .AddGpioPort (AvrIoPort.PortBConfig, out _) + .AddGpioPort (AvrIoPort.PortCConfig, out _) + .AddGpioPort (AvrIoPort.PortDConfig, out _) + .AddTimer (AvrTimer.Timer0Config, out _) + .AddUsart (AvrUsart.Usart0Config, out _) + .AddSpi (AvrSpi.SpiConfig, out _) + .AddTwi (AvrTwi.TwiConfig, out _) + .AddAdc (AvrAdc.AdcConfig, out _) + .AddClock (AvrClock.ClockConfig, out var clock) + .AddWatchdog (AvrWatchdog.WatchdogConfig, clock, out _); + return builder.BuildMcu ("atmega328p"); + } + + static byte[] Assemble (string source) + { + var assembler = new AvrAssembler (); + var bytes = assembler.Assemble (source); + Assert.That (assembler.Errors, Is.Empty); + return bytes; + } + + static string ToHex (byte[] data) + { + var sb = new StringBuilder (); + for (var i = 0; i < data.Length; i += 16) { + var n = Math.Min (16, data.Length - i); + var sum = n + (i >> 8) + (i & 0xff); + sb.Append ($":{n:X2}{i:X4}00"); + for (var j = 0; j < n; j++) { sb.Append (data[i + j].ToString ("X2")); sum += data[i + j]; } + sb.Append ($"{(byte)-sum:X2}\n"); + } + sb.Append (":00000001FF\n"); + return sb.ToString (); + } + + void LoadBlink () => Mcu.Load (FirmwareImage.FromBin (Assemble (Blink))); + + [Test] + public void Identity () + { + Assert.Multiple (() => { + Assert.That (Mcu.Name, Is.EqualTo ("atmega328p")); + Assert.That (Mcu.ClockHz, Is.EqualTo (16_000_000u)); + Assert.That (Mcu.TryGet (out _), Is.True); + Assert.That (Mcu.TryGet (out _), Is.True); + Assert.That (Mcu.TryGet (out _), Is.True); + Assert.That (Mcu.TryGet (out _), Is.True); + Assert.That (((IGpio)_mcu).Banks.Select (b => b.Name), Is.EqualTo (new[] { "PORTB", "PORTC", "PORTD" })); + Assert.That (PortB.PinCount, Is.EqualTo (8)); + }); + } + + [Test (Description = "Repeated small budgets do not drift")] + public void RunBudgetDoesNotDrift () + { + LoadBlink (); + long consumed = 0; + for (var i = 0; i < 1000; i++) consumed += Mcu.Run (15); // odd budget vs 2-cycle instructions + Assert.Multiple (() => { + Assert.That (Mcu.Cycles, Is.EqualTo (15000).Within (1)); + Assert.That (consumed, Is.EqualTo (Mcu.Cycles)); + Assert.That (Mcu.Run (0), Is.EqualTo (0)); + Assert.That (Mcu.Run (-5), Is.EqualTo (0)); + }); + } + + [Test] + public void RunUntilReachesTheAbsoluteCycle () + { + LoadBlink (); + var used = Mcu.RunUntil (1000); + Assert.That (Mcu.Cycles, Is.EqualTo (1000).Within (1)); + Assert.That (used, Is.EqualTo (Mcu.Cycles)); + Assert.That (Mcu.RunUntil (500), Is.EqualTo (0)); + } + + [Test] + public void SoftAndPowerReset () + { + LoadBlink (); + Mcu.Run (100); + Cpu.WriteData (0x200, 0x5a); + Mcu.Reset (ResetKind.Soft); + Assert.Multiple (() => { + Assert.That (Cpu.Pc, Is.EqualTo (0)); + Assert.That (Mcu.Cycles, Is.GreaterThanOrEqualTo (100), "soft reset keeps counting"); + Assert.That (Cpu.Mmio.Data[0x200], Is.EqualTo (0x5a), "SRAM is kept"); + }); + + Mcu.Run (100); + Mcu.Reset (ResetKind.Power); + Assert.Multiple (() => { + Assert.That (Mcu.Cycles, Is.EqualTo (0)); + Assert.That (Cpu.Pc, Is.EqualTo (0)); + Assert.That (Cpu.Mmio.Data[MCUSR] & 1, Is.EqualTo (1), "PORF"); + Assert.That (Cpu.Mmio.Data[0x200], Is.EqualTo (0)); + Assert.That (Cpu.ProgBytes[0], Is.Not.EqualTo (0), "flash is kept"); + }); + Mcu.Run (50); + Assert.That (Mcu.Cycles, Is.EqualTo (50).Within (1)); + } + + [Test] + public void LoadIntelHexAndBin () + { + var program = Assemble (Blink); + Mcu.Load (FirmwareImage.FromIntelHex (ToHex (program))); + Assert.That (Cpu.ProgBytes.AsSpan (0, program.Length).ToArray (), Is.EqualTo (program)); + + Mcu.Load (FirmwareImage.FromBin (new byte[] { 0xaa, 0xbb }, 0x100)); + Assert.Multiple (() => { + Assert.That (Cpu.ProgBytes[0x100], Is.EqualTo (0xaa)); + Assert.That (Cpu.ProgBytes[0x101], Is.EqualTo (0xbb)); + Assert.That (Cpu.ProgBytes[0], Is.EqualTo (program[0]), "bytes outside the image are kept"); + }); + + Assert.Throws (() => Mcu.Load (FirmwareImage.FromIntelHex (":10000000ZZ\n"))); + Assert.Throws (() => Mcu.Load (FirmwareImage.FromUf2 (new byte[4]))); + Assert.Throws (() => Mcu.Load (FirmwareImage.FromElf (new byte[4]))); + Assert.Throws (() => Mcu.Load (FirmwareImage.FromBin (new byte[4], 0x7ffe))); + } + + [Test (Description = "PinChange.Cycle equals the cycle counter at the write")] + public void PinChangedCarriesTheCycle () + { + LoadBlink (); + Mcu.Run (40); // set DDRB, settle + Mcu.Reset (ResetKind.Power); // cycles restart: the base offset must be applied + var seen = new List<(PinChange change, long now)> (); + PortB.PinChanged += (in PinChange e) => seen.Add ((e, Mcu.Cycles)); + + Mcu.Run (60); + Assert.That (seen.Count, Is.GreaterThanOrEqualTo (8)); + Assert.Multiple (() => { + foreach (var (change, now) in seen) { + Assert.That (change.Cycle, Is.EqualTo (now)); + Assert.That (change.Changed, Is.EqualTo (1UL)); + } + Assert.That (seen.Select (s => s.change.IsHigh (0)), Is.EqualTo (Enumerable.Range (0, seen.Count).Select (i => i % 2 == 0)).Or.EqualTo (Enumerable.Range (0, seen.Count).Select (i => i % 2 == 1))); + Assert.That (seen.Zip (seen.Skip (1), (a, b) => b.change.Cycle - a.change.Cycle), Is.All.AnyOf (2L, 4L), "sbi to cbi is 2 cycles, cbi to sbi is 4 (cbi + rjmp)"); + Assert.That (seen[0].change.IsDriven (0), Is.True); + }); + } + + [Test] + public void WatchMaskFiltersPins () + { + var count = 0; + PinChangeHandler handler = (in PinChange e) => count++; + PortB.PinChanged += handler; + Assert.That (PortB.WatchMask, Is.EqualTo (0xffUL)); + + Cpu.WriteData (0x24, 0xff); + count = 0; + PortB.WatchMask = 1UL << 3; + Cpu.WriteData (0x25, 0x01); + Cpu.WriteData (0x25, 0x02); + Assert.That (count, Is.EqualTo (0), "pins outside the mask are silent"); + Cpu.WriteData (0x25, 0x08); + Assert.That (count, Is.EqualTo (1)); + PortB.PinChanged -= handler; + Cpu.WriteData (0x25, 0x00); + Assert.That (count, Is.EqualTo (1)); + } + + [Test] + public void SetInputsReleaseInputsAndPullUps () + { + Cpu.WriteData (0x25, 0x06); // PORTB: pull-ups on PB1, PB2 + Assert.That (PortB.PullUpMask, Is.EqualTo (0x06UL)); + Assert.That (PortB.PullDownMask, Is.EqualTo (0UL)); + Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x06)); + + PortB.SetInputs (0x03, 0x01); + Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x05), "PB0 high, PB1 driven low, PB2 pulled up"); + PortB.SetInputs (0x80, 0x80); // PB0, PB1 untouched + Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x85)); + PortB.ReleaseInputs (0x83); + Assert.That (Cpu.Mmio.Data[0x23], Is.EqualTo (0x06)); + + Cpu.WriteData (0x24, 0x06); // PB1, PB2 become outputs: no pull-up, driven + Assert.That (PortB.PullUpMask, Is.EqualTo (0UL)); + Assert.That (PortB.OutputEnableMask, Is.EqualTo (0x06UL)); + Assert.That (PortB.OutputLevels & PortB.OutputEnableMask, Is.EqualTo (0x06UL)); + } + + [Test] + public void AdcPullsThroughTheInterface () + { + var adc = (IAdcInput)_mcu; + var asked = new List (); + adc.ReadChannelVolts = ch => { asked.Add (ch); return 2.5; }; + Cpu.WriteData (ADMUX, 0x40 | 2); // AVCC, ADC2 + Assert.That (adc.ReferenceVolts, Is.EqualTo (5.0)); + Cpu.WriteData (ADCSRA, 0x80 | 0x40 | 7); + Mcu.Run (128 * 26); + Assert.Multiple (() => { + Assert.That (asked, Is.EqualTo (new[] { 2 })); + Assert.That (Cpu.Mmio.Data[ADCL] | (Cpu.Mmio.Data[ADCH] << 8), Is.EqualTo (512)); + }); + Cpu.WriteData (ADMUX, 0xc0 | 2); // internal 1.1 V + Assert.That (adc.ReferenceVolts, Is.EqualTo (1.1)); + } + + [Test] + public void UartTransmitAndReceive () + { + var uart = ((IPeripheralMap)_mcu).Uarts.Single (); + var tx = new List (); + uart.TxByte += tx.Add; + + Cpu.WriteData (0xc4, 103); // UBRR0 = 103: 9600 baud at 16 MHz + Cpu.WriteData (UCSR0B, 0x18); // RXEN | TXEN + Assert.That (uart.BaudRate, Is.EqualTo (9615u)); + Cpu.WriteData (UDR0, (byte)'A'); + Mcu.Run (20000); + Assert.That (tx, Is.EqualTo (new[] { (byte)'A' })); + + Assert.That (uart.TryInjectRx (0x42), Is.True); + Assert.That (uart.TryInjectRx (0x43), Is.False, "receiver busy with the previous byte"); + Mcu.Run (20000); + Assert.Multiple (() => { + Assert.That (Cpu.Mmio.Data[UCSR0A] & 0x80, Is.EqualTo (0x80), "RXC"); + Assert.That (Cpu.ReadData (UDR0), Is.EqualTo (0x42)); + }); + + Cpu.WriteData (UCSR0B, 0x08); // RX off + Assert.That (uart.TryInjectRx (1), Is.False); + } + + [Test] + public void SpiTransferMapsToTheCallback () + { + var spi = ((IPeripheralMap)_mcu).Spis.Single (); + var mosi = new List (); + Cpu.WriteData (SPCR, 0x50); // SPE | MSTR + + Cpu.WriteData (SPDR, 0x11); + Mcu.Run (200); + Assert.That (Cpu.ReadData (SPDR), Is.EqualTo (0xff), "no callback reads all ones"); + + spi.Transfer = b => { mosi.Add (b); return b + 1u; }; + Cpu.WriteData (SPDR, 0x22); + Mcu.Run (200); + Assert.Multiple (() => { + Assert.That (mosi, Is.EqualTo (new[] { 0x22u })); + Assert.That (Cpu.ReadData (SPDR), Is.EqualTo (0x23)); + Assert.That (spi.FrameBits, Is.EqualTo (8)); + }); + + spi.Transfer = null; + Cpu.WriteData (SPDR, 0x33); + Mcu.Run (200); + Assert.That (Cpu.ReadData (SPDR), Is.EqualTo (0xff)); + } + + sealed class Eeprom : II2cTarget + { + public readonly List Written = []; + public int Stops; + public bool Probe (byte addr7, bool write) => true; + public void Write (byte data) => Written.Add (data); + public byte Read () => 0x5a; + public void Stop () => Stops++; + } + + int TwiStep (byte twcr) + { + Cpu.WriteData (TWCR, twcr); + Mcu.Run (3000); + return Cpu.Mmio.Data[TWSR] & 0xf8; + } + + [Test] + public void I2cTargetAcksNacksAndMovesData () + { + var i2c = ((IPeripheralMap)_mcu).I2cs.Single (); + var target = new Eeprom (); + i2c.AttachTarget (0x50, target); + Cpu.WriteData (TWBR, 8); + + Assert.That (TwiStep (0xa4), Is.EqualTo (0x08), "START"); + Cpu.WriteData (TWDR, 0x50 << 1); + Assert.That (TwiStep (0x84), Is.EqualTo (0x18), "SLA+W acked"); + Cpu.WriteData (TWDR, 0x77); + Assert.That (TwiStep (0x84), Is.EqualTo (0x28), "data acked"); + Assert.That (TwiStep (0x94), Is.EqualTo (0xf8).Or.EqualTo (0x00), "STOP"); + Assert.Multiple (() => { + Assert.That (target.Written, Is.EqualTo (new byte[] { 0x77 })); + Assert.That (target.Stops, Is.EqualTo (1)); + }); + + Assert.That (TwiStep (0xa4), Is.EqualTo (0x08)); + Cpu.WriteData (TWDR, 0x51 << 1); + Assert.That (TwiStep (0x84), Is.EqualTo (0x20), "no device at 0x51: SLA+W NACK"); + TwiStep (0x94); + + Assert.That (TwiStep (0xa4), Is.EqualTo (0x08)); + Cpu.WriteData (TWDR, (0x50 << 1) | 1); + Assert.That (TwiStep (0x84), Is.EqualTo (0x40), "SLA+R acked"); + Assert.That (TwiStep (0xc4), Is.EqualTo (0x50), "byte read with ACK"); + Assert.That (Cpu.Mmio.Data[TWDR], Is.EqualTo (0x5a)); + TwiStep (0x94); + + i2c.DetachTarget (0x50); + Assert.That (TwiStep (0xa4), Is.EqualTo (0x08)); + Cpu.WriteData (TWDR, 0x50 << 1); + Assert.That (TwiStep (0x84), Is.EqualTo (0x20), "detached"); + } + + [Test] + public void I2cSlaveSide () + { + var i2c = ((IPeripheralMap)_mcu).I2cs.Single (); + var addresses = new List (); + i2c.SlaveAddressChanged += addresses.Add; + Cpu.WriteData (0xba, 0x42 << 1); // TWAR + Assert.That (i2c.SlaveAddress, Is.EqualTo (0x42)); + Assert.That (i2c.BeginMasterTransfer (0x42, write: true), Is.True); + Assert.That (i2c.BeginMasterTransfer (0x43, write: true), Is.False); + Cpu.WriteData (TWCR, 0x44); // TWEA | TWEN + Assert.That (i2c.MasterWrite (0x99), Is.True); + Assert.That (Cpu.Mmio.Data[TWDR], Is.EqualTo (0x99)); + i2c.MasterStop (); + Assert.That (Cpu.Mmio.Data[TWSR] & 0xf8, Is.EqualTo (0xa0)); + Assert.Throws (() => i2c.TryMasterRead (out _)); + } + + [Test] + public void PwmFromTimer0 () + { + var pwm = (IPwmSource)_mcu; + Assert.That (pwm.TryGetPwm (2, 6, out var info), Is.True, "PD6 is OC0A"); + Assert.That (info.Enabled, Is.False); + Assert.That (pwm.TryGetPwm (2, 3, out _), Is.False, "PD3 has no mounted timer"); + Assert.That (pwm.TryGetPwm (0, 6, out _), Is.False); + + Cpu.WriteData (0x2a, 0x60); // DDRD: PD5, PD6 + Cpu.WriteData (OCR0A, 128); + Cpu.WriteData (TCCR0A, 0x83); // COM0A1, fast PWM + Cpu.WriteData (TCCR0B, 0x01); + Assert.That (pwm.TryGetPwm (2, 6, out info), Is.True); + Assert.Multiple (() => { + Assert.That (info.Enabled, Is.True); + Assert.That (info.FrequencyHz, Is.EqualTo (16_000_000.0 / 256).Within (1e-6)); + Assert.That (info.Duty, Is.EqualTo(129.0 / 256).Within (1e-9)); + }); + + Cpu.WriteData (TCCR0A, 0xc3); // inverting + pwm.TryGetPwm (2, 6, out info); + Assert.That (info.Duty, Is.EqualTo (1 - 129.0 / 256).Within (1e-9)); + + Cpu.WriteData (TCCR0A, 0x81); // phase correct + pwm.TryGetPwm (2, 6, out info); + Assert.Multiple (() => { + Assert.That (info.FrequencyHz, Is.EqualTo (16_000_000.0 / 510).Within (1e-6)); + Assert.That (info.Duty, Is.EqualTo (128.0 / 255).Within (1e-9)); + }); + + Cpu.WriteData (TCCR0B, 0x00); // clock stopped + pwm.TryGetPwm (2, 6, out info); + Assert.That (info.Enabled, Is.False); + } + + [Test] + public void ClockChangedFollowsClkpr () + { + var raised = 0; + Mcu.ClockChanged += () => raised++; + Cpu.WriteData (CLKPR, 0x80); + Cpu.WriteData (CLKPR, 0x01); // divide by 2 + Assert.Multiple (() => { + Assert.That (Mcu.ClockHz, Is.EqualTo (8_000_000u)); + Assert.That (raised, Is.EqualTo (1)); + }); + } + + [Test] + public void CycleBudgetHelperWorksWithRun () + { + LoadBlink (); + double carry = 0; + for (var i = 0; i < 100; i++) { + var budget = CycleBudget.Next (ref carry, Mcu.ClockHz, 0.000001); + CycleBudget.Settle (ref carry, budget, Mcu.Run (budget)); + } + Assert.That (Mcu.Cycles, Is.EqualTo (1600).Within (2)); + } +} diff --git a/tests/Avr8Sharp.Tests/GpioBatchInputTests.cs b/tests/Avr8Sharp.Tests/GpioBatchInputTests.cs new file mode 100644 index 0000000..d3460bd --- /dev/null +++ b/tests/Avr8Sharp.Tests/GpioBatchInputTests.cs @@ -0,0 +1,263 @@ +using AVR8Sharp.Core.Peripherals; +using Avr8Sharp.Tests.Utils; + +namespace Avr8Sharp.Tests; + +/// SetInputs, the no-change early returns, and OutputEnableMask / OutputLevels. +[TestFixture] +public class GpioBatchInput : AvrTestBase +{ + const int PINB = 0x23; + const int DDRB = 0x24; + const int PORTB = 0x25; + const int DDRD = 0x2a; + const int PCICR = 0x68; + const int PCIFR = 0x3b; + const int PCMSK0 = 0x6b; + const int TCCR0A = 0x44; + const int TCCR0B = 0x45; + const int OCR0A = 0x47; + + AvrIoPort _portB; + AvrIoPort _portD; + AvrTimer _timer0; + + protected override void SetupPeripherals () + { + _timer0 = new AvrTimer (Cpu, AvrTimer.Timer0Config); + _portB = new AvrIoPort (Cpu, AvrIoPort.PortBConfig); + _portD = new AvrIoPort (Cpu, AvrIoPort.PortDConfig); + } + + static void Drive (AvrIoPort port, byte mask, byte levels) + { + for (byte bit = 0; bit < 8; bit++) + if ((mask & (1 << bit)) != 0) + port.SetPinValue (bit, (levels & (1 << bit)) != 0); + } + + static void Release (AvrIoPort port, byte mask) + { + for (byte bit = 0; bit < 8; bit++) + if ((mask & (1 << bit)) != 0) + port.ReleasePin (bit); + } + + static void Apply (AvrIoPort port, byte drivenMask, byte levels) + { + for (byte bit = 0; bit < 8; bit++) { + if ((drivenMask & (1 << bit)) != 0) + port.SetPinValue (bit, (levels & (1 << bit)) != 0); + else + port.ReleasePin (bit); + } + } + + [Test (Description = "SetInputs / ReleaseInputs / ApplyInputs leave PIN, PCINT flag and listeners identical to per-bit calls")] + public void EquivalentToPerBitCalls () + { + // kind: 0 = SetInputs, 1 = ReleaseInputs, 2 = ApplyInputs + var steps = new (int kind, byte mask, byte levels)[] { + (0, 0xff, 0xa5), (0, 0xff, 0xa5), (0, 0x0f, 0xff), (1, 0xf0, 0), (0, 0x3c, 0x18), (1, 0x00, 0), + (2, 0x7e, 0x24), (2, 0x7e, 0x24), (1, 0xff, 0), (0, 0x81, 0x80), (2, 0xff, 0x00), + }; + foreach (var ddr in new byte[] { 0x00, 0x0f, 0xaa }) { + foreach (var port in new byte[] { 0x00, 0x55, 0xff }) { + var a = new AVR8Sharp.Core.Cpu (new ushort[1024], 8192); + var b = new AVR8Sharp.Core.Cpu (new ushort[1024], 8192); + var pa = new AvrIoPort (a, AvrIoPort.PortBConfig); + var pb = new AvrIoPort (b, AvrIoPort.PortBConfig); + var logA = new List<(byte, byte)> (); + var logB = new List<(byte, byte)> (); + pa.AddListener ((v, o) => logA.Add ((v, o))); + pb.AddListener ((v, o) => logB.Add ((v, o))); + var clocksA = new List<(int, bool)> (); + var clocksB = new List<(int, bool)> (); + for (var i = 0; i < 8; i++) { + var bit = i; + pa.ExternalClockListeners[i] = v => clocksA.Add ((bit, v)); + pb.ExternalClockListeners[i] = v => clocksB.Add ((bit, v)); + } + foreach (var cpu in new[] { a, b }) { + cpu.WriteData (DDRB, ddr); + cpu.WriteData (PORTB, port); + cpu.WriteData (PCMSK0, 0xff); + cpu.WriteData (PCICR, 1); + } + foreach (var step in steps) { + switch (step.kind) { + case 0: + Drive (pa, step.mask, step.levels); + pb.SetInputs (step.mask, step.levels); + break; + case 1: + Release (pa, step.mask); + pb.ReleaseInputs (step.mask); + break; + default: + Apply (pa, step.mask, step.levels); + pb.ApplyInputs (step.mask, step.levels); + break; + } + Assert.Multiple (() => { + Assert.That (b.Mmio.Data[PINB], Is.EqualTo (a.Mmio.Data[PINB]), $"PIN ddr={ddr:x2} port={port:x2} step={step}"); + Assert.That (b.Mmio.Data[PCIFR], Is.EqualTo (a.Mmio.Data[PCIFR])); + Assert.That (logB, Is.EqualTo (logA)); + Assert.That (clocksB, Is.EqualTo (clocksA)); + }); + b.Mmio.Data[PCIFR] = 0; + a.Mmio.Data[PCIFR] = 0; + } + } + } + } + + [Test (Description = "SetInputs leaves pins outside the mask alone; ReleaseInputs and ApplyInputs release")] + public void MaskSemantics () + { + Cpu.WriteData (PORTB, 0x03); // pull-ups on PB0, PB1 + _portB.SetInputs (0xff, 0x00); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x00)); + _portB.SetInputs (0x80, 0x80); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x80), "other pins stay driven low"); + _portB.ReleaseInputs (0x03); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x83), "released pins read their pull-up"); + _portB.ApplyInputs (0x80, 0x00); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x03)); + } + + [Test (Description = "PullUpMask matches GetPinState")] + public void PullUpMaskMatchesGetPinState () + { + foreach (var oc in new byte[] { 0x00, 0x0f }) { + _portB.OpenCollector = oc; + foreach (var ddr in new byte[] { 0x00, 0x3c, 0xff }) { + foreach (var port in new byte[] { 0x00, 0x55, 0xff }) { + Cpu.WriteData (DDRB, ddr); + Cpu.WriteData (PORTB, port); + byte expected = 0; + for (byte bit = 0; bit < 8; bit++) + if (_portB.GetPinState (bit) == PinState.InputPullup) expected |= (byte)(1 << bit); + Assert.That (_portB.PullUpMask, Is.EqualTo (expected), $"oc={oc:x2} ddr={ddr:x2} port={port:x2}"); + } + } + } + } + + [Test (Description = "PinChanged reports chip-side changes with the cycle, honours WatchMask, costs nothing unsubscribed")] + public void PinChangedEvent () + { + var events = new List<(byte changed, byte en, byte lev, ulong cycle)> (); + AvrPinChangeHandler handler = (c, e, l, cy) => events.Add ((c, e, l, cy)); + Cpu.WriteData (DDRB, 0x03); + Cpu.WriteData (PORTB, 0x01); // nobody subscribed yet + _portB.PinChanged += handler; + + Cpu.Cycles = 100; + Cpu.WriteData (PORTB, 0x02); + Assert.That (events, Is.EqualTo (new[] { ((byte)0x03, (byte)0x03, (byte)0x02, 100UL) })); + + events.Clear (); + _portB.WatchMask = 0x02; + Cpu.Cycles = 200; + Cpu.WriteData (PORTB, 0x01); + Assert.That (events, Is.EqualTo (new[] { ((byte)0x02, (byte)0x03, (byte)0x01, 200UL) })); + + events.Clear (); + Cpu.WriteData (PORTB, 0x01); // unchanged + Cpu.WriteData (DDRB, 0x01); // PB1 released: output enable changes + Assert.That (events, Is.EqualTo (new[] { ((byte)0x02, (byte)0x01, (byte)0x01, 200UL) })); + + events.Clear (); + _portB.PinChanged -= handler; + Cpu.WriteData (PORTB, 0x00); + Assert.That (events, Is.Empty); + } + + [Test (Description = "Nothing changed: no PCINT flag, no external clock listener")] + public void NoChangeFiresNothing () + { + Cpu.WriteData (PCMSK0, 0xff); + Cpu.WriteData (PCICR, 1); + var clocks = 0; + for (var i = 0; i < 8; i++) _portB.ExternalClockListeners[i] = _ => clocks++; + + _portB.SetInputs (0xff, 0x0f); + Assert.That (clocks, Is.EqualTo (4)); + Assert.That (Cpu.Mmio.Data[PCIFR] & 1, Is.EqualTo (1)); + Cpu.Mmio.Data[PCIFR] = 0; + clocks = 0; + + _portB.SetInputs (0xff, 0x0f); + _portB.SetPinValue (0, true); + _portB.SetPinValue (5, false); + _portB.ReleasePin (7); // changes: pin 7 was driven low, now floats low + Assert.That (clocks, Is.EqualTo (0), "no PIN bit changed, so no listener"); + Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (0)); + + _portB.ReleasePin (7); + _portB.SetInputs (0x7f, 0x0f); + Assert.Multiple (() => { + Assert.That (clocks, Is.EqualTo (0)); + Assert.That (Cpu.Mmio.Data[PCIFR], Is.EqualTo (0)); + }); + } + + [Test (Description = "Redundant SetPinValue / ReleasePin / SetInputs do not recompute PIN")] + public void RedundantCallsAreSilent () + { + _portB.SetPinValue (2, true); + Cpu.Mmio.Data[PINB] = 0x77; // a recompute would overwrite this + _portB.SetPinValue (2, true); + _portB.SetInputs (0x04, 0x04); + _portB.ReleasePin (3); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x77)); + + _portB.SetPinValue (2, false); + Assert.That (Cpu.Mmio.Data[PINB], Is.EqualTo (0x00)); + } + + [Test (Description = "OutputEnableMask is DDR, minus open-collector pins that are high")] + public void OutputEnableMaskFollowsDdr () + { + Cpu.WriteData (DDRB, 0x35); + Cpu.WriteData (PORTB, 0x15); + Assert.Multiple (() => { + Assert.That (_portB.OutputEnableMask, Is.EqualTo (0x35)); + Assert.That (_portB.OutputLevels, Is.EqualTo (0x15)); + }); + + _portB.OpenCollector = 0x01; + Assert.Multiple (() => { + Assert.That (_portB.OutputEnableMask, Is.EqualTo (0x34)); + Assert.That (_portB.OutputLevels, Is.EqualTo (0x14)); + Assert.That (_portB.GetPinState (0), Is.EqualTo (PinState.InputPullup)); + }); + } + + [Test (Description = "With timer 0 driving OC0A, the masks agree with GetPinState on every count")] + public void OutputMasksWithTimerOverride () + { + Cpu.WriteData (DDRD, 0x40); // PD6 = OC0A + Cpu.WriteData (OCR0A, 128); + Cpu.WriteData (TCCR0A, 0x83); + Cpu.WriteData (TCCR0B, 1); + + var sawHigh = false; + var sawLow = false; + var cycle = Cpu.Cycles; + for (var i = 0; i < 600; i++) { + Cpu.Cycles = ++cycle; + Cpu.Tick (); + var high = _portD.GetPinState (6) == PinState.High; + sawHigh |= high; + sawLow |= !high; + Assert.Multiple (() => { + Assert.That (_portD.OutputEnableMask, Is.EqualTo (0x40)); + Assert.That ((_portD.OutputLevels & 0x40) != 0, Is.EqualTo (high)); + Assert.That (_portD.OutputLevels & ~0x40, Is.EqualTo (0)); + }); + } + Assert.That (sawHigh && sawLow, Is.True); + } +}