add Load (LB,LH,LW,LBU,LHU) operations implementation
arnaucube git@arnaucube.com
Tue, 08 Sep 2026 21:38:27 +0200
2 files changed,
97 insertions(+),
7 deletions(-)
M
README.md
→
README.md
@@ -1,6 +1,6 @@
# rv32-rs -Risc-V (RV32I variant) implementation by hand to get familiar with Risc-V's internals. +Risc-V (RV32I variant) implementation by hand to get familiar with Risc-V's internals. Do not use, for learning purposes only. Follows the specification from: - https://docs.riscv.org/reference/isa/v20260120/unpriv/rv32.html
M
src/lib.rs
→
src/lib.rs
@@ -1,3 +1,4 @@
+//! Implementation of the RV32I ISA. Each opcode has a comment explaining it. use anyhow::{anyhow, Result}; pub struct RV32 {@@ -53,6 +54,12 @@ let funct3 = (instruction >> 12) & 7;
let rs1 = ((instruction >> 15) & 31) as usize; let rs2 = ((instruction >> 20) & 31) as usize; let funct7 = instruction >> 25; + // I-type: + // 31 20 19 15 14 12 11 7 6 0 + // ┌────────────────────────┬────────────┬────────┬────────┬─────────┐ + // │ imm[11:0] │ rs1 │ funct3 │ rd │ opcode │ + // └────────────────────────┴────────────┴────────┴────────┴─────────┘ + let imm = sign_extend(instruction >> 20, 12); let a = self.regs[rs1]; let b = self.regs[rs2];@@ -60,6 +67,30 @@ let result: Option<u32>;
// 0x7f=0111_1111, match the lowest 7 bits to read the opcode match instruction & 0x7f { + // I-type op'ns: + // Loads + 0x03 => { + let (size, signed) = match funct3 { + // LB: load a byte at rs1 + immediate and sign-extend it into rd + 0 => (1, true), + // LH: load a halfword at rs1 + immediate and sign-extend it into rd + 1 => (2, true), + // LW: load a 32-bit word at rs1 + immediate into rd + 2 => (4, false), + // LBU: load a byte at rs1 + immediate and zero-extend it into rd + 4 => (1, false), + // LHU: load a halfword at rs1 + immediate and zero-extend it into rd + 5 => (2, false), + _ => return Err(anyhow!("illegal instruction: {}", instruction)), + }; + let value = self.read(a.wrapping_add(imm), size)?; + result = Some(if signed { + sign_extend(value, size as u32 * 8) + } else { + value + }); + } + // R-type (register-register) integer operations 0x33 => { result = Some(match (funct7, funct3) {@@ -128,11 +159,17 @@ Ok(())
} } +// sign-extend a smaller signed number to 32 bits +fn sign_extend(value: u32, bits: u32) -> u32 { + ((value << (32 - bits)) as i32 >> (32 - bits)) as u32 +} + #[cfg(test)] mod tests { use super::*; const OPCODE_R: u32 = 0b0110011; + const OPCODE_LOAD: u32 = 0b0000011; // encodes R-type instruction fn encode_r(name: &str, rd: u32, rs1: u32, rs2: u32) -> u32 {@@ -155,6 +192,20 @@ _ => panic!("unknown R-type instruction: {name}"),
}; (funct7 << 25) | (rs2 << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | OPCODE_R + } + fn encode_i(name: &str, rd: u32, rs1: u32, imm: i32) -> u32 { + let funct3 = match name { + "lb" => 0b000, + "lh" => 0b001, + "lw" => 0b010, + "lbu" => 0b100, + "lhu" => 0b101, + _ => panic!("unknown load instruction: {name}"), + }; + + let imm = (imm as u32) & 0xfff; + + (imm << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | OPCODE_LOAD } #[test]@@ -212,14 +263,14 @@ ("rem", 20, 5, 0),
("rem", 21, 5, 1), ("rem", 0, 10, 0), ("rem", 10, 1, 0), - ("rem", 23, 5, 3), // Positive remainder. - ("rem", (-23i32) as u32, 5, (-3i32) as u32), // Remainder follows the sign of the dividend. - ("rem", 23, (-5i32) as u32, 3), // Negative divisor does not change the sign of the remainder. - ("rem", (-23i32) as u32, (-5i32) as u32, (-3i32) as u32), // Negative / negative. - ("rem", 3, 5, 3), // Dividend smaller than divisor. + ("rem", 23, 5, 3), // positive remainder + ("rem", (-23i32) as u32, 5, (-3i32) as u32), // remainder follows the sign of the dividend + ("rem", 23, (-5i32) as u32, 3), // negative divisor does not change the sign of the remainder + ("rem", (-23i32) as u32, (-5i32) as u32, (-3i32) as u32), // negative / negative + ("rem", 3, 5, 3), // dividend smaller than divisor // RISC-V special case: ("rem", 0x8000_0000, 0xffff_ffff, 0), // INT_MIN % -1 = 0. - ("rem", 123, 0, 123), // Remainder by zero returns the dividend. + ("rem", 123, 0, 123), // remainder by zero returns the dividend. ("rem", u32::MAX, 0, u32::MAX), ("rem", 0x8000_0000, 0, 0x8000_0000), ];@@ -245,6 +296,45 @@ cpu.regs[rd as usize], expected,
"{name} failed: rs1={rs1_value:#010x}, rs2={rs2_value:#010x}, expected={expected:#010x}, got={:#010x}", + cpu.regs[rd as usize], + ); + } + } + + #[test] + fn test_ops_load() { + let test_vectors: [(&str, u32, i32, u32); 8] = [ + ("lb", 0x0000_0080, 0, 0xffff_ff80), + ("lbu", 0x0000_0080, 0, 0x0000_0080), + ("lb", 0x0000_00ff, 0, 0xffff_ffff), + ("lbu", 0x0000_00ff, 0, 0x0000_00ff), + ("lh", 0x0000_8000, 0, 0xffff_8000), + ("lhu", 0x0000_8000, 0, 0x0000_8000), + ("lh", 0x0000_ffff, 0, 0xffff_ffff), + ("lhu", 0x0000_ffff, 0, 0x0000_ffff), + ]; + + for (name, memory_value, address, expected) in test_vectors { + let rd = 1; + let rs1 = 2; + + let instruction = encode_i(name, rd, rs1, address); + + let mut memory = instruction.to_le_bytes().to_vec(); + let data_address = memory.len() as u32; + memory.extend_from_slice(&memory_value.to_le_bytes()); + let mut cpu = RV32::new(memory); + + cpu.regs[rs1 as usize] = data_address; + + cpu.step().unwrap(); + + assert_eq!( + cpu.regs[rd as usize], expected, + "{name} failed: memory={memory_value:#010x}, + address={address}, \ + expected={expected:#010x}, + got={:#010x}", cpu.regs[rd as usize], ); }