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

- цена не самолётная;
- плата должна иметь разъёмы, на которые выведены ноги ПЛИС в количестве не менее 30-ти штук (желательно, что бы разъёмы были расположены так, что бы на них можно было установить мезонинную плату);
- кристалл должен быть заведомо больше, чем мне нужно для реализации проекта (весьма трудновыполнимое условие, ибо я представления не имею, сколько попугаев мне может понадобиться).

И вот всё это добро лежит у меня на столе. Что с ним делать дальше? Для начала разберёмся, чем мы располагаем. Сама ПЛИС 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).

В файле пишем простейший код (да, базовый синтаксис 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`е.

Что бы залить её в чип есть три основных пути. Опишу их по очереди (да, считаю, что дрова для программатора уже стоят).
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 напротив файла конфигурации;
- Жмём старт и ждём, пока зашьётся флешка.
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
Я только одного не понимаю, в том же Quartus можно нарисовать все лог.схемы, задать нужные ножки и работать. Зачем программировать? это ведь не МК, ПЛИС по сути «болванка» в которой реализуются лог.функции.
Если проект состоит из различных модулей (к примеру юарт и декодер на семисегментный индикатор с дин. индикацией), то я каждый модуль программирую, создаю из них символьные файлы и соединяю в схемном редакторе.
- hellraiser
- 15 марта 2011, 23:22
- ↑
- ↓
Посмотрите проекты Юрия на сайте Марсоход. Я в них никогда не мог разобраться, это самый жестокий пример Передаем Ethernet-пакет
- hellraiser
- 15 марта 2011, 23:45
- ↑
- ↓
Да и вообще — код поддерживать проще. Вот например, хочешь ты понять, какая разница между версией 1.1 и 1.2. По коду даже примитивный diff тебе наглядно покажет, что изменилось, а вот сравнивать схемы ты замучаешься. Даже если найдётся какая-нибудь софтина, заточенная на сравнение схем в нужном тебе формате, далеко не факт, что ты сможешь прикрутить её к клиенту SVN/git/darcs/etc (нужное подчеркнуть).
Может и лучше, но Quartus не переваривает русский, а я недолюбливаю транслит. Вообще-то я код пишу в Code::Blocks, а в квартусе только компилю, но иногда по неосторожности открываю в нём исходники… В общем, пару раз я так из-за невнимательности уже убил комментарии… =)
Впрочем, для статьи это, наверно, не важно. Попозже переведу.
Впрочем, для статьи это, наверно, не важно. Попозже переведу.
Странно, у меня с русским все в порядке, но я сразу в квартусе пишу.
- hellraiser
- 16 марта 2011, 00:07
- ↑
- ↓
Хм… А какая система? у меня квартус 10.1 на WinXP-64En и на Win7-32En.
В самом квартусе русские буквы просто не печатаются. Если открыть файл с русским — отображаются вопросы. Если его потом сохранить, то русские буквы вопросами и заменяются.
Как выбрать кирилицу в настройках шрифтов я вообще не нашёл.
В самом квартусе русские буквы просто не печатаются. Если открыть файл с русским — отображаются вопросы. Если его потом сохранить, то русские буквы вопросами и заменяются.
Как выбрать кирилицу в настройках шрифтов я вообще не нашёл.
Квартус 8.1, WinXP-32Ru, даже не знаю что посоветовать, никогда с таким не сталкивался.
- hellraiser
- 16 марта 2011, 09:48
- ↑
- ↓
Наткнулся вот на electronix — пишут, что эта проблема появилась в десятой версии квартуса и что обещают в 11.1 починить.
electronix.ru/forum/index.php?showtopic=88904
electronix.ru/forum/index.php?showtopic=88904
Так, значит, квартусу не всё равно на Initial, да?
Надо будет проверить
Надо будет проверить
- PPetrovich
- 11 октября 2011, 11:03
- ↓
Отношение Квартуса к инитиалу непредсказуемо. Иногда обрабатывает, а иногда игнорирует. Поэтому стараюсь при переносе из Моделсим несинтезируемые вещи просто убирать.
Именно из-за того, что Verilog кроме синтезируемых конструкций имеет огромное подмножество для проведения моделирования, я бы его изучал в изначально Modelsim-е (как, например, здесь: fpga.in.ua/category/fpga/cad-pld/verilog-basics-laboratory-works), не отвлекаясь на синтез, назначение пинов и т.п., а уже бы потом переходил к синтезирующим САПР.
Комментарии (23)
RSS свернуть / развернуть