Module - единица Verilog
Всё в Verilog живёт внутри module. Module оборачивает кусок схемы: объявляет, какие сигналы входят, какие выходят и какие wires/registers/логика между ними сидят. Каждый чип, который ты когда-либо видел, - это дерево модулей, инстанцирующих другие модули, до самых вендорских примитивов вентилей.
Форма всегда одна и та же:
module name(port_list);
// объявления: wire, reg, parameter
// тело: assigns, instantiations, always-блоки
endmodule
Будем строить полный module пошагово.
Самый маленький полезный module: AND с двумя входами
Нужно что-то простое и автономное. AND-вентиль с двумя входами идеально подходит: два входа, один выход, одна строка логики.
Жми Run. Увидишь четыре строки таблицы истинности AND. Пройдёмся по каждому кусочку.
Читаем объявление module
module and_gate(
input wire a,
input wire b,
output wire y
);
module and_gateобъявляет module с именемand_gate. Это имя, по которому другие модули будут его инстанцировать.- Список в скобках - port list - сигналы, видимые снаружи.
input wire a-a- input port; этоwire(приводится в действие снаружи).output wire y-y- output port, который приводится в действие чем-то внутри module.
Если хочешь быть лаконичным, можно написать input a вместо input wire a - направление само по себе делает дефолтным тип wire. Но явно - привычка, которую стоит выработать. Module Ports разбирает все варианты ports.
Тело
assign y = a & b;
Это continuous assignment (непрерывное присваивание). Оно говорит: "каждый раз, когда a или b меняется, пересчитай y как побитовый AND этих двух". Никакого clock, никаких таймингов - отношение всегда истинно. Это чистая комбинационная логика.
endmodule закрывает блок. Module готов.
Testbench
Сам по себе and_gate запустить нельзя. Нужен второй module, который подаёт ему входы и наблюдает выходы. Это testbench, и по соглашению его называют test, tb или <design>_tb.
module test;
reg a, b;
wire y;
and_gate dut(.a(a), .b(b), .y(y));
...
endmodule
Три вещи, на которые стоит обратить внимание:
- Нет port list. Testbench - это вершина симуляции, снаружи него ничего нет.
reg a, bиwire y. Входы DUT - этоregв testbench, потому что мы подаём их из процедурного блока. Выход -wire, потому что его приводит в действие DUT.and_gate dut(.a(a), .b(b), .y(y)). Это instantiation. Мы штампуем копиюand_gateи называем еёdut(распространённое имя - "design under test"). Синтаксис.a(a)говорит: "подключи port с именемaна instance к локальному сигналу с именемa". Module Instantiation разбирает глубже.
Стимулы
initial begin
a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
$finish;
end
Блок initial срабатывает один раз при старте симуляции. Внутри него:
a = 0; b = 0;подаёт входы. Это blocking присваивания - порядок имеет значение, и каждое случается до следующего.#1продвигает симулированное время на 1 единицу. Это нужно, чтобы уyбыло время устаканиться после изменения входов. Без#1$displayнапечатает старое значениеy.$display(...)печатает в консоль симуляции. Форматная строка работает какprintfв C:%b- binary,%d- decimal,%h- hex,%t- симуляционное время.$finishзавершает симуляцию. Без него симулятор бы бесконечно продвигал время, ожидая события, которое не наступит.
Попробуй сломать
Поменяй module на OR (замени & на |) и перезапусти. Таблица истинности перевернётся. Теперь попробуй XOR:
Тот же скелет. Другой оператор. Это вся игра для комбинационной логики - объяви ports, напиши assign-ы, перебери входы, наблюдай выходы.
Module с двумя выходами
У модулей может быть больше одного выхода. Вот half-adder - два входа, sum и carry-out:
Два assign стоят рядом, но происходят параллельно - нет "сначала посчитай sum, потом carry". Оба всегда истинны. Это та самая параллельность, о которой говорили в Hardware vs Software, сделанная конкретной.
Что ты теперь знаешь
Ты увидел весь скелет файла Verilog: design-module с объявленными ports и телом, плюс testbench-module, который его инстанцирует, подаёт входы в блоке initial и репортит результаты. Почти каждый исходник Verilog, который ты прочтёшь, ложится в этот шаблон. Остальной язык - это просто заполнение богаче: более сложная логика, многобитные сигналы, тактируемое поведение и большие testbench.
Дальше - комментарии и стиль кода, чтобы модули оставались читаемыми по мере роста.
Часто задаваемые вопросы
Какой самый простой module на Verilog?
Самый маленький легальный module на Verilog - это просто module name; endmodule: ни ports, ни тела. Самый маленький полезный - module с одним выходом: module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule. Это реальный кусок комбинационной логики, который можно вставить в любой более крупный проект.
Как запустить Verilog module?
Сам по себе module запустить нельзя - это описание схемы, а не программа. Ты пишешь module-testbench, который инстанцирует твой design и подаёт ему входы, потом компилируешь оба через iverilog -o sim design.v test.v и запускаешь vvp sim. Редактор в браузере на этой странице делает оба шага, когда ты жмёшь Run.
Что такое testbench на Verilog?
Testbench - это второй module, обычно без ports, чья задача - потрясти твой design. Он инстанцирует design, дёргает его входы в блоке initial, наблюдает выходы через $display или $monitor и вызывает $finish, когда закончил. Testbench не синтезируется, он существует только чтобы верифицировать поведение.
Зачем коду на Verilog нужен $finish?
Потому что железо не останавливается. Симулятор делает вид, что время идёт, и без явного $finish он бы бесконечно крутил время, ожидая новых событий. $finish говорит симулятору 'мы закончили, выходим чисто'. В testbench это последняя строка блока initial: прогнал тест - финиш.