Общение с ПЛИС
Помимо внутренней генерации данных на ПЛИС или их получения от периферийных компонентов платы, может возникнуть необходимость обмениваться данными напрямую с ПК. Для этого можно использовать шину PCIe. Для реализации этого взаимодействия на стороне устройства (ПЛИС) нужно настроить PCIe-контроллер, а на стороне хоста (ПК) необходимо реализовать драйвер и программу, которая будет управлять передачей данных.
PCIe (Peripheral Component Interconnect Express) — стандарт шины, предназначенный для подключения периферии к ПК. Это одновременно и протокол (набор правил, уровни, структура TLP), и физический интерфейс (разъемы). Поколения PCIe определяют максимальную скорость передачи данных по одной линии (линий может быть несколько).
Данные по шине передаются с помощью TLP (Transaction Layer Packet). Основных видов пакетов три: Memory Write (запись), Memory Read (запрос на чтение), Completion with Data (ответ на запрос). На уровне протокола TLP данные выравниваются по границе dword (4 байта), и длина любого PCIe-пакета измеряется в dword-ах.
Когда линк (стабильное соединение) между устройством и хостом поднимается, начинается инициализация BAR (Base Address Register). Это регистр в конфигурационном пространстве PCIe-контроллера устройства. Он хранит базовый физический адрес пространства, которое ОС закрепляет за устройством. При обращении хоста по адресу внутри этого диапазона генерируется TLP-пакет, затем PCIe-контроллер сравнивает адрес TLP с диапазонами своих BAR и при совпадении обрабатывает запрос со смещением относительно базового адреса.
| Далее пространство, за которое отвечает BAR, — окно. |
Взаимодействие компонентов показано на схеме ниже. Зеленым цветом помечены компоненты, которые необходимо разрабатывать/дорабатывать самостоятельно, серым — готовые IP-ядра, предоставляемые вендором. Далее про каждый компонент будет рассказано подробнее.
Конфигурация PCIe-контроллера
PCIe-контроллер использует модуль SerDes в качестве физического уровня для связи с хостом и внешними устройствами. Пользовательской логике (внутри ПЛИС) он выдает rx/tx порты с «сырыми» TLP-пакетами. Сгенерировать PCIe-контроллер и модуль SerDes можно при помощи SerDes IP: IP Core Generator → Soft IP Core → SerDes → SerDes. Далее в выпадающем списке Protocol требуется выбрать PCI Express Controller.
Пробежимся по настройкам:
-
Lane Width,Maximum Link Speedна вкладкеPCIe ConfigurationиMax Payload Sizeна вкладкеCoreрекомендуем ставить максимальными. Они не влияют на пользовательский интерфейс контроллера, а реальные значения подберутся при установке соединения с хостом. Их можно будет увидеть с помощью командыsudo lspci -vvd 22c2:1100(в строкеLnkSta).-
Maximum Link Speedзадает максимальную скорость соединения: Gen2 (5.0 GT/s) или Gen3 (8.0 GT/s). Скорость измеряется в гигатранзакциях в секунду. -
Lane Widthпоказывает, сколько физических линий используется. Пиковая пропускная способность равняетсяLane Width * Maximum Link Speed. -
Max Payload Sizeуказывает максимально допустимый размер полезной нагрузки (данных) в одном TLP-пакете. Поддерживается размер до 4 КБ (чем больше пакет, тем меньше накладные расходы на заголовки).
-
-
Частоту (
TLP Clock Frequency) будем использовать 100 МГц, как и стоит по умолчанию. -
Vendor / Device IDпозволяют однозначно идентифицировать плату, в нашем случае22c2и1100. -
На вкладке
BARконфигурируются количество BAR-ов, разрядность базового адреса окна и размер окна. Всего доступно 6 BAR, но при выборе 64 бит следующий становится недоступным.
В нашем случае мы не будем напрямую взаимодействовать с «сырыми» rx/tx портами PCIe-контроллера, так как подключим их к другому IP-ядру — SGDMA. Пример работы с «сырыми» пакетами можно посмотреть в примере производителя (требуется разархивировать проект) или здесь. Там же можно позаимствовать файл описания сигналов (.cst) и логику отсчёта задержки для стабильного запуска.
Конфигурация SGDMA
DMA (Direct Memory Access) — механизм, позволяющий устройству читать и писать в оперативную память напрямую, минуя процессор. Scatter-Gather DMA — разновидность DMA, позволяющая передавать данные из разных участков памяти, используя список дескрипторов. Для выполнения этой операции хост выделяет буферное пространство в RAM и создает дескрипторы, описывающие атрибуты передачи (адреса и длины); DMA-контроллер на ПЛИС извлекает дескрипторы из памяти хоста и согласно им переносит данные[1].
Использование PCIe SGDMA IP избавляет нас от ручного разбора TLP-пакетов. Модуль подключается к PCIe-контроллеру и предоставляет пользователю два интерфейса AXI4-Stream (для чтения (H2C, Host to Card) и записи (C2H, Card to Host)). Путь для генерации: IP Core Generator → Soft IP Core → Interface and Interconnect → PCIe SGDMA. Для работы самого контроллера SGDMA используется BAR0 (через него хост настраивает передачу), а пользователю, помимо AXI-стримов, даётся доступ для управления собственной логикой через BAR2 (оба 64-битные, поэтому BAR1 и BAR3 недоступны).
| В User Guide подробно описаны порты, архитектура и пайплайн работы контроллера. |
Хост-программа
В хост-программе описывается весь пайплайн управления передачей данных. Сначала необходимо запросить у драйвера выделение памяти под буферы (H2C, C2H и дескрипторы для них), а затем отобразить физические адреса этих буферов и окон в виртуальное адресное пространство программы. Также требуется проверить установку линка и запустить контроллер, записав управляющие поля в BAR0-окно. Далее хост-программа формирует DMA-дескрипторы, а в BAR0-окно записывается физический адрес первого дескриптора, количество дескрипторов в первом блоке, адрес опроса для обратной связи и сигнал старта передачи. Потом необходимо в цикле считывать значение по адресу опроса (Poll Mode) и в конце отправить сигнал остановки.
При C2H-передаче в BAR0-окно необходимо дополнительно записать количество кредитов (один потраченный кредит — один обработанный дескриптор). Если дескрипторов больше, чем максимально возможное количество кредитов (1023), то можно в цикле опроса увеличивать их количество. Обратите внимание, что поле кредитов работает по принципу добавления, а не перезаписи.
|
С примером хост-программы можно ознакомиться здесь, а со структурой GowinBar0 здесь (порядок полей и их размеры фиксированы документацией SGDMA).
|
Размер каждого дескриптора составляет 32 байта, и он должен быть выровнен по 32-байтовой границе. На странице может уместиться до 128 дескрипторов (4 КБ / 32 Б). Блок дескрипторов представляет собой последовательность смежных дескрипторов. Несколько блоков дескрипторов образуют цепочку дескрипторов.
Так выглядит структура дескриптора:
typedef struct __attribute__((packed, aligned(32))) {
uint32_t flags; //* 0x00 - Stop[0], Eop[1], Completed[2], AdjDescNum[14:8] (1)
uint32_t length; //* 0x04 - Data Length (bytes)
uint32_t addr_src_lo; //* 0x08 - Source Low Address (2)
uint32_t addr_src_hi; //* 0x0C - Source High Address
uint32_t addr_dst_lo; //* 0x10 - Destination Low Address (3)
uint32_t addr_dst_hi; //* 0x14 - Destination High Address
uint32_t next_lo; //* 0x18 - Next Descriptor Low Address
uint32_t next_hi; //* 0x1C - Next Descriptor High Address
} GowinDescriptor;
| 1 | Бит Completed указывает SGDMA выполнить запись количества обработанных дескрипторов по адресу опроса в памяти хоста после обработки данного дескриптора. Рекомендуется ставить его на каждый последний дескриптор в блоке. Помимо данного флага, требуется указать либо количество дескрипторов в следующем блоке (AdjDescNum) и адрес первого дескриптора следующего блока (в next_*), либо флаг завершения работы (Stop). |
| 2 | При H2C-передаче указывается физический адрес памяти хоста, из которой SGDMA прочитает данные. |
| 3 | При C2H-передаче указывается физический адрес памяти хоста, в которую SGDMA запишет данные. |
В текущей версии САПР (V1.9.12.02_SP2) при работе с IP-ядром SGDMA на хосте с порядком байтов Little Endian существует «особенность»: запись/чтение в окно работает, как ожидается, а для записи данных в RAM требуется использовать __builtin_bswap32.
|
Любая операция присваивания/чтения поля в структуре GowinBar0 — это генерация TLP-пакета со стороны хоста в сторону ПЛИС.
|
Драйвер
Без драйвера устройства хост-программа не сможет получить доступ к окну и не сможет выделить физически непрерывную память для DMA-буферов. В Linux такой драйвер реализуется в виде модуля ядра (с расширением .ko), который загружается и выгружается командами insmod и rmmod.
Для связи драйвера с устройством используется структура pci_driver. В ней содержится таблица pci_device_id, содержащая знакомые нам Vendor/Device ID, а также функция probe, которая вызывается при привязке (insmod). Данная функция отвечает за активацию устройства (pci_enable_device), предоставление ему права инициализировать транзакции на шине PCIe (pci_set_master), резервирует адресные пространства для BAR-окон и отображает их в виртуальное адресное пространство ядра (pcim_iomap_regions).
Также функция probe регистрирует символьное устройство в каталоге /dev/, связывая его со структурой file_operations. Она предоставляет пользовательской программе системные вызовы: open, close, mmap, ioctl. Например, ioctl_dma_mem_request, который вызывается перед mmap, выделяет память под DMA-буфер в ядре и возвращает её физический адрес.
Порядок действий
Шаги запуска проекта:
-
Отключить
Secure Bootв BIOS и настроить параметры GRUB на хосте. -
Подключить плату через USB-порт
JTAG/UARTк ноутбуку/ПК и загрузить.fsбитстрим. -
Подключить прошитую плату в слот PCIe хоста и включить/перезагрузить хост.
-
Собрать и загрузить модуль ядра. После успешной загрузки должны появиться подпись
Kernel driver in use:вlspci, логиprobeфункции в сообщениях ядра (dmesg) и символьное устройство в каталоге/dev/. -
Собрать и запустить хост-программу. В случае успешной отработки программы, например в этом примере, выводится статус готовности
BAR0,BAR2и SGDMA-контроллера, отправляемые на плату и принятые с неё данные. Затем проверяется соответствие полученных данных ожидаемым, и программа завершается с кодом возврата 0.
| Процесс сборки проекта можно упростить с помощью make-файлов, а загрузку модуля ядра и запуск хост-программы с помощью скрипта. |
Несколько замечаний из практики:
-
Не подключать в слот PCIe непрошитую плату.
-
После перепрошивки требуется выполнить перезагрузку хоста.
-
После прошивки и установки платы в слот PCIe можно включать хост и отключать USB-кабель от ноутбука/ПК.
-
Если устройство не появилось в
lspci, то проблема может быть в настройках SerDes. Например, у авторов курса и в примере производителя на плате Tang MEGA 138K Pro наблюдается такая проблема при установке скорости Gen2 (5 GT/s) (несмотря на то, что при установке Gen3 (8 GT/s) вlspciотображается статус5 GT/s (downgraded)).