Project 3: Extra Credit - FPGA Demo
UW—Madison ECE 552: Fall 2026
Project Introduction
This is an optional extra credit opportunity for Project 3. A successful demo is worth an extra 1% on top of your final course grade. By the end, you will have compiled a C program, loaded it into your own single-cycle processor, and watched it respond to switches and buttons on a DE1-SoC board.
Your Project 3 submission does not change. Submit it to Gradescope exactly as before.
All material is available in the Project 3 Extra Credit repository on GitHub. Everything except rtl/soc_core.v is finished: the memories, the I/O hardware, the board top level, the displays, the C programs and the whole toolchain are done. Your job is to wire your processor in, verify it in simulation, and compile it in Quartus.
The Demo
To earn the credit, you demo your design in discussion on 10/14 or 10/21.
IMPORTANT: You will not have access to a board before your demo. This is deliberate: it is how chip design works. A chip team does not get to try its design on silicon and fix it afterwards. It verifies everything it can in simulation and timing analysis, sends the design off to be fabricated, and finds out whether it works when the chip comes back. Here, your simulation and your Quartus timing report are all the evidence you get before the board.
What to Bring
We provide the board and its USB-Blaster cable, and nothing else. We do not provide a computer, so you bring:
- Your own laptop with Quartus installed, including the Programmer and the USB-Blaster driver (see Requirements). You flash the board from it, so install and check everything before discussion.
- A compiled
.soffor the program you want to show. Changing the program means a full Quartus recompile (see Running a Different Program), so there is no time to build one in discussion. Bring a.sofforechoas well, as a fallback. - Your Quartus project, so you can open the compilation and timing reports from Step 5 during the questions.
Before You Come
Since you cannot test on the board, make sure everything you can check passes:
make simpasses for the program you will show (Step 3).- Quartus compiles with no errors and
cpu_clkmeets timing (Step 5). - You have read through the flashing steps in Step 6, so you are not seeing them for the first time at the demo.
At the Demo
You power on the board and flash it yourself, in front of us, then use it: flip switches, press buttons, and show that your processor responds. Then we ask a few short questions about your design and the flow.
What We Will Ask About
The questions are about timing and memory: what Quartus reported for your design, and why. You do not need to memorize numbers; have your reports open and find them as we ask. Everything you need is in Step 5.
Timing
- Know your
cpu_clkFmax and whether your design meets timing. Be able to say what would happen on the board if it did not. - Quartus reports timing for several corners: Slow and Fast, at 0 °C and 85 °C. Know why we tell you to read Slow 1100mV 85C.
- Find your critical path and trace it through your own schematic, stage by stage. Think about which instruction uses that path, and why that one is the slowest.
Memory
- Look up what an M10K block is and how its read works, then compare that with how your processor reads instruction and data memory. Be ready to explain why the fitter used zero of them.
- Find what Quartus built
dmemout of instead, and point to the numbers in the fitter report that show it. imemanddmemare both 4 KB, but one costs far more than the other. Compare them in the per-module report and think about what makes them different.
Requirements
Docker, the same way you use it for the assembly tests in tests/asm/. Pull the toolchain image if you have not already:
docker pull coderkalyan/ece552-tools:latest
It carries the RISC-V compiler, Icarus Verilog and Python, so it is the only image you need and you do not need any of them installed on your machine.
On Linux, Docker needs permission to run without sudo. If docker run hello-world fails with permission denied ... docker.sock, add yourself to the docker group, then log out and back in:
sudo usermod -aG docker $USER
make. macOS and Linux usually have it already (run make --version to check). On Windows, you will need to install it yourself; search online for how to install make on Windows.
Quartus Prime. If you do not already have Quartus, download Quartus Prime Lite Edition. It is free and needs no license. Search for "Quartus Prime Lite download" to find it on Altera's (formerly Intel's) FPGA software download page. When the installer asks which devices to support, select Cyclone V, the FPGA on the DE1-SoC. You can skip the others to save several GB. Leave the Programmer selected too. You need it to flash the board at your demo (Step 6), and it installs the USB-Blaster driver.
Directory Layout
project3_ec/
sw/ the C programs and their build
echo.c calc.c pong.c
crt0.S startup code
link.ld memory layout
Makefile
tools/
isa_check.py rejects instructions your processor lacks
rtl/
soc_core.v <-- THE ONLY FILE YOU EDIT
hart.v <-- you add this (plus alu.v, rf.v, decode.v, ...)
Makefile
demo.qpf demo.qsf the Quartus project, pins already assigned
ip/ finished wrapper modules -- leave alone
Everything except rtl/soc_core.v is finished. The memories, the I/O hardware, the board top level, the displays, the C programs and the whole toolchain are done.
The Programs
Three programs are provided, in increasing order of complexity.
Echo: Board Bring-Up
echo.c reads the inputs, writes them to the outputs, and repeats. About 22 instructions.
| Input | Output |
|---|---|
SW[9:0] |
LEDR[9:0] (binary) |
SW[9:0] |
HEX2–HEX0 (same value in hex) |
Flip a switch: the matching LED lights and the hex digits track the value.
Calc: A Live 4-Bit Calculator
calc.c is your ALU, on the board. About 72 instructions.
| Control | Meaning |
|---|---|
SW[3:0] |
operand A |
SW[7:4] |
operand B |
SW[9:8] |
operation within the current bank |
KEY0 |
switch banks (bank 0 = ops 0–3, bank 1 = ops 4–7) |
| Display | Shows |
|---|---|
HEX5 |
operation number, 0–7 |
HEX4 / HEX3 |
blank |
HEX2–HEX0 |
result, 12 bits, two's complement |
LEDR |
low 10 bits of the result, in binary |
| Op | Operation | Op | Operation |
|---|---|---|---|
| 0 | ADD — a + b |
4 | XOR — a ^ b |
| 1 | SUB — a − b |
5 | SLL — a << b |
| 2 | AND — a & b |
6 | SRL — a >> b (logical) |
| 3 | OR — a or b |
7 | SLT — a < b (signed) |
There is no compute button. The result updates continuously, so you can sweep an operand and watch the output follow.
Pong: Two-Player Pong on the LEDs
In pong.c, the ball is a single lit LED travelling along LEDR. When it reaches your end, press your button to hit it back; miss and the other player scores. Every successful return speeds the ball up. First to 9 wins. About 152 instructions.
| Control | Meaning |
|---|---|
KEY0 |
left player's paddle |
KEY3 |
right player's paddle |
HEX5 / HEX0 |
left / right score |
On reset, and after each match, it sweeps the ball back and forth until someone presses a button to start a game; the cycle counter at that instant seeds the random number generator, so serves are not predictable.
Step 1: Compiling a Program
From sw/, run:
make PROGRAM=echo mem
A successful build should output:
ok echo text 88 rodata 0 bss 0 stack 4008
-> build/program.mem
build/program.mem is the file the hardware loads.
Other commands:
| Command | What it does |
|---|---|
make PROGRAM=pong mem |
Builds a different program. PROGRAM can be echo, calc or pong, and the result always goes to build/program.mem, replacing whatever was there. |
make PROGRAM=calc disasm |
Prints the RISC-V assembly the compiler generated for that program, with the address and machine-code word of each instruction. Use it to see how your C became instructions, or to find the instruction at a PC while debugging. A copy is also saved to build/calc.dis whenever you build. |
make clean |
Deletes the build/ directory and everything the build produced. Run it if a build seems stale or you want to start fresh. |
make |
With no arguments, prints this list of commands. |
Step 2: Wiring Your Processor In
- Copy your processor into
rtl/, next tosoc_core.v:hart.vand whatever it instantiates (alu.v,rf.v,decode.v, …). Do not modify them; these should be the same files you submitted. Do not copytb.v. - Open
rtl/soc_core.vand fill in thehartinstantiation. Every port is listed with a blank connection, and the table at the top of the file says what each one attaches to.
Everything else in that file is already done: the wire declarations, the address split, and the memory and I/O instantiations. Only the processor is missing.
Step 3: Simulating Before You Synthesize
From rtl/, run:
make lint # does it elaborate?
make sim PROG=echo # does it actually run?
A passing run looks like:
=== soc_tb: echo ===
ok ledr = 00000155
ok hex low 12 = 00000155
ok ledr after change = 000002aa
ok hex_en = 00000007
=== 0 error(s), 123 cycles ===
Step 4: Making the Quartus Project
The project is already made: rtl/demo.qpf and rtl/demo.qsf. The .qsf holds the device, the top-level entity (de1soc_top), the timing constraints, the ip/ sources, and all 67 pin assignments with their I/O standards.
Picking the device in Quartus's New Project Wizard does not assign pins; Quartus has no built-in notion of this board. So always open the provided demo.qpf rather than making a fresh project.
- Open the project. In Quartus, File → Open Project and pick
rtl/demo.qpf. - Add the rest of your processor.
soc_core.vandhart.vare already in the project. Add every other file yourhartinstantiates with Project → Add/Remove Files in Project (e.g.alu.v,rf.v,decode.v, ...). Your files must sit inrtl/next tosoc_core.v, as in Step 2. - Put the program where Quartus can find it. The memories load
program.memby relative filename, so it has to sit in the project directory,project3_ec/rtl/, next todemo.qpf. Copy it from wheremakeput it in Step 1:
cp <path-to-repo>/project3_ec/sw/build/program.mem <path-to-repo>/project3_ec/rtl/program.mem
IMPORTANT: Do not add ip/soc_tb.v. It is the simulation testbench; it contains delays and initial blocks that cannot be synthesized, and adding it breaks the compile.
Note: the upper right-hand corner has a drop-down next to Project Navigator. From the drop-down select Files to see what files are included in your project. You should see ip/de1soc.sdc, the seven ip/*.v files, soc_core.v, hart.v, and the files you just added. Do not remove ip/de1soc.sdc. It is the timing constraints file, and it tells the Timing Analyzer that your processor runs on the divided 12.5 MHz clock, not the board's 50 MHz oscillator. Without it the Fmax you read in Step 5 is wrong.
Step 5: Compiling in Quartus
Processing → Start Compilation. Expect anywhere from 5-20 minutes.
A successful compile ends with warnings but no errors.
Read the Timing Report
Compilation Report → Timing Analyzer → Slow 1100mV 85C Model → Fmax Summary. This is the fastest your processor could run.
Find the cpu_clk row and compare it against 12.5 MHz, which is what the divider produces. If Fmax is above 12.5 MHz, the design meets timing and you don't need to change anything. If it is below 12.5 MHz (the Setup Summary will also show negative slack for cpu_clk), the processor will not run reliably. Set CLK_DIV_LOG2 = 3 in ip/de1soc_top.v for 6.25 MHz, change -divide_by to 8 and the counter bit to cnt[2] in ip/de1soc.sdc, then recompile.
Find Your Critical Path
The critical path is the slowest register-to-register path. It is what sets your Fmax.
- Tools → Timing Analyzer.
- In the Tasks pane, under Custom Reports, double-click Report Timing…. The Timing Analyzer builds its netlist automatically the first time.
- Set From clock and To clock to
cpu_clk, and click Report Timing. - Click the top path. The Data Path tab lists every node the signal passes through, with the delay of each, from the launching register to the capturing one.
The node names follow your module hierarchy, for example soc_core:u_core|hart:u_hart|pc[3] or soc_core:u_core|dmem:u_dmem|mem[...]. Match them to the blocks on your schematic.
See What Your Design Became
The fitter report shows what Quartus actually built your design from. Open Compilation Report → Fitter → Resource Section.
Resource Usage Summary gives totals for the whole design. Note these:
| Line | What it counts |
|---|---|
| Logic utilization (ALMs needed / total ALMs on device) | general-purpose logic cells used |
| Dedicated logic registers | flip-flops |
| M10K blocks | dedicated block RAMs |
| Total MLAB memory bits | logic cells used as small RAMs |
Resource Utilization by Entity gives the same counts, broken down by module. Expand the tree to find a row for each part of your design: hart and everything inside it (your rf, alu, …), plus imem, dmem and mmio. Compare the rows. Which module is the largest, and does that match what you expected?
To see it as a picture, Tools → Netlist Viewers → RTL Viewer shows your design as a schematic of the modules you wrote. Technology Map Viewer shows what they became on the FPGA.
Step 6: Flashing the Board
You do this live at your demo (see The Demo), from your laptop, with our board. You cannot try it beforehand, so read through the steps now and do the setup that needs no board: install the Programmer and USB-Blaster driver, and on Linux, add the udev rule in step 3.
- Connect the USB-Blaster II port (the mini-USB nearest the power jack) and power the board on.
- Tools → Programmer.
- Hardware Setup… → DE-SoC → Close. If nothing is listed, the driver is not installed or the board is off.
- Mode: JTAG.
- Click Auto Detect; if it asks for a device, pick
5CSEMA5. You should now see two chips: SOCVHPS → 5CSEMA5. - Click the
5CSEMA5row, then Change File… and pick your.sof. - Tick Program/Configure on that row.
- Start. The bar should reach 100% (Successful).
The design runs as soon as configuration finishes.
Note: on Linux, the board will not be listed in step 3 until you give your user access to the USB-Blaster. Do this once, before your demo:
echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="09fb", MODE="0666"' | sudo tee /etc/udev/rules.d/51-usbblaster.rules
sudo udevadm control --reload-rules
If it still does not show up, unplug and replug the board, or run killall jtagd and reopen the Programmer.
Running a Different Program
cd ../sw && make PROGRAM=pong mem
cp build/program.mem ../rtl/
Then recompile in Quartus.
You have to recompile. The program is not data the board loads at startup; it was baked into the bitstream as the initial contents of the instruction memory, so changing a single instruction means re-synthesizing the entire design.
Each compile overwrites demo.sof. To bring more than one program to your demo, copy each .sof to its own name (for example echo.sof, pong.sof) after compiling it.
Writing Your Own Program
You are encouraged to. Drop a .c file in sw/ and build it:
make PROGRAM=yourprogram mem
The constraint is that your processor implements 37 instructions (the RV32I base set minus a few pieces), so not every legal C program will run on it.
| Do not use | Why | Use instead |
|---|---|---|
*, /, % |
no multiply or divide instructions | shift-and-add; repeated subtraction |
long long, float, double |
no 64-bit or floating-point support | 32-bit int / unsigned |
printf, malloc, the C library |
needs ecall, which does not exist here |
memory-mapped I/O |
__builtin_popcount, __builtin_clz, … |
become library calls | write the loop yourself |
| initialized global variables | reset does not reload memory, so they come back stale | const globals, or initialize at the top of main |
If you use * or /, the build fails with a clear message rather than producing a program that misbehaves on the board:
ld: undefined reference to `__mulsi3'
ld: undefined reference to `__udivsi3'
That is the linker telling you the compiler wanted a multiply routine your processor does not have.
Note: multiplying by a constant sometimes works and sometimes does not, because the compiler may turn x * 8 into a shift but x * 12500 into a __mulsi3 call.
Byte and half-word access does work (lb, lbu, lh, lhu, sb, sh), so char arrays and strings are fine.
Why no initialized globals: pressing reset restarts your processor but does not reload memory, so a mutable global would come back holding whatever the previous run left in it. crt0 cannot restore it, because there is no separate copy to restore from. Constants go in .rodata, which is never written; mutable state goes in .bss or a local and is set up at runtime. The build enforces this.
Reference
Memory Map
One flat 4 KB image, loaded into both memories. Each ignores the region it never addresses, which is why one file serves both and crt0 needs no .data copy loop.
| Range | Contents |
|---|---|
0x0000_0000–0x0000_03FF |
.text, fetched through the imem port |
0x0000_0400–0x0000_0EFF |
.rodata, .bss, reached through the dmem port |
0x0000_0F00–0x0000_0FFC |
stack, grows down |
Peripherals
Decoded on address bit 31: anything at 0x8000_0000 and above is I/O.
| Address | Register | Dir | Meaning |
|---|---|---|---|
0x8000_0000 |
LEDR |
W | 10 bits |
0x8000_0004 |
SW |
R | 10 bits, synchronized |
0x8000_0008 |
KEY |
R | 4 bits, active high |
0x8000_000C |
KEY_EDGE |
R | sticky press latch |
0x8000_0010 |
KEY_ACK |
W | write 1 to clear a latch bit |
0x8000_0014 |
HEX_VAL |
W | 24 bits → six hex digits |
0x8000_0020 |
HEX_EN |
W | 6 bits, per-digit blanking |
0x8000_0024 |
CYCLES |
R | free-running cycle counter |
0x8000_0028 |
INSTRET |
R | retired instruction counter |
The counters are memory-mapped because this ISA has no CSRs; there is no rdcycle to read.
KEY_EDGE is cleared by an explicit write to KEY_ACK rather than by the read itself. Read-to-clear would be safe on a single-cycle processor but would drop presses on a pipelined one that can replay a load.
IP Modules
The files in ip/ are finished. Do not modify them.
| File | Role |
|---|---|
de1soc_top.v |
Board top level: pins, reset, clock, LED override, displays |
imem.v |
4 KB instruction memory, combinational read |
dmem.v |
4 KB data memory, combinational read, byte-writable |
mmio.v |
Switches, buttons, LEDs, displays, counters |
hex7seg.v |
Hex digit → seven segments |
clkdiv.v |
50 MHz → 12.5 MHz CPU clock, freezes on halt |
halt_ctrl.v |
Watches your retire interface for ebreak and traps |
soc_tb.v |
The testbench. Simulation only; never add to Quartus |
de1soc.sdc |
Timing constraints; add to Quartus |
Board Behavior
- Reset is KEY1. None of the programs use it, so it never gets in the way of a program's own buttons.
- All LEDs on, steady: your program executed
ebreakand the CPU stopped. Normal for a program that ends. - All LEDs blinking: your processor took a trap (illegal instruction, misaligned load/store, or misaligned branch target). These programs do none of those, so blinking points at the processor, not the software.
Troubleshooting
Software Build
| Symptom | Cause |
|---|---|
undefined reference to '__mulsi3' / '__udivsi3' |
you used *, / or % |
ISA CHECK FAILED |
an instruction your processor does not implement; the message names it |
.data is N bytes, must be 0 |
initialized global; make it const or set it inside main |
region 'RAM' overflowed |
program too big; check -Os is still there, shrink arrays |
warning: ... LOAD segment with RWX permissions |
expected and harmless |
Simulation
| Symptom | Cause |
|---|---|
FAIL lines, or outputs all x |
a connection left blank in soc_core.v |
Unknown module type: hart |
hart.v not copied into rtl/ |
Cannot connect to the Docker daemon |
Docker Desktop is not running (Linux: the Docker service is not running; run sudo systemctl start docker) |
permission denied while trying to connect to the Docker daemon socket |
Linux: you are not in the docker group; see Requirements |
make: command not found |
make is not installed; see Requirements |
Quartus
| Symptom | Cause |
|---|---|
Top-level entity "demo" is undefined |
you made a new project instead of opening the provided demo.qpf; open that one (or set the top-level entity to de1soc_top) |
errors about # delays or initial |
you added ip/soc_tb.v; remove it |
Can't elaborate user hierarchy / unknown module under hart |
one of your processor files is not in the project; add it (Step 4) |
Can't open file hart.v |
hart.v not copied into rtl/ |
| compiles fine, board completely dead | pins not assigned; did you open the provided demo.qpf, or make a fresh project? |
| compiles fine, displays blank | program.mem missing from the project directory |
| huge timing violations, Fmax nonsense | ip/de1soc.sdc was removed from the project; add it back |
Board
| Symptom | Cause |
|---|---|
Can't find hardware in the Programmer |
board off, wrong USB port (use the one by the power jack), or driver not installed (Linux: udev rule missing; see Step 6) |
| Programmer succeeds, nothing happens | .sof attached to SOCVHPS instead of the 5CSEMA5 row |
| Nothing works at all | load echo first; if that fails it is the wiring or the board setup, not the program |