Иерархия: как не утонуть в проводах
В прошлой статье ты собрал свой первый настоящий вычислительный блок — Полусумматор (Half Adder). Два входа, два выхода, пара вентилей — XOR для суммы и AND для переноса. Просто, элегантно, работает.
Но давай посмотрим правде в глаза: Полусумматор умеет складывать только один бит. А что, если нам нужно сложить 113 и 29? 113 в двоичной системе — это 01110001. Девять бит. Нам потребуется складывать их поразрядно, и каждый разряд зависит от переноса предыдущего.
И тут мы упираемся в проблему: у Полусумматора всего два входа. У него нет входа для переноса. А реальное сложение в столбик (вспомни школу!) выглядит так: ты складываешь цифры разряда, прибавляешь к ним то, что «в уме» от предыдущего разряда, записываешь результат и передаёшь новый перенос дальше. Три входных сигнала, не два.
Нам нужен Полный сумматор (Full Adder).
Третий вход: carry in
Полный сумматор — это эволюция полусумматора. Его главное супер-свойство: у него есть третий вход cin (carry in — «перенос на входе»).
Сигнатура модуля:
module FullAdder(
input a,
input b,
input cin,
output sum,
output cout
);
Три входа, два выхода. На выходе sum — результат сложения a + b + cin. На выходе cout — новый перенос в следующий разряд (то самое «единица в уме»).
Давай вспомним двоичную арифметику из первой статьи. Сложение в двоичной системе — это просто: 0+0=0, 0+1=1, 1+1=0 с переносом 1. А теперь у нас три слагаемых. Сколько всего комбинаций? 2³ = 8. И вот как выглядит полная таблица истинности:
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
Обрати внимание на закономерность: sum — это 1, когда количество единиц среди входов нечётное (1 или 3). А cout — это 1, когда количество единиц 2 или больше. Просто? Просто. Но как это реализовать?
Два полусумматора + один OR
А вот и главный трюк всей статьи. Полный сумматор можно собрать из двух экземпляров Полусумматора и одного вентиля OR. Давай разберём эту сборку шаг за шагом.
Шаг 1. Подаём a и b на первый Полусумматор. На его выходе s1 — это a XOR b, а c1 — это a AND b.
Шаг 2. Подаём s1 (результат первого полусумматора) вместе с cin на второй Полусумматор. На его выходе sum — это финальная сумма s1 XOR cin, а c2 — это s1 AND cin.
Шаг 3. Финальный перенос cout — это c1 OR c2. Почему OR, а не XOR? Потому что перенос мог возникнуть и на первом этапе (если a и b оба равны 1), и на втором (если s1 и cin оба равны 1). Поскольку эти ситуации не могут произойти одновременно (в чём легко убедиться по таблице истинности), OR покрывает оба случая.
Получается лаконичная, иерархическая структура: сложное строится из простого.
Вернёмся в 1970-й: почему иерархия — это важно
Представь, что ты инженер в начале 1970-х и проектируешь процессор Intel 4004 — первый в мире коммерческий микропроцессор. В нём около 2300 транзисторов. Если бы каждый транзистор проектировали с нуля, чертежи заняли бы целую стену. Никто бы не смог уследить за таким количеством соединений.
Но инженеры не рисуют каждый транзистор по отдельности. Они собирают их в логические вентили (AND, OR, NOT), из вентилей собирают функциональные блоки (сумматоры, мультиплексоры, регистры), а из этих блоков — процессор целиком. Каждый уровень абстракции прячет внутреннюю сложность и открывает только интерфейс (входы и выходы).
В программировании ты называешь это функцией или классом. Ты написал однажды функцию calculateTotal(items) — и теперь вызываешь её из ста разных мест, не думая о том, какие циклы и условия внутри.
В Verilog это называется модулем и его инстанцированием.
Инстанцирование модулей: как «вызвать» железо
Когда ты написал module HalfAdder(...), ты описал, как устроен полусумматор. Но сам по себе этот код не создаёт никаких вентилей в схеме. Чтобы реально разместить полусумматор внутри другого модуля, нужно его инстанцировать — то есть создать конкретный экземпляр.
Синтаксис очень похож на вызов функции, но не путай: инстанцирование происходит на этапе «компиляции» схемы (синтеза), а не во время работы.
Способ 1: именованное подключение (рекомендуемый)
HalfAdder ha1 (
.a(a),
.b(b),
.sum(s1),
.cout(c1)
);
HalfAdder — это имя модуля (тип). ha1 — это имя конкретного экземпляра. В скобках мы явно пишем: «к порту .a модуля HalfAdder подключи сигнал a из текущего модуля».
Способ 2: позиционное подключение
HalfAdder ha2 (s1, cin, sum, c2);
Здесь порядок сигналов должен строго совпадать с порядком портов в объявлении модуля.
Собираем FullAdder из двух HalfAdder
module FullAdder(
input a,
input b,
input cin,
output sum,
output cout
);
wire s1;
wire c1;
wire c2;
HalfAdder ha1 (
.a(a), .b(b),
.sum(s1), .cout(c1)
);
HalfAdder ha2 (
.a(s1), .b(cin),
.sum(sum), .cout(c2)
);
or (cout, c1, c2);
endmodule
Видишь, что получилось? Внутри модуля FullAdder нет ни одного логического вентиля в явном виде. Вместо этого мы сказали: «поставь сюда ha1, поставь сюда ha2, добавь один OR». Verilog-синтезатор развернёт ha1 и ha2 в их внутреннюю структуру (XOR + AND), и на физическом уровне получится схема из трёх вентилей.
Аналогия с программным кодом
В софте каждый вызов функции выполняется в своё время, на одном и том же CPU. В железе каждый экземпляр модуля — это отдельный кусок кремния. ha1 и ha2 существуют одновременно и работают параллельно.
В софте ты можешь вызвать функцию 8 раз, и она выполнится 8 раз последовательно. В железе ты инстанцируешь модуль 8 раз, и все 8 копий работают одновременно. Это основа параллелизма, о которой мы ещё поговорим в следующей статье.
Совет: давай портам осмысленные имена
Хорошие имена портов и сигналов:
alu_result— понятно, что это результат АЛУpc_next— следующее значение счётчика командmem_addr— адрес памяти
Плохие имена:
sig1,tmp,out,data— непонятно, откуда и куда
Резюме
1. Полный сумматор нужен, чтобы складывать биты с учётом переноса от предыдущего разряда. У него три входа (a, b, cin) и два выхода (sum, cout).
2. FullAdder собирается из двух HalfAdder и одного OR — это первый пример иерархического проектирования.
3. Verilog позволяет инстанцировать модули внутри других модулей, подключая порты по именам (.port(signal)) или по порядку.
Иерархия — это не просто удобство. Это единственный способ справиться со сложностью цифровых схем. Процессор на миллиарде транзисторов спроектирован как многоуровневая иерархия: транзисторы → вентили → функциональные блоки → модули → ядра → чип.
На уровне 3.3 тебе предстоит собрать FullAdder из двух HalfAdder (которые ты уже собрал на уровне 3.2) и одного вентиля OR. Не забудь использовать именованное подключение портов — привыкай к хорошему стилю с самого начала.