Home / Learn Hub / Tutorials
TUTORIALS

Step-by-step VLSI tutorials, from transistor to testbench

Six tutorials that build on each other: how chips are built, your first Verilog module, the ASIC flow, a first FPGA project, RTL design from a spec, and a first UVM testbench. Each one has clear steps, working code, and a quick self-check at the end.

6TUTORIALS
~2 hrsTOTAL, AT YOUR OWN PACE
TestedVERILOG CODE SIMULATED
SUGGESTED ORDER

Start at the top, or jump in

New to VLSI? Go in order. Already comfortable with Verilog? Skip to the RTL or UVM tutorial.

BEGINNERABOUT 15 MINUTES5 STEPS

VLSI basics: from a transistor to a chip

YOU'LL LEARN
  • What a transistor does in a digital circuit
  • How gates, flip-flops and blocks stack up into a chip
  • Where each VLSI job fits in that stack
BEFORE YOU STARTNone. Start here.

A transistor is a switch

In digital design, a MOSFET is treated as a voltage-controlled switch. An NMOS conducts when its gate is high. A PMOS conducts when its gate is low. Put one of each between the supply and ground with their gates tied together, and you have a CMOS inverter: input high pulls the output low, and input low pulls it high. Only one of the two is on at a time in a steady state, which is why CMOS uses very little power when idle.

Switches make gates

Arrange NMOS transistors in series and PMOS in parallel, and you get a NAND gate. Swap the arrangement and you get a NOR. Every logic function can be built from NAND gates alone, so in principle a whole processor is just a very large number of these switch patterns. Modern chips hold billions of them.

Gates make logic that remembers

Gates on their own are combinational: outputs depend only on current inputs. Feed a gate's output back to its input and you get a latch, which can store a bit. Two latches clocked on opposite phases make a flip-flop, which captures its input only on a clock edge. Flip-flops are what let a chip step through time in an orderly way, one clock cycle after another.

Logic becomes blocks, blocks become a chip

Engineers don't place gates by hand. They describe behaviour in a hardware description language (RTL, register-transfer level): "on each clock, add these two registers and store the result". Tools turn that description into gates (synthesis), and then into physical shapes on silicon (place and route). Blocks such as CPU cores, memory controllers and interfaces are combined into a system on chip.

Where the jobs are

StageWho does it
Writing RTLRTL / design engineers
Proving the RTL worksDesign verification engineers
Making it testable after manufactureDFT engineers
Turning it into layoutPhysical design and STA engineers
Transistor-level circuitsAnalog and custom designers, layout engineers
Checking real chips in the labPost-silicon validation engineers

CHECK YOURSELF

Why does a CMOS inverter draw almost no power when it isn't switching?

Because in either steady state only one of the two transistors is on, so there's no direct path from supply to ground. Only leakage flows.

What is the difference between combinational and sequential logic?

Combinational outputs depend only on current inputs. Sequential logic also depends on stored state, held in latches or flip-flops.

What does RTL describe?

How data moves between registers on each clock cycle, and what logic transforms it on the way.

BEGINNERABOUT 25 MINUTES6 STEPS

Verilog: your first module and testbench

YOU'LL LEARN
  • Writing a small combinational module
  • Writing a self-checking testbench
  • Running it on a free simulator and reading the result
BEFORE YOU STARTThe basics tutorial above, or basic digital electronics.

Install a free simulator (or use your browser)

You don't need a paid tool to learn. Two options:

  • Icarus Verilog + GTKWave: free, runs on Windows, Linux and macOS. iverilog compiles, vvp runs, and GTKWave shows waveforms.
  • EDA Playground: runs simulators in the browser, with nothing to install.

Write the design

A 4-bit adder with carry in and out. Save it as adder4.v.

// A 4-bit adder with carry in and carry out
module adder4 (
  input  wire [3:0] a,
  input  wire [3:0] b,
  input  wire       cin,
  output wire [3:0] sum,
  output wire       cout
);
  assign {cout, sum} = a + b + cin;   // 5-bit result: carry out, then the 4-bit sum
endmodule

The concatenation {cout, sum} on the left makes a 5-bit result, so the carry out lands in the top bit automatically. wire plus assign means this is pure combinational logic.

Write a testbench that checks itself

A testbench is a module with no ports that drives the design and checks its outputs. Four inputs, four inputs and one carry give 2⁹ = 512 combinations, so we can test them all. Save it as tb_adder4.v.

// Exhaustive testbench: tries all 512 input combinations
`timescale 1ns/1ps
module tb_adder4;
  reg  [3:0] a, b;
  reg        cin;
  wire [3:0] sum;
  wire       cout;
  integer    i, errors = 0;

  adder4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

  initial begin
    $dumpfile("adder4.vcd");          // waveform file for GTKWave
    $dumpvars(0, tb_adder4);
    for (i = 0; i < 512; i = i + 1) begin
      {a, b, cin} = i;                // 4 + 4 + 1 = 9 bits = 512 combinations
      #1;                             // let the combinational logic settle
      if ({cout, sum} !== a + b + cin) begin
        errors = errors + 1;
        $display("ERROR: %0d + %0d + %0d gave %0d", a, b, cin, {cout, sum});
      end
    end
    if (errors == 0) $display("PASS: all 512 cases correct");
    else             $display("FAIL: %0d errors", errors);
    $finish;
  end
endmodule

Note !==: it compares X and Z values literally, so an undriven output counts as an error instead of slipping through.

Compile and run

From a terminal in the same folder:

iverilog -o sim tb_adder4.v adder4.v
vvp sim

You should see PASS: all 512 cases correct. We ran exactly these files before publishing this tutorial.

Look at the waveform

The testbench wrote adder4.vcd. Open it with gtkwave adder4.vcd, add a, b, cin, sum and cout, and step through time. Reading waveforms is a daily skill in every RTL and verification job, so it's worth getting comfortable now.

Break it on purpose

Change the design to assign {cout, sum} = a + b; (drop the carry in) and run again. The testbench should now print errors. This is the most important habit in verification: a test you have never seen fail proves very little.

CHECK YOURSELF

Why does the testbench use #1 after setting the inputs?

To let the combinational outputs settle before checking them. Without it, the check could read the old values.

What is the difference between != and !==?

!= gives X if either side has X or Z bits. !== compares X and Z literally, returning 0 or 1.

How many combinations would an exhaustive test of a 16-bit adder need?

2³³ (two 16-bit inputs plus carry in), about 8.6 billion. That's why larger designs use constrained-random testing instead.

BEGINNERABOUT 20 MINUTES7 STEPS

The ASIC design flow, following one block

YOU'LL LEARN
  • Every stage from specification to tape-out
  • What each stage takes in and produces
  • Why problems found late are so expensive
BEFORE YOU STARTKnow what RTL and a flip-flop are.

Specification

Imagine a small block: a PWM generator for controlling motor speed. The spec says: 8-bit duty cycle, a period of 256 clocks, a 100 MHz clock, and no glitches on the output. Every later decision traces back to this document, so ambiguity here becomes bugs later.

RTL design and verification

A designer writes the RTL (there's a full PWM tutorial below). In parallel, a verification engineer writes a testbench and a verification plan listing every feature to check: 0%, 50% and 100% duty, changing the duty mid-period, and reset behaviour. The RTL isn't "done" until the plan's coverage goals are met.

Synthesis

The synthesis tool reads the RTL, the standard-cell library and the timing constraints (100 MHz = a 10 ns period) and produces a gate-level netlist: a list of cells and the wires between them. It reports area, timing and power estimates. A logical equivalence check proves the netlist matches the RTL.

DFT insertion

Flip-flops become scan flops and are stitched into scan chains, so the manufactured chip can be tested. ATPG generates the test patterns and reports fault coverage.

Physical design

The netlist is floorplanned, placed, given a clock tree and routed into real metal shapes. After routing, timing is re-checked with real wire parasitics, and small ECO fixes close any remaining violations.

Sign-off and tape-out

Final checks: timing in every corner, DRC, LVS, IR drop and EM, and equivalence again. When everything is clean, the GDS layout goes to the foundry. That's tape-out. Months later, silicon comes back for lab validation.

Why the order matters

A bug caught in RTL simulation costs minutes. The same bug found after tape-out can cost a re-spin: months of delay and millions in masks. That's why verification typically takes more engineering effort than design itself on a modern chip.

CHECK YOURSELF

What are the three main inputs to synthesis?

The RTL, the standard-cell library, and the timing constraints (SDC).

What does an equivalence check prove?

That two representations, such as RTL and netlist, implement exactly the same logic function.

What is tape-out?

Sending the final layout (GDS/OASIS) to the foundry for manufacturing.

BEGINNERABOUT 20 MINUTES5 STEPS

FPGA vs ASIC, then your first FPGA project

YOU'LL LEARN
  • What is physically different about an FPGA
  • The FPGA build flow
  • A blinking-LED design done the right way
BEFORE YOU STARTThe Verilog tutorial.

What an FPGA really is

An FPGA is a grid of look-up tables (small memories that can implement any function of a few inputs), flip-flops, block RAMs, DSP multipliers and programmable routing. Your design becomes a bitstream that configures all of these. Change the bitstream, and you have new hardware in seconds. The guides page has a full ASIC vs FPGA comparison.

The FPGA flow

  1. RTL, the same Verilog you would write for an ASIC.
  2. Synthesis into LUTs and flip-flops.
  3. Place and route onto the specific device.
  4. Timing analysis against your clock constraints.
  5. Bitstream generation, then programming the board over USB/JTAG.

Design: blink an LED once per second

A board clock is often 100 MHz. The beginner mistake is to divide the clock with a counter and use a counter bit as a new clock. The right way is to keep one clock and generate an enable once per half-second:

module blink #(parameter CLK_HZ = 100_000_000) (
  input  wire clk,
  input  wire rst_n,
  output reg  led
);
  localparam HALF = CLK_HZ / 2;                 // toggle twice per second = 1 Hz blink
  reg [$clog2(HALF)-1:0] cnt;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      cnt <= 0; led <= 1'b0;
    end else if (cnt == HALF - 1) begin
      cnt <= 0; led <= ~led;
    end else begin
      cnt <= cnt + 1'b1;
    end
endmodule

It's the same pattern as the tick generator example. Everything stays in one clock domain, which keeps timing analysis simple.

Constrain it

The constraints file tells the tools which physical pins to use and how fast the clock is. The exact syntax depends on the vendor (Xilinx/AMD uses XDC, Intel/Altera uses SDC with QSF pin assignments). Conceptually, it's always "clk is on pin X at 100 MHz, led is on pin Y". Take the pin names from your board's reference manual.

Build, program, debug

Run the flow, check the timing report (no negative slack), and program the board. If the LED doesn't blink, check three things in order: the reset polarity (many boards have an active-high button), the pin assignment, and whether the clock pin is the one your board actually drives. On-chip logic analysers (such as ILA or SignalTap) let you probe internal signals on real hardware.

CHECK YOURSELF

Why not use a counter bit as a slower clock?

It creates a new clock domain with extra skew and glitch risk that the tools handle poorly. An enable keeps one clean clock.

What is a LUT?

A small memory that implements any logic function of its inputs. Its contents are set by the bitstream.

What does the timing report tell you after place and route?

Whether every path meets the clock constraint. Negative slack means the design may fail on hardware.

INTERMEDIATEABOUT 30 MINUTES5 STEPS

RTL design from spec to verified code: a PWM generator

YOU'LL LEARN
  • Turning a written spec into an interface and micro-architecture
  • Writing glitch-free, synthesizable RTL
  • Proving it works with a measuring testbench
BEFORE YOU STARTThe Verilog tutorial, and knowing what a counter is.

Read the spec and write down the questions

"An 8-bit PWM. Output high for duty out of every 256 clocks." Before coding, pin down the edge cases: what does duty = 0 mean (always low)? Can duty reach 100% (with 8 bits, the maximum is 255/256)? What happens if duty changes in the middle of a period? Interviewers reward asking these questions.

Design the micro-architecture

  • A free-running 8-bit counter defines the period: 0 → 255 → 0.
  • The output is high while counter < duty.
  • To avoid a glitchy period when duty changes mid-period, sample duty only at the period boundary (duty_q).
  • Register the output so the comparator's glitches never reach the pin.

Write the RTL

// PWM generator: 8-bit duty cycle, one PWM period = 256 clocks
module pwm8 (
  input  wire       clk,
  input  wire       rst_n,
  input  wire [7:0] duty,     // 0 = always low, 128 = 50%, 255 = high 255/256 of the time
  output reg        pwm_out
);
  reg [7:0] cnt;
  reg [7:0] duty_q;           // duty is sampled once per period, so the output never glitches mid-period

  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      cnt     <= 8'd0;
      duty_q  <= 8'd0;
      pwm_out <= 1'b0;
    end else begin
      cnt <= cnt + 1'b1;                       // free-running, wraps every 256 clocks
      if (cnt == 8'd255) duty_q <= duty;       // take the new duty at the period boundary
      pwm_out <= (cnt < duty_q);               // registered output: glitch-free
    end
endmodule

Every decision from the previous step appears in the code. That traceability is what reviewers look for.

Verify by measurement

Instead of eyeballing waveforms, the testbench measures: set a duty value, wait for it to take effect, then count how many of the next 256 clocks are high. The count must equal the duty exactly.

`timescale 1ns/1ps
module tb_pwm8;
  reg clk = 0, rst_n = 0; reg [7:0] duty = 0; wire pwm_out;
  integer high, k, errors = 0;
  pwm8 dut (.*);
  always #5 clk = ~clk;

  task measure(input [7:0] d);         // set a duty cycle, then count high clocks in one period
    begin
      duty = d;
      repeat (512) @(posedge clk);       // let the new value take effect
      high = 0;
      repeat (256) begin @(posedge clk); #1 if (pwm_out) high = high + 1; end
      if (high != d) begin errors = errors + 1; $display("ERROR: duty %0d gave %0d high clocks", d, high); end
    end
  endtask

  initial begin
    repeat (2) @(posedge clk); #1 rst_n = 1;
    measure(0); measure(1); measure(64); measure(128); measure(200); measure(255);
    if (errors == 0) $display("PASS: duty cycles 0, 1, 64, 128, 200, 255 all exact");
    else             $display("FAIL: %0d errors", errors);
    $finish;
  end
endmodule

Result when we ran it: PASS: duty cycles 0, 1, 64, 128, 200, 255 all exact. The values include both edge cases (0 and 255) and the off-by-one trap at 1.

Review it like an interviewer would

  • Timing: an 8-bit compare after a flop is short, so 100 MHz is easy.
  • Power: the counter toggles constantly. If the PWM is disabled, gate it with an enable.
  • Extensions: make the resolution a parameter, add a prescaler for a lower PWM frequency, or offer center-aligned PWM for motor control.

CHECK YOURSELF

Why sample duty only at the end of a period?

So a mid-period change can't create one malformed pulse. The new value takes effect cleanly on the next period.

Why register pwm_out?

The comparator output can glitch while the counter bits change. A flop outputs one clean value per cycle.

What is the maximum duty cycle with this design?

255/256, about 99.6%. A true 100% needs one more bit, or a special case.

INTERMEDIATEABOUT 30 MINUTES5 STEPS

Getting started with a UVM testbench

YOU'LL LEARN
  • The files a minimal UVM testbench needs
  • How the pieces connect
  • The order to build and debug them in
BEFORE YOU STARTSystemVerilog classes, and a DUT you've already tested with a simple testbench.

Start from the interface

Bundle the DUT's pins in a SystemVerilog interface, ideally with a clocking block so the testbench drives and samples at clean times. The top module instantiates the DUT and the interface, then passes a virtual interface handle to UVM through uvm_config_db before calling run_test().

Build the files in this order

  1. Transaction (uvm_sequence_item): the fields of one operation, plus constraints.
  2. Sequence: generates transactions.
  3. Driver: turns transactions into pin activity through the virtual interface.
  4. Monitor: watches the pins and rebuilds transactions.
  5. Agent: holds the sequencer, driver and monitor.
  6. Scoreboard: compares what the monitor saw against expected results.
  7. Environment: agent plus scoreboard.
  8. Test: builds the environment and starts the sequence.

The skeleton of a test

class base_test extends uvm_test;
  `uvm_component_utils(base_test)
  my_env env;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);   // factory, never new()
  endfunction

  task run_phase(uvm_phase phase);
    my_seq seq = my_seq::type_id::create("seq");
    phase.raise_objection(this);                   // keep the run phase alive
    seq.start(env.agt.sqr);
    phase.drop_objection(this);                    // let it end
  endtask
endclass

This needs a simulator with UVM support, so it isn't simulated here. The UVM example shows a full transaction and sequence.

Bring it up one piece at a time

  • First, the driver alone: send one transaction and check the waveform.
  • Then the monitor: print every transaction it rebuilds, and check it matches what was driven.
  • Then the scoreboard with a trivial reference model.
  • Only then add random sequences and coverage.

Debugging one new component at a time is far faster than debugging eight at once.

Make it trustworthy

A green test means nothing if nothing was checked. Add a check in check_phase that fails when the scoreboard compared zero transactions, then break the DUT on purpose and confirm the test fails.

CHECK YOURSELF

Why must components be created with type_id::create()?

So the UVM factory can substitute a derived class (an override) without editing the environment.

What happens if the test never raises an objection?

The run phase ends at time 0, and the test passes without sending any stimulus.

Where should functional coverage be sampled, and why?

From the monitor, because it reflects what actually happened on the pins, not what the driver intended.

KEEP GOING

After the tutorials

Tutorials build understanding. To make it stick, test yourself, then go deeper.