Capstone Exercise: FPGA Peripheral System
The capstone ties the section together. You will build a small FPGA peripheral system, verify it in simulation, constrain it for a board, check timing, and collect validation evidence.
Learning Objectives
You will:
- design a timer/PWM peripheral with a clean register-style interface;
- simulate normal and error behavior;
- apply clock and pin constraints;
- review timing and resource reports;
- validate the design on hardware or with a realistic simulation substitute.
Prerequisites
- FPGA architecture and LUT basics.
- Combinational and sequential HDL.
- Testbenches and waveform debugging.
- Constraints and static timing analysis.
- FSMs, clock/reset discipline, and peripheral register maps.
Concrete Task
Build a system with:
- one input clock;
- one synchronized button input;
- one PWM LED output;
- a simple register-like interface in the testbench;
- configurable
periodandduty; - shadow registers loaded at PWM period boundary;
- status flag when an invalid
duty > periodwrite is attempted.
System Architecture
Buildable Core Sketch
module pwm_peripheral (
input wire clk,
input wire rst,
input wire cfg_we,
input wire [1:0] cfg_addr,
input wire [31:0] cfg_wdata,
output reg [31:0] status,
output reg pwm
);
reg [31:0] period_shadow, duty_shadow;
reg [31:0] period_active, duty_active;
reg [31:0] count;
reg enable;
localparam ADDR_CTRL = 2'd0;
localparam ADDR_PERIOD = 2'd1;
localparam ADDR_DUTY = 2'd2;
localparam ADDR_STATUS = 2'd3;
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 (cfg_we) begin
case (cfg_addr)
ADDR_CTRL: enable <= cfg_wdata[0];
ADDR_PERIOD: period_shadow <= 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;
end else begin
count <= count + 1'b1;
end
pwm <= (count < duty_active);
end
end
end
endmodule
This is intentionally small. A production version would add bus readback, stronger invalid-value handling, and documented reset values.
Expected Behavior
- After reset, PWM is disabled.
- Writing
CTRL.enable = 1starts PWM. periodanddutyupdates take effect at a period boundary.- If
duty > period,status[0]becomes1. - Writing
1toSTATUS[0]clears the flag.
title "Illustrative PWM update at period boundary"
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
CFG: pulse label="new duty written" low=0 high=1 at=3 width=1 unit=logic color=#dc2626
LOAD: pulse label="shadow loaded at wrap" low=0 high=1 at=6 width=1 unit=logic color=#7c3aed
PWM: square label="PWM changes after load" low=0 high=1 duty=25 cycles=2 unit=logic color=#16a34a
marker WR at=3 label="write"
marker WRAP at=6 label="period boundary"
Verification Steps
- Simulate reset and confirm default values.
- Enable PWM and measure period/duty in cycles.
- Write new duty mid-period and confirm it does not change until wrap.
- Write invalid
duty > periodand confirmstatus[0]. - Clear status and confirm it stays clear after valid writes.
- Add board constraints for clock, LED, and button if using hardware.
- Run implementation and confirm timing passes.
- Capture hardware evidence: LED behavior, internal debug capture, or external logic analyzer waveform.
Common Failure Symptoms
| Symptom | Likely cause |
|---|---|
| PWM glitches on update | active registers changed immediately instead of shadow load |
| Duty never changes | shadow registers not copied at wrap |
| Status flag never clears | write-one-to-clear logic wrong |
| Timing fails | comparator/counter path too wide or unconstrained clock |
| Hardware pin inactive | wrong pin constraint or I/O standard |
Debugging Guidance
- Simulate
count,period_active,duty_active, andpwm. - Add assertions for
duty_active <= period_activeif you clamp invalid values. - Check constraints before suspecting RTL.
- Use an ILA trigger at period wrap.
- Keep a short signoff note with timing slack and validation evidence.
Extension Challenge
Add a tiny AXI-Lite or Wishbone wrapper around the register bank. Then write a firmware-style test sequence that configures the PWM through bus transactions.
Explained Solution
The peripheral separates configuration from output timing. Firmware writes shadow registers at any time, but the PWM core loads them only at a period boundary. That prevents mid-cycle glitches. The status flag records invalid writes so software can detect configuration mistakes. Verification must check both behavior and timing because a correct simulation can still fail if the implemented path misses the clock constraint.
Summary
This capstone combines the core FPGA habits: think in hardware, simulate first, constrain the board, read timing reports, handle clock/reset cleanly, design a reviewable interface, and collect validation evidence. That is the shape of real FPGA work.
Further Reading
- AXI4-Lite and Wishbone interface examples
- Vendor timing closure guides
- Verilator/cocotb regression examples
- Integrated logic analyzer user guides