Menu

Continuous assignment в Verilog: оператор assign

Как работает assign - отношение, истинное всегда, что он может и не может приводить, и паттерны, в которых он блистает по сравнению с процедурным кодом.

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

assign описывает постоянную истину

Объявление wire создаёт сигнал. assign описывает что его приводит. Отношение непрерывное: что бы ни было справа, левая часть равна ему в каждый момент симулированного времени:

wire y;
assign y = a & b;

Две вещи, заложенные в этих двух строках:

  • y существует как wire в схеме.
  • y в любой момент равен побитовому AND a и b. Меняешь любой вход - y следует.

Нет clock и нет события, запускающего обновление. Симулятор видит, что a или b поменялись, помечает y "грязным" и переоценивает выражение. В железе это пара AND-вентилей: комбинационно, без состояния, мгновенно.

Неявная форма

Можно совместить объявление и присваивание в одной строке:

wire y = a & b;

То же, что две строки выше. Полезно для inline-wires, до которых никому снаружи этого scope нет дела. Многие style guides предпочитают неявную форму - она ставит объявление прямо рядом с уравнением.

Что assign может приводить

Цель assign должна быть net-типом - в чистом Verilog это wire (или один из более редких родственников вроде tri, wand, wor). Не может быть reg. Если случайно объявил цель как reg:

reg y;
assign y = a & b;   // ОШИБКА: нельзя приводить reg через assign

Компилятор скажет. Либо меняй цель на wire, либо переноси логику в always @(*), где reg - легальная цель.

Правой частью может быть всё, что вычисляется в значение: литералы, сигналы, parameters, выражения с операторами, вызовы функций. Можно смешивать разные ширины входов; применяются стандартные правила расширения.

Когда использовать assign vs always

Оба могут давать комбинационную логику. Выбор в основном про то, как читается код:

  • assign лучше, когда отношение - одно выражение. Adder'ы, простые mux через ?:, маски, parity-биты - всё, что пишется в одну строку.
  • always @(*) лучше, когда нужны процедурные операторы. Многоветочные case-операторы, вложенные if/else if, что-то, что выигрывает от именованных промежуточных reg. Это разбираем в Always Block.

Вот один и тот же 4-to-1 mux в обоих стилях:

Оба модуля синтезируются по сути в один и тот же мультиплексор. Версия с assign - одна строка кода; версия с always - шесть. Для четырёх случаев близко; для шестнадцати - case-блок явно читаемее.

Частые паттерны

Простая комбинационная логика

assign sum   = a + b;
assign carry = a[7] & b[7];
assign equal = (data == 8'hFF);

Одно выражение, один wire. Хлеб и масло assign.

2-to-1 mux

assign out = sel ? a : b;

Одно conditional выражение - синтезатор превращает в один 2-to-1 mux. Самый чистый способ написать "выбери между a и b".

Bit packing

assign status = {error, overflow, ready, busy, 4'b0};

Concatenation справа от assign - способ упаковать флаги в status-байт. Результат вычисляется и приводится непрерывно.

Tri-state выход

assign data_pin = output_enable ? data_out : 1'bz;

Когда output_enable high - приводим pin. Low - отпускаем в high-impedance. Канонический паттерн на pin'ах чипа, где несколько driver могут делить wire.

Параллельность: несколько assign - это не последовательность

Напоминание, которое не перестаёт быть актуальным: несколько assign-операторов в одном module все работают параллельно. Это не последовательность:

assign y = a & b;     // существует всегда
assign z = a | b;     // тоже существует всегда, независимо

Порядок в файле не имеет значения. Оба уравнения одновременно истинны. Синтезатор может разместить AND-вентиль слева от OR-вентиля или справа - неважно, оба вентиля работают непрерывно.

Если хочешь последовательно выглядящего поведения, тянись к always (и, скорее всего, к clock). Это другая глава.

Несколько driver: bus-паттерн

У wire может быть больше одного assign, направленного на него, но почти никогда этого не хочешь, кроме tri-state-шин. Два driver, дерущиеся за wire, дают неопределённое поведение:

assign y = a;
assign y = b;   // ПЛОХО - два driver, симулятор выбирает один или X-ит

Легитимный паттерн: каждый driver отпускает в z, когда неактивен, и в любой момент активен максимум один.

assign bus = device_a_active ? data_from_a : 1'bz;
assign bus = device_b_active ? data_from_b : 1'bz;

Это работает, потому что в любой момент максимум одна из двух тернарок даёт не-z значение. Реальное значение wire - то, которое не отпустил тот driver, что активен.

На внутренней логике - везде, кроме pin'ов чипа или общих on-chip шин - один driver на wire. Многодрайверные баги - кошмар при отладке.

Что assign не может

Кое-что, для чего assign - неправильный инструмент:

  • Хранилище. assign описывает комбинационные отношения; он не может создать flip-flop. Если значение нужно помнить между тактами - это блок always @(posedge clk).
  • Многошаговая процедурная логика. Нельзя писать if/else или case внутри assign. Самое близкое - цепочка ?:, которая становится уродливой после трёх веток.
  • Приведение регистров изнутри процедурного блока. Цели reg требуют процедурного присваивания, не assign.

Знание границ - это то, как ты понимаешь, когда переключаться на always.

Что дальше

Ты теперь видел всю структурную сторону Verilog: объявление модулей, инстанцирование их и подключение комбинационной логики через assign. Следующая глава переходит в процедурные блоки - конструкции initial и always, где начинают важничать время и порядок.

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

Что такое continuous assignment в Verilog?

assign target = expression; объявляет постоянное непрерывное отношение: target всегда равен expression. Когда любой сигнал в expression меняется, симулятор переоценивает правую часть и обновляет target. Никаких clock, никаких событий - отношение истинно в любой момент времени.

Что можно задать через assign в Verilog?

assign может приводить wire, но никогда reg. Цель должна быть net-типом. Если хочешь присваивать чему-то внутри always, объяви как reg. Компилятор отвергнет assign x = ..., если x - reg, и отвергнет x = ... внутри always, если x - wire.

Когда использовать assign, а когда always-блок?

Используй assign для простой комбинационной логики - одно выражение на входе, один сигнал на выходе, не нужен if/else. Используй always @(*), когда логике нужны процедурные операторы (case, цепочка if/else if, цикл for). Оба дают комбинационное железо; выбор - про читаемость.

Можно ли иметь несколько assign на один wire в Verilog?

Только если моделируешь tri-state-шину, где каждый driver отпускает wire в z, когда неактивен. Два assign, пытающихся приводить wire к определённым значениям одновременно, дают contention - симулятор может выбрать один, может X-нуть сигнал, зависит от тула. Для обычной комбинационной логики - один driver на wire.

Coddy programming languages illustration

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

НАЧАТЬ