Search › A-Z › M › Milk-V Duo (RISC-V Linux)
Milk-V Duo (RISC-V Linux)
This is a RISC-V-Linux-in-the-browser showpiece — a sibling to the ARM "Pi-class" Linux exhibit, but on the open RISC-V ISA. A real RISC-V CPU boots a real Buildroot Linux to an interactive busybox shell over a serial console, entirely in the browser, via the vendored TinyEMU emulator (Fabrice Bellard's JSLinux engine, compiled to WebAssembly). There is no network at runtime — the boot loader, kernel and root filesystem are preloaded — and JavaScript owns the run loop, so the shared debugger can pause the core, single-step it, read and write x0-x31 / PC / CSRs, read physical RAM, disassemble RV32IMAC at the PC, and set breakpoints and write-watchpoints.
Honest scope. The physical Milk-V Duo is built around the Sophgo SG2002 (a.k.a. CV1800B), whose main application core is a 64-bit T-Head C906 (RV64GC) that boots Linux, plus a second small RISC-V/8051 co-processor, on-chip GPIO (the board's on-board LED), a camera/ISP and a small VPU. This exhibit stands in for that SoC with a generic 32-bit TinyEMU RISC-V machine (RV32IMAC) — the same open RISC-V ISA and the same boot-Linux-to-a-shell experience, but not true SG2002 silicon: no SG2002-specific peripherals, no GPIO LED, and a text serial console only (no display).
Runs on: Web browser
Milk-V Duo (RISC-V Linux) Online Emulator
Play Milk-V Duo (RISC-V Linux) using WebAssembly directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| RISC-V Linux (busybox shell) | Milk-V Duo (RISC-V Linux) | Milk-V Duo | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The runtime is TinyEMU (Fabrice Bellard's riscvemu, the engine behind JSLinux) compiled to WebAssembly with emscripten. Rather than Bellard's networked JS front-end, we built a small self-contained front-end (emu.c) that loads the boot loader, kernel and root filesystem from a preloaded in-memory filesystem — so nothing is fetched from a CDN and the page is file://-safe. We self-host riscvemu.js (the glue), riscvemu.wasm (the CPU + machine) and riscvemu.data (bbl + Linux kernel + ext2 rootfs, ~7 MB).
var mod = await createRiscvEmu({
locateFile: p => "/emulator/milkv-duo/src/" + p, // find the .wasm / .data
onConsole: bytes => term.write(bytes) // VirtIO console output -> VT100 terminal
});
mod.ccall("emu_start", "number", [], []); // build the machine, load Linux
We own the run-loop. The wasm build does not use emscripten's main loop; instead it exports slice functions and JavaScript drives them, so the debugger can pause / step / breakpoint the CPU:
| Export | What it does |
|---|---|
emu_run_frame(cycles) | Feed queued keystrokes, run a slice of instructions, honour the RTC sleep. The normal "running" path. |
emu_step(n) | Run exactly n RISC-V instructions (true single step — see the debugger notes). |
emu_service() | Service devices once (console + timer) so a WFI-idle guest can wake via a real interrupt between single steps. |
emu_console_input(ch) | Queue one byte from the keyboard into the VirtIO console. |
The screen is a terminal, not a framebuffer. A RISC-V Linux console is a byte stream, so onConsole feeds a compact VT100 parser (terminal.js) that keeps an 80×30 character grid and paints it onto a <canvas>. The keyboard (physical keydown and the on-screen keys) writes ASCII bytes straight into the console FIFO.
Debugger integration
This shows a full-system WebAssembly emulator getting the same debugger as the pure-JS cores. TinyEMU keeps its whole RISCVCPUState (PC, reg[32], the CSRs) inside the wasm linear memory, and the machine keeps guest RAM there too. The state is not exposed by default, so we extended the core: we added EMSCRIPTEN_KEEPALIVE accessors to riscv_cpu.c and riscv_machine.c and published ccall wrappers on window.EMU_BOOT. None of them run per-cycle — only on a debugger refresh or a step.
| What the debugger needs | The hook we added to the core |
|---|---|
| PC + x0-x31 (read & write) | dbg_cpu_get_pc/reg, dbg_cpu_set_pc/reg — read/write the live RISCVCPUState (x0 stays hard-wired to 0). |
| CSRs | dbg_cpu_get_csr — mstatus, mtvec, mepc, mcause, satp, sepc … |
| Side-effect-free memory | dbg_machine_read_phys/write_phys — go straight to the RAM backing store via phys_mem_get_ram_ptr, so no MMIO device is touched. |
| Disasm "follow PC" under paging | dbg_cpu_va2pa — a read-only Sv32 page-table walk that maps the (virtual) PC to a RAM offset so the disassembly highlights the executing instruction. |
True single-instruction step. TinyEMU's interpreter only tests its cycle budget at basic-block boundaries (a branch or a page edge), so asking it to run "1 cycle" actually runs a whole block. We added a gated per-instruction budget check to the interpreter loop (a global riscv_dbg_single_step that is off during normal runs, so full-speed boot is unaffected); with it set, emu_step(1) advances the PC by exactly one instruction. Between steps the debugger calls emu_service() so a guest sitting in wfi (the idle shell) still wakes on a real timer/console interrupt and single-stepping stays exact.
Breakpoints & watchpoints. Because we own the loop, breakpoints are exact: when any breakpoint is set the loop single-steps and compares the PC's RAM offset against the set. Write watchpoints are implemented by snapshotting the watched bytes and re-reading them after each step, pausing when one changes. The memory window is a 1 MB view of physical RAM at the 0x8000_0000 boot address, disassembled with the shared riscv decoder.
Architecture
RISC-V is the open, royalty-free load/store RISC ISA (Berkeley, 2010) — the architecture the Milk-V Duo's Sophgo SG2002 / CV1800B is built on. This exhibit runs a 32-bit RV32IMAC core — the base integer set (I) plus integer multiply/divide (M), atomics (A) and the compressed 16-bit encodings (C) — booting a real Linux:
riscvemu.wasm— TinyEMU's RISC-V CPU interpreter and the surrounding machine: RAM at0x8000_0000, the CLINT (timer/soft-IRQ), a PLIC, an HTIF console and VirtIO (console + block).bbl(the Berkeley Boot Loader / riscv-pk) sits at the reset vector, sets up machine mode and hands off to…- the Linux kernel (GPLv2), which enables Sv32 paging (hence the virtual PCs like
0xc00…) and mounts… - an ext2 root filesystem built with Buildroot, giving a busybox userland and the
~ #shell.
What this is not: the real Milk-V Duo's SG2002 (CV1800B) has a 64-bit T-Head C906 (RV64GC) application core, a second small RISC-V/8051 co-processor, on-chip GPIO (the board's on-board LED), a camera/ISP and a small VPU. This exhibit is a generic 32-bit TinyEMU RISC-V machine standing in for that silicon — the same open RISC-V ISA and the same boot-Linux-to-a-shell experience, but no SG2002-specific peripherals, no GPIO LED, and a serial console only (no display adapter). It is a RISC-V sibling to the site's ARM "Pi-class" Linux exhibit.