Модули внутри модулей
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 - гарантированные проблемы.
Полный иерархический пример
Это настоящая трёхуровневая иерархия: test → full_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-цикл, это генератор кода для синтезатора.