Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bindings/python/src/avr8sharp/_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
17 changes: 17 additions & 0 deletions bindings/python/src/avr8sharp/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down
28 changes: 28 additions & 0 deletions bindings/python/tests/test_pin_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
33 changes: 33 additions & 0 deletions src/Avr8Sharp.Native/Exports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,39 @@ public static int GpioSetPin(IntPtr h, int portIdx, int pin, int value)
s.Ports[portIdx].SetPinValue((byte)pin, value != 0);
});

/// <summary>Drives only the pins in <paramref name="mask"/> of port <paramref name="portIdx"/> to the
/// matching bits of <paramref name="levels"/>; other pins are left as they were.</summary>
[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);
});

/// <summary>Stops driving the pins in <paramref name="mask"/> of port <paramref name="portIdx"/>
/// (see <c>a8s_gpio_release_pin</c>), in one call.</summary>
[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);
});

/// <summary>Sets the whole external state of port <paramref name="portIdx"/>: pins in
/// <paramref name="drivenMask"/> are driven to <paramref name="levels"/>, every other pin is released.</summary>
[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);
});

/// <summary>Stops driving <paramref name="pin"/> on port <paramref name="portIdx"/>, undoing
/// <c>a8s_gpio_set_pin</c>: 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.</summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Avr8Sharp/Avr8Sharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>

<!-- NuGet package metadata -->
<PackageId>Avr8Sharp</PackageId>
Expand All @@ -25,6 +27,10 @@
<LangVersion>default</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SiliconTwin.Abstractions" Version="0.1.0" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\..\LICENSE-BUSL.txt" Pack="true" PackagePath="" Visible="false" />
Expand Down
Loading
Loading