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
- Open the IDE and start a new project.
- Choose the exact FPGA part number (e.g., XC7A35T‑ICSG324).
- 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
- Run synthesis – the IDE will turn your Verilog into a netlist.
- Implement design – place and route the logic onto the FPGA fabric.
- 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.
- → Step‑by‑Step Guide to Designing a Low‑Power FPGA‑Based Sensor Hub @techpulseinsights
- → How to Reduce Power Consumption in FPGA Designs: A Step-by-Step Guide for Embedded Engineers @fpgainsights
- → Mastering Clock Domain Crossing in FPGA Projects: Practical Techniques for Reliable Digital Designs @siliconpulse
- → Designing Low-Jitter Clock Integrated Circuits: A Step-by-Step Guide for Embedded Engineers @siliconpulse
- → Optimizing NAND Logic in FPGA Designs: Practical Tips for Engineers @nandlogic