Rodolfo Azevedo
Institute of Computing, University of Campinas (UNICAMP), Brazil
rodolfo.azevedo@unicamp.br
http://www.ic.unicamp.br/~rodolfo/mo801
Module 3, Class 1: why interfaces matter, communication vs. computation, and the memory-mapped I/O model.
Project 1 gave you a processor that runs programs. Now we add peripherals — and the question is: how does software reach into hardware? This module is about that boundary: the choices you make here determine whether Project 3's accelerator will be fast or slow.
At the end of this class, you should be able to:
volatile
Your RV32I core can execute instructions. The keyword-spotting model needs to:
"Somewhere" requires a hardware/software interface. The interface design choices are:
Each choice has different latency, throughput, and implementation complexity trade-offs. We will explore all of them this module.
From Schaumont's framework: a system is either bottlenecked by compute (not enough arithmetic units) or by communication (not enough bandwidth to move data to/from those units).
Time = max(T_compute, T_communicate)
For KWS dot products:
M
The communication cost dwarfs the compute cost. Adding more MAC units does not help if the bottleneck is getting data to them.
This is the central insight driving Project 3's design space — we will revisit it with the roofline model in Module 5.
The simplest interface: treat hardware registers as memory addresses. Software reads and writes them with normal load/store instructions.
The CPU does not need to know what is at each address — it just issues lw/sw. The bus routes the transaction to the right peripheral.
lw
sw
Constraints to keep in mind:
A simple rule: assign each peripheral a 256-byte (0x100) aligned block. With 4-byte registers, that gives each peripheral up to 64 registers — far more than any of ours will need.
A minimal bus for a single-master (CPU) system needs:
addr
wdata
rdata
we
re
sel
valid
ready
The valid/ready handshake: the master asserts valid and holds the transaction until the slave asserts ready. Peripherals that respond in one cycle can tie ready to 1. Slower peripherals (e.g., PSRAM access) deassert ready for the required number of cycles.
The CPU stalls (holds PC, does not fetch next instruction) until ready = 1. This is how your multicycle datapath already works for memory accesses — the bus extends that concept to all peripherals.
casez
The bus decoder routes transactions to the correct peripheral:
module bus_decoder ( input logic [31:0] addr, output logic sel_imem, sel_dmem, sel_uart, sel_timer, sel_gpio, sel_accel ); always_comb begin {sel_imem, sel_dmem, sel_uart, sel_timer, sel_gpio, sel_accel} = '0; // default: no peripheral selected unique casez (addr[31:8]) 24'h0000??: sel_imem = 1; // 0x0000_xxxx 24'h0001??: sel_dmem = 1; // 0x0001_xxxx 24'h000200: sel_uart = 1; // 0x0002_00xx 24'h000201: sel_timer = 1; // 0x0002_01xx 24'h000202: sel_gpio = 1; // 0x0002_02xx 24'h000203: sel_accel = 1; // 0x0002_03xx default: ; endcase end endmodule
Each sel_* signal enables one peripheral's ready and rdata to connect to the bus. casez also treats z bits in addr as wildcards, which can mask undriven-bus bugs in simulation. SystemVerilog's case ... inside applies wildcards only to the case items and is the safer modern alternative.
sel_*
z
case ... inside
We will explore all four styles in this module and in Project 3:
MUL rd, rs1, rs2
CMAC rs1, rs2
Project 2 builds the memory-mapped register infrastructure. Project 3 uses it to attach the accelerator — and you will measure whether it is fast enough or whether a different interface style would help.
Two strategies for software to know when a peripheral is ready:
Polling — software loops reading a status register:
// Wait until UART TX is ready while (!(UART_STATUS & UART_TX_READY)); UART_DATA = byte;
Interrupt-driven — hardware signals the CPU when ready:
// ISR (Interrupt Service Routine) — called automatically void uart_tx_isr(void) { if (tx_pending) UART_DATA = *tx_ptr++; } // Main thread: set up and forget uart_enable_tx_irq();
Rule of thumb: poll if you expect to wait < 10 cycles; use interrupts if you expect to wait > 100 cycles. For KWS inference (millions of cycles), interrupts are the right answer — but we use polling in Project 2 for simplicity.
A register map is the hardware/software contract for a peripheral. Example: a simple hardware timer:
CTRL
[0]
[1]
[2]
RELOAD
[31:0]
COUNT
STATUS
Design principles applied here:
addr[3:2]
addr[1:0]
#define TIMER_BASE 0x00020100 #define TIMER_CTRL (*(volatile uint32_t *)(TIMER_BASE + 0x00)) #define TIMER_RELOAD (*(volatile uint32_t *)(TIMER_BASE + 0x04)) #define TIMER_COUNT (*(volatile uint32_t *)(TIMER_BASE + 0x08)) #define TIMER_STATUS (*(volatile uint32_t *)(TIMER_BASE + 0x0C))
The valid/ready handshake in action for a slow peripheral (e.g., PSRAM access taking 3 cycles):
MEM_READ
ready = 1
Your KWS accelerator (Project 3) needs a memory-mapped interface. Design the register map:
Requirements:
Design your register map (15 min):
Expected: CTRL (start, irq_en), STATUS (done, result[1:0]), INPUT_ADDR (pointer to MFCC buffer), LATENCY (read-only cycle count). Many valid designs exist — justify your choices.
INPUT_ADDR
LATENCY
ISA Extensions, Custom Instructions & the On-Chip Bus: how to add Zmmul and CMAC to the RV32I decoder and datapath, and the full SystemVerilog implementation of the minimal bus protocol.
REVIEW: Esta figura não está funcionando. Preciso de uma figura sobre memory map — causa era sintaxe inválida `w:300px` (Marpit exige número sem unidade); corrigido para `w:300`
REVIEW: Transformar a tabela em um SVG conforme a descrição dela abaixo