![](https://habrastorage.org/files/b70/6cc/1a7/b706cc1a7d9f4850a9c80499e73af770.png)
В компьютерах уже давно есть шина PCI Express v3.0 x16; Тесты современных видеоадаптеров показывают на этой шине скорость около 12 Гбайт/с. Хотелось бы сделать модуль на ПЛИС который обладает такой же скоростью. Однако доступные ПЛИС имеют HARDWARE контроллер только для PCIe v3.0 x8; Реализации SOFT IP Core есть, но очень дорогие. Но выход есть.
ПЛИС Virtex 7 VX330T имеет два контроллера PCI Express v3.0 x8; Очевидным решением является размещение коммутатора, который имеет на стороне разъёма x16 и две шины x8 которые подключены к ПЛИС. Получается вот такая структура:
![](https://habrastorage.org/files/d73/22a/839/d7322a839dbc4936a85e9d86e320ec4a.png)
По такой схеме построен модуль HTG-728 компании HighTechGlobal.
По другому пути идёт комания Alpha-Data. Модуль ADM-PCIE-KU3-X16 не имеет коммутатора. Но на разъём x16 выводятся две шины x8. В ПЛИС возможна реализация двух независимых контроллеров. Для этого на ПЛИС заведены два сигнала сброса и две опорных частоты. Но работать это модуль будет только в специальных системных платах, где так же на разъём x16 выводятся два x8. Таких системных плат я не встречал, но видимо они есть.
В нашей компании было принято решение по реализации модуля FMC122P с внутренним коммутатором. Главной задачей была проверка максимальной скорости обмена. Другой, не менее важной задачей, является достижение совместимости с существующим программным обеспечением и компонентами ПЛИС.
Контроллер PCI Express для Virtex 7 кардинально отличается от контроллеров для Virtex 6, Kintex 7. Он стал более удобным, но он другой. На рисунке представлена структурная схема контроллера:
![](https://habrastorage.org/files/fc4/e27/62c/fc4e2762c2604c9fac224c9604f06df4.png)
Контроллер имеет две части Completer и Requester, каждая из которых имеет две шины AXI_Stream. Через узел Completer приходят запросы со стороны шины PCI Express. Эти запросы передаются на шину m_axis_cq. По шине s_axis_cc должен прийти ответ со стороны User компонента. Обычно это узел доступа к внутренним регистрам ПЛИС.
Через узел Requester по шине s_axis_rq контроллер DMA посылает запросы на шину PCI Express. Ответы приходят через шину m_axis_rc.
Моделирование шины
В состав IP Core входит example проект по которому можно понять как это работает. Проект написан на Verilog и, к сожалению, он также может служить примером того как не надо разрабатывать. Давайте рассмотрим структурную схему примера.
![](https://habrastorage.org/files/77f/c0e/470/77fc0e470a7648059002632aeaca5da8.png)
Эта диаграмма взята из описания IP Core. На первый взгляд всё прекрасно – это замечательная картинка, её можно показать менеджерам, руководителям проектов, клиентам. Проблемы начинаются в реализации. В этой системе очень много мест где используется возможности Verilog для доступа к объектам по абсолютному пути. На мой взгляд, в данной системе это оправдано только в одном месте – это обход узлов GTP для моделирования на уровне PIPE. А вот делать связь между userapp_tx и userapp_rx с использованием абсолютных путей совершенно не нужно.
В проекте это выглядит так:
В компоненте pci_exp_usrapp_tx есть функция TSK_SYSTEM_INITIALIZATION которая вызывает через абсолютный путь функцию из pci_exp_userapp_cfg:
board.RP.cfg_usrapp.TSK_WRITE_CFG_DW (здесь и далее я называю функцией то что в Verilog описывается через task). Смотрим компонент pci_exp_userapp_cfg, что видим: cfg_ds_bus_number <= board.RP.tx_usrapp.RP_BUS_DEV_FNS[15:8];
Смотрим компонент pci_exp_userapp_rc, там тоже самое: board.RP.com_usrapp.TSK_PARSE_FRAME(`RX_LOG);
Это не только стилистически не правильно. Это мешает применить модель в своём проекте. Во первых совершенно не обязательно, что в собственном проекте файл верхнего уровня будет называться board и там останется та же самая иерархия. Во вторых может быть два компонента. У нас как раз произошли оба случая. Пришлось поработать с Verilog, хотя мне он совершенно не нравиться. Как оказалось, путём небольшой перестановки весь компонент root_port можно привести к полностью иерархическому виду. В итоге получились файлы компонентов:
- xilinx_pcie_3_0_7vx_rp_m2.v
- pci_exp_usrapp_tx_m2.v
- pci_exp_usrapp_cfg_m2.v
И файлы с функциями:
- task_bar.vh
- task_rd.vh
- task_s1.vh
- task_test.vh
Это позволило включить в модель два компонента root_port. В компоненте VHDL включение двух root_port выглядит так:
root_port
gen_rp0: if( is_rp0=1 ) generate
rp0: xilinx_pcie_3_0_7vx_rp_m2
generic map(
INST_NUM => 0
)
port map(
sys_clk_p => sys_clk_p,
sys_clk_n => sys_clk_n,
sys_rst_n => sys_rst_n,
-- Передача команд
cmd_rw => cmd_rw, -- Признак чтения-записи: 0 - чтение, 1 - запись
cmd_req => cmd_req, -- 1 - Запрос операции
cmd_ack => cmd_ack, -- 1 - подтверждение опреации
cmd_adr => cmd_adr, -- адрес для команды чтения-записи
cmd_data_i => cmd_data_i, -- данные для записи
cmd_data_o => cmd_data_o, -- прочитанные данные
cmd_init_done => cmd_init_done_0 -- 1 - инициализация завершена
);
end generate;
gen_rp1: if( is_rp1=1 ) generate
rp1: xilinx_pcie_3_0_7vx_rp_m2
generic map(
INST_NUM => 1
)
port map(
sys_clk_p => sys_clk_p,
sys_clk_n => sys_clk_n,
sys_rst_n => sys_rst_n,
cmd_init_done => cmd_init_done_1 -- 1 - инициализация завершена
);
end generate;
Через компонент rp0 производятся обращения по записи или чтению 32-х разрядных слов. Компонент rp1 только проводит инициализацию.
К сожалению это моделируется очень долго, даже если проводить моделирование на уровне PIPE. Типичный сеанс моделирования это около десяти минут (а может и больше, я уже не помню). Для оперативной работы с DMA каналом это не подходит. В данной ситуации было принято совершенно естественное решение это удалить из модели контроллер PCI Express. Тем более, что он уже был изучен.
Структурная схема контроллера
Обобщённая схема контроллера представлена на рисунке.
![](https://habrastorage.org/files/b13/840/765/b13840765c4d494a9355dc447851b3e8.png)
Два одинаковых компонента core256_top_engine обеспечивают доступ к двум контроллерам EP0, EP1. core256_top_engine обеспечивает обращение к регистрам со стороны PCI Express, для этого используется только EP0 и компонент reg_access. Компонент dma_access содержит главную логику управления контроллером. Его структурная схема на рисунке ниже:
![](https://habrastorage.org/files/b01/f09/4e9/b01f094e998448c49e58ecfc01507b57.png)
Всем управляет узел ctrl_main. Узел ctrl_dsc содержит блок дескрипторов. Узел ctrl_adr преобразует дескриптор в последовательность адресов четырёхкилобайтных блоков. Адреса поступают на узлы cmd0 и cmd1 для обмена с узлами core256_top_engine;
Со стороны пользовательской части ПЛИС есть две шины шириной 512 бит. Но данные по этим шинам должны передаваться блоками по 4 килобайта и строго по очереди. Это требуется для поочерёдного заполнения узлов памяти ram0, ram1. Каждый узел памяти содержит четыре блока по 4 килобайта. На этих узлах памяти происходит расщепление исходного потока шириной 512 бит на два потока по 256 бит. В дальнейшем два потока по 256 бит уже полностью независимы. Данные потоков встретятся только в оперативной памяти компьютера, где они окажутся по соседним адресам.
Моделирование dma_access
Узел dma_access является самой сложной частью контроллера. Соответственно, он должен быть промоделирован особенно тщательно. Как я уже написал выше, моделировать два ядра PCI Express очень долго. Для ускорения разработана модель которая подключается вместо core256_top_engine. Для dma_access остался тот же самый интерфейс, а скорость моделирования выросла на порядок. В этом проекте, так же как и в проекте PROTEQ используется автоматический запуск тестов через tcl файл.
Вот фрагмент tcl-файла:
run_test "stend_m4" "test_read_8kb " 6 "50 us"
run_test "stend_m4" "test_read_16kb " 7 "100 us"
run_test "stend_m4" "test_read_49blk " 8 "150 us"
run_test "stend_m4" "test_read_8x4_cont " 9 "150 us"
run_test "stend_m4" "test_read_128x1_cont " 12 "200 us"
run_test "stend_m4" "test_read_16kbx2 " 13 "150 us"
run_test "stend_m4" "test_read_step " 14 "200 us"
run_test "stend_m4" "test_read_8kb_sg_eot " 15 "100 us"
run_test "stend_m4" "test_read_64x1 " 16 "100 us"
Это автоматический запуск девяти тестов. Для примера приведу код одного теста:
test_read_4kb
procedure test_read_4kb (
signal cmd: out bh_cmd; --! команда
signal ret: in bh_ret --! ответ
)
is
variable adr : std_logic_vector( 31 downto 0 );
variable data : std_logic_vector( 31 downto 0 );
variable str : line;
variable L : line;
variable error : integer:=0;
variable dma_complete : integer;
variable data_expect : std_logic_vector( 31 downto 0 );
begin
write( str, string'("TEST_READ_4KB" ));
writeline( log, str );
---- Формирование блока дескрипторов ---
for ii in 0 to 127 loop
adr:= x"00100000";
adr:=adr + ii*4;
int_mem_write( cmd, ret, adr, x"00000000" );
end loop;
int_mem_write( cmd, ret, x"00100000", x"00008000" );
int_mem_write( cmd, ret, x"00100004", x"00000100" );
-- int_mem_write( cmd, ret, x"00100080", x"00008000" );
-- int_mem_write( cmd, ret, x"00100084", x"00000100" );
int_mem_write( cmd, ret, x"001001F8", x"00000000" );
int_mem_write( cmd, ret, x"001001FC", x"762C4953" );
---- Программирование канала DMA ----
block_write( cmd, ret, 4, 8, x"00000025" ); -- DMA_MODE
block_write( cmd, ret, 4, 9, x"00000010" ); -- DMA_CTRL - RESET FIFO
block_write( cmd, ret, 4, 20, x"00100000" ); -- PCI_ADRL
block_write( cmd, ret, 4, 21, x"00100000" ); -- PCI_ADRH
block_write( cmd, ret, 4, 23, x"0000A400" ); -- LOCAL_ADR
block_write( cmd, ret, 4, 9, x"00000001" ); -- DMA_CTRL - START
wait for 20 us;
block_read( cmd, ret, 4, 16, data ); -- STATUS
write( str, string'("STATUS: " )); hwrite( str, data( 15 downto 0 ) );
if( data( 8 )='1' ) then
write( str, string'(" - Дескриптор правильный" ));
else
write( str, string'(" - Ошибка чтения дескриптора" ));
error := error + 1;
end if;
writeline( log, str );
if( error=0 ) then
---- Ожидание завершения DMA ----
dma_complete := 0;
for ii in 0 to 100 loop
block_read( cmd, ret, 4, 16, data ); -- STATUS
write( str, string'("STATUS: " )); hwrite( str, data( 15 downto 0 ) );
if( data(5)='1' ) then
write( str, string'(" - DMA завершён " ));
dma_complete := 1;
end if;
writeline( log, str );
if( dma_complete=1 ) then
exit;
end if;
wait for 1 us;
end loop;
writeline( log, str );
if( dma_complete=0 ) then
write( str, string'("Ошибка - DMA не завершён " ));
writeline( log, str );
error:=error+1;
end if;
end if;
for ii in 0 to 3 loop
block_read( cmd, ret, 4, 16, data ); -- STATUS
write( str, string'("STATUS: " )); hwrite( str, data( 15 downto 0 ) );
writeline( log, str );
wait for 500 ns;
end loop;
block_write( cmd, ret, 4, 9, x"00000000" ); -- DMA_CTRL - STOP
write( str, string'(" Прочитано: " ));
writeline( log, str );
data_expect := x"A0000000";
for ii in 0 to 1023 loop
adr:= x"00800000";
adr:=adr + ii*4;
int_mem_read( cmd, ret, adr, data );
if( data=data_expect ) then
fprint( output, L, "%r : %r - Ok\n", fo(ii), fo(data));
fprint( log, L, "%r : %r - Ok\n", fo(ii), fo(data));
else
fprint( output, L, "%r : %r Ожидается: %r - Error \n", fo(ii), fo(data), fo(data_expect));
fprint( log, L, "%r : %r Ожидается: %r - Error \n", fo(ii), fo(data), fo(data_expect));
error:=error+1;
end if;
data_expect := data_expect + 1;
end loop;
-- block_write( cmd, ret, 4, 9, x"00000010" ); -- DMA_CTRL - RESET FIFO
-- block_write( cmd, ret, 4, 9, x"00000000" ); -- DMA_CTRL
-- block_write( cmd, ret, 4, 9, x"00000001" ); -- DMA_CTRL - START
fprint( output, L, "\nTest time: %r \n", fo(now) );
fprint( log, L, "\nTest time: %r \n", fo(now) );
-- вывод в файл --
writeline( log, str );
if( error=0 ) then
write( str, string'("TEST finished successfully" ));
cnt_ok := cnt_ok + 1;
else
write( str, string'("TEST finished with ERR" ));
cnt_error := cnt_error + 1;
end if;
writeline( log, str );
writeline( log, str );
-- вывод в консоль --
writeline( output, str );
if( error=0 ) then
write( str, string'("TEST finished successfully" ));
else
write( str, string'("TEST finished with ERR" ));
end if;
writeline( output, str );
writeline( output, str );
end test_read_4kb
Команды int_mem_write обеспечивают запись в ОЗУ HOST компьютера. В данном тесте туда записывается блок дескрипторов. Команды block_write и block_read обеспечивают обращения к регистрам DMA контроллера. Производится программирование контроллераю, его запуск и завершение обмена. После этого командами int_mem_read производится считывание и проверка принятых данных. Код этого теста практически полностью совпадает с тестом от контроллера PCIe_DS_DMA, который я опубликовал как open source проект на opencores.org; По сравнению с оригиналом добавлена проверка принятых данных.
Логическая организация контроллера
На уровне регистров контроллер полностью повторяет наши предыдущие контроллеры для ПЛИС Virtex 4, Virtex 5, Virtex 6, Kintex 7; С организацией можно ознакомиться в проекте PCIe_DS_DMA.
Особенностью всех контроллеров является объединение одиночных дескрипторов в блок дескрипторов. Это даёт резкое увеличение скорости при использовании фрагментированной памяти.
Подключение к тетрадам
Для нас важно подключить данный контроллер к нашим тетрадам. Что такое тетрады я написал в предыдущей статье: «Интерфейс ADM: Что такое тетрада». Работа с шиной 512 бит потребовала изменение подхода. Для подключения тетрады пришлось использовать дополнительный узел перепаковщика. Структураная схема — на рисунке.
![](https://habrastorage.org/files/2ae/24e/5ef/2ae24e5ef4f043c7ade0dd5cfa6e38c4.png)
Перепаковщик решает две задачи:
- трассировку шины по кристаллу, для этого можно задать число дополнительных стадий конвейера
- подключение к тетрадам с шинами 64 и 128 разрядов
Использование памяти
Конечной целью разработки контроллера и подключения к тетрадам является получение непрерывного потока данных от АЦП на компьютер. И здесь мы сталкиваемся с тем, что шина PCI Express не обеспечивает стабильной скорости. На шине возможны задержки. Это особенно заметно проявляется на высоких скоростях обмена. Задержки возникают из-за работы других устройств. Величина задержки может быть разной, это может быть 5 – 10 мкс, а может и больше. Задержка в 10 мкс на скорости 11 Гбайт/с соответствует блоку памяти в 110 килобайт. Для внутренней памяти даже современных ПЛИС это очень много. А ведь задержка может быть и больше. Если поток данных нельзя приостановить, а это как раз тот случай, когда используются АЦП, то единственным выходом является буферизация во внешней памяти. Причём память должна уметь работать на скорости 22 Гбайта/с. У нас на модуле установлены два SODIMM DDR3-1600. Память работает на частоте 800 МГц. Это соответствует непрерывному потоку данных 8400 Мбайт/с. Это цифра подтверждена экспериментом. Хочу заметить, что скорость 8400 Мбайт/с превосходит скорость выдачи данных от нашего самого быстрого субмодуля в котором установлены два АЦП на 1800 МГц.
Трассировка
На скриншоте представлен результат трассировки в программе PlanAhead:
![](https://habrastorage.org/files/7e4/a2a/8e2/7e4a2a8e2f63427e867db8ecbdccb581.png)
На картинке видно два контроллера PCI Express (подсвечены жёлтым и зелёным) и два контроллера памяти (рядом с PCI Express).
Как оказалась, такой проект является очень сложным для Vivado, она с ним справляется очень плохо. Проект в Vivado разводится плохо и часто просто не работает. ISE показывает гораздо более стабильные результаты. Узлы PCI Express разведены в соответствии с рекомендациями Xilinx, при этом оказалось что они разнесены по кристаллу. А это уже создаёт проблему для совместного использования остальных мультигигабитных линий.
Результаты
Работа модуля проверялась на нескольких компьютерах. Результаты довольно интересные.
Intel Core i7 4820K | P9X79 WS | DDR3-1866 | 11140 МБайт/с |
---|---|---|---|
Intel Core i7 5820K | X99-A | DDR4-2400 | 11128 МБайт/с |
Intel Core i7 3820K | P9X79 | DDR3-1600 | 11120 МБайт/с |
Это скорость ввода данных без проверки. Производится непрерывный ввод данных в буфер размером 1 Гбайт выделенный в системной области памяти, т. е. непрерывный по физическим адресам. Измеряется средняя скорость ввода на интервале не менее 1 минуты.
На компьютере с памятью DDR3-1600 при включении проверки скорость падает до 8500 Мбайт/с.
На компьютере с DDR3-1866 скорость при одном модуле и включённой проверке скорость не уменьшается.
Два модуля FMC122P в компьютере с DDR3-1866 без проверки также показывают максимальную скорость около 11000 Мбайт/с для каждого модуля. Но при включении проверки скорость падает.
При данных измерениях принято что 1 Мбайт это 1024 кбайт, а 1 кбайт это 1024 байта.
Я бы хотел отметить, что в данной работе я представляю результат работы большого коллектива. Особая благодарность — Дмитрию Авдееву, который проделал огромную работу в этом проекте.
P.S. Пока шла разработка Virtex 7 успел устареть. Kintex Ultrascale уже удобнее в работе. А Kintex Ultrascale+ уже имеет HARD блок PCI Express v3.0 x16 – так что такое разделение уже не нужно.
P.S.S. Но Kintex Ultrascale+ также имеет HARD блок PCI Express v4.0 x8 – может всё таки разделение пригодится?