A custom 16-bit pipelined CPU implemented in SystemVerilog and verified with a UVM-style verification environment using Cadence Xcelium and IMC.
The project focuses on pipeline control and design-verification methodology rather than ISA complexity. It includes RAW hazard handling, branch flushing, an architectural reference model, SystemVerilog assertions, constrained-random program generation, regression automation, and functional coverage closure.
Current baseline: stall-based RAW hazard resolution without forwarding
- Custom 16-bit ISA with ADD, SUB, MOVI, LOAD, STORE, BEQ, and HALT
- 3-stage organization: IF → combined Decode/Execute → EX/WB commit
- Explicit pipeline-valid tracking
- RAW hazard detection with PC/IF-ID holds and EX/WB bubble insertion
- Taken-branch redirect and wrong-path flush
- Independent architectural reference model in the scoreboard
- Directed + constrained-random verification
- SystemVerilog Assertions for cycle-level pipeline behavior
- 100% architectural functional coverage
- 100% pipeline / RAW-hazard functional coverage
- All defined assertions exercised and passing
- Automated regression and Cadence IMC coverage merge flow
The CPU uses a three-stage organization:
- Instruction Fetch
- Combined Decode / Execute
- EX/WB / Architectural Commit
The main datapath consists of the program counter, instruction memory, IF/ID pipeline state, decode/control logic, register file, ALU, EX/WB pipeline state, and architectural writeback.
The architecture also contains two important control paths:
- RAW hazard control: holds the PC and IF/ID state while inserting a bubble into EX/WB.
- Branch control: redirects the PC to the target and invalidates the wrong-path instruction in IF/ID.
Separate instruction and data memories avoid a structural conflict between instruction fetch and LOAD/STORE accesses.
| Instruction | Function |
|---|---|
| ADD | Register-register addition |
| SUB | Register-register subtraction |
| MOVI | Write an immediate value to a register |
| LOAD | Load from data memory |
| STORE | Store to data memory |
| BEQ | Branch when two registers are equal |
| HALT | Stop architectural execution |
Configuration:
- Instruction width: 16 bits
- Register width: 16 bits
- General-purpose registers: 8
- PC width: 8 bits
- Instruction memory depth: 256
- Data memory depth: 256
OP_NOP is retained internally as a placeholder/control value and is not treated as a required architectural operation in the functional-coverage goal.
Each pipeline slot carries an explicit valid bit.
Validity distinguishes a real architectural instruction from an empty slot, a RAW-hazard bubble, or a flushed wrong-path instruction.
Architectural side effects and verification trace generation are gated by validity so stale or invalid pipeline contents cannot commit state.
The current baseline does not use forwarding.
A RAW dependency is detected when the instruction in IF/ID reads a register that will be written by the older valid instruction in EX/WB.
On a RAW hazard:
PC : HOLD
IF/ID : HOLD
EX/WB : BUBBLE
The producer is allowed to complete writeback while the consumer remains stalled.
The register file uses synchronous writes and asynchronous reads. After the producer writes on the clock edge, the held consumer can observe the updated register value.
Hazard-source detection separately tracks source A and source B, allowing verification of:
- source-A-only hazards
- source-B-only hazards
- simultaneous source-A/source-B hazards
Dependency detection is kept separate from the final stall decision to keep the hazard logic modular and easier to verify.
RAW hazard stall example. A dependent ADD remains held in IF/ID while an older MOVI producer occupies EX/WB. pipe_stall holds the consumer and the following EX/WB cycle is invalidated to create a bubble before execution continues.
The waveform is interpreted together with pipeline-valid signals. Opcode values in invalid pipeline slots are not treated as architectural instructions.
BEQ compares two source operands using subtraction and the ALU zero result.
The branch target is calculated as the current instruction PC plus a sign-extended branch offset.
For a taken branch:
- the PC redirects to the branch target
- the sequentially fetched wrong-path instruction is invalidated
- execution resumes from the branch target
Directed tests cover:
- taken and non-taken branches
- zero offsets
- negative / backward offsets
Taken-BEQ redirect and flush. The branch at PC 2 redirects execution to PC 4. The sequential ADD at PC 3 is fetched into IF/ID, but pipe_if_id_valid is cleared, preventing the wrong-path instruction from committing.
The important behavior is not that the wrong-path instruction bits disappear. The flush is represented by invalidating the pipeline slot.
HALT propagates through the pipeline rather than stopping the processor immediately at decode.
The processor enters the halted state only after the valid HALT instruction reaches the commit stage, preventing younger instructions from committing architectural state afterward.
cpu_base_test builds cpu_env, which contains and connects:
cpu_monitorcpu_scoreboardcpu_coverage
The verification data path is:
DUT
|
cpu_if
|
cpu_monitor
|
cpu_trace_item
| \
cpu_scoreboard cpu_coverage
cpu_if --> cpu_assertions
The monitor samples DUT signals through cpu_if, builds cpu_trace_item transactions, and publishes them through its analysis port.
The same transaction stream is consumed by the scoreboard and coverage subscriber.
Assertions observe cpu_if directly because they verify cycle-level timing behavior rather than transaction-level architectural state.
The scoreboard contains an independent architectural reference model.
It maintains expected:
- register state
- data-memory state
- program-counter state
For each valid committed instruction, the scoreboard:
- independently decodes the instruction
- predicts the expected architectural behavior
- builds the expected commit event
- compares expected and actual DUT trace values
- updates the reference architectural state
The scoreboard checks:
- register-write enable
- destination register
- writeback data
- memory-write enable
- memory address
- memory write data
- branch outcome
- branch target
- HALT behavior
Architecturally irrelevant datapath values are only compared when their corresponding control enables are active.
The scoreboard is deliberately commit-oriented. Pipeline timing rules such as stalls and flushes are checked separately with assertions.
Assertions verify cycle-level pipeline behavior including:
- known PC, instruction, and trace-valid values
- sequential committed-PC progression
- IF/ID PC hold during stalls
- IF/ID instruction hold during stalls
- IF/ID valid hold during stalls
- EX/WB bubble insertion after a stall
- branch redirection
- HALT behavior
- opcode-specific side-effect rules
All defined assertion properties were exercised and passed in the final verification runs.
Directed tests target scenarios that require precise instruction relationships, including:
- arithmetic operations
- LOAD / STORE behavior
- BEQ taken and not taken
- backward branch loops
- zero-offset BEQ
- individual RAW dependencies
- store dependencies
- branch dependencies
- simultaneous dual-source dependencies
- producer / consumer hazard combinations
- HALT behavior
Random verification is split into two layers.
cpu_rand_instr generates legal randomized instruction fields according to opcode-specific constraints.
cpu_rand_program constructs complete programs and introduces relationships across instructions, including:
- RAW producer / consumer sequences
- valid memory-access relationships
- controlled branch sequences
Program generation and CPU execution use separate simulations because instruction memory is initialized with $readmemh during simulation startup.
Coverage is collected from three verification groups:
- Basic directed architectural tests →
merged - Random seed coverage runs →
merged_random - Targeted RAW hazard tests →
merged_rhazard
The three databases are combined into merged_final and inspected using Cadence IMC.
Cadence IMC coverage closure showing 100% coverage for both arch_cg and pipeline_cg. The architectural covergroup includes opcode behavior and BEQ outcomes/offset classes, while the pipeline covergroup covers stall sources and RAW producer/consumer combinations.
Final defined functional coverage:
- Architectural functional coverage: 100%
- Pipeline / RAW hazard functional coverage: 100%
- All defined assertions exercised and passing
Architectural coverage includes:
- required architectural opcodes
- register-write behavior
- memory-write behavior
- HALT behavior
- BEQ taken / not taken
- positive / zero / negative BEQ offsets
- opcode / side-effect crosses
Pipeline coverage includes:
- stall / non-stall behavior
- source-A-only dependency
- source-B-only dependency
- simultaneous dual-source dependency
- producer opcode
- consumer opcode
- producer × consumer opcode crosses
The aggregate IMC code-coverage percentage is intentionally lower than 100%.
Remaining uncovered code/toggle items were reviewed individually instead of adding tests solely to increase the aggregate percentage.
Reviewed gaps include:
- upper PC bits not toggling because programs occupy only a small portion of the 8-bit address space
- upper instruction-memory address bits for the same reason
- reset toggle metrics
- unused internal
OP_NOPcontrol path - instruction-memory fallback initialization branches not used by the normal
+PROGRAMregression flow - upper bits of saved branch-target values not toggling in the tested address range
Core block and expression behavior was reviewed separately from incidental toggle coverage.
Run one directed test:
make run TEST=smokeGenerate and execute one random program:
make rand SEED=10Run the random regression:
make regressGenerate, merge, and view the complete coverage suite:
make coverage_complete NUM_SEEDS=20Clean generated simulation artifacts:
make cleanrtl/
cpu_top.sv
cpu_pkg.sv
control.sv
decoder.sv
alu.sv
reg_file.sv
instr_mem.sv
data_mem.sv
pc_reg.sv
tb/
cpu_if.sv
tb_cpu_uvm_top.sv
assert/
cpu_assertions.sv
uvm/
cpu_uvm_pkg.sv
cpu_trace_item.sv
cpu_monitor.sv
cpu_scoreboard.sv
cpu_coverage.sv
cpu_env.sv
cpu_base_test.sv
cpu_rand_instr.sv
cpu_rand_program.sv
cpu_rand_test.sv
tests/
directed and generated CPU program hex files
sim/
Makefile
run.sh
run_random.sh
run_regression.sh
run_random_coverage.sh
run_raw_hazard.sh
coverage merge TCL scripts
docs/
images/
cpu_architecture.png
uvm_verification_architecture.png
coverage_flow.png
raw_hazard_stall.png
beq_flush.png
imc_functional_coverage.png
- SystemVerilog
- UVM
- SystemVerilog Assertions
- Cadence Xcelium
- Cadence IMC
- Bash
- Make
- Git
Completed:
- 3-stage pipelined CPU baseline
- pipeline-valid control
- RAW dependency detection
- stall-based RAW hazard resolution
- branch redirect and flush
- HALT handling
- architectural scoreboard / reference model
- directed verification
- constrained-random program generation
- random regression
- SystemVerilog assertions
- functional coverage
- coverage closure and uncovered-item review





