О видах присваиваний в Верилоге
Верилог имеет три вида оператора присваивания: непрерывное, блокирующее и неблокирующее. Если с непрерывным, или постоянным присваиванием все более-менее понятно, то разница между блокирующим и неблокирующим присваиваниями не столь отчетлива и во многих руководствах она остается за кадром. К сожалению, нередко встречаются утверждения о том, что блокирующие присваивания «выполняются последовательно». Некоторые же идут настолько далеко, что дают советы использовать неблокирующие присваивания тем, кто хочет, чтобы их код исполнялся побыстрее. Цель этой статьи — развеять туман и помочь начинающим составить представление о том, что же именно представляют из себя различные виды присваиваний в синтезируемом подмножестве Верилога.
Непрерывное присваивание
Примеры:
// регистр, содержащий семплированное значение входа strobe
reg strobe_sampled;
// декларация сигнала strobe_negedge
wire strobe_negedge;
// непрерывное присваивание выражения сигналу strobe_negedge
assign strobe_negedge = ~strobe & strobe_sampled;
// декларация и присваивание совмещенные в одном операторе
wire strobe_posedge = strobe & ~strobe_sampled;
Неблокирующее присваивание
Неблокирующее присваивание обозначает, что ко входу регистра в левой части присваивания подключается выход комбинаторной схемы, описываемой в правой части выражения. Собственно момент записи определяется списком чувствительности в блоке always, обычно это фронт тактирующего сигнала. Следует знать, что все операторы неблокирующего присваивания внутри одного блока always выполняются одновременно, а условия, определяющие произойдут присваивания или нет, определяются заранее. К моменту присваивания, обычно это фронт тактирующего сигнала, все используемые в выражениях сигналы должны иметь установившиеся значения. В противном случае результат выполнения операции может быть непредсказуемым.Пример
reg reg_A;Человеку непосвященному скорее всего покажется, что по фронту сигнала clk, если swap_en равен «1», регистры reg_A и reg_B примут значение, которое reg_B имел до свершения события. В действительности же эта запись соединяет выход регистра reg_A со входом reg_B, а выход reg_B со входом reg_A. Таким образом, если в момент положительного перепада clk сигнал swap_en установлен в «1», в каждый из регистров записывается предварительно установившееся на его входе значение. Для reg_A это значение reg_B, а для reg_B — это значение reg_A. Два регистра обменялись значениями одновременно!
reg reg_B;
wire swap_en;
always @(posedge clk) begin
if (swap_en) begin
reg_A <= reg_B;
reg_B <= reg_A;
end
end
Пример 2
input strobe;По фронту clk происходит запись текущего значения strobe в регистр strobe_sampled. Параллельно происходит проверка,
reg strobe_sampled;
reg[7:0] count;
always @(posedge clk) begin
strobe_sampled <= strobe;
if (strobe & ~strobe_sampled) begin
// событие: положительный перепад на входе "strobe"
count <= count + 1;
end
end
а не единице ли равно текущее значение strobe и не ноль ли при этом значение strobe_sampled. Схема, синтезируемая из условия if использует выход регистра strobe_sampled. То есть, условие внутри if можно понимать как «strobe равно единице и предыдущее значение strobe равно нулю».
При этом не будет лишним повторить, что эта запись на самом деле не так проста, как кажется. Например, условие внутри if — это выход комбинаторной схемы, которая не связана с тактирующим сигналом и поэтому может быть описана извне:
wire strobe_posedge = strobe & ~strobe_sampled;Но это еще не все. Новичок скорее всего прочитает этот код примерно так: «если обнаружен положительный перепад сигнала strobe, взять содержимое count, увеличить его на 1 и записать обратно в count». В действительности же следует читать это как: «в регистр count записывается значение выражения, которое к моменту обнаружения положительного перепада сигнала strobe имеет установившееся значение count + 1». Вариант записи, иллюстрирующий такое прочтение:
wire strobe_posedge = strobe & ~strobe_sampled;
wire [7:0] count_incr = count + 1;
always @(posedge clk) begin
strobe_sampled <= strobe;
if (strobe_posedge)
count <= count_incr;
end
always @(posedge clk) beginЭтот счетчик имеет период счета равный 11. Выражение count == 10 выполняется на один такт позже после того, как в регистр count было записано значение 10. Один из способов исправить положение — употребить в if то же выражение, что и в правой части присваивания:
count <= count + 1;
if (count == 10) count <= 0;
end
if (count + 1 == 10) count <= 0;Иногда удобно выносить выражения типа count + 1 из блоков always, это позволяет уменьшить вероятность ошибок в случае их многократного использования.
Блокирующее присваивание
Блокирующее присваивание, заклеймленное некорыми как «медленное», в действительности во многих случаях синтезируется в совершенно ту же схему, что и неблокирующее. Так, например фрагменты:always @(posedge clk) beginи
x = x + 1;
y = y + 1;
end
always @(posedge clk) beginдадут один и тот же результат. Оба выражения выполнятся одновременно, в обоих случаях «время выполнения» будет равно времени записи в соответствующий регистр значения на его входе. Пока выражения не зависят друг от друга, никакой разницы между блокирующими и неблокирующими присваиваниями нет.
x <= x + 1;
y <= y + 1;
end
В то же время, следующая запись являет собой что-то новое:
always @(posedge clk) beginЗдесь x увеличится на 1, а y примет значение x + 1. Чтобы записать это выражение неблокирующими присваиваниями, потребовалась бы такая запись:
x = x + 1;
y = x;
end
always @(posedge clk) beginЦепочку блокирующих присваиваний можно рассматривать как одно большое выражение. Еще пример:
x <= x + 1;
y <= x + 1;
end
y <= 3*((input_value >> 4) + center_offset);или:
y = input_value >> 4;Эти две записи эквивалентны. Но вторую запись нельзя понимать как последовательную цепочку вычислений. Это верно лишь в том смысле, что всё выражение действительно выстраивается в схему, в которой сначала отрезаются 4 младших разряда, результат и второй операнд идут на вход сумматора, а выход сумматора отдается умножителю на три. Так это представляется в электрической схеме и человек для удобства нарисует эту схему слева направо и читать он ее будет последовательно. Но в получившейся схеме все это выражение выполняется непрерывно, так же как и в предыдущей записи с неблокирующим присваиванием. Запись результата в регистр, как и следовало ожидать, происходит по фронту тактового импульса.
y = y + center_offset;
y = 3 * y;
Заключительный пример, использующий оба вида присваиваний, заодно напоминающий о неродстве оператора for с одноименными операторами в алгоритмических языках программирования:
// Эквивалентная запись:Этот пример выдает скользящее среднее с запаздыванием на два такта.
// history[3] <= history[2]; history[2] <= history[1]; history[1] <= history[0]; history[0] <= current;
always @(posedge clk) begin
for (i = 1; i < 4; i = i + 1) history[i] <= history[i-1];
history[0] <= current;
end
// Эквивалентная запись:
// avg <= (history[3]+history[2]+history[1]+history[0])/4;
always @(posedge clk) begin
sum = 0;
for (i = 0; i < 4; i = i + 1) sum = sum + history[i];
avg = sum/4;
end
Регистры и провода
Регистровый тип является логической сущностью Верилога и не всегда превращается в физический регистр при синтезе. Иногда регистровый тип нужен для того, чтобы обойти некоторые ограничения синтаксиса языка. Это можно просто помнить, а можно творчески использовать.Пример. Шинный мультиплексор.
wire [7:0] data_in = cpu_memr ? ram_data :Тернарные выражения удобны для описания подобных конструкций. Однако следует проявлять осторожность, потому что в действительности то, что описано в этом выражении является приоритетным шифратором. Кроме приоритетности, побочным эффектом может являться неравное время установки результата в зависимости от входных значений. «Честный» же мультиплексор можно описать только с помощью оператора case. Но оператор case может быть только внутри always блока, а нужно, чтобы шина data_in имела установившееся значение к очередному фронту тактового сигнала, то есть присваивание должно быть асинхронным.
cpu_inport? io_data :
interrupt ? 8'hFF : 8'hZZ;
Выручит конструкция такого вида:
reg [7:0] data_in;Несмотря на то, что переменная data_in формально является регистром, она будет синтезирована как обычная шина типа wire, а присоединена эта шина будет к выходу описанного оператором case мультиплексора. Переменная регистрового типа превращается в регистр только тогда, когда её присваивание происходит по перепаду тактового сигнала. В противном же случае она фактически является эквивалентом переменной типа wire.
always
case ({cpu_memr,cpu_inport,interrupt})
3'b100: data_in <= ram_data;
3'b010: data_in <= io_data;
3'b001: data_in <= 8'hFF;
default: data_in <= 8'hZZ;
endcase
Выводы
Мы рассмотрели на несложных примерах три вида присваиваний, их схожесть и отличия. Если после прочтения этой статьи понимания разницы между блокирующим и неблокирующим присваиваниями не появилось, стоит попробовать запустить симуляцию рассмотренных в статье примеров, проанализировать результаты симуляции и, если возможно, результат синтеза схем. В прилагаемом архиве лежит проект для Altera Quartus II, в котором содержатся все использованные выше примеры.