Skip to main content

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}