尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPGA与Linux间基于PCIe的UART通信:TTY驱动开发与Python访问实践

FPGA与Linux间基于PCIe的UART通信:TTY驱动开发与Python访问实践 1. 项目概述从FPGA到Linux的串行通信桥梁最近在做一个嵌入式数据采集项目核心需求是把FPGA实时采集到的高速传感器数据稳定地送到上位机Linux系统里做分析和可视化。FPGA这边我用的是Xilinx的Artix-7数据准备好了但怎么高效、可靠地传出去是个问题。直接用GPIO模拟时序太慢用DMA配合自定义IP核开发周期又长。这时候我想到了FPGA里一个经典又实用的IP核UARTLite。它结构简单资源占用少但光有它还不够数据到了FPGA的边界还得能“过河”到CPU。于是我选择了PCIe这条“高速公路”作为FPGA与主机之间的物理桥梁。这个项目的标题“UARTLite FPGA to Linux: TTY Driver Python Access via PCIe”清晰地勾勒出了整个技术栈FPGA端用UARTLite IP核实现串行通信引擎物理链路通过PCIe总线连接FPGA板卡和Linux主机Linux内核需要编写一个TTY驱动将PCIe BAR空间映射的UARTLite寄存器暴露成标准的/dev/ttySX设备用户空间则用Python脚本像操作普通串口一样读写这个设备完成数据获取。这本质上是在硬件层面“虚拟”出了一个串口但它的“物理层”是高速的PCIe而非传统的RS-232电平。对于需要将FPGA作为高性能数据源或协处理器集成到Linux系统中的场景这套方案在开发效率、传输可靠性和软件生态兼容性之间取得了很好的平衡。2. 核心需求与方案选型背后的考量为什么是UARTLitePCIeTTY驱动这个组合这源于几个核心且相互关联的需求。2.1 对软件生态的兼容性需求项目最终要在Linux上跑数据分析算法团队最熟悉的语言是Python有大量现成的库如pyserial,numpy,matplotlib可以用于串口通信、数据处理和绘图。如果采用自定义的驱动接口比如一个简单的/dev/mycustom_device字符设备那么上层就需要编写专用的读写和控制库增加了开发成本和后期维护的复杂性。而TTY驱动是Linux内核中成熟、稳定的框架它将设备抽象成统一的文件接口。一旦驱动成功在Python中只需要import serial然后ser serial.Serial(‘/dev/ttyS2’)就能操作几乎零学习成本能立刻复用团队现有的软件资产和开发经验。2.2 对数据传输可靠性与简易性的平衡UART协议虽然速度不如DMA或AXI-Stream快但它有明确的帧结构起始位、数据位、停止位自带简单的错误检测奇偶校验通信逻辑清晰。对于中等数据速率比如几Mbps以下、非连续爆发的传感器数据流UART完全够用。更重要的是它的控制非常简单FPGA端只需要实现FIFO和移位寄存器主机端通过读写几个状态/数据寄存器就能完成交互。相比于实现一个完整的DMA控制器及其配套的驱动UARTLite方案的硬件逻辑和驱动代码复杂度都大大降低从而显著提升了项目的整体可靠性降低了调试难度。2.3 PCIe链路选择的必然性这是连接FPGA与现代x86/ARM主机的标准高性能方案。相比老的PCI总线PCIe速率高、引脚少相比USB它延迟更低、带宽更确定并且不需要额外的协议转换芯片如FTDI的USB转UART芯片实现了FPGA与CPU的直接内存映射访问。通过PCIeFPGA上的UARTLite IP核的寄存器会被映射到主机内存的某个物理地址即BAR空间Linux驱动可以直接用ioremap将其映射到内核虚拟地址空间然后像访问内存一样读写这些寄存器。这种内存映射IOMMIO方式效率极高。2.4 驱动类型的抉择为什么是TTY而不是简单的字符设备Linux驱动有多种类型。最简单的可以写一个“杂项设备”miscdevice或“平台设备”platform driver驱动暴露read/write/ioctl接口。但这需要应用层自己处理串行通信的细节比如波特率设置、流控、数据缓冲等。而TTY子系统终端子系统已经为我们封装好了这一切。它管理了线路规程line discipline、控制台、波特率生成、终端设置通过termios结构体等复杂功能。我们的驱动只需要实现最底层的硬件操作即uart_ops结构体中的函数如启动、停止、设置波特率、发送一个字符、读取一个字符剩下的缓冲、排队、用户态API都由TTY核心层搞定。这相当于站在了巨人的肩膀上避免了重复造轮子。3. FPGA端设计UARTLite IP核配置与PCIe集成整个硬件设计的起点在Vivado或Quartus中。目标是创建一个包含PCIe Endpoint和UARTLite的FPGA逻辑并将两者连接起来。3.1 UARTLite IP核参数化配置在IP Catalog中找到“UART Lite”双击进行配置。这里有几个关键参数决定了硬件行为波特率Baud Rate这个参数在IP核生成时设置但注意在TTY驱动中我们还会通过分频器动态设置波特率。通常IP核内部的波特率生成器会参考一个输入时钟如100MHz。为了灵活性我通常将IP核的波特率设置为一个较高的值如115200或921600然后在驱动中通过计算分频比来匹配目标波特率。IP核的波特率参数应大于等于驱动中可能设置的最大值。数据位Data Bits选择8。这是最通用的设置与字节对齐。奇偶校验Parity选择“无None”。如果需要简单的错误检测可以选择奇校验或偶校验但这会增加驱动中处理校验位的逻辑。对于可靠性要求高的场景建议在应用层使用更健壮的校验如CRC。停止位Stop Bits选择1。使能发送/接收FIFOEnable TX/RX FIFO强烈建议勾选并将FIFO深度设置为16或32。FIFO是平滑数据流、防止丢失的关键硬件缓冲。没有FIFO意味着每个字符的到达都会产生中断在高速或高负载下会压垮系统。中断Interrupt确保“使能中断”选项被选中。我们将使用“发送空TX empty”和“接收数据有效RX valid”中断来高效地驱动数据传输。配置完成后IP核会生成一组标准的AXI4-Lite或LocalLink接口信号包括读写地址、数据、响应以及中断信号。3.2 PCIe IP核配置与地址空间分配添加“PCIe Integrated Block”或“PCIe Hard IP”核。关键配置设备类型Device Type选择“Endpoint”。链路速度与宽度Lane Width根据板卡硬件和支持的规格选择如Gen2 x1。x1的带宽对于UART通信绰绰有余。BARBase Address Register设置这是连接FPGA逻辑与主机内存映射的桥梁。我们需要为UARTLite的寄存器分配一个BAR空间。通常使用BAR0大小设置为4KB或更大必须是2的幂次方且满足PCIe规范的最小要求。4KB足以映射UARTLite的少量寄存器。BAR的类型设置为“Memory”属性设置为“可预取Prefetchable”以获得更好的性能。在“Address Editor”或“AXI Interconnect”中将PCIe IP核的AXI主接口M_AXI或配置空间与一个AXI Interconnect相连再将Interconnect的从接口S_AXI连接到UARTLite IP核的AXI-Lite接口。这样主机对PCIe BAR0地址空间的访问就会通过AXI总线路由到UARTLite的寄存器上。3.3 硬件连接与中断处理连接的关键点在于中断。PCIe使用MSIMessage Signaled Interrupt或INTx传统引脚中断机制。MSI是首选它延迟更低不共享中断线。在PCIe IP核配置中使能MSI。将UARTLite的中断输出信号interrupt连接到PCIe IP核的MSI中断生成逻辑输入。在Vivado中这可能需要通过一个Concat核或直接连接到msi_intr_vector相关的端口。在驱动中我们需要正确请求和配置MSI中断。完成连接后生成比特流文件下载到FPGA。此时在Linux系统中执行lspci -v命令应该能看到一个新的PCI设备并且其BAR0地址已经分配。实操心得FPGA调试第一步在写驱动之前先用一个轻量级工具验证FPGA的PCIe和寄存器访问是否正常。我推荐使用pcimem工具GitHub上可找到。在系统加载FPGA比特流后以root权限运行sudo ./pcimem /sys/bus/pci/devices/0000:01:00.0/resource0 0x0 w。这里的0000:01:00.0是你的PCI设备地址通过lspci -D查看resource0通常对应BAR0。这个命令会尝试从BAR0的偏移0地址读取一个32位字。如果FPGA设计正确你应该能读到一个确定的值比如UARTLite的IP核版本号寄存器。这一步能快速隔离问题是出在FPGA硬件还是Linux驱动上。4. Linux TTY驱动开发详解这是项目的核心软件部分。我们将创建一个内核模块它首先是一个PCI驱动然后在其内部注册一个UART驱动到TTY子系统。4.1 驱动框架搭建PCI驱动与探测Probe首先定义一个pci_driver结构体并填充其id_table用于匹配PCI设备的厂商ID和设备ID这些信息在FPGA的PCIe IP核中配置和probe函数。static struct pci_device_id my_uart_pci_ids[] { { PCI_DEVICE(VENDOR_ID, DEVICE_ID) }, // 替换为你的Vendor/Device ID { 0, } }; MODULE_DEVICE_TABLE(pci, my_uart_pci_ids); static int my_uart_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct uart_port *port; struct my_uart_priv *priv; // 自定义的私有数据结构 int ret, bar 0; // 1. 使能PCI设备 ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, Failed to enable PCI device\n); return ret; } // 2. 请求并映射BAR0内存区域 ret pci_request_region(pdev, bar, my_uart_pcie); if (ret) { dev_err(pdev-dev, Failed to request BAR%d\n, bar); goto err_disable; } priv-regs pci_ioremap_bar(pdev, bar); if (!priv-regs) { dev_err(pdev-dev, Failed to map BAR%d\n, bar); ret -ENOMEM; goto err_release; } // 3. 使能MSI中断 ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (ret 0) { dev_err(pdev-dev, Failed to allocate MSI interrupt\n); goto err_iounmap; } priv-irq pci_irq_vector(pdev, 0); // 4. 初始化uart_port结构体 port priv-port; port-dev pdev-dev; port-type PORT_UNKNOWN; // 或自定义一个类型 port-iotype UPIO_MEM; // 内存映射IO port-mapbase pci_resource_start(pdev, bar); // 物理基地址 port-membase priv-regs; // 映射后的虚拟地址 port-irq priv-irq; port-uartclk 100000000; // 输入给UARTLite IP核的时钟频率单位Hz用于计算波特率分频 port-fifosize 16; // 与IP核中FIFO深度一致 port-ops my_uart_ops; // 关键硬件操作函数集 port-flags UPF_FIXED_TYPE | UPF_FIXED_PORT; // 标志位 // 5. 向串口核心注册这个端口 ret uart_add_one_port(my_uart_driver, port); if (ret) { dev_err(pdev-dev, Failed to add UART port\n); goto err_free_irq; } // 6. 将私有数据保存到pci_dev中 pci_set_drvdata(pdev, priv); return 0; // 错误处理路径略 }4.2 实现硬件操作函数集uart_ops这是驱动与FPGA硬件对话的“语言”。需要实现一个struct uart_ops类型的变量。以下是几个最核心的函数static struct uart_ops my_uart_ops { .tx_empty my_uart_tx_empty, .set_mctrl my_uart_set_mctrl, // 通常为空除非支持硬件流控 .get_mctrl my_uart_get_mctrl, .stop_tx my_uart_stop_tx, .start_tx my_uart_start_tx, .stop_rx my_uart_stop_rx, .enable_ms my_uart_enable_ms, // 通常为空 .break_ctl my_uart_break_ctl, .startup my_uart_startup, .shutdown my_uart_shutdown, .set_termios my_uart_set_termios, // 关键设置波特率、数据格式 .type my_uart_type, .release_port my_uart_release_port, .request_port my_uart_request_port, .config_port my_uart_config_port, .verify_port my_uart_verify_port, }; // 示例启动函数注册中断处理程序 static int my_uart_startup(struct uart_port *port) { struct my_uart_priv *priv port-private_data; int ret; // 初始化硬件清除FIFO使能中断等 my_uart_write(port, UART_CR_REG, CR_ENABLE | CR_RX_INT_EN | CR_TX_INT_EN); // 注册中断处理函数 ret request_irq(port-irq, my_uart_interrupt, IRQF_SHARED, my_uart_pcie, port); if (ret) { dev_err(port-dev, Failed to request IRQ %d\n, port-irq); return ret; } return 0; } // 示例设置串口参数 static void my_uart_set_termios(struct uart_port *port, struct ktermios *termios, struct ktermios *old) { unsigned int baud; u32 divisor; // 1. 计算波特率分频器 baud uart_get_baud_rate(port, termios, old, 0, port-uartclk / 16); divisor DIV_ROUND_CLOSEST(port-uartclk, baud * 16); // 2. 将分频值写入FPGA的波特率寄存器假设偏移地址为BRR_REG my_uart_write(port, BRR_REG, divisor); // 3. 处理数据位、停止位、校验位通过控制寄存器CR_REG // 根据 termios-c_cflag 中的 CSIZE, CSTOPB, PARENB, PARODD 等位组合出硬件控制字 u32 cr_val CR_ENABLE; if (termios-c_cflag PARENB) { cr_val | CR_PARITY_EN; if (!(termios-c_cflag PARODD)) { cr_val | CR_PARITY_EVEN; } } // ... 其他位设置 my_uart_write(port, CR_REG, cr_val); // 4. 更新TTY核心的读取超时和阈值 uart_update_timeout(port, termios-c_cflag, baud); } // 示例中断处理函数 static irqreturn_t my_uart_interrupt(int irq, void *dev_id) { struct uart_port *port dev_id; u32 status my_uart_read(port, SR_REG); // 读取状态寄存器 bool handled false; spin_lock(port-lock); // 处理接收中断 if (status SR_RX_VALID) { my_uart_rx_chars(port); // 从RX FIFO读取所有字符通过 tty_insert_flip_char 送入TTY缓冲区 handled true; } // 处理发送中断 if (status SR_TX_EMPTY) { my_uart_tx_chars(port); // 从uart_port的环形缓冲区取出字符写入TX FIFO handled true; } spin_unlock(port-lock); return IRQ_RETVAL(handled); }4.3 编译、加载与设备节点创建编写好my_uart_pcie.c和对应的Makefile、Kconfig文件后将其编译为内核模块.ko文件。加载模块sudo insmod my_uart_pcie.ko查看PCI设备lspci -v应能看到驱动已绑定Kernel driver in use: my_uart_pcie。查看TTY设备dmesg | tail会显示类似my_uart_pcie 0000:01:00.0: ttyS2 at MMIO 0x.... (irq 123, base_baud 115200) is a my_uart_pcie的信息。这表明驱动成功创建了一个/dev/ttyS2设备节点。设置权限sudo chmod 666 /dev/ttyS2以便普通用户访问。注意事项并发与锁在中断处理函数my_uart_interrupt和UART操作函数如my_uart_start_tx,my_uart_set_termios中你会看到spin_lock(port-lock)。这是必须的因为中断上下文和进程上下文如用户调用write系统调用可能同时访问uart_port结构体中的环形缓冲区等共享数据。不加锁会导致数据竞争和系统崩溃。TTY核心提供的uart_port结构体已经包含了自旋锁port-lock直接使用即可。5. Python用户空间访问与性能优化驱动成功后用户空间的访问就变得极其简单和标准化。5.1 使用pyserial进行基础通信安装pyserial库pip install pyserial。一个简单的读写示例如下import serial import time # 打开设备参数与驱动中设置的termios一致 ser serial.Serial( port/dev/ttyS2, # 设备节点 baudrate115200, # 波特率驱动中的set_termios会处理 bytesizeserial.EIGHTBITS, # 数据位 parityserial.PARITY_NONE, # 校验位 stopbitsserial.STOPBITS_ONE, # 停止位 timeout1, # 读超时秒 xonxoffFalse, # 软件流控 rtsctsFalse # 硬件流控 ) if ser.is_open: print(f成功打开 {ser.port}) # 发送数据 data_to_send bHello FPGA from Python!\n ser.write(data_to_send) print(f已发送: {data_to_send}) # 接收数据阻塞直到收到指定字节数或超时 time.sleep(0.1) # 给FPGA一点响应时间 if ser.in_waiting 0: received_data ser.read(ser.in_waiting) print(f接收到: {received_data}) ser.close()5.2 应对高速数据流缓冲与异步IO当FPGA持续发送数据时简单的ser.read()可能因为Python的GIL或处理不及时导致缓冲区溢出在驱动或硬件FIFO中。优化策略增大内核缓冲区在驱动中可以调整uart_port结构体的fifosize但更有效的是利用TTY层的缓冲。不过用户空间更直接的优化是使用read循环与非阻塞模式ser.timeout 0 # 非阻塞模式 while running: data ser.read(1024) # 尝试读取最多1024字节立即返回 if data: process_data(data) # 快速处理数据 else: time.sleep(0.001) # 短暂休眠避免CPU空转使用多线程或asyncio将数据读取放在一个独立的线程中避免阻塞主线程。pyserial也支持asyncio适合事件驱动型应用。import serial_asyncio import asyncio class OutputProtocol(asyncio.Protocol): def data_received(self, data): # 异步处理接收到的数据 print(fReceived: {data}) async def main(): loop asyncio.get_running_loop() transport, protocol await serial_asyncio.create_serial_connection( loop, OutputProtocol, /dev/ttyS2, baudrate115200) # 保持连接运行 await asyncio.sleep(3600) asyncio.run(main())5.3 与FPGA的通信协议设计虽然底层是串口但传输的内容需要定义应用层协议。一个简单有效的帧结构示例[帧头 0xAA 0x55] [长度L] [命令字CMD] [数据区 DATA...] [校验和 CHK]帧头固定的2字节用于帧同步。长度1字节表示从CMD到CHK之前的字节数。命令字1字节指示数据类型如传感器数据、配置命令、状态查询。数据区可变长度由长度字段定义。校验和1字节可以是简单的累加和取反或CRC-8。在Python端接收后必须验证丢弃校验错误的帧。在Python端你需要实现一个“解帧器”持续从串口读取字节流识别帧头根据长度字段截取完整帧验证校验和然后分发处理。实操心得调试信息打印在驱动开发阶段内核的printk是主要调试手段。但要注意打印级别和频率。在中断处理函数中绝对不要使用printk(KERN_INFO “...”)这可能导致系统死锁或极大增加中断延迟。正确的做法是在probe、set_termios等非中断路径中打印关键信息或者使用dev_dbg()配合动态调试DYNAMIC_DEBUG。在用户空间可以用stty -F /dev/ttyS2 -a查看当前串口的所有termios设置用cat /proc/tty/driver/serial查看串口驱动状态如果驱动支持这些都是排查配置问题的利器。6. 常见问题排查与性能调优实录在实际部署中你肯定会遇到各种问题。以下是我踩过的一些坑和解决方案。6.1 驱动加载后没有创建/dev/ttyS*设备可能原因1uart_add_one_port失败。检查dmesg看是否有错误信息。常见原因是uart_port结构体字段填写不正确特别是iotype、mapbase、membase、irq。确保mapbase是PCI BAR的物理起始地址membase是ioremap返回的虚拟地址。可能原因2TTY核心分配的次设备号用尽。默认情况下8250标准串口驱动会占用大量的ttyS设备号。可以在uart_port中尝试设置port-line为一个较大的、未被使用的编号如32或者在驱动中调用uart_register_driver时指定一个不同的driver_name这样会创建/dev/ttyMyUart0这样的设备节点。排查命令dmesg | grep -i uart或dmesg | grep -i tty。6.2 能打开设备但读写无数据或数据错误可能原因1波特率不匹配。这是最常见的问题。首先确认驱动中port-uartclk设置是否正确它必须是FPGA中UARTLite IP核参考时钟的实际频率单位Hz。然后在Python端和驱动端打印计算出的分频值进行比对。可以用逻辑分析仪或示波器测量FPGA的TX引脚看实际发出的波形波特率是多少。可能原因2数据格式不匹配。检查驱动set_termios中关于数据位、停止位、校验位的设置逻辑是否与Python端serial.Serial的参数以及FPGA IP核的配置完全一致。例如IP核配置了奇校验驱动和Python端也必须配置奇校验。可能原因3中断未正确工作。如果驱动完全依赖中断进行收发而中断未触发数据就会积压在硬件FIFO中。检查/proc/interrupts看你的设备中断号是否有计数在增加。如果没有可能是FPGA中MSI中断信号未正确连接或驱动中request_irq失败。排查命令stty -F /dev/ttyS2查看当前设置cat /proc/interrupts | grep my_uart查看中断统计。6.3 传输速度慢或丢失数据可能原因1Python端处理太慢。这是用户空间应用的典型瓶颈。使用top命令查看Python进程的CPU占用率。如果接收线程是100% CPU说明处理速度跟不上。需要优化数据处理逻辑或者采用生产者-消费者模式将数据快速放入队列由另一个线程处理。可能原因2内核缓冲区太小。虽然TTY层有缓冲但对于突发的高速数据流可能不够。可以尝试在驱动中增大port-fifosize这需要与IP核的硬件FIFO深度匹配但更根本的是优化应用层读取逻辑使用非阻塞读取和更大的read块大小。可能原因3中断延迟或丢失。在高数据速率下如果每个字符都产生中断系统可能不堪重负。确保硬件FIFO深度设置合理如32并利用“接收FIFO触发中断水位”功能如果IP核支持。在驱动中一次中断应读取FIFO中的所有可用数据而不是一个字符。性能测试使用pv工具进行简单的吞吐量测试cat /dev/urandom | pv | sudo tee /dev/ttyS2 /dev/null发送测试或者sudo cat /dev/ttyS2 | pv /dev/null接收测试。观察实时速率。6.4 系统重启或热插拔后设备号变化问题今天设备是/dev/ttyS2明天可能变成/dev/ttyS3。解决方案不要依赖固定的设备节点。使用设备的稳定路径。PCI设备在sysfs中有唯一的标识符。通过udev规则创建持久化符号链接。创建文件/etc/udev/rules.d/99-my-uart.rules内容如下SUBSYSTEMtty, ATTRS{vendor}0x1234, ATTRS{device}0x5678, SYMLINKttyMyUartFPGA将0x1234和0x5678替换为你的PCI设备的实际厂商ID和设备ID通过lspci -nn查看。重新加载udev规则后无论系统分配什么主次设备号都会有一个固定的/dev/ttyMyUartFPGA指向你的设备。在Python程序中可以通过遍历/sys/class/tty/目录下的设备根据其device子目录中的vendor和device文件来动态发现设备。6.5 FPGA逻辑复位导致驱动挂起问题在驱动运行过程中如果FPGA被重新配置下载新的比特流PCIe链路会暂时断开寄存器访问会失败可能导致内核Oops或驱动无响应。策略驱动需要增加鲁棒性。在读写寄存器函数my_uart_read/my_uart_write中加入错误检查。如果读回的值是全F或全0等非法值可以尝试重新初始化硬件。实现一个看门狗watchdog机制。在驱动中创建一个内核定时器定期读取某个稳定的硬件寄存器如IP核版本号。如果连续多次读取失败则认为设备丢失驱动自行卸载或进入错误状态并在dmesg中报告。更优雅的方案是利用PCIe的热插拔支持但这需要更复杂的驱动和FPGA逻辑设计。对于大多数项目简单的超时和重试机制结合清晰的错误日志就足够了。
返回列表