Блокирующее = и неблокирующее <= присваивание
Разбираем главную грабли начинающих в Verilog — два вида присваивания, от выбора которых зависит, какое железо получится.
Неблокирующее присваивание
<=внутри тактируемого блока означает: «вычисли правые части всех присваиваний по старым значениям, а затем одновременно обнови левые» — ровно так ведут себя параллельные триггеры.
В always-блоках Verilog есть два оператора присваивания: блокирующее = и неблокирующее <=. Выбор между ними — самая частая и коварная ошибка новичков, потому что в симуляции они дают разные результаты, и неправильный выбор порождает не то железо, что вы задумали. Разберём раз и навсегда.
В чём разница
Блокирующее = работает как присваивание в обычных языках: выполняется немедленно и по порядку, сразу обновляя переменную. Следующая строка уже видит новое значение — присваивания «блокируют» друг друга, идут последовательно.
Неблокирующее <= работает иначе и моделирует параллельность железа в два шага: (1) на фронте такта вычисляются все правые части — по старым значениям сигналов; (2) затем все левые части обновляются одновременно. Внутри одного фронта присваивания не видят результатов друг друга.
Эта разница критична, когда сигналы зависят друг от друга. Сравним два блока, меняющих местами a и b по такту.
Классический пример: обмен значений
// НЕблокирующее: настоящий обмен (два параллельных триггера)
always @(posedge clk) begin
a <= b; // правые части берутся по СТАРЫМ a и b...
b <= a; // ...поэтому это честный swap
end
// Блокирующее: НЕ обмен, а копирование!
always @(posedge clk) begin
a = b; // a сразу становится равно b
b = a; // b = a, но a уже изменено -> b тоже станет b
endС <= на фронте берутся старые a и b, и они меняются местами — как два независимых триггера. С = первая строка уже испортила a, поэтому вторая копирует новое значение, и обмена не выходит. Промоделируем оба сценария на Python:
a, b = 3, 7
# Неблокирующее: правые части по старым значениям, потом одновременное обновление
old_a, old_b = a, b
new_a = old_b # a <= b
new_b = old_a # b <= a
a_nb, b_nb = new_a, new_b
# Блокирующее: по порядку, немедленно
a2, b2 = 3, 7
a2 = b2 # a = b -> a2 = 7
b2 = a2 # b = a -> b2 = 7 (a уже испорчено)
print(f"Неблокирующее <= : a={a_nb}, b={b_nb} (обмен удался)")
print(f"Блокирующее = : a={a2}, b={b2} (обмена нет)")Вывод:
Неблокирующее <= : a=7, b=3 (обмен удался) Блокирующее = : a=7, b=7 (обмена нет)
Разница налицо: <= дал честный обмен, = — потерю данных. Поскольку триггеры в железе срабатывают одновременно, именно <= моделирует их верно.
Золотое правило
Чтобы не гадать, запомните два простых правила, которым следуют профессионалы:
| Тип логики | Присваивание | Блок |
| Последовательностная (триггеры, по такту) | <= неблокирующее | always @(posedge clk) |
| Комбинационная (без памяти) | = блокирующее | always @(*) |
В тактируемых блоках — всегда <=; в комбинационных — всегда =. Не смешивайте оба вида в одном блоке. Следование этому правилу избавляет от 90% загадочных ошибок симуляции и расхождений «симуляция против железа».
Как работает под капотом
Почему <= так устроено? Реальные триггеры на фронте такта одновременно читают свои входы (старые значения соседей) и одновременно обновляют выходы. Между фронтами выходы стабильны. Неблокирующее присваивание — это и есть точная модель такого поведения: «прочитали все входы → обновили все выходы разом». Блокирующее же навязывает последовательный порядок, которого в параллельном железе нет.
Частые ошибки
- Использовать
=в тактируемом блоке. Это даёт неверную симуляцию цепочек регистров и часто — расхождение с синтезированным железом. - Использовать
<=в комбинационном блоке. Усложняет отладку и может породить лишние задержки в симуляции. - Смешивать
=и<=в одномalways. Поведение становится трудно предсказуемым; многие инструменты выдают предупреждение.
Итог
=блокирующее: последовательно и немедленно (как в обычных языках).<=неблокирующее: правые части по старым значениям, обновление одновременно — модель параллельных триггеров.- Правило: тактируемая логика →
<=, комбинационная →=, не смешивать.