Затолкаем братцы!!! UART Lite через PCIe прямиком в Linux: драйвер за вечер (почти)

от автора

Иногда самые простые задачи превращаются в мини-приключения. Например, когда вам нужно подключить UARTLite на FPGA к Linux через PCIe. Кажется, ну что там? Пару регистров, пара прерываний… А на деле — несколько дней за Vivado, отладка XDMA и борьба с драйверами. 😅

В этой статье я расскажу, как я всё-таки победил UARTLite через XDMA и как вы сможете повторить это без боли.

Аннотация

Этот проект демонстрирует, как подключить периферийное устройство UARTLite на базе FPGA к пользовательским приложениям Linux через PCIe XDMA.
Реализуем TTY-интерфейс (драйвер Linux TTY ) /dev/ttyULx и показываем альтернативный способ быстрого доступа через Python с использованием mmap.
Идеально подходит для интеграции в проекты SDR, робототехники и встраиваемых систем!

  • Что за устройство?
    ➔ Моя SDR-плата на FPGA Artix-7, GPS SIM68 модулем и AD9361 RF рансивером, интерфейсом PCIe и возможностью обмена данными между FPGA и Linux через XDMA.

  • Что мы будем делать?
    ➔ Разработаем Linux-драйвер для UARTLite через XDMA и продемонстрируем альтернативный способ прямого доступа через Python.

  • Почему это важно?
    ➔ Такое решение позволяет легко интегрировать пользовательские FPGA-периферии в Linux-системы без сложной ручной настройки, особенно в SDR-проектах, робототехнике, автономных системах и IoT.


    1. Введение

    Встраиваемые системы на базе FPGA широко применяются в программно-определяемых радиосистемах (SDR, Software-Defined Radio). Программно-определяемые радиосистемы (SDR) требуют эффективного взаимодействия между FPGA и CPU, что осложняется интеграцией периферии через PCIe. Эта статья решает задачу разработки драйвера для UARTlite через XDMA, демонстрируя два подхода: через ядро Linux и Python. Цель — показать архитектуру, реализацию и тестирование решения на базе собственной SDR-платы.

    Особенности проекта:

  • ✅ Разработанная с нуля SDR-плата на FPGA Artix-7 и AD9361

  • ✅ В качестве системной платы используется LattePanda Sigma

  • ✅ Встроенный UARTlite для взаимодействия с GPS модулем подключеным к FPGA

  • ✅ XDMA через PCIe для высокоскоростной передачи данных

  • ✅ Поддержка двух методов работы:

    • Linux-драйвер с TTY-интерфейсом (/dev/ttyULx)

    • Прямая работа с UARTlite на Python через XDMA


2. Архитектура решения

Проект основан на разработанной с нуля SDR-плате с FPGA и интерфейсом PCIe:

Основная платформа проекта:

Компонент

Описание

FPGA

Xilinx Artix-7 (XC7A200T)

RF-трансивер

Analog Devices AD9361 (70 MHz – 6 GHz)

GPS-модуль

SIM68 (NMEA 0183 Output)

DDR

MT41K256M16HA-125 AAT

Хост-плата

LattePanda Sigma

Интерфейс передачи

PCIe Gen1 x4 (10 Gbps через разъём M.2)

Передача данных

Через XDMA и AXI шину

UART-периферия

AXI UARTLite IP Core (9600 бод)

Форм-фактор

M.2 2280 (80mm × 22mm)

Описание системы

Система состоит из следующих компонентов и их связей:

  1. SIM68 (GPS Module)

    • Это GPS-модуль, который отправляет данные через интерфейс UART.

    • Связан с UartLite (UART Controller) через UART.

  2. UartLite (UART Controller)

  3. XDMA (PCIe DMA)

    • Контроллер прямого доступа к памяти (DMA) через PCIe. (AXI PCIe DMA Product Guide (PG195))

    • Связан с CPU (Latte Panda Sigma) через PCIe.

  4. CPU (Latte Panda Sigma)

    • Центральный процессор, который обрабатывает данные от XDMA.

    • Связан с Linux Kernel через обозначение User-space (пространство пользователя).

  5. Linux Kernel

    • Ядро операционной системы Linux, работающее на CPU.

    • Связан с User Application через интерфейсы TTY / Python.

      Linux TTY Documentation

  6. User Application

    • Пользовательское приложение, которое взаимодействует с ядром Linux для доступа к данным, возможно, через TTY-устройства или скрипты на Python.

2.1 Структурная схема системы

На изображении ниже представлена структурная схема системы с собственным драйвером:

данные проходят по цепочке:

  1. SIM68 передает NMEA-сообщения в UartLite на FPGA через UART

  2. UartLite взаимодействует с XDMA (PCIe DMA) через AXI.

  3. XDMA передаёт данные через PCIe в CPU (Latte Panda Sigma).

  4. CPU обрабатывает данные в user-space и взаимодействует с ядром Linux.

  5. Linux Kernel предоставляет интерфейс TTY / Python API для пользовательского приложения (/dev/ttyUL0).

  6. User Application – взаимодействует с UART через Python или терминал.

На изображении ниже представлена структурная схема системы при прямом доступе через Python:

Screenshot from 20250410 171712png

Screenshot from 20250410 171712png

Данные проходят по цепочке:

  1. SIM68 передает NMEA-сообщения в UartLite на FPGA через UART.

  2. UartLite взаимодействует с XDMA (PCIe DMA) через AXI.

  3. XDMA передаёт данные через PCIe в CPU (Latte Panda Sigma).

  4. CPU через /dev/xdma0_user мапит пространство устройства с помощью mmap.

  5. Python Application напрямую читает регистры UARTlite (RX FIFO / TX FIFO), минуя драйвер ядра.

Таким образом, взаимодействие происходит без использования стандартного Linux TTY-драйвера, что позволяет:

  • Минимизировать накладные расходы на системные вызовы.

  • Снизить нагрузку на процессор за счёт использования async IO.

  • Быстро прототипировать и отлаживать системы на основе FPGA.

На диаграмме ниже показано, как происходит обмен данными между компонентами в системе:

2.2 Блок-схема в Vivado

  • В проекте используется XDMA (PCIe-DMA Bridge)

  • AXI Interconnect связывает XDMA с AXI Register Slice, AXI UARTLite

  • AXI UARTLite подключен к системе через AXI Interconnect и к GPS модулю SIM68

  • Есть также AXI GPIO, которые могут использоваться для управления*

Основной поток данных:
FPGAAXI InterconnectAXI UARTLitePCIe XDMACPU/Linux

Ниже представлен скрин проекта в Vivado из реального проекта:

Снимок экрана 20250313 211436png

Снимок экрана 20250313 211436png

или простая реализация только для UartLite:

Screenshot from 20250410 171633png

Screenshot from 20250410 171633png

2.3 Разбор конфигурации XDMA и UartLite:

AXI UARTLite (Настройки IP)

  • Частота AXI CLK: 62.5 МГц

  • Скорость UART: 9600 бод

  • Длина данных: 8 бит

  • Четность (Parity): Отключена (No Parity)

Эти настройки стандартные для UARTLite, но если необходимо увеличить скорость передачи, можно выставить 115200 бод.

Настройки DMA (PCIe : DMA)

AXI PCIe DMA Product Guide (PG195)

Количество каналов

  • 1 канал на чтение (H2C)

  • 1 канал на запись (C2H)

  • Количество Request IDs: 32 (чтение), 16 (запись)

  • AXI ID Width: 4 (идентификатор транзакций в AXI)

    Descriptor Bypass (Отключен)

  • Используется стандартное управление буферами (без байпаса).

Настройки прерываний (PCIe : MISC)

Количество прерываний: 16
MSI включен (Message Signaled Interrupts)
Расширенные теги включены (Extended Tag Field)

Настройки BAR (PCIe : BARS)

PCIe → AXI Lite Master Interface

  • Размер памяти: 1MB

  • Адрес: 0x00000000 PCIe → DMA Interface включен

Идентификаторы устройства (PCIe ID)

Vendor ID: 10EE (Xilinx)
Device ID: 7011
Class Code: 120000 (устройство памяти)

Базовые настройки PCIe (Basic)

[PCIe Base Specification (official summary)](Specifications | PCI-SIG)

PCIe x1, 2.5 GT/s
AXI Address Width: 64-bit
AXI Data Width: 64-bit
AXI Clock: 125 MHz
Режим: AXI Memory Mapped

Адресс UartLite:

2.2 Подключение SDR к материнской плате

1696959565663 1jpeg

1696959565663 1jpeg

3. Реализация драйвера

3.1 Подготовка окружения

Аппаратная платформа:

  • SDR-плата: разработанная мной, FPGA Artix-7 200T

  • GPS-модуль: SIM68

  • Интерфейсы: PCIe, UART

  • Материнская плата: Latte Panda Sigma

Программное обеспечение:

  • ОС: Linux Kernel 6.x+

  • Инструменты: Vivado, Python, GCC

  • Отладка: dmesg, minicom


3.2 Установка зависимостей

sudo apt update sudo apt install -y build-essential dkms linux-headers-$(uname -r) git sudo apt install -y pciutils lshw sudo apt install -y gcc-13 g++-13  sudo apt install -y gpsd gpsd-clients

Что устанавливаем?

  • build-essential — инструменты сборки (gcc, make, binutils)

  • dkms — динамическое управление модулями ядра

  • linux-headers-$(uname -r) — заголовочные файлы текущего ядра

  • git — для загрузки исходников драйвера XDMA

  • pciutils (lspci) — просмотр PCIe-устройств

  • lshw — детальная информация об оборудовании

  • gcc-13, g++-13 — компиляторы C и C++ версии 13

  • gpsd, gpsd-clients — демоны и утилиты для работы с GPS-приёмниками

Проверка версии:

gcc-13 --version

3.3 Проверка оборудования

После установки убедимся, что PCIe-устройство (FPGA) распознаётся системой:

lspci -d 10ee:

Если XDMA-карта видна, будет вывод типа:

59:00.0 Memory controller: Xilinx Corporation Device 7011

Вот что показывает у меня:

3.4 Реализация драйвера

Принцип работы драйвера

При загрузке модуля драйвер регистрирует PCIe-устройство с Vendor ID 0x10EE и Device ID 0x7011. После инициализации он создаёт TTY-устройство /dev/ttyUL0. Входящие данные из UARTlite обрабатываются через механизм workqueue, передаются в буфер TTY и становятся доступны пользователю через функцию tty_flip_buffer_push (она сообщает ядру Linux, что в приёмном буфере появились новые данные, и эти данные нужно передать в пользовательское пространство и без её вызова данные будут висеть внутри драйвера и не дойдут до пользователя).

Linux TTY Documentation

Определение констант

#define DRIVER_NAME "uartlite_xdma" // Имя драйвера #define VENDOR_ID 0x10EE  // Xilinx Vendor ID #define DEVICE_ID 0x7011  // Device ID для FPGA Hard PCIe block #define UARTLITE_BASE_OFFSET 0x40000 // Базовый адрес AXI**
  • VENDOR_ID = 0x10EE — это Xilinx (задается в конфигурации IP ядра в Vivado, каждый производитель PCIe-устройств имеет свой ID).

  • DEVICE_ID = 0x7011идентификатор устройства (задается в конфигурации IP ядра в Vivado).

  • UARTLITE_BASE_OFFSET = 0x40000смещение в памяти, где находится UARTlite в пространстве XDMA (в моем прмере 0x40000 , орпеделяется проектом в Vivado)

Адреса регистров в памяти:

В соотвествии с datashhet AXI UartLite

#define UARTLITE_RX_FIFO  0x00  // FIFO приёма #define UARTLITE_TX_FIFO  0x04  // FIFO передачи #define UARTLITE_STATUS   0x08  // Регистр состояния #define UARTLITE_CONTROL  0x0C  // Регистр управления
  • RX_FIFO (0x00) — читаем данные, полученные от UART.

  • TX_FIFO (0x04) — записываем данные для передачи.

  • STATUS (0x08) — флаги состояния (данные есть? FIFO заполнен?).

  • CONTROL (0x0C) — управление UARTlite.

Флаги регистра состояния

#define STATUS_RXVALID    BIT(0) // Данные есть в RX FIFO #define STATUS_TXFULL     BIT(3) // TX FIFO заполнен
  • RXVALID (бит 0)1, если в RX FIFO есть данные.

  • TXFULL (бит 3)1, если TX FIFO заполнен.

Основная структура данных

struct uartlite_priv {     void __iomem *base;     struct tty_port port;     struct work_struct rx_work; // Обработчик RX (workqueue)     bool running; };

Структура uartlite_priv хранит:

  • baseуказатель на базовый адрес устройства в PCIe.

  • portTTY-порт для связи с Linux.

  • rx_workзадача для обработки входящих данных (workqueue).

  • runningфлаг работы (установлен, пока устройство активно).

Функции работы с UARTlite

static int uartlite_tx_ready(struct uartlite_priv *priv) {     return !(ioread32(priv->base + UARTLITE_STATUS) & STATUS_TXFULL); }

Проверяет, можно ли передавать данные:

  • Читает STATUS

  • Если TXFULL == 0, значит, можно передавать.

static void uartlite_write_byte(struct uartlite_priv *priv, u8 val) {     iowrite32(val, priv->base + UARTLITE_TX_FIFO); }

Записывает 1 байт в TX FIFO.

static int uartlite_rx_ready(struct uartlite_priv *priv) {     return ioread32(priv->base + UARTLITE_STATUS) & STATUS_RXVALID; }

Проверяет, есть ли данные в RX FIFO.

static u8 uartlite_read_byte(struct uartlite_priv *priv) {     return ioread32(priv->base + UARTLITE_RX_FIFO); }

Считывает 1 байт из RX FIFO.

Обработка приёма данных (RX)

В этом драйвере используется поллинг RX FIFO (приёмного буфера UARTlite) через workqueue , для упрощения реализации, что бы не использовать механизм работы через прерывания.

Поллинг (polling) — это метод обработки данных, при котором процессор периодически проверяет состояние устройства и считывает данные, если они доступны.

Как работает поллинг в драйвере?

1. Приложение открывает TTY-устройство (/dev/ttyUL0)

  • Вызывается uartlite_tty_open(), который устанавливает флаг running = true.

  • Запускается обработчик uartlite_rx_work() с помощью schedule_work().

2. Функция uartlite_rx_work() проверяет, есть ли данные в RX FIFO

  • Читает регистр статуса (UARTLITE_STATUS).

  • Если в RX FIFO есть данные (STATUS_RXVALID = 1), читает и буферизует их и передаёт в TTY (tty_flip_buffer_push,tty_insert_flip_string).

3. Если FIFO не пуст, данные передаются в TTY-подсистему

  • Вызываются:

    tty_insert_flip_string(&priv->port, buf, count);  tty_flip_buffer_push(&priv->port);
  • Данные становятся доступны в /dev/ttyUL0.

4. После обработки данных обработчик uartlite_rx_work() самозапускается

  • Если running = true, функция снова вызывается (schedule_work()).

  • Если running = false, процесс останавливается (например, после закрытия /dev/ttyUL0).

static void uartlite_rx_work(struct work_struct *work) {   struct uartlite_priv *priv = container_of(work, struct uartlite_priv, rx_work);     struct tty_struct *tty = tty_port_tty_get(&priv->port);     unsigned char buf[16];     int i, count;      if (!tty)         return;      while (priv->running && uartlite_rx_ready(priv)) {         count = 0;         for (i = 0; i < sizeof(buf) && uartlite_rx_ready(priv); i++) {             buf[i] = uartlite_read_byte(priv);             count++;         }         if (count) {             tty_insert_flip_string(&priv->port, buf, count);             tty_flip_buffer_push(&priv->port);         }     }      if (priv->running)         schedule_work(&priv->rx_work);      tty_kref_put(tty); }

Регистрация PCIe-устройства

Регистрирует PCIe-драйвер следующая функция:

static struct pci_driver uartlite_pci_driver = {     .name = DRIVER_NAME,     .id_table = uartlite_pci_tbl,     .probe = uartlite_probe,     .remove = uartlite_remove, };
  • При обнаружении устройства → вызов uartlite_probe()

  • При удалении устройства → вызов uartlite_remove()

Код ниже определяет таблицу идентификаторов PCI-устройств, которые поддерживает драйвер.

static const struct pci_device_id uartlite_pci_tbl[] = {     { PCI_DEVICE(VENDOR_ID, DEVICE_ID) },     { 0, } };
  • static const struct pci_device_id uartlite_pci_tbl[]

    • Определяет массив uartlite_pci_tbl с информацией о поддерживаемых PCIe-устройствах.

    • Используется ядром Linux для поиска устройств, которые драйвер может обслуживать.

  • { PCI_DEVICE(0x10EE, 0x7011) }

    • PCI_DEVICE(vendor, device) — макрос, создающий pci_device_id структуру.

    • 0x10EEVendor ID (Xilinx).

    • 0x7011Device ID (ID конкретного устройства, в данном случае UARTlite).

  • { 0, }

    • Завершающий элемент массива (нулевой идентификатор), который указывает конец списка.

Если система найдёт устройство с Vendor ID 0x10EE и Device ID 0x7011, вызовется probe()— функция драйвера:

static int uartlite_probe(struct pci_dev *pdev, const struct pci_device_id *ent)

Основная задача:

  • Выделить память для структуры драйвера (uartlite_priv).

  • Настроить доступ к PCIe-ресурсам (IO-адреса, регистры).

  • Зарегистрировать UARTlite как TTY-устройство (/dev/ttyUL0).

  • Подготовить очередь задач (workqueue) для приёма данных.

Инициализация модуля

module_init(uartlite_init); module_exit(uartlite_exit);

3.5 Полный код драйвера (uartlite_xdma.c)

/*  * UARTlite TTY Driver over XDMA  *  * Author:   * Date:   *  * This driver enables communication with AXI UART Lite over PCIe XDMA.  * It implements a TTY interface (ttyULx) for user-space interaction and supports  * RX polling using a work queue mechanism.  *  * License: GPL v2  */   #include <linux/version.h> #include <linux/module.h> #include <linux/pci.h> #include <linux/tty.h> #include <linux/tty_driver.h> #include <linux/tty_flip.h> // tty_insert_flip_string и tty_flip_buffer_push #include <linux/io.h> #include <linux/workqueue.h> // work_struct  // External information #define DRIVER_NAME "uartlite_xdma" // Driver name #define VENDOR_ID 0x10EE  // Xilinx Vendor ID #define DEVICE_ID 0x7011  // Device ID for 7-Series FPGA Hard PCIe block #define UARTLITE_BASE_OFFSET 0x40000 // AXI base address  // AXI UART Lite Register Offsets #define UARTLITE_RX_FIFO  0x00  // Receive FIFO #define UARTLITE_TX_FIFO  0x04  // Transmit FIFO #define UARTLITE_STATUS   0x08  // Status register #define UARTLITE_CONTROL  0x0C  // Control register  // Status Register Flags #define STATUS_RXVALID    BIT(0) // Data available in RX FIFO #define STATUS_TXFULL     BIT(3) // TX FIFO is full    struct uartlite_priv {     void __iomem *base;     struct tty_port port;     struct work_struct rx_work; // Polling     bool running; };  static struct tty_driver *uartlite_tty_driver;  /* UART Lite Functions */ static int uartlite_tx_ready(struct uartlite_priv *priv) {     return !(ioread32(priv->base + UARTLITE_STATUS) & STATUS_TXFULL); }  static void uartlite_write_byte(struct uartlite_priv *priv, u8 val) {     iowrite32(val, priv->base + UARTLITE_TX_FIFO); }  static int uartlite_rx_ready(struct uartlite_priv *priv) {     return ioread32(priv->base + UARTLITE_STATUS) & STATUS_RXVALID; }  static u8 uartlite_read_byte(struct uartlite_priv *priv) {     return ioread32(priv->base + UARTLITE_RX_FIFO); }  /* Work function for polling RX FIFO */ static void uartlite_rx_work(struct work_struct *work) {     struct uartlite_priv *priv = container_of(work, struct uartlite_priv, rx_work);     struct tty_struct *tty = tty_port_tty_get(&priv->port);     unsigned char buf[16];     int i, count;      if (!tty)         return;      while (priv->running && uartlite_rx_ready(priv)) {         count = 0;         for (i = 0; i < sizeof(buf) && uartlite_rx_ready(priv); i++) {             buf[i] = uartlite_read_byte(priv);             count++;         }         if (count) {             tty_insert_flip_string(&priv->port, buf, count);             tty_flip_buffer_push(&priv->port);         }     }      if (priv->running)         schedule_work(&priv->rx_work);      tty_kref_put(tty); }  /* TTY Operations */ static int uartlite_tty_open(struct tty_struct *tty, struct file *filp) {     struct uartlite_priv *priv = container_of(tty->port, struct uartlite_priv, port);     priv->running = true;     schedule_work(&priv->rx_work);     return tty_port_open(tty->port, tty, filp); }  static void uartlite_tty_close(struct tty_struct *tty, struct file *filp) {     struct uartlite_priv *priv = container_of(tty->port, struct uartlite_priv, port);     priv->running = false;     cancel_work_sync(&priv->rx_work);     tty_port_close(tty->port, tty, filp); }   #if LINUX_VERSION_CODE <= KERNEL_VERSION(6, 5, 0) static int uartlite_tty_write(struct tty_struct *tty, const unsigned char *buf, int count) #else static ssize_t uartlite_tty_write(struct tty_struct *tty, const u8 *buf, size_t count) #endif {     struct uartlite_priv *priv = tty->driver_data;     int i;      for (i = 0; i < count; i++) {         while (!uartlite_tx_ready(priv))             cpu_relax();         uartlite_write_byte(priv, buf[i]);     }     return i; }   static unsigned int uartlite_tty_write_room(struct tty_struct *tty) {     struct uartlite_priv *priv = tty->driver_data;     return uartlite_tx_ready(priv) ? 16 : 0; }  static unsigned int uartlite_tty_chars_in_buffer(struct tty_struct *tty) {     return 0; }  static const struct tty_operations uartlite_tty_ops = {     .open = uartlite_tty_open,     .close = uartlite_tty_close,     .write = uartlite_tty_write,     .write_room = uartlite_tty_write_room,     .chars_in_buffer = uartlite_tty_chars_in_buffer, };  /* TTY Port Initialization */ static int uartlite_port_activate(struct tty_port *port, struct tty_struct *tty) {     struct uartlite_priv *priv = container_of(port, struct uartlite_priv, port);     tty->driver_data = priv;     return 0; }  static void uartlite_port_shutdown(struct tty_port *port) {     struct uartlite_priv *priv = container_of(port, struct uartlite_priv, port);     priv->running = false;     cancel_work_sync(&priv->rx_work); }  static const struct tty_port_operations uartlite_port_ops = {     .activate = uartlite_port_activate,     .shutdown = uartlite_port_shutdown, };  /* PCI Probe Function */ static int uartlite_probe(struct pci_dev *pdev, const struct pci_device_id *ent) {     struct uartlite_priv *priv;     int ret;      priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);     if (!priv)         return -ENOMEM;      ret = pcim_enable_device(pdev);     if (ret)         return ret;      priv->base = pcim_iomap(pdev, 0, 0);     if (!priv->base)         return -ENOMEM;      priv->base += UARTLITE_BASE_OFFSET;      tty_port_init(&priv->port);     priv->port.ops = &uartlite_port_ops;     INIT_WORK(&priv->rx_work, uartlite_rx_work);     priv->running = false;      tty_port_register_device(&priv->port, uartlite_tty_driver, 0, &pdev->dev);      pci_set_drvdata(pdev, priv);     dev_info(&pdev->dev, "UARTlite over XDMA registered as TTY");     return 0; }  static void uartlite_remove(struct pci_dev *pdev) {     struct uartlite_priv *priv = pci_get_drvdata(pdev);     tty_unregister_device(uartlite_tty_driver, 0);     tty_port_destroy(&priv->port); }  static const struct pci_device_id uartlite_pci_tbl[] = {     { PCI_DEVICE(VENDOR_ID, DEVICE_ID) },     { 0, } };  static struct pci_driver uartlite_pci_driver = {       .name = DRIVER_NAME,     .id_table = uartlite_pci_tbl,     .probe = uartlite_probe,     .remove = uartlite_remove, };  /* Module Initialization */ static int __init uartlite_init(void) {     int ret;      uartlite_tty_driver = tty_alloc_driver(1, TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);     if (IS_ERR(uartlite_tty_driver))         return PTR_ERR(uartlite_tty_driver);      uartlite_tty_driver->driver_name = DRIVER_NAME;     uartlite_tty_driver->name = "ttyUL";     uartlite_tty_driver->major = 0;     uartlite_tty_driver->minor_start = 0;     uartlite_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;     uartlite_tty_driver->subtype = SERIAL_TYPE_NORMAL;     uartlite_tty_driver->init_termios = tty_std_termios;     uartlite_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;      tty_set_operations(uartlite_tty_driver, &uartlite_tty_ops);      ret = tty_register_driver(uartlite_tty_driver);     if (ret) {         tty_driver_kref_put(uartlite_tty_driver);         return ret;     }      ret = pci_register_driver(&uartlite_pci_driver);     if (ret) {         tty_unregister_driver(uartlite_tty_driver);         tty_driver_kref_put(uartlite_tty_driver);     }      return ret; }  static void __exit uartlite_exit(void) {     pci_unregister_driver(&uartlite_pci_driver);     tty_unregister_driver(uartlite_tty_driver);     tty_driver_kref_put(uartlite_tty_driver); }  module_init(uartlite_init); module_exit(uartlite_exit);  MODULE_LICENSE("GPL v2"); MODULE_AUTHOR("Konstantin"); MODULE_DESCRIPTION("UARTlite TTY driver over XDMA with RX support");

3.6 Сборка, тестирование и отладка

Перед сборкой драйвера убедись, что установлены заголовочные файлы текущего ядра:

sudo apt update sudo apt install -y linux-headers-$(uname -r)

Проверка:

ls /lib/modules/$(uname -r)/build

Если каталог существует, значит заголовочные файлы установлены.

Сборка драйвера

Ниже представлена Makefile для сборки драйвера

# Build the kernel module obj-m += uartlite_xdma.o  # Kernel build directory (default: current running kernel) KDIR ?= /lib/modules/$(shell uname -r)/build  # Build the module all:     make CC=/usr/bin/gcc-13 -C $(KDIR) M=$(PWD) modules  # Clean up compiled files clean:     make -C $(KDIR) M=$(PWD) clean  # Install the module into the system install:     make -C $(KDIR) M=$(PWD) modules_install  # Enable debugging symbols (-g flag for debugging) EXTRA_CFLAGS += -g  # Define targets that are not actual files .PHONY: all clean install

Запустим команду:

make

После успешной сборки появится файл uartlite_xdma.ko — готовый модуль ядра.

Примеры вывода при правильной сборке драйвера

Проверка:

ls -l uartlite_xdma.ko

в мое случае следующий вывод:

-rwxr-xr-x 1 nvx root 390152 Mar 13 18:15 uartlite_xdma.ko

Запуск драйвера

Загрузка драйвера в ядро и проверяем, что драйвер загружен:

sudo insmod uartlite_xdma.ko lsmod | grep uartlite_xdma

Если всё правильно, модуль появится в списке.

uartlite_xdma          12288  0

Вывод логов драйвера:

sudo dmesg | tail -n 20

Пример вывода:

Проверка создания TTY-устройства:

ls /dev/ttyUL*

Пример вывода:

/dev/ttyUL0

Проверка работы

sudo minicom -D /dev/ttyUL0 

Ожидаемый вывод:

$GPGGA,123456.78,5540.1234,N,03734.5678,E,1,08,0.9,100.0,M,0.0,M,,*47

Мой пример вывода (GPS антенна не подклчена):

Так же можно подключить наш tty к gpsd:

Запускаем gpsd, привязываем его к ttyUL0:

sudo gpsd /dev/ttyUL0 -F /var/run/gpsd.sock
  • /dev/ttyUL0 — устройство UART, к которому подключён GPS

  • -F /var/run/gpsd.sock — создаёт UNIX-сокет для работы с GPS

или:

sudo gpsd -N -D3 -F /var/run/gpsd.sock /dev/ttyUL0
  • -N — не уходит в фон (можно сразу видеть ошибки).

  • -D3 — включает отладку.

  • /dev/ttyUL0 — указываем правильный UART-порт GPS.

Проверяем, работает ли gpsd

ps aux | grep gpsd

Если gpsd запущен, должно быть что-то вроде:

root         7131  0.0  0.0   9084  2432 pts/3    S+   18:51   0:00 grep --color=auto gpsd

Запускаем cgps (отображает координаты, скорость, высоту и время):

cgps -s

Если GPS работает, появится информация, например:

Time: 2025-03-13T12:34:56Z  Latitude: 55.7558 N  Longitude: 37.6173 E  Speed: 0.5 km/h  Altitude: 200 m

Проверяем сырые данные с GPS-модуля:

gpspipe -r

Это покажет сырые NMEA-пакеты, например:

$GPGGA,123456.00,5537.348,N,03736.882,E,1,08,1.0,200.0,M,0.0,M,,*47  $GPRMC,123456.00,A,5537.348,N,03736.882,E,0.5,45.0,130324,,,A*7C

Удаление драйвера

Выгрузка модуля:

sudo rmmod uartlite_xdma

Очистка собранных файлов:

make clean

5 Работа с UARTlite через XDMA на Python

Помимо использования драйвера, можно работать с UARTlite напрямую через XDMA с помощью Python. Это полезно для отладки и быстрого тестирования или для безопастной реализации функционала в userspace.

5.1 Сборка драйвера XDMA

Перед использованием XDMA необходимо скомпилировать и установить драйвер. Драйвер XDMA поставляется Xilinx и поддерживает работу с PCIe-устройствами, реализующими DMA.
XDMA Linux Kernel Drivers GitHub

5.2 Загрузка исходников драйвера XDMA

Исходники драйвера доступны в официальном репозитории Xilinx:

git clone https://github.com/Xilinx/dma_ip_drivers.git cd dma_ip_drivers/XDMA/linux-kernel###

5.3 Компиляция драйвера

Перед сборкой убедитесь, что у вас установлены заголовочные файлы ядра:

sudo apt-get install linux-headers-$(uname -r)

Компилируем драйвер :

make

пример вывода:

5.4 Установка и загрузка драйвера

sudo make install sudo modprobe xdma

пример вывода:

Если не получилось , есть другой путь загрузки драйвера после сборки:

cd dma_ip_drivers/XDMA/linux-kernel/tests/ sudo ./load_driver.sh 

Ожидаемый вывод:

interrupt_selection . xdma                   24576  0 Loading driver...insmod xdma.ko interrupt_mode=2 ...  The Kernel module installed correctly and the xmda devices were recognized. DONE

Проверяем, что модуль загружен:

lsmod | grep xdma

ожидаемый вывод:

xdma                  110592  0

После установки драйвера в системе появятся файлы устройств:

ls /dev/xdma*

Ожидаемый вывод:

/dev/xdma0_c2h_0  /dev/xdma0_h2c_0  /dev/xdma0_control  /dev/xdma0_user

В моем случае:

Файл

Описание

/dev/xdma0_c2h_0

C2H (Card-to-Host) – передача данных из FPGA в CPU

/dev/xdma0_h2c_0

H2C (Host-to-Card) – передача данных из CPU в FPGA

/dev/xdma0_user

Интерфейс для пользовательских команд (если используется в FPGA).

/dev/xdma0_xvc

**Xilinx Virtual Cable (XVC) – позволяет использовать JTAG через PCIe. Если используется XVC (JTAG-over-PCIe), можно проверить с  telnet localhost 2542

/dev/xdma0_control

Регистр управления XDMA

/dev/xdma0_events_0/dev/xdma0_events_15

события от FPGA к CPU. Можно прослушивать события. Если XDMA настроен на прерывания, этот файл выдаст данные при каждом событии.

  • DMA-драйвер загружен и создал файлы в /dev/.

  • Можно чтение/запись через c2h_0 (FPGA → CPU) и h2c_0 (CPU → FPGA).

  • Прерывания можно отслеживать через xdma_events_*.

  • XVC позволяет работать с JTAG через PCIe.

Если файлы появились, драйвер установлен и XDMA готов к работе, то можно переходить к тестированию UARTlite.

5.6 Код работы с UARTlite через XDMA в Python (uaxdma.py)

Принцип работы:
Скрипт открывает устройство /dev/xdma0_user, отображая память XDMA в адресное пространство процесса с помощью mmap. Затем он обращается к регистрам UARTlite по смещению 0x40000, читая данные из RX или записывая в TX . Это позволяет напрямую взаимодействовать с UARTlite без использования драйвера ядра.

import os import time import numpy as np import mmap import asyncio  # ============================ # #  XDMA + UARTLite (Объединено) # ============================ # class XdmaUartLite:     BASE_ADDR = 0x40000  # Базовый адрес UARTLite в XDMA      # Оффсеты регистров UARTLite     RX_FIFO = 0x00     TX_FIFO = 0x04     STATUS = 0x08     CONTROL = 0x0C      # Флаги     TX_FULL = 0x08     RX_VALID = 0x01     TX_RESET = 0x01     RX_RESET = 0x02      def __init__(self, device_index=0):         """Инициализация XDMA и UARTLite"""         base = f"/dev/xdma{device_index}"          # Открываем файлы для работы с XDMA         self.fd_user = os.open(f"{base}_user", os.O_RDWR)          # Создаём отображение памяти для быстрого доступа         self.m_rmap = np.frombuffer(mmap.mmap(self.fd_user, int(1e6)), np.uint32)          # Сбрасываем FIFO UARTLite         self.reset_fifos()      def close(self):         """Закрываем файлы XDMA при завершении"""         os.close(self.fd_user)       # ============================ #     #  Чтение/Запись через XDMA     # ============================ #     def read_reg(self, addr):         """Читаем 32-битное значение из регистра"""         return self.m_rmap[addr >> 2] & 0xFFFF      def write_reg(self, addr, data):         """Записываем 32-битное значение в регистр"""         self.m_rmap[addr >> 2] = np.uint32(data)      # ============================ #     #  Работа с UARTLite     # ============================ #     def reset_fifos(self):         """Сбрасываем FIFO передатчика и приёмника"""         self.write_reg(self.BASE_ADDR + self.CONTROL, self.TX_RESET | self.RX_RESET)      def send_byte(self, data):         """Отправляем 1 байт через UARTLite"""         while self.read_reg(self.BASE_ADDR + self.STATUS) & self.TX_FULL:             pass  # Ждём, пока FIFO не освободится         self.write_reg(self.BASE_ADDR + self.TX_FIFO, data)      def recv_byte(self):         """Получаем 1 байт через UARTLite"""         while not (self.read_reg(self.BASE_ADDR + self.STATUS) & self.RX_VALID):             pass  # Ждём, пока появятся данные         return self.read_reg(self.BASE_ADDR + self.RX_FIFO)      def send_data(self, data):         """Отправляем массив байтов через UARTLite"""         for byte in data:             self.send_byte(byte)      def recv_data(self, size):         """Получаем массив байтов через UARTLite"""         return bytearray([self.recv_byte() for _ in range(size)])      async def recv_byte_async(self):         """Асинхронное чтение 1 байта."""         while not (self.read_reg(self.BASE_ADDR + self.STATUS) & self.RX_VALID):             await asyncio.sleep(0.001)  # Дадим CPU отдохнуть, чтобы не нагружать 100%         return self.read_reg(self.BASE_ADDR + self.RX_FIFO)      async def recv_data_async(self, size):         """Асинхронное чтение массива байтов."""         return bytearray([await self.recv_byte_async() for _ in range(size)])  # ============================ # #  Тестирование XDMA + UARTLite # ============================ #    ### Простой вариант  # if __name__ == "__main__": #     # Инициализируем XDMA+UARTLite #     xdma_uart = XdmaUartLite(device_index=0)  #     print("Ожидание данных с UARTLite...") #     while True: #         data = xdma_uart.recv_data(128)  # Читаем 128 байта #         if data: #             print(data.decode(errors="ignore").strip())  # Убираем пустые строки и пробелы   ### Вариант с async async def main():     uart = XdmaUartLite()     while True:         data = await uart.recv_data_async(128)         print(data.decode(errors="ignore"))  asyncio.run(main())

Запустим :

sudo python3 uaxdma.py

Пример вывода:

5.6 Профилирование

Анализ производительности uaxdma.py с помощью py-spy для реализации с async:

для обычной реализации:

Загруженность процессора

  • Polling (второе изображение): 100% CPU, read_reg занимает 93% времени.

  • Async-версия (первое изображение): 1% CPU, read_reg практически не нагружает систему.

Время выполнения функций

  • Polling: read_reg занимает 8.7s из 8.7s (то есть программа почти полностью занята проверкой данных).

  • Async: read_reg выполняется всего 0.01s, а основное время уходит на asyncio.sleep(), что снижает нагрузку.

6. Выводы

Разработанные решения обеспечивают стабильную передачу данных от GPS-модуля через UARTlite к приложению, упрощая интеграцию периферии FPGA в SDR-системы. Использование прерываний вместо polling может снизить нагрузку на CPU, а оптимизация буферизации — увеличить пропускную способность. Это делает решение полезным для широкого круга проектов, от прототипирования до промышленных приложений.

Данный проект реализует два подхода к работе с UARTlite через XDMA:

  1. Классический Linux-драйвер с TTY, который позволяет работать с устройством как с обычным UART

  2. Прямая работа через Python – даёт возможность разрабатывать приложения без написания kernel драйверов, а так же отлаживать код в безопастном режиме по сравнению с разработкой в kernel.

Достоинства решения:

Поддержка стандартного Linux TTY

Возможность работы через Python без собственного драйвера

Возможные улучшения:

Добавить поддержку прерываний вместо polling


7. Заключение

Разработка Linux-драйвера и работа с XDMA через Python позволяют эффективно использовать UARTlite и другие IP в проектах.

Так же статья есть на Medium!

📚 Дополнительные материалы

Если эта статья помогла вам не бояться писать драйверы, или хотя бы сэкономила пару вечеров на отладке XDMA — значит, я старался не зря. 😉

В мире FPGA и Linux драйверов всегда найдётся что-то, что можно сделать красивее. Главное — не бояться лезть под капот. Всем лёгкой разработки и
Удачной сборки, мягких загрузок, и чтоб dmesg всегда был полон только хорошими новостями, и меньше kernel panic!🙌

👉 Понравилось? Жду ваши вопросы и мысли в комментариях!


ссылка на оригинал статьи https://habr.com/ru/articles/900644/