Capstone Exercise: FPGA Peripheral System
This capstone combines the FPGA section into one deliverable system. You will build a configurable PWM peripheral, verify it in simulation, constrain it for a board, check timing, and collect validation evidence. The goal is not a large design. The goal is a complete engineering loop: requirements, RTL, testbench, constraints, implementation reports, debug evidence, and a signoff note.
Learning Objectives
You will be able to:
- design a small register-controlled peripheral with a clean datapath and control path;
- avoid output glitches by loading shadow registers at a safe boundary;
- simulate normal, boundary, and error behavior with self-checking tests;
- apply clock and pin constraints;
- inspect timing, resources, and warnings;
- validate hardware behavior with a repeatable evidence record.
Prerequisites
- FPGA fabric, LUTs, flip-flops, routing, and block resources.
- Verilog or VHDL combinational and sequential design.
- Testbenches, waveform debugging, and assertions.
- Constraints and static timing analysis.
- Clock/reset discipline, FSMs, and basic register maps.
Concrete Task
Build a system with:
- one synchronous system clock;
- one synchronous reset;
- one external button input synchronized into the clock domain;
- one PWM LED output;
- a simple register-like write interface in the testbench;
- configurable
periodandduty; - shadow registers that load into active PWM registers only at the period boundary;
- a status flag when software attempts
duty > period; - simulation evidence, timing evidence, and board-validation evidence.
Register Map
| Address | Name | Bits | Behavior |
|---|---|---|---|
0 |
CTRL |
bit 0 enable |
1 runs PWM, 0 forces output low |
1 |
PERIOD |
bits 31:0 |
shadow period in clock cycles |
2 |
DUTY |
bits 31:0 |
shadow duty in clock cycles |
3 |
STATUS |
bit 0 invalid duty |
write 1 to clear |
Use period > 0. A robust production peripheral would also clamp or reject period = 0.
System Architecture
Buildable Core RTL
module pwm_peripheral (
input wire clk,
input wire rst,
input wire button_async,
input wire cfg_we,
input wire [1:0] cfg_addr,
input wire [31:0] cfg_wdata,
output reg [31:0] status,
output reg pwm
);
localparam ADDR_CTRL = 2'd0;
localparam ADDR_PERIOD = 2'd1;
localparam ADDR_DUTY = 2'd2;
localparam ADDR_STATUS = 2'd3;
reg [31:0] period_shadow, duty_shadow;
reg [31:0] period_active, duty_active;
reg [31:0] count;
reg enable;
reg button_meta, button_sync, button_prev;
wire button_rise = button_sync && !button_prev;
always @(posedge clk) begin
if (rst) begin
button_meta <= 1'b0;
button_sync <= 1'b0;
button_prev <= 1'b0;
end else begin
button_meta <= button_async;
button_sync <= button_meta;
button_prev <= button_sync;
end
end
always @(posedge clk) begin
if (rst) begin
enable <= 1'b0;
period_shadow <= 32'd1000;
duty_shadow <= 32'd500;
period_active <= 32'd1000;
duty_active <= 32'd500;
count <= 32'd0;
pwm <= 1'b0;
status <= 32'd0;
end else begin
if (button_rise)
enable <= ~enable;
if (cfg_we) begin
case (cfg_addr)
ADDR_CTRL: enable <= cfg_wdata[0];
ADDR_PERIOD: period_shadow <= (cfg_wdata == 0) ? 32'd1 : cfg_wdata;
ADDR_DUTY: begin
duty_shadow <= cfg_wdata;
if (cfg_wdata > period_shadow)
status[0] <= 1'b1;
end
ADDR_STATUS: status <= status & ~cfg_wdata;
endcase
end
if (!enable) begin
count <= 32'd0;
pwm <= 1'b0;
end else begin
if (count >= period_active - 1) begin
count <= 32'd0;
period_active <= period_shadow;
duty_active <= (duty_shadow > period_shadow) ? period_shadow : duty_shadow;
end else begin
count <= count + 1'b1;
end
pwm <= (count < duty_active);
end
end
end
endmodule
This core intentionally uses a small custom write interface so the exercise stays focused. A real SoC integration would usually wrap the register bank in AXI-Lite, APB, or Wishbone.
Expected Behavior
- After reset, PWM is disabled and the output is low.
- Writing
CTRL.enable = 1starts PWM. periodanddutywrites update shadow registers immediately.- Active PWM timing changes only at a period boundary.
- If
duty > period,status[0]becomes1. - Writing
1toSTATUS[0]clears that flag. - A rising synchronized button edge toggles enable.
title "Illustrative PWM shadow update"
time start=0 end=12 unit=cycles divisions=12
CLK: square label="clk" low=0 high=1 duty=50 cycles=6 unit=logic color=#2563eb
WE: pulse label="write duty" low=0 high=1 at=3 width=1 unit=logic color=#dc2626
WRAP: pulse label="period wrap" low=0 high=1 at=6 width=1 unit=logic color=#7c3aed
PWM: square label="pwm after load" low=0 high=1 duty=25 cycles=2 unit=logic color=#16a34a
marker WRITE at=3 label="shadow"
marker LOAD at=6 label="active"
The waveform is an explanatory timing sketch, not a measured board capture.
Verification Steps
- Simulate reset and confirm documented defaults.
- Enable PWM and measure high time and period in clock cycles.
- Write a new duty value mid-period and confirm the output does not glitch immediately.
- Confirm the new duty takes effect at the next wrap.
- Write
duty > periodand confirmstatus[0]asserts. - Clear
status[0]with a write-one-to-clear transaction. - Toggle the asynchronous button in simulation and confirm only one enable toggle after synchronization.
- Add clock, LED, and button constraints for the target board.
- Run implementation and confirm timing passes with nonnegative setup and hold slack.
- Capture board evidence using an LED observation, scope, logic analyzer, or on-chip analyzer.
Suggested Assertions
always @(posedge clk) begin
if (!rst) begin
assert (period_active > 0);
assert (duty_active <= period_active);
assert (!(cfg_we && cfg_addr == 2'd3 && cfg_wdata[0] && status[0] && (status & ~cfg_wdata) != 0));
end
end
Adapt assertion style to your simulator. The important point is to make invalid states executable checks.
Common Failure Symptoms
| Symptom | Likely cause | Debugging move |
|---|---|---|
| PWM glitches when duty changes | Active duty updated immediately | Trace shadow and active registers separately |
| Duty never changes | Shadow registers are not copied at wrap | Trigger waveform on count >= period_active - 1 |
| Status flag never clears | Write-one-to-clear logic is wrong | Simulate status writes in isolation |
| Button toggles many times | No debouncing, or edge detector is wrong | Separate synchronizer from debounce requirement |
| Timing fails | Wide comparator/counter path, no constraint, or poor placement | Read the critical path report |
| LED pin inactive | Wrong pin name, I/O standard, or board polarity | Compare constraints with board schematic |
Debugging Guidance
- Keep
period_shadow,duty_shadow,period_active, andduty_activevisible in simulation. - Use small periods such as
8or16cycles in simulation so behavior is easy to inspect. - Check constraints before blaming RTL on hardware.
- If using an on-chip analyzer, trigger on period wrap and capture
count,duty_active, andpwm. - Record the tool version, target part, clock period, worst slack, and test setup.
Deliverables
Your completed capstone should include:
- RTL for the peripheral;
- a self-checking testbench log;
- waveform or text evidence for reset, valid update, invalid duty, and status clear;
- constraint file entries for clock, LED, and button;
- timing summary with worst setup and hold slack;
- resource summary;
- hardware validation note or a documented simulation substitute if no board is available;
- a short list of limitations.
Extension Challenge
Wrap the register bank in AXI-Lite or Wishbone. Then write a firmware-style test sequence that configures the PWM, reads status, clears errors, and verifies that mid-period writes still take effect only at a boundary.
Explained Solution
The peripheral separates configuration timing from output timing. Software writes shadow registers whenever needed, but the PWM datapath copies them only when the counter wraps. That prevents mid-period glitches. The status flag records invalid configuration attempts, while the active comparator uses a safe clamped value. The synchronized button demonstrates a small CDC structure. The design is complete only after the same behavior passes simulation, constraints are applied, timing is reviewed, and board evidence is collected.
Summary
This capstone practices the full FPGA loop: write reviewable RTL, verify behavior before hardware, constrain the board, read reports, handle CDC deliberately, and preserve evidence. A small peripheral built this way is more valuable than a larger design that only appears to work.
Further Reading
- AXI4-Lite and Wishbone register-interface examples
- Vendor timing closure methodology guides
- Verilator, cocotb, and SymbiYosys verification examples
- Integrated logic analyzer user guides for AMD, Intel, and Lattice FPGA tools