diff options
| author | 2026-10-12 08:46:23 +0330 | |
|---|---|---|
| committer | 2026-10-12 08:46:23 +0330 | |
| commit | ecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch) | |
| tree | 70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /src/baremetal | |
a
Diffstat (limited to 'src/baremetal')
| -rw-r--r-- | src/baremetal/link_aarch64.ld | 37 | ||||
| -rw-r--r-- | src/baremetal/link_riscv.ld | 39 | ||||
| -rw-r--r-- | src/baremetal/port.c | 339 | ||||
| -rwxr-xr-x | src/baremetal/run.sh | 104 | ||||
| -rw-r--r-- | src/baremetal/start_aarch64.S | 38 | ||||
| -rw-r--r-- | src/baremetal/start_riscv.S | 20 |
6 files changed, 577 insertions, 0 deletions
diff --git a/src/baremetal/link_aarch64.ld b/src/baremetal/link_aarch64.ld new file mode 100644 index 0000000..6740f21 --- /dev/null +++ b/src/baremetal/link_aarch64.ld @@ -0,0 +1,37 @@ +OUTPUT_ARCH(aarch64) +OUTPUT_FORMAT(elf64-littleaarch64) +ENTRY(_start) + +/* virt RAM starts at 0x40000000. The PL011 UART is at 0x09000000, outside this. */ +MEMORY +{ + RAM (rwx) : ORIGIN = 0x40000000, LENGTH = 48M +} + +SECTIONS +{ + .text : { + KEEP(*(.text.init)) + *(.text .text.*) + } > RAM + .rodata : { + *(.rodata .rodata.*) + } > RAM + .data : { + *(.data .data.*) + } > RAM + .bss : { + . = ALIGN(16); + _sbss = .; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(16); + _ebss = .; + } > RAM + .stack : { + . = ALIGN(16); + . += 64K; + _stack_top = .; + } > RAM + /DISCARD/ : { *(.eh_frame*) *(.comment) *(.note*) } +} diff --git a/src/baremetal/link_riscv.ld b/src/baremetal/link_riscv.ld new file mode 100644 index 0000000..cd470a4 --- /dev/null +++ b/src/baremetal/link_riscv.ld @@ -0,0 +1,39 @@ +OUTPUT_ARCH(riscv) +OUTPUT_FORMAT(elf64-littleriscv) +ENTRY(_start) + +MEMORY +{ + RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 48M +} + +SECTIONS +{ + .text : { + KEEP(*(.text.init)) + *(.text .text.*) + } > RAM + .rodata : { + *(.rodata .rodata.*) + *(.srodata .srodata.*) + } > RAM + .data : { + *(.data .data.*) + *(.sdata .sdata.*) + } > RAM + .bss : { + . = ALIGN(16); + _sbss = .; + *(.bss .bss.*) + *(.sbss .sbss.*) + *(COMMON) + . = ALIGN(16); + _ebss = .; + } > RAM + .stack : { + . = ALIGN(16); + . += 64K; + _stack_top = .; + } > RAM + /DISCARD/ : { *(.eh_frame*) *(.comment) *(.note*) *(.riscv.attributes) } +} diff --git a/src/baremetal/port.c b/src/baremetal/port.c new file mode 100644 index 0000000..636c1ea --- /dev/null +++ b/src/baremetal/port.c @@ -0,0 +1,339 @@ +/* UART port for the microcore. No libc. BARE_RISCV is the 16550 at + 0x10000000 on qemu's riscv virt. BARE_AARCH64 is the PL011 at 0x09000000 + on qemu's aarch64 virt. + + BARE_INPUT=0 (a program baked into the image, nothing typed): eof is + true and getc is -1. BARE_INPUT=1: bytes come from the UART. getc and + eof both wait until a byte is there. 0x04 (EOT) is end of input and is + not delivered. A gap is not the end, so a stream longer than the UART + FIFO still echoes until EOT. The compiler's backtick still ends a unit; + the byte after it is the program's first input, or EOT when the program + reads nothing. + + Syscalls, same numbers as the hosted ports: + 1 open 2 close 3 read 4 write a small RAM file store + 5 getenv BOARD is "riscv" or "aarch64" + 6 time the hardware counter, not unix seconds + 7 exit(status) + 8 random + 9 memory 0 micro_mem_words(), 1 HEAP_LO, 2 HEAP_HI, 3 micro_mem_words()*8 + 10 bopen 11 bread 12 bwrite 13 bgetenv + 10..13 use packed bytes. Anything else is -1. */ +#include "micro_port.h" +#include "bops.h" + +#ifndef BARE_INPUT +#define BARE_INPUT 0 +#endif + +#if defined(BARE_RISCV) +static volatile unsigned char *const uart = (volatile unsigned char *)0x10000000; +#define UART_LSR 5 +#define LSR_RX 0x01 +#define LSR_TX 0x20 + +static void uart_init(void) +{ + /* QEMU's 16550 already speaks 8N1. Rewriting LCR or FCR after + bytes have arrived drops the first one, and the source is + already in the FIFO by the time this runs. */ +} + +static int rx_ready(void) { return uart[UART_LSR] & LSR_RX; } +static void uart_write(unsigned char c) +{ + while ((uart[UART_LSR] & LSR_TX) == 0) ; + uart[0] = c; +} +static int uart_read(void) +{ + while (!rx_ready()) ; + return uart[0]; +} +static unsigned long timer(void) +{ + unsigned long t; + __asm__ volatile("rdtime %0" : "=r"(t)); + return t; +} +static void exit_status(int code) +{ + volatile unsigned *test = (volatile unsigned *)0x100000; + /* PASS exits 0. FAIL's high half is the status (16 bits). */ + if (code == 0) *test = 0x5555; + else *test = ((unsigned)(code & 0xffff) << 16) | 0x3333; + for (;;) ; +} +#elif defined(BARE_AARCH64) +static volatile unsigned *const uart = (volatile unsigned *)0x09000000; +#define UART_FR 6 /* 0x18 / 4 */ +#define FR_RXFE 0x10 +#define FR_TXFF 0x20 + +static void uart_init(void) { } +static int rx_ready(void) { return (uart[UART_FR] & FR_RXFE) == 0; } +static void uart_write(unsigned char c) +{ + while (uart[UART_FR] & FR_TXFF) ; + uart[0] = c; +} +static int uart_read(void) +{ + while (!rx_ready()) ; + return (int)uart[0]; +} +static unsigned long timer(void) +{ + unsigned long t; + __asm__ volatile("mrs %0, cntvct_el0" : "=r"(t)); + return t; +} +static void exit_status(int code) +{ + /* SYS_EXIT. x1 points at {ADP_Stopped_ApplicationExit, status}. + QEMU has to be started with -semihosting; PSCI SYSTEM_OFF cannot + carry a status. */ + unsigned long blk[2]; + register unsigned long x0 __asm__("x0") = 0x18; + register unsigned long x1 __asm__("x1"); + blk[0] = 0x20026ul; + blk[1] = (unsigned long)code; + x1 = (unsigned long)blk; + __asm__ volatile("hlt #0xf000" :: "r"(x0), "r"(x1) : "memory"); + for (;;) ; +} +#else +#error define BARE_RISCV or BARE_AARCH64 +#endif + +static int saw_end, have_peek, peekc; + +void micro_putc(int c) { uart_write((unsigned char)c); } + +void micro_msg(const char *s) +{ + while (*s) uart_write((unsigned char)*s++); +} + +/* the machine's memory on this board: the arena below, which is the board's RAM as + memmap.h sizes it. a board's RAM is a fixed amount of hardware, so this is the one + place the port has a size; it returns NULL past it. */ +/* physical backing is separate from the vm's configurable growth limit. */ +#ifndef MICRO_PORT_WORDS +#define MICRO_PORT_WORDS (HEAP_HI > PW ? HEAP_HI : PW) +#endif +static Cell arena[MICRO_PORT_WORDS]; + +Cell *micro_mem_alloc(UCell words) +{ + if (words > MICRO_PORT_WORDS) return 0; + return arena; +} + +static int inmem(Cell a) { return (UCell)a < (UCell)micro_mem_words(); } + +static int zcstr(Cell *mem, Cell addr, char *dst, int max) +{ + int i = 0, c; + if (max < 1) return -1; + if (!inmem(addr)) { dst[0] = 0; return -1; } + while (i < max - 1 && inmem(addr + i)) { + c = (int)mem[addr + i]; + if (!c) break; + dst[i++] = (char)(c & 255); + } + dst[i] = 0; + return i; +} + +static int streq(const char *a, const char *b) +{ + int i = 0; + while (a[i] && a[i] == b[i]) i++; + return a[i] == b[i]; +} + +#define NFILES 4 +#define FCAP 128 +#define NLEN 32 +static struct { + char name[NLEN]; + unsigned char data[FCAP]; + int len, pos; +} ramf[NFILES]; + +static int ram_slot(const char *name, int create) +{ + int i, free_i = -1, n; + for (i = 0; i < NFILES; i++) { + if (!ramf[i].name[0]) { if (free_i < 0) free_i = i; continue; } + if (streq(ramf[i].name, name)) return i; + } + if (!create || free_i < 0) return -1; + for (n = 0; name[n] && n < NLEN - 1; n++) ramf[free_i].name[n] = name[n]; + ramf[free_i].name[n] = 0; + ramf[free_i].len = 0; + ramf[free_i].pos = 0; + return free_i; +} + +static UCell rng; +static Cell rnd(void) +{ + if (!rng) rng = timer() ^ 0x9E3779B97F4A7C15ull; + if (!rng) rng = 0x9E3779B97F4A7C15ull; + rng ^= rng << 13; + rng ^= rng >> 7; + rng ^= rng << 17; + if ((Cell)rng == -1) rng ^= 0x100; + return (Cell)rng; +} + +static void board_name(char *dst) +{ +#if defined(BARE_RISCV) + dst[0] = 'r'; dst[1] = 'i'; dst[2] = 's'; dst[3] = 'c'; dst[4] = 'v'; dst[5] = 0; +#else + dst[0] = 'a'; dst[1] = 'a'; dst[2] = 'r'; dst[3] = 'c'; dst[4] = 'h'; + dst[5] = '6'; dst[6] = '4'; dst[7] = 0; +#endif +} + +Cell micro_syscall(Cell *mem, Cell op, Cell a, Cell b, Cell c) +{ + char path[NLEN]; + int fd, n, i; + switch ((int)op) { + case 1: + if (zcstr(mem, a, path, NLEN) < 0 || !path[0]) return -1; + fd = ram_slot(path, b == 1); + if (fd < 0) return -1; + if (b == 1) { ramf[fd].len = 0; ramf[fd].pos = 0; } + else ramf[fd].pos = 0; + return fd; + case 2: + if ((UCell)a >= NFILES || !ramf[a].name[0]) return -1; + ramf[a].pos = 0; + return 0; + case 3: + if ((UCell)a >= NFILES || !ramf[a].name[0] || c < 0 || !inmem(b)) return -1; + n = 0; + while (n < c && ramf[a].pos < ramf[a].len && inmem(b + n)) { + mem[b + n] = ramf[a].data[ramf[a].pos++]; + n++; + } + return n; + case 4: + if ((UCell)a >= NFILES || !ramf[a].name[0] || c < 0 || !inmem(b)) return -1; + n = 0; + while (n < c && ramf[a].len < FCAP && inmem(b + n)) { + ramf[a].data[ramf[a].len++] = (unsigned char)mem[b + n]; + n++; + } + ramf[a].pos = ramf[a].len; + return n; + case 5: { + char board[8], *e; + if (zcstr(mem, a, path, NLEN) < 0) return -1; + board_name(board); + e = streq(path, "BOARD") ? board : 0; + if (!e) return -1; + if (c < 1 || !inmem(b)) return -1; + i = 0; + while (e[i] && i < c - 1 && inmem(b + i)) { mem[b + i] = (unsigned char)e[i]; i++; } + if (inmem(b + i)) mem[b + i] = 0; + return i; + } + case 6: + return (Cell)timer(); + case 7: + exit_status((int)a); + return 0; + case 8: + return rnd(); + case 9: + if (a == 0) return micro_mem_words(); + if (a == 1) return HEAP_LO; + if (a == 2) return HEAP_HI; + if (a == 3) return (Cell)micro_mem_words() * 8; + return -1; + case 10: + if (bop_cstr(mem, a, path, NLEN, micro_mem_words()) < 0 || !path[0]) return -1; + fd = ram_slot(path, b == 1); + if (fd < 0) return -1; + if (b == 1) { ramf[fd].len = 0; ramf[fd].pos = 0; } + else ramf[fd].pos = 0; + return fd; + case 11: + if ((UCell)a >= NFILES || !ramf[a].name[0] || c < 0 || (c > 0 && !bop_ok(b, 1, micro_mem_words()))) return -1; + n = 0; + while (n < c && ramf[a].pos < ramf[a].len && bop_ok(b + n, 1, micro_mem_words())) { + bop_put1(mem, b + n, ramf[a].data[ramf[a].pos++]); + n++; + } + return n; + case 12: + if ((UCell)a >= NFILES || !ramf[a].name[0] || c < 0 || (c > 0 && !bop_ok(b, 1, micro_mem_words()))) return -1; + n = 0; + while (n < c && ramf[a].len < FCAP && bop_ok(b + n, 1, micro_mem_words())) { + ramf[a].data[ramf[a].len++] = (unsigned char)bop_load(mem, b + n, 1); + n++; + } + ramf[a].pos = ramf[a].len; + return n; + case 13: { + char board[8], *e; + if (bop_cstr(mem, a, path, NLEN, micro_mem_words()) < 0) return -1; + board_name(board); + e = streq(path, "BOARD") ? board : 0; + if (!e) return -1; + if (c < 1 || !bop_ok(b, 1, micro_mem_words())) return -1; + i = 0; + while (e[i] && i < c - 1 && bop_ok(b + i, 1, micro_mem_words())) { bop_put1(mem, b + i, (unsigned char)e[i]); i++; } + if (bop_ok(b + i, 1, micro_mem_words())) bop_put1(mem, b + i, 0); + return i; + } + case 21: /* argc: a board has no command line, so <eval> alone */ + return 1; + case 22: { /* argv: entry 0 is <eval>, as in zy(1) */ + static const char lead[] = "<eval>"; + if (a != 0) return -1; + if (!b) return 6; + if (c < 7 || !inmem(b) || !inmem(b + 6)) return -1; + for (i = 0; i < 6; i++) mem[b + i] = (unsigned char)lead[i]; + mem[b + 6] = 0; + return 6; + } + case 23: /* envc: a board has no environment */ + return 0; + default: + return -1; + } +} + +int micro_getc(void) +{ + int ch; + if (!BARE_INPUT || saw_end) return -1; + if (have_peek) { have_peek = 0; ch = peekc; } + else ch = uart_read(); + if (ch == 4) { saw_end = 1; return -1; } + return ch; +} + +int micro_eof(void) +{ + if (!BARE_INPUT || saw_end) return 1; + if (!have_peek) { + peekc = uart_read(); /* wait: an empty FIFO is not the end */ + have_peek = 1; + } + if (peekc == 4) { saw_end = 1; have_peek = 0; return 1; } + return 0; +} + +void bare_main(void) +{ + uart_init(); + exit_status(micro_run() != 0); +} diff --git a/src/baremetal/run.sh b/src/baremetal/run.sh new file mode 100755 index 0000000..d6ac333 --- /dev/null +++ b/src/baremetal/run.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Build a freestanding microcore and boot it on qemu-system-*. +# src/baremetal/run.sh ready riscv|aarch64 +# src/baremetal/run.sh riscv|aarch64 run examples/hello.zc +# src/baremetal/run.sh riscv|aarch64 io examples/echo.zc +# src/baremetal/run.sh riscv|aarch64 compile examples/hello.zc +# run: the program is compiled into the image. nothing is read. +# io: the same image, but stdin is the program's input and 0x04 ends it. +# compile: the default 1,048,576-word genome. the source, a backtick, and +# 0x04 are written to the UART. the backtick ends the unit; the +# 0x04 is what eof() waits for after that. +# Prints the program's UART output and nothing else. Exits 0 on a clean +# power-off. A missing toolchain exits 0 from `ready` and 1 otherwise. +set -euo pipefail +cd "$(dirname "$0")/../.." + +ready() { + case "$1" in + riscv) command -v riscv64-linux-gnu-gcc >/dev/null && command -v qemu-system-riscv64 >/dev/null ;; + aarch64) command -v aarch64-linux-gnu-gcc >/dev/null && command -v qemu-system-aarch64 >/dev/null ;; + *) return 1 ;; + esac +} + +if [ "${1:-}" = ready ]; then + ready "${2:-}" + exit $? +fi +if [ $# -ne 3 ]; then + echo "usage: src/baremetal/run.sh riscv|aarch64 run|io|compile FILE" >&2 + exit 2 +fi +target=$1 mode=$2 src=$3 +ready "$target" || { echo "baremetal: missing toolchain for $target" >&2; exit 1; } +[ -f "$src" ] || { echo "baremetal: no $src" >&2; exit 1; } + +case "$target" in +riscv) + cc=riscv64-linux-gnu-gcc + qemu=qemu-system-riscv64 + cflags="-march=rv64gc -mabi=lp64d -mcmodel=medany -mno-relax" + asm=src/baremetal/start_riscv.S + ld=src/baremetal/link_riscv.ld + def=-DBARE_RISCV + qargs=(-machine virt -bios none -cpu rv64 -m 64) + ;; +aarch64) + cc=aarch64-linux-gnu-gcc + qemu=qemu-system-aarch64 + cflags="-march=armv8-a" + asm=src/baremetal/start_aarch64.S + ld=src/baremetal/link_aarch64.ld + def=-DBARE_AARCH64 + qargs=(-machine virt -cpu cortex-a57 -m 64 -semihosting-config enable=on,target=native) + ;; +*) echo "baremetal: unknown target $target" >&2; exit 2 ;; +esac + +D=$(mktemp -d) +trap 'rm -rf "$D"' EXIT +common="$cflags -ffreestanding -fno-pic -fno-pie -fno-builtin -fno-unwind-tables -fno-asynchronous-unwind-tables -O2 -Wall -Wextra" + +if [ "$mode" = run ] || [ "$mode" = io ]; then + # the map is sized to the program, so a bigger runtime or program just gets a bigger board + gcc -O2 -w src/tools/genmap.c -o "$D/genmap" + GENMAP="$D/genmap" bash src/tools/automap.sh run "$D/img" src/runtime/run.zc "$src" >"$D/genmap.log" 2>&1 || { cat "$D/genmap.log" >&2; exit 1; } + cp src/micro/micro.c src/micro/micro_port.h src/core/fops.h src/core/bops.h src/core/stack.h "$D/img/" + "$cc" $common $def -I"$D/img" -c "$D/img/micro.c" -o "$D/micro.o" + if [ "$mode" = io ]; then + "$cc" $common $def -DBARE_INPUT=1 -I"$D/img" -c src/baremetal/port.c -o "$D/port.o" + timeout=60 + else + "$cc" $common $def -DBARE_INPUT=0 -I"$D/img" -c src/baremetal/port.c -o "$D/port.o" + timeout=120 + fi +elif [ "$mode" = compile ]; then + cp src/micro/micro.c src/micro/micro_port.h src/core/memmap.h src/core/genome.h src/core/fops.h src/core/bops.h src/core/stack.h "$D/" + "$cc" $common $def -I"$D" -c "$D/micro.c" -o "$D/micro.o" + "$cc" $common $def -DBARE_INPUT=1 -I"$D" -c src/baremetal/port.c -o "$D/port.o" + timeout=90 +else + echo "baremetal: mode is run, io or compile" >&2 + exit 2 +fi +"$cc" $common $def -c "$asm" -o "$D/start.o" +"$cc" $common $def -nostdlib -static -Wl,--build-id=none -Wl,--no-warn-rwx-segments -T "$ld" -o "$D/bare.elf" "$D/start.o" "$D/port.o" "$D/micro.o" +if [ "${BARE_SIZE:-}" = 1 ]; then + size=$(wc -c < "$D/bare.elf") + text=$(size "$D/bare.elf" | awk 'NR==2{print $1+$2}') + bss=$(size "$D/bare.elf" | awk 'NR==2{print $3}') + echo "elf $size bytes, text+data $text, bss $bss" >&2 +fi + +# -serial stdio is the UART. The monitor stays off so it cannot eat bytes. +runq() { + timeout "$timeout" "$qemu" "${qargs[@]}" -nographic -monitor none -serial stdio -kernel "$D/bare.elf" +} +if [ "$mode" = compile ]; then + (cat "$src"; printf '`\004') | runq +elif [ "$mode" = io ]; then + (cat; printf '\004') | runq +else + runq </dev/null +fi diff --git a/src/baremetal/start_aarch64.S b/src/baremetal/start_aarch64.S new file mode 100644 index 0000000..36b2752 --- /dev/null +++ b/src/baremetal/start_aarch64.S @@ -0,0 +1,38 @@ +/* AArch64 virt. Drop EL2 to EL1 if we came up there, enable SIMD + (the compiler copies the genome with q registers), then stack, bss, C. */ +.section .text.init +.global _start +_start: + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #2 + b.ne 1f + mov x0, #(1 << 31) /* EL1 is AArch64 */ + msr HCR_EL2, x0 + mrs x0, CPTR_EL2 + bic x0, x0, #(1 << 10) /* do not trap SIMD to EL2 */ + msr CPTR_EL2, x0 + mov x0, #0x3c5 /* EL1h, interrupts masked */ + msr SPSR_EL2, x0 + adr x0, 1f + msr ELR_EL2, x0 + eret +1: + mrs x0, CPACR_EL1 + orr x0, x0, #(3 << 20) /* FPEN: SIMD does not trap */ + msr CPACR_EL1, x0 + isb + ldr x0, =_stack_top + mov sp, x0 + ldr x0, =_sbss + ldr x1, =_ebss +2: + cmp x0, x1 + b.hs 3f + str xzr, [x0], #8 + b 2b +3: + bl bare_main +4: + wfe + b 4b diff --git a/src/baremetal/start_riscv.S b/src/baremetal/start_riscv.S new file mode 100644 index 0000000..527f109 --- /dev/null +++ b/src/baremetal/start_riscv.S @@ -0,0 +1,20 @@ +/* RISC-V virt, machine mode, no firmware. Stack, clear bss, enter C. */ +.section .text.init +.global _start +_start: + /* FS = Dirty, so a compiler that emits float ops does not trap. */ + li t0, 0x6000 + csrs mstatus, t0 + la sp, _stack_top + la t0, _sbss + la t1, _ebss +1: + bgeu t0, t1, 2f + sd zero, 0(t0) + addi t0, t0, 8 + j 1b +2: + call bare_main +3: + wfi + j 3b |
