Designing a Minimal FPGA Project for Embedded Systems: A Practical Walkthrough

Why does a tiny FPGA matter today? Because every new gadget, from a smart watch to a tiny sensor node, needs a bit of custom logic that can’t be squeezed into a micro‑controller alone. A small, well‑planned FPGA design gives you that extra flexibility without blowing up the bill or the board size. In this post I’ll walk you through a minimal FPGA project that you can finish in a weekend, using only the tools you probably already have.

Pick the Right FPGA for a Tiny Project

When I was a graduate student I once tried to fit a whole image‑processing pipeline onto a 100‑pin CPLD. The board wouldn’t even power up! The lesson? Start with a device that matches the size of the job.

Look for a low‑pin count, low‑cost part

A good starting point is a 20‑ to 30‑pin FPGA from the Xilinx Artix‑7 or Intel Cyclone IV families. These chips cost under $5 in small quantities and have enough logic cells (usually a few thousand) for simple control, state‑machine, or peripheral‑interface tasks.

Check the development board

If you already have a dev board, great – use it. If not, the Digilent Basys 3 (Artix‑7, 33 k logic cells) is a solid, inexpensive option. It comes with a USB‑powered programmer, a few LEDs, switches, and a tiny VGA connector – perfect for quick demos.

Define the Project Scope – Keep It Minimal

The key to a minimal project is to limit yourself to one clear function. I like to call this the “one‑thing‑well‑done” rule. For this walkthrough we’ll build a pulse‑width modulated (PWM) LED dimmer that can be controlled by a push‑button and reports its duty cycle over a UART link. That’s three simple blocks: a button debouncer, a PWM generator, and a UART transmitter.

Why PWM?

PWM is a staple in embedded systems – it drives motors, LEDs, and even audio. Implementing it in an FPGA shows you how to use counters and state machines without drowning in code.

Set Up Your Toolchain

I use the free Vivado WebPACK for Xilinx devices and Quartus Prime Lite for Intel parts. Both run on Windows, macOS, and Linux. Install the IDE, then add the board’s support files (usually a zip you can download from the vendor’s site).

Create a new project

  1. Open the IDE and start a new project.
  2. Choose the exact FPGA part number (e.g., XC7A35T‑ICSG324).
  3. Set the language to Verilog – it’s concise and easy for beginners.

Write the Verilog Modules

Below are the three modules you need. Feel free to copy‑paste into separate files or keep them in one file – the IDE will handle it.

1. Button Debouncer

Mechanical switches bounce for a few milliseconds. A simple counter can filter that out.

module debouncer (
    input  clk,
    input  btn_raw,
    output reg btn_clean
);
    reg [15:0] cnt;
    reg btn_sync0, btn_sync1;

    // Synchronize to the clock domain
    always @(posedge clk) begin
        btn_sync0 <= btn_raw;
        btn_sync1 <= btn_sync0;
    end

    // Count stable periods
    always @(posedge clk) begin
        if (btn_sync1 == btn_clean)
            cnt <= 0;
        else begin
            cnt <= cnt + 1;
            if (cnt == 16'hFFFF) btn_clean <= btn_sync1;
        end
    end
endmodule

2. PWM Generator

We’ll use an 8‑bit counter for a 0‑255 duty cycle.

module pwm (
    input  clk,
    input  [7:0] duty,
    output pwm_out
);
    reg [7:0] cnt;
    always @(posedge clk) cnt <= cnt + 1;
    assign pwm_out = (cnt < duty);
endmodule

3. UART Transmitter (8‑N‑1, 115200 baud)

A minimal UART just shifts out bits when the line is idle.

module uart_tx (
    input        clk,
    input        start,
    input  [7:0] data,
    output reg   tx,
    output reg   busy
);
    parameter CLK_FREQ = 50000000; // 50 MHz board clock
    parameter BAUD = 115200;
    localparam DIV = CLK_FREQ / BAUD;

    reg [15:0] div_cnt;
    reg [3:0]  bit_cnt;
    reg [9:0]  shift_reg;

    always @(posedge clk) begin
        if (start && !busy) begin
            busy      <= 1;
            bit_cnt   <= 0;
            shift_reg <= {1'b1, data, 1'b0}; // start bit, data, stop bit
            div_cnt   <= 0;
        end else if (busy) begin
            if (div_cnt == DIV-1) begin
                div_cnt <= 0;
                tx      <= shift_reg[0];
                shift_reg <= {1'b1, shift_reg[9:1]};
                bit_cnt <= bit_cnt + 1;
                if (bit_cnt == 9) busy <= 0;
            end else begin
                div_cnt <= div_cnt + 1;
            end
        end else begin
            tx <= 1'b1; // idle state
        end
    end
endmodule

Connect the Blocks in a Top‑Level Module

Now we tie everything together.

module top (
    input  clk,          // 50 MHz from board
    input  btn,          // raw push‑button
    output led,          // PWM output to LED
    output uart_tx       // UART line
);
    wire btn_clean;
    wire [7:0] duty;
    wire tx_busy;

    // Debounce the button
    debouncer db(.clk(clk), .btn_raw(btn), .btn_clean(btn_clean));

    // Simple up‑counter for duty cycle, increments on each clean press
    reg [7:0] duty_reg;
    always @(posedge clk) begin
        if (btn_clean) duty_reg <= duty_reg + 8'd1;
    end
    assign duty = duty_reg;

    // PWM generator
    pwm pwm_inst(.clk(clk), .duty(duty), .pwm_out(led));

    // UART transmitter – send duty value as ASCII hex
    reg start_tx;
    reg [7:0] tx_data;
    uart_tx uart_inst(
        .clk(clk),
        .start(start_tx),
        .data(tx_data),
        .tx(uart_tx),
        .busy(tx_busy)
    );

    // Trigger UART every 100 ms
    reg [23:0] uart_timer;
    always @(posedge clk) begin
        uart_timer <= uart_timer + 1;
        if (uart_timer == 24'd5_000_000) begin // 0.1 s at 50 MHz
            uart_timer <= 0;
            start_tx   <= 1;
            tx_data    <= duty_reg; // raw value; you can convert to ASCII if you like
        end else begin
            start_tx <= 0;
        end
    end
endmodule

Synthesize, Implement, and Test

  1. Run synthesis – the IDE will turn your Verilog into a netlist.
  2. Implement design – place and route the logic onto the FPGA fabric.
  3. Generate bitstream – this is the file you load onto the board.

If the tool reports any “timing violations,” don’t panic. For a minimal design like this, you can usually fix it by lowering the clock or adding a simple constraint that tells the tool the clock is 50 MHz.

Load the Bitstream and See It Work

Plug the board into your PC, open the programmer, select the bitstream, and click “Program.”

  • Press the button – the LED should get brighter step by step.
  • Open a serial terminal (115200‑8‑N‑1) and you’ll see a stream of numbers representing the current duty cycle.

That’s it! You’ve just built a complete embedded system on an FPGA: input, processing, output, and communication.

Lessons Learned and Tips for the Next Project

  • Start small. A single‑function design lets you learn the flow without getting lost.
  • Reuse code. The debouncer and UART modules are useful in many projects; keep them in a personal library.
  • Watch the clock. Even a tiny design can run into timing problems if you forget to constrain the clock source.
  • Document as you go. I keep a simple text file next to each project that notes the part number, tool versions, and any quirks I ran into. It saves a lot of head‑scratching later.

At Logic Design Lab we love showing how a few lines of code can bring hardware to life. The next time you need a custom peripheral, try the same minimal‑first approach – you’ll be surprised how much you can do with a modest FPGA.

Reactions
Do you have any feedback or ideas on how we can improve this page?