Verilog как образ жизни. Изучаем языки описания железа на примере VerilogЕвгений «Vshmuk» Бейсембаев (diver@edu.ioffe.ru) В прошлых своих статьях я не раз упоминал про ПЛИС (Программируемые Логические Электронные Схемы). Если не читал, то вкратце напомню: ПЛИС - это такой чип, на основе которого можно создать свой собственный микропроцессор, идеально заточенный под твои задачи. Для его описания мы использовали специально разработанный язык Verilog, так называемый HDL (Hardware Description Language). Об основах программирования на таком непривычном языке сегодня и пойдет речь. Verilog был придуман в 1985 году, в США. Вскоре Институт Электроинжененров взял его за стандарт IEEE 1364, и на текущий момент существует несколько расширений языка (Verilog-2001, SystemVerilog - http://en.wikipedia.org/wiki/SystemVerilog), куча связок и интерфейсов к С-подобным языкам. В общем, разновидностей Verilog'а придумано немало. Каждая корпорация-производитель ПЛИС затачивает язык и компилятор под свои нужды, поэтому, прочитав об интересном приеме в учебнике, будь готов к тому, что какой-нибудь Altera Quartus этот прием не переварит, и тебе придется придумывать что-то свое. Ноги у этого языка растут из известного тебе C, – он считается менее громоздким и более простым в реализации. Главный конкурент Verilog'а, VHDL, напоминающий языки ADA и Pascal, также является стандартом, но использовать мы его не будем, можешь про него почитать отдельно. При этом у языков больше сходств, чем различий, поэтому, оседлав один из них, ты сможешь с легкостью переключиться на другой, если он вдруг приглянется больше. Что есть HDL?Представь себе механизм, имеющий какую-либо связь с внешним миром. Как только снаружи приходит сигнал (нажали кнопку на пульте управления), внутри механизма что-то срабатывает, и им совершается действие (например, загорается лампочка). Для удобного описания всяких механизмов и причинно-следственных связей и были придуманы HDL-языки. Совсем необязательно HDL описывает микросхему или ПЛИС. На нем, по сути, можно накодить, что угодно – скажем, устройство, которое никогда не будет реализовано в принципе. Или старый ламповый телевизор. Или маятниковые часы. Даже запаянную с двух сторон металлическую кружку можно так или иначе описать на HDL, потому как она железная (hardware) и ты ее можешь потрогать :). Все, что можно представить блок-схемой, пишется запросто. Главная сложность, с которой сталкивается программист, начинающий писать на HDL, – ему необходимо осознать, что весь код будет в итоге исполняться одновременно. Как шестеренки в часах вращаются синхронно по событию маятника, так и операции внутри процессора выполняются сразу же, параллельно, успевая до наступления следующего такта! Грубо говоря, в этом и есть отличие алгоритмических языков типа С или Pascal от HDL-подобных. Если первые «указывают» некоторому абстрактному роботу-исполнителю последовательность действий, то вторые описывают внутренности самого «исполнителя», поведение которого будет зависеть от этих самых внутренностей. Зачем же описывать эти внутренности, спросишь ты, когда можно описать поведение? Отвечаю. Допустим, цель жизни какого-нибудь робота - перевозить поддоны с печеньками из печки в упаковочный цех. Естественно, приделывать к нему манипуляторы, например, для перевозки бутылок лимонада, смысла особого нет. А твой Intel Core, в который ты загружаешь свежескомпиленную сишную прогу, как раз и является мегауниверсальным роботом, который может делать абсолютно все. Не всегда и не для всех задач нужен такой монстр. Тогда ты берешь HDL и пишешь свой проц, который почти ничего не умеет, но то, что умеет – делает быстро, эффективно и без потребления сотен ватт мощности! Что ты должен знатьУчти, именно HDL-языки сейчас становятся все более востребованными. Корпорации вплотную приблизили свои процессоры к порогу частоты, и теперь для увеличения производительности им приходится оптимизировать и параллелить их. Ты можешь сам тренироваться создавать свои устройства, просто купив или спаяв отладочную плату с Altera Cyclone и скачав бесплатный компилятор Quartus с altera.com. Крутой криптоанализатор или брутфорсер, работающий в разы быстрее современных процессоров и при этом лениво стоящий на полочке, еще никому в хозяйстве не мешал! Для простого программирования было бы неплохо, чтобы ты, несмотря на другой принцип HDL, неплохо владел Си или Ассемблер. Знание более высокоуровневых языков тоже подойдет, но будь готов к тому, что некоторые тонкости работы процессоров покажутся сюрпризом. Скомпилированный файл-«прошивку» тебе захочется залить в ПЛИС и потестировать, как оно работает на реальном железе. Поэтому знание основ электроники, умение конфигурировать порты микросхем также будут нелишними. Итак, цель создания языков описания железа понятна. Разберем синтаксис. Как я уже говорил, Verilog очень похож на язык С. Базовый синтаксис1. Комментарии, имена, константы, числа Если не умеешь писать на каком-либо языке, научись, как минимум, синтаксису его комментариев. Тогда хотя бы не будет ругаться компилятор.
В Verilog'е имя (идентификатор) - последовательность букв и цифр, знаков «$» и «_», причем начинаться оно обязано не с цифры. Регистр имеет значение. Константы в Verilog'е имеют особую форму записи. Сначала идет разрядность числа, потом кавычка ('), за ним - основание системы счисления (b, o, d, h) и сами цифры. Примеры:
2. Регистры То, что ты привык называть переменной, в Verilog'е называется регистром. Например:
Так мы объявили регистр шириной 8 бит (от нуля до семи) с именем character. Как и в переменную, в регистр можно класть значение и читать его оттуда:
Здесь мы кладем в регистр var1 старшую половину регистра var2, который в два раза «шире». В итоге, там будет лежать число 8'b1001_0110, то есть 0x96h.
Примечание: строго говоря, в Verilog'е тоже есть нормальные человеческие переменные, причем регистр - это переменная типа reg. Также бывают типы integer, time, real и другие. Но в ближайшее время это тебе не понадобится, поэтому считай, что переменная в Verilog'е и есть регистр. 3. Сигналы (wire) Это особый вид объектов, аналогом которому в алгоритмических языках, полагаю, являются указатели. Так называемое «соединение-цепь» или ярлык, связывающий части схемы между собой. Предположим, есть у нас некая переменная-регистр, к отдельным битам которой часто приходится обращаться по ходу действия. Введем сигнал, привязанный к одному биту переменной:
Теперь port_0_direction будет равен 1, только если единице равны 0 и 2 биты регистра device_config. Связи могут быть также типа Монтажное И (wand), Монтажное ИЛИ (wor), tri0, tri1 – и много других, но они реже используются. 4. Процессы always & initial Процесс - это такой кусок кода, внутри которого все операции выполняются последовательно. Это то самое движение, шестеренки, ради которых мы и городим весь остальной код. Процесс может быть непрерывным, срабатывающим на какое-либо событие или вообще исполняемым ровно один раз для инициализации.
Ключевое слово, указывающее на процесс - always. Затем идет так называемый оператор ожидания события «@» и список чувствительности в скобках (смотри врезку). А между словами begin и end - само тело процесса. Как только значение переменной Sysclock сменится с 0 на 1, регистр counter увеличится на единичку. Всякие «нормальные» последовательные операторы ("=","<=","if","case" etc.) разрешено использовать только внутри процессов. Полный список операторов в языке Verilog указан во врезке. Если вместо always стоит initial, то процесс выполнится однократно. 5. Группы операторов То, что ограничено словами begin и end, называется составной группой. Это то же самое, что и фигурные скобки в языке Cи. А вот чего нет в Си, так это параллельных групп:
Здесь изменение регистров counter и anticounter будет происходить параллельно в один и тот же момент времени. Используя fork и join вместо begin и end, мы расходуем в два раза меньше времени, выполняя эти две операции независимо друг от друга! 6. Модули Модули (module) в Verilog'е - это что-то типа черных ящиков или блоков обработки. Больше всего они похожи на функции в алгоритмических языках программирования. Как и функции, модули имеют входные и выходные параметры. Просто инвертирует входной бит
Здесь из «ящика» торчат два провода. Как только на входном проводе меняется значение, оно инвертируется и появляется на выходном. В скобках мы указываем все провода, какие есть, а уже внутри модуля - указываем их «направление». Порт может быть входной (input), выходной (output) и двунаправленный (inout). Уважающий себя черный ящик должен иметь внутри себя какие-нибудь шестеренки и движущиеся детали, реагирующие на внешние импульсы. Что мы и видим по ключевому слову always. Могут ли быть статические, не «движущиеся» черные ящики? Да, могут. Просто инвертирует входной бит Этот ящик делает все ту же операцию, но уже без отслеживания внешних событий, – просто непрерывно инвертирует входной бит, статически выдавая его наружу. Шестеренок нет, зато есть намертво запаянные провода. Чуешь разницу? 7. Операции Здесь - все, как ты привык, так что вкратце:
А также есть интересная операция «склеивания», то есть конкатенации:
То, что справа – имеет ширину 8 бит и склеено из нулевого и оставшихся семи бит переменной Lights. В общем, выведи мы содержимое этого регистра наружу на светодиоды, наблюдали бы бегающий «огонек». На каждый такт «хвост» перемещается в «голову» регистра, а потом неторопливо ползет назад. И так по кругу. ЗаключениеЛаконичный материал этой статьи уже позволит тебе написать нормальную, «взрослую» прошивку для своей ПЛИС. Но это лишь основы, – тот минимум, который ты должен знать, садясь за кодинг. Важно размять свой привыкший к последовательной логике мозг и представить механизм, в котором параллельно происходят десятки взаимосвязанных движений деталей. Надеюсь, что подвигнул тебя к изучению чужих примеров и чтению учебников. Может быть когда-нибудь мы таки услышим о создателе сверхбыстрой процессорной архитектуры, читавшем в молодости «Хакер». Последовательные операторыВнутри процессов типа always надо использовать обыкновенные и привычные тебе «последовательные» операторы. Привожу краткий список того, что ты можешь использовать в Verilog'е. 1. Оператор ожидания. На этом операторе мы «висим» и ждем чего-нибудь.
В последнем примере слова posedge и negedge внутри списка чувствительности обозначают ожидание фронта и среза сигналов. То есть, как только значение С начнет меняться с 0 на 1 или D - с 1 на 0, тут же сработает событие. Зачем это надо? Любой процессор получает тактовую частоту на вход. Это те самые гигагерцы, на которые ты смотришь при покупке и дальнейшем разгоне твоего камня. Для синхронности операций внутри ядра все должно начинаться на каждый новый фронт сигнала тактовой частоты. А в Verilog'е это будет записываться как «always @(posedge sysclock)...». 2. Присваивание. Следующая операция не начнется до его завершения. 3. Неблокирующее присваивание. Советую тебе обратить внимание на эту штуку. Дело в том, что операция установки регистра занимает определенное время. Но зачем нам стоять на месте и ждать, пока копирование завершится (оператор «=»), если мы можем параллельно выполнять еще кучу несвязанных с регистром дел? Тогда мы используем неблокирующее присваивание, при котором перескок на следующую операцию идет сразу после того, как регистр начнет устанавливаться. Хочешь по-настоящему быструю систему? Всегда используй неблокирующий оператор.
4. Условный. Тут сложностей нет.
5. Оператор выбора. Сложностей тоже нет.
Четырехзначный алфавитИз-за того, что Verilog создан для проектирования и эмуляции реальных схем, – двоичного представления уровня сигнала на линии бывает недостаточно. Кроме описания логического нуля (0) и логической единицы (1) требуется еще так называемое высокоимпедансное состояние z, когда порт ни к чему не подключен и «болтается в воздухе». К тому же, во время симуляции твоей схемы вполне возможно увидеть на линии неопределенное значение «x», когда симулятор не смог точно вычислить состояние линии, например, из-за конфликта на общей шине. При программировании ПЛИС состояния z и x тебе вряд ли понадобятся, но как только ты начнешь копать глубже в схемотехнику и пытаться симулировать какое-либо устройство, тебе придется использовать расширенный алфавит. Пример ЗадержкиМеханизмы неидеальны, а свет имеет конечную скорость. Verilog не был бы языком описания железа, не умей он учитывать подобные проблемы. По сути, главное знание, отличающее профессионального HDL-кодера от новичка, это правильное понимание того, что сигналы распространяются не мгновенно, и умение правильно составлять код для уменьшения задержек. В компиляторы под ПЛИС часто уже включена опция Timing Analysis, которая автоматически просчитает время распространения сигналов на кристалле. Все, что тебе остается, это убедиться, что максимальная задержка не превышает периода тактовой частоты твоего будущего процессора. Если нет - оптимизируй свой код. Например, пользуйся неблокирующими операторами присваивания (<=) вместо блокирующих (=) везде, где только можно. Это позволит компилятору распараллелить копирование регистров и увеличит шансы, что операция выполнится до прихода нового фронта синхроимпульса. В идеале, такое присваивание завершится через бесконечно малый промежуток времени, в моделировании обзываемый «дельта-задержкой» (для воспроизведения причинно-следственных отношений), но в случае реального железа время будет ощутимое, хоть и в разы меньше, чем при использовании блокирующего присваивания. Пример триггера (D, RS)Давай рассмотрим пример полностью готового и рабочего модуля для простого триггера - примитивного последовательного устройства. Этот черный ящик будет запоминать значение a или b в момент подачи на него сигнала clock. Что именно он запомнит, будет зависеть от значения управляющего сигнала switch. При подаче сигнала reset модуль все забудет и станет выдавать 0.
|