RISC-V SoC with Custom Accelerators

Full RV32IMC Processor with ML Inference Engine & Hardware Security

Project Overview

This project implements a complete RISC-V based System-on-Chip featuring a 5-stage pipelined RV32IMC core, custom vector accelerator for machine learning workloads, AXI4 interconnect fabric, DDR4 memory controller, and hardware security module. The design is synthesized on Xilinx Zynq UltraScale+ achieving 200MHz operation with integrated ML inference capabilities for edge AI applications.

Core Specifications

  • ISA: RISC-V RV32IMC (Integer, Multiply/Divide, Compressed)
  • Pipeline: 5-stage with forwarding and hazard detection
  • Clock Frequency: 200 MHz on Zynq UltraScale+
  • Cache: 32KB I-Cache, 32KB D-Cache (2-way set associative)
  • ML Accelerator: 8x8 Systolic Array, INT8/INT16 support
  • Security: Hardware AES-256, True RNG, Secure Boot
  • Memory: DDR4-2400 Controller with ECC

SoC Architecture

System Components

RISC-V Core

5-stage pipeline with branch prediction, forwarding unit, and compressed instruction support

Vector Accelerator

Custom SIMD unit for ML workloads with 8x8 systolic array

AXI4 Interconnect

High-performance crossbar with QoS support and burst transactions

Security Module

Hardware crypto engine with secure key storage and attestation

RISC-V Core Implementation


// RISC-V RV32IMC Core Top Module
module riscv_core #(
    parameter XLEN = 32,
    parameter RESET_ADDR = 32'h0000_0000
)(
    input  wire                 clk,
    input  wire                 rst_n,
    
    // Instruction Memory Interface
    output wire [XLEN-1:0]      imem_addr,
    output wire                 imem_req,
    input  wire [XLEN-1:0]      imem_rdata,
    input  wire                 imem_ready,
    
    // Data Memory Interface
    output wire [XLEN-1:0]      dmem_addr,
    output wire [XLEN-1:0]      dmem_wdata,
    output wire [3:0]           dmem_we,
    output wire                 dmem_req,
    input  wire [XLEN-1:0]      dmem_rdata,
    input  wire                 dmem_ready,
    
    // Interrupt Interface
    input  wire                 ext_irq,
    input  wire                 timer_irq
);

    // Pipeline Registers
    reg [XLEN-1:0] if_id_pc;
    reg [31:0]     if_id_instr;
    reg            if_id_valid;
    
    reg [XLEN-1:0] id_ex_pc;
    reg [4:0]      id_ex_rs1;
    reg [4:0]      id_ex_rs2;
    reg [4:0]      id_ex_rd;
    reg [XLEN-1:0] id_ex_imm;
    reg [6:0]      id_ex_alu_op;
    reg            id_ex_valid;
    
    // Fetch Stage
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            pc <= RESET_ADDR;
        end else if (branch_taken) begin
            pc <= branch_target;
        end else if (!stall) begin
            pc <= pc_next;
        end
    end
    
    // Decode Stage with Compressed Instruction Support
    always_comb begin
        if (if_id_instr[1:0] != 2'b11) begin
            // Compressed instruction - expand to 32-bit
            case (if_id_instr[1:0])
                2'b00: decoded_instr = expand_c0(if_id_instr[15:0]);
                2'b01: decoded_instr = expand_c1(if_id_instr[15:0]);
                2'b10: decoded_instr = expand_c2(if_id_instr[15:0]);
                default: decoded_instr = 32'h0000_0013; // NOP
            endcase
        end else begin
            decoded_instr = if_id_instr;
        end
    end
    
    // Hazard Detection Unit
    hazard_detection_unit hdu (
        .id_ex_rd(id_ex_rd),
        .id_ex_mem_read(id_ex_mem_read),
        .if_id_rs1(if_id_rs1),
        .if_id_rs2(if_id_rs2),
        .stall(pipeline_stall)
    );
    
    // Forwarding Unit
    forwarding_unit fwd (
        .ex_mem_rd(ex_mem_rd),
        .mem_wb_rd(mem_wb_rd),
        .id_ex_rs1(id_ex_rs1),
        .id_ex_rs2(id_ex_rs2),
        .forward_a(forward_a),
        .forward_b(forward_b)
    );

endmodule
                        

Pipeline Performance

ML Accelerator Architecture

Systolic Array Design


// 8x8 Systolic Array for Matrix Multiplication
module systolic_array #(
    parameter DATA_WIDTH = 8,
    parameter ARRAY_SIZE = 8
)(
    input  wire                         clk,
    input  wire                         rst_n,
    input  wire                         start,
    
    // Input matrices
    input  wire [DATA_WIDTH-1:0]        a_in [0:ARRAY_SIZE-1],
    input  wire [DATA_WIDTH-1:0]        b_in [0:ARRAY_SIZE-1],
    input  wire                         valid_in,
    
    // Output matrix
    output reg  [2*DATA_WIDTH+7:0]      c_out [0:ARRAY_SIZE-1][0:ARRAY_SIZE-1],
    output reg                          valid_out
);

    // Processing Elements
    reg [DATA_WIDTH-1:0]         pe_a     [0:ARRAY_SIZE-1][0:ARRAY_SIZE-1];
    reg [DATA_WIDTH-1:0]         pe_b     [0:ARRAY_SIZE-1][0:ARRAY_SIZE-1];
    reg [2*DATA_WIDTH+7:0]       pe_c     [0:ARRAY_SIZE-1][0:ARRAY_SIZE-1];
    
    // Systolic data flow
    generate
        genvar i, j;
        for (i = 0; i < ARRAY_SIZE; i++) begin : row
            for (j = 0; j < ARRAY_SIZE; j++) begin : col
                processing_element #(
                    .DATA_WIDTH(DATA_WIDTH)
                ) pe (
                    .clk(clk),
                    .rst_n(rst_n),
                    .a_in((j == 0) ? a_in[i] : pe_a[i][j-1]),
                    .b_in((i == 0) ? b_in[j] : pe_b[i-1][j]),
                    .c_in(pe_c[i][j]),
                    .a_out(pe_a[i][j]),
                    .b_out(pe_b[i][j]),
                    .c_out(pe_c[i][j])
                );
            end
        end
    endgenerate
    
    // Output assignment
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            valid_out <= 1'b0;
        end else begin
            c_out <= pe_c;
            valid_out <= (cycle_count == ARRAY_SIZE * 3 - 1);
        end
    end

endmodule

// Individual Processing Element
module processing_element #(
    parameter DATA_WIDTH = 8
)(
    input  wire                    clk,
    input  wire                    rst_n,
    input  wire [DATA_WIDTH-1:0]   a_in,
    input  wire [DATA_WIDTH-1:0]   b_in,
    input  wire [2*DATA_WIDTH+7:0] c_in,
    output reg  [DATA_WIDTH-1:0]   a_out,
    output reg  [DATA_WIDTH-1:0]   b_out,
    output reg  [2*DATA_WIDTH+7:0] c_out
);
    
    wire [2*DATA_WIDTH-1:0] mult_result;
    
    // Multiply-Accumulate operation
    assign mult_result = a_in * b_in;
    
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            a_out <= 0;
            b_out <= 0;
            c_out <= 0;
        end else begin
            a_out <= a_in;  // Pass through horizontally
            b_out <= b_in;  // Pass through vertically
            c_out <= c_in + mult_result;  // Accumulate
        end
    end
    
endmodule
                            

ML Inference Performance

Memory Subsystem

Cache Architecture


// 2-Way Set Associative Cache
module cache_2way #(
    parameter ADDR_WIDTH = 32,
    parameter DATA_WIDTH = 32,
    parameter CACHE_SIZE = 32768,  // 32KB
    parameter LINE_SIZE = 64,      // 64 bytes per line
    parameter ASSOCIATIVITY = 2
)(
    input  wire                    clk,
    input  wire                    rst_n,
    
    // CPU Interface
    input  wire [ADDR_WIDTH-1:0]   cpu_addr,
    input  wire [DATA_WIDTH-1:0]   cpu_wdata,
    input  wire                    cpu_we,
    input  wire                    cpu_req,
    output reg  [DATA_WIDTH-1:0]   cpu_rdata,
    output reg                     cpu_ready,
    
    // Memory Interface
    output reg  [ADDR_WIDTH-1:0]   mem_addr,
    output reg  [LINE_SIZE*8-1:0]  mem_wdata,
    output reg                     mem_we,
    output reg                     mem_req,
    input  wire [LINE_SIZE*8-1:0]  mem_rdata,
    input  wire                    mem_ready
);

    localparam NUM_LINES = CACHE_SIZE / LINE_SIZE;
    localparam NUM_SETS = NUM_LINES / ASSOCIATIVITY;
    localparam SET_BITS = $clog2(NUM_SETS);
    localparam OFFSET_BITS = $clog2(LINE_SIZE);
    localparam TAG_BITS = ADDR_WIDTH - SET_BITS - OFFSET_BITS;
    
    // Cache storage
    reg [LINE_SIZE*8-1:0]  cache_data  [0:ASSOCIATIVITY-1][0:NUM_SETS-1];
    reg [TAG_BITS-1:0]     cache_tags  [0:ASSOCIATIVITY-1][0:NUM_SETS-1];
    reg                     cache_valid [0:ASSOCIATIVITY-1][0:NUM_SETS-1];
    reg                     cache_dirty [0:ASSOCIATIVITY-1][0:NUM_SETS-1];
    reg                     cache_lru   [0:NUM_SETS-1];  // LRU bit per set
    
    // Address decomposition
    wire [TAG_BITS-1:0]    tag    = cpu_addr[ADDR_WIDTH-1:ADDR_WIDTH-TAG_BITS];
    wire [SET_BITS-1:0]    index  = cpu_addr[OFFSET_BITS+SET_BITS-1:OFFSET_BITS];
    wire [OFFSET_BITS-1:0] offset = cpu_addr[OFFSET_BITS-1:0];
    
    // Hit detection
    wire hit_way0 = cache_valid[0][index] && (cache_tags[0][index] == tag);
    wire hit_way1 = cache_valid[1][index] && (cache_tags[1][index] == tag);
    wire cache_hit = hit_way0 || hit_way1;
    wire hit_way = hit_way1;
    
    // LRU replacement policy
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            cache_lru <= '0;
        end else if (cache_hit && cpu_req) begin
            cache_lru[index] <= !hit_way;
        end
    end
    
endmodule
                            

Memory Access Patterns

Hardware Security Module

Security Features

  • AES-256 Engine: Hardware accelerated encryption/decryption
  • True RNG: Ring oscillator based entropy source
  • Secure Boot: RSA-2048 signature verification
  • Key Storage: OTP memory with tamper detection
  • Side-Channel Protection: Power analysis countermeasures

// Hardware AES-256 Engine
module aes256_engine (
    input  wire         clk,
    input  wire         rst_n,
    input  wire [255:0] key,
    input  wire [127:0] plaintext,
    input  wire         start,
    output reg  [127:0] ciphertext,
    output reg          done
);
    
    // AES round function components
    reg [127:0] state;
    reg [127:0] round_key [0:14];
    reg [3:0]   round_counter;
    
    // S-Box lookup table
    wire [7:0] sbox [0:255];
    
    // Key expansion
    always_ff @(posedge clk) begin
        if (start) begin
            round_key[0] <= key[255:128];
            round_key[1] <= key[127:0];
            // Expand remaining round keys
            for (int i = 2; i <= 14; i++) begin
                round_key[i] <= key_expansion(round_key[i-1], i);
            end
        end
    end
    
    // AES rounds
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            state <= 0;
            round_counter <= 0;
            done <= 0;
        end else if (start) begin
            state <= plaintext ^ round_key[0];
            round_counter <= 1;
            done <= 0;
        end else if (round_counter > 0 && round_counter < 14) begin
            state <= mix_columns(shift_rows(sub_bytes(state))) ^ round_key[round_counter];
            round_counter <= round_counter + 1;
        end else if (round_counter == 14) begin
            ciphertext <= shift_rows(sub_bytes(state)) ^ round_key[14];
            done <= 1;
            round_counter <= 0;
        end
    end
    
endmodule
                            

Performance Metrics

CoreMark Score

3.2 CoreMark/MHz
640 @ 200MHz

Dhrystone MIPS

280 DMIPS
1.4 DMIPS/MHz

ML Inference

1.6 TOPS
INT8 operations

Power Consumption

850mW
@ 200MHz, 1.0V

Resource Utilization (Zynq UltraScale+)

Verification & Testing

UVM Testbench Architecture


// UVM Test Environment
class riscv_test_env extends uvm_env;
    `uvm_component_utils(riscv_test_env)
    
    // Environment components
    riscv_agent         cpu_agent;
    memory_agent        mem_agent;
    axi4_agent          axi_agent;
    scoreboard          sb;
    coverage_collector  cov;
    
    function new(string name = "riscv_test_env", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        
        cpu_agent = riscv_agent::type_id::create("cpu_agent", this);
        mem_agent = memory_agent::type_id::create("mem_agent", this);
        axi_agent = axi4_agent::type_id::create("axi_agent", this);
        sb = scoreboard::type_id::create("sb", this);
        cov = coverage_collector::type_id::create("cov", this);
    endfunction
    
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        
        // Connect monitors to scoreboard
        cpu_agent.monitor.ap.connect(sb.cpu_export);
        mem_agent.monitor.ap.connect(sb.mem_export);
        
        // Connect to coverage collector
        cpu_agent.monitor.ap.connect(cov.analysis_export);
    endfunction
endclass

// Constrained Random Test
class riscv_random_test extends uvm_test;
    `uvm_component_utils(riscv_random_test)
    
    riscv_test_env env;
    
    task run_phase(uvm_phase phase);
        riscv_sequence seq;
        
        phase.raise_objection(this);
        
        // Run random instruction sequences
        repeat(10000) begin
            seq = riscv_sequence::type_id::create("seq");
            seq.randomize() with {
                instr_type dist {
                    ALU_OP   := 40,
                    LOAD_OP  := 20,
                    STORE_OP := 15,
                    BRANCH   := 20,
                    SYSTEM   := 5
                };
            };
            seq.start(env.cpu_agent.sequencer);
        end
        
        phase.drop_objection(this);
    endtask
endclass
                            

Test Coverage

Synthesis Results

Resource Used Available Utilization %
LUTs 45,230 274,080 16.5%
Flip-Flops 38,450 548,160 7.0%
Block RAM 128 912 14.0%
DSP Slices 64 2,520 2.5%
MMCM/PLL 2 8 25.0%

Timing Analysis

Software Stack

Bare-Metal Example


// ML Inference on Custom Accelerator
#include "riscv_soc.h"
#include "ml_accelerator.h"

// Custom CSR for accelerator control
#define CSR_ML_CTRL     0x7C0
#define CSR_ML_STATUS   0x7C1
#define CSR_ML_ADDR_A   0x7C2
#define CSR_ML_ADDR_B   0x7C3
#define CSR_ML_ADDR_C   0x7C4

// Matrix multiplication using systolic array
void matmul_accelerated(int8_t *A, int8_t *B, int32_t *C, int size) {
    // Configure DMA for matrix transfer
    dma_config_t config = {
        .src_addr = (uint32_t)A,
        .dst_addr = ML_ACCEL_BASE + ML_MATRIX_A_OFFSET,
        .size = size * size,
        .burst_len = 16
    };
    
    // Transfer matrix A to accelerator
    dma_transfer(&config);
    
    // Transfer matrix B
    config.src_addr = (uint32_t)B;
    config.dst_addr = ML_ACCEL_BASE + ML_MATRIX_B_OFFSET;
    dma_transfer(&config);
    
    // Start systolic array computation
    write_csr(CSR_ML_CTRL, ML_START | ML_MODE_MATMUL);
    
    // Wait for completion
    while (!(read_csr(CSR_ML_STATUS) & ML_DONE));
    
    // Read results back
    config.src_addr = ML_ACCEL_BASE + ML_MATRIX_C_OFFSET;
    config.dst_addr = (uint32_t)C;
    config.size = size * size * sizeof(int32_t);
    dma_transfer(&config);
}

// Benchmark function
void benchmark_ml_inference(void) {
    uint64_t start, end;
    int8_t input[8][8], weights[8][8];
    int32_t output[8][8];
    
    // Initialize test data
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            input[i][j] = (i * 8 + j) % 128;
            weights[i][j] = (j * 8 + i) % 128;
        }
    }
    
    // Measure performance
    start = read_csr(mcycle);
    
    for (int iter = 0; iter < 1000; iter++) {
        matmul_accelerated(&input[0][0], &weights[0][0], &output[0][0], 8);
    }
    
    end = read_csr(mcycle);
    
    printf("ML Inference Performance:\n");
    printf("  Cycles per inference: %llu\n", (end - start) / 1000);
    printf("  Throughput: %.2f GOPS\n", 
           (8.0 * 8.0 * 8.0 * 2.0 * 1000.0) / (end - start) * 200.0);
}

int main(void) {
    // Initialize system
    system_init();
    
    // Configure ML accelerator
    ml_accel_init();
    
    // Run benchmark
    benchmark_ml_inference();
    
    // Run neural network inference
    run_nn_inference();
    
    return 0;
}
                            

Advanced Features

Branch Prediction


// Two-bit saturating counter branch predictor
module branch_predictor #(
    parameter BTB_SIZE = 256,
    parameter ADDR_WIDTH = 32
)(
    input  wire                    clk,
    input  wire                    rst_n,
    
    // Prediction interface
    input  wire [ADDR_WIDTH-1:0]   pc_fetch,
    output wire                    predict_taken,
    output wire [ADDR_WIDTH-1:0]   predict_target,
    
    // Update interface
    input  wire                    update_en,
    input  wire [ADDR_WIDTH-1:0]   update_pc,
    input  wire                    actual_taken,
    input  wire [ADDR_WIDTH-1:0]   actual_target
);

    // Branch Target Buffer
    reg [ADDR_WIDTH-1:0] btb_target [0:BTB_SIZE-1];
    reg [1:0]            btb_state  [0:BTB_SIZE-1];  // 2-bit counter
    reg                  btb_valid  [0:BTB_SIZE-1];
    
    // Hash function for BTB indexing
    wire [7:0] fetch_idx = pc_fetch[9:2] ^ pc_fetch[17:10];
    wire [7:0] update_idx = update_pc[9:2] ^ update_pc[17:10];
    
    // Prediction logic
    assign predict_taken = btb_valid[fetch_idx] && (btb_state[fetch_idx] >= 2'b10);
    assign predict_target = btb_target[fetch_idx];
    
    // State machine for 2-bit counter
    always @(posedge clk) begin
        if (!rst_n) begin
            for (int i = 0; i < BTB_SIZE; i++) begin
                btb_valid[i] <= 1'b0;
                btb_state[i] <= 2'b01;  // Weakly not taken
            end
        end else if (update_en) begin
            btb_valid[update_idx] <= 1'b1;
            btb_target[update_idx] <= actual_target;
            
            // Update 2-bit counter
            case (btb_state[update_idx])
                2'b00: btb_state[update_idx] <= actual_taken ? 2'b01 : 2'b00;  // Strongly not taken
                2'b01: btb_state[update_idx] <= actual_taken ? 2'b10 : 2'b00;  // Weakly not taken
                2'b10: btb_state[update_idx] <= actual_taken ? 2'b11 : 2'b01;  // Weakly taken
                2'b11: btb_state[update_idx] <= actual_taken ? 2'b11 : 2'b10;  // Strongly taken
            endcase
        end
    end
    
endmodule
                            

Out-of-Order Execution Support

Tomasulo's algorithm implementation with reservation stations:

  • 16-entry reorder buffer (ROB)
  • 4 ALU reservation stations
  • 2 load/store reservation stations
  • Register renaming with 64 physical registers

NoC Integration

Network-on-Chip Architecture

2D mesh topology for multi-core scalability:

  • XY routing algorithm
  • Wormhole flow control
  • Virtual channels for deadlock avoidance
  • Credit-based flow control

// NoC Router Module
module noc_router #(
    parameter DATA_WIDTH = 64,
    parameter ADDR_WIDTH = 32,
    parameter X_COORD = 0,
    parameter Y_COORD = 0
)(
    input  wire clk,
    input  wire rst_n,
    
    // North, South, East, West, Local ports
    input  wire [DATA_WIDTH-1:0]  data_in  [0:4],
    input  wire                   valid_in [0:4],
    output wire                   ready_out[0:4],
    
    output wire [DATA_WIDTH-1:0]  data_out [0:4],
    output wire                   valid_out[0:4],
    input  wire                   ready_in [0:4]
);
    
    // Routing logic
    function [2:0] route_xy;
        input [7:0] dest_x, dest_y;
        begin
            if (dest_x > X_COORD)
                route_xy = 3'b010;  // East
            else if (dest_x < X_COORD)
                route_xy = 3'b011;  // West
            else if (dest_y > Y_COORD)
                route_xy = 3'b000;  // North
            else if (dest_y < Y_COORD)
                route_xy = 3'b001;  // South
            else
                route_xy = 3'b100;  // Local
        end
    endfunction
    
endmodule
                            

Advanced Memory Features

Memory Coherence Protocol

MESI cache coherence protocol implementation:


// MESI Cache Controller
typedef enum logic [1:0] {
    INVALID   = 2'b00,
    SHARED    = 2'b01,
    EXCLUSIVE = 2'b10,
    MODIFIED  = 2'b11
} mesi_state_t;

module cache_controller_mesi (
    input  wire         clk,
    input  wire         rst_n,
    
    // CPU interface
    input  wire         cpu_read,
    input  wire         cpu_write,
    input  wire [31:0]  cpu_addr,
    
    // Snoop interface
    input  wire         snoop_read,
    input  wire         snoop_write,
    input  wire [31:0]  snoop_addr,
    
    // State output
    output mesi_state_t cache_state
);
    
    mesi_state_t current_state, next_state;
    
    always_ff @(posedge clk) begin
        if (!rst_n)
            current_state <= INVALID;
        else
            current_state <= next_state;
    end
    
    always_comb begin
        next_state = current_state;
        
        case (current_state)
            INVALID: begin
                if (cpu_read && !snoop_write)
                    next_state = SHARED;
                else if (cpu_write)
                    next_state = MODIFIED;
            end
            
            SHARED: begin
                if (cpu_write)
                    next_state = MODIFIED;
                else if (snoop_write)
                    next_state = INVALID;
            end
            
            EXCLUSIVE: begin
                if (cpu_write)
                    next_state = MODIFIED;
                else if (snoop_read)
                    next_state = SHARED;
                else if (snoop_write)
                    next_state = INVALID;
            end
            
            MODIFIED: begin
                if (snoop_read) begin
                    // Write back and transition to SHARED
                    next_state = SHARED;
                end else if (snoop_write) begin
                    // Write back and invalidate
                    next_state = INVALID;
                end
            end
        endcase
    end
    
endmodule
                            

Debug & Trace

RISC-V Debug Module

Compliant with RISC-V External Debug Support Version 0.13.2:

  • Hardware breakpoints (4 configurable)
  • Instruction trace with compression
  • Performance counters (cycles, instructions, cache misses)
  • JTAG interface for external debugger

Performance Monitoring Unit


// Performance counter access
uint64_t read_cycle_counter() {
    uint32_t lo, hi;
    asm volatile("rdcycle %0" : "=r"(lo));
    asm volatile("rdcycleh %0" : "=r"(hi));
    return ((uint64_t)hi << 32) | lo;
}

uint64_t read_instruction_counter() {
    uint32_t lo, hi;
    asm volatile("rdinstret %0" : "=r"(lo));
    asm volatile("rdinstreth %0" : "=r"(hi));
    return ((uint64_t)hi << 32) | lo;
}

// Custom performance counter for cache misses
uint32_t read_cache_misses() {
    uint32_t misses;
    asm volatile("csrr %0, 0x7C5" : "=r"(misses));  // Custom CSR
    return misses;
}