Визуальная электроника

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

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

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

Примеры:

Verilog Code:
  1. // регистр, содержащий семплированное значение входа strobe
  2. reg strobe_sampled;
  3.  
  4. // декларация сигнала strobe_negedge
  5. wire strobe_negedge;
  6. // непрерывное присваивание выражения сигналу strobe_negedge
  7. assign strobe_negedge = ~strobe & strobe_sampled;
  8.  
  9. // декларация и присваивание совмещенные в одном операторе
  10. wire strobe_posedge = strobe & ~strobe_sampled;

Неблокирующее присваивание

Неблокирующее присваивание обозначает, что ко входу регистра в левой части присваивания подключается выход комбинаторной схемы, описываемой в правой части выражения. Собственно момент записи определяется списком чувствительности в блоке always, обычно это фронт тактирующего сигнала. Следует знать, что все операторы неблокирующего присваивания внутри одного блока always выполняются одновременно, а условия, определяющие произойдут присваивания или нет, определяются заранее. К моменту присваивания, обычно это фронт тактирующего сигнала, все используемые в выражениях сигналы должны иметь установившиеся значения. В противном случае результат выполнения операции может быть непредсказуемым.

Пример:

Verilog Code:
  1. reg reg_A;
  2. reg reg_B;
  3. wire swap_en;
  4.  
  5. always @(posedge clk) begin
  6. if (swap_en) begin
  7. reg_A <= reg_B;
  8. reg_B <= reg_A;
  9. end
  10. 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

Verilog Code:
  1. input strobe;
  2. reg strobe_sampled;
  3.  
  4. reg[7:0] count;
  5.  
  6. always @(posedge clk) begin
  7. strobe_sampled <= strobe;
  8. if (strobe & ~strobe_sampled) begin
  9. // событие: положительный перепад на входе "strobe"
  10. count <= count + 1;
  11. end
  12. end

По фронту clk происходит запись текущего значения strobe в регистр strobe_sampled. Параллельно происходит проверка, а не единице ли равно текущее значение strobe и не ноль ли при этом значение strobe_sampled. Схема, синтезируемая из условия if использует выход регистра strobe_sampled. То есть, условие внутри if можно понимать как «strobe равно единице и предыдущее значение strobe равно нулю».

При этом не будет лишним повторить, что эта запись на самом деле не так проста, как кажется. Например, условие внутри if — это выход комбинаторной схемы, которая не связана с тактирующим сигналом и поэтому может быть описана извне:

Verilog Code:
  1. wire strobe_posedge = strobe & ~strobe_sampled;

Но это еще не все. Новичок скорее всего прочитает этот код примерно так: «если обнаружен положительный перепад сигнала strobe, взять содержимое count, увеличить его на 1 и записать обратно в count». В действительности же следует читать это как: «в регистр count записывается значение выражения, которое к моменту обнаружения положительного перепада сигнала strobe имеет установившееся значение count + 1». Вариант записи, иллюстрирующий такое прочтение:

Verilog Code:
  1. wire strobe_posedge = strobe & ~strobe_sampled;
  2. wire [7:0] count_incr = count + 1;
  3. always @(posedge clk) begin
  4. strobe_sampled <= strobe;
  5. if (strobe_posedge)
  6. count <= count_incr;
  7. end

В чем же разница? Казалось бы, как ни читай, суть от этого не меняется. Но понимание внутренней структуры происходящего необходимо для описания более сложных систем. Понимание того, как происходит синтез схемы, позволит избежать некоторых досадных ошибок. Вот простейший пример:

Verilog Code:
  1. always @(posedge clk) begin
  2. count <= count + 1;
  3. if (count == 10) count <= 0;
  4. end

Этот счетчик имеет период счета равный 11. Выражение count == 10 выполняется на один такт позже после того, как в регистр count было записано значение 10. Один из способов исправить положение — употребить в if то же выражение, что и в правой части присваивания:

Verilog Code:
  1. if (count + 1 == 10) count <= 0;

Иногда удобно выносить выражения типа count + 1 из блоков always, это позволяет уменьшить вероятность ошибок в случае их многократного использования.

Блокирующее присваивание

Блокирующее присваивание, заклеймленное некорыми как «медленное», в действительности во многих случаях синтезируется в совершенно ту же схему, что и неблокирующее. Так, например фрагменты:

Verilog Code:
  1. always @(posedge clk) begin
  2. x = x + 1;
  3. y = y + 1;
  4. end

и

Verilog Code:
  1. always @(posedge clk) begin
  2. x <= x + 1;
  3. y <= y + 1;
  4. end

дадут один и тот же результат. Оба выражения выполнятся одновременно, в обоих случаях «время выполнения» будет равно времени записи в соответствующий регистр значения на его входе. Пока выражения не зависят друг от друга, никакой разницы между блокирующими и неблокирующими присваиваниями нет.

В то же время, следующая запись являет собой что-то новое:

Verilog Code:
  1. always @(posedge clk) begin
  2. x = x + 1;
  3. y = x;
  4. end

Здесь x увеличится на 1, а y примет значение x + 1. Чтобы записать это выражение неблокирующими присваиваниями, потребовалась бы такая запись:

Verilog Code:
  1. always @(posedge clk) begin
  2. x <= x + 1;
  3. y <= x + 1;
  4. end

Цепочку блокирующих присваиваний можно рассматривать как одно большое выражение. Еще пример:

Verilog Code:
  1. y <= 3*((input_value >> 4) + center_offset

или

Verilog Code:
  1. y = input_value >> 4;
  2. y = y + center_offset;
  3. y = 3 * y;

Эти две записи эквивалентны. Но вторую запись нельзя понимать как последовательную цепочку вычислений. Это верно лишь в том смысле, что всё выражение действительно выстраивается в схему, в которой сначала отрезаются 4 младших разряда, результат и второй операнд идут на вход сумматора, а выход сумматора отдается умножителю на три. Так это представляется в электрической схеме и человек для удобства нарисует эту схему слева направо и читать он ее будет последовательно. Но в получившейся схеме все это выражение выполняется непрерывно, так же как и в предыдущей записи с неблокирующим присваиванием. Запись результата в регистр, как и следовало ожидать, происходит по фронту тактового импульса.

Заключительный пример, использующий оба вида присваиваний, заодно напоминающий о неродстве оператора for с одноименными операторами в алгоритмических языках программирования:

Verilog Code:
  1. // Эквивалентная запись:
  2. // history[3] <= history[2]; history[2] <= history[1]; history[1] <= history[0]; history[0] <= current;
  3. always @(posedge clk) begin
  4. for (i = 1; i < 4; i = i + 1) history[i] <= history[i-1];
  5.  
  6. history[0] <= current;
  7. end
  8.  
  9. // Эквивалентная запись:
  10. // avg <= (history[3]+history[2]+history[1]+history[0])/4;
  11. always @(posedge clk) begin
  12. sum = 0;
  13. for (i = 0; i < 4; i = i + 1) sum = sum + history[i];
  14.  
  15. avg = sum/4;
  16. end

Этот пример выдает скользящее среднее с запаздыванием на два такта.

Регистры и провода

Регистровый тип является логической сущностью Верилога и не всегда превращается в физический регистр при синтезе. Иногда регистровый тип нужен для того, чтобы обойти некоторые ограничения синтаксиса языка. Это можно просто помнить, а можно творчески использовать.

Пример. Шинный мультиплексор.

Verilog Code:
  1. wire [7:0] data_in = cpu_memr ? ram_data :
  2. cpu_inport? io_data :
  3. interrupt ? 8'hFF : 8'hZZ;

Тернарные выражения удобны для описания подобных конструкций. Однако следует проявлять осторожность, потому что в действительности то, что описано в этом выражении является приоритетным шифратором. Кроме приоритетности, побочным эффектом может являться неравное время установки результата в зависимости от входных значений. «Честный» же мультиплексор можно описать только с помощью оператора case. Но оператор case может быть только внутри always блока, а нужно, чтобы шина data_in имела установившееся значение к очередному фронту тактового сигнала, то есть присваивание должно быть асинхронным.

Выручит конструкция такого вида:

Verilog Code:
  1. reg [7:0] data_in;
  2.  
  3. always
  4. case ({cpu_memr,cpu_inport,interrupt})
  5. 3'b100: data_in <= ram_data;
  6. 3'b010: data_in <= io_data;
  7. 3'b001: data_in <= 8'hFF;
  8. default: data_in <= 8'hZZ;
  9. endcase

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

Выводы

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

В прилагаемом assignments.zip архиве лежит проект для Altera Quartus II, в котором содержатся все использованные выше примеры.

 

Автор: Viacheslav Slavinsky

Источник

 

Добавить комментарий