riscv_emulator/mmio.rs
1//! Memory-mapped I/O — peripheral registers and RAM access helpers.
2//!
3//! Any address in `0x1000_0000..0x1200_0000` is treated as MMIO.
4//! Reads and writes to RAM use `ptr::read_unaligned` and `ptr::write_unaligned` to handle
5//! unaligned accesses, which are legal in RV32I.
6//!
7//! ## Peripheral map
8//!
9//! | Address | Peripheral | Notes |
10//! |---------|-----------|-------|
11//! | `0x1000_0000` | UART TX/RX | Write = transmit byte; read = receive byte |
12//! | `0x1000_0005` | UART LSR | Bit 5 = TX ready, bit 0 = RX ready |
13//! | `0x1100_4000` | CLINT mtimecmp low | Written by kernel to schedule next timer IRQ |
14//! | `0x1100_4004` | CLINT mtimecmp high | |
15//! | `0x1100_BFF8` | CLINT mtime low | Read-only; driven by [`CpuState::tick_timer`] |
16//! | `0x1100_BFFC` | CLINT mtime high | |
17//! | `0x1110_0000` | SYSCON | `0x5555` = poweroff, `0x7777` = restart |
18//!
19//! ## UART
20//!
21//! The UART is a simplified NS16550-compatible device. TX is always ready
22//! (`LSR` bits 5 and 6 hardwired to 1). RX readiness reflects whether the
23//! host has a key waiting in stdin.
24//!
25//! In WASM mode, TX bytes are pushed to an `output_buf` instead of being
26//! printed to stdout. This lets `EmulatorWasm::step_batch`
27//! collect output and hand it back to the JavaScript caller.
28//!
29//! ## CLINT
30//!
31//! The Core Local Interruptor provides `mtime` (a free-running 64-bit counter)
32//! and `mtimecmp` (the comparison threshold). When `mtime > mtimecmp`, MTIP
33//! fires. The actual increment happens in [`CpuState::tick_timer`]; this
34//! module only handles the MMIO read/write interface.
35//!
36//! [`CpuState::tick_timer`]: crate::cpu::CpuState::tick_timer
37
38use crate::cpu::{CpuState, Csr, StepResult};
39use crate::platform::Platform;
40use std::io::{self, Write};
41
42// ── UART NS16550 ──────────────────────────────────────────────────────────────
43
44/// UART TX register (write) / RX register (read).
45pub const UART_TX: u32 = 0x1000_0000;
46/// UART Line Status Register.
47pub const UART_LSR: u32 = 0x1000_0005;
48/// LSR value when TX is ready: THRE (bit 5) + TEMT (bit 6).
49const UART_LSR_TX_READY: u32 = 0x60;
50/// LSR DR bit (bit 0) — set when a byte is available to read.
51const UART_LSR_RX_READY: u32 = 0x01;
52
53// ── CLINT ─────────────────────────────────────────────────────────────────────
54
55/// CLINT `mtimecmp` — low 32 bits (write-only from software perspective).
56pub const CLINT_MTIMECMP_LO: u32 = 0x1100_4000;
57/// CLINT `mtimecmp` — high 32 bits.
58pub const CLINT_MTIMECMP_HI: u32 = 0x1100_4004;
59/// CLINT `mtime` — low 32 bits (read-only).
60pub const CLINT_MTIME_LO: u32 = 0x1100_BFF8;
61/// CLINT `mtime` — high 32 bits (read-only).
62pub const CLINT_MTIME_HI: u32 = 0x1100_BFFC;
63
64// ── SYSCON ────────────────────────────────────────────────────────────────────
65
66/// System controller address. Writing `0x5555` powers off; `0x7777` restarts.
67pub const SYSCON_ADDR: u32 = 0x1110_0000;
68
69/// Returns `true` if `addr` falls in the MMIO region (`0x1000_0000..0x1200_0000`).
70#[inline]
71pub fn is_mmio(addr: u32) -> bool {
72 (0x1000_0000..0x1200_0000).contains(&addr)
73}
74
75/// Handle an MMIO store (write).
76///
77/// Returns [`StepResult::Ok`] in all normal cases. Returns
78/// [`StepResult::Restart`] or [`StepResult::Poweroff`] when the kernel writes
79/// to SYSCON, which causes the run loop to exit immediately.
80///
81/// The `output_buf` parameter controls UART output routing:
82/// - `None` — bytes are printed directly to stdout (native binary mode).
83/// - `Some(buf)` — bytes are appended to the buffer (WASM mode).
84pub fn handle_store(
85 cpu: &mut CpuState,
86 addr: u32,
87 val: u32,
88 output_buf: Option<&mut Vec<u8>>,
89) -> StepResult {
90 match addr {
91 // UART TX — send the low byte to the console.
92 UART_TX => {
93 let byte = val as u8;
94 if let Some(buf) = output_buf {
95 buf.push(byte);
96 } else {
97 print!("{}", byte as char);
98 let _ = io::stdout().flush();
99 }
100 }
101
102 // CLINT mtimecmp — schedule the next timer interrupt.
103 CLINT_MTIMECMP_LO => {
104 cpu.timermatchl = val;
105 }
106 CLINT_MTIMECMP_HI => {
107 cpu.timermatchh = val;
108 }
109
110 // SYSCON — power control.
111 SYSCON_ADDR => {
112 // Advance the PC past the store instruction before returning so
113 // the state is consistent if the caller inspects it after halt.
114 cpu.pc += 4;
115 return match val {
116 0x7777 => StepResult::Restart,
117 0x5555 => StepResult::Poweroff,
118 _ => StepResult::Fault,
119 };
120 }
121
122 _ => {}
123 }
124 StepResult::Ok
125}
126
127/// Handle an MMIO load (read).
128///
129/// Returns `0` for unmapped addresses, which is harmless and consistent with
130/// how real hardware behaves on unimplemented registers.
131pub fn handle_load(cpu: &CpuState, addr: u32, plat: &mut dyn Platform) -> u32 {
132 match addr {
133 // UART LSR — TX is always ready; RX ready bit reflects stdin.
134 UART_LSR => {
135 let rx = if plat.is_kb_hit() != 0 {
136 UART_LSR_RX_READY
137 } else {
138 0
139 };
140 UART_LSR_TX_READY | rx
141 }
142
143 // UART RX — consume one byte from stdin if available.
144 UART_TX if plat.is_kb_hit() != 0 => plat.read_kb_byte() as u32,
145
146 // CLINT mtime — the kernel reads these to get the current time.
147 CLINT_MTIME_LO => cpu.timerl,
148 CLINT_MTIME_HI => cpu.timerh,
149
150 _ => 0,
151 }
152}
153
154/// Handle a write to an unrecognized CSR number.
155///
156/// The standard CSRs are handled directly in the decode loop. This function
157/// covers the custom debug CSRs (`0x136`–`0x139`) that the original
158/// mini-rv32ima added for bare-metal printf-style debugging without a UART
159/// driver.
160pub fn handle_csr_write(ram: &[u8], ram_size: u32, csrno: u32, value: u32) {
161 const RAM_BASE: u32 = 0x8000_0000;
162 match csrno {
163 x if x == Csr::PrintInt as u32 => {
164 print!("{}", value as i32);
165 let _ = io::stdout().flush();
166 }
167 x if x == Csr::PrintHex as u32 => {
168 print!("{:08x}", value);
169 let _ = io::stdout().flush();
170 }
171 x if x == Csr::PrintChar as u32 => {
172 print!("{}", value as u8 as char);
173 let _ = io::stdout().flush();
174 }
175 x if x == Csr::PrintStr as u32 => {
176 let offset = value.wrapping_sub(RAM_BASE) as usize;
177 if offset >= ram_size as usize {
178 eprintln!("DEBUG: invalid PrintStr pointer ({:08x})", value);
179 return;
180 }
181 let slice = &ram[offset..];
182 let len = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
183 let _ = io::stdout().write_all(&slice[..len]);
184 }
185 _ => {}
186 }
187}
188
189/// Handle a read from an unrecognized CSR number.
190///
191/// Currently only `ReadKbd` (`0x140`) is handled here. It returns the next
192/// byte from stdin, or `-1` cast to `i32` if none is available.
193pub fn handle_csr_read(csrno: u32, plat: &mut dyn Platform) -> i32 {
194 if csrno == Csr::ReadKbd as u32 {
195 if plat.is_kb_hit() == 0 {
196 return -1;
197 }
198 return plat.read_kb_byte();
199 }
200 0
201}
202
203// ─────────────────────────────────────────────────────────────────────────────
204// RAM access helpers
205//
206// RV32I allows unaligned loads and stores. We use ptr::read/write_unaligned
207// to implement these without undefined behaviour. All offsets are validated
208// against ram_size before these functions are called.
209// ─────────────────────────────────────────────────────────────────────────────
210
211/// Load 1 byte, zero-extended to 32 bits (`LBU`).
212#[inline]
213pub fn mem_load1(ram: &[u8], ofs: u32) -> u32 {
214 ram[ofs as usize] as u32
215}
216
217/// Load 2 bytes, zero-extended to 32 bits (`LHU`). Handles unaligned offsets.
218#[inline]
219pub fn mem_load2(ram: &[u8], ofs: u32) -> u32 {
220 let v = unsafe { (ram.as_ptr().add(ofs as usize) as *const u16).read_unaligned() };
221 v as u32
222}
223
224/// Load 4 bytes (`LW`). Handles unaligned offsets.
225#[inline]
226pub fn mem_load4(ram: &[u8], ofs: u32) -> u32 {
227 unsafe { (ram.as_ptr().add(ofs as usize) as *const u32).read_unaligned() }
228}
229
230/// Load 1 byte, sign-extended to 32 bits (`LB`).
231#[inline]
232pub fn mem_load1s(ram: &[u8], ofs: u32) -> u32 {
233 ram[ofs as usize] as i8 as i32 as u32
234}
235
236/// Load 2 bytes, sign-extended to 32 bits (`LH`). Handles unaligned offsets.
237#[inline]
238pub fn mem_load2s(ram: &[u8], ofs: u32) -> u32 {
239 let v = unsafe { (ram.as_ptr().add(ofs as usize) as *const u16).read_unaligned() };
240 v as i16 as i32 as u32
241}
242
243/// Store 1 byte (`SB`).
244#[inline]
245pub fn mem_store1(ram: &mut [u8], ofs: u32, val: u32) {
246 ram[ofs as usize] = val as u8;
247}
248
249/// Store 2 bytes (`SH`). Handles unaligned offsets.
250#[inline]
251pub fn mem_store2(ram: &mut [u8], ofs: u32, val: u32) {
252 unsafe {
253 (ram.as_mut_ptr().add(ofs as usize) as *mut u16).write_unaligned(val as u16);
254 }
255}
256
257/// Store 4 bytes (`SW`). Handles unaligned offsets.
258#[inline]
259pub fn mem_store4(ram: &mut [u8], ofs: u32, val: u32) {
260 unsafe {
261 (ram.as_mut_ptr().add(ofs as usize) as *mut u32).write_unaligned(val);
262 }
263}