简介:本资源面向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处理器及一系列外设。
- 在Block Design中,点击“+”号添加IP,搜索并添加“ZYNQ7 Processing System”。
- 双击添加进来的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核。
- 再次点击“+”号添加IP,搜索“AXI Uartlite”。将其拖入Diagram中。你会看到一个名为
axi_uartlite_0的IP。 - 关键的一步:配置第一个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频率,内部计算分频系数来产生目标波特率。如果这里填错,实际波特率就会偏差,导致通信失败。
- 重复以上步骤,再添加8个AXI Uartlite IP核,分别命名为
axi_uartlite_1到axi_uartlite_8。每个的配置(波特率、数据格式)可以相同也可以不同,根据实际需求来。但请务必为每一个IP核单独配置,并确认其CLOCK频率设置正确。
2.3 连接时钟、复位与AXI总线
添加完IP后,我们需要用“连线”把它们组织成一个能工作的系统。
时钟连接:
- 将ZYNQ7 IP的
FCLK_CLK0输出端口,连接到每一个UARTLite IP的s_axi_aclk(AXI总线时钟)和uart端口下的clk(UART模块时钟)。这确保了整个AXI通信和UART核心逻辑同步在同一个时钟下工作。你可以使用Vivado的“自动连接”功能,但连接后务必检查是否正确。
- 将ZYNQ7 IP的
复位连接:
- 将ZYNQ7 IP的
FCLK_RESET0_N输出端口,连接到每一个UARTLite IP的s_axi_aresetn(AXI总线复位,低电平有效)。这确保了系统上电或复位时,所有IP能同步复位。
- 将ZYNQ7 IP的
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_AXI到M08_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,具体取决于配置)。记下这些地址,后续写设备树要用。
- 这是连接PS和PL逻辑的数据通道。ZYNQ7 IP默认有一个
外部端口连接:
- 每个UARTLite IP都有
RX和TX信号。在Block Diagram中,分别右键点击每个UARTLite的RX和TX,选择“Make External”。这会在设计顶层创建对应的引脚端口,例如uart_0_rxd,uart_0_txd一直到uart_8_rxd,uart_8_txd。
- 每个UARTLite IP都有
2.4 生成顶层HDL与引脚约束
在Diagram空白处右键,选择“Create HDL Wrapper”,让Vivado根据Block Design自动生成顶层的Verilog或VHDL文件。
接下来是硬件设计的关键一步:编写XDC约束文件。你需要根据你的FPGA板卡原理图,将刚才创建的
uart_x_rxd和uart_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引脚配置外部上拉电阻,避免因浮空产生误触发。
- 例如:
完成约束后,进行“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通常使用电平触发。
clocks和clock-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.tcl或xparameters.h文件,搜索XPAR_INTC_0_UARTLITE_0_VEC_ID这样的宏定义,其值就是中断ID。
3.3 编译与更新设备树
- 修改好设备树源文件(.dts)后,使用设备树编译器(DTC)将其编译成二进制设备树 blob(.dtb)文件。
dtc -I dts -O dtb -o devicetree.dtb system.dts - 将生成的
devicetree.dtb文件,连同Linux内核镜像(uImage)和FPGA比特流文件(system.bit或加载后的.bin文件),一起放到启动介质(如SD卡的FAT分区)中。 - 配置你的Bootloader(如U-Boot)在启动内核时加载这个新的dtb文件。
4. Linux系统下的驱动加载与测试
当系统启动后,如果一切配置正确,内核会自动识别并加载xilinx_uartlite驱动。
4.1 检查驱动加载情况
在Linux终端中,执行以下命令进行诊断:
查看内核启动日志:
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... 以此类推。查看设备树节点状态:
ls /sys/bus/platform/devices/ | grep serial这会列出所有平台设备中名称包含“serial”的设备,应该能看到
43c00000.serial等9个设备。查看生成的设备文件:
ls -l /dev/ttyUL*确认9个设备文件(
ttyUL0到ttyUL8)都已创建。
4.2 串口通信测试
你可以使用任何串口调试工具进行测试,例如minicom,picocom,或者简单的echo和cat命令。
配置并打开一个串口(例如
ttyUL0):stty -F /dev/ttyUL0 115200 cs8 -parenb -cstopb # 配置波特率等参数 picocom -b 115200 /dev/ttyUL0 # 使用picocom打开串口在另一个终端,向该串口发送数据:
echo "Hello UARTLite 0" > /dev/ttyUL0在
picocom终端应该能看到接收到的字符串。回环测试(Loopback Test): 这是验证硬件连接和驱动是否正常的最直接方法。将FPGA板上对应
uart_0_txd和uart_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,poll或epoll等多路复用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:波特率不匹配。
- 排查:这是头号嫌疑犯。请进行三重核对:
- Vivado中UARTLite IP配置的
CLOCK频率(如50MHz)。 - 设备树节点中
clock-frequency属性值(必须与第1点相同)。 - 用户态程序(如
stty,picocom)设置的波特率(如115200)与设备树中current-speed及IP配置的BAUD一致。
- Vivado中UARTLite IP配置的
- 计算验证: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会产生大量中断。可以通过
vmstat或mpstat命令观察系统中断数(in列)和CPU使用率。如果中断数极高且si(软中断)CPU使用率高,基本可以确定。 - 缓解方案:
- 降低波特率:如果应用允许,这是最直接的方法。
- 优化软件:使用非阻塞I/O和多路复用,减少不必要的读操作和上下文切换。
- 硬件升级:考虑使用带FIFO或DMA功能的UART IP核(如AXI UART16550),可以大幅减少中断次数。
- 分析与解决:如前所述,多路高速UARTLite会产生大量中断。可以通过
5.4 实战心得:版本管理与自动化
- 保存好Vivado的
xdc和tcl脚本:引脚约束(.xdc)和创建Block Design的Tcl脚本非常重要。它们能确保你下次重建工程或移植到其他板卡时,硬件配置的一致性。 - 设备树片段化:不要把所有9个串口节点都堆在主
.dts文件里。可以为这些PL外设创建一个单独的.dtsi片段文件(如pl-uartlites.dtsi),然后在主文件中用#include引入。这样模块化管理更清晰。 - 利用Vitis自动生成设备树节点:在Vivado导出硬件后,使用Vitis的“Create Device Tree”功能,可以基于
.hdf文件自动生成包含地址和中断信息的设备树节点框架,大大减少手动编写出错的可能。但生成后,仍需仔细核对时钟、波特率等参数。 - 先单路,再多路:调试时,强烈建议先只使能一路UARTLite,从硬件约束到设备树到驱动测试,确保这一路完全调通。然后再逐步添加其他路,并注意修改地址和中断号。这种“增量式”调试能快速定位问题是共性还是个性。
通过以上从Vivado硬件设计、设备树配置到Linux驱动测试的完整流程,你应该能够独立完成Zynq PL侧多路UARTLite串口的扩展。这个方案成本低、灵活性高,非常适合需要大量低速串行通信的嵌入式应用。记住,耐心和细致的核对是成功的关键,尤其是在地址、时钟、中断这三个核心配置上,多花一分钟检查,可能就能省下几小时的调试时间。
本文还有配套的精品资源,点击获取