Menu

If-else в Verilog: условная логика в процедурных блоках

Как работает if/else внутри always-блока, latch-ловушка, в которую попадают новички, и priority-encoder-железо, которое получается из цепочек else if.

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

Знакомый синтаксис, другая ментальная модель

if/else выглядит ровно как в C:

if (condition) begin
    // ... операторы ...
end else begin
    // ... операторы ...
end

Но правила другие, потому что Verilog не софт. Две вещи держим в голове:

  1. if/else живёт только внутри процедурного блока. Нельзя написать отдельностоящий if на уровне module.
  2. Во что синтезируется if/else, зависит от типа блока. В комбинационном блоке - в mux или priority-encoder. В тактируемом - во flip-flop с условной логикой обновления.

Внутри комбинационного блока

Комбинационный always @(*) перезапускается, когда меняется a, b или c. Цепочка if/else выбирает одну ветку, присваивает max, и блок заканчивается. Поскольку max всегда присваивается (на каждом пути), синтезатор выдаёт чистую комбинационную логику - без latch.

Заметь, что max объявлен reg, хотя в железе нет flip-flop. То же правило, что всегда: всё, что присваивается внутри always, должно быть reg.

Latch-ловушка

Самый частый баг в комбинационном коде новичка:

// НЕПРАВИЛЬНО - выводит latch
always @(*) begin
    if (enable)
        out = data;
    // нет else! когда `enable` low, что делает `out`?
end

Синтезатор читает "когда enable low, out не присваивается" и решает, что out должен помнить старое значение. Помнить значение требует ячейку памяти, и тул вставляет latch. Latch в синхронных проектах вызывают timing-проблемы, их сложно сбрасывать и они почти никогда не то, что ты имел в виду.

Два способа фикса:

Оба дают одно и то же комбинационное железо - 2-to-1 mux. Паттерн "дефолт в начале" лучше масштабируется, когда у одного сигнала много условных присваиваний.

Внутри тактируемого блока

Замечай, что отличается от комбинационного случая:

  • Блок - always @(posedge clk) - территория flip-flop.
  • Присваивание через <= (non-blocking).
  • Нет else для случая "ни reset, ни enable". Это нормально. В тактируемом блоке, когда ни одна ветка не срабатывает, flip-flop просто держит старое значение - что физически делает flip-flop. Никакой latch не выводится, потому что сигнал уже регистр.

Это единственное место, где опустить else безопасно. Вне тактируемых блоков всегда обрабатывай каждый путь.

Цепочка else if: priority encoder

Цепочка else if-операторов имеет неявный приоритет - более ранние условия перекрывают более поздние:

requests[0] - высший приоритет: если он установлен, grant равен 0 независимо от того, что делают старшие биты. Синтезатор превращает цепочку в каскад mux: сначала проверяем бит 0, потом 1, потом 2, потом 3. Каждый уровень добавляет небольшую задержку.

Если условия взаимоисключающие - скажем, декодируешь one-hot вход - case (следующий документ) даёт более плоское и быстрое железо, чем цепочка else if. Используй форму case, когда нет реальной потребности в приоритете.

if без else в тактируемом коде

Тактируемому блоку не нужен else, потому что "держать старое значение" - дефолтное поведение. Так строят enables:

always @(posedge clk) begin
    if (load) target <= incoming;
    // нет else: когда load low, target держит значение
end

Это load-enabled-регистр. Большинство pipeline-регистров, счётчиков и конфигурационных регистров используют этот паттерн.

begin/end и одиночные операторы

Как в C, для одного оператора begin/end можно опустить:

if (a) out = 1;
else   out = 0;

Для чего-то длиннее одного оператора - используй блок:

if (a) begin
    out = 1;
    flag = 1;
end else begin
    out = 0;
    flag = 0;
end

Два паттерна свободно смешиваются. Style guides обычно рекомендуют всегда использовать begin/end, чтобы добавлять второй оператор было безболезненно.

Что дальше

Следующий документ - Case Statement - разбирает case, правильный инструмент для multi-way декодирования (state machines, диспетчеризация opcode, ROM-таблицы). После - циклы for, чуть отличающиеся от софтовых родственников, потому что разворачиваются на elaboration.

Часто задаваемые вопросы

Как работает оператор if в Verilog?

if (cond) statement; выполняет statement, когда cond non-zero. Можно обернуть несколько операторов в begin ... end. Добавь else statement; для альтернативной ветки или объедини с else if (other_cond) .... if/else существует только внутри процедурных блоков - initial или always - не на верхнем уровне module.

Что такое inferred latch в Verilog?

Latch, который синтезатор создал, когда ты его не просил, потому что твой комбинационный always-блок не присвоил сигнал в каждом пути. Тул видит 'if a then out = 1' без else, решает, что неприсвоенный случай должен помнить старое значение, и выдаёт latch. Latch почти всегда неправильны; фикс - давать каждому сигналу дефолтное значение в начале блока или явный else.

Как избежать inferred latch в Verilog?

В комбинационном always @(*)-блоке убедись, что каждый выходной reg присвоен на каждом пути. Самый чистый паттерн - выставить дефолты в начале блока и потом переопределять условно. Компилятор обычно предупреждает, когда выводит latch - относись к warning как к ошибке.

Во что синтезируется цепочка if-else в Verilog?

В priority encoder. Первый if имеет высший приоритет, следующий else if проверяется только если первый false, и так далее. В железе это становится цепочкой mux с зашитым порядком приоритета. Если условия взаимоисключающие, case-оператор с той же логикой часто синтезируется в более плоское железо и читается чище.

Coddy programming languages illustration

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

НАЧАТЬ