Верилог

О видах присваиваний в Верилоге

Верилог имеет три вида оператора присваивания: непрерывное, блокирующее и неблокирующее. Если с непрерывным, или постоянным присваиванием все более-менее понятно, то разница между блокирующим и неблокирующим присваиваниями не столь отчетлива и во многих руководствах она остается за кадром. К сожалению, нередко встречаются утверждения о том, что блокирующие присваивания «выполняются последовательно». Некоторые же идут настолько далеко, что дают советы использовать неблокирующие присваивания тем, кто хочет, чтобы их код исполнялся побыстрее. Цель этой статьи — развеять туман и помочь начинающим составить представление о том, что же именно представляют из себя различные виды присваиваний в синтезируемом подмножестве Верилога.

Непрерывное присваивание

То, что в Верилоге именуется «постоянным», или «непрерывным» присваиванием на самом деле всегда развертывается в комбинаторную схему. Непрерывное присваивание может встречаться в операторе assign, либо же прямо в декларации сигнала wire. Левой частью всегда является сигнал, правой — выражение, использующее любые другие сигналы. Значения регистровых переменных тоже являются сигналами.

Примеры:
// регистр, содержащий семплированное значение входа 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;
reg reg_B;
wire swap_en;

always @(posedge clk) begin
    if (swap_en) begin
        reg_A <= reg_B;
        reg_B <= reg_A;
    end
end
Человеку непосвященному скорее всего покажется, что по фронту сигнала 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. Два регистра обменялись значениями одновременно!

Пример 2
input strobe;
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

По фронту clk происходит запись текущего значения strobe в регистр strobe_sampled. Параллельно происходит проверка,
а не единице ли равно текущее значение 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
    count <= count + 1;
    if (count == 10) count <= 0;
end
Этот счетчик имеет период счета равный 11. Выражение count == 10 выполняется на один такт позже после того, как в регистр count было записано значение 10. Один из способов исправить положение — употребить в if то же выражение, что и в правой части присваивания:
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 = x + 1;
    y = x;
end
Здесь x увеличится на 1, а y примет значение x + 1. Чтобы записать это выражение неблокирующими присваиваниями, потребовалась бы такая запись:
always @(posedge clk) begin
    x <= x + 1;
    y <= x + 1;
end
Цепочку блокирующих присваиваний можно рассматривать как одно большое выражение. Еще пример:
y <= 3*((input_value >> 4) + center_offset);
или:
y = input_value >> 4;
y = y + center_offset;
y = 3 * y;
Эти две записи эквивалентны. Но вторую запись нельзя понимать как последовательную цепочку вычислений. Это верно лишь в том смысле, что всё выражение действительно выстраивается в схему, в которой сначала отрезаются 4 младших разряда, результат и второй операнд идут на вход сумматора, а выход сумматора отдается умножителю на три. Так это представляется в электрической схеме и человек для удобства нарисует эту схему слева направо и читать он ее будет последовательно. Но в получившейся схеме все это выражение выполняется непрерывно, так же как и в предыдущей записи с неблокирующим присваиванием. Запись результата в регистр, как и следовало ожидать, происходит по фронту тактового импульса.

Заключительный пример, использующий оба вида присваиваний, заодно напоминающий о неродстве оператора 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 :
                                cpu_inport? io_data :
                                interrupt ? 8'hFF : 8'hZZ;
Тернарные выражения удобны для описания подобных конструкций. Однако следует проявлять осторожность, потому что в действительности то, что описано в этом выражении является приоритетным шифратором. Кроме приоритетности, побочным эффектом может являться неравное время установки результата в зависимости от входных значений. «Честный» же мультиплексор можно описать только с помощью оператора case. Но оператор case может быть только внутри always блока, а нужно, чтобы шина data_in имела установившееся значение к очередному фронту тактового сигнала, то есть присваивание должно быть асинхронным.

Выручит конструкция такого вида:
reg [7:0] data_in;

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
Несмотря на то, что переменная data_in формально является регистром, она будет синтезирована как обычная шина типа wire, а присоединена эта шина будет к выходу описанного оператором case мультиплексора. Переменная регистрового типа превращается в регистр только тогда, когда её присваивание происходит по перепаду тактового сигнала. В противном же случае она фактически является эквивалентом переменной типа wire.

Выводы

Мы рассмотрели на несложных примерах три вида присваиваний, их схожесть и отличия. Если после прочтения этой статьи понимания разницы между блокирующим и неблокирующим присваиваниями не появилось, стоит попробовать запустить симуляцию рассмотренных в статье примеров, проанализировать результаты симуляции и, если возможно, результат синтеза схем. В прилагаемом архиве лежит проект для Altera Quartus II, в котором содержатся все использованные выше примеры.



—-—
Адрес этой статьи: https://caglrc.cc/verilog/assignments
Просьба указывать ссылку на первоисточник.
Copyright © 2009 Viacheslav Slavinsky  svofski on gmail
Updated: Fri Feb 27 00:01:57 UTC 2009