arnaucube
arnaucube/rv32-rsPublic Self-hosted repository
add B-type op'ns (branching)
arnaucube git@arnaucube.com
Wed, 09 Sep 2026 08:27:14 +0200
commit

d0bfde762de1d5edfe6edfce7bc1ecf555d56d39

parent

e7dd3ba1d668839d763f5b6eac6f66b8d6f68ac3

2 files changed, 139 insertions(+), 7 deletions(-)

jump to
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. Do not use, for learning purposes only. +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

@@ -41,7 +41,7 @@ Ok(u32::from_le_bytes(bytes))

} pub fn step(&mut self) -> Result<()> { let instruction = self.read(self.pc, 4)?; - let next_pc = self.pc.wrapping_add(4); + let mut next_pc = self.pc.wrapping_add(4); // R-type instruction: // R-type:

@@ -64,9 +64,16 @@ // 31 25 24 20 19 15 14 12 11 7 6 0

// ┌───────────┬────────────┬────────────┬────────┬───────────┬─────────┐ // │ imm[11:5] │ rs2 │ rs1 │ funct3 │ imm[4:0] │ opcode │ // └───────────┴────────────┴────────────┴────────┴───────────┴─────────┘ + // B-type: + // 31 30 25 24 20 19 15 14 12 11 8 7 6 0 + // ┌─────┬─────────┬────────┬────────────┬────────┬────────┬──┬──────┐ + // │imm12│imm[10:5]│ rs2 │ rs1 │ funct3 │imm[4:1]│11│opcode│ + // └─────┴─────────┴────────┴────────────┴────────┴────────┴──┴──────┘ // // Note: we keep `imm` as the I-type imm, and in the S-type we compute - // the S-type respective imm from 25-31 and 7-11 positions + // the S-type respective imm from 31-25 and 11-7 positions, and for + // B-type we obtain imm from the positions 31, 31-25, 11-8, 7, and + // imm[0]=0. let imm = sign_extend(instruction >> 20, 12); let a = self.regs[rs1];

@@ -89,7 +96,7 @@ // 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)), + _ => return Err(anyhow!("illegal instruction in I-type: {}", instruction)), }; let value = self.read(a.wrapping_add(imm), size)?; result = Some(if signed {

@@ -107,7 +114,7 @@ // SH: store the low halfword of rs2 at rs1 + immediate

1 => 2, // SW: store the full 32-bit value of rs2 at rs1 + immediate 2 => 4, - _ => return Err(anyhow!("illegal instruction: {}", instruction)), + _ => return Err(anyhow!("illegal instruction in S-type: {}", instruction)), }; // for the S-type imm, get it from 25-31 and 7-11 positions let s_type_imm = ((instruction >> 25) << 5) | ((instruction >> 7) & 31);

@@ -167,10 +174,38 @@ (a as i32).wrapping_rem(b as i32) as u32

} } - _ => return Err(anyhow!("illegal instruction (A): {}", instruction)), + _ => return Err(anyhow!("illegal instruction in R-type: {}", instruction)), }); } - _ => return Err(anyhow!("illegal instruction (B): {}", instruction)), + // B-type operations + 0x63 => { + let taken = match funct3 { + // BEQ: branch by the signed PC-relative offset if the registers are equal + 0 => a == b, + // BNE: branch if the registers are unequal + 1 => a != b, + // BLT: branch if rs1 is less than rs2 using signed comparison + 4 => (a as i32) < (b as i32), + // BGE: branch if rs1 is at least rs2 using signed comparison + 5 => (a as i32) >= (b as i32), + // BLTU: branch if rs1 is less than rs2 using unsigned comparison + 6 => a < b, + // BGEU: branch if rs1 is at least rs2 using unsigned comparison + 7 => a >= b, + _ => return Err(anyhow!("illegal instruction in B-type: {}", instruction)), + }; + if taken { + // offset: reconstruct imm for B-type from positions 31, 31-25, + // 11-8, 7, and imm[0]=0 + let b_type_imm = ((instruction >> 31) << 12) + | (((instruction >> 7) & 1) << 11) + | (((instruction >> 25) & 0x3f) << 5) + | (((instruction >> 8) & 15) << 1); + next_pc = self.pc.wrapping_add(sign_extend(b_type_imm, 13)); + } + } + + _ => return Err(anyhow!("illegal instruction: {}", instruction)), } if next_pc & 3 != 0 {

@@ -247,7 +282,45 @@ let imm_4_0 = imm & 0x1f;

(imm_11_5 << 25) | (rs2 << 20) | (rs1 << 15) | (funct3 << 12) | (imm_4_0 << 7) | opcode } + fn encode_b(name: &str, rs1: u32, rs2: u32, offset: i32) -> u32 { + let funct3 = match name { + "beq" => 0b000, + "bne" => 0b001, + "blt" => 0b100, + "bge" => 0b101, + "bltu" => 0b110, + "bgeu" => 0b111, + _ => panic!("unknown B-type instruction: {name}"), + }; + assert!( + offset % 2 == 0, + "B-type branch offset must be 2-byte aligned" + ); + + assert!( + (-4096..=4094).contains(&offset), + "B-type branch offset out of range: {offset}" + ); + + let imm = offset as u32; + + let imm12 = (imm >> 12) & 0x1; + let imm11 = (imm >> 11) & 0x1; + let imm10_5 = (imm >> 5) & 0x3f; + let imm4_1 = (imm >> 1) & 0xf; + + (imm12 << 31) + | (imm10_5 << 25) + | (rs2 << 20) + | (rs1 << 15) + | (funct3 << 12) + | (imm4_1 << 8) + | (imm11 << 7) + | 0b1100011 + } + + // R-type ops test #[test] fn test_ops_r_type() { let test_vectors = vec![

@@ -341,6 +414,7 @@ );

} } + // I-type ops test #[test] fn test_ops_load() { let test_vectors: Vec<(&str, u32, i32, u32)> = vec![

@@ -379,6 +453,8 @@ cpu.regs[rd as usize],

); } } + + // S-type ops test #[test] fn test_ops_store() { let test_vectors: Vec<(&str, u32, i32)> = vec![

@@ -428,6 +504,62 @@ "{name} failed: value={value:#010x},

address={address}, \ expected={expected:#010x}, got={stored:#010x}", + ); + } + } + + // B-type ops test + #[test] + fn test_ops_branch() { + let test_vectors: Vec<(&str, u32, u32, i32, bool)> = vec![ + // (name, rs1 value, rs2 value, offset, taken) + ("beq", 10, 10, 8, true), + ("beq", 10, 20, 8, false), + ("bne", 10, 20, 8, true), + ("bne", 10, 10, 8, false), + ("blt", 10, 20, 8, true), + ("blt", 20, 10, 8, false), + ("bge", 20, 10, 8, true), + ("bge", 10, 20, 8, false), + ("bltu", 10, 20, 8, true), + ("bltu", 20, 10, 8, false), + ("bgeu", 20, 10, 8, true), + ("bgeu", 10, 20, 8, false), + ("beq", 10, 10, -4, true), // negative branch offsets + ("bne", 10, 20, -4, true), + ("blt", 0xffff_ffff, 0, 8, true), // signed vs unsigned comparison + ("bltu", 0xffff_ffff, 0, 8, false), + ("bge", 0xffff_ffff, 0, 8, false), + ("bgeu", 0xffff_ffff, 0, 8, true), + ]; + + for (name, rs1_value, rs2_value, offset, taken) in test_vectors { + let rs1 = 1; + let rs2 = 2; + + let instruction = encode_b(name, rs1, rs2, offset); + let mut cpu = RV32::new(instruction.to_le_bytes().to_vec()); + + cpu.regs[rs1 as usize] = rs1_value; + cpu.regs[rs2 as usize] = rs2_value; + + let initial_pc = cpu.pc; + + cpu.step().unwrap(); + + let expected_pc = if taken { + initial_pc.wrapping_add(offset as u32) + } else { + initial_pc.wrapping_add(4) + }; + + assert_eq!( + cpu.pc, expected_pc, + "{name} failed: rs1={rs1_value:#010x}, \ + rs2={rs2_value:#010x}, offset={offset}, \ + expected_pc={expected_pc:#010x}, \ + got={:#010x}", + cpu.pc, ); } }