Beginner · Lesson 5 of 10

Your first HDL module

The anatomy of a Verilog module and a VHDL entity, ports, wires, regs and signals, continuous and clocked assignment, and a complete blinking-LED counter in both languages explained line by line

Beginner35 minUpdated 13 Nov 2026

You will learn

  • Describe the structure of a Verilog module and a VHDL entity and architecture, and map each part to the other language
  • Explain the difference between a continuous assignment and a clocked assignment and what hardware each produces
  • Write a parameterised blinking-LED counter in Verilog and VHDL and explain every line of it
  • Use a parameter or generic to make a module reusable across clock frequencies

Before this: registers clocks and state

You now know what a LUT does (lesson 03) and what a register does (lesson 04). This lesson is where you write the file that describes them. It takes one small design, a counter that blinks an LED once a second, and builds it in Verilog and VHDL at the same time, line by line. By the end you will be able to read either language well enough to follow the rest of this track, and to write a module of your own.

The shape of a module

An HDL file describes one block of hardware with a boundary. In Verilog the block is a module. In VHDL it is an entity (the boundary) paired with an architecture (the contents). Both say three things: the name of the block, the ports that cross its boundary, and what happens inside.

blinkCLK_HZ, BLINK_HZclkrstcountregister, N bits== HALF-1comparatorledregisterwrapwrap resets count, else count + 1led
The blink module as a box. Two inputs, one output, and inside it a counter register, a comparator that detects the terminal count, and the LED register that toggles when the counter wraps.

Here is the complete Verilog version. Read it once now, then read the explanation, then read it again.

module blink #(
  parameter CLK_HZ   = 100_000_000,
  parameter BLINK_HZ = 1
) (
  input  wire clk,
  input  wire rst,
  output reg  led
);
  localparam HALF_PERIOD = CLK_HZ / (2 * BLINK_HZ);
  localparam CNT_WIDTH   = $clog2(HALF_PERIOD);

  reg [CNT_WIDTH-1:0] count;

  always @(posedge clk) begin
    if (rst) begin
      count <= 0;
      led   <= 1'b0;
    end else if (count == HALF_PERIOD - 1) begin
      count <= 0;
      led   <= ~led;
    end else begin
      count <= count + 1'b1;
    end
  end
endmodule

And the same hardware in VHDL.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity blink is
  generic (
    CLK_HZ   : positive := 100_000_000;
    BLINK_HZ : positive := 1
  );
  port (
    clk : in  std_logic;
    rst : in  std_logic;
    led : out std_logic
  );
end entity;

architecture rtl of blink is
  constant HALF_PERIOD : natural := CLK_HZ / (2 * BLINK_HZ);
  signal count : natural range 0 to HALF_PERIOD - 1 := 0;
  signal led_r : std_logic := '0';
begin
  process (clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        count <= 0;
        led_r <= '0';
      elsif count = HALF_PERIOD - 1 then
        count <= 0;
        led_r <= not led_r;
      else
        count <= count + 1;
      end if;
    end if;
  end process;

  led <= led_r;
end architecture;

Line by line

Header and parameters

Verilog opens with module blink, followed by an optional parameter list in #( ... ) and then the port list in ( ... ). A parameter is a compile-time constant that whoever uses the module can override. The two here say how fast the clock is and how fast the LED should blink. The defaults, 100 MHz and 1 Hz, suit the Basys 3 and many other boards. Underscores inside numbers are ignored and make large values readable.

VHDL opens with two library and use lines that import the standard logic types. Almost every VHDL file begins this way. std_logic_1164 provides the std_logic type and numeric_std provides arithmetic on vectors. Then the entity declares the generics (the VHDL word for parameters) and the ports. Note that VHDL generics have types: positive is an integer that must be at least 1, which gives you a compile error if someone passes 0 and would otherwise cause a division by zero.

Ports

Each port has a direction and a type. In Verilog, input wire clk is a 1-bit input, and output reg led is a 1-bit output that will be assigned inside an always block. In VHDL, clk : in std_logic and led : out std_logic say the same. std_logic is a 1-bit type with nine possible values, of which you will mostly use ‘0’, ‘1’ and occasionally ‘Z’ for high impedance and ‘X’ for unknown in simulation.

Multi-bit ports are declared as vectors: input wire [7:0] data in Verilog, data : in std_logic_vector(7 downto 0) in VHDL. Both put bit 7 on the left. The add_or_pass module in lesson 03 shows them in use.

Wires, regs and signals

Inside the module you declare the internal connections. This is the point where Verilog’s naming misleads almost every beginner. Verilog has two kinds of net: wire and reg. A wire can only be driven by a continuous assign statement or by the output of another module. A reg can only be assigned inside an always or initial block. The name reg does not mean “register”. A reg assigned in a clocked always block becomes a flip-flop. A reg assigned in a combinational always block becomes a LUT. What decides the hardware is the block, not the keyword. SystemVerilog introduced logic, which can be used in both places, and removes the confusion. Vivado, Quartus Prime and Yosys all accept it.

VHDL has one kind of internal connection, the signal, which can be driven from a process or from a concurrent statement, and the hardware is again decided by where the assignment is written. The only subtlety is that in VHDL before 2008 an out port cannot be read inside the architecture. That is why the code uses an internal signal led_r for the toggle and copies it to the port at the end. The habit is worth keeping even when the tool accepts VHDL-2008, because it makes the register explicit.

The two localparam lines in Verilog and the constant line in VHDL compute derived constants. HALF_PERIOD is the number of clock cycles in half a blink period: at 100 MHz and 1 Hz it is 50,000,000. CNT_WIDTH in the Verilog version uses the built-in $clog2 function, ceiling of log base 2, to size the counter: 50,000,000 needs 26 bits. The VHDL version does not need the width because declaring count as a natural range 0 to HALF_PERIOD - 1 lets synthesis size it, which is a common VHDL idiom for counters.

The clocked block

Everything that happens in this design happens inside one block that runs on the rising edge of clk. In Verilog that is always @(posedge clk) begin ... end. In VHDL it is process (clk) begin if rising_edge(clk) then ... end if; end process;. Lesson 04 showed the same shape for a plain register. Every assignment inside this block creates a flip-flop for the signal on its left, so count becomes 26 flip-flops and led becomes one.

The body is a three-way priority. First, if rst is high, clear everything. Second, if the counter has reached its terminal value, HALF_PERIOD minus 1, wrap it to zero and flip the LED. Otherwise, add one. The comparison count == HALF_PERIOD - 1 is a 26-bit equality, which synthesis builds from a handful of LUTs, and count + 1 is a 26-bit incrementer on a carry chain. Exactly the hardware the diagram shows.

The order of the if branches is the priority. Reset wins over wrap, wrap wins over increment. You will write this pattern hundreds of times.

Assignment operators

Verilog has two assignment operators inside blocks. The non-blocking <= means “schedule this value to appear when the clock edge is done, and in the meantime keep using the old one”. All assignments in a clocked block should use it. The blocking = means “update right now, and later statements in the same block see the new value”, and belongs in combinational blocks and testbenches. Using = in a clocked block is a classic source of simulation-versus-hardware mismatch.

VHDL uses <= for all signal assignment, and inside a process it always behaves as non-blocking: the value appears when the process finishes, not immediately. The := operator is for variables and constants, and you will not need variables yet.

Outside any process, VHDL’s led <= led_r; is a concurrent assignment, continuously connecting the port to the internal signal, with no flip-flop. The Verilog equivalent is assign, which lesson 03 used.

Why toggle at the wrap

A beginner’s first attempt at this design often takes the top bit of a free-running counter as the LED. That works, but it only gives blink rates that are the clock divided by a power of two, and it tempts people into using the bit as a clock for something else, which lesson 04 warned against. Counting to an exact terminal value and toggling a separate register is the general pattern. The LED blinks at any rate you like, the counter’s wrap is a single-cycle event you can reuse as an enable, and clk remains the only clock.

Seeing it run

The timing diagram shows the design with HALF_PERIOD reduced to 3, so that a whole blink fits in a few cycles. On the board HALF_PERIOD is 50 million and you would wait half a second between LED changes, but the shape is identical.

The blink counter with HALF_PERIOD = 3. count climbs 0, 1, 2 and wraps. led toggles on the edge where count wraps, so one full blink period is 6 cycles.clkrstcount012012012led
The blink counter with HALF_PERIOD = 3. count climbs 0, 1, 2 and wraps. led toggles on the edge where count wraps, so one full blink period is 6 cycles.

Read it carefully against the code. During reset count is 0 and led is 0. After reset is released count increments at each edge. On the edge where count is 2, which equals HALF_PERIOD minus 1, the wrap branch fires: count goes back to 0 and led flips. Note that the LED changes on the edge after count reads 2, not during the cycle when it reads 2. That one-cycle delay between seeing a condition and acting on it is the register behaviour from lesson 04 and it is present in every synchronous design.

A testbench and what it tells you

Here is a small Verilog testbench that overrides the parameters to make the simulation short, releases reset, and counts the number of LED toggles. It passes the parameters with #(...) at the instantiation, which is how you reuse one module at different settings.

`timescale 1ns/1ps
module blink_tb;
  reg  clk = 1'b0;
  reg  rst = 1'b1;
  wire led;

  // Small numbers so the simulation finishes quickly: 1 kHz clock, 100 Hz blink
  blink #(.CLK_HZ(1000), .BLINK_HZ(100)) dut (.clk(clk), .rst(rst), .led(led));

  always #5 clk = ~clk;

  integer toggles = 0;
  always @(led) if (!rst) toggles = toggles + 1;

  initial begin
    repeat (2) @(posedge clk);
    rst <= 1'b0;
    repeat (52) @(posedge clk);   // 10 half-periods of 5 cycles, plus margin
    $display("led toggled %0d times (expect 10)", toggles);
    $finish;
  end
endmodule

With CLK_HZ at 1000 and BLINK_HZ at 100, HALF_PERIOD is 5, so the LED toggles every 5 cycles and 52 cycles contain 10 toggles. Run iverilog -g2012 -o blink.vvp blink.v blink_tb.v and vvp blink.vvp and you should see led toggled 10 times (expect 10).

The equivalent VHDL testbench uses a generic map at the instantiation. It is written in VHDL-2008, which nvc and GHDL both accept.

library ieee;
use ieee.std_logic_1164.all;

entity blink_tb is
end entity;

architecture sim of blink_tb is
  signal clk : std_logic := '0';
  signal rst : std_logic := '1';
  signal led : std_logic;
  signal done : boolean := false;
begin
  dut : entity work.blink
    generic map (CLK_HZ => 1000, BLINK_HZ => 100)
    port map (clk => clk, rst => rst, led => led);

  clk <= not clk after 5 ns when not done;

  stim : process
  begin
    wait for 20 ns;
    rst <= '0';
    wait for 500 ns;
    report "simulation finished";
    done <= true;
    wait;
  end process;
end architecture;

Open the waveform and count the toggles yourself. Lesson 06 turns this manual check into self-checking testbenches.

Parameters and generics in practice

The reason to parameterise is reuse. The same blink module serves a 12 MHz iCE40 board (CLK_HZ = 12_000_000), a 50 MHz Altera board and a 100 MHz AMD board without editing the file. It can also blink at 2 Hz for a heartbeat or 10 Hz for an alarm by changing BLINK_HZ. In a larger design you would instantiate it several times with different settings.

A few rules of thumb:

  • Parameters are for things that must be fixed when the hardware is built: widths, depths, clock rates, addresses. Anything that changes at run time must be a port.
  • Give every parameter a sensible default so the module can be used without specifying all of them.
  • Derive internal constants from parameters (localparam, constant) rather than repeating arithmetic in the body.
  • In VHDL, give generics a constrained type like positive or a subrange so that bad values fail at compile time. In SystemVerilog you can do the same with typed parameters.

Exercise

  1. Change blink so that the LED is on for one quarter of the period and off for three quarters, keeping the same blink frequency. You will need the counter to run over the whole period rather than half of it, and a comparison rather than a toggle for the LED. Keep both languages in step.
  2. Add a second output, tick, that is high for exactly one clock cycle each time the counter wraps. This is the enable pulse that lesson 04 recommended instead of a derived clock.
  3. Instantiate blink twice in a new top-level module, once at 1 Hz and once at 5 Hz, driving two LEDs, and simulate with the testbench above adapted to the new top level.

If you have a board, lesson 10 walks through building this exact design and loading it. You can jump ahead, build it, and come back.

Summary

  • A Verilog module and a VHDL entity plus architecture each declare a name, ports with directions and types, and a body.
  • Parameters (Verilog) and generics (VHDL) are compile-time constants that make a module reusable. Derived constants come from localparam and constant.
  • In Verilog, wire is driven by assign and reg is driven inside always. The block, not the keyword, decides whether the result is a LUT or a flip-flop. VHDL uses signal for both.
  • A clocked always @(posedge clk) or process (clk) with rising_edge makes flip-flops. Use non-blocking <= inside it, reset first, then the conditions in priority order.
  • The counter-to-terminal-count-then-toggle pattern gives any blink rate from a single clock and a one-cycle wrap event you can reuse as an enable.

Further reading