Проектирование синхронных схем. Быстрый старт с Verilog HDL

    На просторах рунета можно найти достаточно много статей с введением в Verilog HDL. Все они описывают синтаксис и семантику языка, но, к сожалению, не раскрывают основных парадигм, используемых при проектировании цифровых схем. Представьте себе, что вам объясняют синтаксис языка Java, но не рассказывают ничего про объектно-ориентированное проектирование. Если вы знакомы с ООП, то такого введения будет достаточно, но если вы знаете только Си, то писать скорей всего будете “по-старому”, создавая огромные классы со сложными методами.

    Примерно так происходит с программистами, изучающими цифровую схемотехнику и языки описания аппаратуры. Быстро разобравшись с несложным синтаксисом языка, они начинают описывать конструкции, безумные с точки зрения хардверного инженера. Среди моих студентов встречались люди, написавшие “сортировку пузырьком” за такт, сумасшедшие асинхронные схемы, которые работали по-разному при каждом запуске и разной погоде за окном, огромные комбинационные делители, уводившие place&route в глубокую многочасовую задумчивость.

    Для тех, у которых нет времени прочитать учебник для начинающих, но есть желание или
    необходимость спроектировать несколько простых схем я решил написать это небольшое введение об основной современной парадигме проектирования цифровых схем – синхронных схемах. И об одном из языков, используемых для их описания.
    Статья рассчитана на новичков. Для понимания текста потребуется минимальный набор знаний – понимание работы синхронного D-триггера и вентилей.

    Синхронные схемы


    Синхронные цифровые схемы состоят из комбинационных вентилей(gates), цепей (nets) и триггеров (flip-flops). В синхронной схеме есть единственный сигнал синхронизации, который управляет всеми элементами памяти (триггерами).

    Формально, синхронную схему можно определить следующим образом:
    Синхронная схема — это цифровая схема C состоящая из вентилей, триггеров и цепей распространения сигналов, которая удовлетворяет следующим условиям:
    • В схеме существует единственная цепь clk, по которой распространяется сигнал синхронизации (тактовый сигнал, clock signal)
    • Сигнал clk управляется входным портом схемы.
    • Множество портов, управляемых сигналом clk, эквивалентно множеству входов синхронизации триггеров
    • Определим схему C’ следующим образом: Схема C’ получается из схемы C путем: (1) удаления цепи clk, (2) удаления входного порта, управляющего сигналом clk, (3) заменой всех триггеров на выходной порт (вместо входа D) и входной порт (вместо выхода Q). Полученная схема C’ должна быть комбинационной

    Пример синхронной схемы показан на рисунке:


    А вот несколько примеров схем, не являющихся синхронными:


    Практически все существующие цифровые схемы являются синхронными, либо состоят из нескольких синхронных схем, взаимодействующих через асинхронные каналы. Причина популярности синхронных схем — в простоте анализа времени распространения сигналов. Временной анализ (англ. Timing analysis) — тема для отдельной статьи, но кратко коснуться этого вопроса все-таки надо.

    Рассмотрим следующую схему, реализующую функцию R = A+B+1:


    Регистры A, B и R сохраняют значения на входах D по переднему фронту сигнала синхронизации (clk), т.е. в тот момент времени, когда значение clk изменяется из 0 в 1.

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

    Предположим, что сначала во всех регистрах был записан 0. А на входы in0, in1 сначала подаются значения 1 и 4, а потом 2 и 1. Тогда временная диаграмма для нашей схемы может выглядеть следующим образом:



    По первому фронту clk значения 1 и 4 будут записаны в регистры A и B. После того как сигнал распространится через сумматоры, значение результата 1 + 4 + 1 = 6 появится на проводе t1. Затем, по второму фронту clk результат будет записан в регистр R, а новые входные значения в регистры A и B.

    Теперь представим, что период сигнала clk уменьшился в два раза. Тогда второй фронт сигнала clk появится на регистре R до того, как на t1 появятся правильные данные. Т.е. схема будет работать неверно!

    Отсюда следует основное правило корректности работы синхронной схемы:

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

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

    Синхронная схема работает на частоте, определяемой критическим путем в схеме.

    Представьте, что в схеме тысячи комбинационных путей с задержкой в 1 наносекунду. И один путь с задержкой в 2 наносекунды. Из-за этого единственного пути схема должна тактироватся на частоте в 500 МГц, хотя могла бы работать на гигагерце. Поэтому, при проектировании синхронных схем длинные комбинационные цепочки разбивают регистрами на конвейерные стадии. К примеру, в процессоре AMD Bulldozer средняя длина комбинационного пути – 12-14 FO4 эквивалентных вентилей (задержка, эквивалентная инвертору единичного размера, нагруженному 4-мя инверторами).

    Несмотря на этот недостаток, синхронные схемы стали очень популярны. Синхронные схемы без труда поддаются автоматическому временному анализу, т.е. частота, на которой схема может корректно работать, определяется программой (временным анализатором) автоматически. Когда разработчик может отстраниться от этих деталей, синхронную схему можно специфицировать набором пересылок между регистрами. Такой подход к описанию схем – Register Transfer Logic (RTL) стал мэйнстримом в описании логики работы цифровых схем. К примеру, схему из нашего примера можно описать следующими пересылками:

    A = in0
    B = in1
    R = A+B+1


    На каждом такте в регистр А записывается in0, в регистр B записывается in1, а в регистр R значение A + B + 1. Идея описывать схемы на RTL в виде текста лежит в основе языков описания аппаратуры: Verilog HLD и VHDL. Пришло время познакомиться с одним из них поближе.

    Описание синхронных схем на Verilog HDL


    Модули

    Программа на Verilog, она же описание схемы, состоит из модулей (module), точнее из экземпляров модулей (module instances). Модуль можно представить как “черный ящик” с торчащими из него проводами — портами (ports). Порты бывают трех типов: входные (input), выходные (output) и двунаправленные (inout). В большинстве случаев используются первые два типа портов. Двунаправленные порты нужны для моделирования двунаправленных шин, на базе выходов с тремя состояниями и открытым стоком. Их мы рассматривать не будем.

    Список портов описывается в заголовке модуля. К примеру, рассмотрим этот пустой модуль:

    module blackbox // module - ключевое слово, blackbox - имя модуля
    (
    input a, b, c // входные порты a,b,c
    input [7:0] bus, // входной порт bus - 8-разрядная шина
    output [7:0] bus_out // выходной порт bus_out, также 8-разрядный
    );
    // Здесь добавляется тело модуля

    endmodule // endmodule - конец описания модуля, ключевое слово


    В теле модуля описывается его функциональность. Этот модуль пустой, его порты никуда не подключены. Перед тем как перейти к описанию функциональности модулей, познакомимся с основными типами данных в Verilog.

    Типы данных

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

    В Verilog сигнал может принимать 4 значения:
    • 0 – логический ноль, или ложь
    • 1 – логическая единица, или истина
    • x – неопределенное значение. К примеру, значение регистра в начальный момент симуляции (до сброса или первой записи в регистр)
    • z – состояние с высоким импедансом. Чаще всего сигнал принимает это значение, если он никуда не подключен – “обрыв провода”

    В большинстве модулей на Verilog используются 2 основных типа данных – wire и reg. Из названия может показаться, что wire моделирует провод, а reg – регистр, но, как будет показано далее, это не совсем так. Наличие двух типов это скорее баг в дизайне языка, в SystemVerilog – современной версии Verilog, есть универсальный тип logic, который может использоваться во всех случаях.

    Рассмотрим каждый из типов по отдельности.

    Wires

    Тип wire служит для моделирования сигналов, которые не могут “хранить” состояние. К примеру, значение на выходе комбинационной схемы полностью определяется значениями на входах. Если значения на входе меняются, меняется и значение на выходе, т.е. состояние не хранится. Тип wire используется вместе с операцией непрерывного присваивания — assign. При непрерывном присваивании, всякий раз когда меняется значение переменных в правой части присваивания, обновляется значение переменной в левой части. К примеру, простую комбинационную схему можно описать следующим образом:
    module comb
    (
    input wire [7:0] a,b,
    output [7:0] res // тип wire устанавливается по-умолчанию, но можно написать output wire [7:0] res
    );

    assign res = a+b+1;
    endmodule




    Сигналы можно объявлять и внутри тела модуля:

    module comb
    (
    input wire [7:0] a,b,
    output [7:0] res
    );

    wire [3:0] x,y;
    assign x = a[3:0] + b[3:0]; // складываются 4 младших разряда на шинах а и b
    assign y = a[7:4] + b[7:4]; // складываются 4 старших разряда на шинах а и b
    assign res = x+y;
    endmodule




    Regs

    Тип reg может хранить значение и используется в процедурных блоках. Процедурный блок в Verilog – процедура, срабатывающая по определенному событию. К примеру, этим событием может быть фронт тактового сигнала или начало симуляции. В процедурных блоках могут использоваться Си-подобные управляющие конструкции:
    • if… else..
    • for
    • do… while..
    • case (аналог switch)


    Процедурные блоки могут моделировать как синхронные схемы (схемы с памятью), так и комбинационные схемы. К примеру, рассмотрим описание схемы двоичного счетчика:

    module counter
    (
    input clk, reset,
    output [7:0] data
    );
    reg [7:0] counter;

    assign data = counter;

    always @(posedge clk) // процедурный блок срабатывает по положительному фронту clk
    begin
    if (reset)
    counter = 0;
    else
    counter = counter + 1;
    end

    endmodule




    Строка always @(posedge clk) называется списком чувствительности. Она определяет события, по которым выполняется процедурный блок. Данный блок выполняется по каждому положительному фронту (positive edge) сигнала синхронизации. Таким образом, блок моделирует логику работы счетчика, сигнал counter будет просинтезирован как регистр.

    Процедурные блоки могут моделировать и комбинационные схемы. К примеру, следующий код просинтезируется в комбинационный сумматор:
    wire [7:0] a,b;
    reg [7:0] res;

    always @*
    begin
    res = a+b;
    end

    Здесь список чувствительности always @* означает, что процедурный блок выполняется каждый раз, когда изменяются значения сигналов в правой части операций присваивания. В данном случае процедурный блок срабатывает каждый раз, когда изменяются сигналы a и b. Эквивалентном этой строчке, будет следующий список чувствительности:

    always @(a or b) // блок срабатывает каждый раз, когда меняется a или b


    При описании комбинационной схемы с помощью процедурного блока always необходимо помнить, что значения всех изменяемых в блоке сигналов, должны полностью определятся сигналами в списке чувствительности. В противном случае синтезатору придется вставить в схему элементы памяти — защелки (latches).

    К примеру, рассмотрим следующий процедурный блок:

    wire a,b;
    reg res;

    always @*
    begin
    if(a & b) res = 1;
    else if (!a & b) res = 0;
    end


    В процедурном блоке не описано, что будет в случае если b = 0. Если b = 0 значение res не должно меняться, не зависимо от значения a. Поэтому будет просинтезирована следующая схема:



    Как правило, появление защелок в схеме означает ошибку в коде.

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

    Помимо блокирующих присваиваний, в Verilog есть еще один тип присваивания, используемый в процедурных блоках — неблокирующее присваивание, обозначаемое оператором "<=". Неблокирующее присваивание выполняется не сразу, в том месте где объявлено, а откладывается до выхода из процедурного блока. Рассмотрим пример:

    reg a,b;
    always @(posedge clk)
    begin
    a <= b;
    b <= a;
    end

    В этом примере на каждом такте сигналы a и b будут обмениватся значениями. Синтезированная схема будет выглядеть следующим образом:



    Неблокирующие присваивания обычно используют при описании логики работы регистров. Блокирующие присваивания чаще используются для описания комбинационных схем.

    Заключение


    На этом небольшое введение в Verilog заканчивается. Надеюсь, кому-то оно было полезно.

    Тем кто хочет познакомиться с Verilog и проектированием цифровых схем немного глубже, могу порекомендовать эти две книжки для начинающих:
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 20

      0
      Большое спасибо за статью! Хотелось бы больше узнать о подводных камнях в реализации более-менее сложных стейт машин.

      Еще такой вопрос: есть некая шина данных и её клок. Насколько я понимаю, клокать данные нужно не синхронно с их изменением, а в середине data eye. Этого можно добиться несколькими вариантами. Например на нечетных тактах выкладывать данные на шину, а на четных дергать клок. Вроде как еще есть вариант с DCM, а именно брать два синхросигнала, смещенных на 90° и по запаздывающему синхронизировать данные.

      Хотел бы услышать ваши коментарии по этому поводу. Не совсем понимаю, как надо делать. В чем грабли и т. д.

      Спасибо.
        +1
        Все сильно зависит от временных параметров вашего устройства. Простейший случай (для малых скоростей) — клокать данные не по фронту, а по спаду того же самого клока. Потом перещелкиватьт фронтом. Более сложный случай — смотреть задержки сигналов и двигать фазу клока в оптимальное место.
          +2
          в синхронных схемах, как правило все всегда делается по фронту.
          Если на шине выставляются данные по фронту, то и принимаются по фронту.
            0
            Ну насколько я понимаю DDR сейчас вполне себе применяется. И не только для интерфейсов памяти.
              0
              Не обязательно. И интерфейс с внешним миром — как раз та область где флюктуации наблюдаются наиболее часто ;) Ну и про ддр уже было сказано.
              0
              Да, вариант с синхронизацией по спаду я тоже пробовал, но была какая-то проблема, про которую я уже забыл. То ли не хотело синтезировать, то ли еще что-то.

              Игры с фазой сигнала это уже шаманство какое-то. Хоть DCM&PLL позволяют это делать, но я всегда думал что этот функционал используется скорее для подгонки фронтов на входе в устройство, и для устранения clock skew.

              Ведь если схема изменится, либо PAR по другому разложит, то придется опять все пересматривать.
                0
                представьте себе шину, значение на которой устанавливается по фронту.
                Вы хотите надежно зафиксировать это значение в регистре и выбираете момент «посередине периода» (видимо для надежности), например по спаду. Теперь ваш регистр выдает свой сигнал дальше, следующей логике, но он сдвинут по фазе. Следующий регистр, который будет фиксировать уже это значение должен так же принять его по середине? То есть вы думаете для следующего в цепочке регистре нужно опять сдвигать фронт, теперь уже на четверть периода основной частоты?

                Нет и нет, только по фронту. Все регистры в простой синхронной схеме работают по фронтам.
                  0
                  Если вы посмотрите спецификацию какого-нибудь SPI-интерфейса (у внешней микросхемы, будь то ЦАП, АЦП или какая-нибудь экзотика), то в 90% случаев вы увидите, что «данные выставляются по фронту, принимаются по спаду» или наоборот. Это то решение, которое малыми усилиями позволяет избежать головной боли.
                    0
                    Нет, конечно. Я так не думал. Когда все происходит в рамках ПЛИС, проблем нет. Просто есть устройства, где явно нарисована временна́я диаграмма с запаздыванием клока относительно установки данных. Почему и спросил, как такое обычно разруливается. Если можно было бы просто выставить значения на шину и тут же дернуть выходной клок, так вообще проблем не было бы.
                  0
                  Да, вот вы сказали про скорости. А что делать в случае высоких требований к скорости? Если чередование в тактах уже недопустимо ввиду снижения пропускной способности в 2 раза.
                    0
                    Не совсем понимаю, что вы имеете ввиду под чередованием в тактах. Вы дублируете пути в тракте данных и создаете multicycle path? Не могли бы нарисовать картинку для пояснения вашей проблемы.
                      +1
                      Нет, просто на первом такте выставляются данные, на втором дергается клок, затем опять данные (клок опускается), и т. д. То есть получается, что частота клока шины в 2 раза меньше.
                +3
                Мне понравилась статья: красивые картинки, описание код-синтез и т.п. Но, честно говоря, таких статей тут не мало. Почему-то все останавливаются на описании счетчика и морганием светодиодом, что по сути одно и то же. Видно же, что Вы по уровню ушли вперед от этого. Конечно, никто не заставляет Вас описывать свои большие проекты. Но смысл FPGA, что они инструмент и у него масса применений. Существует масса направлений развития этой темы. Если вы занимаетесь ЦОС, то расскажите как делаются FIR, IIR, CIC фильтры, как делать децимацию и интерполяцию; если вы занимаетесь обработкой видео, то расскажите о реализации различных алгоритмов в железе; если вы занимаетесь интерфейсами, то расскажите о FSM Мура и Мили, о конвееризации; если ethernet, то формирование udp/tcp пакеты и реализации MAC уровня. Это было бы гораздо интереснее читать, хотя бы мне. Я понимаю, что некоторые темы сложные для объяснения, но всегда можно найти какой-то промежуточный уровень, когда еще писать не много, но и понимать это нужно для дальнейшего. Не страшно, что многие люди не поймут, это узконаправленная тема.
                Извините за такое излияние, еще раз скажу, что статья мне понравилась. Бы было полезно добавить, как отличаются "<=" и "=" в testbanch`ах. И про wire, через которые удобно описывать комбинаторику как раз через "=", а reg через "<=".
                  +1
                  Спасибо за комментарий.
                  Изначально я думал написать небольшое введение для своих студентов, но в процессе понял что взялся за слишком широкую тему. И без объяснения множества подводных камней ничего не выйдет. Поэтому буду как раньше плясать у доски и исправлять глупые баги =)

                  Хорошо, попробуем копнуть глубже. Пока ехал в метро понял что у меня есть материал по нескольким темам. Буду искать время чтобы оформить и выложить.
                    0
                    Это было бы просто шикарно. Очень нехватает именно подобных развернутых вещей. А про триггеры, типы регистров и стейт машины и в книгах пишут.
                    0
                    Вы правы, но есть проблема: Чтобы рассказать «как делаются FIR, IIR, CIC фильтры, как делать децимацию и интерполяцию» надо сперва объяснить, что такое «FIR, IIR… и т.д.» А это в первую очередь «матан», объянять который здесь затруднительно и весьма трудозатратно…
                      0
                      откровенно говоря, не так нужен матан в описании фильтров. Весь ЦОС это умножение с накоплением. Если посмотрите мои созданные темы, то найдете описание ких фильтра без формул вообще. Смысл ких фильтра — перемножить пары чисел и их сложить.
                        0
                        Без матана он никому не нужен, вот в чем беда — не будет понятно для чего всю эту канитель разводить.
                      0
                      вот здесь есть описание нескольких проектов для ПЛИС: http://marsohod.org/index.php/projects

                      там и USB, и видеоигра, простой процессор и т.д.
                        0
                        За их проектами я слежу, очень многое для себя подчеркнул. Могут, делают!

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое