QEMU k230 machine and standalone CPU0 payloads (tasks 1.3 and 3.2)
Evidence class: QEMU only, run 2026-10-12 on the host (QEMU 11.1.0). No board, no serial port, no reset or vector write. Nothing here shows what CPU0 does on silicon.
Task 1.3 answer
Yes, with -bios none and a loader device; no with -kernel.
| Invocation | Result |
|---|---|
qemu-system-riscv64 -M k230 -bios none -device loader,file=BIN,addr=0x7000000,cpu-num=0 | Runs. The loader device sets hart 0’s PC to 0x07000000; both heartbeat.bin and echo.bin execute in M-mode. |
-M k230 -bios none -kernel BIN | Refused: K230 direct boot requires OpenSBI firmware; omit -bios none or pass OpenSBI with -bios |
-M k230 -kernel BIN | Refused: K230 direct boot requires -dtb |
-M k230 with no image | Hart 0 starts at the modeled boot ROM, pc 0x91200000. |
What the model covers (from info cpus, info mtree, info registers):
- One hart:
CPU #0 model=thead-c908,mhartid 0. It is described as the small core, but there is no second CPU and no way to start one. - RAM is
0x0-0x7fffffff(2 GiB; the board has 1 GiB), so0x07000000is ordinary RAM.misareads0x800000000094112d; the board’s CPU0 read0x800000000094112f(bit 1, B, differs), so the model is not the same ISA string as the silicon. - The system-control blocks
cmu,rmu(0x91101000),boot(0x91102000),pwrandipcm(0x91104000, the TRM mailbox block) are listed asprio -1000i/o regions, i.e. placeholder windows. This project did not test them, and nothing implies they model CPU release, the vector register or the mailbox. The CPU0 release sequence cannot be rehearsed here. l2cache.ciall(0x0170000b) executes without a trap. QEMU has no cache model, so it is a no-op here; every cache-contract question stays a board question.- Timing is host time under TCG and says nothing about the board’s ~2.5 s uncached loop.
Harness
tools/small-core-qemu-probe.py starts QEMU with the loader device, then
reads and writes guest RAM through QEMU’s gdbstub (m/M packets, the VM is
halted around each access) and prints info cpus, info registers and xp
through QMP. It exits non-zero on failure.
Heartbeat (existing payload, 80 bytes, SHA-256 11d675ac...ba133)
$ python3 tools/small-core-qemu-probe.py heartbeat heartbeat.bin
# command: qemu-system-riscv64 -M k230 -bios none -display none ... -device loader,file=heartbeat.bin,addr=0x7000000,cpu-num=0
# qemu version / cpus:
11.1.0
* CPU #0: thread_id=1404405 model=thead-c908
priv M
pc 7000044
misa 800000000094112d
mhartid 0000000000000000
# xp /8wx 0x7002000 (initial)
07002000: 0x00000053 0x00000000 0x00000000 0x00000000
07002010: 0x0094112d 0x80000000 0x00000000 0x00000000
counter @0x7002000 = 123
counter @0x7002000 = 161
counter @0x7002000 = 200
mhartid word = 0x0, misa word = 0x800000000094112d
RESULT: PASS counters strictly increase
Echo payload (task 3.2)
echo.bin: 224 bytes, SHA-256
2ec68316e7d66a9380701c2011cd02c663ef945826afb30736c44b42017e46cc, CRC32
7492e46f (built by bash tools/test-small-core-coprocessor-echo.sh).
Slot layout: ../../../tools/small-core-echo.S.
$ python3 tools/small-core-qemu-probe.py echo echo.bin
# command: qemu-system-riscv64 -M k230 -bios none -display none ... -device loader,file=echo.bin,addr=0x7000000,cpu-num=0
# qemu version / cpus:
11.1.0
* CPU #0: thread_id=1404235 model=thead-c908
priv M
pc 7000064
misa 800000000094112d
mhartid 0000000000000000
# xp /8wx 0x7002000 (initial)
07002000: 0x00000000 0x00000000 0x00000000 0x00000000
07002010: 0x00000000 0x00000000 0x00000000 0x00000000
magic = 0x4f484345 (expect 0x4f484345), mhartid = 0, misa = 0x800000000094112d
req seq=1 value=0x11111111 -> rsp_seq=1 value=0x11111112 inv=0xeeeeeeee served=1 (0.021s poll) ok
req seq=2 value=0xdeadbeef -> rsp_seq=2 value=0xdeadbef0 inv=0x21524110 served=2 (0.020s poll) ok
req seq=3 value=0xffffffff -> rsp_seq=3 value=0x00000000 inv=0x00000000 served=3 (0.020s poll) ok
req seq=9 value=0x00000000 -> rsp_seq=9 value=0x00000001 inv=0xffffffff served=4 (0.020s poll) ok
req seq=4 value=0x12345678 -> rsp_seq=4 value=0x12345679 inv=0xedcba987 served=5 (0.021s poll) ok
re-sent seq=4 with new value: served stays 5 -> ignored (ok)
heartbeat words: [483, 620]
# xp: request line, then status line
07002000: 0x00000004 0x55555555 0x00000000 0x00000000
07002010: 0x00000000 0x00000000 0x00000000 0x00000000
07002040: 0x0000026c 0x00000000 0x00000000 0x00000000
07002050: 0x0094112d 0x80000000 0x4f484345 0x00000005
RESULT: PASS
The final xp lines show the request line (req_seq=4, req_value=0x55555555
left by the resend that was correctly ignored) and the status line
(heartbeat 0x28d, magic 0x4f484345, served count 5 at +0x5c).
What this proves and does not
Proves, in QEMU: the poll/serve state machine (a new req_seq is answered
once with value+1, ~value and a served count; rsp_seq is written last;
32-bit wrap works; a resent sequence number is ignored; seq & 7 selects
distinct response lines, seq 9 reusing seq 1’s line); the heartbeat advances
independently of requests; the slot layout decodes with xp.
Does not prove: anything about silicon, cache behavior, CPU1-visible
freshness, the 2.5 s-per-8M-loop uncached speed, release via boot_baremetal,
or the latency of a poll with l2cache.ciall on the real core. The
latency column above is the harness’s polling interval, not a CPU0 figure.
Board task 3.3 stays open: echo-runbook.md.