Loading header...

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 period and duty;
  • shadow registers loaded at PWM period boundary;
  • status flag when an invalid duty > period write is attempted.

System Architecture

flowchart LR A["Board clock"] --> B["Reset/clock discipline"] C["Button input"] --> D["Synchronizer + edge detector"] D --> E["Control registers"] F["Testbench bus writes"] --> E E --> G["PWM core with shadow load"] G --> H["LED output"] G --> I["Status flags"]

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 = 1 starts PWM.
  • period and duty updates take effect at a period boundary.
  • If duty > period, status[0] becomes 1.
  • Writing 1 to STATUS[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

  1. Simulate reset and confirm default values.
  2. Enable PWM and measure period/duty in cycles.
  3. Write new duty mid-period and confirm it does not change until wrap.
  4. Write invalid duty > period and confirm status[0].
  5. Clear status and confirm it stays clear after valid writes.
  6. Add board constraints for clock, LED, and button if using hardware.
  7. Run implementation and confirm timing passes.
  8. 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, and pwm.
  • Add assertions for duty_active <= period_active if 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

Mind Map

mindmap root((FPGA Capstone)) Core concept Peripheral system Registers plus datapath Simulation first Board validation Applications Applications Custom IO block Embedded integration Design checks Checks Address map Timing pass Common mistakes Common mistakes No error handling Untested edge cases