Minimal multi-arch bare-metal kernel (x86 / RISC-V / ARM) — boots on QEMU and real Allwinner D1 & Zynq-7020 silicon
Boot minos on the real Zynq-7020 PS (Cortex-A9) without Vivado, an FSBL,
a bitstream, or DDR init — loaded straight into on-chip RAM over JTAG.
- link-zynq-ocm.ld: link the ARM build into OCM (0x0, 192 KiB), which is
alive from reset, so no FSBL/ps7_init and no DDR are needed.
- zynq/openocd-zynq.cfg: OpenOCD config for the on-board FT2232H. Its
EEPROM reports a generic 0403:6010 ("Adapt Device"), which Xilinx
hw_server refuses to auto-detect but OpenOCD drives directly; it sees
the full chain (PL boundary-scan 0x23727093 + ARM DAP 0x4ba00477).
- Makefile: 'zynq' builds the OCM ELF; 'run-zynq' halts A9 core0, loads
the image, and resumes.
The one non-obvious step: the on-board boot firmware leaves the core with
MMU + D-cache + I-cache enabled, which shadow the freshly loaded OCM and
make a plain resume run nothing. run-zynq clears SCTLR to 0x08C54C78 and
forces SVC mode before resuming. Halting (not resetting) keeps the
firmware's PS clock setup so the UART stays live.
Verified on hardware: banner once, then live keystroke echo over the
UART console (ttyUSB1 @115200). Same shared C core now boots on real
RISC-V (D1) and real ARM (Zynq) silicon, plus QEMU x86/riscv/arm.
|
||
|---|---|---|
| arch | ||
| common | ||
| include | ||
| zynq | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
minos — a minimal multi-arch kernel
One tiny OS core, three ISAs: x86 (i386), RISC-V (rv64), ARM
(Cortex-A9 / Zynq-7000). Built by a single clang in parallel so you can
compare how the same C behaves across architectures.
Milestone M1 (this state): each target boots, brings up its UART, sets its
interrupt/trap vector, and prints a banner from shared C (kmain).
Design
The only boundary is a small per-arch contract (include/arch.h):
arch_early_init() bring up the UART
arch_uart_putc(c) emit one byte
arch_set_trap_vector() install IDT / mtvec / VBAR
arch_name() banner string
arch_halt() hlt / wfi
Everything in common/ is portable C and calls only that contract. Each
arch/<name>/ supplies the irreducible assembly (boot.S, trap/vector table)
plus a C file implementing the contract and a linker script.
common/ main.c (kmain), console.c (putc/puts/puthex)
include/ arch.h (the contract), console.h
arch/x86/ boot.S (multiboot) arch.c (COM1 0x3F8, IDT) link.ld (1 MiB)
arch/riscv/ boot.S (_start) trap.S (mtvec) arch.c (UART 0x10000000)
arch/arm/ boot.S (_start) vectors.S (VBAR) arch.c (Cadence UART 0xE0000000)
Build & run
make -j # build all three in parallel
make run-x86 # boot under QEMU (Ctrl-A X to quit)
make run-riscv
make run-arm
make sizes # compare stripped binary size
make clean
The interesting comparison (M1)
| Aspect | x86 (i386) | RISC-V (rv64) | ARM (Cortex-A9) |
|---|---|---|---|
| Entry protocol | Multiboot1 header | _start @ 0x80000000 |
_start @ DDR |
| Console | port I/O outb 0x3F8 |
MMIO 16550 0x10000000 | MMIO Cadence 0xE0000000 |
| Trap vector | IDT via lidt |
mtvec CSR |
VBAR (CP15) |
| Multi-core gate | (single by default) | mhartid check |
MPIDR check |
| Halt | hlt |
wfi |
wfi |
| Idle byte size | ~890 | ~816 | ~800 |
Real hardware (next)
- RISC-V: the only change is the UART base —
0x10000000(QEMU virt) →0x02500000(Allwinner D1 / Lichee RV). Loadable viaxfel. - ARM/Zynq: same Cadence UART base as the real PS; boot via U-Boot/JTAG.
- x86: boot the multiboot ELF from GRUB.
Roadmap
- M2: real trap handlers + a timer tick per arch.
- M3: expose a tiny syscall-style interface (the "own interface").