Bazelizing a Custom RISC CPU with 4x4 Systolic Matrix Accelerator
Open-source digital signal processing and machine learning accelerators are frequently published as student or research projects. While the RTL architecture is often innovative, the surrounding build and test scaffolding frequently suffers from workstation-specific assumptions, absolute file paths, and missing simulation fixtures.
In this case study, we document the Bazelization of Custom-RISC-CPU-with-Systolic-Matrix-Accelerator, authored by Mathew George. By applying Bazel and rules_openxc7, we transformed this 2D systolic array processor into a portable, reproducible hardware project that synthesizes and outputs a valid FPGA bitstream in under 25 seconds from a clean slate.
Architecture Overview
The design integrates a custom 32-bit pipelined RISC core with a dedicated 2D systolic coprocessor:
- Pipelined RISC Core: Implements standard fetch, decode, execute, memory, and writeback pipeline stages with hazard detection and forwarding.
- 4x4 Systolic Array (16 Processing Elements): A grid of 16 multiply-accumulate (MAC) processing elements optimized for matrix-matrix multiplication and 2D convolution/filtering operations.
- Dual Memory Architecture: Separate instruction memory and data memory banks with direct memory-mapped coprocessor interfaces.
Portability Challenges in Upstream RTL
When testing the original repository, we identified three common friction points that prevented it from building on standard machines:
1. Hardcoded Absolute Workstation Paths
In instruction_memory.v and data_memory.v, the memory arrays were loaded using $readmemh with absolute file system paths specific to the author’s local workstation:
// Original upstream code in instruction_memory.v:
initial begin
$readmemh("/home/mathew/Pixel_Processor_SystolicArray/program.hex", memory);
end
Any clone on a different computer, CI runner, or cloud development container immediately failed at synthesis and simulation because the directory /home/mathew/... did not exist.
2. Missing Firmware and Test Fixtures
The required .hex image files (program.hex and image_data2.hex) were not committed to the repository, leaving the memory contents uninitialized.
3. Lack of Physical Board Integration
The project lacked top-level I/O pin mappings, board clock buffers, and constraint files (.xdc) for physical deployment onto an evaluation board.
The Bazelization Solution
1. Parameterized Memory Modules
Instead of hardcoded file system paths, we refactored instruction_memory and data_memory to accept a parameterized memory file path:
module instruction_memory #(
parameter MEM_FILE = ""
) (
input wire clk,
input wire [31:0] address,
output reg [31:0] instruction
);
reg [31:0] memory [0:1023];
initial begin
if (MEM_FILE != "") begin
$readmemh(MEM_FILE, memory);
end
end
...
By making MEM_FILE a Verilog parameter, the top-level testbench or board wrapper can supply any application binary at build time without modifying RTL source code.
2. Decoupled Test Fixtures
We created clean, minimal verification fixtures in fixtures/:
fixtures/program.hex: Contains test instructions verifying the pipeline and systolic coprocessor kick-off sequence.fixtures/image_data2.hex: Contains sample matrix operands for the 4x4 systolic processing elements.
In BUILD.bazel, these fixtures are declared as explicit data dependencies:
filegroup(
name = "fixtures",
srcs = glob(["fixtures/*.hex"]),
visibility = ["//visibility:public"],
)
In Bazel’s hermetic sandbox, inputs are explicitly tracked, preventing accidental leakage of host state.
3. Digilent Arty A7-100T Board Integration
To enable physical demonstration, we implemented an FPGA board wrapper (arty_top.v) and timing constraint file (arty_a7_100t.xdc):
- Clock & Reset: Bridges the Arty A7’s 100 MHz oscillator (
E3) and active-low pushbutton reset (C2) through internal clock dividers and reset conditioners. - Status LEDs: Maps CPU stall flags, coprocessor active signals, and memory write-enable strobes to the on-board green LEDs (
H5,J5,T9,T10). - Pin Constraints: Fully specified XDC pinout and IOSTANDARD declarations for the Xilinx Artix-7 100T (
xc7a100tcsg324-1).
Lightning-Fast Open-Source Builds
Using rules_openxc7 with Yosys and nextpnr-xilinx on eph1, once toolchain dependencies are fetched, running a complete clean rebuild without remote caching (bazel clean && time bazel build //:top_pnr --noremote_accept_cached) executes all 30 synthesis, placement, routing, and bitstream generation actions in only 24.7 seconds (critical path: 15.5 seconds). Subsequent incremental runs with action caching complete in under 2 seconds:
# 1. Synthesize RTL with Yosys (synth_xilinx)
bazel build //:top_synth
# 2. Place and route with nextpnr-xilinx, generating top_pnr.bit
bazel build //:top_pnr
# 3. Flash to the Arty A7-100T board
bazel run //:top_prog
Because Bazel caches intermediate action outputs, subsequent runs after minor constraint or pin adjustments complete near-instantaneously.
Upstream Contribution
We contributed the entire Bazel build system, parameterization refactoring, and Arty A7-100T board wrapper back to the author in an open pull request:
- Pull Request: filmil/Custom-RISC-CPU-with-Systolic-Matrix-Accelerator PR #1
- Upstream Project: MathewGeorgeDev/Custom-RISC-CPU-with-Systolic-Matrix-Accelerator by Mathew George
This contribution ensures that anyone studying or expanding on Mathew George’s systolic array architecture can immediately clone the repository, run tests, and generate working FPGA bitstreams without struggling with vendor tooling or broken local paths.
To learn more about modern hardware build engineering, check out our guide Hardware Development with Bazel.