Menu

Твой первый module на Verilog: пошаговый разбор

Напиши свой первый полный module на Verilog с нуля - объявление, ports, кусок комбинационной логики и testbench, который его дёргает. Запускается прямо в браузере.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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: прогнал тест - финиш.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ