在嵌入式系统开发中,UART串口通信是最基础也最常用的外设接口之一。基于Xilinx ZYNQ系列FPGA的UART模块设计,既要考虑PL(可编程逻辑)侧的高效数据处理,又要实现与PS(处理系统)侧的协同工作。这个项目正是为了解决ZYNQ芯片在复杂系统中多串口通信的需求而设计的。
我最近在一个工业控制项目中遇到了这样的场景:需要同时连接6个不同波特率的传感器设备,而ZYNQ芯片自带的UART接口数量有限。通过设计这个6通道FPGA UART发送模块,不仅解决了接口资源不足的问题,还实现了纳秒级精度的多路串口时序控制。下面就把这个实战经验完整分享给大家。
我们选用的是Xilinx ZYNQ-7000系列XC7Z020芯片,主要基于以下几点考虑:
系统采用典型的PS+PL协同架构:
code复制[PS侧]
├── ARM双核运行Linux系统
├── 通过AXI-GP总线与PL交互
└── 管理2个硬核UART接口
[PL侧]
├── AXI接口模块
├── 6路UART发送引擎
├── 波特率时钟生成器
└── 共享FIFO缓冲区
时钟域划分:
资源预估:
每路UART发送器采用经典的三段式状态机:
verilog复制localparam [2:0]
IDLE = 3'b000,
START = 3'b001,
DATA = 3'b010,
PARITY = 3'b011,
STOP = 3'b100;
always @(posedge clk_baud) begin
case(state)
IDLE:
if(tx_en) begin
state <= START;
tx_out <= 1'b0; // 起始位
end
START:
begin
state <= DATA;
bit_cnt <= 3'd0;
end
DATA:
if(bit_cnt == DATA_BITS-1) begin
state <= PARITY;
tx_out <= ^tx_data; // 奇偶校验
end else begin
tx_out <= tx_data[bit_cnt];
bit_cnt <= bit_cnt + 1;
end
// ...其他状态转移
endcase
end
采用DDS(直接数字频率合成)技术生成可编程波特率:
code复制波特率时钟 = 系统时钟 × (分频系数/2^32)
例如生成115200bps:
verilog复制// 100MHz系统时钟,32位相位累加器
parameter CLK_FREQ = 100_000_000;
reg [31:0] phase_accum;
always @(posedge clk) begin
phase_accum <= phase_accum + (115200 * (2**32) / CLK_FREQ);
end
assign baud_clk = phase_accum[31]; // 取最高位作为波特率时钟
自定义AXI-Lite从接口关键信号:
verilog复制// 写通道
input wire [31:0] S_AXI_AWADDR,
input wire S_AXI_AWVALID,
output reg S_AXI_AWREADY,
// 数据通道
input wire [31:0] S_AXI_WDATA,
input wire [3:0] S_AXI_WSTRB,
input wire S_AXI_WVALID,
output reg S_AXI_WREADY,
// 响应通道
output reg [1:0] S_AXI_BRESP,
output reg S_AXI_BVALID,
input wire S_AXI_BREADY
地址映射方案:
采用如下测试方案:
硬件平台:
测试软件:
单路极限速率测试:
多路交叉测试:
python复制# Python测试脚本示例
ports = [serial.Serial(f'/dev/ttyUSB{i}', baudrate=rates[i])
for i in range(6)]
for i in range(1000):
for port in ports:
port.write(f'Test {i}\n'.encode())
长时间稳定性测试:
逻辑分析仪捕获的典型波形:
code复制起始位 | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | 校验位 | 停止位
____|‾‾‾‾|____|‾‾‾‾|____|‾‾‾‾|____|‾‾‾‾|____|‾‾‾‾|____|‾‾‾‾‾‾‾
关键参数测量:
初期遇到的关键问题:
解决方案:
tcl复制create_clock -name clk_baud0 -period 8680 [get_pins uart0/baud_gen/clk_out]
set_clock_groups -asynchronous -group [get_clocks clk_baud*]
通过以下方法节省了23%的LUT资源:
优化前后的资源对比:
| 资源类型 | 优化前 | 优化后 |
|---|---|---|
| LUTs | 2142 | 1648 |
| FFs | 1872 | 1536 |
| BRAMs | 2 | 2 |
字符设备驱动关键实现:
c复制static int uart_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct uart_dev *dev = file->private_data;
uint32_t reg_val;
// 检查FIFO状态
reg_val = ioread32(dev->base + FIFO_STATUS_OFFSET);
if (reg_val & FIFO_FULL)
return -EAGAIN;
// 写入数据
iowrite32(*buf, dev->base + TX_DATA_OFFSET);
return 1;
}
用户空间测试命令:
bash复制echo "test" > /dev/uart0
cat /proc/uart_stats
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 无数据输出 | 波特率配置错误 | 1. 检查时钟树 2. 测量实际波特率 3. 验证寄存器配置 |
| 数据错位 | 时序约束不完善 | 1. 分析setup/hold时间 2. 添加跨时钟域约束 3. 检查亚稳态防护 |
| 系统卡死 | AXI死锁 | 1. 检查AW/W/B通道握手 2. 添加AXI协议检查器 3. 验证突发传输长度 |
ILA在线调试:
tcl复制create_debug_core uart_ila ila
set_property C_DATA_DEPTH 1024 [get_debug_cores uart_ila]
connect_debug_port uart_ila/clk [get_nets clk_100m]
connect_debug_port uart_ila/probe0 [get_nets uart0/tx_out]
Linux调试命令:
bash复制# 查看中断统计
cat /proc/interrupts | grep uart
# 调试消息打印
echo 8 > /proc/sys/kernel/printk
信号完整性检查:
这个6路UART发送模块已经在多个工业现场稳定运行超过2000小时,期间处理了超过50GB的串口数据。对于需要扩展ZYNQ串口能力的开发者,建议重点关注波特率精度和多路隔离这两个核心问题。实际部署时,根据具体应用场景,可能还需要增加硬件流控或协议过滤功能。