Menu

Verilog blocking vs non-blocking: когда использовать = vs <=

Самая путающая тема для новичков в Verilog. Что на самом деле значат = и <= внутри always-блока и правило, предотвращающее большинство race conditions.

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

Два оператора

Внутри always и initial-блоков у Verilog два оператора присваивания:

  • = - blocking-присваивание. Обновляет LHS сейчас, до перехода к следующему оператору.
  • <= - non-blocking-присваивание. Вычисляет RHS сейчас, планирует LHS на обновление в конце текущего временного шага.

Вне процедурных блоков (в assign) существует только = - assign a <= b это синтаксическая ошибка. Внутри процедурных блоков оба легальны, и выбор правильного - самое важное решение для новичка в Verilog.

Что делает blocking

Blocking - то, чего ждёшь от софтового языка: строка за строкой, сверху вниз. Оператор N завершается до того, как стартует N+1:

Чисто последовательно. Каждый оператор видит эффекты тех, что выше. Соответствует софтовой интуиции.

Что делает non-blocking

Non-blocking имеет форму железа. Все правые части вычисляются с использованием значений в начале временного шага. Все левые части обновляются в конце временного шага. Порядок операторов в исходнике не меняет зависимости между сигналами:

Этот фрагмент реализует ротацию a → b → c → a за один шаг. С blocking понадобилась бы временная переменная, чтобы не затереть одно из значений. С non-blocking обмен атомарен, потому что каждое RHS читает до-шаговые значения.

Это ровно то, как три flip-flop ведут себя на фронте clock: все захватывают входы в один и тот же момент, независимо от зависимостей между ними.

Правило, которое спасает

Используй <= в тактируемых блоках. Используй = в комбинационных блоках.

Это всё. Запомни. Кодируй не задумываясь. Большинство race conditions, несовпадений симуляции и синтеза и багов "у меня работает, а в синтезе нет" происходят от нарушения этого правила.

У правила есть железная причина: тактируемые блоки моделируют flip-flop, которые сэмплируют входы одновременно. Комбинационные блоки моделируют логику, распространяющуюся настолько быстро, насколько может. Семантика операторов присваивания соответствует этим поведениям.

Каждое <= читает текущее значение исходного регистра и планирует регистр-приёмник принять это значение в конце временного шага. Чистый эффект - ровно то, что делает аппаратный shift register: каждый flip-flop захватывает значение соседа, всё на одном фронте clock, без race.

Теперь посмотри, что было бы с blocking:

// НЕПРАВИЛЬНО - это не shift register!
always @(posedge clk) begin
    out[3] = out[2];   // out[3] становится out[2]
    out[2] = out[1];   // out[2] становится out[1], которое мы только что поставили выше
    out[1] = out[0];
    out[0] = in;
end

Каждый оператор затирает источник до того, как следующий его прочтёт. За один такт in пробежал бы насквозь до out[3], потому что каждая строка видит свежезаписанное значение от той, что выше. Поведение в реальном железе (где non-blocking-семантика) было бы совершенно иным, чем показал симулятор.

Комбинационные блоки: blocking - правильно

Для always @(*) правильно blocking-присваивание. Нет flip-flop, нет правила одновременного захвата, которое надо обеспечивать, и промежуточные переменные полезны:

sum вычисляется первым, затем result использует свежепосчитанное значение. Комбинационная логика сплющивается в один кусок железа: result = ~(a + b). Flip-flop не появляются, потому что нет clock.

Если бы здесь использовал <=, симулятор всё равно обновлял бы sum до того, как result был против него вычислен (потому что оба обновления случаются в конце шага), но порядок был бы чуть иным, и многие синтезаторы жалуются. Не смешивай; выбирай оператор, соответствующий типу блока.

Самая болезненная ошибка

Вот она: тактируемый блок с blocking-присваиванием.

// БАГ: race condition в ожидании
always @(posedge clk) begin
    a = b;
    b = c;
    c = a;
end

В симуляции симулятор может дать a сначала, потом b, потом c, выдав один набор значений. Железо даст другой, потому что реальные flip-flop захватывают одновременно. Эти двое разойдутся молча, и ты убьёшь день на поиск бага. Используй <= в тактируемых блоках.

Почему вообще два оператора

Дизайнеры Verilog могли бы выбрать одну семантику присваивания и держаться её. Не выбрали, потому что язык должен моделировать два разных поведения железа:

  • Комбинационная логика: сигналы распространяются непрерывно, зависимости важны, "что считает этот вентиль" имеет смысл.
  • Последовательностная логика: происходит фронт clock, каждый flip-flop захватывает одновременно, зависимости между входами flip-flop и выходами разделены.

Blocking - для первого. Non-blocking - для второго. Оператор выбирает семантику; симулятор делает остальное.

Что дальше

Теперь у тебя есть правила, чтобы писать любой процедурный блок правильно. Следующая глава поднимается от отдельных блоков к конструкциям управления потоком, которые в них живут: if/else, case и for-циклы. Правила blocking vs non-blocking продолжают применяться внутри всех них.

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

В чём разница между blocking и non-blocking присваиванием в Verilog?

Blocking (=) обновляет цель немедленно, до того как выполнится следующий оператор. Моделирует последовательное выполнение. Non-blocking (<=) планирует обновление на конец текущего временного шага: каждое RHS у non-blocking вычисляется с использованием старых значений всех сигналов, затем каждое LHS обновляется одним согласованным шагом. Non-blocking - то, как реально ведут себя flip-flop.

Когда использовать = vs <= в Verilog?

Правило: используй <= в тактируемых always @(posedge clk)-блоках и = в комбинационных always @(*)-блоках. Это одно правило предотвращает весь класс race conditions, которые порождают смешанные присваивания. Внутри testbench-initial-блоков = - обычный выбор; <= там встречается редко.

Почему в Verilog нужно non-blocking присваивание?

Потому что все flip-flop в железе захватывают свои входы одновременно на фронте clock. Если бы ты использовал = (blocking) в тактируемом коде, порядок операторов в файле менял бы, какой сигнал видит новое значение какого другого - race condition между симуляцией и реальным железом. <= соответствует поведению железа: сначала вычисляются все RHS, потом все обновления.

Что будет, если смешать = и <= в одном Verilog always-блоке?

Получишь race condition. Смесь даёт железо, чьё поведение зависит от внутренностей симулятора (порядка планирования событий), и хуже - симуляция может не совпадать с тем, что выдаёт синтез. Большинство lint-тулов помечают это как ошибку. Фикс - закоммититься на один или другой в зависимости от роли блока: non-blocking для тактируемых, blocking для комбинационных.

Coddy programming languages illustration

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

НАЧАТЬ