Skip to content

Commit e11d658

Browse files
committed
feat: implement programmable fetch/execute logic and BRAM-mapped IMEM
Refactors the Borg module from a single-step arithmetic unit into a minimal shading processor. Key changes include: - Instruction Memory: Replaced the single 'instr' register with an 8-word instruction memory (imem) mapped to 0x20-0x38. - Execution Control: Added a Program Counter (PC) and a control register (0x3C) to handle Start and Reset/Stop logic. - Status Polling: Implemented a Status Register (0x10) with a 'Halted' bit, allowing software to poll for execution completion. - RISC-V Style Decoding: Updated decoding logic to support a 'rd' (destination register) field, enabling flexible register file writes. - Test Updates: Synchronized Scala and Python test drivers to use the new memory-mapped I/O flow and polling-based execution.
1 parent e029118 commit e11d658

3 files changed

Lines changed: 175 additions & 69 deletions

File tree

borg/src/Borg.scala

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
package borg
55

66
import chisel3.*
7-
import chisel3.util.MuxLookup
7+
import chisel3.util.*
88

9+
/** BorgIO defines the interface for the shading processor. It uses
10+
* memory-mapped I/O for register and instruction memory access.
11+
*/
912
class BorgIO extends Bundle {
10-
val address = Input(UInt(6.W))
13+
val address = Input(
14+
UInt(6.W)
15+
) // 64-word address space (byte-addressed internally by shifting)
1116
val data_in = Input(UInt(32.W))
12-
val data_write_n = Input(UInt(2.W))
17+
val data_write_n = Input(UInt(2.W)) // 0b10 for write
1318
val data_read_n = Input(UInt(2.W))
1419
val data_out = Output(UInt(32.W))
1520
val data_ready = Output(Bool())
@@ -18,71 +23,124 @@ class BorgIO extends Bundle {
1823
val user_interrupt = Output(Bool())
1924
}
2025

26+
/** Borg is a minimal shading processor with instruction memory and a program
27+
* counter. It executes floating-point addition instructions in a 4-cycle
28+
* pipeline.
29+
*/
2130
class Borg extends Module {
2231
val io = IO(new BorgIO)
2332

24-
val rf = Reg(Vec(3, UInt(32.W)))
25-
val instr = RegInit(0.U(32.W))
33+
// --- Storage ---
34+
// registerFile: 4 general-purpose 32-bit registers for floating-point data
35+
val registerFile = Reg(Vec(4, UInt(32.W)))
2636

37+
// instructionMemory: 8 words of instruction memory to store the shader program
38+
val instructionMemory = Reg(Vec(8, UInt(32.W)))
39+
40+
// programCounter: Points to the current instruction in instructionMemory
41+
val programCounter = RegInit(0.U(3.W))
42+
43+
// currentInstruction: The instruction currently being decoded/executed
44+
val currentInstruction = RegInit(0.U(32.W))
45+
46+
// running: Status flag indicating if the processor is currently executing a program
47+
val running = RegInit(false.B)
48+
49+
// --- Pipeline Control ---
50+
// busy_counter: Tracks the 4-cycle execution stage of the current instruction
2751
val busy_counter = RegInit(0.U(3.W))
2852
val is_busy = busy_counter > 0.U
2953

3054
val is_writing = io.data_write_n === "b10".U
31-
val writing_instr = is_writing && io.address === 60.U
3255

33-
when(writing_instr) {
34-
busy_counter := 4.U // Start 4-cycle countdown
35-
instr := io.data_in
56+
// --- Memory-Mapped Write Logic ---
57+
when(is_writing) {
58+
when(io.address < 16.U) {
59+
// 0x00 - 0x0C: Register File (rf0, rf1, rf2, rf3)
60+
registerFile(io.address(3, 2)) := io.data_in
61+
}.elsewhen(io.address >= 32.U && io.address < 64.U) {
62+
when(io.address === 60.U) {
63+
// 0x3C (60): Control Register
64+
// Bit 0 = Start execution
65+
// Bit 1 = Reset PC and stop
66+
when(io.data_in(0)) { running := true.B }
67+
when(io.data_in(1)) { programCounter := 0.U; running := false.B }
68+
}.otherwise {
69+
// 0x20 - 0x38: Instruction Memory (8 slots)
70+
instructionMemory(io.address(4, 2)) := io.data_in
71+
}
72+
}
73+
}
74+
75+
// --- Fetch & Execute State Machine ---
76+
when(running && !is_busy) {
77+
// Fetch Stage: Load next instruction from memory
78+
currentInstruction := instructionMemory(programCounter)
79+
busy_counter := 4.U
3680
}.elsewhen(is_busy) {
81+
// Execution Stages: Counting down 4 cycles
3782
busy_counter := busy_counter - 1.U
38-
}
3983

40-
val funct7 = instr(31, 25)
41-
val rs2_idx = instr(24, 20) % 3.U
42-
val rs1_idx = instr(19, 15) % 3.U
84+
when(busy_counter === 1.U) {
85+
// End of execution: Increment PC
86+
programCounter := programCounter + 1.U
4387

44-
when(is_writing && !writing_instr) {
45-
when(io.address === 0.U) { rf(0) := io.data_in }
46-
.elsewhen(io.address === 4.U) { rf(1) := io.data_in }
47-
.elsewhen(io.address === 16.U) { rf(2) := io.data_in }
88+
// Stop execution if the next instruction is all zeros (HALT)
89+
// Note: This creates an "Implicit Halt" at the end of the program
90+
when(instructionMemory(programCounter + 1.U) === 0.U) {
91+
running := false.B
92+
}
93+
}
4894
}
4995

50-
val recA = recFNFromFN(8, 24, rf(rs1_idx))
51-
val recB = recFNFromFN(8, 24, rf(rs2_idx))
96+
// --- Instruction Decoding (RISC-V inspired) ---
97+
val rs2_idx = currentInstruction(24, 20)(1, 0)
98+
val rs1_idx = currentInstruction(19, 15)(1, 0)
99+
val rd_idx = currentInstruction(11, 7)(1, 0)
52100

53-
// --- Simplified Math: Adder Only ---
101+
// Floating Point Operands
102+
val recA = recFNFromFN(8, 24, registerFile(rs1_idx))
103+
val recB = recFNFromFN(8, 24, registerFile(rs2_idx))
104+
105+
// --- Arithmetic Logic Unit: Floating Point Adder ---
54106
val f_add = Module(new AddRecFN(8, 24))
55107
f_add.io.subOp := false.B
56108
f_add.io.a := recA
57109
f_add.io.b := recB
58110
f_add.io.roundingMode := 0.U
59111
f_add.io.detectTininess := 1.U
60112

61-
// --- Optimization: Multi-Stage Pipeline ---
113+
// --- Multi-Stage Pipeline ---
62114
val stage1_math_rec = Reg(UInt(33.W))
63-
val math_result_reg = RegInit(0.U(32.W))
64115

65116
// Stage 1: Capture (Cycle 2 of 4)
66117
when(busy_counter === 2.U) {
67118
stage1_math_rec := f_add.io.out
68119
}
69120

70-
// Stage 2: Final Conversion (Cycle 1 of 4)
121+
// Stage 2: Writeback (Cycle 1 of 4)
71122
when(busy_counter === 1.U) {
72-
math_result_reg := fNFromRecFN(8, 24, stage1_math_rec)
123+
registerFile(rd_idx) := fNFromRecFN(8, 24, stage1_math_rec)
73124
}
74125

126+
// --- Memory-Mapped Read Logic ---
75127
io.data_out := MuxLookup(io.address, 0.U)(
76128
Seq(
77-
0.U -> rf(0),
78-
4.U -> rf(1),
79-
16.U -> rf(2),
80-
8.U -> math_result_reg,
81-
60.U -> instr
129+
0.U -> registerFile(0),
130+
4.U -> registerFile(1),
131+
8.U -> registerFile(2),
132+
12.U -> registerFile(3),
133+
16.U -> Cat(
134+
0.U(30.W),
135+
!running,
136+
0.U(1.W)
137+
), // Status Register: [Halted, _]
138+
60.U -> currentInstruction
82139
)
83140
)
84-
85-
io.data_ready := !is_busy && !writing_instr
141+
// Memory bus reads from peripheral registers take 1 cycle.
142+
// We rely on software polling `status[1]` (Halted) to avoid reading `res` while busy.
143+
io.data_ready := true.B
86144

87145
io.uo_out := 0.U
88146
io.user_interrupt := false.B

borg/test/src/BorgTests.scala

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,46 @@ object BorgTests extends TestSuite {
3030

3131
def readAddr(borg: Borg, addr: Int): Float = {
3232
borg.io.address.poke(addr.U)
33-
val res = bitsToFloat(borg.io.data_out.peek().litValue)
34-
borg.clock.step(1)
35-
res
33+
bitsToFloat(borg.io.data_out.peek().litValue)
3634
}
3735

3836
def runBasicMathTest(borg: Borg, a: Float, b: Float, epsilon: Float): Unit = {
39-
// 1. Load operands into registers 0 and 1
37+
// 1. Reset PC and stop execution
38+
writeAddr(borg, 60, 2) // Bit 1 = Reset PC
39+
40+
// 2. Load operands into registers 0 and 1
4041
writeAddr(borg, 0, floatToBits(a))
4142
writeAddr(borg, 4, floatToBits(b))
4243

43-
// 2. Setup Addition Instruction
44-
// rs1 = reg0, rs2 = reg1, funct7 = 0x00 (Add)
45-
val add_instr = (0x00 << 25) | (1 << 20) | (0 << 15)
46-
writeAddr(borg, 60, BigInt(add_instr))
44+
// 3. Setup Addition Instruction in imem(0)
45+
// opcode/funct7 = 0x00 (Add), rs1 = reg0, rs2 = reg1, rd = reg2
46+
// RISC-V like format: funct7(7) | rs2(5) | rs1(5) | funct3(3) | rd(5) | opcode(7)
47+
// We only use funct7, rs2, rs1, rd.
48+
val rd = 2
49+
val rs1 = 0
50+
val rs2 = 1
51+
val add_instr = (0x00 << 25) | (rs2 << 20) | (rs1 << 15) | (rd << 7)
52+
writeAddr(borg, 32, BigInt(add_instr)) // imem(0)
53+
54+
// Halt instruction (zero) in imem(1)
55+
writeAddr(borg, 36, 0)
4756

48-
// 3. Wait for Hardware Pipeline
49-
while (!borg.io.data_ready.peek().litToBoolean) {
57+
// 4. Start execution
58+
writeAddr(borg, 60, 1) // Bit 0 = Start
59+
60+
// 5. Wait for Halted bit (status address 16, bit 1)
61+
var status: BigInt = 0
62+
do {
63+
borg.io.address.poke(16.U)
64+
status = borg.io.data_out.peek().litValue
5065
borg.clock.step(1)
51-
}
52-
53-
// 4. Read result from math_result register (addr 8)
66+
} while ((status & 2) == 0)
67+
68+
// 6. Read result from rf(2) (addr 8)
5469
val addActual = readAddr(borg, 8)
5570
val expectedSum = a + b
5671

57-
// 5. Report results to console
72+
// 7. Report results to console
5873
println(
5974
f"Check: $a%8.2f + $b%8.2f -> Actual: $addActual%8.2f (Exp: $expectedSum%8.2f)"
6075
)

test/test.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,42 @@ def load_test_data():
2828
class BorgDriver:
2929
def __init__(self, tqv):
3030
self.tqv = tqv
31-
# Address Map matches the simplified Borg.scala
32-
self.ADDR_A = 0
33-
self.ADDR_B = 4
34-
self.ADDR_RESULT = 8
35-
self.ADDR_INSTR = 60
31+
# Address Map matches Borg.scala
32+
self.ADDR_STATUS = 16
33+
self.ADDR_IMEM = 32
34+
self.ADDR_CONTROL = 60
3635

3736
async def write_reg(self, reg_idx, val_float):
37+
# Register file at 0x00 - 0x0C
3838
addr = reg_idx * 4
3939
bits = float_to_bits(np.float32(val_float))
4040
await self.tqv.write_word_reg(addr, bits)
4141

42-
async def write_instr(self, instr_bits):
43-
await self.tqv.write_word_reg(self.ADDR_INSTR, instr_bits)
44-
45-
async def read_result(self):
46-
# read_word_reg handles the polling of data_ready internally
47-
bits = await self.tqv.read_word_reg(self.ADDR_RESULT)
42+
async def write_imem(self, idx, instr_bits):
43+
# Instruction memory at 0x20 - 0x38
44+
addr = self.ADDR_IMEM + (idx * 4)
45+
await self.tqv.write_word_reg(addr, instr_bits)
46+
47+
async def start_execution(self, reset_pc=False):
48+
# Control register at 0x3C (60)
49+
# Bit 0 = Start, Bit 1 = Reset PC
50+
val = 1
51+
if reset_pc:
52+
val |= 2
53+
await self.tqv.write_word_reg(self.ADDR_CONTROL, val)
54+
55+
async def wait_for_halt(self):
56+
# Status register at 0x10 (16)
57+
# Bit 1 = Halted
58+
while True:
59+
status = await self.tqv.read_word_reg(self.ADDR_STATUS)
60+
if status & 2:
61+
break
62+
await cocotb.triggers.Timer(100, units="ns")
63+
64+
async def read_register(self, reg_idx):
65+
addr = reg_idx * 4
66+
bits = await self.tqv.read_word_reg(addr)
4867
return bits_to_float(bits)
4968

5069
async def reset(self):
@@ -55,25 +74,39 @@ async def run_basic_math_test(dut, driver, a, b, epsilon):
5574
a_32 = np.float32(a)
5675
b_32 = np.float32(b)
5776

58-
# 1. Upload Data
77+
# 1. Reset PC and stop execution
78+
await driver.start_execution(reset_pc=True)
79+
80+
# 2. Upload Data to registers 0 and 1
5981
await driver.write_reg(0, a_32)
6082
await driver.write_reg(1, b_32)
6183

62-
# 2. Execute ADD: funct7=0x00, rs2=1, rs1=0
63-
# In the current Borg.scala, any instr with busy_counter triggers the adder
64-
instr_add = (0x00 << 25) | (1 << 20) | (0 << 15)
65-
await driver.write_instr(instr_add)
66-
67-
# 3. Read Result (Wait for 4-cycle pipeline)
68-
add_res = await driver.read_result()
84+
# 3. Setup Addition Instruction in imem(0)
85+
# funct7=0x00 (Add), rs2=1, rs1=0, rd=2
86+
instr_add = (0x00 << 25) | (1 << 20) | (0 << 15) | (2 << 7)
87+
await driver.write_imem(0, instr_add)
88+
89+
# Halt instruction (zero) in imem(1)
90+
await driver.write_imem(1, 0)
91+
92+
# 4. Start execution
93+
await driver.start_execution()
94+
95+
# 5. Wait for Halted status
96+
await driver.wait_for_halt()
97+
98+
# 6. Read Result from register 2
99+
add_res = await driver.read_register(2)
69100

70-
# 4. Assertions
101+
# 7. Assertions
71102
expected_add = a_32 + b_32
72103

73-
assert abs(add_res - expected_add) < epsilon, f"Add failed: {a_32}+{b_32}={add_res} (Exp: {expected_add})"
104+
assert (
105+
abs(add_res - expected_add) < epsilon
106+
), f"Add failed: {a_32}+{b_32}={add_res} (Exp: {expected_add})"
74107

75108
dut._log.info(
76-
f"Checked Adder: {a_32:8.2f} + {b_32:8.2f} -> Result: {add_res:8.2f}"
109+
f"Checked Shader Adder: {a_32:8.2f} + {b_32:8.2f} -> Result: {add_res:8.2f}"
77110
)
78111

79112

@@ -82,7 +115,7 @@ async def run_basic_math_test(dut, driver, a, b, epsilon):
82115

83116
@cocotb.test()
84117
async def test_borg_vulkan_style_math(dut):
85-
dut._log.info("Starting Single-Port Programmable Borg Adder Test")
118+
dut._log.info("Starting Programmable Borg Shading Processor Test")
86119

87120
test_data = load_test_data()
88121
epsilon = test_data["epsilon"]
@@ -97,4 +130,4 @@ async def test_borg_vulkan_style_math(dut):
97130
for a, b in test_data["pairs"]:
98131
await run_basic_math_test(dut, driver, a, b, epsilon)
99132

100-
dut._log.info("All Borg Adder Tests Passed!")
133+
dut._log.info("All Borg Shading Processor Tests Passed!")

0 commit comments

Comments
 (0)