From 775a02cf3f06fa14acf146ea2e44765d168ba070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A8=D1=83=D1=80=D1=83=D0=BF=D0=BE=D0=B2=20=D0=98=D0=BB?= =?UTF-8?q?=D1=8C=D1=8F=20=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Fri, 25 Sep 2026 21:13:59 +0300 Subject: [PATCH] Add D1/Lichee RV hardware boot and UART input Bring up minos on the Allwinner D1 (Lichee RV) over FEL/xfel and add bidirectional UART so the console can read keystrokes, not just print. D1 / Lichee RV boot: - riscv arch.c: select UART0 base by -DBOARD_D1 (0x02500000 vs QEMU 0x10000000) and add a reg-shift abstraction (uart_rd/uart_wr): the D1's Synopsys DW-8250 uses 32-bit registers at 4-byte spacing, which is what made the first hardware boot silent. - d1_uart0_init(): gate/deassert UART0 clock, mux PB8/PB9 to UART0, set 115200 8N1, clear FIFOs, force non-loopback, drain stale RX. - link-d1.ld: link for DRAM at 0x40000000 (xfel exec target, M-mode). - Makefile: 'lichee' builds the flat binary; 'run-lichee' loads it via xfel (ddr d1 / write / exec). - arch_name() reports the concrete board. UART input (all archs): - arch contract gains arch_uart_getc() and arch_uart_rx_ready(), implemented for riscv, x86 (COM1) and arm (Cadence UART). - console gains getc() and drain_rx(). - kmain() runs an interactive echo loop (seed of the M3 console). Verified: echo works under QEMU on x86/riscv/arm, and on real D1 hardware (banner once, then live keystroke echo over UART0 @115200). --- Makefile | 33 +++++++++++ arch/arm/arch.c | 15 ++++- arch/riscv/arch.c | 126 +++++++++++++++++++++++++++++++++++++++--- arch/riscv/link-d1.ld | 31 +++++++++++ arch/x86/arch.c | 12 ++++ common/console.c | 15 +++++ common/main.c | 18 +++++- include/arch.h | 8 +++ include/console.h | 2 + 9 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 arch/riscv/link-d1.ld diff --git a/Makefile b/Makefile index 4635bd5..41f1385 100644 --- a/Makefile +++ b/Makefile @@ -73,5 +73,38 @@ sizes: all stat -c '%s' /tmp/_sz.bin; \ done; rm -f /tmp/_sz.bin +# --- real hardware: Allwinner D1 / Lichee RV via xfel ------------------------ +# Same RISC-V sources, rebuilt with -DBOARD_D1 (UART 0x02500000) and linked +# for DRAM 0x40000000, emitted as a flat binary xfel can load. +XFEL ?= xfel +D1_TARGET := $(riscv_TARGET) -DBOARD_D1 +D1_OBJS := $(patsubst %,build/lichee/%.o,$(basename $(notdir $(riscv_SRC)))) + +build/lichee/%.o: arch/riscv/%.S | build/lichee + $(CC) $(D1_TARGET) $(CFLAGS) -c $< -o $@ +build/lichee/%.o: arch/riscv/%.c | build/lichee + $(CC) $(D1_TARGET) $(CFLAGS) -c $< -o $@ +build/lichee/%.o: common/%.c | build/lichee + $(CC) $(D1_TARGET) $(CFLAGS) -c $< -o $@ +build/lichee: + mkdir -p $@ + +build/lichee/minos.elf: $(D1_OBJS) arch/riscv/link-d1.ld + $(LD) -m $(riscv_LDEMU) -T arch/riscv/link-d1.ld -nostdlib $(D1_OBJS) -o $@ +build/lichee/minos.bin: build/lichee/minos.elf + $(OBJCOPY) -O binary $< $@ + @echo " [lichee] flat binary -> $@ ($$(stat -c%s $@) bytes)" + +.PHONY: lichee run-lichee +lichee: build/lichee/minos.bin ## build the D1 flat binary + +# Load & run on the board (must be in FEL mode; needs sudo for USB). +run-lichee: build/lichee/minos.bin + @echo "== loading minos onto D1 via xfel ==" + $(XFEL) ddr d1 + $(XFEL) write 0x40000000 build/lichee/minos.bin + $(XFEL) exec 0x40000000 + @echo "== running. Watch /dev/ttyUSB0 @115200 for output. ==" + clean: rm -rf build diff --git a/arch/arm/arch.c b/arch/arm/arch.c index 37217de..08ab503 100644 --- a/arch/arm/arch.c +++ b/arch/arm/arch.c @@ -10,7 +10,8 @@ #define UART_MR 0x04 /* mode register */ #define UART_SR 0x2C /* channel status register */ #define UART_FIFO 0x30 /* tx/rx FIFO */ -#define UART_SR_TXFULL (1u << 4) +#define UART_SR_TXFULL (1u << 4) +#define UART_SR_RXEMPTY (1u << 1) /* Control register bits */ #define CR_RXRES (1u << 0) /* RX logic reset */ @@ -39,6 +40,18 @@ void arch_uart_putc(char c) *reg(UART_FIFO) = (unsigned int)(unsigned char)c; } +char arch_uart_getc(void) +{ + while (*reg(UART_SR) & UART_SR_RXEMPTY) /* wait for a received byte */ + ; + return (char)(*reg(UART_FIFO) & 0xff); +} + +int arch_uart_rx_ready(void) +{ + return (*reg(UART_SR) & UART_SR_RXEMPTY) ? 0 : 1; +} + /* M1: point the vector base (VBAR) at our table (defined in vectors.S). */ extern void vector_table(void); void arch_set_trap_vector(void) diff --git a/arch/riscv/arch.c b/arch/riscv/arch.c index cc8b1bd..e9f550b 100644 --- a/arch/riscv/arch.c +++ b/arch/riscv/arch.c @@ -6,24 +6,130 @@ #include "arch.h" #include "console.h" +/* UART base differs by platform. Both are 16550-compatible, so the same + * driver works; only the MMIO base changes. + * QEMU 'virt' : 0x10000000 + * Allwinner D1 (Lichee RV) UART0 : 0x02500000 + * Select with -DBOARD_D1 at build time (see 'make lichee'). */ +#ifdef BOARD_D1 +#define UART0_BASE 0x02500000UL +#else #define UART0_BASE 0x10000000UL -#define UART_THR 0x00 /* transmit holding register */ -#define UART_LSR 0x05 /* line status register */ +#endif +/* Register numbers (16550). The BYTE offset depends on the platform's + * register spacing: + * QEMU 'virt' : 8-bit registers, 1-byte spacing (reg-shift 0) + * Allwinner D1: Synopsys DW 8250, 32-bit registers, 4-byte spacing + * (reg-shift 2) — this is the difference that made the + * first hardware boot silent. */ +#define UART_RBR 0 /* receive buffer register (read) */ +#define UART_THR 0 /* transmit holding register (write) */ +#define UART_LSR 5 /* line status register */ +#define UART_LSR_DR 0x01 /* data ready (RX byte available) */ #define UART_LSR_THRE 0x20 /* THR empty */ -static volatile unsigned char *const uart = - (volatile unsigned char *)UART0_BASE; +#ifdef BOARD_D1 +#define UART_SHIFT 2 +static inline unsigned int uart_rd(int reg) +{ + return *(volatile unsigned int *)(UART0_BASE + ((unsigned long)reg << UART_SHIFT)); +} +static inline void uart_wr(int reg, unsigned int v) +{ + *(volatile unsigned int *)(UART0_BASE + ((unsigned long)reg << UART_SHIFT)) = v; +} +#else +static inline unsigned int uart_rd(int reg) +{ + return *(volatile unsigned char *)(UART0_BASE + reg); +} +static inline void uart_wr(int reg, unsigned int v) +{ + *(volatile unsigned char *)(UART0_BASE + reg) = (unsigned char)v; +} +#endif + +#ifdef BOARD_D1 +/* Full UART0 bring-up on the Allwinner D1 (sun20i). The FEL boot ROM does + * NOT guarantee UART0 is clocked/muxed/baud-set, so we do it ourselves. + * Register map from the D1 user manual. */ +#define CCU_BASE 0x02001000UL +#define CCU_UART_BGR (CCU_BASE + 0x090C) /* UART bus gating & reset */ + +#define PIO_BASE 0x02000000UL +#define PB_CFG1 (PIO_BASE + 0x0034) /* port B, pins 8..15, 4 bits each */ + +#define w32(a, v) (*(volatile unsigned int *)(a) = (unsigned int)(v)) +#define r32(a) (*(volatile unsigned int *)(a)) + +/* DW-8250 register numbers (reg-shift 2 applied by uart_wr/uart_rd). */ +#define UART_DLL 0 /* divisor low (DLAB=1) */ +#define UART_DLH 1 /* divisor high (DLAB=1) */ +#define UART_FCR 2 /* FIFO control (write) */ +#define UART_LCR 3 /* line control */ +#define UART_MCR 4 /* modem control */ +#define UART_MCR_LOOP 0x10 /* internal loopback (TX->RX) */ + +static void d1_uart0_init(void) +{ + unsigned int v; + + /* 1. Enable UART0: bus gate (bit0) + deassert reset (bit16). */ + v = r32(CCU_UART_BGR); + v |= (1u << 16) | (1u << 0); + w32(CCU_UART_BGR, v); + + /* 2. Mux PB8=UART0_TX, PB9=UART0_RX to function 6. */ + v = r32(PB_CFG1); + v &= ~0xFFu; /* clear PB8 [3:0] and PB9 [7:4] */ + v |= (6u << 0) | (6u << 4); + w32(PB_CFG1, v); + + /* 3. Program 115200 8N1. UART src = 24 MHz OSC -> + * divisor = 24000000 / (16 * 115200) ~= 13. */ + uart_wr(UART_LCR, 0x80); /* DLAB = 1 */ + uart_wr(UART_DLL, 13); + uart_wr(UART_DLH, 0); + uart_wr(UART_LCR, 0x03); /* 8N1, DLAB = 0 */ + uart_wr(UART_FCR, 0x07); /* enable + clear RX/TX FIFOs */ + + /* Defensive: force normal (non-loopback) operation in case the boot + * ROM left the modem-control internal-loopback bit set, which would + * feed TX straight back into RX. */ + uart_wr(UART_MCR, 0x00); + + /* Drain any stale bytes sitting in the RX FIFO so the first getc() + * waits for a real keypress instead of returning garbage. */ + while (uart_rd(UART_LSR) & UART_LSR_DR) + (void)uart_rd(UART_RBR); +} +#endif void arch_early_init(void) { - /* QEMU's UART is usable from reset; nothing to do. */ +#ifdef BOARD_D1 + d1_uart0_init(); +#endif + /* QEMU's UART is usable from reset; nothing to do there. */ } void arch_uart_putc(char c) { - while ((uart[UART_LSR] & UART_LSR_THRE) == 0) + while ((uart_rd(UART_LSR) & UART_LSR_THRE) == 0) ; - uart[UART_THR] = (unsigned char)c; + uart_wr(UART_THR, (unsigned char)c); +} + +char arch_uart_getc(void) +{ + while ((uart_rd(UART_LSR) & UART_LSR_DR) == 0) + ; + return (char)(uart_rd(UART_RBR) & 0xff); +} + +int arch_uart_rx_ready(void) +{ + return (uart_rd(UART_LSR) & UART_LSR_DR) ? 1 : 0; } /* M1: install a minimal machine trap vector (defined in trap.S). */ @@ -37,7 +143,11 @@ void arch_set_trap_vector(void) const char *arch_name(void) { - return "riscv64 (rv64imac)"; +#ifdef BOARD_D1 + return "riscv64 (Allwinner D1 / Lichee RV)"; +#else + return "riscv64 (rv64imac, QEMU virt)"; +#endif } const char *arch_cpu_mode(void) diff --git a/arch/riscv/link-d1.ld b/arch/riscv/link-d1.ld new file mode 100644 index 0000000..d180c92 --- /dev/null +++ b/arch/riscv/link-d1.ld @@ -0,0 +1,31 @@ +/* link-d1.ld (RISC-V, Allwinner D1 / Lichee RV) — bare-metal via xfel. + * + * The D1 boot ROM (FEL) leaves us free to load into DRAM. xfel initialises + * DRAM ('xfel ddr d1') then writes/execs at 0x40000000 (start of the 512MB+ + * DDR). 'xfel exec' jumps here still in M-mode, which is what minos expects. + * (QEMU 'virt' uses 0x80000000 instead — see link.ld.) */ +OUTPUT_ARCH(riscv) +ENTRY(_start) + +SECTIONS +{ + . = 0x40000000; /* DRAM base on D1 */ + + .text : { + KEEP(*(.text.boot)) /* _start must be first */ + *(.text .text.*) + } + + .rodata : { *(.rodata .rodata.*) } + .data : { *(.data .data.*) } + + . = ALIGN(8); + __bss_start = .; + .bss : { *(.bss .bss.*) *(COMMON) } + . = ALIGN(8); + __bss_end = .; + + . = ALIGN(16); + . += 0x4000; /* 16 KiB boot stack */ + _stack_top = .; +} diff --git a/arch/x86/arch.c b/arch/x86/arch.c index 0231e21..a198dd7 100644 --- a/arch/x86/arch.c +++ b/arch/x86/arch.c @@ -36,6 +36,18 @@ void arch_uart_putc(char c) outb(COM1, (unsigned char)c); } +char arch_uart_getc(void) +{ + while ((inb(COM1 + 5) & 0x01) == 0) /* wait RX data ready (LSR.DR) */ + ; + return (char)inb(COM1); /* read RBR */ +} + +int arch_uart_rx_ready(void) +{ + return (inb(COM1 + 5) & 0x01) ? 1 : 0; +} + /* M1: minimal IDT. We load a valid-but-empty IDT so the CPU has a * table; M2 will fill gates and handle the timer (IRQ0). */ struct idt_entry { diff --git a/common/console.c b/common/console.c index 838d706..8a6bc3a 100644 --- a/common/console.c +++ b/common/console.c @@ -16,6 +16,21 @@ void puts(const char *s) putc(*s++); } +/* Read one byte from the UART. Portable wrapper over arch_uart_getc(). */ +char getc(void) +{ + return arch_uart_getc(); +} + +/* Discard any bytes currently waiting in the RX FIFO. Used to drop the + * bytes our own TX echoes back on half-duplex-wired boards before we start + * reading real keystrokes. */ +void drain_rx(void) +{ + while (arch_uart_rx_ready()) + (void)arch_uart_getc(); +} + /* Minimal unsigned hex printer — enough to show addresses/registers. */ void puthex(unsigned long v) { diff --git a/common/main.c b/common/main.c index 7039625..f2f7f13 100644 --- a/common/main.c +++ b/common/main.c @@ -25,5 +25,21 @@ void kmain(void) puts("========================================\n"); puts("hello from kmain()\n"); - arch_halt(); + /* Interactive echo: proves UART input (RX) works. Type on the serial + * console and minos echoes each key back. This is the seed of a real + * console — M3 will turn it into a command interface. */ + puts("\nType something (keys are echoed back):\n> "); + drain_rx(); /* drop any bytes already sitting in the RX FIFO */ + for (;;) { + char c = getc(); + if (c == '\r' || c == '\n') { + puts("\n> "); /* newline + fresh prompt */ + continue; + } + if (c == 0x7f || c == 0x08) { /* DEL / backspace */ + puts("\b \b"); + continue; + } + putc(c); /* echo the key */ + } } diff --git a/include/arch.h b/include/arch.h index 4b5fb1a..99c346b 100644 --- a/include/arch.h +++ b/include/arch.h @@ -16,6 +16,14 @@ void arch_early_init(void); * common/console.c builds print()/puts() on top of just this. */ void arch_uart_putc(char c); +/* Blocking read of one byte from the debug UART (polls the RX FIFO). + * The UART is full-duplex, so this is the mirror of arch_uart_putc. */ +char arch_uart_getc(void); + +/* Non-blocking: return 1 if a received byte is waiting, else 0. Lets the + * console drain any stale/echoed RX bytes without blocking. */ +int arch_uart_rx_ready(void); + /* Install the interrupt/trap vector table (IDT / mtvec / VBAR). * M1 stubs this; M2 makes it handle a timer tick. */ void arch_set_trap_vector(void); diff --git a/include/console.h b/include/console.h index 6beea02..db48fae 100644 --- a/include/console.h +++ b/include/console.h @@ -4,5 +4,7 @@ void putc(char c); void puts(const char *s); void puthex(unsigned long v); +char getc(void); +void drain_rx(void); #endif