End-to-end tutorial#
This guide walks through the full path: from zero to a passing integration test that compiles a real Arduino sketch and validates its output in the simulator. No physical hardware required.
What you’ll build#
A .NET test project that:
Compiles an Arduino sketch to a
.hexfile usingarduino-cliLoads the firmware into
ArduinoUnoSimulationAsserts that the correct output appears on Serial within a bounded run
The sketch prints a counter every 500 ms. The test verifies the first three lines.
Prerequisites#
.NET 10 SDK —
dotnet --versionshould show10.xarduino-cli — install from arduino.cc/en/software or
brew install arduino-clion macOS
Install the AVR core#
arduino-cli core install arduino:avr
1. Create the test project#
mkdir firmware-tests && cd firmware-tests
dotnet new nunit
dotnet add package Avr8Sharp.TestKit --prerelease
Your project file should look like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="NUnit" Version="4.*" />
<PackageReference Include="NUnit3TestAdapter" Version="4.*" />
<PackageReference Include="Avr8Sharp.TestKit" Version="*-*" />
</ItemGroup>
</Project>
2. Write the Arduino sketch#
Create sketch/sketch.ino:
void setup() {
Serial.begin(115200);
}
void loop() {
static int count = 0;
Serial.print("count=");
Serial.println(count++);
delay(500);
}
3. Compile with arduino-cli#
arduino-cli compile \
--fqbn arduino:avr:uno \
--output-dir build/ \
sketch/
This produces build/sketch.ino.hex.
4. Write the test#
Replace Tests.cs with:
using Avr8Sharp.TestKit.Boards;
using FluentAssertions;
using NUnit.Framework;
[TestFixture]
public class FirmwareTests
{
private static readonly string HexPath =
Path.Combine(TestContext.CurrentContext.TestDirectory,
"..", "..", "..", "..", "build", "sketch.ino.hex");
[Test]
public void Counter_increments_and_prints_to_serial()
{
var uno = new ArduinoUnoSimulation()
.WithHex(File.ReadAllText(HexPath));
// Wait up to 3 s of simulated time for three lines to appear
uno.RunUntilSerial(uno.Serial, "count=2", maxMs: 3000);
uno.Serial.Should().ContainLine("count=0");
uno.Serial.Should().ContainLine("count=1");
uno.Serial.Should().ContainLine("count=2");
}
[Test]
public void Counter_stays_within_cycle_budget()
{
var uno = new ArduinoUnoSimulation()
.WithHex(File.ReadAllText(HexPath));
uno.RunUntilSerial(uno.Serial, "count=2", maxMs: 3000);
// Deterministic across machines — use as a regression guard
Assert.That(uno.Cpu.Cycles, Is.LessThan(50_000_000UL));
}
}
5. Run the tests#
dotnet test
Expected output:
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2
6. What just happened#
ArduinoUnoSimulationwired up an ATmega328P with 16 MHz clock, PortB/C/D, three timers, USART0, and EEPROM — all in one line.WithHex(...)loaded the compiled firmware into flash.RunUntilSerial(uno.Serial, "count=2", maxMs: 3000)ran the simulation until"count=2"appeared in the captured serial output, or threwTimeoutExceptionafter 3 s of simulated time. Simulated time is driven by CPU cycles — the run always takes the same number of cycles regardless of host machine speed.ContainLineasserted that specific lines appeared in the output.
7. Testing GPIO#
If the sketch also blinks an LED:
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT); // PB5 on Arduino Uno
}
void loop() {
static int count = 0;
Serial.println(count++);
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
}
Add an assertion on pin state after an odd number of blink cycles:
// After 3 toggles the LED should be HIGH
uno.RunUntilSerial(uno.Serial, "2", maxMs: 2000);
uno.PortB.Should().HavePinHigh(5); // PB5 = Arduino digital pin 13 = LED_BUILTIN
See Board pin maps to translate Arduino pin numbers to the port/pin pairs used by the simulator.
8. Testing with sensors (ADC / UART RX / I²C)#
If the firmware reads a sensor, inject the simulated value before running:
var uno = new ArduinoUnoSimulation()
.WithHex(hex)
.AddAdc(AvrAdc.AdcConfig, out var adc);
// Simulate a temperature sensor reading ~25 °C (500 mV on a TMP36)
adc.ChannelValues[0] = 500; // millivolts on A0
uno.RunUntilSerial(uno.Serial, "temp=25", maxMs: 2000);
uno.Serial.Should().ContainLine("temp=25");
See Simulating external input for UART RX, GPIO, and I²C injection.