Модули и порты: первый блок схемы

Знакомимся с модулем — базовой единицей описания схемы в Verilog.

module — именованный блок схемы с описанными входами и выходами (портами); из модулей, как из микросхем на плате, собираются более крупные схемы.

Любая схема в Verilog — это модуль. Думайте о модуле как о микросхеме в корпусе: у неё есть ножки (порты), через которые сигналы входят и выходят, а внутри — начинка, описывающая логику. Модуль можно вставлять в другие модули, выстраивая иерархию: маленькие блоки собираются в большие, как сумматоры — в АЛУ, а АЛУ — в процессор.

Анатомия модуля

Объявление модуля начинается с ключевого слова module, за ним имя и список портов, а заканчивается endmodule. Каждый порт имеет направление: input (вход), output (выход) или inout (двунаправленный, встречается реже). Вот модуль полусумматора — он складывает два бита и выдаёт сумму и перенос:

module half_adder(
    input  wire a,      // первое слагаемое (1 бит)
    input  wire b,      // второе слагаемое (1 бит)
    output wire sum,    // бит суммы
    output wire carry   // бит переноса
);
    assign sum   = a ^ b;   // сумма  = a XOR b
    assign carry = a & b;   // перенос = a AND b
endmodule

Читаем это как описание схемы: «есть блок half_adder с двумя входами и двумя выходами; внутри стоят вентиль XOR (даёт sum) и вентиль AND (даёт carry)». Оба вентиля работают параллельно и постоянно.

Иерархия: модуль внутри модуля

Сила модулей — в переиспользовании. Полный сумматор складывает три бита (включая входной перенос) и собирается из двух полусумматоров. Подключение подмодуля называют инстанцированием:

module full_adder(
    input  wire a, b, cin,
    output wire sum, cout
);
    wire s1, c1, c2;

    // инстанцируем первый полусумматор и соединяем его порты по имени
    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

Запись .a(a) означает «порт a подмодуля соединить с сигналом a текущего модуля». Это соединение по имени — самый надёжный способ: он не путается при изменении порядка портов. Внутренние провода s1, c1, c2 объявлены как wire — они связывают подмодули между собой.

Как работает под капотом

Инстанцирование — это не «вызов функции», как в обычных языках. Когда вы пишете два half_adder, в железе появляются две независимые физические копии схемы полусумматора, работающие одновременно. Это прямое следствие параллельной природы Verilog: повторное использование модуля множит железо, а не переиспользует один экземпляр во времени. Если инстанцировать модуль 100 раз — на кристалле будет 100 копий, и все они займут ресурсы.

Проверим логику полного сумматора на Python, смоделировав его таблицу истинности, — чтобы убедиться, что схема из двух полусумматоров считает верно:

def full_adder(a, b, cin):
    s1 = a ^ b
    c1 = a & b
    summ = s1 ^ cin
    c2 = s1 & cin
    cout = c1 | c2
    return summ, cout

print("a b cin | sum cout")
print("--------+---------")
for a in (0, 1):
    for b in (0, 1):
        for cin in (0, 1):
            s, c = full_adder(a, b, cin)
            print(f"{a} {b}  {cin}  |  {s}   {c}")

Вывод:

a b cin | sum cout
--------+---------
0 0  0  |  0   0
0 0  1  |  1   0
0 1  0  |  1   0
0 1  1  |  0   1
1 0  0  |  1   0
1 0  1  |  0   1
1 1  0  |  0   1
1 1  1  |  1   1

Сумма и перенос совпадают с арифметикой: например, 1+1+1 даёт sum=1, cout=1, то есть двоичное «11» = 3. Логика модуля верна.

Частые ошибки

  • Соединять порты по позиции. Запись half_adder ha1(a, b, s1, c1) работает, но при изменении порядка портов молча сломается. Всегда соединяйте по имени: .port(signal).
  • Считать инстанцирование вызовом функции. Каждый экземпляр — отдельное железо, а не переиспользуемый код во времени.
  • Забывать endmodule или точку с запятой. Verilog строг к синтаксису; пропуск ломает разбор всего файла.

Итог

  • Модуль — базовый блок схемы с портами input/output.
  • Из модулей строят иерархию; подмодули инстанцируют, соединяя порты по имени.
  • Каждый экземпляр модуля — отдельная физическая копия железа.
Проверьте себя
1. Что такое модуль (module) в Verilog?
AФункция, вызываемая во время выполнения
BИменованный блок схемы с портами input/output, из которого строят иерархию
CПеременная для хранения данных
DКоманда процессора
2. Что происходит в железе, если инстанцировать один модуль 100 раз?
AСоздаётся одна копия, используемая 100 раз во времени
BСоздаётся 100 независимых физических копий схемы, работающих параллельно
CВозникает ошибка синтеза
DМодуль выполняется 100 раз подряд
3. Почему соединение портов по имени (.a(a)) предпочтительнее соединения по позиции?
AОно работает быстрее в железе
BОно не ломается при изменении порядка портов в объявлении модуля
CЭто единственный допустимый синтаксис
DПо позиции соединять вообще нельзя