Блокирующее = и неблокирующее <= присваивание

Разбираем главную грабли начинающих в 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. Поведение становится трудно предсказуемым; многие инструменты выдают предупреждение.

Итог

  • = блокирующее: последовательно и немедленно (как в обычных языках).
  • <= неблокирующее: правые части по старым значениям, обновление одновременно — модель параллельных триггеров.
  • Правило: тактируемая логика → <=, комбинационная → =, не смешивать.
Проверьте себя
1. Как ведёт себя неблокирующее присваивание <= внутри тактируемого always-блока?
AВыполняется немедленно и по порядку, как в обычных языках
BСначала вычисляются все правые части по старым значениям, затем все левые обновляются одновременно
CНе выполняется вовсе
DСлучайно выбирает порядок
2. Какое присваивание следует использовать для последовательностной (тактируемой) логики?
AБлокирующее =
BНеблокирующее <=
CЛюбое — разницы нет
DОба одновременно в одном блоке
3. Что произойдёт при попытке обмена a и b блокирующими присваиваниями (a = b; b = a;) по такту?
AЧестный обмен значений
BОбмена не будет: оба сигнала станут равны старому b
CОшибка синтеза
Da и b обнулятся