Learning FPGA development can feel overwhelming at first, especially when transitioning from software programming to hardware description languages like VHDL. Yet the best way to build confidence—and real skills—is through hands-on practice with simple, progressive projects. This post gathers 10 beginner-friendly FPGA projects designed to help you understand digital logic, timing circuits, finite state machines, communication protocols, display control, and more.
Each project focuses on a core concept that every FPGA developer must master, from basic logic gates and counters to UART communication and PWM signal generation. Whether you're a student, hobbyist, or aspiring embedded systems engineer, these projects offer a clear path to learning by doing—using real hardware, real circuits, and real VHDL code.
By following this collection, you’ll gradually build a strong foundation that prepares you for more advanced FPGA work such as DSP processing, soft-core CPUs, and high-speed communication systems. Let’s explore the world of programmable logic, one project at a time.
Nice, let’s turn this into a mini “project pack” you can actually drop into Vivado/Quartus.
🔧 Assumptions for all projects
- 1 system clock input
clk- Active-high push buttons / switches
- Common-anode or common-cathode 7-segment as noted (you can adapt)
- VHDL-2008-ish style using
numeric_std- You’ll add your own
.xdc/.qsfpin constraints
At the top of every VHDL file, include:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
I’ll use a generic CLK_FREQ so you can adapt to 50 MHz, 100 MHz, etc.
Goal: 4 switches → simple logic expressions → 4 LEDs.
Concepts: entity/architecture, combinational logic.
entity logic_playground is
Port (
sw : in STD_LOGIC_VECTOR(3 downto 0);
led : out STD_LOGIC_VECTOR(3 downto 0)
);
end logic_playground;
architecture Behavioral of logic_playground is
begin
-- led(0) = AND of sw(0) and sw(1)
led(0) <= sw(0) and sw(1);
-- led(1) = OR of sw(0) and sw(1)
led(1) <= sw(0) or sw(1);
-- led(2) = XOR of sw(2) and sw(3)
led(2) <= sw(2) xor sw(3);
-- led(3) = (sw(0) AND NOT sw(2)) OR sw(3)
led(3) <= (sw(0) and not sw(2)) or sw(3);
end Behavioral;
Goal: Show a 4-bit counter on led(3 downto 0).
Concepts: clocked process, unsigned counter, generic clock divider.
entity led_counter is
generic (
CLK_FREQ : integer := 50_000_000; -- Hz
TICK_HZ : integer := 2 -- count increments per second
);
Port (
clk : in STD_LOGIC;
led : out STD_LOGIC_VECTOR(3 downto 0)
);
end led_counter;
architecture Behavioral of led_counter is
constant DIVIDER_MAX : integer := CLK_FREQ / TICK_HZ;
signal div_cnt : unsigned(31 downto 0) := (others => '0');
signal tick : STD_LOGIC := '0';
signal count4 : unsigned(3 downto 0) := (others => '0');
begin
-- clock divider
process(clk)
begin
if rising_edge(clk) then
if div_cnt = DIVIDER_MAX - 1 then
div_cnt <= (others => '0');
tick <= '1';
else
div_cnt <= div_cnt + 1;
tick <= '0';
end if;
end if;
end process;
-- 4-bit counter
process(clk)
begin
if rising_edge(clk) then
if tick = '1' then
count4 <= count4 + 1;
end if;
end if;
end process;
led <= std_logic_vector(count4);
end Behavioral;
Goal: Press button → LED toggles state once per press.
Concepts: input sync, edge detect, simple debounce.
entity button_toggle is
generic (
CLK_FREQ : integer := 50_000_000;
DEBOUNCE_MS : integer := 20
);
Port (
clk : in STD_LOGIC;
btn : in STD_LOGIC; -- active-high push button
led : out STD_LOGIC
);
end button_toggle;
architecture Behavioral of button_toggle is
constant DEBOUNCE_MAX : integer := CLK_FREQ / 1000 * DEBOUNCE_MS;
signal btn_sync1, btn_sync2 : STD_LOGIC := '0';
signal stable_cnt : unsigned(31 downto 0) := (others => '0');
signal btn_clean : STD_LOGIC := '0';
signal btn_prev : STD_LOGIC := '0';
signal led_reg : STD_LOGIC := '0';
begin
-- synchronize button to clk
process(clk)
begin
if rising_edge(clk) then
btn_sync1 <= btn;
btn_sync2 <= btn_sync1;
end if;
end process;
-- debounce: only change when stable long enough
process(clk)
begin
if rising_edge(clk) then
if btn_sync2 = btn_clean then
stable_cnt <= (others => '0');
else
if stable_cnt = DEBOUNCE_MAX - 1 then
btn_clean <= btn_sync2;
stable_cnt <= (others => '0');
else
stable_cnt <= stable_cnt + 1;
end if;
end if;
end if;
end process;
-- detect rising edge of clean signal
process(clk)
begin
if rising_edge(clk) then
if btn_clean = '1' and btn_prev = '0' then
led_reg <= not led_reg;
end if;
btn_prev <= btn_clean;
end if;
end process;
led <= led_reg;
end Behavioral;
Goal: Light moves left and right across 8 LEDs.
Concepts: shift registers, simple direction control.
entity knight_rider is
generic (
CLK_FREQ : integer := 50_000_000;
STEP_HZ : integer := 5
);
Port (
clk : in STD_LOGIC;
led : out STD_LOGIC_VECTOR(7 downto 0)
);
end knight_rider;
architecture Behavioral of knight_rider is
constant DIV_MAX : integer := CLK_FREQ / STEP_HZ;
signal div_cnt : unsigned(31 downto 0) := (others => '0');
signal tick : STD_LOGIC := '0';
signal pattern : STD_LOGIC_VECTOR(7 downto 0) := "00000001";
signal dir : STD_LOGIC := '0'; -- 0 = right, 1 = left
begin
-- clock divider
process(clk)
begin
if rising_edge(clk) then
if div_cnt = DIV_MAX - 1 then
div_cnt <= (others => '0');
tick <= '1';
else
div_cnt <= div_cnt + 1;
tick <= '0';
end if;
end if;
end process;
-- move LED
process(clk)
begin
if rising_edge(clk) then
if tick = '1' then
if dir = '0' then
pattern <= pattern(6 downto 0) & '0';
if pattern(6) = '1' then
dir <= '1';
end if;
else
pattern <= '0' & pattern(7 downto 1);
if pattern(1) = '1' then
dir <= '0';
end if;
end if;
end if;
end if;
end process;
led <= pattern;
end Behavioral;
Goal: Show 0–F repeatedly on a single 7-seg display.
Concepts: combinational decode, counter, clock divider.
Assume common-anode display (segment low turns ON). Adapt as needed.
entity sevenseg_hex is
generic (
CLK_FREQ : integer := 50_000_000;
STEP_HZ : integer := 2
);
Port (
clk : in STD_LOGIC;
seg : out STD_LOGIC_VECTOR(6 downto 0) -- a b c d e f g
-- add 'an' or 'dp' ports as needed
);
end sevenseg_hex;
architecture Behavioral of sevenseg_hex is
constant DIV_MAX : integer := CLK_FREQ / STEP_HZ;
signal div_cnt : unsigned(31 downto 0) := (others => '0');
signal digit : unsigned(3 downto 0) := (others => '0');
begin
-- divider and digit counter
process(clk)
begin
if rising_edge(clk) then
if div_cnt = DIV_MAX - 1 then
div_cnt <= (others => '0');
digit <= digit + 1;
else
div_cnt <= div_cnt + 1;
end if;
end if;
end process;
-- hex to 7-segment (common-anode)
with digit select
seg <=
"0000001" when "0000", -- 0
"1001111" when "0001", -- 1
"0010010" when "0010", -- 2
"0000110" when "0011", -- 3
"1001100" when "0100", -- 4
"0100100" when "0101", -- 5
"0100000" when "0110", -- 6
"0001111" when "0111", -- 7
"0000000" when "1000", -- 8
"0000100" when "1001", -- 9
"0001000" when "1010", -- A
"1100000" when "1011", -- b
"0110001" when "1100", -- C
"1000010" when "1101", -- d
"0110000" when "1110", -- E
"0111000" when others; -- F
end Behavioral;
Goal: Adjust LED brightness through duty-cycle steps using two buttons.
Concepts: PWM, comparing counter against duty value.
entity pwm_dimmer is
generic (
CLK_FREQ : integer := 50_000_000;
PWM_HZ : integer := 1000
);
Port (
clk : in STD_LOGIC;
btn_up : in STD_LOGIC;
btn_down : in STD_LOGIC;
led : out STD_LOGIC
);
end pwm_dimmer;
architecture Behavioral of pwm_dimmer is
constant PWM_MAX : integer := CLK_FREQ / PWM_HZ;
signal pwm_cnt : unsigned(15 downto 0) := (others => '0');
signal duty : unsigned(15 downto 0) := (others => '0');
signal led_reg : STD_LOGIC := '0';
signal btnu_prev, btnd_prev : STD_LOGIC := '0';
begin
-- simple PWM counter
process(clk)
begin
if rising_edge(clk) then
if pwm_cnt = PWM_MAX - 1 then
pwm_cnt <= (others => '0');
else
pwm_cnt <= pwm_cnt + 1;
end if;
if pwm_cnt < duty then
led_reg <= '1';
else
led_reg <= '0';
end if;
end if;
end process;
-- adjust duty on button rising edge (no fancy debounce here)
process(clk)
begin
if rising_edge(clk) then
if btn_up = '1' and btnu_prev = '0' then
if duty < to_unsigned(PWM_MAX - PWM_MAX/16, duty'length) then
duty <= duty + to_unsigned(PWM_MAX/16, duty'length);
end if;
end if;
if btn_down = '1' and btnd_prev = '0' then
if duty > to_unsigned(PWM_MAX/16, duty'length) then
duty <= duty - to_unsigned(PWM_MAX/16, duty'length);
end if;
end if;
btnu_prev <= btn_up;
btnd_prev <= btn_down;
end if;
end process;
led <= led_reg;
end Behavioral;
Goal: 3 LEDs (Red, Yellow, Green) change with fixed timing.
Concepts: FSM with timer.
entity traffic_light is
generic (
CLK_FREQ : integer := 50_000_000;
GREEN_SEC : integer := 5;
YELLOW_SEC : integer := 2;
RED_SEC : integer := 5
);
Port (
clk : in STD_LOGIC;
red, yellow, green : out STD_LOGIC
);
end traffic_light;
architecture Behavioral of traffic_light is
type state_type is (S_GREEN, S_YELLOW, S_RED);
signal state : state_type := S_GREEN;
constant T_GREEN : integer := CLK_FREQ * GREEN_SEC;
constant T_YELLOW : integer := CLK_FREQ * YELLOW_SEC;
constant T_RED : integer := CLK_FREQ * RED_SEC;
signal timer : unsigned(31 downto 0) := (others => '0');
begin
process(clk)
begin
if rising_edge(clk) then
case state is
when S_GREEN =>
if timer = T_GREEN - 1 then
state <= S_YELLOW;
timer <= (others => '0');
else
timer <= timer + 1;
end if;
when S_YELLOW =>
if timer = T_YELLOW - 1 then
state <= S_RED;
timer <= (others => '0');
else
timer <= timer + 1;
end if;
when S_RED =>
if timer = T_RED - 1 then
state <= S_GREEN;
timer <= (others => '0');
else
timer <= timer + 1;
end if;
end case;
end if;
end process;
-- outputs
red <= '1' when state = S_RED else '0';
yellow <= '1' when state = S_YELLOW else '0';
green <= '1' when state = S_GREEN else '0';
end Behavioral;
Goal: Send a single byte repeatedly over TX line.
Concepts: baud-rate generator, serial shift FSM.
(For real use you’d send a string; this is minimal.)
entity uart_tx_byte is
generic (
CLK_FREQ : integer := 50_000_000;
BAUD : integer := 115200;
DATA : std_logic_vector(7 downto 0) := x"48" -- 'H'
);
Port (
clk : in STD_LOGIC;
tx : out STD_LOGIC
);
end uart_tx_byte;
architecture Behavioral of uart_tx_byte is
constant BAUD_DIV : integer := CLK_FREQ / BAUD;
type state_type is (IDLE, START, DATA_BITS, STOP);
signal state : state_type := IDLE;
signal baud_cnt : unsigned(15 downto 0) := (others => '0');
signal bit_idx : unsigned(2 downto 0) := (others => '0');
signal tx_reg : STD_LOGIC := '1'; -- idle high
begin
process(clk)
begin
if rising_edge(clk) then
case state is
when IDLE =>
-- immediately start new frame
state <= START;
baud_cnt <= (others => '0');
bit_idx <= (others => '0');
when START =>
tx_reg <= '0'; -- start bit
if baud_cnt = BAUD_DIV - 1 then
baud_cnt <= (others => '0');
state <= DATA_BITS;
else
baud_cnt <= baud_cnt + 1;
end if;
when DATA_BITS =>
tx_reg <= DATA(to_integer(bit_idx));
if baud_cnt = BAUD_DIV - 1 then
baud_cnt <= (others => '0');
if bit_idx = "111" then
state <= STOP;
else
bit_idx <= bit_idx + 1;
end if;
else
baud_cnt <= baud_cnt + 1;
end if;
when STOP =>
tx_reg <= '1';
if baud_cnt = BAUD_DIV - 1 then
baud_cnt <= (others => '0');
state <= IDLE;
else
baud_cnt <= baud_cnt + 1;
end if;
end case;
end if;
end process;
tx <= tx_reg;
end Behavioral;
Goal: Generate square-wave audio at a given frequency; select tone with switches.
Concepts: frequency generation, lookup table.
entity tone_generator is
generic (
CLK_FREQ : integer := 50_000_000
);
Port (
clk : in STD_LOGIC;
sw : in STD_LOGIC_VECTOR(1 downto 0); -- select tone
buz : out STD_LOGIC
);
end tone_generator;
architecture Behavioral of tone_generator is
-- frequencies for notes (approx): 440, 880, 1.76k, 3.52k
type int_array is array (0 to 3) of integer;
constant NOTE_FREQ : int_array := (440, 880, 1760, 3520);
signal half_period : integer := CLK_FREQ / (2 * NOTE_FREQ(0));
signal cnt : unsigned(31 downto 0) := (others => '0');
signal buz_reg : STD_LOGIC := '0';
begin
-- update half_period based on switches
process(sw)
begin
case sw is
when "00" => half_period <= CLK_FREQ / (2 * NOTE_FREQ(0));
when "01" => half_period <= CLK_FREQ / (2 * NOTE_FREQ(1));
when "10" => half_period <= CLK_FREQ / (2 * NOTE_FREQ(2));
when others => half_period <= CLK_FREQ / (2 * NOTE_FREQ(3));
end case;
end process;
process(clk)
begin
if rising_edge(clk) then
if cnt = to_unsigned(half_period - 1, cnt'length) then
cnt <= (others => '0');
buz_reg <= not buz_reg;
else
cnt <= cnt + 1;
end if;
end if;
end process;
buz <= buz_reg;
end Behavioral;
Goal: Count 00–99 on 2 digits using multiplexing.
Concepts: time-multiplexing, BCD counters, resource reuse.
entity counter_2digit is
generic (
CLK_FREQ : integer := 50_000_000;
STEP_HZ : integer := 1;
REFRESH_HZ : integer := 1000 -- multiplex freq per digit
);
Port (
clk : in STD_LOGIC;
seg : out STD_LOGIC_VECTOR(6 downto 0); -- segments
an : out STD_LOGIC_VECTOR(1 downto 0) -- digit enables
);
end counter_2digit;
architecture Behavioral of counter_2digit is
constant STEP_DIV : integer := CLK_FREQ / STEP_HZ;
constant REFRESH_DIV : integer := CLK_FREQ / (REFRESH_HZ * 2);
signal step_cnt : unsigned(31 downto 0) := (others => '0');
signal refresh_cnt : unsigned(31 downto 0) := (others => '0');
signal ones, tens : unsigned(3 downto 0) := (others => '0');
signal current_digit : STD_LOGIC := '0'; -- 0=ones,1=tens
signal bcd : unsigned(3 downto 0);
begin
-- 0.5s or 1s decimal counter update
process(clk)
begin
if rising_edge(clk) then
if step_cnt = STEP_DIV - 1 then
step_cnt <= (others => '0');
if ones = 9 then
ones <= (others => '0');
if tens = 9 then
tens <= (others => '0');
else
tens <= tens + 1;
end if;
else
ones <= ones + 1;
end if;
else
step_cnt <= step_cnt + 1;
end if;
end if;
end process;
-- multiplex between ones and tens
process(clk)
begin
if rising_edge(clk) then
if refresh_cnt = REFRESH_DIV - 1 then
refresh_cnt <= (others => '0');
current_digit <= not current_digit;
else
refresh_cnt <= refresh_cnt + 1;
end if;
end if;
end process;
bcd <= ones when current_digit = '0' else tens;
-- select which digit is on (common-anode example: '0' = ON)
an <= "10" when current_digit = '0' else "01";
-- BCD to 7-seg (reuse mapping from project 5)
with bcd select
seg <=
"0000001" when "0000",
"1001111" when "0001",
"0010010" when "0010",
"0000110" when "0011",
"1001100" when "0100",
"0100100" when "0101",
"0100000" when "0110",
"0001111" when "0111",
"0000000" when "1000",
"0000100" when "1001",
"1111110" when others; -- blank
end Behavioral;