code wiki / _hdl_build / rv64im_min_uart.nx

rv64im_min_uart.nx source

↩ module page · 218 lines · 8708 B

1// rv64im_min_uart.nx -- 16550-compatible UART MMIO device. 2// 3// The kernel's only observable output channel. Without this, no 4// "[m0-mcs-live]" marker prints, no bench output reaches the host, 5// nothing about the kernel's run is visible. Tier A FPGA bring-up 6// exit criterion #2 from rv64im_min_target_spec.md depends on this 7// module producing the QEMU-reference selftest output. 8// 9// Tier A scope: TX-only, polled (no RX, no FIFO, no interrupts). 10// Kernel never reads stdin and uses busy-wait on LSR.THRE between 11// bytes (uart.nx:38-41); the kernel's MIE-masking-for-atomicity 12// pattern still works because nothing else generates interrupts 13// during a write. 14// 15// Memory map (matches QEMU virt machine; kernel hardcodes 0x10000000): 16// 0x10000000 THR (offset 0) TX holding register; 8-bit write 17// 0x10000001..4 unused; reads return 0 18// 0x10000005 LSR (offset 5) Line status; bit 5 = THRE 19// 0x10000006.. reads return 0 20// 21// LSR bit semantics (subset relevant to kernel): 22// bit 0: DR (data ready -- not used, RX absent) 23// bit 5: THRE (TX holding empty -- always 1 in Tier A; no FIFO) 24// bit 6: TEMT (TX empty -- always 1; alias of THRE for this device) 25// 26// Status: SEED. 2026-05-26. Register storage + TX byte sink 27// (host stdout for simulator; real UART line for FPGA bitstream). 28 29import "nx_syscalls.nx" 30import "nishi_hdl_primitives.nx" 31 32// ===== MMIO addresses ================================================= 33const NX_UART_BASE: i64 = 0x10000000 34const NX_UART_OFFSET_THR: i64 = 0 35const NX_UART_OFFSET_LSR: i64 = 5 36const NX_UART_END: i64 = 0x10000008 // 8 bytes covers all registers 37 38// ===== LSR bit positions ================================================= 39const NX_UART_LSR_DR_BIT: i64 = 0x01 // bit 0: data ready 40const NX_UART_LSR_THRE_BIT: i64 = 0x20 // bit 5: TX holding empty 41const NX_UART_LSR_TEMT_BIT: i64 = 0x40 // bit 6: TX shift empty 42 43// Tier A: LSR is always (THRE | TEMT) because there's no FIFO and 44// the simulator/silicon sink accepts every byte immediately. 45const NX_UART_LSR_TIER_A: i64 = 0x60 // 0b01100000 46 47// ===== Verdicts ================================================= 48const NX_UART_OK: i64 = 0 49const NX_UART_ADDR_OUT_OF_RANGE: i64 = 1 50const NX_UART_BAD_ALIGNMENT: i64 = 2 // 1-byte access required for THR/LSR 51 52// ===== Storage + TX byte counter ================================================= 53// 54// V1: caller allocates 1 i64 slot: 55// slot 0: TX byte count (for the simulator path; FPGA path drops 56// it since the byte stream goes out the physical UART line) 57// 58// The simulator's "transmit" path appends the byte to a host-side 59// buffer that the harness can capture; the silicon synth path 60// generates the actual UART TX shift register from the same 61// declared port shape. 62 63const NX_UART_SLOT_TX_COUNT: i64 = 0 64const NX_UART_SLOT_N: i64 = 1 65 66struct NxUart { 67 storage: *i64 // 1 i64 68 tx_buf: *u8 // host-side TX capture buffer (simulator only) 69 tx_cap: i64 // tx_buf capacity in bytes 70 valid: i64 71} 72 73func nx_uart_init(u: *NxUart, storage: *i64, tx_buf: *u8, tx_cap: i64) -> i64 { 74 if (u as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 75 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 76 u.storage = storage 77 u.tx_buf = tx_buf 78 u.tx_cap = tx_cap 79 u.valid = 1 80 storage[NX_UART_SLOT_TX_COUNT] = 0 81 return NX_UART_OK 82} 83 84// ===== Range check ================================================= 85func nx_uart_addr_in_range(addr: i64) -> i64 { 86 if addr < NX_UART_BASE { return 0 } 87 if addr >= NX_UART_END { return 0 } 88 return 1 89} 90 91// ===== MMIO read (8-bit) ================================================= 92// 93// Kernel reads LSR via uart.nx:39 (`while (lsr_addr[0] & 0x20) == 0`). 94// THR reads not used (the kernel never reads back TX state besides 95// LSR). Per riscv-spec the access is 1-byte; the executor presents 96// the result in the low byte of the return value. 97 98func nx_uart_read8(u: *NxUart, addr: i64, value_out: *i64) -> i64 { 99 if u.valid != 1 { return 0 - NX_UART_ADDR_OUT_OF_RANGE } 100 if (value_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 101 if nx_uart_addr_in_range(addr) != 1 { return 0 - NX_UART_ADDR_OUT_OF_RANGE } 102 103 let off: i64 = addr - NX_UART_BASE 104 if off == NX_UART_OFFSET_LSR { 105 // Tier A: TX is always empty (no FIFO; immediate-sink TX). 106 // Kernel busy-wait exits on first read. 107 value_out[0] = NX_UART_LSR_TIER_A 108 return NX_UART_OK 109 } 110 // Other offsets (THR read, RBR not implemented, IIR/MCR/MSR 111 // not modeled): return 0. Kernel doesn't read them. 112 value_out[0] = 0 113 return NX_UART_OK 114} 115 116// ===== MMIO write (8-bit) ================================================= 117// 118// Kernel writes THR via uart.nx:42 (`thr_addr[0] = byte`). This is 119// the device's primary side effect: bytes go out the line. In the 120// simulator path we capture into u.tx_buf for the host harness; in 121// the silicon path the byte enters the TX shift register and gets 122// clocked out at the configured baud. 123 124func nx_uart_write8(u: *NxUart, addr: i64, value: i64) -> i64 { 125 if u.valid != 1 { return 0 - NX_UART_ADDR_OUT_OF_RANGE } 126 if nx_uart_addr_in_range(addr) != 1 { return 0 - NX_UART_ADDR_OUT_OF_RANGE } 127 128 let off: i64 = addr - NX_UART_BASE 129 if off == NX_UART_OFFSET_THR { 130 // Simulator-side capture (FPGA path: this slot routes the 131 // byte to the physical UART TX pin via the bitstream). 132 let n: i64 = u.storage[NX_UART_SLOT_TX_COUNT] 133 if (u.tx_buf as i64) != 0 { 134 if n < u.tx_cap { 135 u.tx_buf[n] = (value & 0xff) as u8 136 } 137 } 138 u.storage[NX_UART_SLOT_TX_COUNT] = n + 1 139 return NX_UART_OK 140 } 141 // Other offsets: silently dropped (kernel doesn't write them). 142 return NX_UART_OK 143} 144 145// ===== Capture-buffer accessor ================================================= 146// 147// Simulator-only helper: how many bytes has the kernel transmitted 148// since init? Harness can read tx_buf[0..tx_count) to verify 149// expected output. 150 151func nx_uart_tx_count(u: *NxUart) -> i64 { 152 if u.valid != 1 { return 0 } 153 return u.storage[NX_UART_SLOT_TX_COUNT] 154} 155 156// ===== HDL-graph builder ================================================= 157// 158// Two MMIO ports (read + write); LSR is a constant in Tier A so it 159// has no driver edge; THR sinks bytes via the txd output (FPGA pin 160// in the bitstream). 161 162const NX_UART_WIDTH_ADDR: i64 = 32 163const NX_UART_WIDTH_BYTE: i64 = 8 164const NX_UART_WIDTH_VERDICT: i64 = 4 165const NX_UART_WIDTH_TXD: i64 = 1 // serial line pin 166 167struct NxUartPorts { 168 clk: i64 // fabric clock 169 reset: i64 // sync reset 170 rd_addr: i64 // input wire, 32-bit 171 rd_en: i64 // input wire, 1-bit 172 rd_data: i64 // output wire, 8-bit (LSR or 0) 173 wr_addr: i64 // input wire, 32-bit 174 wr_data: i64 // input wire, 8-bit (THR byte) 175 wr_en: i64 // input wire, 1-bit 176 wr_verdict: i64 // output wire, 4-bit 177 txd_pin: i64 // output wire, 1-bit -- serial line (FPGA pin) 178} 179 180func nx_uart_build(m: *NxHdlModule, ports: *NxUartPorts) -> i64 { 181 let p_clk: i64 = nx_hdl_clock(m) 182 if p_clk < 0 { return p_clk } 183 let p_rst: i64 = nx_hdl_reset(m) 184 if p_rst < 0 { return p_rst } 185 let p_rd_addr: i64 = nx_hdl_input(m, NX_UART_WIDTH_ADDR) 186 if p_rd_addr < 0 { return p_rd_addr } 187 let p_rd_en: i64 = nx_hdl_input(m, 1) 188 if p_rd_en < 0 { return p_rd_en } 189 let p_rd_data: i64 = nx_hdl_output(m, NX_UART_WIDTH_BYTE) 190 if p_rd_data < 0 { return p_rd_data } 191 let p_wr_addr: i64 = nx_hdl_input(m, NX_UART_WIDTH_ADDR) 192 if p_wr_addr < 0 { return p_wr_addr } 193 let p_wr_data: i64 = nx_hdl_input(m, NX_UART_WIDTH_BYTE) 194 if p_wr_data < 0 { return p_wr_data } 195 let p_wr_en: i64 = nx_hdl_input(m, 1) 196 if p_wr_en < 0 { return p_wr_en } 197 let p_wr_verdict: i64 = nx_hdl_output(m, NX_UART_WIDTH_VERDICT) 198 if p_wr_verdict < 0 { return p_wr_verdict } 199 let p_txd: i64 = nx_hdl_output(m, NX_UART_WIDTH_TXD) 200 if p_txd < 0 { return p_txd } 201 202 ports.clk = p_clk 203 ports.reset = p_rst 204 ports.rd_addr = p_rd_addr 205 ports.rd_en = p_rd_en 206 ports.rd_data = p_rd_data 207 ports.wr_addr = p_wr_addr 208 ports.wr_data = p_wr_data 209 ports.wr_en = p_wr_en 210 ports.wr_verdict = p_wr_verdict 211 ports.txd_pin = p_txd 212 213 // Synth target: 1 byte-wide shift register for TX + baud-rate 214 // divider counter + 1-bit "transmitting" flag. Tier A FPGA: 215 // ~200 gates + the TX pin. Tier B+ MPW: same plus optional 216 // RX/UART_FIFO/interrupt-source for richer console handling. 217 return NX_HDL_OK 218}