

This project implements a compact 32-bit RISC-V processor with a five-stage pipeline architecture consisting of Instruction Fetch (IF), Decode (ID), Execute (EX), Memory (MEM), and Write-Back (WB) stages. The pipelined design allows multiple instructions to be processed concurrently, improving throughput while maintaining a small hardware footprint.
The system includes the following communication and control interfaces:
Bootloader Mode (Reset): After reset, the processor enters bootloader mode via UART1. Instructions are received serially as bytes through the UART1 RX pin and stored sequentially into instruction memory. Once the bootloader detects the sentinel value (0xBAADF00D), it automatically transitions to execution mode.
Execution Mode: The processor fetches and executes instructions through the five-stage pipeline. Peripheral access is controlled via memory-mapped I/O registers.
Peripherals are accessed through memory-mapped I/O registers using a polling-based architecture. The CPU continuously reads status registers to determine when operations can proceed, rather than using interrupts. This simplifies the design while maintaining deterministic behavior.
The SPI master interface supports full-duplex communication with external peripherals such as sensors or external memory devices via dedicated signals (MOSI, MISO, SCLK, and CS).
Memory-mapped registers for UART2 (general-purpose communication):
| Register | Address | Description |
|---|---|---|
UART_TX |
0x1000_0000 |
Write data byte to transmit |
UART_RX |
0x1000_0004 |
Read received data byte |
UART_TX_STATUS |
0x1000_0008 |
TX buffer status (Bit 0: 1 = ready/empty, 0 = busy) |
UART_RX_STATUS |
0x1000_000C |
RX buffer status (Bit 0: 1 = data available, 0 = empty) |
Polling Protocol:
UART_TX_STATUS Bit 0 until high, then write to UART_TXUART_RX_STATUS Bit 0 until high, then read from UART_RXMemory-mapped registers for SPI master mode (Mode 0):
| Register | Address | Description |
|---|---|---|
SPI_TX |
0x4000_0000 |
Write data byte to transmit |
SPI_RX |
0x4000_0008 |
Read received data byte |
SPI_TX_STATUS |
0x4000_0004 |
TX status (Bit 0: 1 = idle/ready, 0 = shifting) |
SPI_RX_STATUS |
0x4000_000C |
RX status (Bit 0: 1 = data available, 0 = empty) |
Polling Protocol:
SPI_TX_STATUS Bit 0 until high, then write to SPI_TX to initiate transferSPI_RX_STATUS Bit 0 until high, then read from SPI_RXMemory-mapped GPIO output registers:
| Register | Address | Description |
|---|---|---|
GPIO1 |
0x3000_0000 |
General-purpose output (LED or other peripheral control) |
GPIO2 |
0x3000_0004 |
SPI Chip Select control (typically active-low) |
The design can be tested using both simulation and hardware deployment.
Use Icarus Verilog and cocotb for functional verification:
0xBAADF00D) is received, the bootloader halts and execution begins0xBAADF00D, and transitions to execution mode cleanlyThe following assembly routine demonstrates a practical I/O sequence:
0xAA) to SPI# ==============================================================================
# RISC-V MMIO SPI Polling and UART2 Relay Loop
# Target: 32-Bit Pipelined Core (RV32I)
# SPI to UART test
# ==============================================================================
# --- Setup Base Address Pointers ---
lui x10, 0x40000 # x10 = SPI Base Address (0x4000_0000)
lui x18, 0x30000 # x18 = GPIO Base Address (0x3000_0000)
lui x19, 0x10000 # x19 = UART Base Address (0x1000_0000)
# --- Calculate Register Offsets ---
addi x11, x10, 8 # x11 = SPI_RX Data Offset (0x4000_0008)
addi x12, x10, 4 # x12 = SPI_TX Status Offset (0x4000_0004)
addi x13, x10, 12 # x13 = SPI_RX Status Offset (0x4000_000C)
addi x21, x19, 8 # x21 = UART_TX Status Offset (0x1000_0008)
addi x14, x18, 4 # x14 = GPIO2 CS Offset (0x3000_0004)
# --- Initialize Loop Constants ---
addi x3, x0, 1 # x3 = GPIO2 HIGH (CS de-assert)
addi x16, x0, 0 # x16 = GPIO2 LOW (CS assert)
addi x5, x0, 200 # x5 = Transfer counter (200 iterations)
addi x22, x0, 1 # x22 = Status bit mask (Bit 0)
addi x7, x0, 0xAA # x7 = Dummy data byte
# --- Assert SPI Chip Select ---
sw x16, 0(x14) # GPIO2 = 0 (CS asserted to peripheral)
loop:
beq x5, x0, release_cs # If counter == 0, exit loop
# --- Wait for SPI TX Ready ---
wait_spi_tx:
lw x6, 0(x12) # Load SPI_TX_STATUS
and x6, x6, x22 # Isolate Bit 0 (ready flag)
beq x6, x0, wait_spi_tx # If 0 (busy), loop back
sw x7, 0(x10) # Write 0xAA to SPI_TX, initiate transfer
# --- Wait for SPI RX Data ---
wait_spi_rx:
lw x8, 0(x13) # Load SPI_RX_STATUS
and x8, x8, x22 # Isolate Bit 0 (data available flag)
beq x8, x0, wait_spi_rx # If 0 (no data), loop back
lw x9, 0(x11) # Read received byte from SPI_RX
# --- Wait for UART TX Ready ---
wait_uart_tx:
lw x6, 0(x21) # Load UART_TX_STATUS
and x6, x6, x22 # Isolate Bit 0 (ready flag)
beq x6, x0, wait_uart_tx # If 0 (busy), loop back
sw x9, 0(x19) # Write SPI byte to UART_TX
# --- Next Iteration ---
addi x5, x5, -1 # Decrement counter
jal x0, loop # Jump to next iteration
release_cs:
sw x3, 0(x14) # GPIO2 = 1 (CS de-asserted from peripheral)
ecall # Halt simulation
Corresponding 32-bit hexadecimal instruction sequence: (link:https://riscvasm.lucasteske.dev/#)
0x40000537, # lui x10, 0x40000
0x30000937, # lui x18, 0x30000
0x100009b7, # lui x19, 0x10000
0x00850593, # addi x11, x10, 8
0x00450613, # addi x12, x10, 4
0x00c50693, # addi x13, x10, 12
0x00898a93, # addi x21, x19, 8
0x00490713, # addi x14, x18, 4
0x00100193, # addi x3, x0, 1
0x00000813, # addi x16, x0, 0
0x03200293, # addi x5, x0, 200
0x00300b13, # addi x22, x0, 1
0x0aa00393, # addi x7, x0, 0xAA
0x01072023, # sw x16, 0(x14)
0x02028663, # beq x5, x0, release_cs
0x00752023, # sw x22, 0(x12) [wait_spi_tx start - poll]
0x0006a403, # lw x6, 0(x12)
0xfe040ee3, # beq x6, x0, wait_spi_tx
0x0005a483, # lw x8, 0(x10)
0x000aa303, # lw x3, 0(x13)
0x01637333, # and x6, x6, x22
0xfe031ce3, # beq x6, x0, wait_spi_rx
0x0099a023, # sw x9, 0(x11)
0xfff28293, # addi x5, x5, -1
0xfd9ff06f, # jal x0, loop
0x00372023, # sw x3, 0(x14) [release_cs]
0x00000073, # ecall
0xBAADF00D # SENTINEL VALUE (bootloader halt marker)
# ==============================================================================
# RISC-V MMIO SPI Polling and UART2 Relay Loop
# Target: 32-Bit Pipelined Core (RV32I)
#GPIO test
# ==============================================================================
lui x13, 0x30000 /* GPIO1 base = 0x30000000 GPIO1*/
addi x14, x13, 4 /* GPIO2 = 0x30000004 GPIO2*/
addi x5, x0, 10 /* loop counter */
addi x6, x0, 1 /* HIGH */
addi x7, x0, 0 /* LOW */
loop:
/* BOTH OFF */
sw x7, 0(x13)
sw x7, 0(x14)
/* delay */
addi x3, x0, 1000
delay1:
addi x3, x3, -1
bne x3, x0, delay1
/* BOTH ON */
sw x6, 0(x13)
sw x6, 0(x14)
/* delay */
addi x3, x0, 1000
delay2:
addi x3, x3, -1
bne x3, x0, delay2
addi x5, x5, -1
bne x5, x0, loop
/* OFF both */
sw x7, 0(x13)
sw x7, 0(x14)
ecall
0x300006B7
0x00468713
0x00A00293
0x00100313
0x00000393
0x0076A023
0x00772023
0x3E800193
0xFFF18193
0xFE019EE3
0x0066A023
0x00672023
0x3E800193
0xFFF18193
0xFE019EE3
0xFFF28293
0xFA029EE3
0x0076A023
0x00772023
0x00000073, # ecall
0xBAADF00D # SENTINEL VALUE (bootloader halt marker)
Bootloader Operation: When the bootloader receives the sentinel value 0xBAADF00D, it:
Standard external signals required for operation:
rst_n)ena) from external controller| # | Input | Output | Bidirectional |
|---|---|---|---|
| 0 | BOOT_UART1_RX | BOOT_UART1_TX | SPI_CS_GPIO2_TOP |
| 1 | PER_UART2_RX | PER_UART2_TX | SPI_MOSI_TOP |
| 2 | SPI_MISO_TOP | ||
| 3 | SPI_SCLK_TOP | ||
| 4 | GPIO1_TOP | ||
| 5 | SPI_CS_GPIO3_TOP | ||
| 6 | |||
| 7 |