Пишем "демку" для LESO2 на Verilog

 

Шауэрман Александр А. 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-ми битную шину и описать так:

output [7:0] led_o, 

Думаю, очевидно, что "[7:0]" – разрядность шины. Обращаю внимание на название "led_o". Удобно к входным портам добавлять постфикс "i", от английского "input", а к выходным постфикс "o", соответственно, от английского "output". Если порт предназначен для ввода и вывода, то постфикс будет "io". Теперь встретив в тексте программы переменную с названием "led_o" сразу станет понятно, что это порт вывода из модуля, и отвечает за светодиоды.

По аналогии, тумблеры опишем как:

input [7:0] sb_i,

Тактовую кнопку SW1:

input sw_i,

В названии порта ввода для тактовых импульсов укажем, что это именно тактовые импульсы ("clk" – от "clock"), и частота этих импульсов равна 50 МГц:

input clk_50MHz_i,

С семисегментным индикатором посложнее, он сдвоенный, потому удобнее каждую цифру описать отдельно, например так:

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. Простейшая схема

Настало время описать какую-нибудь схему. Самое простое, что приходит в голову, это перенаправить состояние с тумблеров на светодиоды. Делается это одной строкой:

assign led_o = sb_i;

Строку нужно поместить между описанием портов модуля и ключевым словом 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 будем называть не переменной, а регистром. Объявляется так:

reg [31:0] count;

Теперь остается назначить вывод старших восьми бит регистра count на светодиоды:

assign led_o = count[31:24];

Естественно, предыдущее назначение, где мы на светодиоды назначили тумблеры, придется удалить.

Компилируем, загружаем, наблюдаем. Если не забыли в "Pin Planer" входу clk_50MHz_i назначить ножку Pin_23, то светодиоды начали мигать, демонстрируя работу двоичного счетчика. Тумблеры по прежнему управляют семисегментными индикаторами.

7. Тактовая кнопка

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

Введем вспомогательную переменную, назовем ее, например, sourse. Переменная будет выполнять роль шины, поступать на вход модулей seven и управлять светодиодами. С помощью кнопки на эту шину будем коммутировать либо тумблеры, либо старшие разряды выхода счетчика. Переменная sourse не должна хранить данные, потому должна быть объявлена как wire:

wire [7:0] sourse;

На языке 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 , такая же как и в классических языках программирования. Теперь, при нажатии кнопки, счетчик начинает считать с нуля.

8 января 2016
Орфографическая ошибка в тексте:
Чтобы сообщить об ошибке автору, нажмите кнопку "Отправить сообщение об ошибке". Вы также можете отправить свой комментарий.