Затолкаем, братцы!!! UART Lite через PCIe прямиком в Linux: драйвер за вечер (почти)
- среда, 16 апреля 2025 г. в 00:00:16
Иногда самые простые задачи превращаются в мини-приключения. Например, когда вам нужно подключить 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.
Встраиваемые системы на базе 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
Проект основан на разработанной с нуля 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 |
Хост-плата | |
Интерфейс передачи | PCIe Gen1 x4 (10 Gbps через разъём M.2) |
Передача данных | Через XDMA и AXI шину |
UART-периферия | AXI UARTLite IP Core (9600 бод) |
Форм-фактор | M.2 2280 (80mm × 22mm) |
Система состоит из следующих компонентов и их связей:
SIM68 (GPS Module)
Это GPS-модуль, который отправляет данные через интерфейс UART.
Связан с UartLite (UART Controller) через UART.
UartLite (UART Controller)
Контроллер UART, принимающий данные от GPS-модуля.
Связан с XDMA (PCIe DMA) через шину AXI.
XDMA (PCIe DMA)
Контроллер прямого доступа к памяти (DMA) через PCIe. (AXI PCIe DMA Product Guide (PG195))
Связан с CPU (Latte Panda Sigma) через PCIe.
CPU (Latte Panda Sigma)
Центральный процессор, который обрабатывает данные от XDMA.
Связан с Linux Kernel через обозначение User-space (пространство пользователя).
Linux Kernel
Ядро операционной системы Linux, работающее на CPU.
Связан с User Application через интерфейсы TTY / Python.
User Application
Пользовательское приложение, которое взаимодействует с ядром Linux для доступа к данным, возможно, через TTY-устройства или скрипты на Python.
На изображении ниже представлена структурная схема системы с собственным драйвером:
данные проходят по цепочке:
SIM68 передает NMEA-сообщения в UartLite на FPGA через UART
UartLite взаимодействует с XDMA (PCIe DMA) через AXI.
XDMA передаёт данные через PCIe в CPU (Latte Panda Sigma).
CPU обрабатывает данные в user-space и взаимодействует с ядром Linux.
Linux Kernel предоставляет интерфейс TTY / Python API для пользовательского приложения (/dev/ttyUL0
).
User Application – взаимодействует с UART через Python или терминал.
На изображении ниже представлена структурная схема системы при прямом доступе через Python:
Данные проходят по цепочке:
SIM68 передает NMEA-сообщения в UartLite на FPGA через UART.
UartLite взаимодействует с XDMA (PCIe DMA) через AXI.
XDMA передаёт данные через PCIe в CPU (Latte Panda Sigma).
CPU через /dev/xdma0_user
мапит пространство устройства с помощью mmap.
Python Application напрямую читает регистры UARTlite (RX FIFO / TX FIFO), минуя драйвер ядра.
Таким образом, взаимодействие происходит без использования стандартного Linux TTY-драйвера, что позволяет:
Минимизировать накладные расходы на системные вызовы.
Снизить нагрузку на процессор за счёт использования async IO.
Быстро прототипировать и отлаживать системы на основе FPGA.
На диаграмме ниже показано, как происходит обмен данными между компонентами в системе:
В проекте используется XDMA (PCIe-DMA Bridge)
AXI Interconnect связывает XDMA с AXI Register Slice, AXI UARTLite
AXI UARTLite подключен к системе через AXI Interconnect и к GPS модулю SIM68
Есть также AXI GPIO, которые могут использоваться для управления*
Основной поток данных:
FPGA ↔ AXI Interconnect ↔ AXI UARTLite ↔ PCIe XDMA ↔ CPU/Linux
Ниже представлен скрин проекта в Vivado из реального проекта:
или простая реализация только для UartLite:
Частота AXI CLK: 62.5 МГц
Скорость UART: 9600 бод
Длина данных: 8 бит
Четность (Parity): Отключена (No Parity)
Эти настройки стандартные для UARTLite, но если необходимо увеличить скорость передачи, можно выставить 115200 бод
.
AXI PCIe DMA Product Guide (PG195)
Количество каналов
1 канал на чтение (H2C)
1 канал на запись (C2H)
Количество Request IDs: 32 (чтение), 16 (запись)
AXI ID Width: 4 (идентификатор транзакций в AXI)
Descriptor Bypass (Отключен)
Используется стандартное управление буферами (без байпаса).
Количество прерываний: 16
MSI включен (Message Signaled Interrupts)
Расширенные теги включены (Extended Tag Field)
PCIe → AXI Lite Master Interface
Размер памяти: 1MB
Адрес: 0x00000000
PCIe → DMA Interface включен
Vendor ID: 10EE
(Xilinx)
Device ID: 7011
Class Code: 120000
(устройство памяти)
[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:
Аппаратная платформа:
SDR-плата: разработанная мной, FPGA Artix-7 200T
GPS-модуль: SIM68
Интерфейсы: PCIe, UART
Материнская плата: Latte Panda Sigma
Программное обеспечение:
ОС: Linux Kernel 6.x+
Инструменты: Vivado, Python, GCC
Отладка: dmesg
, minicom
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
После установки убедимся, что PCIe-устройство (FPGA) распознаётся системой:
lspci -d 10ee:
Если XDMA-карта видна, будет вывод типа:
59:00.0 Memory controller: Xilinx Corporation Device 7011
Вот что показывает у меня:
При загрузке модуля драйвер регистрирует PCIe-устройство с Vendor ID 0x10EE
и Device ID 0x7011
. После инициализации он создаёт TTY-устройство /dev/ttyUL0
. Входящие данные из UARTlite обрабатываются через механизм workqueue, передаются в буфер TTY и становятся доступны пользователю через функцию tty_flip_buffer_push
(она сообщает ядру Linux, что в приёмном буфере появились новые данные, и эти данные нужно передать в пользовательское пространство и без её вызова данные будут висеть внутри драйвера и не дойдут до пользователя).
#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.
port
— TTY-порт для связи с Linux.
rx_work
— задача для обработки входящих данных (workqueue).
running
— флаг работы (установлен, пока устройство активно).
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 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-драйвер следующая функция:
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
структуру.
0x10EE
— Vendor ID (Xilinx).
0x7011
— Device 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);
/*
* 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");
Перед сборкой драйвера убедись, что установлены заголовочные файлы текущего ядра:
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
Помимо использования драйвера, можно работать с UARTlite напрямую через XDMA с помощью Python. Это полезно для отладки и быстрого тестирования или для безопастной реализации функционала в userspace.
Перед использованием XDMA необходимо скомпилировать и установить драйвер. Драйвер XDMA поставляется Xilinx и поддерживает работу с PCIe-устройствами, реализующими DMA.
XDMA Linux Kernel Drivers GitHub
Исходники драйвера доступны в официальном репозитории Xilinx:
git clone https://github.com/Xilinx/dma_ip_drivers.git
cd dma_ip_drivers/XDMA/linux-kernel###
Перед сборкой убедитесь, что у вас установлены заголовочные файлы ядра:
sudo apt-get install linux-headers-$(uname -r)
Компилируем драйвер :
make
пример вывода:
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
В моем случае:
Файл | Описание |
---|---|
| C2H (Card-to-Host) – передача данных из FPGA в CPU |
| H2C (Host-to-Card) – передача данных из CPU в FPGA |
| Интерфейс для пользовательских команд (если используется в FPGA). |
| **Xilinx Virtual Cable (XVC) – позволяет использовать JTAG через PCIe. Если используется XVC (JTAG-over-PCIe), можно проверить с |
| Регистр управления XDMA |
| события от FPGA к CPU. Можно прослушивать события. Если XDMA настроен на прерывания, этот файл выдаст данные при каждом событии. |
DMA-драйвер загружен и создал файлы в /dev/
.
Можно чтение/запись через c2h_0
(FPGA → CPU) и h2c_0
(CPU → FPGA).
Прерывания можно отслеживать через xdma_events_*
.
XVC позволяет работать с JTAG через PCIe.
Если файлы появились, драйвер установлен и XDMA готов к работе, то можно переходить к тестированию UARTlite.
Принцип работы:
Скрипт открывает устройство /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
Пример вывода:
Анализ производительности 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()
, что снижает нагрузку.
Разработанные решения обеспечивают стабильную передачу данных от GPS-модуля через UARTlite к приложению, упрощая интеграцию периферии FPGA в SDR-системы. Использование прерываний вместо polling
может снизить нагрузку на CPU, а оптимизация буферизации — увеличить пропускную способность. Это делает решение полезным для широкого круга проектов, от прототипирования до промышленных приложений.
Данный проект реализует два подхода к работе с UARTlite через XDMA:
Классический Linux-драйвер с TTY, который позволяет работать с устройством как с обычным UART
Прямая работа через Python – даёт возможность разрабатывать приложения без написания kernel драйверов, а так же отлаживать код в безопастном режиме по сравнению с разработкой в kernel.
Поддержка стандартного Linux TTY
Возможность работы через Python без собственного драйвера
Добавить поддержку прерываний вместо polling
Разработка Linux-драйвера и работа с XDMA через Python позволяют эффективно использовать UARTlite
и другие IP в проектах.
Так же статья есть на Medium!
Если эта статья помогла вам не бояться писать драйверы, или хотя бы сэкономила пару вечеров на отладке XDMA — значит, я старался не зря. 😉
В мире FPGA и Linux драйверов всегда найдётся что-то, что можно сделать красивее. Главное — не бояться лезть под капот. Всем лёгкой разработки и
Удачной сборки, мягких загрузок, и чтоб dmesg
всегда был полон только хорошими новостями, и меньше kernel panic!🙌
👉 Понравилось? Жду ваши вопросы и мысли в комментариях!