Menu

Always-блок в Verilog: комбинационная и последовательностная логика

Как работают always-блоки, разница между комбинационным always @(*) и тактируемым always @(posedge clk) и правила, которые решают, какое железо получится.

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

Рабочая лошадка behavioral-Verilog

assign описывает комбинационную логику из одного уравнения. Как только нужны if/else, case или память - тянешься к always. Always-блок - это процедурный код, который перезапускается, когда меняются определённые сигналы. Сигналы, инициирующие перезапуск, - это sensitivity list.

Чаще всего встретишь две формы always:

  1. always @(*) - перезапускается, когда любой читаемый в блоке сигнал меняется. Строит комбинационную логику.
  2. always @(posedge clk) - перезапускается только на нарастающем фронте clk. Строит тактируемую последовательностную логику (flip-flop).

Существуют и другие формы (@(a or b), @(negedge clk), @(posedge clk or negedge reset_n)), но две выше покрывают почти каждый синтезируемый блок, который ты напишешь.

Комбинационный always @(*)

Три вещи, которые стоит заметить:

  • out - это reg. Всё, чему присваивают внутри always, должно быть reg. Ключевое слово не значит "это flip-flop"; здесь оно просто значит "пишу из процедурного блока".
  • always @(*). * говорит "просыпайся, когда меняется всё, что я читаю". Симулятор сам выводит sensitivity list. Можно написать список вручную - always @(sel) - но @(*) безопаснее, потому что пропущенный сигнал - классический источник багов.
  • Никакого clock. Этот блок описывает комбинационную логику. Синтезатор выдаёт кусок логики, который вычисляет out из sel напрямую - без flip-flop, без clock-пина.

Случай default не опционален по сути, хотя опционален по синтаксису. Опусти его - и любое неуказанное значение оставит out со старым значением, что синтезируется в нежелательный latch. Всегда включай default.

Последовательностный always @(posedge clk)

Ключевые отличия от комбинационной версии:

  • always @(posedge clk). Блок перезапускается только на нарастающем фронте clk. Между фронтами ничего не происходит.
  • Non-blocking присваивание <=. Внутри тактируемого блока это правильный оператор. Он говорит "запланируй count принять новое значение в конце временного шага", что и есть поведение flip-flop. Почему и альтернатива - в Blocking vs Non-blocking.
  • default не нужен. if покрывает обе ветки (reset и не-reset). Риска latch нет.

Синтезатор видит этот силуэт - тактируемая sensitivity, non-blocking присваивание - и выдаёт 4-битный регистр (четыре flip-flop) плюс комбинационную логику, считающую count + 1, и mux, выбирающий между reset и инкрементом.

Разница для синтеза

Один и тот же исходник module может описать два совершенно разных куска железа в зависимости от формы always-блока:

БлокЖелезо
always @(*) y = expr;Чистая комбинационная логика. Без памяти.
always @(posedge clk) y <= expr;Flip-flop. Захватывает expr раз за такт.
always @(*) if (en) y = expr;Latch - обычно баг. "Else"-ветка сохраняет старое значение.
always @(posedge clk) if (en) y <= expr;Flip-flop с enable. Захватывает только когда en high.

Третий случай - latch-ловушка. Latch - прозрачная ячейка памяти, которая держит свой выход, когда вход не выставлен - полезен в специфических проектах, почти всегда баг при случайном появлении. Большинство синтезаторов громко предупреждают, когда выводят latch, который ты не просил. Относись к warning как к ошибке.

Варианты sensitivity list

Встретишь несколько менее частых:

  • always @(a or b or c) - явный список. Verilog-2001 добавил разделитель ,: always @(a, b, c). Работает и то, и то.
  • always @(posedge clk or negedge reset_n) - асинхронный reset. Блок срабатывает на нарастающем фронте clock или ниспадающем фронте reset. Используется, когда reset должен сработать немедленно, не дожидаясь следующего clock.
  • always @(negedge clk) - тактирование по падающему фронту. Редко; некоторые проекты используют для "negative-edge-triggered" flip-flop, захватывающих на падающем, а не на нарастающем.

Для новых проектов предпочитай always @(*) для комбинационной и always @(posedge clk) для последовательностной. К асинхронному reset тянись только когда дизайн реально требует.

Два блока - два куска железа

Несколько always-блоков в одном module независимы - каждый становится своим куском железа:

Тактируемый блок даёт flip-flop-регистр. Комбинационный - XOR-вентиль. Они живут бок о бок; ни один не знает о другом. Два выхода меняются по совершенно разным графикам.

Что always-блоки не могут

Несколько вещей, выглядящих заманчиво, но запрещённых:

  • Присваивать wire: цель должна быть reg. Компилятор обеспечивает.
  • Присваивать одному reg из двух разных always-блоков: даёт undefined-поведение в симуляции и не синтезируется. Один driver на сигнал.
  • Читать и писать один и тот же сигнал в одном комбинационном блоке так, чтобы создать петлю обратной связи: always @(*) x = x + 1; - это zero-delay-петля, которую симулятор не может разрешить.

Первые два ловит компилятор. Третье иногда проявляется только в момент симуляции как зависание.

Что дальше

Следующий документ - Initial Block - разбирает родственника always: блок, который срабатывает ровно один раз при старте симуляции. Это рабочая лошадка testbench. После - правила blocking vs non-blocking, которые определяют, делает ли твой тактируемый блок то, что ты хотел.

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

Что такое always-блок в Verilog?

always вводит процедурный блок, который перезапускается каждый раз, когда сигналы в его sensitivity list меняются. Есть два вида: always @(*) строит комбинационную логику (перезапускается при любом изменении входов), а always @(posedge clk) строит последовательностную (перезапускается на каждом нарастающем фронте clk). Тело always-блока может содержать if, case, for и процедурные присваивания.

В чём разница между always @(*) и always @(posedge clk)?

always @(*) чувствителен к любому сигналу, читаемому в блоке; даёт комбинационную логику без памяти. always @(posedge clk) чувствителен только к нарастающему фронту clk; даёт flip-flop, захватывающие состояние раз за такт. У первого нет clock и нет регистра; у второго есть оба.

Что такое sensitivity list в Verilog?

Список сигналов после @, определяющий, когда always-блок перезапускается. @(*) - shorthand для 'каждый сигнал, читаемый в блоке'. @(posedge clk) срабатывает только на нарастающем фронте clk. @(posedge clk or negedge reset_n) срабатывает на любом из событий - для асинхронных reset. Неправильный sensitivity list - один из самых частых источников несовпадения симуляции и синтеза.

Можно ли присвоить wire внутри always-блока?

Нет. always-блоки могут присваивать только reg (или logic в SystemVerilog). Компилятор это обеспечивает. Если хочешь, чтобы wire был выходом процедурной логики, объяви промежуточный reg, приводи его внутри always и assign-ни wire от reg снаружи - или просто меняй wire на reg.

Coddy programming languages illustration

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

НАЧАТЬ