A fast, multi-architecture emulator and network simulator for Contiki-NG, written in portable C. Cooja-NG (codename 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 — 89 / 89 including the TUN/border-router cases. See Test results.
Architecture and refactor direction are tracked in
docs/design/refactor-plan.md, including the
internal simulation kernel boundary, static plugin registry model, and staged
runner extraction plan.
┌──────────────────────────────────────────────┐
│ Cooja-NG test_runner │
│ │
firmware/*.sky / .z1 ────►│ MSP430 F149/F1611/F2617/F5437/CC430/FR5969 │
firmware/*.cc2538dk ──►│ ARM Cortex-M3 + CC2538 RF Core (on-chip) │──► UART, packets,
firmware/*.zoul-firefly►──│ ARM Cortex-M3 + CC2538 + CC1200 sub-GHz │ timeline,
firmware/*.nrf52840-dk ──►│ ARM Cortex-M4F + Nordic 802.15.4 radio │ web UI,
firmware/*.nrf54l15-dk ──►│ ARM Cortex-M33 + Nordic 802.15.4 + DPPI/GRTC│ COOJA.testlog
firmware/*.cooja ──►│ Native Cooja motes (dlopen) │
│ │
│ shared event queue • per-radio medium │
│ multi-channel • ns-precise time │
│ JS-driven assertions │
└──────────────────────────────────────────────┘
Designed for: headless CI for the Contiki-NG test suite, network research with hundreds of mixed-architecture nodes, and single-firmware debugging with deterministic seeds. Roughly an order of magnitude faster than Cooja + MSPSim, no JVM dependency, builds with make on Linux and macOS. Not a full Cooja replacement — no GTK GUI, no Java plugin ecosystem, no closed-source motes.
# Clone with the Contiki-NG sibling for firmware builds
git clone https://github.com/joakimeriksson/cooja-ng.git
git clone https://github.com/contiki-ng/contiki-ng.git
cd cooja-ng
# Build (auto-detects GNU Lightning for JIT)
make
# Unit tests (~5 seconds)
./build/test_runner correctness # 68 MSP430 instruction tests
./build/test_runner arm-correctness # 81 ARM Cortex-M3/M4F/M33 tests
./build/test_runner cc1200-mock-host # 73 CC1200 chip-driver tests
./build/test_runner radio-medium # 235 radio-medium routing tests
# 2-node 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
# The full Contiki-NG Cooja test suite (89 tests, ~10–15 min warm)
make configure CONTIKI_DIR=$(pwd)/../contiki-ng
make cooja-tests VERBOSE=1| Target | Notes |
|---|---|
make |
Default release build (-O3 -flto -march=native), JIT auto-detected via pkg-config |
make debug |
-O0 -g -DDEBUG, no LTO |
make pgo |
Two-stage profile-guided optimization, ~40 % faster on hot loops |
make clean |
Remove build/ |
Optional dependencies: GNU Lightning (MSP430 JIT, silent fallback if missing), QuickJS / cJSON / cbor (bundled under lib/), Contiki-NG (csim.conf, CONTIKI_DIR env, or ../contiki-ng). Runtime needs only libc/libm/libpthread, plus iproute2 + tunslip6 on Linux for the TUN tests.
make configure CONTIKI_DIR=/abs/path sets the persistent path used by make cooja-tests / tools/build-test-firmware.sh. Pre-built firmware ships under firmware/ for direct use without Contiki.
| Extension | Platform |
|---|---|
.sky |
MSP430F1611 (Tmote Sky) + CC2420 |
.esb |
MSP430F149 (ETH ESB) |
.z1 |
MSP430F2617 (Zolertia Z1) + CC2420 |
.wismote / .exp5438 |
MSP430F5437 |
.cc430 |
CC430F5137 eval board |
.msp430fr5969 |
MSP-EXP430FR5969 LaunchPad (FRAM, no radio) |
.cc2538dk |
ARM Cortex-M3 — TI CC2538 + on-chip 802.15.4 |
.zoul-firefly |
ARM Cortex-M3 — Zolertia Firefly (CC2538 + CC1200 sub-GHz) |
.nrf52840-dongle |
ARM Cortex-M4F — Nordic PCA10059 USB Dongle |
.nrf52840-dk |
ARM Cortex-M4F — Nordic PCA10056 DK |
.nrf54l15-dk |
ARM Cortex-M33 — Nordic PCA10156 DK |
.cooja |
Native Cooja shared library |
Per-board details live in devices/ and the SoC source files under src/{msp430,arm}/.
MSP430 emulator — full MSP430 + MSP430X instruction set (computed-goto interpreter, cycle-accurate operand-mode tables, MSPSim-compatible 6-cycle interrupt service); optional GNU Lightning JIT (~430 MIPS, ALU only); full CC2420 model (ns-based byte timing, CCITT-16 + bit reversal, auto-ACK, RXFIFO); Timer A/B + USART/USCI/eUSCI + GPIO + BCS/CS clock modules + 16-bit / 32-bit hardware multiplier.
ARM emulator — Thumb/Thumb-2 for Cortex-M3/M4F/M33 with IT blocks, ADR T2/T3 alignment, hi-reg ADD/MOV/CMP, ARMv8-M LDAEX/STLEX stubs; optional FPv4-SP-D16 VFP for M4F; NVIC with per-instruction pending check (peripherals raising IRQs mid-instruction don't sit behind a never-cleared PRIMASK); SysTick. SoC peripherals:
- TI CC2538 — UART, GPIO, GPTimer, Sleep Timer, IOC, SSI, on-chip RF Core (802.15.4 with FFSM filter and frame IRQs).
- TI CC1200 (off-SoC) — event-driven SPI peripheral, register-level fidelity, IOCFG-driven GPIO events, full software auto-ACK, 73-test mock-host suite.
- Nordic nRF52840 — CLOCK/HFCLK/LFCLK, RTC0–2, TIMER0–4, GPIO/GPIOTE, PPI, RNG, NVMC, FICR (per-node DEVICEID), UARTE EasyDMA, and a full 802.15.4 RADIO (PACKETPTR EasyDMA, SHORTS, INTENSET, BCMATCH, hardware-style auto-ACK).
- Nordic nRF54L15 — Cortex-M33 family with GRTC (1 MHz syscounter, RELATIVE_COMPARE + RELATIVE_SYSCOUNTER), DPPI (32-channel publish/subscribe), EGU (software-event bridge to NVIC), TIMER10/20–24, per-node FICR.DEVICEID, UARTE20 EasyDMA, and an 802.15.4 RADIO with deferred PHYEND so the driver's NVIC-disabling critical section exits before the IRQ fires. 2-node RPL-UDP exchanges request/response end-to-end.
Shared 802.15.4 helpers live in include/common/ieee_802154.h (PHY constants + CCITT-16 FCS used by all four radios) and include/arm/nrf_radio_common.h (one nrf_radio_emit_ieee802154_frame() for both nRF radios).
Native Cooja motes — .cooja shared libraries via dlopen, exposing the simInSize / simOutSize / simRtimerNextExpirationTime interface; mixable in a single network with MSP430 and ARM motes; dynamic radio_is_transmitting interval matching Cooja's ContikiRadio.doActionsAfterTick().
Multi-node — time-stepped event loop, all nodes on one ns-precise clock; per-node CPU frequencies handled across DCO calibration; per-radio multi-channel medium routes frames by (band, channel) so 2.4 GHz and sub-GHz networks coexist; per-byte RF delivery (one symbol at a time, not whole frames) which is what makes TSCH and tight CSMA timing work; packet analyzer for 802.15.4 / 6LoWPAN / IPv6 / RPL / UDP; timeline recorder exportable to JSON; single-threaded default with optional --threads N.
Test scripting — JSON config files (see docs/test-format.md), embedded QuickJS engine for Cooja-style JS scripts (TIMEOUT, WAIT_UNTIL, log.testOK, log.testFailed), tools/csc2json.py to convert .csc files, tools/run-cooja-tests.sh to drive the whole upstream test suite.
Web UI — --ui [port] starts a WebSocket server, single embedded HTML page at http://localhost:8080/: live node positions, packet animations, RPL parent edges, per-node UART, pause/step/speed. No Node.js, no build step.
./build/test_runner correctness # 68 MSP430 instruction tests
./build/test_runner arm-correctness # 81 ARM (Thumb-2 + M4 DSP + M4 VFP)
./build/test_runner timeline # 76 radio-event serializer tests
./build/test_runner cc1200-mock-host # 73 CC1200 chip-driver tests
./build/test_runner radio-medium # 235 multi-channel routing tests
./build/test_runner firmware # boots cputest.sky, timertest.sky
./build/test_runner arm-firmware # boots hello-world.cc2538dk + nRF
./build/test_runner bench # MIPS micro + firmware benchmarks
./build/test_runner all # everything 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 CC2538DK RPL-UDP
./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
# 100-node grid from a JSON config
./build/test_runner mixed-multinode configs/udgm-100node-grid-arm.jsonPlatform is auto-detected from the firmware extension (see table above). Options: -t ms (sim duration), -n nodes, -v (verbose), -q (quiet), --threads N, --ui [port].
The most thorough validation Cooja-NG has — runs every test in contiki-ng/tests/ headlessly using Cooja-NG instead of Cooja --no-gui.
make configure CONTIKI_DIR=/path/to/contiki-ng # one-time
# All 81 non-TUN tests (no sudo) — ~10–15 min warm
make cooja-tests
make cooja-tests VERBOSE=1 # per-test output
make cooja-tests PATTERN='14-rpl-lite*' # subset
# Force a rebuild against current Contiki sources
./tools/run-cooja-tests.sh --clean
# All 89 tests including TUN border-router cases.
# One-time: setcap so tunslip6 doesn't need sudo per run.
sudo setcap cap_net_admin+eip ../contiki-ng/tools/serial-io/tunslip6
./tools/run-cooja-tests.sh --with-tun -v 2>&1 | tee cooja-tests-tun.log
# Rebuild test firmware (only if Contiki sources changed)
make build-firmwarerun-cooja-tests.sh globs tests/*/*.csc in your Contiki checkout, converts each to JSON via csc2json.py, builds any missing firmware (--no-build to skip), runs test_runner mixed-multinode with the JS test script attached, and reports compatible PASS / FAIL / SKIP totals.
Full schema in docs/test-format.md. Short version:
Working examples in configs/: rpl-udp-{sky,cc2538dk,native}.json, mixed-sky-native.json, udgm-{3node,in-range,out-of-range,100node-grid}.json, test-4node-chain.json, test-js-rpl-udp.json, ui-rpl-udp-grid.json. Run any with ./build/test_runner mixed-multinode configs/<file>.json [-v].
Measured on Apple Silicon (PGO build) and Linux x86-64 (release). See bench for reproducible numbers.
| Platform / scenario | Speed |
|---|---|
| MSP430 micro-benchmarks (JIT, Apple Silicon, avg of 7) | ~430 MIPS |
MSP430 blink.sky / energest-demo.sky (JIT) |
~195 MIPS |
| MSP430 2-node nullnet (60 s sim) | ~500× real-time |
| MSP430 2-node RPL-UDP (60 s sim, Apple Silicon PGO) | ~1600× real-time |
| MSP430 2-node RPL-UDP (Linux x86-64 release) | ~250× real-time |
| CC2538DK 2-node RPL-UDP (interpreter) | ~4× real-time |
| nRF52840 2-node RPL-UDP (interpreter) | ~9× real-time |
| nRF54L15 2-node RPL-UDP (interpreter, deferred PHYEND) | ~0.1× real-time |
The nrf54l15 is slow because the GRTC is modeled at full 1 MHz and every TX defers PHYEND through the event queue — correctness-over-speed. Speed is the next optimisation once the RPL-UDP regression has stabilised.
Dual time domains. Every CPU tracks cycles (CPU-cycle count, the unit the interpreter advances) and sim_time_ns (ns-precise wall clock, the unit peripherals and inter-node coordination use). Reconciled at three sync points: before each event callback, at the end of *_step_until(), and inside *_set_frequency() on DCO calibration. This is what lets one node speed up after DCO cal while another is still booting, or an MSP430 and a CC2538 (different MHz) share the same medium.
Hot path.
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()
No ns-conversion or floating-point on the hot path. Events scheduled in cycle units; ns-based events are converted once at scheduling time and re-converted only on frequency change.
Multi-node loop.
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 — receivers see one symbol at a time at the exact ns it arrives on the link, instead of whole frames dropped in one go.
Per-radio multi-channel medium. Every radio chip (CC2420, CC2538 RF Core, CC1200, nRF52840/nRF54L15) registers its own slot. Frames route by (band, channel) so 2.4 GHz and sub-GHz networks coexist without cross-band leakage; TSCH channel hops correctly affect what each receiver hears. 235-test safety net — see docs/radio-medium.md and docs/porting-a-device.md §8 for the chip-driver event model.
Cooja test wrapper. tools/run-cooja-tests.sh is the bridge to upstream Contiki-NG's test infrastructure — treats Cooja-NG as a drop-in for java -jar Cooja.jar --no-gui, runs the same .csc topologies, parses the same JS scripts via csc2json.py, reports compatible PASS/FAIL. Every fix is validated against the upstream test suite, not a private regression set.
Deeper notes on each subsystem in CLAUDE.md and docs/architecture.md.
| Variable | Default | Effect |
|---|---|---|
MSPSIM_JIT_THRESHOLD |
100 |
Number of times a basic block must execute before JIT compiles it |
MSPSIM_JIT_INBLOCK_CHECKS |
1 |
Emit IRQ/event-fire checks inside JIT blocks (needed for tight timer loops; 0 for max throughput) |
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 |
NRF54L_RADIO_TRACE |
unset | nrf54l15 RADIO state/event/INTENSET trace; pair with NRF54L_RADIO_NODE=<cpu-tag> to filter |
CONTIKI_DIR |
../contiki-ng |
Where make cooja-tests and tools/build-test-firmware.sh look for the Contiki checkout |
| Suite | Result |
|---|---|
correctness (MSP430 instructions) |
68 / 68 PASS |
arm-correctness (Cortex-M3/M4F/M33, incl. DSP + VFP) |
81 / 81 PASS |
timeline (event serializer) |
76 / 76 PASS |
cc1200-mock-host (CC1200 chip driver) |
73 / 73 PASS |
radio-medium (per-radio multi-channel) |
235 / 235 PASS |
firmware (cputest.sky, timertest.sky) |
2 / 2 PASS |
arm-firmware (cc2538dk + nRF bring-up) |
PASS |
zoul-firefly-multinode RPL-UDP |
6 / 6 hello cycles in 60 s, ~9× real-time |
nrf52840-dongle-multinode RPL-UDP |
UDP request/response round-trip, ~9× real-time |
nrf54l15-dk-multinode RPL-UDP |
UDP request/response end-to-end (slow, ~0.1× real-time) |
Cooja test suite (89 tests, with --with-tun) |
89 / 89 PASS |
Cooja-suite coverage: all 27 07-simulation-base/* (RPL-Lite, TSCH, Orchestra, multicast, IPv6, stack guard, data structures) including the once-stubborn 26-tsch-drift-z1 (16 s); all 12 09-ipv6/*; all 9 13-ieee802154/*; all 14 14-rpl-lite/* and 19 15-rpl-classic/* (including 28-h simulated DAG stability tests); all 8 17-tun-rpl-br/* with real tun0 + tunslip6, including 10-native-nat64-cooja (214 s, UDP+TCP echo through the NAT64 gateway).
Real, currently reproducible quirks in the standalone CLI shortcuts — none affect the Cooja test wrapper, JSON-config flow, or 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.- nRF54L15 RPL-UDP convergence is slow (~30 s sim). End-to-end works (DIO → DAO → DAO-ACK → UDP request/response), but RPL takes longer to settle than nrf52840 because of the deferred-PHYEND model and 1 MHz GRTC fidelity. CSMA retransmits visible in the packet log; cosmetic.
test_firmware.creportstimertest.skyas PASS even when the firmware itself printsFW: FAIL: count > 10 failed at timertest.c:166. The runner only matchesEXIT. Cosmetic.- The deferred-PHYEND fix that unblocked nRF54L15 has not been ported to nrf52840. That platform works today, but the same critical-section-during-TX scenario would surface if a faster RPL config exercises it.
Active development state in PLAN.md; most recently verified totals in STATUS.txt.
3-clause BSD. See LICENSE. Copyright © 2026 Joakim Eriksson, RISE Research Institutes of Sweden.
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 } ] } }