A small 16-bit FPGA softcore “ecosystem” based on (and adapted from) Gray’s GR0040 RISC CPU design, wrapped into a SoC together with hardware vectored interrupts, true Harvard ROM/RAM split, and a handful of MMIO peripherals.
This repo contains the Vivado project targeting the Zybo Z7-10 (XC7Z010), plus a Python assembler and example bare-metal programs.
- 16-bit RISC CPU with 16 GPRs (
r0..r15) and fixed-width 16-bit ISA - CPU module with integrated control/datapath and interrupt-aware execution state (
in_irq, nesting depth tracking) - Hardware vectored interrupt controller with fixed vectors and limited nesting/preemption
- Harvard memory split: 1 KiB instruction ROM + 1 KiB data RAM (both byte-sliced hi/lo)
- MMIO peripheral bus mapped to
0x8000–0x8FFF - Peripherals: Timer0, Timer1 (higher priority), PARIO (4-bit), UART (RX interrupt), I2C (master, IRQ-capable)
- Python two-pass assembler with
.includeand macro support
srcs/– Verilog RTLm_soc.v– top-levelsoc(CPU + BRAM + MMIO bus)m_cpu.v– integratedcpumodule (control + datapath + IRQ depth tracking)m_ctrl_unit.v– control unitm_datapath.v– datapathm_alu.v– ALU helpersm_regfile16x16.v– register fileconstants.vh– shared ISA/width constantsm_periph_bus.v– MMIO decode/mux + IRQ wiringm_irq_ctrl.v– interrupt controllerm_timer16.v,m_timerH.v– timersm_uart_mmio.v,m_uart_rx.v,m_uart_tx.v– UART blocksm_i2c_mmio.v,m_i2c_master.v– I2C MMIO + master enginem_brom.v– 1 KiB hi/lo instruction ROM model (init from hex files)m_bram.v– 1 KiB hi/lo data RAM model (zero-init by default)
m_pario.v– simple parallel I/Omem/– BRAM init hex images (mem_hi.hex,mem_lo.hex)
sim/– testbenchestb_Soc.v– SoC testbench (optional UART MMIO self-test)tb_soc_refactor_regression.v– SoC regression (IRQ/MMIO activity)tb_anchor_preemption_abi.v– anchor checks (timer preemption + ABI preservation)tb_timer_start_reg.v– timer start/reload register checkstb_harvard_mem_isolation.v– ROM/RAM isolation check (RAM writes do not mutate ROM)tb_cpu_irq_depth.v– IRQ depth robustness checkstb_soc_byte_lane.v– SoC byte-lane behavior checks forLB/SB
tools/– software toolsassembler.py– assembler that emits BRAM init imagesabi.inc– ABI register aliases + convenience macros
assembly/– example assembly programsinput.asm– vector table + ISRs + small ABI tests
constraints/– Zybo XDC constraints (and optional ILA constraints)docs/– Markdown implementation baseline + report sources
- Vivado (preferrably Vivado 2025.1)
- Python 3.7+ for the assembler
- Board: Zybo Z7-10 (or adapt constraints for your board)
git clone https://github.com/EmbSys25-26/processor && cd processor/Open the Vivado Design Suite™ and import the .xpr file in the project root. Verify the hierarchy of the project:
- In the design sources, check if the SoC module
m_soc.vis selected as top. - In the simulation sources, check if the testbench
tb_Soc.vis selected as top. If not, right-click and "Select as Project/Hierarchy root". Any other issues, refer to the Troubleshooting section.
The instruction ROM model loads two byte-lane files (hi/lo). Generate them from an assembly program:
# Defaults:
# input: assembly/input.asm
# output: srcs/mem/mem.hex + srcs/mem/mem_hi.hex + srcs/mem/mem_lo.hex
python3 tools/assembler.pyNotes:
- The assembler uses byte addresses in
.org/labels, but internally tracks locations in words and enforces alignment. .includeis resolved relative to the including file. Example:assembly/input.asmuses.include "../tools/abi.inc".
Open the project (processor.xpr) in Vivado and run simulation with tb_Soc.
Useful defines in sim/tb_Soc.v:
SIM– uses a much faster UART baud rate for simulationTB_USE_INTERNALS– exposes internal DUT signals and prints IRQ/UART activityTB_UART_MMIO_TEST– bypasses the CPU and directly pokes UART MMIO registers
In order to define a set of properties for simulation, run the following command in the TCL prompt:
set_property verilog_defines {SIM=1 TB_USE_INTERNALS=1} [get_filesets sources_1]Change the {SIM=1 TB_USE_INTERNALS=1} part with the properties you want to set.
The testbench writes a VCD: waves_soc.vcd.
Timing-simulation waveform presets:
- behavioral:
wavecfgs/coolWaveBehav.wcfg - post-synthesis:
wavecfgs/coolWaveSynth.wcfg(targetstb_Soc_time_synth.wdb) - post-implementation:
wavecfgs/coolWaveImpl.wcfg(targetstb_Soc_time_impl.wdb)
- Open
processor.xprin Vivado - Run synthesis + implementation
- Generate bitstream (
.bit) - Program the Zybo Z7-10 via Hardware Manager
If you use ILA debug, you’ll also need the generated .ltx.
This repository includes a deterministic simulation CI gate for pull requests:
- Workflow:
.github/workflows/ci-baseline.yml - Runner script:
scripts/ci/run_iverilog_regression.sh
Run the same suite locally:
bash scripts/ci/run_iverilog_regression.shDetailed CI methodology and GitHub setup steps are documented in:
docs/ci_baseline_verification.md
The top-level module is srcs/m_soc.v:
- Instantiates integrated
cpumodule (control, datapath, IRQ depth tracking) - Instantiates instruction ROM (
srcs/m_brom.v) and data RAM (srcs/m_bram.v) - Instantiates the peripheral bus (
srcs/m_periph_bus.v)
Instruction fetch is synchronous: the BRAM instruction output is registered into an instruction latch (insn_q). A taken branch annuls the fall-through by injecting a NOP.
- Instruction addresses are byte addresses (
PCincrements by 2 per instruction). - Current datapath computes load/store/MMIO address as
d_ad = (sum << 1)insrcs/m_datapath.v. - ROM and RAM are word-indexed using
addr[9:1]; lane select usesaddr[0]. - For core-generated byte operations (
LB/SB), lane selection is derived fromd_ad[1]in SoC glue logic. LBreturns zero-extended byte data from the selected lane;SBstores low bytedata_out[7:0]into the selected lane.- Software examples therefore use pre-shift constants for MMIO/data references (for example,
0x4000in code maps to MMIO base0x8000after the datapath shift). - MMIO is selected by
d_ad[15] == 1(i.e.,0x8000–0xFFFF), with this design using0x8000–0x8FFF.
0x0000–0x03FF– instruction fetch address range in ROM0x0000–0x03FF– data load/store address range in RAM0x0020–0x009F– interrupt vector region (fixed entry points)0x0100– reset vector (default PC after reset)0x8000–0x8FFF– MMIO window
MMIO is decoded by addr[11:8] in srcs/m_periph_bus.v:
| Address range | Block |
|---|---|
0x8000–0x80FF |
Timer0 (timer16) |
0x8100–0x81FF |
Timer1 (timerH, higher priority) |
0x8200–0x82FF |
PARIO |
0x8300–0x83FF |
UART MMIO |
0x8400–0x84FF |
I2C MMIO |
0x8F00–0x8FFF |
IRQ controller regs |
Timer0 base 0x8000, Timer1 base 0x8100:
| Address | Name | Meaning |
|---|---|---|
BASE+0x0 |
CR0 | [0]=int_en, [1]=timer_mode |
BASE+0x2 |
CR1 | [0]=int_req (write-any clears) |
BASE+0x4 |
CNT_INIT | start/reload counter value (R/W) |
BASE+0x6 |
CNT | live counter value (debug read) |
Writing CNT_INIT also updates the running counter immediately.
Base 0x8200:
| Address | Meaning |
|---|---|
0x8200 |
write: par_o[3:0], read: par_o[3:0] |
0x8202 |
read: par_i[3:0] |
Current RTL asserts a PARIO IRQ when par_i == 4'hF.
Base 0x8300:
| Address | Name | Meaning |
|---|---|---|
0x8300 |
DATA | write: enqueue TX byte (if not busy); read: last RX byte (also clears rx_pending) |
0x8302 |
STATUS | bit0 tx_busy, bit1 rx_pending (write with wdata[1]=1 clears rx_pending) |
The UART asserts its interrupt request when RX data is pending.
Base 0x8400:
| Address | Name | Meaning |
|---|---|---|
0x8400 |
CTRL | bit0 en, bit1 start (auto-clear), bit2 rw (0=write,1=read), bit3 irq_en |
0x8402 |
STATUS | bit0 busy, bit1 done, bit2 ack_err, bit3 rx_valid, bit4 irq_pend |
0x8404 |
DIV | 16-bit transfer divider |
0x8406 |
ADDR | I2C address in bits [7:1] |
0x8408 |
LEN | transfer length in bytes [7:0] |
0x840A |
DATA | write: TX FIFO push, read: RX FIFO pop |
STATUS write-one-to-clear semantics:
- bit1 clears
done - bit2 clears
ack_err - bit3 flushes RX FIFO
- bit4 clears
irq_pend
irq_pend is asserted when a transaction completes (done or ack error) while irq_en=1.
IRQ controller base 0x8F00. The controller is word-indexed internally via addr[3:1], which corresponds to these byte offsets:
| Address | Name | Access | Meaning |
|---|---|---|---|
0x8F00 |
IRQ_PEND |
R | pending bitfield |
0x8F04 |
IRQ_MASK |
R/W | enable mask (1=enabled) |
0x8F08 |
IRQ_FORCE |
W | set pending bits (`pending |
0x8F0C |
IRQ_CLEAR |
W | clear pending bits (pending &= ~wdata[7:0]) |
Priority is fixed (higher IRQ index wins) and a small priority stack enables limited nesting/preemption (see DEPTH in srcs/m_irq_ctrl.v).
The interrupt controller generates hardware vectors:
| Source | Vector |
|---|---|
| Timer0 | 0x0020 |
| Timer1 | 0x0040 |
| PARIO | 0x0060 |
| UART (RX pending) | 0x0080 |
| I2C | 0x00A0 |
An example vector table + ISRs live in assembly/input.asm.
If Vivado does not recognize the sources correctly, or files appear missing or misconfigured after opening the .xpr, follow these steps.
When opening the project:
- Remove all files:
remove_files [get_files]- Then, add all the source files again (considering they are in the srcs/ directory)
set proj_dir [get_property DIRECTORY [current_project]]
add_files [glob $proj_dir/srcs/*.v]
set_property file_type {Verilog Header} [get_files constants.vh]
set_property top soc [current_fileset]- Then add the test bench files (considering they are in the sim/ directory)
set proj_dir [get_property DIRECTORY [current_project]]
add_files -fileset sim_1 [glob $proj_dir/sim/*.v]
set_property top tb_Soc [get_filesets sim_1]- Add constraint files
add_files -fileset constrs_1 [glob $proj_dir/constraints/*.xdc]This will let the .xpr know they exist and where they are.
Primary docs are in:
docs/README.mddocs/architecture_and_memory.mddocs/isa_reference.mddocs/abi_spec.md
Legacy report sources are in:
docs/report/docs.texdocs/report/docs-implementation.tex
LICENSE- XSOC license terms and attribution requirements.CODE_OF_CONDUCT.md- expected behavior in project spaces.CONTRIBUTING.md- contribution flow, local checks, and PR requirements.SECURITY.md- vulnerability reporting policy.
srcs/m_brom.vsupports mode-specific ROM image paths:- CI (
SIM+CI):srcs/mem/mem_hi.hex,srcs/mem/mem_lo.hex - Vivado behavioral sim (
SIM):../../../../srcs/mem/mem_hi.hex,../../../../srcs/mem/mem_lo.hex - synthesis/implementation default: absolute paths, or override with
BROM_MEM_HI_PATH/BROM_MEM_LO_PATH(or legacyBRAM_MEM_*) defines.
- CI (
srcs/m_bram.vis data RAM and starts zero-initialized by default.tools/assembler.pynow pads output to 512 words by default, so ROM init warnings about short hex files are avoided.- UART baud rate is compile-time selectable in
srcs/m_periph_bus.v:SIMbuilds use a much faster baud for testbench convenience.
- Original GR0040 concepts based on Gray’s “Designing a Simple FPGA-Optimized RISC CPU and System-on-a-Chip” (see references in the LaTeX docs).