Часть 0 - покупаем, паяем, ставим драйвера и софт Часть 1 - что это вообще за зверь? Часть 2 - наша первая схема!Часть 3 - кнопочки и лампочки
Часть 4 - делитель частоты Часть 5 - подавление дребезга кнопки Часть 6 - заканчиваем кнопочки и лампочки Часть 7 - счетчики и жаба Часть 8 - передатчик UART Часть 9 - Hello, wolf! Часть 'hA - приёмник UART Часть 'hB - UART и жаба Часть 'hC - полудуплексный UART. Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО Часть 'h10 - передатчик сообщений МКО Часть 'h20 - работа с АЦП ADC124s051 Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD) Часть 'h22 - Bin2Bcd с последовательной выдачей данных Часть 'h23 - перемножитель беззнаковых чисел с округлением Часть 'h24 - перемножитель беззнаковых чисел, реализация Часть 'h25 - передаём показания АЦП на компьютер Часть 'h26 - работа над ошибками (быстрый UART) Часть 'h27 - PNG и коды коррекции ошибок CRC32 Часть 'h28 - передатчик изображения PNG Часть 'h29 - принимаем с ПЛИС изображение PNG Часть 'h2A - ZLIB и коды коррекции ошибок Adler32 Часть 'h2B - ускоряем Adler32 Часть 'h2C - формирователь потока Zlib Часть 'h2D - передаём сгенерированное PNG-изображение Часть 'h2E - делим отрезок на равные части Часть 'h2F - знаковые умножители, тысячи их! Часть 'h30 - вычислитель множества Мандельброта Часть 'h31 - ускоренные сумматоры Часть 'h32 - ускоренные счётчики (делаем часы) Часть 'h33 - ускоряем ВСЁ Часть 'h34 - ускоренные перемножители Часть 'h35 - умножители совсем просто Часть 'h36 - уравновешенный четверичный умножитель Пора поиграться немного с Verilog'ом - описать простенькие схемки. Сначала - дешифратор двоичного кода, так что тремя кнопками мы будем просить включиться соответствующий светодиод.
Никакой специфики конкретно этой ПЛИС тут нет - данные модули заработают на любой, хоть Альтера, хоть Xilinx, хоть Воронеж, хоть Lattice semiconductor. Есть в этом определённая прелесть - язык, который считается наиболее "низкоуровневым" (описывает конфигурацию цифровой схемы), внезапно проявляет свойства самых-самых "высокоуровневых", дескать, один раз написал - запускай повсюду!
Принимаю пожелания - не слишком ли "разжевано", или наоборот, хочется побольше "азов" пояснить? Мне всегда нравилось в институте, когда объясняют чуточку "тупее". Были преподы, которые боялись, что их на смех поднимут, перескакивали очень шустро с темы на тему, так что на миг отвлечёшься - и всё...
Дешифратор двоичного кода
У нас 8 светодиодов. Нажимая в разных комбинациях на 3 кнопки, мы хотим научиться зажигать любой из них:
0 0 0 - включаем первый,
0 0 1 - второй
0 1 0 - третий
0 1 1 - четвёртый
1 0 0 - пятый
1 0 1 - шестой
1 1 0 - седьмой
1 1 1 - восьмой.
Ничего хитрого.
Проще всего продолжить предыдущий проект.
Создаём новый файл, verilog HDL file, и пишем:
module Decode3to8 (input [2:0] D, output [7:0] Q);
assign Q = 1'b1 << (~D);
endmodule
Сохраняем его под именем Decode3to8.v, Quartus это любит! Дело в том, что в проекте есть понятие top-level entity - наиболее общая "схема" или модуль. Именно его он начинает синтезировать во время компиляции, подцепляя другие модули, если они там задействованы. Мы можем назначить любой модуль как top-level в меню assignments - settings - general - top-level entity.
Но каждый раз туда лезть немножко лениво - есть "обходной путь" тыкнуть по файлу в project navigator в левой части экрана, и там в контекстном меню выбрать "Set as top-level entity". Как оказывается, всё, что делает Quartus при этом - вводит в то текстовое поле имя файла, что мы выбрали. Если файл называется также, как модуль внутри него - всё хорошо, заработает как ожидалось. Если названия не совпадают - он выругается. Смешнее всего будет обозвать файл по имени совсем другого модуля - такого рода трюк вполне подойдёт, если вас попёрли с работы, и напоследок хочется немножко усложнить жизнь своим коллегам. Примерно как #define TRUE FALSE.
Пара пояснений по синтаксису. В одном verilog'овском файле должен содержаться ровно один модуль. Нас учили, что в одном verilog'овском файле надо размещать ровно один модуль, но это не обязательно - их может быть сколько угодно, в любом порядке. У каждого модуля есть имя, а также список входов и выходов. Квадратные скобки означают шину - значит, у нас не один провод идёт, а несколько, в случае [2:0] - их три: 0, 1, 2, причём нулевой считается самым младшим. Возможна и запись [0:2], значит, 2-й будет самым младшим, но привычнее как-то от большего к меньшему всё-таки.
Этот дешифратор - чисто комбинаторная логика, то есть в ней нет никакой памяти, состояние на выходах полностью определяется состоянием на входах в данный момент времени.
Для комбинаторной логики принято использовать "непрерывное присвоение", continious assignment, ключевое слово для которого - assign. Это означает - мы ЖЕСТКО связали выходы со входами, присвоение происходит не в какой-то момент времени, а ВСЕГДА. Также заметим, оператор присвоения - обычный знак "=".
1'b1 - это способ представления констант с заданной шириной. Тут буква b означает - binary, двоичный. Единичка слева - количество разрядов, один. Единичка справа - непосредственно значение этой константы, единица.
Если мы поставим здесь просто 1, всё нормально откомпилируется, но возникнет предупреждение "truncated value of size 32 to match size of target (8)". Иными словами, обычные числа воспринимаются конкретно Quartus'ом, как 32-битные целые.
Что интересно, если у нас шина Q восьмибитная, а мы поставили всего однобитную константу справа - никаких проблем не будет, он все старшие разряды попросту заполнит нулями.
Но мы могли бы записать и так:
assign Q = 8'b00000001 << (~D);
Результат от этого не изменится.
Знак ~ - это побитовое отрицание, мы его сюда поставили, поскольку при нажатии кнопки значение "1" сменяется на "0", вот мы и хотим, чтобы всё было наоборот.
И наконец, << - операция битового сдвига влево. Сдвиг влево, хоть арифметический, хоть логический, работает одинаково: имеющиеся биты сдвигаются, а на их место поступают нули. Биты, которые были "вытолканы" за пределы шины, "исчезают бесследно".
Чтобы узнать, во что превращается такой модуль "внутри ПЛИС", откомпилируем его по отдельности. В списке файлов проекта (слева) тыкаем в наш Decode3to8 правой кнопкой и выбираем Set as top level entity. Затем запускаем Compile design и смотрим Compilation report. Total logic elements: 8. Это теоретический минимум, уложиться в меньшее количество просто нельзя, поскольку у каждой логической ячейки всего один выход, а нам этих выходов надо 8. По сути, были сформированы 8 функций типа Q[0] = f(D[0], D[1], D[2]), которые и были занесены в генераторы функций (LUT) - в комбинаторную часть логической ячейки.
Давайте напишем для этого модуля "юнит-тест". Для этого создаём файл типа Vector Waveform File. В левой строке жмём правой кнопкой мыши, выбираем Insert - Insert node or bus. В открывшемся окне - Node Finder... Выбираем pins:all, тыкаем "List", слева появляется список.
Переносим слева направо D и Q, и после нескольких OK они переносятся к нам на "осциллограмму".
Затем по строке D тыкаемся правой кнопкой, лезем в value - Count value...
Выбираем radix - к примеру, unsigned decimal, оставляем start value - 0, increment by: 1, обычный бинарный счёт (не в кодах Грея). Во второй вкладке я предпочитаю поставить интервал чуть побольше, 100 нс, чтобы не шибко отвлекаться на задержки сигнала, слишком уж заметные на 10 нс.
Вот что получается:
Сохраняем этот файл под каким-нибудь понятным именем, например Decode3to8Test.vwf.
Заходим в Assignments - settings - simulator settings, там в поле Simulation input выбираем только что сохранённый файл.
Теперь мы можем нажать на кнопку Start simulation - на синюю стрелочку.
Всё работает вполне ожидаемо. Можно заметить отдельные короткие "пички", это следствие разной задержки по различным цепям - например, при переключении от 1 к 2 у нас одновременно меняется 2 бита - нулевой переключается с 1 в 0, первый - с 0 в 1. В большинстве случаев, когда мы будем иметь дело с синхронными схемами, эти пички совершенно не страшны - тактовая частота подбирается так, чтобы все переходные процессы успели пройти, на входах всех регистров появились бы правильные значения, и только после этого они бы "защёлкнулись". Но иногда эти вещи могут ОЧЕНЬ нас нервировать. Допустим, один из выходов ПЛИС подсоединён к системе аварийного подрыва объекта, и там время от времени будут происходить такие "выбросы"... Нам очень захочется не слать комбинаторный выход напрямую - лучше пустить его через триггер, он будет подавать на выход только установившиеся значения. Такая же история, если мы захотим использовать асинхронный сброс или установку - они могут и отреагировать на этот "мусор", и попробуй сообрази, почему не работает!
Есть ещё интересная штука - коды Грея. Давайте отредактируем наш vector waveform file - снова в D выберем value - count value, но теперь выберем именно коды Грея. В итоге, при моделировании получим вот что:
Никаких пичков: всё чинно и благородно, а всё благодаря тому, что при счете в кодах Грея у нас каждый раз переключается всего один бит, поэтому никаких промежуточных значений быть не может! (когда мы переключались с 1 до 2, у нас кратковременно проскочил нолик, и только потом он переправился в двойку)
Но мы отвлеклись. Давайте запихаем этот модуль декодера в нашу принципиальную схему. Для этого тыкаем по Decode3to8.v в левой вкладке, в project navigator, и выбираем Create symbol files for current file. Пора вернуть бразды правления нашей схеме "верхнего уровня" - тыкаемся по ней и выбираем Set as top-level entity. Открываем её, размещаем новый элемент, из "папки" project. Слева и справа добавляем входы и выходы. Вход переименовываем как Buttons[2..0], а выход - как LED[7..0]:
Нужно заметить: в verilog'е и на схеме синтаксис разный! В верилоге мы для обозначения шины ставим числа через двоеточие, а здесь - через две точки. А в VHDL - и вовсе пишется 7 downto 0, либо 0 to 7 в скобках. Язык Ада - что вы хотели?
Запускаем compile design, затем лезем в Pin Planner. Сверившись с табличкой, вбиваем номера выводов:
LED[7] : 109
LED[6] : 108
LED[5] : 107
LED[4] : 106
LED[3] : 105
LED[2] : 103
LED[1] : 102
LED[0] : 101
Buttons[2] : 118
Buttons[1] : 116
Buttons[0] : 119
Компилим ещё разочек, преобразуем файл прошивки из epc2 в epc4, как было описано в прошлой части, и прошиваем.
Либо можно попробовать ещё финт ушами. Пока мы "играемся", вовсе не обязательно раз за разом прошивать Flash - это медленно. Запишем .sof - файл напрямую в ПЛИС! Вот так:
Моргнуть не успеешь, как прошивка уже будет завершена. Вот теперь ни в коем случае нельзя нажимать на сброс (SA1) - тогда только что прошитая схема пропадёт! Нет, работать она начинает уже сразу.
Результат - вполне ожидаемый. Первым светодиодом, так получается, мы назвали крайний левый, и вот так слева направо они пронумерованы.
А допустим, теперь мы захотели пронумеровать их справа налево, как номера битов? Лезть в Pin Planner - последнее дело, эта сволочь ни на секунду не допускает двух выводов с одинаковым номером! То есть, нам придётся сначала удалить номер вывода в LED[7], потом этот же номер ввести на LED[0], и так далее. Долго, муторно и никому не нужно.
Гораздо проще в схеме переименовать output port: вместо LED[7..0] назовём его LED[0..7]!
Это определит, как именно соединяется шина LED с выходной шиной нашего дешифратора, и действительно, теперь первым светодиодом станет крайний правый.