Передача данных из Raspbbery Pi3 в FPGA платы Марсоход2RPI

Marsohod2RPI FPGA board, 40MBytes/sec

В первом проекте для платы 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. Протокол передачи данных описывается следующим рисунком:

data transfer protocol

Я программирую 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 сервер в другом окне терминала:

rpi desktop and JTAG server

С ноутбука из среды САПР Quartus Prime, с помощью инструмента SignalTap по сети Ethernet через JTAG сервер а плате распберри я загружаю проект в ПЛИС и смотрю сигналы на выходе FIFO.

Получается вот такая красивая картинка:

Intel Quartus SignalTap JTAG capture

Видно, что поток на выходе FIFO равномерный, скорость чтения 20МГц словами по 16 бит.
Значит скорость передаваемого потока 40МБайт/секунду. Я когда-то занимался реализацией функции PCI в ПЛИС, так вот хорошо помню, что несмотря на частоту шины PCI 33МГц  передавать даже 40Мбайт в секунду в режиме PCI-Target удавалось с трудом. Только PCI-master может передавать более 100Мбайт/сек. таким образом, я считаю достигнутый результат в 40Мб/сек вполне достойным.

На временных диаграммах SignalTap видны еще сигналы rd_fullness - это уровень наполнения FIFO в ПЛИС. Видно, что он немного плавает. Как только уровень наполнения фифо опускается ниже порога, устанавливается запрос новых данных GPIO2. Программа читает этот пин и если он установлен, то выполняет очередную передачу данных по шине.

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

Тем не менее, я думаю этот проект будет интересен тем, кто реализует свои приложения для связки плат Raspberry Pi3 и Marsohod2RPI.

 


Добавить комментарий