Введение в Verilog. Пятый урок, Синхронная логика.

Verilog HDL

На предыдущих уроках мы уже познакомились с типами источников сигналов, узнали как установить экземпляры разных модулей в свой модуль и как соединить их проводами. Так же мы уже рассмотрели разные арифметические и логические выражения в поведенческом коде. Теперь нам осталось познакомиться с синхронной логикой и триггерами.

Существует несколько видов триггеров. Наиболее важные для практического применения – это D-триггера. Вот графическое изображение D-триггера:

D-триггер (flipflop)

D-Триггер (flipflop) – это специальный логический элемент, способный запоминать. Такой триггер запоминает логическое значение сигнала входа D, когда на втором входе C (обозначен треугольничком)  появляется фронт сигнала. Фронт сигнала - это момент, когда входная линия переходит из состояния ноль в единицу. Один такой триггер запоминает один бит информации. Текущее значение записанное в триггер - на его выходе Q. Еще триггер может иметь сигналы ассинхронного сброса clrn или установки prn. По сигналу clrn = 0 триггер переходит в состояние ноль не зависимо от других входных сигналов D или C. Кое-что про триггера можно почитать в Википедии.

Все сложные цифровые схемы используют и комбинаторную логику и триггера. Вот типичный пример некой цифровой схемы:

цифровая схема на триггерах

Логические функции func1 и func2 что-то вычисляют и по каждому фронту тактовой частоты результат вычисления запоминается в регистрах. Синхронно с каждым импульсом тактовой частоты схема переходит из одного устойчивого состояния в другое. Состояние всей схемы определяется текущими значениями в триггерах. В синхронной логике (в отличии от комбинаторной логики) обратные связи используются очень часто – то есть выход триггера может спокойно подаваться на его вход прямо или через другую логику.  

Язык Verilog позволяет легко описать синхронные процессы.

Описание синхронной логики в поведенческом коде Verilog очень похоже на описание комбинаторной логики с одним важным отличием – в списке чувствительности always блока теперь будут не сами сигналы, а фронт тактовой частоты clock. Только в этом случае регистры, которым идет присвоение, будут реализованы в виде D-триггеров (flipflops).

Давайте посмотрим простой пример. Вот графическое представление восьмибитного регистра, запоминающего входные данные по фронту тактовой частоты:

восьмибитный регистр
Его представление в Verilog может быть такое:


module DFF8 (clock, data, q);
input clock;
input [7:0] data;
output [7:0] q;
reg [7:0] q;
always @(posedge clock) begin
    q <= data;
end
endmodule

Здесь блок always чувствителен к фронту тактовой частоты: posedge clock. Еще, вместо posedge – положительного фронта тактовой частоты можно использовать negedge – отрицательный фронт. При использовании negedge запоминание в регистр происходит по перепаду сигнала clock с «1» в «0». Сам факт запоминания входных данных data в регистре q здесь описан оператором "<=".  Это так называемое «неблокирующее присвоение».

Понятно, что на вход in такого регистра может быть подано, например, значение выражения какой нибудь комбинаторной функции, вычисленной в другом always блоке.

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

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

Вот пример реализации синхронного сброса регистра с активным положительным уровнем (active-high):

Схема с синхронным сбросом
Вот так эту схему можно описать на языке Verilog:


module DFFSR (reset, clock, data, q);
input reset;
input clock;
input [7:0] data;
output [7:0] q;

reg [7:0] q;
always @(posedge clock) begin
  if (reset) begin
    q <= 0;
  end else begin
    q <= data;
  end
end

endmodule


Фактически, здесь при использовании синхронного сброса перед входом запоминающего регистра стоит мультиплексор. Если сигнал сброса reset активен ("единица"), то на вход регистра подается ноль. Иначе – загружаются другие полезные данные. Запись в регистр происходит синхронно с фронтом тактовой частоты clock.

Ассинхронный сброс c активным сигналом единица описывается следующим образом. Вот схема:

Ассинхронный сброс регистра
А вот представление на Verilog:


module DFFAR (reset, clock, data, q);
input reset;
input clock;
input [7:0] data;
output [7:0] q;
reg [7:0] q;
always @(posedge clock or posedge reset) begin
  if (reset) begin
    q <= 0;
  end else begin
    q <= data;
  end
end
 

endmodule


Обратите внимание, что сигнал reset теперь попал в список чувствительности для always блока.

Вообще-то для ассинхронных сбросов желательно всегда придерживаться вот такого стиля описания синхронной логики, как в приведенном выше примере. Сперва пишите "if(reset)" и все присвоения связанные с начальной инициализацией регистров. Потом пишите "else" и всю остальную логику. Дело в том, что синтезатор пытается в нашем коде выделить знакомые ему конструкции. Если написать иначе, хотя и логически правильно, то остается риск, что синтезатор чего-то не поймет и сгенерирует схему иначе, не используя все возможности триггера, не используя его ассинхронный сброс. В результате схема может стать больше (для ее реализации нужно больше логических элементов) и "медленнее" (работает стабильно на меньшей частоте).

Давайте рассмотрим еще один полезный практический пример. Предположим, что нам нужен счетчик по модулю 6, но чтобы исходное значение счетчика можно было бы загружать. Вот код описывающий такой счетчик:


module mod_counter(
    input wire reset,
    input wire clock,
    input wire [3:0]in,
    input wire load,
    output reg [3:0]cnt
    );

parameter MODULE = 6;

always @(posedge clock or posedge reset)
begin
  if
(reset)
    cnt <= 4'b0000;
  else
  begin
  if
(load)
    cnt <= in;
  else
    if
(cnt+1==MODULE)
       cnt <= 4'b0000;
    else
       cnt <= cnt + 1'b1;
  end
end

endmodule


В этом модуле у нас есть ассинхронный сброс счетчика, синхронная загрузка счетчика по условию load и синхронное увеличение значения счетчика до модуля. Эквивалентная схема такого модуля будет вот такая (нажмите чтобы увеличить):

Схема счетчика по модулю

Здесь видно два мультиплексора, сумматор, компаратор и собственно регистр.
Оба модуля, и написаный на Verilog и выполненный в виде схемы,  работают абсолютно одинаково:

Симуляция счетчика по модулю

На диаграмме симуляции видно, что если нет сигнала reset, то по каждому фронту тактовой частоты значение счетчика растет до модуля. Потом счет начинается сначала. Если появляется сигнал загрузки load, то значение счетчика перезагружается новым входным значением.

Теперь давайте посмотрим внимательнее на операторы присвоения языка Verilog. В случае синхронной логики присвоение обычно обозначает запись в регистр или триггер синхронно с фронтом тактовой частоты. Однако с точки зрения языка программирования Verilog мы опять подошли к этим двум понятиям: блокирующее и неблокирующее присвоение.

Блокирующее присвоение (с помощью оператора "=") называется так потому, что вычисления производятся строго в порядке, описанном в always блоке. Второе выражение вычисляется только после первого и результат второго выражения может зависить от результата первого.

Неблокирующее присвоение в always блоке (используется оператор "<=") обозначает факт одновременного запоминания вычисленных значений в соответствующих регистрах. Выражения в always блоке выполняются не последовательно, а одновременно. И присвоение,  тоже произойдет одновременно.

Обычно принято применять блокирующие присвоения при описании комбинаторной логики, а неблокирующие для синхронной логики. Так проще всего представлять себе описываемые алгоритмы (хотя оба типа присвоений могут соседствовать в одном always блоке).

Давайте попробуем поэкспериментировать с блокирующими и неблокирующими присвоениями.
Рассмотрим вот такой пример:


module test(
  input wire clock,
  input wire [3:0]in,
  output reg [3:0]x,
  output reg [3:0]y,
  output reg [3:0]z );

always @(posedge clock)
begin
  x <= in + 1;
  y <= x + 1;
  z <= y + 1;
end

endmodule

Из этого кода синтезатор сделает три физических четырехбитных регистра на триггерах и три сумматора. Эквивалентная схема к такому коду будет вот такая:

Последовательные регистры

Попробуем просимулировать такой модуль. Предположим исходное состояние триггеров неизвестно (значение X). Пусть, например,  входное значение шины in равно 3 и модуль тактируется частотой clock.

Симуляция модуля на Verilog

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

Вместе с первым фронтом тактовой частоты clock в регистр x будет записано значение суммы in+1 и оно равно четырем. В этот же момент времени в регистр y должна быть записана сумма x+1, но симулятор еще не знает текущего значения регистра x. Оно еще не определено. Только второй импульс тактовой частоты запишет в регистр y число 5. Присвоение в регистр z так же происходит синхронно с остальными присвоениями. Но только на третьем такте в регистр z запишется число 6, так как только к этому времени симулятору будет известно значение регистра y. В реальной схеме все работает точно так же. Сигнал на выходе z получается задержанным на 2 такта относительно выхода x.

Попробуем теперь изменить наш модуль. Сейчас мы будем использовать блокирующее присвоение:


module test(
  input wire clock,
  input wire [3:0]in,
  output reg [3:0]x,
  output reg [3:0]y,
  output reg [3:0]z );

always @(posedge clock)
begin
  x = in + 1;
  y = x + 1;
  z = y + 1;
end

endmodule

Поведение модуля стало принципиально другим! И это неудивительно, ведь этот код описывает теперь уже другую цифровую схему. Регистры теперь соединены не последовательно один за другим, а как бы параллельно, только загружаемые значения разные:

Регистры и сумматоры

Результат симуляции:

Симуляция модуля на Verilog

Поскольку мы использовали блокирующие присвоения, то это обозначает, что следующее выражение учитывает результат предыдущего (в порядке написания).  Фактически в этом случае наша запись подразумевает следущее:


always @(posedge clock)
begin
  x = in + 1;
  y = in + 2; // y = x+1 = (in+1)+1
  z = in + 3; // z = y+1 = ((in+1)+1)+1
end

Мы так же получим три регистра, только в них будет записаны суммы одновременно уже в первом такте.

Давайте теперь попробуем изменить порядок строк наших выражений в always блоке. Неблокирующие присвоения даже не будем пробовать -  результат не изменится.  А вот с блокирующими присвоениями не так. Тут важна очередность выражений. Поменяем местами две строки, вот так:


always @(posedge clock)

begin
x = in + 1;
z = y + 1;
y = x + 1;
end


Получаем вот такой результат:

Симуляция модуля Verilog

В регистр z будет помещено результирующая сумма на такт позже, чем в регистр y. И это правильно! По первому фронту тактовой частоты может быть вычисленно точно только x и y, но нельзя вычислить z. К моменту вычисления z по первому фронту значение y неопределено. А вот на втором фронте сигнала clock уже z будет посчитан.

Вот так. Блокирующие и неблокирующие присвоения действуют по разному.

Сейчас можно подвести некоторый итог: неблокирующие присвоения "<=" в языке Verilog позволяют описать многочисленные одновременные присвоения регистрам и учесть все задержки распространения данных внутри цифровой схемы.

Пожалуй на этом  можно и закончить наш краткий курс по языку Verilog. Конечно за пять уроков невозможно рассказать все, но я думаю, что основные моменты я описал.

Надеюсь было не очень скучно. Smile

 

 

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