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}