Zynq-7000 PL侧扩展9路UARTLite串口:从Vivado到Linux驱动的完整指南
2026/9/18 16:17:32 网站建设 项目流程

简介:本资源面向Zynq嵌入式开发工程师与FPGA系统集成人员,聚焦PL侧扩展多路UART通信的实际工程痛点——Zynq PS端仅提供2路串口,难以满足工业控制、多传感器接入等场景下9路及以上串口需求。资源提供完整可运行的Vivado工程(含9路UARTLITE IP并行例化)、适配Linux系统的设备树源文件(.dts/.dtsi)及详细调试文档,覆盖从IP配置、约束添加、综合实现到设备树修改、内核编译与串口验证的全流程。压缩包共606个文件,27.43MB,包含73个Verilog逻辑文件、49个XDC约束、36个DCP综合结果、30个Shell脚本及4个设备树接口文件,结构清晰、模块分离,便于按阶段复现与问题定位。已有948人学习下载,配套文档深入剖析常见综合失败、设备树节点挂载异常、/dev/ttyUL*设备缺失等典型排错思路,显著降低多路UARTLITE在Zynq PL侧落地门槛。

1. 项目背景与核心价值

最近在做一个基于Zynq-7000系列SoC的工业网关项目,需要在PL(可编程逻辑)侧扩展多路串口来连接各种传感器和PLC。Xilinx官方提供的PS(处理系统)端UART控制器只有两个,远远不够用。这时候,一个非常经典且高效的方案就浮出水面了:使用PL侧的UARTLite IP核来扩展串口。这个方案听起来简单,但真要把从Vivado工程创建、IP核例化、约束文件编写,到设备树(Device Tree)配置、驱动加载这一整套流程跑通,尤其是要稳定地跑起来9路串口,中间有不少细节和“坑”需要留意。

网上关于UARTLite的零散教程很多,但要么只讲Vivado部分,要么只贴一段设备树代码,缺乏一个从硬件到软件、从理论到实操的完整闭环指南。特别是当串口数量增多时,地址分配、中断管理、设备树节点命名这些细节,如果处理不当,很容易导致驱动加载失败或者数据收发异常。这篇文章,我就结合自己最近成功实现9路UARTLite串口的实战经验,把每一步的操作、背后的原理以及容易踩的坑,掰开揉碎了讲清楚。无论你是FPGA新手,还是有一定经验的嵌入式工程师,都能跟着这个指南,一步步搭建起自己的多串口通信系统。

2. Vivado工程创建与UARTLite IP核配置

整个流程的起点在Vivado。我们的目标是在Zynq的PL部分,实例化9个独立的UARTLite IP核,并通过AXI总线与PS端的ARM处理器进行通信。

2.1 新建工程与添加Zynq Processing System

首先,打开Vivado,创建一个新的RTL工程,选择对应的Zynq器件型号(例如xc7z020clg400-1)。工程创建好后,第一步就是添加Zynq Processing System IP核。这是整个系统的核心,它代表了PS端的ARM双核Cortex-A9处理器及一系列外设。

  1. 在Block Design中,点击“+”号添加IP,搜索并添加“ZYNQ7 Processing System”。
  2. 双击添加进来的ZYNQ7 IP核,进行配置。这里有几个关键点:
    • MIO Configuration: 根据你的硬件板卡原理图,配置好PS端外设的引脚复用,例如QSPI Flash、SD卡、USB、以太网等。确保这些基础功能正常,系统才能启动。
    • Clock Configuration: 注意PS输入时钟(FCLK_CLK0)的频率,它通常来自板载晶振。同时,我们需要为PL提供时钟。在“PL Fabric Clocks”中,使能FCLK_CLK0(或其他可用的FCLK),并设置一个合适的频率,例如50MHz或100MHz。这个时钟将作为我们后续添加的PL逻辑的主时钟。
    • DDR Configuration: 正确配置DDR内存的型号、位宽和时钟频率,这是系统运行Linux所必需的。
    • 勾选UART0或UART1: 在“PS-PL Configuration” -> “General” -> “Enable UART 0”或“UART 1”。这一步是为了让PS端的UART控制器可用,我们通常用它作为Linux系统的控制台(Console),方便调试。注意:这是PS自带的UART,和我们后面要在PL添加的9路UARTLite是两回事。

配置完成后,点击“OK”,Vivado会自动生成一个包含ZYNQ7 IP的Block Design。

2.2 添加并配置UARTLite IP核

接下来就是主角登场了。我们需要连续添加9个UARTLite IP核。

  1. 再次点击“+”号添加IP,搜索“AXI Uartlite”。将其拖入Diagram中。你会看到一个名为axi_uartlite_0的IP。
  2. 关键的一步:配置第一个UARTLite实例。双击axi_uartlite_0,打开配置窗口。
    • Baud Rate: 设置你需要的波特率,例如115200。UARTLite的波特率是通过时钟分频产生的,所以这里的设置必须和后续提供给IP的时钟频率匹配。
    • Data Bits, Parity, Stop Bits: 根据你的通信协议设置,通常为8-N-1(8位数据,无校验,1位停止位)。
    • 最重要的是“Interface Options”
      • 确保“Enable UART”被勾选。
      • “BAUD”和“CLOCK”的频率设置:这里的“BAUD”就是你上面设置的波特率目标值。“CLOCK”频率必须填写我们之前从ZYNQ7 IP的FCLK输出给PL的时钟频率(例如50MHz)。UARTLite IP会基于这个CLOCK频率,内部计算分频系数来产生目标波特率。如果这里填错,实际波特率就会偏差,导致通信失败。
  3. 重复以上步骤,再添加8个AXI Uartlite IP核,分别命名为axi_uartlite_1axi_uartlite_8。每个的配置(波特率、数据格式)可以相同也可以不同,根据实际需求来。但请务必为每一个IP核单独配置,并确认其CLOCK频率设置正确。

2.3 连接时钟、复位与AXI总线

添加完IP后,我们需要用“连线”把它们组织成一个能工作的系统。

  1. 时钟连接

    • 将ZYNQ7 IP的FCLK_CLK0输出端口,连接到每一个UARTLite IP的s_axi_aclk(AXI总线时钟)和uart端口下的clk(UART模块时钟)。这确保了整个AXI通信和UART核心逻辑同步在同一个时钟下工作。你可以使用Vivado的“自动连接”功能,但连接后务必检查是否正确。
  2. 复位连接

    • 将ZYNQ7 IP的FCLK_RESET0_N输出端口,连接到每一个UARTLite IP的s_axi_aresetn(AXI总线复位,低电平有效)。这确保了系统上电或复位时,所有IP能同步复位。
  3. AXI总线连接

    • 这是连接PS和PL逻辑的数据通道。ZYNQ7 IP默认有一个M_AXI_GP0接口(通用AXI主端口)。我们需要一个AXI Interconnect(互联器)来管理一个主设备(PS)对多个从设备(9个UARTLite)的访问。
    • 添加一个“AXI Interconnect” IP核。将其S00_AXI从接口连接到ZYNQ7的M_AXI_GP0主接口。
    • 将AXI Interconnect的M00_AXIM08_AXI共9个主接口,分别连接到9个UARTLite IP的S_AXI从接口。这样,PS端的ARM处理器就能通过M_AXI_GP0,经由AXI Interconnect,访问到每一个UARTLite的内部寄存器了。
    • 地址分配:连接过程中,Vivado会自动为每个UARTLite分配一个基地址(Base Address)。你可以在“Address Editor”标签页中查看和微调。务必确保这9个地址空间没有重叠,且落在Zynq PS访问PL的地址范围内(通常是0x4000_0000到0x7FFF_FFFF或0x8000_0000到0xBFFF_FFFF,具体取决于配置)。记下这些地址,后续写设备树要用。
  4. 外部端口连接

    • 每个UARTLite IP都有RXTX信号。在Block Diagram中,分别右键点击每个UARTLite的RXTX,选择“Make External”。这会在设计顶层创建对应的引脚端口,例如uart_0_rxd,uart_0_txd一直到uart_8_rxd,uart_8_txd

2.4 生成顶层HDL与引脚约束

  1. 在Diagram空白处右键,选择“Create HDL Wrapper”,让Vivado根据Block Design自动生成顶层的Verilog或VHDL文件。

  2. 接下来是硬件设计的关键一步:编写XDC约束文件。你需要根据你的FPGA板卡原理图,将刚才创建的uart_x_rxduart_x_txd这些端口,映射到具体的FPGA物理引脚上,并设置正确的I/O标准(如LVCMOS33)。

    • 例如:set_property PACKAGE_PIN AB12 [get_ports {uart_0_txd}]
    • 例如:set_property IOSTANDARD LVCMOS33 [get_ports {uart_0_txd}]
    • 特别注意:UART是异步通信,RX线在空闲时应为高电平。通常FPGA引脚内部有弱上拉,但为了可靠,建议在约束文件或原理图中为RX引脚配置外部上拉电阻,避免因浮空产生误触发。
  3. 完成约束后,进行“Generate Bitstream”。这个过程包括综合、实现和生成比特流文件。如果一切顺利,你将得到一个.bit文件,它包含了整个PL部分的硬件配置信息。

3. 设备树(Device Tree)的详细配置

硬件比特流准备好了,接下来要让Linux系统认识并驱动这9个“新外设”。这就要靠设备树(DTS)来描述了。设备树相当于一张硬件的“地图”,告诉Linux内核在地址空间的什么位置有什么样的设备。

3.1 理解设备树中的UARTLite节点

UARTLite在Linux内核中通常有对应的驱动程序(xilinx_uartlite)。我们需要在设备树中为每一个UARTLite IP实例创建一个节点。

一个标准的UARTLite设备树节点如下所示:

axi_uartlite_0: serial@43c00000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x43c00000 0x10000>; interrupts = <0 29 4>; clocks = <&clkc 15>; clock-names = "s_axi_aclk"; current-speed = <115200>; port-number = <0>; status = "okay"; };

我们来逐行解析关键属性:

  • axi_uartlite_0: 节点标签(label),可以自定义,方便其他地方引用。
  • serial@43c00000: 节点名称。serial表示这是一个串口设备,@后面是该设备的基地址(Base Address),必须与Vivado Address Editor中分配的地址一致。这是最重要的信息之一,填错了内核就找不到硬件。
  • compatible = “xlnx,xps-uartlite-1.00.a”;: 兼容性字符串。这是驱动匹配设备的“密码”。必须准确填写为UARTLite驱动所识别的字符串。不同版本内核可能略有差异,但xlnx,xps-uartlite-1.00.a是通用的。
  • reg = <0x43c00000 0x10000>;: 寄存器区域。第一个值是基地址(同上),第二个值是地址空间的长度(length)。UARTLite的寄存器空间很小,通常0x10000(64KB)足够,也可以设为0x1000(4KB)。地址和长度必须与硬件设计匹配
  • interrupts = <0 29 4>;: 中断描述。这是一个难点。
    • 第一个值(0): 中断类型,0通常代表SPI(共享外设中断),这是ARM GIC的中断类型。
    • 第二个值(29):中断号(Interrupt ID)。这个数字不是随便写的,它由Zynq的硬件中断映射决定。在Vivado中,当你将UARTLite的interrupt端口引出并连接到xlconcat(中断连接器)再送给Zynq的IRQ_F2P时,可以在Address Editor或system.hdf/xparameters.h文件中找到每个IP的中断ID。务必使用Vivado分配的正确ID。例如,第一个UARTLite可能是29,第二个是30,以此类推。
    • 第三个值(4): 中断触发类型。4代表高电平触发(IRQ_TYPE_LEVEL_HIGH)。UARTLite通常使用电平触发。
  • clocksclock-names: 指定该设备使用的时钟源。&clkc 15表示指向clkc(时钟控制器)的第15个输出。这个数字需要参考你的Zynq PS-PL时钟配置。更简单的做法是,如果时钟频率已知,可以直接用clock-frequency属性指定,例如clock-frequency = <50000000>;(50MHz)。这个频率必须和Vivado中配置给UARTLite的“CLOCK”频率完全一致,否则波特率不准。
  • current-speed = <115200>;: 默认波特率。驱动加载时会以此波特率初始化串口。
  • port-number = <0>;: (可选)自定义属性,可以用来在用户空间标识这是第几个串口,方便应用程序识别。
  • status = “okay”;: 设备状态,设为“okay”表示启用该设备。

3.2 为9路串口编写完整的设备树源文件

在你的Linux内核源码目录(如arch/arm/boot/dts/)下,找到或创建对应你板子的.dtsi.dts文件。将9个UARTLite节点添加到amba_pl(或axi)总线节点下,因为它们是挂在AXI总线上的PL外设。

&amba_pl { axi_uartlite_0: serial@43c00000 { /* 配置详情见上,注意地址、中断号、时钟要逐一修改 */ compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x43c00000 0x10000>; interrupts = <0 29 4>; clock-frequency = <50000000>; current-speed = <115200>; port-number = <0>; status = "okay"; }; axi_uartlite_1: serial@43c10000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x43c10000 0x10000>; interrupts = <0 30 4>; clock-frequency = <50000000>; current-speed = <115200>; port-number = <1>; status = "okay"; }; // ... 依次添加 axi_uartlite_2 到 axi_uartlite_8 axi_uartlite_8: serial@43c80000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x43c80000 0x10000>; interrupts = <0 37 4>; // 假设中断号递增到37 clock-frequency = <50000000>; current-speed = <115200>; port-number = <8>; status = "okay"; }; };

注意:在实际操作中,中断号(29, 30...37)是示例,你必须根据Vivado工程中xlconcat输出的实际中断号来填写。一个可靠的查找方法是:在Vivado中生成比特流后,导出硬件(Export Hardware),在Vitis或SDK中打开,查看ps7_init.tclxparameters.h文件,搜索XPAR_INTC_0_UARTLITE_0_VEC_ID这样的宏定义,其值就是中断ID。

3.3 编译与更新设备树

  1. 修改好设备树源文件(.dts)后,使用设备树编译器(DTC)将其编译成二进制设备树 blob(.dtb)文件。
    dtc -I dts -O dtb -o devicetree.dtb system.dts
  2. 将生成的devicetree.dtb文件,连同Linux内核镜像(uImage)和FPGA比特流文件(system.bit或加载后的.bin文件),一起放到启动介质(如SD卡的FAT分区)中。
  3. 配置你的Bootloader(如U-Boot)在启动内核时加载这个新的dtb文件。

4. Linux系统下的驱动加载与测试

当系统启动后,如果一切配置正确,内核会自动识别并加载xilinx_uartlite驱动。

4.1 检查驱动加载情况

在Linux终端中,执行以下命令进行诊断:

  1. 查看内核启动日志

    dmesg | grep -i uart

    你应该能看到类似下面的信息,表明驱动成功探测到了硬件:

    [ 1.234567] xuartlite 43c00000.serial: at MMIO 0x43c00000 (irq = 29, base_baud = 5000000) is a xuartlite [ 1.234568] 43c00000.serial: ttyUL0 at MMIO 0x43c00000 (irq = 29, base_baud = 5000000) is a xuartlite ... (为其他8个串口打印类似信息)

    注意ttyUL0这个名称。UARTLite驱动创建的设备节点通常是/dev/ttyUL0,/dev/ttyUL1... 以此类推。

  2. 查看设备树节点状态

    ls /sys/bus/platform/devices/ | grep serial

    这会列出所有平台设备中名称包含“serial”的设备,应该能看到43c00000.serial等9个设备。

  3. 查看生成的设备文件

    ls -l /dev/ttyUL*

    确认9个设备文件(ttyUL0ttyUL8)都已创建。

4.2 串口通信测试

你可以使用任何串口调试工具进行测试,例如minicom,picocom,或者简单的echocat命令。

  1. 配置并打开一个串口(例如ttyUL0):

    stty -F /dev/ttyUL0 115200 cs8 -parenb -cstopb # 配置波特率等参数 picocom -b 115200 /dev/ttyUL0 # 使用picocom打开串口

    在另一个终端,向该串口发送数据:

    echo "Hello UARTLite 0" > /dev/ttyUL0

    picocom终端应该能看到接收到的字符串。

  2. 回环测试(Loopback Test): 这是验证硬件连接和驱动是否正常的最直接方法。将FPGA板上对应uart_0_txduart_0_rxd的引脚用杜邦线短接。然后在终端执行:

    cat /dev/ttyUL0 & # 后台监听串口输出 echo "Loopback Test" > /dev/ttyUL0

    如果驱动和硬件工作正常,你会在终端看到“Loopback Test”被打印出来。用同样的方法测试其他8路串口。

4.3 性能考量与多路复用

当9路串口同时高速收发数据时,需要考虑CPU中断负载。UARTLite每收到一个字节就会产生一个中断。在115200波特率下,每秒最多可产生约11520个中断(115200/10),对于9路就是每秒超过10万个中断。这对CPU是个负担。

  • 优化建议1:使用更高的波特率时钟。在Vivado中给UARTLite提供更高的CLOCK频率(如100MHz),可以让IP内部使用更精细的分频,但中断频率不变。
  • 优化建议2:驱动层使用DMA(如果支持)。但标准的UARTLite IP核不支持DMA,需要更高级的UART IP核如AXI UART16550。
  • 优化建议3:应用层缓冲与批量处理。在用户态应用程序中,使用select,pollepoll等多路复用I/O机制来同时监控多个串口,避免为每个串口创建一个阻塞的读线程,减少上下文切换开销。对于高吞吐场景,可以考虑使用内核的serial_core框架或编写专门的多端口优化驱动。

5. 常见问题排查与实战心得

在调试多路UARTLite的过程中,我遇到了几个典型问题,这里分享出来,希望能帮你节省时间。

5.1 问题:驱动加载成功,但/dev/ttyULx设备未出现

  • 可能原因1:设备树节点状态或兼容性字符串错误
    • 排查:检查dmesg日志,看是否有该地址设备的probe失败信息。确认compatible属性完全匹配内核驱动支持的字符串。检查status是否为“okay”
  • 可能原因2:内核配置未启用UARTLite驱动
    • 排查:确保内核编译时勾选了Device Drivers -> Character devices -> Serial drivers -> Xilinx uartlite serial port support
  • 可能原因3:地址冲突或错误
    • 排查:这是最常见的原因。反复核对设备树中的reg属性地址,必须与Vivado Address Editor中分配的完全一致,包括地址长度。一个字节都不能错。

5.2 问题:能打开设备,但收发数据全为乱码或根本无数据

  • 可能原因1:波特率不匹配
    • 排查:这是头号嫌疑犯。请进行三重核对:
      1. Vivado中UARTLite IP配置的CLOCK频率(如50MHz)。
      2. 设备树节点中clock-frequency属性值(必须与第1点相同)。
      3. 用户态程序(如stty,picocom)设置的波特率(如115200)与设备树中current-speed及IP配置的BAUD一致。
    • 计算验证:UARTLite波特率计算公式为:BAUD = CLOCK / (16 * (Divisor + 1))。你可以用这个公式反推实际波特率。例如,CLOCK=50MHz,驱动设置的Divisor为27,则实际波特率=50,000,000 / (16*28) ≈ 111607,与115200有偏差。确保CLOCK频率能被目标波特率整除,以获得精确的波特率
  • 可能原因2:硬件引脚约束错误或电平不匹配
    • 排查:用示波器或逻辑分析仪测量TX引脚。当发送数据时,应该有明显的波特率方波。如果没有,检查XDC约束文件中的引脚号是否写错,I/O标准(如LVCMOS33)是否与对接设备匹配。确保RX引脚在空闲时有稳定的高电平(可通过外部上拉电阻实现)。
  • 可能原因3:中断问题
    • 排查:如果发送正常但接收不到,可能是中断未正确触发。检查设备树中的interrupts属性,特别是中断号是否正确。在/proc/interrupts文件中查看对应中断号(如29)是否有计数增加。如果没有,说明中断未成功注册或触发。

5.3 问题:多路串口同时工作时,系统响应变慢或丢失数据

  • 可能原因:中断风暴
    • 分析与解决:如前所述,多路高速UARTLite会产生大量中断。可以通过vmstatmpstat命令观察系统中断数(in列)和CPU使用率。如果中断数极高且si(软中断)CPU使用率高,基本可以确定。
    • 缓解方案
      1. 降低波特率:如果应用允许,这是最直接的方法。
      2. 优化软件:使用非阻塞I/O和多路复用,减少不必要的读操作和上下文切换。
      3. 硬件升级:考虑使用带FIFO或DMA功能的UART IP核(如AXI UART16550),可以大幅减少中断次数。

5.4 实战心得:版本管理与自动化

  1. 保存好Vivado的xdctcl脚本:引脚约束(.xdc)和创建Block Design的Tcl脚本非常重要。它们能确保你下次重建工程或移植到其他板卡时,硬件配置的一致性。
  2. 设备树片段化:不要把所有9个串口节点都堆在主.dts文件里。可以为这些PL外设创建一个单独的.dtsi片段文件(如pl-uartlites.dtsi),然后在主文件中用#include引入。这样模块化管理更清晰。
  3. 利用Vitis自动生成设备树节点:在Vivado导出硬件后,使用Vitis的“Create Device Tree”功能,可以基于.hdf文件自动生成包含地址和中断信息的设备树节点框架,大大减少手动编写出错的可能。但生成后,仍需仔细核对时钟、波特率等参数。
  4. 先单路,再多路:调试时,强烈建议先只使能一路UARTLite,从硬件约束到设备树到驱动测试,确保这一路完全调通。然后再逐步添加其他路,并注意修改地址和中断号。这种“增量式”调试能快速定位问题是共性还是个性。

通过以上从Vivado硬件设计、设备树配置到Linux驱动测试的完整流程,你应该能够独立完成Zynq PL侧多路UARTLite串口的扩展。这个方案成本低、灵活性高,非常适合需要大量低速串行通信的嵌入式应用。记住,耐心和细致的核对是成功的关键,尤其是在地址、时钟、中断这三个核心配置上,多花一分钟检查,可能就能省下几小时的调试时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询