Loading header...

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 period and duty;
  • 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

flowchart LR CLK["board clock"] --> RST["reset discipline"] BTN["button input"] --> SYNC["two-flop synchronizer"] SYNC --> EDGE["edge detector"] BUS["testbench writes"] --> REGS["control and shadow registers"] EDGE --> REGS REGS --> PWM["PWM counter and comparator"] PWM --> LED["LED pin"] PWM --> STAT["status flags"] STAT --> REGS

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 = 1 starts PWM.
  • period and duty writes update shadow registers immediately.
  • Active PWM timing changes only at a period boundary.
  • If duty > period, status[0] becomes 1.
  • Writing 1 to STATUS[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

  1. Simulate reset and confirm documented defaults.
  2. Enable PWM and measure high time and period in clock cycles.
  3. Write a new duty value mid-period and confirm the output does not glitch immediately.
  4. Confirm the new duty takes effect at the next wrap.
  5. Write duty > period and confirm status[0] asserts.
  6. Clear status[0] with a write-one-to-clear transaction.
  7. Toggle the asynchronous button in simulation and confirm only one enable toggle after synchronization.
  8. Add clock, LED, and button constraints for the target board.
  9. Run implementation and confirm timing passes with nonnegative setup and hold slack.
  10. 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, and duty_active visible in simulation.
  • Use small periods such as 8 or 16 cycles 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, and pwm.
  • 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

Mind Map

mindmap root((FPGA Capstone)) Core idea Register peripheral PWM datapath Shadow load Evidence loop Key formulas PWM duty ratio equals duty over period fPWM equals fclk over period Tclk equals 1 over fclk Slack required minus arrival Design rules Sync async inputs Load at wrap Clamp invalid duty Constrain pins Practical checks Self checking sim Timing pass Resource report Board capture Common mistakes Glitchy updates Wrong pin constraint No status clear No evidence note