AGPL-3.0 · Rust · one binary

Run STM32 firmware in CI, and get the same answer every time.

zensim boots the firmware you flash on the board, on models of the silicon, under virtual time. A script drives the console and judges the run — so a test that passes once passes always, on any host.

$ curl -fsSL https://zensimlabs.com/install.sh | sh

macOS arm64 today. Elsewhere, a Rust toolchain builds it.

zensim test tests/scenarios -j 4
running 235 scenario(s), 4 at a time
PASS  stm32f4_peripherals   virtual   1.204ms   wall  0.061s
PASS  stm32h7_full          virtual  3.7785s    wall 22.301s
PASS  stm32l0_lowpower      virtual  412.0ms    wall  0.088s
PASS  zephyr_hello_h743     virtual   84.9ms    wall  0.412s
────────────────────────────────────────────────
235 scenarios: 208 passed, 0 failed, 27 skipped
same virtual time on every host, every run
146
STM32 profiles
23
boards
149
device models
235
scenarios

What it does

Deterministic to the picosecond

Time is virtual and every external input is timestamped. A run that passes once passes always, however fast or busy the host is — which is what makes a simulator worth putting in CI.

Your firmware, unmodified

The same ELF you flash on the board. The console, the LEDs, the timers and the DMA are the real register interfaces, so the firmware cannot tell the difference until it reads something nobody modelled.

Peripheral models, not stubs

USART with its FIFO, timers to the advanced ones, RCC with a clock tree per family, flash with its programming sequences, ADC, SPI, I²C, CAN, SDMMC, QSPI, USB, Ethernet. What is missing says so and is counted.

Scripted, and honest about it

Wait for text on a UART, type into it, read memory, set a breakpoint, drive a pin, assert. Scenarios run in parallel with a timeout each and write JUnit, so a failure names the expectation it broke.

Boards from your devicetree

Point the importer at the flattened devicetree your firmware build leaves behind and it writes the platform file: memory map, peripherals, NVIC lines, bus clocks, DMA requests, console, LEDs, buttons.

One binary

No emulator, no JIT, no toolchain at run time, no container. A Cortex-M interpreter in Rust, the models beside it, and a Starlark reader for the platforms and the scenarios.

A scenario

Platforms and scenarios are Starlark files. Load an ELF, wait for what the firmware prints, assert on it. Run one with zensim run, or a directory of them in parallel with zensim test.

m = platform("//boards/st/nucleo_h743zi.star")
m.load_elf("build/zephyr/zephyr.elf")

console = m.device("usart3")
console.echo(True)
console.wait_for("Hello World!", timeout = "1s")
log("booted in %d us" % (m.now_ns() // 1000))

Install

$ curl -fsSL https://zensimlabs.com/install.sh | sh

It reads the latest release, checks its SHA-256, and installs the binary and the platform library. It clones nothing and compiles nothing. ZENSIM_VERSION, ZENSIM_INSTALL_DIR and ZENSIM_NO_MODIFY_PATH change what it does.

What it is not, yet

  • —A profile's clocks are fixed at the frequency the generator chose; the clock tree answers its registers but the models do not follow a change.
  • —TrustZone has its registers and filters no access.
  • —Some scenarios need a firmware built from a workspace of your own, and skip cleanly without it.
  • —Only STM32 is published. Other vendors exist and do not ship.