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:
parent
59a433f949
commit
775a02cf3f
9 changed files with 250 additions and 10 deletions
33
Makefile
33
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
31
arch/riscv/link-d1.ld
Normal 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 = .;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue