Шауэрман Александр А. shamrel@yandex.ru
Эта статья на простом примере показывает общие принципы работы с учебным стендом LESO2.4. Научимся создавать проект в Quartus II, начнем использовать Verilog, и проверим работоспособность периферии стенда.
В учебном стенде использована ПЛИС семейства Cyclone IV E фирмы Altera. Для работы нам понадобится среда проектирования Quartus II актуальной версии. Достаточно бесплатного варианта Web Edition. На моем рабочем компьютере установлена версия 13.1.0, на сайте altera.com доступна для скачивания версия 15.0 (нужна регистрация).
1. Создаем проект
Запускаем Quartus II. В меню выбираем: File -> New Project Wizard .
Пробегаемся по вкладкам (жмем Next), выбираем директорию для проекта (вкладка "Directory, Name, Top-Level Entity [page 1 of 5]"), там будут храниться все файлы. Для примера я создал папку d:\leso\rep\leso2_demo\. В строке "What is the name of this Project?" введем имя проекта, например, leso2_demo. В третьей строке автоматически появится имя модуля верхнего уровня. Это имя можно при необходимости далее изменить в настройках проекта, или оставить по умолчанию. В последнем случае, для того, чтобы Quartus автоматически нашел "сущность" верхнего уровня, следует файл с основным модулем назвать так же, как и сам проект, то есть, в нашем случае leso2_demo.v .
На следующей вкладке "Add Files [page 2 of 5]" будет предложено добавить существующий файл в проект. Можно пропустить этот пункт и добавить файл, когда проект будет уже создан. Мне удобнее сейчас создать в рабочей директории пустой текстовый файл leso2_demo.v и сразу включить его в проект. На вкладке "Family & Device Settings [page 3 of 5]" мы должны выбрать микросхему ПЛИС, с которой мы собираемся работать. В лабораторном стенде LESO2.4 используется Cyclone IV E EP4CE6E22C8N (можно посмотреть на принципиальной схеме, либо непосредственно на устройстве). На четвертой вкладке предлагается выбрать внешние инструменты для работы с проектом. Пока нам достаточно встроенных в Quartus II. Смело пропускаем этот пункт.
На пятой вкладке приводятся сводные параметры создаваемого проекта. Читаем, проверяем, и, если со всем согласны, то завершаем создание: жмем Finish. Теперь можно через любой файловый браузер зайти в рабочую директорию и убедиться, что САПР создал рабочие файлы.
2. Модуль верхнего уровня
Проект создан. Пишем рабочий модуль. Используем Verilog.
Почему текстовый язык, а не такой понятный редактор Block Diagram/Schematic? Плюсы у графического представления схемы бесспорно есть, и, прежде всего, это наглядность и привычность. Но привычки – это дело приходящее и уходящее, а ряд весомых аргументов, все же диктует нам переход к текстовому стилю описания схем. Во-первых, универсальность и портируемость. Код в текстовом виде легко переносить из проекта в проект даже если используется другое семейство ПЛИС или другой САПР (вдруг мы решим перейти на Xilinx). Как следствие, единожды написанные функциональные блоки и модули просто использовать в последующих проектах. Во-вторых, код в текстовом виде можно редактировать в любом текстовом редакторе, а не только в среде Quartus II. В тексте легко найти и заменить нужную переменную, скопировать часть кода из одного модуля в другой. И в-третьих, а это для меня даже важнее первых двух пунктов, с текстовым материалом гораздо проще работать в системах контроля версий, таких как git. Всегда видно на каком этапе, что и кем было изменено. Это существенное преимущество при работе с серьезным проектом.
Почему Verilog? На мой взгляд, язык описания Verilog более универсален, чем VHDL, и совместим с более продвинутым и современным System Verilog.
И так, холивар оставим комментаторам, а мы создаем модуль верхнего уровня. Помним, что на этапе создания проекта мы уже добавили в него рабочий файл leso2_demo.v, открываем его любым текстовым редактором. Если в самом Quartus II в окне Project Navigator выбрать leso2_demo, то откроется встроенный стандартный редактор Verilog. Мне он сразу не понравился. Сменить текстовый редактор по умолчанию в Quartus просто: заходим в Tools -> Option ... Выбираем пункт "preferred Text Editor" и там назначаем желаемый редактор в зависимости от личных предпочтений, серьезности намерений и желании заморочиться. Для новичков, рекомендую попробовать Notepad++.
Назовем модуль leso2_demo и опишем входные/выходные порты. Как уже говорилось выше, в демонстрационном проекте поставим задачу проверить периферию платы. Открываем схему, смотрим на стенд и видим группу тумблеров (SB1-SB8), ими можно задавать входные уровни, 8 светодиодов (HL1-HL8) – самый простой способ отображать цифровой код, семисегментный индикатор (HG1), одна кнопка (SW1). Чуть не забыли про генератор тактовых импульсов D1 на 50 МГц. Еще есть микросхема FTDI для связи с компьютером и внешний разъем IDC-40 – их пока трогать не будем.
Линии, которые служат для ввода информации в ПЛИС, объявляются как input, те которые для вывода – output. На светодиоды идет сразу 8 линий, мы можем каждую линии описать как отдельный вход, но гораздо удобнее объединить все светодиоды в одну 8-ми битную шину и описать так:
Думаю, очевидно, что "[7:0]
" – разрядность шины. Обращаю внимание на название "led_o
". Удобно к входным портам добавлять постфикс "i", от английского "input", а к выходным постфикс "o", соответственно, от английского "output". Если порт предназначен для ввода и вывода, то постфикс будет "io". Теперь встретив в тексте программы переменную с названием "led_o
" сразу станет понятно, что это порт вывода из модуля, и отвечает за светодиоды.
По аналогии, тумблеры опишем как:
Тактовую кнопку SW1:
В названии порта ввода для тактовых импульсов укажем, что это именно тактовые импульсы ("clk" – от "clock"), и частота этих импульсов равна 50 МГц:
С семисегментным индикатором посложнее, он сдвоенный, потому удобнее каждую цифру описать отдельно, например так:
output [7:0] hg0_o,
output [7:0] hg1_o,
где: hg0_o
– правая цифра, hg1_o
– левая.
Название модуля введено, входные порты описаны. Модуль завершается ключевым словом endmodule
. Содержимое файла leso2_demo.v:
module leso2_demo(
// Генератор
input clk_50MHz_i,
// Тумблеры
input [7:0] sb_i,
// Тактовая кнопка
input sw_i,
// Светодиоды
output [7:0] led_o,
// Индикатор
output [7:0] hg0_o,
output [7:0] hg1_o
);
// Тело самого модуля, которого пока еще нет.
endmodule
Для компиляции выбираем Processing -> Start Compilation. Или нажимаем CTRL+L, или нажимаем соответствующий значок (сиреневый треугольник) на панели инструментов. Проект должен компилироваться без ошибок. Однако в консоли (внизу окна Quartus II) появится ряд предупреждений. Конечно, ведь модуль у нас пустой, компилятору есть о чем предупредить нас. Пока не будем обращать на это внимание.
3. Назначаем Pin
Проект пережил первую удачную сборку, настало время сообщить Quartus какие порты каким физическим ножкам микросхемы соответствуют. По принципиальной схеме определяем к каким выводам ПЛИС подключены внешние устройства. Запускаем "Pin Planer" (находим в меню Assignments или пиктограмма на панели инструментов). Видим список всех портов (столбец "Node Name"), в столбце "Location" нужно задать каждому узлу определенную ножку ПЛИС. Можно с помощью мышки выбрать из списка, а можно ввести с клавиатуры. Мне удобнее с клавиатуры: начинаю вводить требуемый номер, редактор сам добавляет префикс "pin_" и варианты заполнения. Назначаем для тумблеров и светодиодов:
led_o[7] Output PIN_11
led_o[6] Output PIN_10
led_o[5] Output PIN_8
led_o[4] Output PIN_7
led_o[3] Output PIN_6
led_o[2] Output PIN_3
led_o[1] Output PIN_2
led_o[0] Output PIN_1
sb_i[7] Input PIN_65
sb_i[6] Input PIN_64
sb_i[5] Input PIN_60
sb_i[4] Input PIN_59
sb_i[3] Input PIN_58
sb_i[2] Input PIN_55
sb_i[1] Input PIN_54
sb_i[0] Input PIN_53
Пробуем скомпилировать проект и получаем ошибку:
Error (176310): Can not place multiple pins assigned to pin location Pin_6 (IOPAD_X0_Y22_N21)
Info (176311): Pin led_o[3] is assigned to pin location Pin_6 (IOPAD_X0_Y22_N21)
Info (176311): Pin ~ALTERA_ASDO_DATA1~ is assigned to pin location Pin_6 (IOPAD_X0_Y22_N21)
Error (176310): Can not place multiple pins assigned to pin location Pin_8 (IOPAD_X0_Y21_N14)
Info (176311): Pin led_o[5] is assigned to pin location Pin_8 (IOPAD_X0_Y21_N14)
Info (176311): Pin ~ALTERA_FLASH_nCE_nCSO~ is assigned to pin location Pin_8 (IOPAD_X0_Y21_N14)
Дело в том, что по умолчанию, выводы Pin_6 и Pin_8 в нашей ПЛИС объявлены как служебные и используются для конфигурации: Pin_6 – ASDO DATA1, Pin_8 – nCE/nCSO. Однако, мы можем сказать Quartus использовать их как обычные линии ввода-вывода. Для этого заходим Assignments -> Device ... Там выбираем "Device and Pin Options ...". Выбираем пункт "Dual-Purpose pins" и устанавливаем Data[1]/ASDO и FLASH_nCE/nCSO как "Use as regular I/O".
Проект компилируется без ошибок.
4. Простейшая схема
Настало время описать какую-нибудь схему. Самое простое, что приходит в голову, это перенаправить состояние с тумблеров на светодиоды. Делается это одной строкой:
Строку нужно поместить между описанием портов модуля и ключевым словом endmodule
. Строка назначает ("assign" – с английского дословно переводится как "назначить") входа sb_i
на выводы светодиодов.
Для того, чтобы проверить работоспособность модуля, нужно загрузить получившийся проект в ПЛИС учебного стенда. Поможет нам в этом утилита l2flash. Для работы этой утилиты, помимо подключенного к компьютеру стенда с установленными драйверами, требуется скомпилированный файл в формате *.rbf . Но Quartus по умолчанию при компиляции генерирует файл в формате *.sof. (генерируемые файлы располагаются в рабочем каталоге в папке "output_files").
Настроить генерацию файла в нужном формате можно в "Device and Pin Options ...", выбираем пункт "programming Files", устанавливаем галочку напротив "Raw Binary File (.rbf)".
Пересобираем проект. В директории выходных файлов получили leso2_demo.rbf. С помощью утилиты l2flash загружаем файл в ПЛИС.
Отлично! Переключаем тумблеры, загораются светодиоды. Только вот, крайний левый тумблер соответствует крайнему правому светодиоду. Формально это верно, нумерация светодиодов и тумблеров на схеме выполнена в порядке увеличения номера ножек ПЛИС, вот только тумблеры снизу микросхемы, а светодиоды сверху, потому результат зеркальный. Для удобства предлагаю переназначить порядок тумблеров таким образом:
sb_i[7] PIN_53
sb_i[6] PIN_54
sb_i[5] PIN_55
sb_i[4] PIN_58
sb_i[3] PIN_59
sb_i[2] PIN_60
sb_i[1] PIN_64
sb_i[0] PIN_65
Можно это сделать в редакторе "Pin Planer", но удобнее в "Assignment Editor" (выбираем в меню Assignments -> Assignment Editor, или пиктограмму на панели инструментов). Компилируем, загружаем (утилиту l2flash перезапускать всякий раз не нужно, просто нажимаем "Программировать"). Порядок светодиодов соответствует порядку тумблеров.
5. Семисегментный индикатор
Каждый сегмент индикатора является по сути своей светодиодом и управляется отдельной ножкой ПЛИС. Судя по схеме, в учебном стенде используется индикатор с общим анодом, это значит, что все аноды светодиодов в индикаторе объединены, и на них подается напряжение питания, на ПЛИС выведены катоды светодиодов. Для того, чтобы зажечь требуемый сегмент, на соответствующий вывод нужно подать логический ноль. Так как на индикаторе два символа, то очевидно - для управления первой и второй цифрой использоваться будет одинаковый код, потому целесообразно блок управления семисегментным индикатором оформить в виде отдельного модуля.
Будем выводить в шестнадцатеричном формате двоичный код, установленный тумблерами. Как раз ввести можно 8 бит, что соответствует двум шестнадцатеричным знакам. Обычно для этих целей используется дешифратор или декодер. На языке Verilog подобную задачу просто реализовать в виде конструкции Case.
Внимательно смотрим на принципиальную схему. И выясняем, что нулевому разряду шины hg0_o
(или hg1_o) соответствует сегмент "A", разряду hg0_o[1]
соответствует сегмент "B", и так далее. Руководствуясь этим наблюдением, в "Pin Planer" назначаем выводы.
Должно получится так:
hg0_o[7] Output PIN_124
hg0_o[6] Output PIN_126
hg0_o[5] Output PIN_138
hg0_o[4] Output PIN_128
hg0_o[3] Output PIN_127
hg0_o[2] Output PIN_125
hg0_o[1] Output PIN_136
hg0_o[0] Output PIN_137
hg1_o[7] Output PIN_129
hg1_o[6] Output PIN_143
hg1_o[5] Output PIN_144
hg1_o[4] Output PIN_135
hg1_o[3] Output PIN_133
hg1_o[2] Output PIN_132
hg1_o[1] Output PIN_141
hg1_o[0] Output PIN_142
Исходный код модуля декодера для семисегментного индикатора:
module seven (
input [3:0] code_i,
output reg [7:0] hg_o
);
always @*
begin
case (code_i) // hgfedcba
4'h0 : hg_o = ~8'b00111111;
4'h1 : hg_o = ~8'b00000110;
4'h2 : hg_o = ~8'b01011011;
4'h3 : hg_o = ~8'b01001111;
4'h4 : hg_o = ~8'b01100110;
4'h5 : hg_o = ~8'b01101101;
4'h6 : hg_o = ~8'b01111101;
4'h7 : hg_o = ~8'b00000111;
4'h8 : hg_o = ~8'b01111111;
4'h9 : hg_o = ~8'b01101111;
4'hA : hg_o = ~8'b01110111;
4'hB : hg_o = ~8'b01111100;
4'hC : hg_o = ~8'b00111001;
4'hD : hg_o = ~8'b01011110;
4'hE : hg_o = ~8'b01111001;
4'hF : hg_o = ~8'b01110001;
default:hg_o= ~8'b10000000;
endcase
end
endmodule
У модуля два порта: 4-х битный вход code_i
и 8-ми битный выход hg_o
. В зависимости от значения code_i
на выход устанавливается комбинация, определяющая, какие сегменты должны гореть, чтобы высветить требуемый символ. Комбинацию удобно составлять по схеме и записывать в бинарном виде: 8'b00111111
. Единичка соответствует горящему сегменту. Но индикатор у нас с общим анодом и работает по инверсной логике, потому в модуле перед этими константами стоит значок "~" (тильда). Тильда говорит компилятору, что значение должно быть побитно инвертировано.
Вставляем код модуля seven по тексту ниже основного модуля (ниже – это значит после слова endmodule
). Убеждаемся, что проект успешно компилируется. Пробуем загрузить полученный rbf-файл в ПЛИС и видим, что ничего не происходит. И действительно, модуль написали, а встроить его в основной модуль забыли. Добавляем в модуль leso2_demo два экземпляра модуля seven:
seven hg0 (.code_i(sb_i[3:0]), .hg_o(hg0_o));
seven hg1 (.code_i(sb_i[7:4]), .hg_o(hg1_o));
В первой строке создали экземпляр hg0
, по названию очевидно, что он отвечает за первый (младший) шестнадцатеричный разряд индикатора. На вход модуля code_i
подали младшие 4 бита шины sb_i[3:0]
, выход подключили к порту hg0_o
. Вторая строка создает экземпляр модуля для старшего разряда индикатора. Управляется старшими четырьмя тумблерами.
Компилируем, загружаем в ПЛИС. Теперь введенное тумблерами двоичное значение высвечивается в шестнадцатеричном коде на индикаторах. При этом, светодиоды все также показывают введенную комбинацию.
6. Проверяем тактовый генератор
Самое первое, что приходит в голову для проверки работоспособности генератора тактовых импульсов, - это вывести сигнал с генератора на светодиод, но частота 50МГц слишком велика, что бы увидеть ее невооруженным глазо. Частоту можно поделить с помощью счетчика. Самый элементарный двоичный счетчик на языке Verilog описывается достаточно просто:
always @ (posedge clk_50MHz_i)
count <= count + 1;
Читается код примерно так: каждый положительный фронт сигнала на линии clk_50MHz_i
приведет к увеличению переменной count (пока будем называть ее переменной, по аналогии с другими языками программирования) на единицу. Вроде как все просто. Вот только какой разрядности должна быть переменная count? Давайте разберемся. За каждый такт задающего генератора значение младшего разряда переменной count меняется на противоположное. Значит, частота изменения младшего разряда будет в два раза меньше частоты задающего генератора. Следующий разряд еще в два раза медленнее меняет свое значение, и так далее. То есть, для того, что бы разделить частоту в два раза, нам достаточно однобитного count. Каждый дополнительный разряд делит частоту еще в два раза. Для комфортного восприятия мигания светодиода будет достаточно частот около 1-10 Гц. Таким образом, исходную частоту в 50МГц нужно поделить в 50 000 000 раз! Ориентировочно для этого нам понадобится: log250000000 ≈ 26 разрядов. Выводить будем не на один светодиод, а на все сразу. Потому рекомендую выбрать значение с запасом: возьмем 32.
Теперь мы должны объявить переменную count
соответствующим образом. Вспоминаем, что мы работаем не с классическим языком программирования, а с языком описания аппаратуры, потому все переменные в Verilog представляют собой некоторые синтезируемые аппаратные сущности. Пока нам достаточно знать две такие сущности – это wire (линия, провод) и reg (регистр). Регистр, в отличие от линии, способен хранить значение и по сути компилятором синтезируется как набор триггеров (как правило, но далеко не всегда). В счетчике переменная count
должна быть объявлена как reg
, так как между фронтами задающего генератора хранит свое значение. И, с этого момента, count
будем называть не переменной, а регистром. Объявляется так:
Теперь остается назначить вывод старших восьми бит регистра count на светодиоды:
assign led_o = count[31:24];
Естественно, предыдущее назначение, где мы на светодиоды назначили тумблеры, придется удалить.
Компилируем, загружаем, наблюдаем. Если не забыли в "Pin Planer" входу clk_50MHz_i
назначить ножку Pin_23, то светодиоды начали мигать, демонстрируя работу двоичного счетчика. Тумблеры по прежнему управляют семисегментными индикаторами.
7. Тактовая кнопка
Осталось добавить в работу устройства кнопку. Изучив схему, приходим к выводу, что кнопка подключена к Pin_52 и при нажатии генерирует логический ноль. Реализуем такую логику: если кнопка не нажата, то на светодиоды и индикатор выводится код, устанавливаемый тумблером, если кнопка нажата, то на светодиоды и индикатор подаются данные со старших разрядов счетчика.
Введем вспомогательную переменную, назовем ее, например, sourse. Переменная будет выполнять роль шины, поступать на вход модулей seven и управлять светодиодами. С помощью кнопки на эту шину будем коммутировать либо тумблеры, либо старшие разряды выхода счетчика. Переменная sourse не должна хранить данные, потому должна быть объявлена как wire
:
На языке Verilog коммутировать две шины в одну удобно с помощью тернарного оператора выбора:
assign sourse = sw_i ? sb_i : count[31:24];
Если значение sw_i
истина (а это логическая единица, значит, кнопка не нажата), то на шину sourse коммутируются шина sb_i
, в противном случае (sw_i
равно нулю, кнопка нажата) коммутируется значения регистра count
. Незабываем назначить на светодиоды и на входа seven
шину sourse
:
assign led_o = sourse;
seven hg0 (.code_i(sourse[3:0]), .hg_o(hg0_o));
seven hg1 (.code_i(sourse[7:4]), .hg_o(hg1_o));
Компилируем, загружаем, проверяем. Если нажать кнопку SW1, то на индикаторах и светодиодах меняются значения. При этом, счетчик продолжает считать, даже если кнопка не нажата, потому при нажатии, счет начинается не с нуля, а с какого-то значения. При желании это легко исправить, добавив в счетчик условие сброса:
always @ (posedge sw_i or posedge clk_50MHz_i)
if(sw_i)
count <= 0;
else
count <= count + 1;
Логика оператора if( ) … else
, такая же как и в классических языках программирования. Теперь, при нажатии кнопки, счетчик начинает считать с нуля.