Skip to content

Hardware vs Sim86

Compare ESP32-C3 firmware execution on hardware and in Sim86. See signal, timing, and CPU cycle results, with downloadable benchmark evidence.

Will your firmware behave the same way in Sim86 as it does on a board? We ran a firmware test on a physical ESP32-C3 and in Sim86 to compare their behavior.

Sim86 produced the same signals, in the same order, as the board. Some happened a little earlier or later.

We also measured CPU cycles—the processor’s clock ticks—with the same instrumented firmware on both sides. We ran this test five times on hardware and five times in Sim86. Hardware repeated its cycle counts exactly, and Sim86 repeated its own counts exactly. The small differences were between the board and Sim86, not between repeated runs of either. Here’s how they compare.

We tested three ways firmware exchanges data: I2C, SPI, and CAN protocol logic. Both sides of the conversation run on one chip. This test doesn’t use a physical CAN transceiver or a fleet of devices.

A signal transition is a change from low to high or back again. CPU cycles count processor clock ticks during each part of the test.

What we checkedHardwareSim86Result
Data signal208 transitionsSame 208 transitions, in the same orderMatches
Clock signal480 transitionsSame 480 transitions, in the same orderMatches
Data timingRecorded edge timesUp to 0.167 ms earlier or laterDiffers
Clock timingRecorded edge timesUp to 0.020 ms earlier or laterDiffers
I2C execution1,082,154 CPU cycles1,085,296 CPU cycles0.29% more cycles
SPI execution1,273,780 CPU cycles1,272,574 CPU cycles0.095% fewer cycles
Two CAN frames1,003,694 / 1,179,694 cycles1,003,685 / 1,179,686 cycles9 / 8 fewer cycles

For timing, we lined up the first signal change in both recordings. The signal and CPU-cycle measurements use separate firmware builds. These results come from a local Sim86 build. The full setup is below.

In this test, Sim86 produced the same sequence of signals as the board. Some signals arrived a little earlier or later. The largest timing difference was about 0.17 milliseconds.

Think of two people playing the same notes in the same order, with one slightly ahead of the other. The sequence matches; the timing isn’t identical.

We found that Sim86 moved through one pause faster than the board. That explains where most of the timing gap appeared. We’re still investigating what caused it.

Some parts of the test deliberately wait before continuing. Those waits can make the overall times look very close, even if individual steps take different amounts of time. So these results don’t mean every processor step takes exactly as long as it does on hardware.

It depends on what your firmware needs to do. If you’re checking the order of signals, these results show a match. If your device must respond within a strict time limit, that limit needs its own test.

This is one test on one controller. It doesn’t yet tell us how your firmware handles a failed update, a lost connection, or a power cut. We can help you define a comparison around the situations that matter to your product.

Book a call to discuss your board and the behavior you need to verify. We add controllers on demand, free of charge for our users when you ship us a development board. Contact us about the controller or component you need.

Download the benchmark files for the firmware, raw captures, results, and comparison scripts. The details below explain exactly what we measured and how to check it yourself.

Signal measurements and test setup

A signal transition is an edge: a pin changing from low to high or high to low. We line up the first falling edge on D4, then compare the time of each corresponding edge. This removes the boot-time difference.

Offsets below are simulator minus hardware. A negative value means the simulated edge happened earlier. These are offsets from the aligned start, not the error in each individual bit period.

Timing measurementD4 dataD5 clock
Median edge offset−7.317 µs−4.088 µs
Minimum edge offset−166.737 µs−14.202 µs
Maximum edge offset+19.571 µs+19.596 µs
Largest absolute edge offset166.737 µs19.596 µs

The logic analyzer samples at 12 MHz, approximately once every 83.3 ns. The measured timing differences are larger than that sampling interval. One capture per environment does not measure run-to-run variation or temperature sensitivity. Matching signal sequences alone does not demonstrate timing accuracy.

ItemSetup
Physical boardXIAO ESP32-C3, D4/GPIO6 and D5/GPIO7
CaptureFX2 logic analyzer, 12 MHz, common ground
Hardware acquisitionSeptember 29, 2026; archived capture
Simulator replayOctober 15, 2026; local native simulator build, 1,000 simulated ms
FirmwareMerged flash image; binary matches the repository artifact retained with the original bench capture
AlignmentFirst falling edge on D4 is time zero in each trace

The original acquisition did not record a separate hash read back from the physical board. The manifest records that limitation, the artifact hashes, and the provenance of this replay. The local simulator executable is identified by its hash.

Where the timing differences accumulate

We re-analyzed the original GPIO captures, aligning the first falling D4 edge and looking for intervals longer than 1 ms with no transitions on either recorded pin. There are three such gaps in each capture. The last is 2,603.416 µs on hardware and 2,458.044 µs in Sim86: a difference of −145.372 µs.

The offset is −18.842 µs at the start of this gap and −164.214 µs at its end. The largest data-edge offset later reaches −166.737 µs. This localizes most of that cumulative difference to the quiet interval; it does not establish its cause. A quiet interval can include firmware work with no visible pin transitions.

The firmware source shows that can_frame uses deadlines calculated from the frame’s start, while wait_us starts a fresh relative wait each time. The CAN measurement excludes reporting and the wait between frames; the GPIO trace includes the gaps between visible transitions. Also, the GPIO and cycle experiments use different binaries. Their numbers cannot be treated as measurements of the same run.

Instruction costs, interrupt handling, memory access, and reporting overhead are possible contributors to investigate, not established explanations for the measured differences. The next diagnostic step is to measure those intervals separately on hardware and in the same simulator build.

The bundle includes explain-timing.py and its output. To reproduce:

Terminal window
python3 hardware-benchmark/explain-timing.py
CPU cycle counts and measurement method
PhaseHardwareSim86Difference
I2C phase: CPU cycles1,082,1541,085,296Differs — +3,142 cycles (+0.2903%)
SPI phase: CPU cycles1,273,7801,272,574Differs — −1,206 cycles (−0.0947%)
CAN arbitration frame: CPU cycles1,003,6941,003,685Differs — −9 cycles (−0.000897%)
CAN retry frame: CPU cycles1,179,6941,179,686Differs — −8 cycles (−0.000678%)

On October 16, 2026, we flashed a newly instrumented version of the loopback firmware onto the connected ESP32-C3 and ran the same merged binary in a local Sim86 build. Flash verification confirmed that the board’s image matched the file. We collected five hardware runs with a USB reset before each, and five fresh simulator runs. All ten completed the workload.

The firmware reads esp_cpu_get_cycle_count() at the start and end of the I2C phase, SPI phase, and each of the two CAN frames. It subtracts the start count from the end count and reports the totals after the workload. CPU frequency is configured at 160 MHz, with power management disabled.

The I2C and SPI measurements include interrupts and intentional waits; CAN frame measurements run inside the workload’s existing critical section. Console reporting is outside the measured intervals. Counter-read overhead is retained on both sides. Startup is excluded; the hardware USB reset and simulator fresh boot differ.

This is an elapsed-cycle comparison for these four phases. It does not isolate individual instruction costs, interrupt latency, or cache effects. In particular, the workload uses cycle-counter waits to control bus timing. A small percentage difference here must not be presented as a general CPU accuracy percentage.

The instrumented firmware is a different binary from the original GPIO capture. Its results are a separate experiment, not extra measurements of the older run.

The benchmark bundle includes a cycles/ directory containing the instrumented firmware source, merged binary, all ten console captures, results, and build and measurement details. To recalculate the table after extracting the bundle:

Terminal window
python3 hardware-benchmark/cycles/compare-cycles.py hardware-benchmark/cycles
Bus edge cases included in the workload

These cases are present in the published firmware. The comparison checks the complete pin traces; it does not separately decode and score each protocol case. Included means exercised by the workload, not an independent pass for that behavior.

Edge caseHardwareSim86What the comparison establishes
I2C clock stretchingFirmware holds the clock low while processingSame workloadIncluded — overall signal sequence matches; stretch duration not scored separately
I2C repeated STARTSwitches from write to read without an intervening STOPSame workloadIncluded — overall signal sequence matches; transaction not separately decoded
I2C missing device / NACKAddresses an unassigned device addressSame workloadIncluded — overall signal sequence matches; recovery policy not evaluated
I2C final-byte NACKReader ends the read with a NACKSame workloadIncluded — overall signal sequence matches; ACK/NACK values not separately scored
Irregular I2C clock periodsFixed-seed variation in bit delaysSame workloadIncluded — timing differences reported above; not a timing-boundary sweep
SPI shared data-line turnaroundCommand sender releases the line for the responseSame workloadIncluded — overall signal sequence matches; electrical contention not measured
CAN arbitrationTwo software roles compete; one stops transmittingSame workloadIncluded — single-chip GPIO logic, not a physical CAN bus test
CAN retry after arbitration lossLosing role transmits in the next frameSame workloadIncluded — overall signal sequence matches; retry latency not separately scored
Failure and recovery cases still to benchmark

For a deployment decision, we also want evidence for failure and recovery paths. The cases below are not measured in this published benchmark. “Not measured” is a gap in the evidence here, not a statement that Sim86 supports or cannot support the scenario. Each needs a defined setup and paired hardware/simulator runs.

Edge caseHardware resultSim86 resultWhat we would compare
Power loss during an OTA writeNot measuredNot measuredImage integrity, selected boot image, and recovery after restart
Reset during OTA activationNot measuredNot measuredBoot selection and whether the device reaches a usable state
Corrupted or incomplete updateNot measuredNot measuredRejection behavior and preservation of the previous working image
New firmware fails its first bootNot measuredNot measuredRollback decision, persistent state, and time to recovery
Network drops during an updateNot measuredNot measuredRetry, resume, timeout, and final image integrity
Devices running mixed firmware versionsNot measuredNot measuredCommunication and behavior while only part of the fleet is updated
Duplicate, delayed, or reordered messagesNot measuredNot measuredState transitions, repeated actions, and timeout handling
Device disconnects and rejoinsNot measuredNot measuredReconnection, state synchronization, and handling of stale data
I2C data or clock held lowNot measuredNot measuredTimeout, bus recovery, and whether other work continues
Peripheral stops responding mid-transferNot measuredNot measuredError reporting, retries, and return to normal operation
Corrupted bus frameNot measuredNot measuredError detection, discarded data, and recovery behavior
Interrupt burst or competing interruptsNot measuredNot measuredMissed events, execution order, and response latency
Watchdog expiry during a stalled taskNot measuredNot measuredReset timing, reset cause, and state after reboot
Deadline under peak loadNot measuredNot measuredCompletion times against an application-defined deadline
Reset while an output is activeNot measuredNot measuredPin states during reset and the application’s required output state
Interrupted persistent-state writeNot measuredNot measuredStored-data integrity and recovery on the next boot
Supply dip or brownoutNot measuredNot measuredReset thresholds and recovery; requires a defined electrical model

These are candidate comparisons, not safety qualification results. Acceptance criteria must come from the specific device and its required behavior.

Reproduce the comparison

The Sim86 CLI is currently available to a closed group of users. Book a call with us to request access. You do not need CLI access to compare the downloadable traces below.

The complete benchmark bundle contains both captures, the comparison script, results, manifest, and firmware source project.

Download hardware.json, simulation.json, and compare.py into one directory. Python 3 is sufficient; the script has no third-party dependencies.

Terminal window
python3 compare.py hardware.json simulation.json

This checks edge counts and ordered levels, and reports timing offsets. It exits nonzero when the level sequences differ. Without a timing threshold, a pass means sequence agreement only. The complete output is available as result.json.

You can set an acceptance threshold explicitly:

Terminal window
python3 compare.py hardware.json simulation.json --max-offset-us 20

That command fails for this capture because D4 exceeds 20 µs. The threshold is an illustration, not a recommended tolerance for your application. Choose a tolerance from the behavior your firmware must satisfy, before inspecting results.

The published traces reproduce the calculation above. They do not include a rebuildable distribution of the simulator backend. For a fresh hardware capture, the firmware source and merged firmware image identify the workload. The bundle includes the ESP-IDF project under firmware-source. In an ESP-IDF shell, build it with idf.py set-target esp32c3 and idf.py build from that directory. A different compiler or ESP-IDF version can change timing; use the retained binary to keep the artifact fixed when repeating the bench run.

Design a benchmark for your own firmware
  1. Keep the artifact fixed. Record the firmware hash, board revision, toolchain, clocks, and configuration. Flash that artifact to the physical board and load the corresponding image into the simulator.
  2. Keep the circuit explicit. Record connections, supplies, pull-ups, loads, and measurement points. A carrier board or analyzer can change pin behavior.
  3. Choose observations and tolerances. Separate protocol bytes, pin levels, event timing, and wall-clock execution speed. Declare which differences fail the test and how startup is aligned.
  4. Capture both sides. Preserve the raw traces, simulator version or build identifier, seed, analyzer sample rate, and acquisition method. Repeat runs when the behavior varies; report the distribution rather than one favorable run.
  5. Keep the failures. Record missing edges, unexpected states, unsupported peripherals, and timing differences alongside successful checks.

The comparison script accepts two channels named w0 and w1, each with an edges array of [timestamp_ns, level] pairs. Level 1 means low and 2 means high in this capture format. Export or convert both instruments to those units and conventions before comparing them; reject floating or unknown states instead of silently turning them into zeros.

Use the automated firmware testing guide to turn a requirement into a repeatable CI check. For board setup, see the ESP32-C3 guide or the firmware simulation examples. For wireless tests, start with the RF Environment and define a separate comparison for the radio behavior you need to measure.

Docs
Open dashboard ↗