Модули и порты: первый блок схемы
Знакомимся с модулем — базовой единицей описания схемы в 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.
- Из модулей строят иерархию; подмодули инстанцируют, соединяя порты по имени.
- Каждый экземпляр модуля — отдельная физическая копия железа.