Menu

Module instantiation в Verilog: подключение подмодулей

Как инстанцировать один module внутри другого, разница между именованными и позиционными соединениями ports и паттерны нескольких instance, которые ты будешь использовать в реальных проектах.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Модули внутри модулей

Verilog-проект - это дерево модулей. Top-level-module (твой testbench или top-level wrapper чипа) инстанцирует более низкоуровневые модули, которые инстанцируют ещё более низкоуровневые, до самых вендорских примитивов вентилей. Instantiation - это синтаксис для такой вложенности.

Форму ты уже видел - мы использовали её в Первом module:

and_gate dut(.a(a), .b(b), .y(y));

Эта строка штампует один instance and_gate, называет его dut и подключает его ports к локальным сигналам. Разберём каждый кусочек.

Форма instantiation

module_name instance_name (port_connections);
  • module_name должно совпадать с именем из объявления module где-то в твоём проекте. Verilog регистрозависимый.
  • instance_name - метка, которую ты выбираешь, обычно описательная для роли этого instance. Ты будешь использовать её в иерархических путях и waveform-просмотрщиках.
  • port_connections соединяет ports instance с локальными сигналами. Есть два способа это написать.

Именованные port-соединения (используй их)

Именованная форма выглядит так:

my_module instance_name(
    .clk    (clk),
    .reset  (reset_n),
    .data_in(in_bus),
    .data_out(out_bus),
    .valid  (out_valid)
);

Каждая пара .port(signal) говорит: "подключи port этого instance под именем port к локальному сигналу с именем signal". Порядок не важен. Если ты добавишь новый port в объявление module, существующие instantiations не сломаются - пока ты дашь новому port дефолт или обновишь каждое место.

Две практичные пометки:

  • Имя port (слева от скобок) должно точно совпадать с объявлением module.
  • Имя сигнала (внутри скобок) локально к месту, где живёт instantiation - обычно к родительскому module.

Если port не подключен - оставь скобки пустыми: .optional_port(). Сигнал внутри instance плавает (z). Некоторые синтезаторы предупреждают; большинство принимает.

Позиционные port-соединения (избегай)

Более лаконичная форма перечисляет сигналы в порядке port list:

my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);

Короче, но хрупко. Переупорядочишь port list module (реальный рефакторинг, который случается) - и каждое позиционное instantiation тихо подключится неправильно. Не тянись к позиционному, если port list не состоит из одного-двух членов и вряд ли когда-то поменяется.

Где это всё ещё приемлемо: крошечные утилитарные модули, где порядок ports - часть API. Двухвходовый вентиль - нормально позиционно. 30-port memory controller - гарантированные проблемы.

Полный иерархический пример

Это настоящая трёхуровневая иерархия: testfull_adder → два instance half_adder. У каждого instance - своя копия вентилей внутри half_adder; инструмент синтеза выдаст по одной схеме на каждое instantiation.

Несколько instance одного module

Когда инстанцируешь один и тот же module несколько раз, каждый instance - это независимое железо. Они не делят состояние. Не делят вентили. Представляй каждый instance как свежую копию, отштампованную из проекта.

adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));

Это четыре отдельных adder, работающих параллельно. Если бы adder содержал регистр, у каждого instance была бы своя копия этого регистра со своим состоянием.

Циклы generate: штамповка повторяющегося железа

Написать четыре instance руками - нормально. Написать 64 - нудно. Блок generate позволяет elaborator-у нажать клавиши за тебя:

Три куска нового синтаксиса:

  • genvar i объявляет loop-переменную для generate. Это не runtime-сигнал - она существует только на etapie elaboration.
  • generate ... endgenerate оборачивает цикл. Некоторые тулы принимают generate-циклы без явного ключевого слова generate, но писать его - делать намерение очевидным.
  • begin : invert_loop даёт имя generate-скоупу. Метка становится частью иерархического имени каждого сгенерированного instance (dut.invert_loop[0].u_inv, dut.invert_loop[1].u_inv и т.д.).

Синтезатор разворачивает цикл и выдаёт WIDTH копий bit_inverter. Каждая копия - независимое железо.

Override parameters при instantiation

Если у module есть parameters, их можно переопределить через #(.PARAM(value)) между именем module и именем instance:

counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));

Оба instance используют один и тот же источник counter, но имеют разные ширины. Синтаксис разбирали в Parameters; он чисто встраивается в instantiation.

Иерархические имена

Когда есть иерархия, у каждого сигнала есть иерархический путь:

test.dut.ha0.sum

Читается как: в module test, внутри instance dut, внутри instance ha0, сигнал с именем sum. Ты будешь видеть такие пути в waveform-просмотрщиках, сообщениях об ошибках и в редких вызовах $display, которые лезут глубоко в подмодуль из testbench:

$display("внутренний carry1 = %b", dut.carry1);

Иерархические референсы такие - только для testbench и отладки. Синтезируемый RTL не лезет в чужие модули.

Типичные ошибки

Несоответствие имени port. .clk_in(clk) подключает локальный clk к port под именем clk_in. Если port в module реально называется clk, парсер тебе скажет (некоторые тулы яснее других).

Несоответствие ширины на port. Подключение 4-битного сигнала к 8-битному port тихо zero-extend'ит; обратное тихо обрезает. Большинство тулов предупреждает; если предупреждений не видишь - ищи внимательнее.

Забыли # для parameter. counter (.WIDTH(8)) c(.clk(clk)) выглядит как override, но это не он - парсер пытается принять (.WIDTH(8)) как port-соединение и падает. Правильно: counter #(.WIDTH(8)) c(.clk(clk)).

Повторное использование имени instance. Два instance не могут иметь одинаковое имя в одном scope. Сообщение об ошибке обычно ясное; ловушка - соблазн copy-paste.

Что дальше

Теперь умеешь соединять модули в реальную иерархию. Следующий документ закругляет структурную сторону Verilog - Continuous Assignment - и глубже разбирает оператор assign, которым мы уже свободно пользовались с первой главы.

Часто задаваемые вопросы

Как инстанцировать module в Verilog?

Напиши имя module, потом имя instance, потом список port-соединений в скобках: my_module instance_name(.port(signal), ...);. Самый распространённый стиль - именованные соединения (.port(signal)), которые матчатся по имени port независимо от порядка. Более лаконичный позиционный стиль (my_module instance(signal1, signal2)) зависит от порядка port list и опасен в сопровождении.

В чём разница между именованными и позиционными port-соединениями?

Позиционные соединения перечисляют сигналы в том же порядке, что и port list module: первый сигнал подключается к первому port, второй ко второму и т.д. Именованные соединения используют .port_name(signal_name), матчатся по имени. Именованные многословнее, но устойчивы к переупорядочиванию ports и самодокументируются на месте вызова. Используй именованные, если ports больше двух-трёх.

Можно ли инстанцировать один и тот же Verilog module несколько раз?

Да - в этом весь смысл. Каждый instance - это независимое железо со своим состоянием. Если у тебя есть module adder, его можно инстанцировать 64 раза в SIMD-юните, каждый раз с разными входами. Цикл generate - каноничный синтаксис, когда instance похожи и индексируются.

Что такое generate-блок в Verilog?

generate ... endgenerate - это compile-time-конструкция, которая штампует повторяющееся железо. Цикл for внутри generate создаёт N instance того, что в теле. generate срабатывает в момент elaboration, до начала симуляции - это не runtime-цикл, это генератор кода для синтезатора.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ