Освоение ПЛИС с использованием языка Verilog

Давненько мне приходили мысли о том, что засиделся я на платформе AVR и пора бы уже освоить что-то новое для себя. В качестве кандидатур рассматривались AVR32, ARM и FPGA. И вот, когда на горизонте замаячил новый проект, я стал думать, на чём бы его сделать. В принципе, проект этот можно реализовать на любой платформе, но желание расширить кругозор победило и было принято решение попробовать поработать с ПЛИС. Далее я с практически нулевыми знаниями в этой области погрузился в выбор камня для проекта. Почесав репу и прочесав инет я решил, что делать схему самому без опыта работы с этими штуками не стоит, а лучше найти подходящую стартёр плату. Условия были такие:
  1. цена не самолётная;
  2. плата должна иметь разъёмы, на которые выведены ноги ПЛИС в количестве не менее 30-ти штук (желательно, что бы разъёмы были расположены так, что бы на них можно было установить мезонинную плату);
  3. кристалл должен быть заведомо больше, чем мне нужно для реализации проекта (весьма трудновыполнимое условие, ибо я представления не имею, сколько попугаев мне может понадобиться).
В конечном итоге через eBay был приобретён набор разработчика на основе FPGA Altera CycloneIII. Набор включает плату, на которой установлена ПЛИС с базовой обвязкой (конфигурационная флешка, банка памяти, кварц на 50МГц, преобразователи питания), мезонинную плату расширения (всякая байда, типа кнопочек, светодиодиков, разъёмов) и программатор USB Blaster (собранный, кстати, на основе ПЛИС MAX 3000A и драйвера FT245).
Фото девелопмент-кита на базе ПЛИС Altera Cyclone III
И вот всё это добро лежит у меня на столе. Что с ним делать дальше? Для начала разберёмся, чем мы располагаем. Сама ПЛИС Altera CycloneIII EP3C25Q240 является типичным представителем FPGA начального уровня — на борту пачка логических элементов (чуть меньше, чем 25k, на что указывает название), 75 кб ОЗУ, 66 восемнадцати битных умножителей, 4 блока PLL и 148 ножек ввода-вывода. И всё — никакой привычной нам по микроконтроллерам тучи аппаратных интерфейсов и девайсов. Даже ПЗУ нет. Кстати, вывода RESET тоже нет. Грустно? Да ни сколько. Зачем нужна переферия в контроллерах? В первую очередь оно делается не для того, что бы не парить программиста с реализацией стандартных боков, а для того, что бы не загружать ядро лишней работой, обрабатывать данные параллельно с работой других блоков. А в ПЛИСе это и так работает параллельно, так за чем же городить аппаратные блоки, которые во многих случаях будут болтаться бесполезным грузом, занимая ценное место, если их не хуже можно реализовать программно. Есть, конечно, исключения: например ADC/DAC, но для этого зачастую даже лучше использовать отдельные микросхемы. А отсутствие флешки объясняется тем, что для хранения полной конфигурации для более-менее крупной ПЛИС требуется порядочно места, встраивать всё в ПЛИС накладно. Кроме того, это позволяет при необходимости конфигурять ПЛИС на лету дополнительным контроллером. Ну и не забываем об ограниченности циклов перезаписи FLASH и EEPROM.
На базовой плате кроме самой микросхемы FPGA, из интересного нам, расположены ещё кварц на 50МГц, пара светодиодов (питание, активность), кнопка RESET (угу, подключена к обычной ноге ввода вывода, как, впрочем, и светодиод активности), регуляторы напряжения на 1.2, 3.3 и 5 вольт, конфигурационная флешка и пачка разъёмов (в том числе два порта программирования, но об этом чуть ниже). Программатор умеет работать по JTAG и ActiveSerial. Ну а что может плата расширения в общих чертах видно по картинке =).
Осталась самая малость — оживить всё это дело. Где взять Quartus II и как с ним работать мы уже знаем. Там, правда, как мне показалось, маловато-то внимания уделено вопросу заливки кода в кристалл и ещё паре важных мелочей — попробую это исправить.
Создаём новый проект и файл для исходника (только на этот раз не VHDL, а Verilog HDL).
Quartus II, создание проекта
В файле пишем простейший код (да, базовый синтаксис Verilog мы тоже уже знаем):


module root (
	input wire [7:0]key,
	input wire [7:0]dip,
	output wire [7:0]led
);

assign led = ~(key & dip);

endmodule


Сохраняем файл, назначаем его топ-левелом и компилируем (Ctrl + L). Теперь нам надо указать на какой ноге что висит. Заходим в Pin Planner (Ctrl + Shift + N) и в списке снизу, или слева прописываем номера пинов для каждой цепи и выставляем для них стандарт. Заодно объявим ножку beep, на которой висит пищалка, и укажем ей в поле «Reserved» что-нибудь типа «As output driving ground», что б не свистела без дела (хотя можно и другим путём пойти — включить заземление для всех неиспользуемых ног через Assigments->Device...->Device and Pin Options...->Unused Pins). Так же не забываем указать, что у нас 3.3-вольтовая логика (Assigments->Device...->Device and Pin Options...->Voltage, в поле Default I/O standart выбираем 3.3-V LVTTL). После этого снова компилируем. Зачем две компиляции? Дело в том, что пока мы не скомпилируем наш модуль, среда не знает, какие у нас есть линии и, соответственно, не показывает их в Pin Planner`е. Можно, конечно, их и руками забить, но проще лишний раз скомпилировать. Кстати, раз уж зашла речь, в меню Assigments есть ещё Assigment Editor, который позволяет более тонко настраивать порты и инструменты для сохранения и восстановления конфигурации, что весьма удобно при работе с демо платами.
Теперь у нас есть откомпилированная схема, готовая к употреблению. Разглядеть её в подробностях можно с помощью утилит группы Tools->Netlist Viewers, а так же в Chip Planner`е.
Quartus II, просмотр синтезированной схемы
Что бы залить её в чип есть три основных пути. Опишу их по очереди (да, считаю, что дрова для программатора уже стоят).
1) Запись прошивки непосредственно в память FPGA через JTAG.
Самый простой метод — просто заливаем конфигурацию в энергозависимую память FPGA. Пока питание есть программа работает, питание пропало — нужно заливать заново. Зато не изнашиваем FLASH/EEPROM. Делается следующим образом: подключаем программатор к разъёму JTAG на плате, запускаем утилиту программирования (Tools -> Programmer), убеждаемся, что выбран нужный программатор, девайс и файл конфигурации (.sof), при необходимости меняем настройки/делаем Autodetect, ставим галочку Program/Configure напротив файла конфигурации и жмём Start. Готово.
2) Запись прошивки в конфигурационную флешку через ActiveSerial.
В этом случае мы заливаем конфигурацию не в FPGA, а в установленную на плате микросхему флешь-памяти. FPGA при подаче питания сама обращается к флешке и заливает в себя конфигурацию (кстати, для справки: ActiveSerial — это по сути обычный SPI). Тут махинации посложнее:
  • Запускаем утилиту конвертирования (File -> Convert Programming Files...);
  • В поле Programming file type выбираем Programmer Object File (.pof);
  • В поле Configuration device выбираем тип конфигурационной флешки (в моём случае на плате стоит микросхема 25P16 от ST, что соответствует альтеровской EPCS16);
  • По желанию задаём имя и путь для выходного файла;
  • В разделе Input files to convert жмём Add File… и выбираем файл конфигурации (.sof);
  • Жмём Generate;
  • Закрываем утилиту конвертирования и запускаем утилиту программирования (Tools -> Programmer);
  • Подключаем программатор к разъёму ActiveSerial на плате;
  • В поле Mode выставляем Active Serial Programming;
  • Жмём Add File и выбираем созданный (.pof);
  • Ставим галочки Program/Configure и Verify напротив файла конфигурации;
  • Жмём старт и ждём, пока зашьётся флешка.
После зашивки FLASH ПЛИС сама загрузит конфигурацию и начнёт работать.
3) Запись прошивки в конфигурационную флешку через JTAG.
Что бы избавиться от разъёма ActiveSerial и не перетыкать периодически программатор бравые работники фирмы Altera придумали как вырезать гланды через… ээ… заливать образ во флешь через JTAG. Суть в том, что мы заливаем в память FPGA бутлоадер, который загружает наш файл конфигурации во флешку, после этого при перезагрузке ПЛИСа конфигурация бутлоадера теряется, а FPGA грузится с флешки так же как в предыдущем случае. Шаги примерно теже, с небольшими изменениями:
  • Запускаем утилиту конвертирования (File -> Convert Programming Files...);
  • В поле Programming file type выбираем JTAG Indirect Configuration File (.jic);
  • В поле Configuration device выбираем тип конфигурационной флешки;
  • По желанию задаём имя и путь для выходного файла;
  • В разделе Input files to convert выделяем строку Flash Loader, жмём Add Device… и выбираем нашу ПЛИСину;
  • В разделе Input files to convert выделяем строку SOF Data Page_0, жмём Add File… и выбираем файл конфигурации (.sof);
  • Жмём Generate;
  • Закрываем утилиту конвертирования и запускаем утилиту программирования (Tools -> Programmer);
  • Подключаем программатор к разъёму JTAG на плате;
  • В поле Mode выставляем JTAG;
  • Если список файлов пуст жмём Add File (если что-то уже есть — просто дважды тыкаем на существующую строчку) и выбираем созданный (.jic);
  • Ставим галочки Program/Configure и Verify напротив файла конфигурации;
  • Жмём старт и ждём, пока зашьётся флешка.
  • Передёргиваем питание FPGA, что бы очистить память.
Наша конфигурация начнёт работать только перезагрузки ПЛИС (удаления конфигурации лоадера).

Ну вот, светодиодиками помигали, можно сказать электронщицкий «Hello world» запустили, теперь можно и чем-то посерьёзнее заняться.
Как я уже отмечал выше, почти никакой периферии на ПЛИС нет, но данные то передавать как-то надо. Так что для освоения концепции программирования ПЛИС предлагаю реализовать простейший драйвер UART. Простейший — значит работа по-байтно и никаких настроек — жёсткие 9600,8,n,1. Передача — набираем байт DIP-переключателями, жмём кнопочку, байт отсылается компу. Приёмник — шлём с компа байт он принимается и отображается на светодиодах. Ниже приведён код с построчными комментариями.
Внимание! Код только для демонстрации принципов, для использования в реальном проекте необходима основательная адаптация.


module root (
	input wire clk_50M,		//внешняя тактовая частота
	input wire [7:0]key,		//кнопки
	input wire [7:0]dip,		//dip-переключатели
	output reg [7:0]led,		//светодиоды
	input wire uart_rx,		//приёмник UART
	output reg uart_tx		//передатчик UART
);
parameter MAIN_CLK = (50_000_000);	//50MHz - внешня тактовая
parameter UART_CLK = (9600);		//скорость UART

///переменные, относящиеся к передатчику
reg clk_9600;				//частота для UART - 9600Hz
reg [9:0]uart_tx_reg = 10'b1xxxxxxxx0; //инициализация регистра UART (StartBit - 0, StopBit - 1, биты данных - не важно)
reg [3:0]uart_tx_cur_bit =4'b0;		//номер передаваемого бита
reg uart_tx_send = 0;			//вход для запуска передачи данных
reg uart_tx_send_pulse = 0;		//импульс, по которому начинается передача данных
reg uart_tx_busy = 0;			//состояние передатчика (1 - идёт передача, 0 - простой)
///переменные, относящиеся к приёмнику
reg [7:0]uart_rx_byte =8'b0;		//регистр для хранения принятого байта
reg [3:0]uart_rx_cur_bit =4'b0;		//индекс считываемого бита
reg uart_rx_data_ready = 0;		//импульс, оповещающий о завершении приёма данных
reg uart_rx_busy = 0;			//состояние приёмника (1 - идёт приём, 0 - простой)

initial begin				//начальная установка (не верьте тем, кто говорит, что initial работает только для симуляции)
	uart_tx = 1;				//установка линии передатчика в 1 (IDLE) при старте
end

always @(posedge clk_50M) begin : send_byte
/*Здесь должен быть код для фильтрации дребезга контактов*/
	uart_tx_send = ~key[7];			//устанавливаем uart_tx_send в 1 когда нажата крайняя кнопка и в 0 - когда отжата
end


always @(posedge clk_50M) begin : baud_rate_generator
	reg [13:0]clkr_9600 = 14'b0;		//счётчик генератора частоты для 9600Гц
	clkr_9600 = clkr_9600 + 14'b1;		//инкремент счтчика
	if(clkr_9600 == (MAIN_CLK/UART_CLK/2)) begin
		clkr_9600 = 14'b0;		//сброс счётчика
		clk_9600 = ~clk_9600;		//формирование фронта частоты 9600Гц
	end
end

always @(posedge uart_tx_send_pulse) begin : init_uart_tx_buf
	uart_tx_reg[8:1] = dip;			//запись отправляемого значения в uart rx register
end

always @(negedge clk_9600) begin : uart_tx_start_pulse_generator //генератор старт-импульса, работа по отрицательному фронту, что бы во время положительного находиться  гарантированном состоянии 1, или 0
	reg uart_tx_send_prev = 0;		//временная переменная для поиска фронта uart_tx_send
	if((uart_tx_send_prev == 0) && (uart_tx_send == 1)) begin //если найден положительный фронт
		uart_tx_send_pulse <= 1;	//формируем положительный фронт uart_tx_send_pulse
	end
	if(uart_tx_send_pulse == 1)
		uart_tx_send_pulse <= 0;	//в следующем цикле частоты 9600 формируем отрицательный фронт uart_tx_send_pulse
	else
		uart_tx_send_prev = uart_tx_send;	//сохраняем состояние uart_tx_send только если не формировали только что импульс
end

always @(posedge clk_9600) begin : uart_transmitter
	if(uart_tx_cur_bit > 9) begin : uart_tx_done			//байт отправлен, переходим в ждущее состояние
		uart_tx = 1;						//установка линии передатчика в 1 (IDLE)
		uart_tx_cur_bit = 0;
		uart_tx_busy = 0;
	end else if(uart_tx_busy == 1) begin : uart_tx_set_cur_bit	//если идёт передача
		uart_tx = uart_tx_reg[uart_tx_cur_bit];			//выставляем следующий бит
		uart_tx_cur_bit = uart_tx_cur_bit + 4'b0001;		//увеличиваем значение счётчика бит
	end else if(uart_tx_send_pulse == 1) begin : uart_tx_start	//если требуется передать данные
		uart_tx_busy = 1;					//начинаем передачу на следующем такте частоты 9600
	end
end


always @(posedge clk_9600) begin : uart_receiver
	if((uart_rx_busy == 0)&&(uart_rx == 0)) begin : uart_new_byte	//если не в состоянии приёма данных и линия rx не в IDLE
		uart_rx_busy = 1;					//переходим в состояние приёма данных
		uart_rx_cur_bit = 0;					//устанавливаем счётчик принятых бит в 0
	end else if (uart_rx_cur_bit < 8) begin
		uart_rx_byte[uart_rx_cur_bit] = uart_rx;		//сохраняем текущий бит в регистр
		uart_rx_cur_bit = uart_rx_cur_bit + 4'b0001;		//увеличиваем счётчик считанных бит
	end else begin
		uart_rx_busy = 0;					//данные считаны, переходим в режим ожидания (тут должна быть проверка стоп-бита)
	end
end

always @(negedge clk_9600) begin : uart_rx_data_ready_pulse_generator
	if(uart_rx_cur_bit == 4'd8) begin	//если байт полностью принят
		uart_rx_data_ready <= 1;	//формируем положительный фронт uart_rx_data_ready
		led = uart_rx_byte;		//отображаем считанный байт на светодиодах
	end
	if(uart_rx_data_ready == 1)
		uart_rx_data_ready <= 0;	//в следующем цикле частоты 9600 формируем отрицательный фронт uart_rx_data_ready
end

endmodule



Кстати, меня тут поджидала засада — проект не компилируется с ошибками типа «Can't place multiple pins assigned to pin location Pin_xx». Дело в том, что автор платы сделал весьма странную разводку — RX и TX com-порта посажены аккуратно на те немногие ноги, которые имеют двойное назначение, а именно драйвер ActiveSerial, что можно увидеть, считав конфигурацию пинов из прошивки, собранной без назначения чего либо на эти ноги (Assigments->Back-Annotate Assigments...). Что бы обойти эту неприятность придётся отключить функции ActiveSerial для этих выводов. Для этого идём в Assigments->Device...->Device and Pin Options->Dual-Purpose Pins и выставляем для Data[1]/ADSO и FLASH_nCE/nCSO значения «Use as regular I/O». Приведённая махинация, кстати говоря, совершенно не мешает шить прошивку вторым и третьим способами, так что мы ничего не теряем.
Основная сложность при описании подобных схем заключается в том, нельзя управлять одним сигналом из двух разных always блоков. То есть, например, так просто нельзя выставить uart_tx_busy в единицу когда появился фронт uart_tx_send и сбросить в ноль когда передан последний бит, если они находятся не в пределах одного блока.
Код работает следующим образом:
ПЕРЕДАТЧИК
  • при нажатии на кнопку key[7] считываются состояния линий dip и выставляется сигнал готовности данных к отправке;
  • в блоке uart_tx_start_pulse_generator формируется импульс длиной 1/9600 = 104мкс таким образом, что положительный фронт частоты УАРТа попадает на середину этого импульса;
  • блок uart_transmitter в случае, если в данный момент не занят передачей данных, анализирует уровень uart_tx_send_pulse и, если там единица, выставляет флаг busy и начинает передавать данные;
  • блок uart_transmitter последовательно передаёт 10 бит, после чего выставляет флаг busy и возвращает линию uart_tx в единицу;
  • после отпускания кнопки система готова к повторному срабатыванию.
ПРИЁМНИК
  • блок uart_receiver, если не занят приёмом данных, анализирует линию uart_rx и, если она опустилась в 0 выставляет флаг busy, сигнализируя о начале приёма данных;
  • блок uart_receiver принимает последовательно 8 бит, после чего снимает флаг busy;
  • блок uart_rx_data_ready_pulse_generator смотрит, за счётчиком бит и когда принялся последний бит посылки отображает весь принятый байт на светодиодах и формирует импульс uart_rx_data_ready, который может использоваться логикой производящей дальнейшую обработку данных.

Ну для начала сойдёт, но для работы с реальным уартом надо иметь отдельные счётчики для приёмника и передатчика и счётчик приёмника сбрасывать перед приёмом каждого нового байта для компенсации разницы частот приёмника и передатчика. Но эти нюансы оставим на потом.

  • +5
  • 15 марта 2011, 22:31
  • Alatar

Комментарии (23)

RSS свернуть / развернуть
Я только одного не понимаю, в том же Quartus можно нарисовать все лог.схемы, задать нужные ножки и работать. Зачем программировать? это ведь не МК, ПЛИС по сути «болванка» в которой реализуются лог.функции.
0
  • avatar
  • pkm
  • 15 марта 2011, 23:03
Каждый выбирает свою методику. Я например не люблю рисовать. Ну и в больших проектах разных кросс-связей настолько много, что они превращаются в кашу и не воспринимаются…
А так Вы правы, все можно нарисовать.
0
Писать код гораздо проще, чем рисовать схемы. Особенно когда его много.
Лично я, например, потрачу значительно больше времени, что бы нарисовать тот же UART, а через пару месяцев ещё и разобраться в этом рисунке не смогу.
Структура да — рисуется наглядно, а вот нарисовать алгоритм сложнее.
0
Нарисовать ты можешь что то не сложней тупого сумматора. А попробуй нарисовать лог схему конечного автомата того же УАРТА или ииц. да ты опухнешь, получишь жуткого паука, а если что придется править, то проще уж снести все и заново с нуля сделать.
0
лог схему конечного автомата мучался но нарисовал. в общем согласен что все-таки код написать практичние будет.
0
Если проект состоит из различных модулей (к примеру юарт и декодер на семисегментный индикатор с дин. индикацией), то я каждый модуль программирую, создаю из них символьные файлы и соединяю в схемном редакторе.
0
Я писал генетический алгоритм на плис, обрабатывал параллельно по 64 хромосомы, получалось что у меня были шина например 64x64, т.е. двумерные массивы или массивы массивов. Рисовать такие соединения, как я понял вообще нельзя, по-этому делалось все ручками :)
0
Посмотрите проекты Юрия на сайте Марсоход. Я в них никогда не мог разобраться, это самый жестокий пример Передаем Ethernet-пакет
0
Да и вообще — код поддерживать проще. Вот например, хочешь ты понять, какая разница между версией 1.1 и 1.2. По коду даже примитивный diff тебе наглядно покажет, что изменилось, а вот сравнивать схемы ты замучаешься. Даже если найдётся какая-нибудь софтина, заточенная на сравнение схем в нужном тебе формате, далеко не факт, что ты сможешь прикрутить её к клиенту SVN/git/darcs/etc (нужное подчеркнуть).
0
Попробуйте схематично реализовать любой цифровой фильтр порядка эдак 7. Или БПФ, например :)
0
Рисовать имеет смысл только если Вы собираете Ваш проект из готовых мегафункций. Ну или можно допустим файл верхнего уровня иерархи сделать в схемном редакторе — определить места для основных узлов, развести пины, а пользовательские модули описывать на верилоге и подключать в виде символов.
0
Может комментарии в программе лучше на русском делать.
0
Может и лучше, но Quartus не переваривает русский, а я недолюбливаю транслит. Вообще-то я код пишу в Code::Blocks, а в квартусе только компилю, но иногда по неосторожности открываю в нём исходники… В общем, пару раз я так из-за невнимательности уже убил комментарии… =)
Впрочем, для статьи это, наверно, не важно. Попозже переведу.
0
Странно, у меня с русским все в порядке, но я сразу в квартусе пишу.
0
Хм… А какая система? у меня квартус 10.1 на WinXP-64En и на Win7-32En.
В самом квартусе русские буквы просто не печатаются. Если открыть файл с русским — отображаются вопросы. Если его потом сохранить, то русские буквы вопросами и заменяются.
Как выбрать кирилицу в настройках шрифтов я вообще не нашёл.
0
Квартус 8.1, WinXP-32Ru, даже не знаю что посоветовать, никогда с таким не сталкивался.
0
Ну, скорее всего, проблема в том, что винда английская…
0
Наткнулся вот на electronix — пишут, что эта проблема появилась в десятой версии квартуса и что обещают в 11.1 починить.
electronix.ru/forum/index.php?showtopic=88904
0
Так, значит, квартусу не всё равно на Initial, да?
Надо будет проверить
0
Да, квартус в большинстве случаев отрабатывает initial, но на 100% рассчитывать на это не стоит — иногда он не справляется.
0
Отношение Квартуса к инитиалу непредсказуемо. Иногда обрабатывает, а иногда игнорирует. Поэтому стараюсь при переносе из Моделсим несинтезируемые вещи просто убирать.
0
Именно из-за того, что Verilog кроме синтезируемых конструкций имеет огромное подмножество для проведения моделирования, я бы его изучал в изначально Modelsim-е (как, например, здесь: fpga.in.ua/category/fpga/cad-pld/verilog-basics-laboratory-works), не отвлекаясь на синтез, назначение пинов и т.п., а уже бы потом переходил к синтезирующим САПР.
0
Кстати, вывода RESET тоже нет.
как это нет? а воон та нога DEV_CLRn? это глобальный ресет и есть. другое дело, что можешь его использовать или нет, но он есть.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.