Getting Started with FPGA: DE10-Lite and Verilog, from Your First Blinking Circuit
🌏 繁體中文版:[學習FPGA] DE10-Lite + Verilog:從長出第一條電路開始
This post is about the "lowest-level" kind of programming there is — not instructions that run on a CPU, but a hardware description language that grows actual circuits. My learning platform is the Terasic DE10-Lite board, and the code examples below are written in Verilog, the hardware description language I used for my own lab work on it.
FPGA vs CPU: same silicon, two ways of working
A CPU is a general-purpose engine: one chip runs any program by following a stream of instructions. An FPGA (Field Programmable Gate Array) is more like a blank substrate — a grid of tens of thousands of configurable blocks (look-up tables plus flip-flops). Your code is translated into actual circuitry: once you write it, compile it, and program the board, the counter you described physically exists in the silicon. There is no fetch-and-decode step; on every clock cycle, all the circuits run at the same time.
That's why a 50 MHz clock can feed a counter directly. Verilog isn't a "program" in the usual sense; it's a hardware description. You describe the structure of a circuit and what happens on each clock edge, not a list of steps to execute in order.
What's on the DE10-Lite
- Intel (Altera) MAX 10 FPGA (10M50DAF484C7G): about 50,000 logic elements, plenty for every beginner project
- Six 7-segment displays (HEX0–HEX5): each one has its own pins and is active-low. They're the most direct way to see values inside your design
- 10 slide switches (SW), 10 red LEDs (LEDR), 2 push buttons (KEY0, KEY1). The buttons are active-low: they read 0 when pressed
- 50 MHz clock (MAX10_CLK1_50): the standard clock source for every starter project
- SDRAM, VGA output, an accelerometer, Arduino-style headers, and a 40-pin GPIO header — material for later projects (framebuffers, UART, video)
The toolchain: Quartus Prime
Intel's Quartus Prime is the "compiler" for this board: Verilog → synthesis → placement & routing → a .sof configuration file → programmed into the FPGA. The free Lite edition supports MAX 10 and is all you need for learning. The workflow is always the same:
- Project → Add File: add your
.vfile and select device 10M50DAF484C7G. - Pin Planner: map each port to a pin on the board — clock, LEDR[9..0], SW, KEY, HEX0–HEX3. (Terasic's System Builder tool can generate a project with all pins pre-assigned, which saves a lot of typing.)
- Compilation: run a full compile and check the timing report. 50 MHz means a 20 ns period, and starter designs usually have plenty of margin.
- Programmer: program over USB. Only now does your "circuit" actually start running.
Circuit #1: blink an LED
A 50 MHz clock is far too fast to see (50 million cycles per second), so the first step is always a clock divider. Count to 25 million and flip a bit: the bit toggles every 0.5 s, so the LED blinks at 1 Hz, which you can actually see.
module led_blink (
input wire clk, // 50 MHz (MAX10_CLK1_50)
output wire [9:0] led // LEDR
);
reg [24:0] cnt = 0; // 2^25 > 25M, wide enough
reg toggle = 0;
always @(posedge clk) begin
if (cnt == 25'd24_999_999) begin // toggle every 25M cycles = every 0.5 s -> 1 Hz blink
cnt <= 0;
toggle <= ~toggle;
end else begin
cnt <= cnt + 1'b1;
end
end
assign led = {10{toggle}};
endmodule
Three things to notice. (1) All the logic lives inside always @(posedge clk) and uses non-blocking <= assignments — the basic rule of synchronous design. (2) When the counter hits its limit, it resets to zero and flips a bit in the same clock edge. (3) {10{toggle}} (replication) wires all ten LEDs to the same signal at once. The moment you see ten LEDs blinking in perfect lockstep, the difference between an FPGA and a CPU becomes concrete.
Circuit #2: a counter on the 7-segment displays
This one is a milestone: three functions — a timing tick, a counter, and a display decoder — cooperate in one module, which is the beginning of modular design. The counter advances 10 times per second and shows its value in hex on HEX3–HEX0. Holding KEY0 resets it, and the switches are echoed to the LEDs.
module counter (
input wire clk, // 50 MHz (MAX10_CLK1_50)
input wire [1:0] key, // KEY[0] = reset, active-low
input wire [9:0] sw,
output wire [9:0] led,
output wire [6:0] hex0, hex1, hex2, hex3 // segments g..a, active-low
);
reg [22:0] div = 0; // 0 .. 4_999_999
reg tick = 0; // one-cycle pulse, 10 times per second
reg [15:0] count = 0;
// hex digit -> segments (bit 6 = g ... bit 0 = a), 0 = segment on
function [6:0] to7seg(input [3:0] d);
case (d)
4'h0: to7seg = 7'b1000000;
4'h1: to7seg = 7'b1111001;
4'h2: to7seg = 7'b0100100;
4'h3: to7seg = 7'b0110000;
4'h4: to7seg = 7'b0011001;
4'h5: to7seg = 7'b0010010;
4'h6: to7seg = 7'b0000010;
4'h7: to7seg = 7'b1111000;
4'h8: to7seg = 7'b0000000;
4'h9: to7seg = 7'b0010000;
4'hA: to7seg = 7'b0001000;
4'hB: to7seg = 7'b0000011;
4'hC: to7seg = 7'b1000110;
4'hD: to7seg = 7'b0100001;
4'hE: to7seg = 7'b0000110;
default: to7seg = 7'b0001110; // F
endcase
endfunction
// 10 Hz tick: a clock ENABLE, not a new clock
always @(posedge clk) begin
if (div == 23'd4_999_999) begin
div <= 0;
tick <= 1'b1;
end else begin
div <= div + 1'b1;
tick <= 1'b0;
end
end
// counter: +1 on each tick, KEY0 pressed (= 0) resets
always @(posedge clk) begin
if (!key[0])
count <= 0;
else if (tick)
count <= count + 1'b1;
end
assign led = sw;
assign hex0 = to7seg(count[3:0]);
assign hex1 = to7seg(count[7:4]);
assign hex2 = to7seg(count[11:8]);
assign hex3 = to7seg(count[15:12]);
endmodule
Two design choices are worth calling out. First, the slow "tick" is a clock enable: everything still runs on the one 50 MHz clock, and the counter only advances on cycles where tick is high. Generating a slow signal and using it as a new clock works in simple cases, but it's a classic source of timing problems, so it's a good habit to avoid it from day one. Second, try deriving the 7-segment table yourself: for each digit, work out which of segments a–g light up, then compare with the table above. It sticks much better than memorizing.
On the DE10-Lite each display has its own pins, so no multiplexing is needed. On boards where several digits share segment lines (common on cheaper boards), you'd scan the digits one at a time — the same persistence-of-vision trick as an LED cube.
My own lab results: switches in, numbers out
My lab exercises on this board used the same building blocks: reading the slide switches as a binary input, converting it into a number shown in decimal on the 7-segment displays, and using a push button to toggle the LEDs. The red LEDs above each switch mirror the input bits, so it's easy to check the conversion by eye.
Converting binary to decimal in hardware is a nice next step after the counter above: instead of four hex digits, you split the value into hundreds, tens, and ones (for example with the "double dabble" shift-and-add-3 algorithm) and feed each digit through the same 7-segment decoder.
A learning path, from easy to hard
- LED blink → two counters → traffic light (your first finite state machine)
- Clock-enable trees → timing constraints
- Synchronous vs asynchronous reset (on this board the keys are active-low; getting the polarity backwards doesn't raise an error, it just makes the design misbehave)
- VGA output with a framebuffer → your first external interface
- Advanced: UART / SPI / I²C → working alongside a microcontroller
Four classic beginner mistakes
- Writing Verilog like C: Verilog looks like C, but every
alwaysblock andassignruns concurrently, and sequential C-style thinking can produce combinational loops in hardware. Picture the circuit first, then write the code. - Mixing up
=and<=: in clocked blocks use non-blocking<=, so every register updates from the values before the clock edge. Blocking=in a clocked block is the most common source of designs that simulate one way and behave another. - Logic outside the clock edge: counters, decoders that feed registers, and anything with state belong in an
always @(posedge clk)block. Otherwise you get timing violations, and even a design that programs fine will glitch. - Forgetting active-low: on the DE10-Lite both the 7-segment segments and the push buttons are active-low. Drive a segment low to light it; a pressed key reads 0. Get it backwards and the displays go dark or show garbage, and beginners often assume the board is broken when it's just polarity.
Wrapping up
From the moment the DE10-Lite powers up, the subject shifts from "writing programs" to "growing circuits". It's a small, sturdy board: get the LED blink and the 7-segment counter working, and everything after that gets much easier.
This post is adapted from the Traditional Chinese original on Vic's Iteration Notes.
Comments
Post a Comment