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).
This commit is contained in:
Шурупов Илья Викторович 2026-09-25 21:13:59 +03:00
parent 59a433f949
commit 775a02cf3f
9 changed files with 250 additions and 10 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

31
arch/riscv/link-d1.ld Normal file
View file

@ -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 = .;
}

View file

@ -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 {

View file

@ -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)
{

View file

@ -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 */
}
}

View file

@ -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);

View file

@ -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