Два оператора
Внутри 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 для комбинационных.