A fast, multi-architecture emulator and network simulator for Contiki-NG, written in portable C. csim is a clean-room re-implementation of the parts of Cooja and MSPSim needed to run the upstream Contiki-NG test suite headlessly, with a strong focus on simulation speed, deterministic timing, and faithful peripheral behaviour.
Status: the full Contiki-NG Cooja test suite passes — 88 / 88 including the TUN/border-router cases. See Test results below.
┌──────────────────────────────────────────┐
│ csim test_runner │
│ │
firmware/*.sky ────►│ MSP430F1611/F2617/F5437/CC430 + CC2420 │
firmware/*.cc2538dk►│ ARM Cortex-M3 + CC2538 RF Core │──► UART, packets,
firmware/*.cooja ─►│ Native Cooja motes (dlopen) │ timeline, UI
│ │
│ shared event queue • UDGM radio medium │
│ ns-precise time • JS-driven assertions │
└──────────────────────────────────────────┘
- What is csim?
- Features
- Quick start
- Building
- Running tests
- Multi-node simulation
- JSON simulation configs
- The Cooja test suite
- Web UI
- Performance
- Architecture
- Tuning and environment variables
- Project layout
- Test results
- Known issues
- License
csim runs Contiki-NG firmware binaries inside an emulator process and lets you connect multiple emulated nodes through a shared 802.15.4 radio medium. It is designed for three use cases:
- Headless CI / regression testing — replace
Cooja --no-guifor the upstream Contiki-NG test suite. csim consumes the same.cscfiles (viatools/csc2json.py) and the same JS test scripts (via the embedded QuickJS engine). - Network research — run hundreds of emulated nodes on a single machine, mix MSP430 / ARM / native motes in the same network, script topology changes, and capture full packet timelines.
- Firmware debugging — boot a single firmware image, watch UART, inspect register state on a wedge, and re-run with deterministic seeds.
Compared to upstream Cooja + MSPSim, csim is roughly an order of magnitude faster, has no JVM dependency, and builds cleanly with make on Linux and macOS. It is not a full Cooja replacement: there is no GTK GUI, no Java plugin ecosystem, and no support for closed-source mote types.
- Full MSP430 + MSP430X instruction set — computed-goto interpreter, all double-op / single-op / jump instructions, extension words for
.A/.W/.Bmodes, repeat counts (immediate and register), zero-carry, 20-bit PC and addressing. - Cycle-accurate timing — operand-mode cycle tables matching MSPSim, 6-cycle interrupt service, deterministic event scheduling.
- Optional JIT — GNU Lightning compiles hot basic blocks to native code (~430 MIPS for ALU-bound micro-benchmarks on Apple Silicon). Auto-detected via
pkg-config; without it the interpreter is used everywhere. Disabled for MSP430X by design (extension-word ambiguity). - CC2420 radio — full state machine matching
CC2420.java: VREG_OFF → POWER_DOWN → IDLE → calibrate → SFD search → frame reception, plus the TX chain. CCITT-16 with bit reversal, address filtering, auto-ACK, RXFIFO circular buffer, RX "incoming buffer" for bytes that arrive during calibration. ns-based byte timing (16 µs symbol / 32 µs byte / 192 µs cal / 1 ms VREG startup). - Peripherals — Timer A/B (on-demand counter, CCR compare, capture mode, all clock sources), USART/USCI in UART and SPI mode, GPIO ports P1–P10 with edge-triggered interrupts, DCO clock with the exact MSPSim frequency formula, hardware multiplier (MPY/MPYS/MAC/MACS).
- Platforms — Tmote Sky (F1611), ETH ESB (F149), Zolertia Z1 (F2617), WisMote / EXP5438 (F5437), CC430F5137 eval board.
- Debug helpers — ELF loader with symbol lookup, instruction-level tracing hooks, JIT cache inspection.
- Thumb / Thumb-2 interpreter — full Cortex-M3 user instruction set including IT blocks, exclusive load/store stubs, bit-band region support.
- NVIC — interrupt priority, pending/enable, exception entry/exit, tail-chaining.
- SysTick — periodic tick generation through the shared event queue.
- CC2538 SoC peripherals — UART (TX callback + status flags), GPIO ports A–D with interrupts, General Purpose Timers (one-shot / periodic / prescaler), Sleep Timer (32 kHz, compare-match interrupt), System Control (clock config, OSC32K), IO Controller (pin mux), and the on-chip RF Core (802.15.4 TX/RX, FFSM address filter, RFRND, frame interrupts).
- Platform — TI SmartRF06 + CC2538EM (
cc2538dk).
.coojashared libraries loaded withdlopen, exposing the samesimInSize/simOutSize/simRtimerNextExpirationTimeinterface that real Cooja uses.- Cross-platform networking — native, MSP430, and ARM nodes can be mixed in a single simulation network.
- Dynamic transmission state — explicit
radio_is_transmitting/radio_tx_finishedinterval matching Cooja'sContikiRadio.doActionsAfterTick().
- Time-stepped event loop — all nodes share a single ns-precise simulation clock; each step advances every node to the same target.
- Per-node CPU frequencies handled correctly across DCO calibration.
- UDGM radio medium — Unit Disk Graph with separate transmission and interference ranges, configurable TX/RX success ratios, deterministic seed.
- Per-byte RF delivery matching Cooja's wire-level model — bytes are streamed into receivers as they arrive on the medium, not as full frames.
- Packet analyzer that decodes 802.15.4 / 6LoWPAN / IPv6 / RPL / UDP frames at runtime for human-readable logs.
- Timeline recorder — every radio TX/RX event is timestamped and can be exported to JSON for offline analysis.
- Threading — single-threaded by default (deterministic, fastest for ≤100 nodes), with an optional
--threads Nmode for very large topologies.
- JSON config files describing nodes, topology, radio medium, timed actions (move, send, remove, add), assertion steps, fail-on patterns, and aggregate validators. See
docs/test-format.mdfor the full schema. - Embedded QuickJS engine that runs Cooja-style JavaScript test scripts inline (
TIMEOUT,WAIT_UNTIL,log.testOK,log.testFailed). csc2json.pyauto-converts Cooja.cscfiles (and most of their JS scripts) to csim JSON.run-cooja-tests.shdrives the entire upstream Contiki-NG test suite headlessly, with PASS / FAIL / SKIP reporting and auto-build of missing firmware.
- Embedded WebSocket server (
--ui [port]) serves a single-page topology view athttp://localhost:8080/showing live node positions, RPL parent links, packet animations, and per-node UART output. - Pure C, no Node.js, no build dependencies.
# 1. Clone with the Contiki-NG sibling for firmware builds
git clone https://github.com/your-org/csim.git
git clone https://github.com/contiki-ng/contiki-ng.git
cd csim
# 2. Build (auto-detects GNU Lightning for JIT)
make
# 3. Run the unit tests (~5 seconds)
./build/test_runner correctness # 68 MSP430 instruction tests
./build/test_runner arm-correctness # 33 ARM instruction tests
# 4. Run a real RPL-UDP simulation: 60 simulated seconds in ~250 ms wall-clock
./build/test_runner mixed-multinode \
firmware/sky/udp-server.sky firmware/sky/udp-client.sky -t 60000
# 5. Run the full Contiki-NG Cooja test suite (88 tests)
make configure CONTIKI_DIR=$(pwd)/../contiki-ng
make cooja-tests VERBOSE=1| Target | Flags | Notes |
|---|---|---|
make |
-O3 -flto -march=native |
Default release build, JIT auto-detected |
make debug |
-O0 -g -DDEBUG |
Debuggable, no LTO |
make pgo |
Two-stage profile-guided optimization | Apple Clang only, ~40 % faster on hot loops |
make clean |
— | Remove build/ |
Optional dependencies
| Dependency | Purpose | How it's detected |
|---|---|---|
| GNU Lightning | MSP430 JIT compiler | pkg-config --libs lightning (silent fallback to interpreter if missing) |
| QuickJS | JS test scripts | Bundled under lib/quickjs/ — built automatically |
| cJSON, cbor | Config / serialization | Bundled under lib/ |
| Contiki-NG | Test firmware sources | csim.conf, CONTIKI_DIR env var, or ../contiki-ng |
There are no other runtime dependencies besides libc, libm, libpthread, and (on Linux for the TUN tests) iproute2 + tunslip6.
Setting CONTIKI_DIR
make configure CONTIKI_DIR=/absolute/path/to/contiki-ng
# or
export CONTIKI_DIR=/absolute/path/to/contiki-ngThis is only needed if you want to (re)build firmware or run make cooja-tests. Pre-built firmware ships under firmware/ for direct use.
| Command | What it does |
|---|---|
./build/test_runner correctness [-v] |
68 MSP430 instruction-level tests (MOV, ADD, jumps, MSP430X MOVA/ADDA/SUBA, cycle counts, byte mode, CALL/RET, CMP flags…) |
./build/test_runner arm-correctness [-v] |
33 ARM Cortex-M3 instruction tests (MOV, ADD, CMP, logic, shifts, load/store, branches, extensions, ADC/SBC) |
./build/test_runner timeline [-v] |
76 unit tests for the radio event timeline serializer |
./build/test_runner firmware [-v] |
Boots cputest.sky and timertest.sky to completion |
./build/test_runner arm-firmware [-v] |
Boots hello-world.cc2538dk (full Contiki-NG init + UART output) |
./build/test_runner bench |
7 micro-benchmarks + 2 firmware benchmarks, prints MIPS |
./build/test_runner all [-v] |
All of the above except multi-node |
# MSP430 RPL-UDP, 2 Sky nodes, 60 simulated seconds
./build/test_runner mixed-multinode \
firmware/sky/udp-server.sky firmware/sky/udp-client.sky -t 60000
# ARM RPL-UDP, 2 CC2538DK nodes, 60 simulated seconds
./build/test_runner mixed-multinode \
firmware/cc2538dk/udp-server.cc2538dk \
firmware/cc2538dk/udp-client.cc2538dk -t 60000
# Mixed network: Sky server + CC2538 client
./build/test_runner mixed-multinode \
firmware/sky/udp-server.sky \
firmware/cc2538dk/udp-client.cc2538dk -t 60000
# Native Cooja mote (cooja-side compiled to a shared library)
./build/test_runner mixed-multinode \
firmware/cooja/udp-server.cooja \
firmware/cooja/udp-client.cooja -t 60000
# 100-node grid from a JSON config
./build/test_runner mixed-multinode configs/udgm-100node-grid-arm.jsonNode platform is auto-detected from the firmware extension:
| Extension | Platform |
|---|---|
.sky |
MSP430 (Tmote Sky + CC2420) |
.cc2538dk |
ARM Cortex-M3 (CC2538 + on-chip 802.15.4) |
.cooja |
Native Cooja shared library |
| Option | Default | Description |
|---|---|---|
-t ms |
20 000 | Simulated duration (overrides JSON timeout_ms if both given) |
-n nodes |
from firmware count | Override node count |
-v |
off | Verbose: print every UART line, RF event, packet decode |
-q |
off | Quiet: suppress per-node UART, only summary stats |
--threads N |
0 (single-threaded) | Use N worker threads for very large simulations |
--ui [port] |
off | Start the WebSocket UI server (default port 8080) |
This is the most thorough validation csim has against real Contiki-NG behaviour. It runs every test in contiki-ng/tests/ headlessly using csim instead of Cooja --no-gui.
# One-time setup
make configure CONTIKI_DIR=/path/to/contiki-ng
# Run all 88 tests, with the 7 TUN border-router tests skipped (no sudo needed)
make cooja-tests
make cooja-tests VERBOSE=1 # show per-test output
# Run a single test or a subset
make cooja-tests PATTERN='07-simulation-base/26-tsch-drift-z1'
make cooja-tests PATTERN='14-rpl-lite*'
make cooja-tests PATTERN='19-cooja-rpl-tsch'
# Include the TUN/border-router tests (needs sudo for tun0 + tunslip6)
sudo -v
sudo ./tools/run-cooja-tests.sh --with-tun -v 2>&1 | tee cooja-tests-tun.log
# Rebuild the test firmware (only needed if Contiki-NG sources changed)
make build-firmware
make build-firmware PATTERN='07-*'run-cooja-tests.sh automatically:
- Globs
tests/*in your Contiki-NG checkout. - Converts each
.cscto JSON viatools/csc2json.py. - Builds any missing firmware (unless
--no-buildis passed). - Runs
build/test_runner mixed-multinode <generated.json>with the JS test script attached. - Aggregates PASS / FAIL / SKIP / ERROR counts and exits non-zero if anything fails.
A complete schema is documented in docs/test-format.md. The short version:
The bundled configs/ directory has working examples for many scenarios:
| File | What it demonstrates |
|---|---|
rpl-udp-sky.json |
Minimal MSP430 server + client |
rpl-udp-cc2538dk.json |
ARM CC2538 RPL-UDP |
rpl-udp-native.json |
Native Cooja motes |
mixed-sky-native.json |
Mixed MSP430 + native in one network |
udgm-3node.json |
UDGM topology with explicit positions |
udgm-in-range.json / out-of-range.json |
Connectivity boundary tests |
udgm-100node-grid.json |
100-node grid (sky / arm / mixed variants) |
test-4node-chain.json |
4-node multi-hop chain with delivery assertions |
test-js-rpl-udp.json |
Inline QuickJS test script |
ui-rpl-udp-grid.json |
16-node 4×4 grid for the web UI |
Run any of them with:
./build/test_runner mixed-multinode configs/udgm-100node-grid-arm.json -v./build/test_runner mixed-multinode configs/ui-rpl-udp-grid.json --ui
# then open http://localhost:8080/ in a browserWhat you get:
- Live 2D node positions (auto-laid out from
x/y). - Animated TX/RX packet flashes when frames are exchanged.
- Per-node UART output streamed to the browser.
- RPL parent links rendered as edges (when the packet analyzer detects DIO/DAO).
- Pause / step / speed controls.
The UI is a single embedded HTML page (ui/index.html) served by a tiny non-blocking WebSocket server (src/ui/ws_server.c). No Node.js, no build step. Up to 8 concurrent browser clients.
Measured on Apple Silicon (M-series, PGO build):
| Benchmark | Speed |
|---|---|
| Micro-benchmarks (avg of 7) | ~430 MIPS |
Firmware blink.sky |
~195 MIPS |
Firmware energest-demo.sky |
~196 MIPS |
| 2-node nullnet (60 s sim) | ~500× real-time |
| 2-node RPL-UDP (60 s sim) | ~1600× real-time |
On Linux x86-64 (release build, no PGO) you can reproducibly expect ~250× real-time on the 2-node RPL-UDP test — your mileage will depend heavily on the host CPU and whether GNU Lightning is available.
| Benchmark | Speed |
|---|---|
| 2-node RPL-UDP (60 s sim) | ~4× real-time |
make pgo # or `make` for non-PGO
./build/test_runner bench # micro + firmware MIPS
./build/test_runner mixed-multinode \
firmware/sky/udp-server.sky firmware/sky/udp-client.sky -t 60000 -q
./build/test_runner mixed-multinode \
firmware/cc2538dk/udp-server.cc2538dk \
firmware/cc2538dk/udp-client.cc2538dk -t 60000 -qThe tail of every multinode run prints a phase-timing breakdown (CPU step / radio deliver / output flush / channel sync / overhead) and a Speed ratio line.
Every CPU tracks two clocks in parallel:
cycles— monotonically increasing CPU-cycle count, the unit the interpreter advances each instruction.sim_time_ns— monotonic ns-precise wall clock, the unit the radio, peripherals, and inter-node coordination use.
These two are reconciled at three sync points: before each event callback fires, at the end of *_step_until(), and inside *_set_frequency() whenever DCO calibration changes the CPU frequency. This dual model is what lets the simulator stay correct when one node speeds up after DCO calibration while another is still running at boot frequency, or when an MSP430 and a CC2538 (different MHz, different clocks) share the same radio medium.
fetch -> dispatch via computed goto (or JIT block)
-> execute, update regs/flags/cycles
-> if (cycles >= next_event_cycle) execute_events()
-> if (interrupts pending && GIE) service_interrupt()
There is no ns-conversion or floating-point math on the hot path. Events are scheduled in cycle units; ns-based events are converted to cycles once at scheduling time and re-converted only on frequency change.
include/common/event_queue.h provides a header-only intrusive linked-list event queue, instantiated once per architecture (EVENT_QUEUE_IMPL(msp430, ...) / EVENT_QUEUE_IMPL(arm, ...)). The same data structure is used for timer compares, radio state transitions, GPIO debounce, and the multi-node sim's per-node wakeups.
while (sim_ns < end_ns) {
sim_ns += time_step_ns; // 1 ms by default
for (each node) {
delta_ns = sim_ns - cpu->sim_time_ns;
target_cycle = cpu->cycles + ns_to_cycles(delta_ns, cpu->cpu_freq_hz);
cpu_step_until(cpu, target_cycle);
}
deliver_rf_bytes(); // per-byte radio medium delivery
flush_uart();
process_actions(); // timed move/send/remove/add
check_test_engine(); // step matching, validators, fail-on
}Per-byte RF delivery is the key fidelity choice: instead of dropping a whole frame at the receiver in one go (which is wrong for byte-level firmware that polls SPI / FIFO between bytes), csim hands receivers one symbol at a time at the exact ns when it would arrive on a real link. This is what makes TSCH and tight CSMA timing actually work.
tools/run-cooja-tests.sh is the bridge between csim and the upstream Contiki-NG test infrastructure. It treats csim as a drop-in replacement for java -jar Cooja.jar --no-gui, runs the same .csc topologies, parses the same Cooja JS scripts via csc2json.py, and reports compatible PASS/FAIL output. This is how csim claims compatibility — every fix is validated against the upstream test suite, not a private regression set.
| Variable | Default | Effect |
|---|---|---|
MSPSIM_JIT_THRESHOLD |
100 |
Number of times a basic block must execute before the JIT compiles it |
MSPSIM_JIT_INBLOCK_CHECKS |
1 |
Emit interrupt / event-fire checks inside JIT blocks (needed for tight timer loops; set to 0 for max throughput on pure compute loops) |
CSIM_TRACE_TSCH_ACK |
unset | Verbose CC2420 ACK timing trace for TSCH debugging |
CSIM_TRACE_EVENT_SPIN |
unset | Trace multi-node event spin-loop iterations (useful when investigating wakeup hangs) |
CONTIKI_DIR |
../contiki-ng |
Where make cooja-tests and tools/build-test-firmware.sh look for the Contiki-NG checkout |
csim/
├── src/
│ ├── msp430/ MSP430 CPU, peripherals, JIT, CC2420
│ ├── arm/ ARM Cortex-M3 CPU, NVIC, CC2538 SoC peripherals
│ ├── native/ Native Cooja mote loader
│ ├── common/ Shared ELF loader, radio medium, event queue,
│ │ timeline, packet analyzer, JS test engine
│ └── ui/ WebSocket server + sim state serializer
├── include/
│ ├── msp430/ arm/ native/ common/ ui/ Public headers per subsystem
├── lib/
│ ├── cJSON.{c,h} JSON parser
│ ├── cbor.{c,h} CBOR encoder
│ └── quickjs/ Embedded JavaScript engine
├── test/
│ ├── test_main.c Test runner entry point
│ ├── test_correctness.c MSP430 instruction tests
│ ├── test_arm_correctness.c ARM instruction tests
│ ├── test_firmware.c MSP430 firmware integration
│ ├── test_arm_firmware.c ARM firmware integration
│ ├── test_benchmark.c Performance benchmarks
│ ├── test_mixed_multinode.c Multi-node sim driver (MSP430 + ARM + native)
│ └── test_timeline.c Timeline serializer unit tests
├── configs/ Example JSON simulation configs
├── docs/test-format.md Full JSON config / test scripting reference
├── firmware/ Pre-built Contiki-NG firmware
│ ├── sky/ Tmote Sky (MSP430F1611)
│ ├── z1/ Zolertia Z1 (MSP430F2617)
│ ├── cc2538dk/ CC2538DK (ARM Cortex-M3)
│ └── cooja/ Native Cooja mote .so libraries
├── tools/
│ ├── csc2json.py Cooja .csc → csim JSON converter
│ ├── run-cooja-tests.sh Run upstream Contiki-NG test suite
│ ├── build-test-firmware.sh Auto-build firmware needed by tests
│ └── ... Debug/timing utilities
├── ui/index.html Embedded UI page
├── csim.conf CONTIKI_DIR config (created by `make configure`)
├── Makefile
├── CLAUDE.md Detailed architecture notes
├── PLAN.md Active development log / roadmap
└── STATUS.txt Latest verified test counts
For deeper technical notes on each subsystem (CPU dispatch loop, JIT code generation, CC2420 state machine, ns-time event scheduling) see CLAUDE.md.
Last verified run on Linux x86-64 with make (release, JIT auto-detected):
| Suite | Result |
|---|---|
correctness (MSP430 instructions) |
68 / 68 PASS |
arm-correctness (ARM instructions) |
33 / 33 PASS |
timeline (event serializer) |
76 / 76 PASS |
firmware (cputest.sky, timertest.sky) |
2 / 2 PASS |
arm-firmware (hello-world.cc2538dk) |
PASS (boots Contiki-NG cleanly) |
bench |
runs cleanly, ~430 MIPS micro avg |
=== Cooja Test Suite (csim) ===
Total: 88
Passed: 88
Failed: 0
Skipped: 0
Errors: 0
That includes:
- All 27
07-simulation-base/*cases (RPL-Lite, TSCH, Orchestra variants, multicast, IPv6, stack guard, data structures), including the previously stubborn26-tsch-drift-z1(16 s). - All 12
09-ipv6/*ping permutations (CSMA / TSCH × LLA / ULA × with / without RPL). - All 9
13-ieee802154/*6top tests. - All 11
14-rpl-lite/*and 1815-rpl-classic/*cases, including the long-running 28-hour simulated DAG stability tests. - All 7
17-tun-rpl-br/*border-router tests with realtun0+tunslip6, including01-border-router-cooja(94 s).
The exact log is in cooja-tests-tun.log after running:
sudo -v
sudo ./tools/run-cooja-tests.sh --with-tun -v 2>&1 | tee cooja-tests-tun.logThese are real, currently reproducible bugs in the standalone CLI shortcuts — they do not affect the Cooja test wrapper path, the JSON-config flow, or any production use.
-
./build/test_runner multinode(no firmware) hangs. The default-firmware shortcut routes tofirmware/sky/nullnet-broadcast.skyand gets stuck after init. Workaround: use a JSON config or explicit firmware pair, e.g../build/test_runner mixed-multinode firmware/sky/udp-server.sky firmware/sky/udp-client.sky -t 60000. -
arm-multinodewith CC2538 firmware reports zero CPU progress. Thearm-multinodeand ARM-onlymixed-multinodeshortcuts exit "successfully" without actually running the ARM CPU aftermain. Workaround: drive ARM nodes through a JSON config (e.g.configs/rpl-udp-cc2538dk.json) or through the Cooja test wrapper, both of which work correctly. -
test_firmware.creportstimertest.skyas PASS even when the firmware itself printsFW: FAIL: count > 10 failed at timertest.c:166. The runner only matchesEXIT. Cosmetic, but worth tightening.
The active development state and any new regressions are tracked in PLAN.md. The most recently verified totals live in STATUS.txt.
3-clause BSD. See LICENSE.
Copyright © 2026 Joakim Eriksson, RISE Research Institutes of Sweden.
The bundled lib/quickjs/ is © Fabrice Bellard and Charlie Gordon, MIT license.
{ "title": "RPL-UDP 3-node linear chain", "timeout_ms": 60000, "seed": 42, "startup_delay_ms": 1000, "radiomedium": { "type": "udgm", "tx_range": 50.0, "interference_range": 100.0, "success_ratio_tx": 1.0, "success_ratio_rx": 1.0 }, "nodes": [ { "firmware": "firmware/sky/udp-server.sky", "id": 1, "x": 0.0, "y": 0.0 }, { "firmware": "firmware/sky/udp-client.sky", "id": 2, "x": 30.0, "y": 0.0 }, { "firmware": "firmware/sky/udp-client.sky", "id": 3, "x": 60.0, "y": 0.0 } ], "test": { "timeout_is_success": true, "fail_on": ["packet loss", "parent switch: -> (NULL"], "validators": [ { "pattern": "Received response", "min_count": 6 } ], "actions": [ { "at_ms": 30000, "type": "move", "node": 3, "x": 200.0, "y": 0.0 }, { "at_ms": 45000, "type": "move", "node": 3, "x": 60.0, "y": 0.0 } ] } }