В первом проекте для платы M2RPI мы передавали данные в FPGA и обратно используя выводы Raspberry GPIO14 и GPIO15 как линии последовательного порта TxD и RxD.
Как быть, если нужно передавать больший объем и на больших скоростях?
Я попробовал сделать такой проект.
Весь проект состоит из двух частей: аппаратная часть - это проект для FPGA в среде Intel Quartus Prime Lite. Вторая часть - это программа на языке C, которая готовит и передает данные в ПЛИС через GPIO пины Raspberry. С чего начнем?
Весь проект можно будет взять на GITHUB: https://github.com/marsohod4you/Raspberry-to-FPGA
Рассмотрим программу на C.
Для работы с портами GPIO нужно получить доступ к регистрам контроллера GPIO. В файле rpi_gpio.cpp определена функция setup_rpi_gpio(), которая открывает файл /dev/mem и делает отображение портов ввода/вывода в адресное пространство моего процесса с помощью mmap().
Макросы для работы с отдельными пинами GPIO определены в файле rpi_gpio.h
Дальше программа инициализирует отдельные пины на вывод или на ввод. Я хочу реализовать 16-ти битную передачу данных в ПЛИС из Raspberry. Протокол передачи данных описывается следующим рисунком:
Я программирую GPIO12-GPIO27 на вывод - это моя шина данных. Еще один вывод GPIO4 я хочу использовать, как строб передачи - сигнал будет сообщать ПЛИС, что новые данные готовы и она может их забирать с шины данных.
Один пин GPIO2 я буду использовать на чтение. Я хочу, чтобы ПЛИС сообщала программе, что ей требуются новые данные. Для чего это нужно? Идея такая. Я делаю внутри ПЛИС приемное FIFO - приходящие данные из Raspberry приходят и записываются в FIFO. Поскольку я буду передавать данные из программы и она наверняка работает не равномерно, то и приходящий поток данных будет не равномерным. Неравномерность работы программы может быть связана с переключением задач в операционной системе, общей загруженностью системы, кешированием данных при доступе к ОЗУ и всякими другими причинами.
А вот из FIFO данные я хочу читать равномерно на фиксированной скорости без разрывов. Насколько это получится? Не знаю, давайте попробуем.
Итак, программирование режимов работы пинов GPIO в моей программе выглядит вот так:
setup_rpi_gpio();
......
//pin2 used as FIFO level signal
INP_GPIO(2);
//output data bus 16 bit
for(int i=0; i<16; i++)
{
OUT_GPIO(12+i);
}
//pin4 used as write strobe
OUT_GPIO(4);
После этого подготавливаю блок данных для передачи: 512 байт или, что то же самое 256 16-ти битных слов:
double t = 0;
const uint32_t len = 256;
uint8_t block[len*2];
//make data block
for(uint32_t i=0; i<len; i++)
{
block[i*2+0]=128+(int8_t)(127*sin( t )); t+=2.0*M_PI/256;
block[i*2+1]=i;
}
Здесь младший байт слова будет содержать сигнал синусоиды, а старший байт слова будет содержать сигнал "пилы".
Передача данных в плату будет выглядеть вот так (вечный цикл):
while(1)
{
//wait FPGA says need data
while(1)
{
//wait request from FPGA
uint32_t req = GET_GPIO(2);
if( req )
break;
}
//send block to fpga
send_block(block,len);
}
В цикле есть второй цикл, который опрашивает пин GPIO2. Таким образом, я жду, когда ФИФО в ПЛИС немного опустошится. Как только в ФИФО появится место для целого блока новых данных внутренний цикл прерывается и вызывается функция send_block(). Я хочу сделать ФИФО внутри ПЛИС глубиной 1024 слова. GPIO2 будет выставляться, когда в ФИФО будет менее 3/4 данных, то есть менее 768 слов.
Функция send_block() пожалуй самая мудреная.
void send_block(uint8_t* pdata, int len)
{
uint16_t* pblk = (uint16_t*)pdata;
uint32_t mask = 0xFFFF<<12;
for(int i=0; i<len; i++)
{
uint32_t word = (pblk[i])<<12;
uint32_t w_set = word | (1<<4);
uint32_t w_clr = word ^ mask;
GPIO_SET = w_set;
GPIO_CLR = w_clr;
GPIO_CLR = 1<<4;
}
}
Тут нужно немного сказать про макросы GPIO_SET и GPIO_CLR. Они определены в файле rpi_gpio.h вот так:
#define GPIO_SET *(gpio+7) // sets bits which are 1 ignores bits which are 0
#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0
Слово gpio - это указатель на 32-х битные порты ввода-вывода. Выражение *(gpio+7) обозначает запись управляющего слова в седьмой регистр. В процессорах broadcom, которые используются на платах Raspberry регистры на установку бит и на сброс бит разные. Это значит, чтобы передать новые данные потребуется не менее двух записей в регистры. Немного жаль, что это так, это значит, что скорость передачи будет меньше, чем могла бы быть. С другой стороны, есть и положительный момент. Получается можно устанавливать и сбрасывать только избранные биты. Если две независимые программы используют разные GPIO пины для своих нужнд, то они и мешать друг другу не будут.
Кстати говоря - это очень полезно в моих реальных экспериментах.
Я использую на Raspberry Pi3 сетевой JTAG программатор nw_jtag_srv. Этот программатор управляет GPIO0 (TMS), GPIO1 (TDO), GPIO7 (TCK) и GPIO11 (TDI). С помощью этого сервера я могу удаленно с ноутбука загружать ПЛИС и исследовать внутренние сигналы с помощью инструмента SignalTap.
Несмотря на то, что сервер JTAG забирает себе 4 пина, я свободно буду использовать еще 16 GPIO для своей шины данных и 2 GPIO для управления потоком.
Кстати про управление потоком.. К сожалению, я не придумал простого и надежного способа передавать и фиксировать данные за две операции записи в регистр GPIO. Пришлось использовать еще одну запись, и естественно скорость передачи от этого ожидаемо упала. У меня получается одно слово передается вот так:
GPIO_SET = w_set | (1<<4);
GPIO_CLR = w_clr;
GPIO_CLR = 1<<4;
Первая запись устанавливает биты, которые стоят в слове в единице и устанавливает GPIO4. Вторая запись очищает биты, которые должны быть в нуле. Третья запись сбрасывает GPIO4, что и есть событие записи.
Теперь о проекте в FPGA.
Есть 16 входных GPIO из Raspberry Pi3 и сигналы на этих контактах записываются в регистр по спадающему фронту (Verilog HDL):
wire [15:0]w_input_data;
assign w_input_data =
{
GPIO27,GPIO26,GPIO25,GPIO24,
GPIO23,GPIO22,GPIO21,GPIO20,
GPIO19,GPIO18,GPIO17,GPIO16,
GPIO15,GPIO14,GPIO13,GPIO12
};
wire gpio_clk; assign gpio_clk = GPIO4;
reg [15:0]r_input_data;
always @( negedge gpio_clk )
r_input_data <= w_input_data;
Данные из регистра r_input_data далее записываются в FIFO уже по фронту сигнала gpio_clk
data_fifo fifo_inst(
.data( r_input_data ),
.wrreq( 1'b1 ),
.wrfull( w_wrfull ),
.wrusedw( w_wrusedw ),
.wrclk( gpio_clk ),
.rdclk( w_clk ),
.rdreq( ~w_rdempty ),
.q( o_data ),
.rdfull( w_rdfull ),
.rdempty( w_rdempty ),
.rdusedw( w_rdusedw )
);
reg [15:0]r_data;
always @(posedge w_clk)
r_data <= o_data;
Чтение из FIFO идет постоянно, если оно не пустое. Чтение по тактовой частоте w_clk, которая у меня вырабатывается в PLL, здесь 20МГц.
Запрос на новые данные из Raspbbery формируется вот так:
reg [10:0]rd_fullness;
always @(posedge w_clk)
rd_fullness <= { w_rdfull,w_rdusedw };
//request new data from RPI
assign GPIO2 = rd_fullness < 16'd768;
Пин GPIO2 установится в единицу, когда в FIFO будет место для нового блока данных.
В проект я установлю модуль SignalTap, который позволит мне смотреть данные на выходе из FIFO. Так же я хочу посмотреть как с течением времени меняется заполненность FIFO данными и как формируется запрос на новые данные GPIO2.
Итак, на ноутбуке я компилирую проект ПЛИС в среде Intel Quartus Prime.
На распберри я компилирую свою программу командой make. Далее запускаю передачу данных в одном окне терминала и запускаю JTAG сервер в другом окне терминала:
С ноутбука из среды САПР Quartus Prime, с помощью инструмента SignalTap по сети Ethernet через JTAG сервер а плате распберри я загружаю проект в ПЛИС и смотрю сигналы на выходе FIFO.
Получается вот такая красивая картинка:
Видно, что поток на выходе FIFO равномерный, скорость чтения 20МГц словами по 16 бит.
Значит скорость передаваемого потока 40МБайт/секунду. Я когда-то занимался реализацией функции PCI в ПЛИС, так вот хорошо помню, что несмотря на частоту шины PCI 33МГц передавать даже 40Мбайт в секунду в режиме PCI-Target удавалось с трудом. Только PCI-master может передавать более 100Мбайт/сек. таким образом, я считаю достигнутый результат в 40Мб/сек вполне достойным.
На временных диаграммах SignalTap видны еще сигналы rd_fullness - это уровень наполнения FIFO в ПЛИС. Видно, что он немного плавает. Как только уровень наполнения фифо опускается ниже порога, устанавливается запрос новых данных GPIO2. Программа читает этот пин и если он установлен, то выполняет очередную передачу данных по шине.
Я думаю здесь еще есть место для оптимизации.
Если бы было чуть больше времени я думаю смог бы передавать одно слово не тремя записями в регистры, а двумя. Тогда и скорость передачи была бы заметно выше.
Тем не менее, я думаю этот проект будет интересен тем, кто реализует свои приложения для связки плат Raspberry Pi3 и Marsohod2RPI.
Подробнее...