Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
    • Digital Logic for RTL Engineers
    • Number Systems for RTL Engineers
    • Verilog Fundamentals
      • Modules and Ports
      • Wire vs reg
      • assign vs always
      • The Two Process Templates
      • Blocking vs Non-blocking
      • From Code to Hardware
    • Verilog Operators and Types
    • SystemVerilog for RTL
    • Simulation and Testbenches
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Verilog Fundamentals
  1. Topics
  2. RTL Design
  3. RTL Foundations
  4. Verilog Fundamentals
  5. Modules and Ports

Part 1 · Foundations · Beginner

Modules and Ports

Modules declare ports, reuse sizes through parameters, connect by name, and check typos with default nettype none.

On this page

    Why this matters

    A misspelt signal name on the left of an assign quietly makes a new 1-bit wire. The design is wrong, the compiler says nothing, and the bug appears as one stuck bit.

    After this lesson you can: write a module with ANSI ports, a parameter and named connections, and turn implicit-net typos into errors.

    Modules and ports

    A module declares its ports and defines the logic inside. Ports are the contract with the outside world.

    Use the ANSI style, with direction, type, and width in the port list. It keeps the interface in one place.

    half_adder.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    `default_nettype none
    
    module half_adder (
      input  wire a,
      input  wire b,
      output wire sum,
      output wire carry
    );
      assign sum   = a ^ b;
      assign carry = a & b;
    endmodule
    
    module full_adder (
      input  wire a, b, cin,
      output wire sum, cout
    );
      wire s1, c1, c2;
    
      // Instances connected BY NAME: order of ports does not matter
      half_adder ha1 (.a(a),  .b(b),   .sum(s1),  .carry(c1));
      half_adder ha2 (.a(s1), .b(cin), .sum(sum), .carry(c2));
    
      assign cout = c1 | c2;
    endmodule
    
    `default_nettype wire

    A full adder built from two half adders. Every connection uses the .port(signal) form, called connection by name.

    full_adder as two half_adder instances and an OR gate, with every internal wire named as in the listing

    Read it as the netlist the code describes: each box is an instance, grey names next to a box are its ports, and orange names are the wires declared inside full_adder.

    • s1 joins ha1.sum to ha2.a; that is the .sum(s1) and .a(s1) pair in the two instance lines.

    • cin goes straight to ha2.b, so ha2 adds the carry in to the first partial sum.

    • c1 and c2 never both equal 1, so the OR gate gives the single carry out: assign cout = c1 | c2;.

    Style

    Example

    Risk

    By name (recommended)

    half_adder u (.a(x), .b(y), .sum(s), .carry(c));

    None: order is irrelevant and ports are self-documenting.

    By position

    half_adder u (x, y, s, c);

    Swap two arguments and the code still compiles. Adding a port silently shifts every connection.

    Parameters

    A parameter makes a block reusable at different sizes. Give every parameter a default and override it by name:

    invert_bus.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module invert_bus #(parameter WIDTH = 8) (
      input  wire [WIDTH-1:0] a,
      output wire [WIDTH-1:0] y
    );
      assign y = ~a;
    endmodule
    
    module invert_bus_top (input wire [15:0] a, output wire [15:0] y);
      invert_bus #(.WIDTH(16)) u_inv (.a(a), .y(y));
    endmodule

    WIDTH=8 is the default. The instance overrides it with 16, so [WIDTH-1:0] becomes [15:0] on both ports. The logic is still a per-bit NOT: parameters choose the hardware dimensions before simulation starts, not during operation. Predict the port ranges for WIDTH=4 before moving on.

    Later application: a clocked register

    The same width parameter also sizes storage. The listing below previews clock/reset events, replication and <=; those are explained in the following lessons. First understand the parameter substitution, then return to the storage behavior after the two process templates.

    reg_n.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    module reg_n #(parameter WIDTH = 8) (
      input  wire             clk,
      input  wire             rst_n,
      input  wire [WIDTH-1:0] d,
      output reg  [WIDTH-1:0] q
    );
      always @(posedge clk or negedge rst_n)
        if (!rst_n) q <= {WIDTH{1'b0}};
        else        q <= d;
    endmodule
    
    module top (input wire clk, rst_n, input wire [15:0] din, output wire [15:0] dout);
      reg_n #(.WIDTH(16)) u_reg (.clk(clk), .rst_n(rst_n), .d(din), .q(dout));
    endmodule

    Catching typos with default nettype none

    An undeclared name on the left of a connection or assign quietly creates a 1-bit implicit wire. The design is then wrong in a way the compiler never reports.

    Start the file with the compiler directive default_nettype none, written with a leading backtick, to turn that mistake into an error. End the file with the same directive set to wire, so other files are unaffected.

    typo_demo.v
    1
    2
    3
    4
    // bad-example: without default_nettype none the typo 'sm' compiles
    module typo_demo (input wire a, b, output wire sum);
      assign sm = a ^ b;     // typo: creates an implicit wire 'sm'; 'sum' stays undriven
    endmodule

    Tip

    Naming conventions that pay off: _n suffix for active-low, _i/_o or _in/_out for direction (pick one and keep it), u_ prefix for instance names, and one module per file named after the module.

    Common pitfall

    A port without an explicit type defaults to a wire; a port without a range is 1 bit. input [7:0] a is an 8-bit wire, but a forgotten width on an output (output y) gives a single bit and silently drops the upper bits of a bus.

    Check yourself Why is connection by position dangerous even when it works today?

    It couples the instance to the port order of the module. If someone later inserts or reorders a port, positional instances still compile but connect the wrong signals. By-name connections remain correct and make a mismatch an error.

    Remember

    • Connect by name, not by position: a reordered port list still compiles and connects the wrong signals.

    • Give every parameter a default and override it by name.

    • A port with no range is 1 bit, so a forgotten width on an output silently drops the upper bits.

    Rule of thumb: turn on default_nettype none at the top of every file.

    Keep going

    • Code lab: Build a full adder from half adders

    • Interview: run a mock interview on this topic

    Related topics

    • Wire vs reg

    Previous

    13 / 350

    Next: Wire vs reg

    Up next

    Up nextWire vs reg

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • assign vs always
    • The Two Process Templates
    • Blocking vs Non-blocking