未分类

The basic syntax of the field-programmable gate array hardware description language

FPGA HDL Basic Syntax: The Grammar You Actually Need to Write Working Code

Learning an HDL is not like learning a software language. There is no standard library to import. There is no runtime to abstract away the hardware. Every line you write becomes a physical circuit, and if your syntax is wrong, the tool either rejects the design or builds something that does not do what you thought it did.

This guide covers the core syntax patterns you will use every single day in FPGA development. Not the obscure edge cases. Not the academic exercises. The stuff that actually maps to hardware and actually works.


The Two Big Languages and Why It Does Not Matter Which One You Pick

Verilog and VHDL are the two dominant hardware description languages in the FPGA world. They do the same thing. They describe the same hardware. The syntax looks different, but the underlying concepts are identical.

Pick one. Learn it well. Move on.

Most new FPGA developers start with Verilog because the syntax looks more like C, which makes the learning curve feel less steep. VHDL is more verbose but also more explicit, which catches certain classes of bugs at compile time that Verilog lets slide. Neither one is better. The tools support both equally, and the job market does not care which one you use.

This article uses Verilog syntax for all examples because it is more concise and easier to read in a web format. The concepts translate directly to VHDL if that is what your team uses.


Module Declaration: The Container for Everything You Build

Every piece of synthesizable code lives inside a module. A module is like a function in software, except it describes hardware, not a sequence of instructions.

1module counter (
2    input wire clk,
3    input wire rst,
4    input wire enable,
5    output reg [7:0] count
6);
7

The port list defines the interface. input wire means the signal comes into the module. output reg means the signal leaves the module and is driven by a procedural block. The reg keyword here does not mean “register” in the hardware sense. It means the signal is assigned inside an always block. This confuses everyone at first. Get used to it.

Named port connections are safer than positional ones. When you instantiate a module, always use named connections:

1counter u_count (
2    .clk(sys_clk),
3    .rst(reset_n),
4    .enable(enable_signal),
5    .count(counter_value)
6);
7

Positional connections work until they do not, and when they break, the error message points to the instantiation line, not to the actual mismatch. Named connections save you from that headache.


Always Blocks: Where Your Logic Actually Lives

The always block is the heart of Verilog. It describes behavior. What happens inside the block depends on the sensitivity list, which is the part in parentheses after the @ symbol.

Sequential Logic with Clock Edges

For flip-flops and registered outputs, use a clock edge in the sensitivity list:

1always @(posedge clk or posedge rst) begin
2    if (rst)
3        count <= 8'b0;
4    else if (enable)
5        count <= count + 1;
6end
7

The <= operator is non-blocking assignment. This is critical. Non-blocking assignment means all right-hand sides are evaluated first, then all left-hand sides are updated simultaneously. This matches how real flip-flops behave: every register captures its input at the same clock edge.

If you use = (blocking assignment) inside a clocked always block, you create race conditions. The simulator will execute statements in order, which does not match real hardware. The code might simulate correctly but synthesize into something broken.

Combinational Logic with Full Sensitivity

For combinational logic, use @(*) or list every signal in the sensitivity list:

1always @(*) begin
2    if (enable)
3        next_count = count + 1;
4    else
5        next_count = count;
6end
7

The @(*) tells the tool to automatically include every signal that appears on the right-hand side. This is the safest option. If you manually list signals and forget one, the simulation and synthesis will disagree, and you will spend hours tracking down a bug that is not really a bug.

Use = (blocking assignment) for combinational logic. Blocking assignment executes in order, which is exactly what combinational logic does: the output depends immediately on the inputs.


Data Types and Widths: Getting the Bit Count Right

FPGA hardware is fixed-width. Every signal has a specific number of bits, and if you do not think about widths explicitly, the tool will make decisions for you, and those decisions will probably not be what you wanted.

Wires, Regs, and When to Use Each

wire is for combinational signals. It cannot hold a value; it only reflects what drives it. reg is for signals that hold a value, typically inside an always block. Despite the name, reg does not always synthesize to a physical register. If the sensitivity list is @(*), the reg becomes combinational logic.

The rule of thumb: use wire for everything that connects modules together. Use reg only inside always blocks. This keeps your code readable and matches how the synthesis tool interprets your intent.

Sizing Your Buses Explicitly

1wire [7:0] data_bus;
2reg [3:0] state;
3

The range [7:0] means 8 bits, with bit 7 as the most significant bit. This is the convention you will see everywhere. Some tools accept [0:7] as well, but [7:0] is the standard.

When you concatenate signals, use the curly brace syntax:

1assign full_word = {high_byte, low_byte};
2

When you slice a bus, use square brackets:

1assign lower_nibble = data_bus[3:0];
2

Get comfortable with these two operators. You will use them in every single module you write.


Conditional Logic: If-Else and Case Statements

If-Else Chains Synthesize to Multiplexers

An if-else chain inside a combinational always block becomes a cascade of multiplexers in hardware:

1always @(*) begin
2    if (sel == 2'b00)
3        out = a;
4    else if (sel == 2'b01)
5        out = b;
6    else if (sel == 2'b10)
7        out = c;
8    else
9        out = d;
10end
11

This is clean and readable. The synthesis tool turns it into a 4-to-1 mux. No surprises.

Case Statements Are Better for Decoders

When you have a signal that selects between many options, a case statement is clearer and synthesizes more efficiently:

1always @(*) begin
2    case (opcode)
3        4'b0000: result = a + b;
4        4'b0001: result = a - b;
5        4'b0010: result = a & b;
6        4'b0011: result = a | b;
7        default: result = 0;
8    endcase
9end
10

The default branch is mandatory. Without it, the tool infers a latch to hold the previous value. Latches in FPGA designs are almost never what you want, and they cause timing analysis failures because the tool cannot predict when the latch is transparent.

Always include default. Even when you think every case is covered. Especially then.


Generate Blocks: Writing Code That Writes Code

When you need to replicate a piece of logic N times, do not copy-paste it. Use a generate block:

1genvar i;
2generate
3    for (i = 0; i < 8; i = i + 1) begin : gen_stage
4        always @(posedge clk) begin
5            if (rst)
6                stage[i] <= 0;
7            else
8                stage[i] <= stage[i-1];
9        end
10    end
11endgenerate
12

The genvar is a special variable used only at elaboration time. The loop does not execute at runtime. It tells the synthesis tool to create 8 copies of the always block, each with its own index. This is how you build shift registers, pipeline stages, and replicated logic without writing the same code eight times.

Generate blocks also work with if statements, which lets you conditionally include or exclude hardware based on parameters. This is how you build configurable designs where different variants share the same source code.


Parameters and Localparam: Making Your Code Reusable

Hardcoded numbers in RTL are a maintenance nightmare. If you need to change a bus width or a counter limit, you do not want to hunt through 50 files to find every instance.

Use parameter for values that change between instantiations:

1module fifo #(
2    parameter DEPTH = 16,
3    parameter WIDTH = 8
4)(
5    input wire clk,
6    input wire [WIDTH-1:0] data_in,
7    output wire [WIDTH-1:0] data_out
8);
9

Use localparam for values that are constant within a module:

1localparam IDLE = 2'b00;
2localparam RUN  = 2'b01;
3localparam DONE = 2'b10;
4

localparam cannot be overridden from outside the module. This makes it safe for internal state encoding. parameter can be set at instantiation time, which makes it ideal for configurable modules like FIFOs, counters, and state machines.


Tasks and Functions: Reusable Code Inside Modules

Verilog lets you define reusable code blocks inside modules. The difference between a task and a function matters for synthesis.

A function executes in zero simulation time and returns a single value. It cannot contain timing controls like @(posedge clk). Functions are pure combinational logic and synthesize cleanly:

1function [7:0] max;
2    input [7:0] a;
3    input [7:0] b;
4    begin
5        max = (a > b) ? a : b;
6    end
7endfunction
8

A task can contain timing controls and can call other tasks. Tasks do not synthesize. They exist only for simulation. Use tasks to build testbenches, not synthesizable logic.

The rule: if it needs to become hardware, write it as a function or inline it. If it is only for simulation, use a task.


Common Syntax Mistakes That Break Your Design

Using = instead of <= in clocked blocks. This is the single most common mistake in Verilog code. It causes simulation-synthesis mismatches that are extremely hard to debug.

Forgetting the begin and end keywords. If your always block has more than one statement, you need begin and end. Without them, only the first statement is part of the block. The rest executes unconditionally. This compiles. It just does not do what you think.

Mixing signed and unsigned without thinking. By default, all Verilog values are unsigned. If you need signed arithmetic, declare the signal as signed. Mixing signed and unsigned in the same expression produces results that are technically correct but almost never what you intended.

Not resetting everything. If a signal does not have a value after reset, the FPGA powers up with whatever was in that memory cell. This is random. Every register that holds state must have a defined reset value. No exceptions.

ChipApex is a global distributor of electronic components: ICs, semiconductors, passives & interconnects. Source active & obsolete parts with wholesale pricing, fast RFQ response, and worldwide delivery.Official website address:chipapex.com

Related Articles

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Back to top button