try ai
Popular Science
Edit
Share
Feedback
  • Verilog

Verilog

SciencePediaSciencePedia
Key Takeaways
  • Verilog is a Hardware Description Language (HDL) for creating blueprints of physical, parallel-operating circuits, not a sequential programming language.
  • The crucial difference between blocking (=) and non-blocking (<=) assignments dictates whether logic is modeled sequentially or concurrently.
  • Use blocking assignments for memoryless combinational logic and non-blocking assignments for clocked sequential logic to ensure correct synthesis.
  • Incomplete or ambiguous logic descriptions can cause the synthesizer to infer unwanted memory elements like latches, leading to design flaws.
  • Verilog serves a dual role: describing hardware for physical implementation (synthesis) and creating virtual environments to test and verify the design's correctness (simulation).

Introduction

Why does writing code to describe a physical machine feel so different from writing a software application? This is the central question for anyone learning Verilog, the dominant language for modern digital chip design. Unlike a software program that executes instructions one by one, a hardware circuit operates in massive parallel, with millions of events happening simultaneously. The primary challenge, and a common source of confusion, is learning to use a text-based language to describe this concurrent reality. This article bridges that gap. It is designed to transform your perspective from that of a programmer to that of a hardware architect.

In the chapters that follow, you will gain a deep understanding of Verilog's core philosophy. In "Principles and Mechanisms," we will unravel the most critical concept: the "tale of two equals," explaining the profound difference between blocking and non-blocking assignments and why this distinction is the key to describing both sequential and combinational logic. Then, in "Applications and Interdisciplinary Connections," we will apply these principles to build real-world components, from simple logic gates and counters to scalable registers and complex dual-port memories, demonstrating how to engineer and verify entire digital systems.

Principles and Mechanisms

Imagine you want to build a new kind of machine—say, a complex mechanical clock. You wouldn't just write a list of instructions like "move this gear, then turn that spring." That's a recipe for a baker. Instead, you'd draw a blueprint. The blueprint wouldn't describe the sequence of building the clock; it would describe the relationships between all its parts: how this gear meshes with that one, how that spring connects to the escapement, and how the whole assembly fits together. The blueprint describes a static design that, once built, will spring to life with its own parallel, interconnected dynamics.

This is the most important idea to grasp about Verilog. It’s not a programming language in the way that Python or C are; it’s a ​​Hardware Description Language (HDL)​​. You are not writing a recipe of sequential steps for a computer to follow. You are creating a blueprint for a physical, electronic machine. This single shift in perspective is the key to unlocking everything that follows.

A Language for Blueprints

Every good blueprint starts by defining the boundaries of the component you're building. In Verilog, this is the ​​module​​. A module is like the black plastic casing of a microchip; it declares what the component is called and defines the "pins" that connect it to the outside world—its inputs and outputs.

For instance, if we were designing a simple "Packet Integrity Checker," our blueprint would first need to define its connections: a clock signal comes in, a reset line comes in, data and parity bits come in, and signals indicating success or error go out. The Verilog code to define this boundary is a direct translation of that physical concept, carefully specifying the name, direction, and size of each port.

loading

This act of description immediately highlights a crucial duality: the world of ​​simulation​​ versus the world of ​​synthesis​​. A simulation is a software program running on your computer that pretends to be your hardware. Because it's a program, it can do things your final chip can't, like read a file from your hard drive to pre-load a memory with filter coefficients. This is incredibly useful for testing.

However, ​​synthesis​​ is the process of turning your blueprint into a real, physical circuit layout for a silicon chip. That finished chip, operating in a network router or a smartphone, has no concept of your computer's file system. The synthesizer tool knows this, and it will reject any instruction that relies on the outside world of the development computer. Your description must be self-contained and physically realizable. You are describing a machine that must stand on its own.

The Illusion of Sequence and the Reality of Concurrency

So, how do we describe the inner workings of this machine? In hardware, everything is happening at once. Billions of transistors are flipping in parallel, guided by the metronome of a master clock signal. This massive parallelism is what makes hardware so fast. But we have to write our description using text, which is inherently sequential, one line after another. This is the central challenge Verilog must solve.

To do this, Verilog provides constructs like the always block, which describes how outputs should react to changes in inputs. But to handle the conflict between sequential text and parallel hardware, Verilog does something ingenious and, at first, a little strange. It gives us two different ways to assign a value to a variable: two different meanings for the humble "equals" sign. Understanding the "tale of the two equals" is the single most important step to mastering Verilog.

The Blocking Assignment: A Familiar Tale of Sequence

First, there is the ​​blocking assignment​​, written with a single equals sign: =. This operator behaves exactly as you would expect from a language like Python or C. When the simulation sees a line like a = b;, it immediately evaluates b, assigns its value to a, and blocks anything else from happening until this line is complete. Only then does it move to the next line.

This creates a clear, predictable sequence of events. For example, if we write a loop to sum the first few integers, the blocking assignment updates the result in each and every iteration, just like a software program would. A loop summing from i=0i=0i=0 to 444 using result = result + i; will faithfully calculate 0+1+2+3+40+1+2+3+40+1+2+3+4 and end with the value 101010.

This sequential behavior is perfectly captured in a classic thought experiment. Imagine three registers, reg_A, reg_B, and reg_C, and we execute the following code on a clock tick:

loading

If we start with reg_A=25, reg_B=50, and reg_C=100, what happens? The first line executes, and reg_A becomes 50. Then the second line executes, and reg_B becomes 100. Finally, the third line executes. But what value does it use for reg_A? It uses the new value, which is 50. So, reg_C becomes 50. The final state is (A=50, B=100, C=50), not a three-way swap. The execution is strictly sequential.

This behavior is exactly what we want for describing ​​combinational logic​​—circuits without memory, like logic gates, where outputs change instantaneously (in a logical sense) in response to inputs. Think of a Rube Goldberg machine: a ball rolling down a ramp triggers a lever, which in turn releases a weight. The actions happen in a direct, causal chain. For this reason, the blocking = is the standard, recommended operator to use inside a combinational always @(*) block.

The Non-Blocking Assignment: Embracing Parallelism

But what about the parts of a circuit that are meant to update simultaneously on a clock tick? Think of a bank of registers in a processor pipeline. On the rising edge of the clock, they all need to capture their new values at the exact same time. A sequential, blocking update would completely fail to model this.

This is where the second operator, the ​​non-blocking assignment​​ (<=), becomes our hero. It embodies the principle of ​​concurrency​​. Let's use an analogy. Imagine an orchestra. The clock edge is the conductor's downbeat. In the instant before the downbeat, every musician looks at the current state of the music (the values of all signals in the circuit). Based on that, they figure out what note they are supposed to play next. When the conductor's baton falls, they all play their new note at the same instant.

The non-blocking assignment works exactly this way. When a simulator sees a <= b;, it evaluates the right-hand side (b) immediately, but it schedules the update to the left-hand side (a) to happen a moment later, in a special phase at the end of the simulation time step. All non-blocking assignments in the entire design that are triggered by the same event have their right-hand sides evaluated first, using the "old" values. Then, all the left-hand sides are updated simultaneously.

Let's see this magic in action with the classic problem of swapping two registers, reg_X and reg_Y. If we try this with blocking assignments in separate always blocks, we create a ​​race condition​​: the result depends on which block the simulator decides to run first, leading to non-deterministic chaos. But look what happens with non-blocking assignments:

loading

On the clock edge, both blocks trigger. The first one evaluates reg_Y (let's say its value is 1) and schedules reg_X to become 1. The second one evaluates reg_X (let's say its old value is 0) and schedules reg_Y to become 0. Then, at the end of the time step, both updates happen. reg_X becomes 1 and reg_Y becomes 0. The values are swapped perfectly, every single time. This models the physical reality of two flip-flops swapping values using a shared clock.

This principle is what allows us to describe fundamental hardware structures like pipelines and shift registers. The code q2 <= q1; q1 <= d; naturally synthesizes to two flip-flops in a series. On each clock tick, the value of d is sampled for the new q1, and the old value of q1 is sampled for the new q2. The data shifts down the line, one stage per clock cycle. This elegant mapping between concise code and physical structure is possible only because of non-blocking semantics.

The behavior is so consistent that it holds even in strange cases. If we re-run our for loop sum with a non-blocking assignment (result <= result + i;), something bizarre happens. In each iteration of the loop, the original value of result (say, 0) is used to calculate the new value. The loop schedules result <= 0 + 1, then overwrites that with result <= 0 + 2, then result <= 0 + 3, and finally result <= 0 + 4. Since only the last scheduled assignment to a variable in a time step "wins," the final value of result is simply 4. This is not how you would write an accumulator, but it's a perfect test of your understanding of the non-blocking model: all RHS evaluations first, using old values, then one coordinated update at the end.

The Synthesizer: An Interpreter with Physical Constraints

The final piece of the puzzle is the synthesis tool itself. It is an incredibly sophisticated interpreter, but it is also ruthlessly literal. It reads your Verilog blueprint and does its best to build exactly what you described, using the physical components available on the chip. This leads to two critical rules of thumb:

  1. Use ​​blocking assignments (=)​​ for ​​combinational logic​​ (always @(*)).
  2. Use ​​non-blocking assignments (<=)​​ for ​​sequential logic​​ (always @(posedge clk)).

Following these rules helps the synthesizer understand your intent and prevents mismatches between what you simulate and what you get in hardware.

What happens if you're ambiguous? Suppose you describe a piece of combinational logic, but you fail to specify what the output should be for every possible input condition. For example: if (en) q <= d;. What should q do when en is false? You haven't said. A software program might crash or have an undefined value. But hardware can't just "do nothing." If the output isn't being driven to a new value, it must hold its old value. To do this, the synthesizer is forced to infer a memory element—a ​​latch​​. A latch is a transparent storage element that can cause all sorts of timing problems and is usually a bug born from an incomplete description.

This is a powerful lesson: in hardware, there is no "undefined." If you don't specify the behavior, the synthesizer will build a circuit that remembers.

Finally, always remember that you are describing a physical system of electrons and wires. A clever trick in software might be a disaster in hardware. For example, writing always @(posedge (clk & enable_signal)) seems like a smart way to gate a clock and save power. But what you have actually described is a circuit where the enable_signal is physically combined with the clk signal using an AND gate. This new, "gated clock" signal will be delayed relative to the main clock, creating timing skew. Worse, if the enable_signal has any spurious glitches—tiny, unwanted electrical pulses—it can create fake clock edges, causing the register to latch garbage data. What looks like an elegant line of code is actually a blueprint for a dangerously unreliable circuit.

By embracing the mindset of drawing a blueprint, by understanding the profound difference between blocking and non-blocking assignments, and by respecting the physical reality that the synthesizer must obey, you can move from writing code to truly designing hardware.

Applications and Interdisciplinary Connections

We have spent some time learning the rules of a new language, Verilog. But learning a language is not about memorizing grammar; it is about what you can say with it. What thoughts can you express? What worlds can you build? Verilog is not merely a programming language like Python or C++. It is something more profound: a formal language for describing physical structure and behavior. It is a blueprint for creating slices of reality, for etching ideas into silicon. With Verilog, we command an army of electrons to perform logic, to remember, and to compute. Let us now embark on a journey to see what we can build, from a single logical thought to bustling cities of computation.

The Alphabet of Logic: From Truth Tables to Circuits

At its heart, all of digital computation boils down to simple true-or-false questions. Can we translate this fundamental logic into hardware? Of course. Consider one of the simplest arithmetic operations: subtracting one bit from another. This "half subtractor" circuit produces a difference and a borrow. If we write down its truth table, we find the Difference bit is '1' only when the two input bits are different. This is the exclusive OR (XOR) function. In Verilog, we don't have to painstakingly draw a diagram of logic gates; we can state this fact directly and beautifully: assign Difference = A ^ B;. With this single line, we have captured the essence of the circuit's behavior. We have described a piece of physical reality.

This elegance extends to more complex tasks. Imagine you are sending a stream of data—say, an 8-bit byte—from one part of a chip to another. How can you be reasonably sure the data hasn't been corrupted by noise? A classic technique is to add a "parity bit." For an odd parity system, you set this extra bit so the total count of '1's is always odd. To calculate this bit, you could chain together seven XOR gates, a tedious process. But Verilog understands the idea of performing an operation across a whole collection of bits. We can use a "reduction operator" to express this thought with breathtaking conciseness: ~^data_in. This single expression performs an XNOR across all 8 bits, instantly telling us if the number of '1's is even, giving us our desired parity bit. This is a powerful theme: the language provides us with tools to express high-level intent, which the synthesis tools then dutifully translate into an optimal arrangement of physical gates.

Sometimes, we want to describe behavior not by a Boolean formula, but by a set of cases. Think of an encoder, a circuit that takes a "one-hot" input (where only one input bit is high) and outputs the binary index of that active bit. For a 4-to-2 encoder, we can simply list the possibilities: if input 4'b0001 is active, output 2'b00; if 4'b0010 is active, output 2'b01, and so on. Verilog's case statement allows us to describe this behavior directly inside an always block, almost like a direct transcript of the functional specification. This shift from describing how gates are connected to what the circuit does is a monumental leap in abstraction, allowing us to design much more complex systems without getting lost in a sea of individual gates.

Introducing Time and Memory: The Birth of State

So far, our circuits have been purely combinational; their outputs depend only on their current inputs. They have no memory, no past. They are like a simple calculator. To build computers, we need circuits that can remember. This is the domain of sequential logic, and its fundamental building block is the flip-flop.

Let’s design a versatile D-type flip-flop, the workhorse of digital memory. We want it to capture its input d on the rising edge of a clock signal clk. But we also need control. We'll add a synchronous enable en, so it only captures data when we tell it to. And for safety, we'll add an asynchronous, active-low clear clr_n. This clear signal is the "big red button"; when pressed (brought low), it must reset the flip-flop to 0 immediately, overriding all other inputs.

How do we describe this intricate dance of conditions in Verilog? We use a clocked always block. The sensitivity list, always @(posedge clk, negedge clr_n), tells the circuit what to "listen" for: the rising edge of the clock or a falling edge of the clear signal. Inside the block, priority is everything. The very first thing we check is if (!clr_n). This reflects its physical reality as the highest-priority, asynchronous input. Only in the else branch does the synchronous, clock-dependent logic live. By correctly describing the events and their priorities, we have precisely modeled a physical memory element.

With this ability to store a single bit, we can now build circuits that count, that sequence operations, that form the very rhythm of a processor. A simple counter is just a collection of these flip-flops where the output of one feeds the logic for the next. To build a 16-bit counter with an asynchronous reset and a synchronous enable, we use the exact same principles. The always @(posedge clk or posedge reset) block listens for either the clock or the reset, and the if (reset) check takes absolute priority, unconditionally clearing the counter to zero. Otherwise, on a clock edge, if (en) is true, we increment. We have built a state machine. The circuit now has a past, a present, and a predictable future.

Engineering at Scale: Blueprints for Cities of Logic

Building a single flip-flop or counter is one thing. Building a 64-bit processor is another. No engineer builds a skyscraper by designing each brick individually. They design one perfect brick and then create a plan to lay millions of them. Verilog gives us this same power through parameterization and structural generation.

Suppose we need an N-bit register. It could be 8 bits, 32 bits, or 128 bits, depending on the application. We don't want to write different code for each. Instead, we can write a template. We start with our verified 1-bit flip-flop module, our "perfect brick". Then, in a new module for our N-bit register, we declare a parameter N. We can then use a generate loop, which is not a loop that runs in time, but a command to the synthesis tool to generate N copies of our flip-flop at design time. For each copy i, it connects the i-th bits of the data input and output busses. We have created not just a single design, but a blueprint for an infinite family of registers.

This principle of scalable design is ubiquitous. Consider an equality comparator, which checks if two N-bit numbers, A and B, are identical. The logic is simple: A equals B if and only if A[0] equals B[0], and A[1] equals B[1], and so on for all N bits. We can generate N 1-bit comparators. Each produces a '1' if its pair of bits match. How do we get the final answer? We need to AND all these intermediate results together. Again, Verilog's reduction operators provide an elegant solution. The statement assign EQ = performs a logical AND across every bit in our intermediate result vector, giving the final equality signal. This is the essence of modern hardware engineering: design reusable components, and then write rules to compose them into larger, scalable structures.

Orchestrating Complexity: Systems and Interconnections

With these powerful tools, we can now assemble not just components, but entire systems. In any modern processor, one of the most critical components is memory. Data needs to be stored and retrieved quickly. Often, different parts of the processor need to access this memory simultaneously. For instance, one stage of a pipeline might be writing a result while another stage is fetching a new instruction.

This leads to the design of a dual-port RAM, a memory block with two independent sets of controls: one for writing and one for reading. Crucially, these ports might operate on completely different, unrelated clocks (w_clk and r_clk). To model this in Verilog, we must respect their independence. We use two separate always blocks. One is sensitive only to posedge w_clk and handles the write logic. The other is sensitive only to posedge r_clk and handles the registered read logic. This clean separation in the code directly mirrors the physical reality of two independent circuits interacting with a shared resource. Getting this right, especially using non-blocking assignments (=) to prevent race conditions, is vital for designing the high-performance data paths at the heart of digital signal processors (DSPs), graphics processing units (GPUs), and central processing units (CPUs). Verilog is the language we use to architect these complex data highways.

The Mirror World: Simulation and Verification

We have designed a beautiful, complex machine. But does it work? Before we spend millions of dollars fabricating a silicon chip, we must be absolutely certain. Here, Verilog reveals its second, equally important identity: it is also a language for creating a virtual world to test our designs. This is the world of simulation.

In this simulated universe, we are the masters of time. Using the `timescale directive, we can define the fundamental units of our world. A directive like timescale 1ns / 10ps means that the default unit of time is 1 nanosecond, but the simulator's resolution, its "quantum" of time, is 10 picoseconds. Every delay we specify is interpreted and rounded according to these rules.

Within this world, we must create the stimuli to exercise our design. We need to generate clock signals, for instance. But not just a simple, 50% duty cycle clock. Perhaps our real system has a clock with a 70% duty cycle. We can write a small piece of code in our testbench to generate this exact waveform, holding the clock high for 7 time units and low for 3, endlessly repeating, providing the precise heartbeat our design expects.

The ultimate goal of simulation is automated verification. It's not enough to just look at waveforms and say "it looks right." We must build a checker, an impartial observer within our testbench that automatically flags any deviation from the specification. For example, after applying an input to our 2-to-4 decoder, we can write a small loop that counts how many output bits are high. The specification says there must be exactly one. If our checker counts zero, or two, or more, it can immediately print an error message, telling us precisely which input caused the failure. This concept of self-checking testbenches and assertions is the bedrock of modern verification methodology, connecting the world of hardware design to the rigorous principles of software testing and quality assurance.

From a single XOR gate to a self-verifying simulation of a complex memory system, Verilog provides the language to express, build, and test our digital creations. It is the essential bridge between human intent and silicon reality, a testament to the power of abstraction in engineering the complex digital world that surrounds us.

module PacketIntegrityChecker( input clk, input rst_n, input [3:0] data_in, // ... and so on for other ports output packet_ok, output error_flag ); // The description of the internal machinery goes here. endmodule
// Inside an [always block](/sciencepedia/feynman/keyword/always_block)... reg_A = reg_B; reg_B = reg_C; reg_C = reg_A;
// Implementation I: The correct way always @(posedge clk) reg_X <= reg_Y; always @(posedge clk) reg_Y <= reg_X;