The file at the end of this lesson is short: a 4-bit register with an enable input and a reset input, plus the test code that drives it. Nothing in it looks like hardware. There are no gates, no wires, no transistors, only assignments and conditions.
What does a few lines of RTL actually describe?
RTL means register-transfer level. It describes hardware as registers and the logic that computes the next value of each register. A register is storage that updates once per clock edge: a small group of memory cells that all capture their inputs together. A clock edge is the moment the clock signal rises from 0 to 1. In this lesson every register captures on the rising edge, and nothing changes between edges.
The logic between registers comes in two kinds. Combinational logic computes its outputs from its current inputs and has no memory: change an input and the output follows after the gate delays. Sequential logic has memory: its output depends on the current inputs and on values stored earlier. Every register is sequential logic, and so is anything built from registers.
Here is the part that surprises software engineers. Everything described in RTL happens at the same time. When the edge arrives, every register in the design captures at once. The lines of code are not steps in a program: separate blocks and separate register assignments update together, so their order in the file does not change the hardware. (Inside one if / else if chain the order still matters, because the first true condition wins.) There is no top to bottom, only edge to edge.
The enabled register shows the whole idea in three lines. At each rising edge, reset wins first: while the reset input sits low, the register takes 0. Otherwise the enable input decides: high loads the input d, low keeps the current value. Reset here is synchronous, which means it takes effect only at the edge, never in between.
The figure below draws the pattern behind every RTL block. Read it clockwise starting from the output: the current q feeds back to a multiplexer, the enable input steers that multiplexer between d and the current q, and each rising clock edge loads the chosen value into the register. The dashed cloud between the two registers is where the combinational logic lives.
The figure below is the simulation of the register driven through eight rising edges. The test code changes the inputs on each falling edge, so every input is stable when the rising edge samples it. Read the q row from left to right and check each step against the three-line rule above.
How to read it, slot by slot:
Edge 0: the reset input is low at the edge, so q becomes 0. Reset wins over everything.
Edge 1: reset is high, enable is high, d is 5, so q becomes 5.
Edge 2: enable is still high and d is 9, so q becomes 9.
Edge 3: enable is low and d is 3, so the register ignores d and q holds at 9.
Edge 4: enable is still low, so q holds at 9 a second time.
Edge 5: enable is high again and d is 3, so q becomes 3.
Edge 6: enable is low while d is 7, so q holds at 3.
Edge 7: enable stays low, so q holds at 3 to the end of the run.
The listing uses a few forms that may be new: [3:0] for a four-bit bus, 4'd0 for the number zero written with its width, posedge for a rising clock edge, and <= for a register update. The Syntax Primer names each one and runs them in a short program; read it first if any of these look unfamiliar.
The listing below is the complete experiment: the register design plus the test code that drives its eight edges and checks each answer. The waveform above comes from this file.
`timescale 1ns/1ps
// What RTL is: a 4-bit register with enable and synchronous active-low reset.
// On every rising edge of clk: reset (rst_n == 0) forces q to 0; otherwise,
// when en is high q takes the value of d, and when en is low q holds.
// Reset is synchronous: it only takes effect at a rising edge.
module enabled_reg (
input logic clk,
input logic rst_n,
input logic en,
input logic [3:0] d,
output logic [3:0] q
);
always_ff @(posedge clk) begin
if (!rst_n)
q <= 4'd0;
else if (en)
q <= d;
end
endmodule
// Self-checking testbench. Inputs change on the falling edge; the register
// samples on the rising edge; q is checked 1 ns after each rising edge.
// Eight rising edges: reset, load 5, load 9, hold, hold, load 3, hold, hold.
// Not synthesizable.
module tb;
logic clk = 0;
logic rst_n = 0; // edge 0 stimulus: hold reset (matches the first check)
logic en = 0;
logic [3:0] d = 4'd0;
logic [3:0] q;
enabled_reg dut (.clk, .rst_n, .en, .d, .q);
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
int errors = 0;
// Check q 1 ns after the rising edge. Mismatches are reported but do not
// stop the run, so one failure cannot hide the rest of the sequence.
task automatic check(input logic [3:0] want, input int idx);
if (q !== want) begin
errors++;
$display("ERROR edge %0d: expected q=%0d got %0d", idx, want, q);
end
endtask
initial begin
@(posedge clk); #1; check(4'd0, 0); // edge 0: reset -> q = 0
@(negedge clk); rst_n = 1; en = 1; d = 4'd5;
@(posedge clk); #1; check(4'd5, 1); // edge 1: en=1, d=5 -> q = 5
@(negedge clk); rst_n = 1; en = 1; d = 4'd9;
@(posedge clk); #1; check(4'd9, 2); // edge 2: en=1, d=9 -> q = 9
@(negedge clk); rst_n = 1; en = 0; d = 4'd3;
@(posedge clk); #1; check(4'd9, 3); // edge 3: en=0 -> holds 9
@(negedge clk); rst_n = 1; en = 0; d = 4'd3;
@(posedge clk); #1; check(4'd9, 4); // edge 4: en=0 -> holds 9
@(negedge clk); rst_n = 1; en = 1; d = 4'd3;
@(posedge clk); #1; check(4'd3, 5); // edge 5: en=1, d=3 -> q = 3
@(negedge clk); rst_n = 1; en = 0; d = 4'd7;
@(posedge clk); #1; check(4'd3, 6); // edge 6: en=0 -> holds 3
@(negedge clk); rst_n = 1; en = 0; d = 4'd7;
@(posedge clk); #1; check(4'd3, 7); // edge 7: en=0 -> holds 3
if (errors == 0) $display("PASS: enabled register");
else $fatal(1, "FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThis listing is the register design next to the test code that drives it, so you can match each of the eight edges to the line that caused it. Running it looks like this:
# captured terminal output
$ iverilog -g2012 -Wall -s tb -o build/reg.vvp enabled_reg.sv
$ vvp build/reg.vvp | grep -v finish
VCD info: dumpfile wave.vcd opened for output.
PASS: enabled registerThe first command compiles the file and prints nothing when it succeeds. The second runs the simulation: the first line says the testbench opened its waveform file, and the last line is the testbench's own verdict that all eight edges matched. This output came from Icarus Verilog 13.0 on macOS.
9. The enable input is low, so the multiplexer feeds the current q back into the register and d is ignored. A changing d with a low enable is the normal case for holding a value, not a conflict.
Reading RTL from top to bottom. The reset branch sits above the enable branch in the file, and it is tempting to imagine reset being checked first in time. In hardware both are evaluated together at the edge. Read each register block as one sentence: at every edge, this register takes the first value whose condition holds.
Expecting q to follow d between edges. When d changes from 9 to 3 with enable low, nothing happens to q until an edge arrives, and then q still holds. The register samples d only at the edge and only when enable is high. If your mental picture shows q tracking d, replace it with the figure above.
Leaving out reset. Without the reset branch the register starts at an unknown value and the first edges check garbage. Real designs give every register a defined starting value, exactly as this one does with its synchronous active-low reset.
Rule: RTL says what every register becomes at the next clock edge; everything else is the logic in between.
At the next rising edge, and it takes the value d holds at that edge, not the value d held earlier. Between edges q keeps its old value no matter how often d moves, because the register samples only at the edge.
So every input is stable when the register samples it on the rising edge. Changing an input at the same instant the register captures it would make the result a race between the two, and the check 1 ns later would compare against luck instead of logic.
Stay at 0 for every edge. Reset sits above enable in priority, so a low reset forces q to 0 at each edge even with enable high and d nonzero. That priority is why the reset branch is safe to check first when you read the code.
Simulation is where this register comes alive, and the course that owns simulation detail is the RTL course:
RTL describes registers and the logic that computes their next values; every register captures at the clock edge.
Combinational logic has no memory and sequential logic does; the register is sequential and the steering around it is combinational.
Code order means nothing in hardware: all registers update together, so read each block as one edge-to-edge rule.