1. 为什么需要PL端串口扩展
很多工程师第一次接触ZYNQ时都会遇到一个尴尬的问题:PS端自带的两个UART接口根本不够用。在实际项目中,我们经常需要连接多个传感器、显示屏或调试终端,这时候就需要在PL端扩展串口。
我去年做过一个工业控制项目,需要同时连接4个Modbus RTU设备,PS端的两个串口完全不够分配。这时候在PL端用AXI UART16550 IP核扩展串口就成了最佳选择。这个IP核不仅兼容标准16550串口控制器,还支持中断驱动,实测波特率可以达到3Mbps,完全满足工业场景需求。
2. Vivado硬件工程搭建
2.1 创建基础工程
首先新建一个ZYNQ7020工程(其他ZYNQ型号也适用),在Block Design中添加ZYNQ7 Processing System核。这里有个小技巧:建议先双击ZYNQ核进入配置界面,在PS-PL Configuration中勾选"Fabric Resets"和"Interrupts"下的PL-PS中断选项,这样后续添加中断时会更方便。
接着添加AXI UART16550 IP核,这个核在Xilinx官方IP库里就能找到。我在多个项目中发现,这个IP核的FIFO深度设置很有讲究:
- 对于低速串口(115200bps以下),16字节FIFO足够
- 对于高速通信(1Mbps以上),建议设置为64或128字节
2.2 关键连接配置
完成IP核添加后,需要特别注意几个关键连接:
- 将UART核的AXI接口连接到ZYNQ的GP接口(通常是M_AXI_GP0)
- 将UART核的中断输出连接到ZYNQ的IRQ_F2P[0:0]
- 将UART核的串口信号引出到顶层(注意TX/RX交叉连接)
这里有个我踩过的坑:如果发现中断无法触发,请检查Vivado是否自动生成了concat IP。有时候需要手动添加一个Concat核,将多个中断信号合并后再接入ZYNQ。
2.3 引脚约束与生成
完成连接后,建议先运行Validate Design检查连接关系。确认无误后,在约束文件中添加如下典型配置:
set_property PACKAGE_PIN Y18 [get_ports uart_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart_tx] set_property PACKAGE_PIN AA18 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart_rx]最后生成Bitstream时,建议勾选"-bin_file"选项,这样会同时生成.bin文件,方便后续通过SD卡更新固件。
3. SDK软件工程开发
3.1 创建基础工程
导出硬件到SDK后,新建一个空白应用工程。这里推荐使用BSP模板,它会自动包含UART驱动库。我习惯先测试基础收发功能,再添加中断处理,这样排查问题更有条理。
在BSP设置中,需要确认:
- 已启用xuartns550驱动
- 中断控制器设置为ScuGic(ZYNQ的标准配置)
- 编译器优化等级建议先设为-O0方便调试
3.2 中断服务程序实现
Xilinx提供了标准的中断示例,但实际使用时需要做以下关键修改:
- 在xparameters.h中确认中断ID定义正确:
#define UART_IRPT_INTR XPAR_FABRIC_AXI_UART16550_0_IP2INTC_IRPT_INTR- 中断初始化代码需要包含以下关键步骤:
// 初始化中断控制器 XScuGic_Config *IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID); XScuGic_CfgInitialize(&IntcInstance, IntcConfig, IntcConfig->CpuBaseAddress); // 设置中断优先级和触发类型 XScuGic_SetPriorityTriggerType(&IntcInstance, UART_IRPT_INTR, 0xA0, 0x3); // 连接中断处理函数 XScuGic_Connect(&IntcInstance, UART_IRPT_INTR, (Xil_ExceptionHandler)XUartNs550_InterruptHandler, &UartInstance); // 启用中断 XScuGic_Enable(&IntcInstance, UART_IRPT_INTR);- 中断处理函数建议按这个模板编写:
void UartIntrHandler(void *CallBackRef, u32 Event, unsigned int EventData) { static u8 buffer[64]; if (Event == XUN_EVENT_RECV_DATA) { int count = XUartNs550_Recv(&UartInstance, buffer, sizeof(buffer)); // 处理接收到的数据 for(int i=0; i<count; i++){ xil_printf("Recv: 0x%02X\r\n", buffer[i]); } } }3.3 调试技巧
调试中断驱动时,我总结了几条实用经验:
- 先用示波器确认硬件线路正常
- 在中断入口处添加调试打印,确认中断是否触发
- 检查中断屏蔽寄存器(IER)设置是否正确
- 使用XUartNs550_GetInterruptMask()读取当前中断状态
如果遇到数据丢失问题,可以尝试:
- 增大FIFO深度
- 提高中断优先级
- 在中断服务程序中尽快读取数据
4. 性能优化实战
4.1 中断响应时间测试
通过GPIO翻转+逻辑分析仪测量,在150MHz CPU频率下,从中断触发到进入ISR的平均延迟约为1.2us。这个时间主要消耗在:
- 中断控制器响应时间:约0.4us
- 上下文保存:约0.3us
- 跳转到ISR:约0.5us
对于波特率115200的串口(每个字节间隔86us),这个延迟完全可接受。但在3Mbps时(每个字节间隔3.3us),就需要考虑DMA方案了。
4.2 缓冲区设计
为了避免数据丢失,我设计了一个双缓冲机制:
#define BUF_SIZE 256 typedef struct { u8 buffer1[BUF_SIZE]; u8 buffer2[BUF_SIZE]; u8 *active_buf; volatile int write_idx; } DoubleBuffer; void IntrHandler(void *CallBackRef, u32 Event, unsigned int EventData) { DoubleBuffer *dbuf = (DoubleBuffer *)CallBackRef; if(Event == XUN_EVENT_RECV_DATA) { int avail = XUartNs550_Recv(&UartInstance, dbuf->active_buf + dbuf->write_idx, BUF_SIZE - dbuf->write_idx); dbuf->write_idx += avail; if(dbuf->write_idx >= BUF_SIZE/2) { // 切换缓冲区 ProcessBuffer(dbuf->active_buf, dbuf->write_idx); dbuf->active_buf = (dbuf->active_buf == dbuf->buffer1) ? dbuf->buffer2 : dbuf->buffer1; dbuf->write_idx = 0; } } }4.3 实际项目经验
在一个智能电表项目中,我们需要同时处理4个串口数据。通过以下优化实现了稳定运行:
- 为每个串口分配独立的中断优先级
- 使用RTOS的任务通知机制唤醒处理线程
- 在Vivado中为每个UART核分配独立的中断线(避免共享中断线)
关键配置参数如下表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 中断优先级 | 0xA0-0xC0 | 高于系统任务 |
| FIFO深度 | 64字节 | 平衡延迟和资源 |
| 接收超时 | 4字符时间 | 避免数据滞留 |
| 波特率容差 | <3% | 保证稳定性 |
5. 常见问题解决
5.1 中断不触发
这是新手最常见的问题,可按以下步骤排查:
- 确认Vivado中中断线已正确连接
- 检查SDK中的中断ID是否与硬件设计一致
- 验证中断控制器是否已正确初始化
- 用XUartNs550_GetInterruptStatus()读取中断状态寄存器
5.2 数据丢失
遇到数据丢失时,建议:
- 降低波特率测试
- 增大FIFO深度
- 检查中断服务程序是否耗时过长
- 使用逻辑分析仪捕获实际波形
5.3 性能瓶颈
当需要更高性能时,可以考虑:
- 使用AXI DMA+串口方案
- 在PL端实现数据预处理
- 改用更高端的ZYNQ UltraScale+系列
我在一个高速数据采集项目中,通过将UART IP核时钟提升到100MHz,配合DMA传输,实现了5Mbps的稳定传输。关键是在Vivado中正确设置时钟域交叉(Clock Domain Crossing)参数。
6. 进阶应用
6.1 与Linux驱动集成
如果需要移植到Linux系统,需要注意:
- 在设备树中添加UART节点
- 配置正确的时钟频率
- 实现中断资源映射
典型设备树配置示例:
axi_uart16550_0: serial@43c00000 { compatible = "xlnx,xps-uart16550-2.00.a"; reg = <0x43c00000 0x10000>; interrupts = <0 29 4>; clock-frequency = <100000000>; current-speed = <115200>; };6.2 多串口管理
管理多个PL串口时,我推荐采用面向对象的设计模式:
typedef struct { XUartNs550 instance; DoubleBuffer buffer; u32 baudrate; } UartDevice; void Uart_Init(UartDevice *dev, u16 dev_id, u16 intr_id) { XUartNs550_Initialize(&dev->instance, dev_id); XUartNs550_SetHandler(&dev->instance, IntrHandler, dev); // ...其他初始化 } void Uart_Send(UartDevice *dev, const u8 *data, size_t len) { XUartNs550_Send(&dev->instance, data, len); }这种设计在需要管理4个以上串口时特别有用,代码可维护性大大提高。