简介:基于Xilinx Zynq7020 FPGA的串口通信实验资源,围绕RS232、RS485、RS422三种常用工业串口协议展开,面向正在学习FPGA UART通信、需要工程化验证的开发者,旨在解决多协议串口调试与硬件验证落地问题。压缩包内含341个文件,大小约18.87MB,覆盖Vivado完整工程(xpr/xci/bit)、Verilog与VHDL源码、XDC约束、Tcl/Bat/Sh脚本、综合实现报告及开发板硬件原理图,文件类型齐全,便于从代码阅读到上板调试的完整链路学习。资源中RS422顶层模块清晰展示了例化两个uart_test子模块实现双通道收发的方法,对理解多路串口扩展与模块化设计很有帮助;同时附带的bit文件、时序约束和原理图PDF,可直接配合Zynq7020开发板进行实验验证。整体目录结构清晰,便于快速定位工程源码、脚本与硬件文档。目前已有129人学习下载,内容偏工程实战,适合希望快速上手Vivado串口通信项目并对照硬件原理图做二次开发的读者。
1. 一套 Verilog 收发器覆盖三种串口总线:Zynq7020 串口实验的正确打开方式
串口是 FPGA 开发里最容易上手、也最容易被低估的外设。你翻开任意一块 Zynq7020 开发板的原理图,几乎都能找到 1 到 2 个 RS232 电平的 DB9 接口,旁边的排针或跳线帽则同时引出了 RS485 和 RS422 的收发引脚。很多初学者在第一个 LED 闪烁实验之后,第二个目标就是打通 UART 回环——但真正面对“RS232、RS485、RS422 同时出现”的工程时,往往会困惑:这三种接口到底需要在 FPGA 里写三套逻辑,还是同一套 UART 核就能全部搞定?答案是后者。从 FPGA 侧看,RS232、RS485、RS422 共享完全相同的 UART 帧格式和波特率机制,区别只在于物理层电平标准和收发方向控制。本文要做的,就是拿到标题里那套“Vivado 工程 + Verilog 源码 + 开发板原理图”之后,顺着工程把三者的关系、UART 收发状态机、方向控制时序、上板调试方法一次讲透。你不需要额外买协议分析仪,一块 Zynq7020 开发板加一个 USB 转串口模块就能把三种总线全部验证完。
2. 三种串口协议的电气边界:RS232、RS422、RS485 在 FPGA 侧的真正区别
很多人把 RS232、RS422、RS485 当成三种不同的“协议”,其实它们是三种不同的电气标准。帧格式都是起始位 + 8 位数据 + 停止位(通常无校验),波特率也都是 9600、115200 这类常见值。FPGA 里跑的那套 UART 逻辑,对三种总线完全通用。
2.1 从原理图看懂电平转换芯片的角色
打开标题压缩包里的硬件原理图,你会看到 Zynq7020 的 PL 端引脚(比如 BANK500 或 BANK501 上的引脚)并没有直接连接到 DB9 或接线端子。中间一定隔着一颗电平转换芯片,常见型号有:
| 总线类型 | 常见收发器芯片 | 电平标准 | 备注 |
|---|---|---|---|
| RS232 | MAX3232 / SP3232 | 单端,±5V ~ ±15V | 必须用电荷泵产生负压 |
| RS485 | MAX3485 / SP3485 | 差分,A/B 两线 | 半双工,需要方向控制 |
| RS422 | MAX3490 / SP3490 | 差分,A/B/Y/Z 四线 | 全双工,收发独立 |
原理图里 RS232 部分的电容(通常是 0.1uF 或 1uF)是电荷泵的储能元件,没有这些电容,MAX3232 无法产生负压,RS232 电平根本出不来。这是一条很实用的排查经验:RS232 完全不工作、量不到负压时,先检查电荷泵电容是否焊反或虚焊。
RS485 和 RS422 的收发器则是把 FPGA 侧的 3.3V TTL 单端信号转换成差分信号。注意 RS485 收发器(如 MAX3485)的 DE 和 RE 引脚经常在原理图上并在一起,由 FPGA 的一个 GPIO 控制——这就是半双工方向控制的硬件基础。RS422 的收发器则没有 DE/RE 概念,因为它的发送和接收是独立的两对差分线,不需要切换方向。
2.2 FPGA 引脚约束上的几个关键点
拿到原理图后,先别急着写代码,把引脚约束梳理清楚。常见做法是按照信号流向分组:
# RS232 部分 set_property PACKAGE_PIN E6 [get_ports uart232_tx] set_property PACKAGE_PIN E7 [get_ports uart232_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart232_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart232_rx] # RS485 部分(包含方向控制) set_property PACKAGE_PIN F6 [get_ports uart485_tx] set_property PACKAGE_PIN F7 [get_ports uart485_rx] set_property PACKAGE_PIN G6 [get_ports uart485_dir] set_property IOSTANDARD LVCMOS33 [get_ports uart485_*] # RS422 部分 set_property PACKAGE_PIN H6 [get_ports uart422_tx] set_property PACKAGE_PIN H7 [get_ports uart422_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart422_*]这段约束里,RS485 比 RS232 多了一个uart485_dir信号,它控制的是收发器的 DE/RE 引脚。实际工程里引脚号以你自己的原理图为准,但结构一定是这个结构:每一个与收发器相连的 FPGA 引脚都要写PACKAGE_PIN和IOSTANDARD,缺一个 Vivado 综合后就会报 IO 约束不完整。
有一个细节值得注意:RS485 的rx引脚在 FPGA 侧经常标记为DI(Receiver Data In)而不是rx,在原理图上是收发器的 RO 引脚。看到RO、DI、DE、RE这几个引脚名时要能对应上。
2.3 逻辑层视角:FPGA 只认识 TTL 电平
把三种收发器的数据手册摊开对比,会发现 FPGA 侧的接口都是一个发送引脚、一个接收引脚、加上可能的方向控制引脚。MAX3232 和 MAX3485、MAX3490 在 FPGA 侧的接口差异,只有 RS485 多了一个dir控制。这意味着你写的 Verilog UART 收发模块,完全可以被三个顶层实例各调用一次,不需要为每种总线写不同的逻辑。
这也是为什么标题里的工程把“RS232、RS485、RS422”放在同一个实验里——工作量就是一份 UART 收发器,加上 RS485 的少量方向控制逻辑。搞清楚这个边界,后面看源码就不会被三种总线绕晕。
3. Vivado 工程里的 UART 最小可运行代码:波特率生成与收发状态机
这一章给出一个可以在 Zynq7020 上直接落地的 UART 模块。以 115200 波特率、8 位数据位、1 位停止位为例,完整的工程包含顶层例化、波特率生成、发送状态机、接收状态机四个部分。
3.1 波特率计数的计算方式
Zynq7020 的 PL 端时钟通常来自 PS 端的FCLK_CLK0,默认 100MHz。波特率计数器的核心公式是:
计数上限 = 时钟频率 / 波特率 / 分频系数如果采样方式是每个位周期计数一次,那么 100MHz / 115200 ≈ 868。这 868 个时钟周期就是一位的时长。但接收方向通常会做 16 倍过采样,原因是需要在位的中间位置采样以避开边沿抖动。16 倍过采样时,计数上限就是 54(868 / 16 ≈ 54.25),取整后实际波特率为 100MHz / 54 / 16 ≈ 115741,误差约 0.41%,在 UART 的 ±2% 容限内完全可用。
3.2 发送状态机的 Verilog 实现
module uart_tx #( parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire [7:0] tx_data, input wire tx_start, output reg tx_line, output reg tx_busy ); localparam BAUD_CNT = CLK_FREQ / BAUD_RATE; localparam IDLE = 3'd0; localparam START = 3'd1; localparam DATA = 3'd2; localparam STOP = 3'd3; reg [2:0] state; reg [8:0] baud_cnt; reg [2:0] bit_cnt; reg [7:0] data_buf; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; tx_line <= 1'b1; tx_busy <= 1'b0; baud_cnt <= 0; bit_cnt <= 0; data_buf <= 0; end else begin case (state) IDLE: begin tx_line <= 1'b1; if (tx_start && !tx_busy) begin data_buf <= tx_data; tx_busy <= 1'b1; state <= START; end end START: begin tx_line <= 1'b0; if (baud_cnt == BAUD_CNT - 1) begin baud_cnt <= 0; state <= DATA; bit_cnt <= 0; end else begin baud_cnt <= baud_cnt + 1; end end DATA: begin tx_line <= data_buf[bit_cnt]; if (baud_cnt == BAUD_CNT - 1) begin baud_cnt <= 0; if (bit_cnt == 3'd7) begin state <= STOP; end else begin bit_cnt <= bit_cnt + 1; end end else begin baud_cnt <= baud_cnt + 1; end end STOP: begin tx_line <= 1'b1; if (baud_cnt == BAUD_CNT - 1) begin baud_cnt <= 0; state <= IDLE; tx_busy <= 1'b0; end else begin baud_cnt <= baud_cnt + 1; end end endcase end end endmodule这段代码的状态划分是标准做法:IDLE 时发送线保持高电平,收到tx_start脉冲后进入 START 位输出低电平,延时一个位周期后开始输出 8 个数据位,最后输出停止位并回到 IDLE。tx_busy信号用于外部逻辑判断“上一帧是否发完”,避免在发送过程中写入新数据导致帧错乱。
参数说明:CLK_FREQ必须与实际时钟匹配,如果 Zynq7020 的 FCLK_CLK0 改成了 50MHz 而这里没改,波特率会直接翻倍错乱。tx_data在tx_start拉高的同拍被锁存,所以外部逻辑只需要保证tx_start是单周期脉冲即可,不需要一直维持tx_data不变。
3.3 接收状态机的边沿采样策略
接收方向的难点在于异步采样——你不知道数据什么时候到达,必须在rx_line上检测到下降沿时启动接收。以下给出接收模块的关键部分:
module uart_rx #( parameter CLK_FREQ = 100_000_000, parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire rx_line, output reg [7:0] rx_data, output reg rx_done ); localparam BAUD_CNT = CLK_FREQ / BAUD_RATE; localparam SAMPLE_CNT = CLK_FREQ / BAUD_RATE / 16; reg [3:0] rx_shift; reg rx_meta, rx_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_meta <= 1'b1; rx_sync <= 1'b1; end else begin rx_meta <= rx_line; rx_sync <= rx_meta; end end wire rx_negedge = rx_sync && !rx_meta;这里用到两级同步器消除亚稳态。Zynq7020 的引脚电平是异步时钟域信号,如果直接拿原始rx_line做下降沿检测,在跨时钟域采样时可能采到中间电平导致误判。两级触发器把信号打拍到本地时钟域后,下降沿检出的结果是稳定的。
接收数据的具体采样点是每个位的 1/2 处。启动后跳过 8 个采样时钟再开始采样数据位,这样采到的是位中心值,抗干扰能力最强。注意这里不能用divide by 16再采,因为起始位下降沿本身有抖动,正确的做法是用一个采样计数器在每个位周期的中央触发一次取值。
3.4 顶层例化与 Testbench 验证
顶层模块只需要三个实例:一个uart_tx、一个uart_rx、再加上一个简单的回环逻辑(接收到的数据直接送发送端)。回环是验证 UART 工程最有效的手段——电脑串口发一个字节,开发板返回同一个字节。如果回环数据一致,说明收、发两条链路都通了。
// 顶层例化片段 wire [7:0] loop_data; wire loop_done; uart_rx u_rx ( .clk (clk_100m), .rst_n (rst_n), .rx_line (uart232_rx), .rx_data (loop_data), .rx_done (loop_done) ); uart_tx u_tx ( .clk (clk_100m), .rst_n (rst_n), .tx_data (loop_data), .tx_start (loop_done), .tx_line (uart232_tx), .tx_busy () );这段例化中tx_start直接接rx_done,意味着每收到一个字节就立刻回发一个字节。这是最粗暴但最有效的串口调试方式。Vivado 自带的仿真工具里,Testbench 需要造一个模拟的串口发送端,用#8680000这样的延时模拟位周期,从 0 开始逐位把tx_line拉低再拉高,驱动 DUT 的接收端就可以观察波形。
4. RS485 方向控制与 RS422 全双工:同一收发模块的三种接线拓扑
三种总线的 UART 逻辑既然一样,差别就全部集中在物理层的接法和控制逻辑上。这一章给出实际工程里 RS485 方向切换的标准做法和 RS422 的接线注意点。
4.1 RS485 半双工的方向切换时序
RS485 总线上所有节点共享一对差分线,同一时刻只能有一个节点发送。FPGA 侧通过dir信号控制 MAX3485 的 DE/RE 引脚:
// RS485 方向控制 reg dir_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) dir_reg <= 1'b0; // 默认接收状态 else if (tx_start) dir_reg <= 1'b1; // 开始发送,占用总线 else if (tx_busy_done) dir_reg <= 1'b0; // 发送结束,释放总线 endtx_start是发送启动脉冲,tx_busy_done是发送状态机结束脉冲。方向切换的关键是 DE 拉高后必须等发送状态机的第一个位真正输出到总线,才能开始计数,否则起始位会被吞掉。MAX3485 的 DE 到输出有效延时约 50ns,对于 115200 波特率来说一个位周期约 8.68us,这个延时可以忽略,但方向切换的完成必须晚于发送状态机的最后一个停止位结束。
一个非常容易踩的坑是方向控制的释放时机。如果dir_reg在发送状态机进入 IDLE 的同一拍拉低,那么最后一个停止位可能还没完整地送出电平。常见做法是在tx_busy下降沿后再延时一到两个时钟周期释放dir_reg。
4.2 RS422 全双工:不需要方向控制的简化
RS422 收发器的发送输出(Y/Z)和接收输入(A/B)是两对独立的差分线,所以uart422_tx和uart422_rx各连各的线,不存在方向切换问题。
在顶层例化时,RS422 部分直接把 uart_tx 的输出接到 MAX3490 的 DI 引脚,MAX3490 的 RO 引脚接 uart_rx 的输入,代码和 RS232 几乎一样。RS422 的唯一注意点是终端电阻:长距离传输时,接收端需要在 A-B 之间跨接 120 欧姆终端电阻,开发板上一般通过跳线帽启用。如果距离短、线缆在 1 米以内,不接终端电阻也能正常通信。
4.3 三总线复用的共享思路
如果你的开发板引脚紧张,可以只写一套 UART 收发器,通过一个 2 位选择信号在三个物理引脚组之间切换:
wire uart_tx_out, uart_rx_in; // 三个总线的回环共享同一套收发器 assign uart232_tx_out = uart_tx_out; assign uart485_tx_out = uart_tx_out; assign uart422_tx_out = uart_tx_out; // 接收选择 always @(*) begin case (bus_sel) 2'd0: uart_rx_in = uart232_rx_in; 2'd1: uart_rx_in = uart485_rx_in; 2'd2: uart_rx_in = uart422_rx_in; default: uart_rx_in = uart232_rx_in; endcase end这样做的代价是三个总线不能同时通信,但实验验证阶段完全够用。标题工程里的代码很可能是三套独立例化的方式,方便同时跑三路回环测试,具体看源码里的顶层文件怎么组织。
5. 串口不通时先查这些:Vivado 综合约束、上板调试与常见误配置
Vivado 工程从综合到生成比特流,再到上板串口调试,每一层都有坑。这一章按排查顺序给出最实用的检查清单。
5.1 综合前必须检查的三个约束点
首先是时钟约束。Zynq7020 的 FCLK_CLK0 来自 PS 端配置,Vivado 识别它需要 PS 的 XML 文件正确导入。在 block design 里勾选FCLK_CLK0并设置频率为 100MHz 后,综合工具会自动识别这个时钟,不需要手动写create_clock。如果你把引脚上的输入时钟直接用于逻辑(比如外接晶振),就必须在 XDC 里手动写:
create_clock -period 10.000 -name sys_clk [get_ports clk_in]这里-period 10.000对应 100MHz。忘记写这一行会导致时序分析结果不可信,虽然功能上不一定出错,但遇到时序收敛问题时很容易误判。
其次是引脚约束。标题工程里如果提供了开发板原理图,对应的 XDC 会以表格形式列出每个引脚的物理封装号。注意PACKAGE_PIN的大小写不能错,Vivado 的大小写敏感,e6和E6是不同的引脚。
最后是差分信号约束。RS485 和 RS422 的差分线在 FPGA 侧是单端信号,不需要写DIFF_TERM。但 MAX3485 的 A/B 引脚如果接反,数据会完全错乱,这种问题在原理图阶段就要仔细核对。
5.2 上板后串口乱码的排查顺序
串口乱码是所有串口实验中最常见的现象,可能的原因按概率排列如下:
波特率不匹配。先在串口助手里确认选的是 115200,再确认 Vivado 工程里的CLK_FREQ参数是 100 还是 50。很多开发板默认的时钟是 50MHz,代码里参数没改,实际波特率就只有 57600,收到的肯定乱码。
电平标准不匹配。RS232 的电平转换芯片如果用的是 MAX3232,供电必须是 3.3V,用 5V 供电会让输出电平超过接收端的输入范围。同理,RS485 的 A/B 线如果和另一端的 GND 不共地,共模电压漂移也会导致乱码。
差分线接反。RS485 和 RS422 的 A、B 接反会让数据完全翻转,表现为乱码或者完全收不到。排查方法是交换一对差分线再试。
电荷泵电容问题。RS232 的负压没建立起来时,发送端的输出波形会异常,接收端收到的都是 0xFF 或者无法同步。用示波器量一下 DB9 的 3 脚(TX)是否在空闲状态下为负电平,这是最直接的判断依据。
5.3 ILA 抓内部的 rx_done 信号
当串口助手完全收不到数据时,最有效的调试手段是插入 Vivado ILA(Integrated Logic Analyzer)核抓内部信号。在 block design 里右键添加 ILA,把uart_rx.rx_line、rx_done、rx_data连到探针上,上板后通过 hw_server 连接。
ILA 有一个非常实用的功能:触发条件是rx_done == 1'b1。如果触发不了,说明接收状态机根本没走到完成状态,问题出在硬件链路;如果能触发且rx_data值正确,说明接收侧没问题,故障在发送侧或者电脑串口设置。这个二分法能把排查时间缩短一半。
如果不想加 ILA 核,也可以用 LiteX 或直接写一个简单的状态机,把rx_done的情况映射到一个 LED 上闪烁一次,作为最简单的调试手段。在纯 Verilog 工程里,加一个 LED 指示比开 ILA 更快。
6. 多字节帧协议与 FIFO 缓冲:把 UART 用到量产固件里的收尾技巧
实验回环能跑通只是第一步。真正的工程应用里,UART 不会只收发单字节,而是收发成帧的数据报文。标题里的工程如果看懂了 UART 状态机,再往前一步就是这一章的内容。
6.1 定义一个可扩展的帧格式
常见的自定义帧格式是“帧头 + 长度 + 数据 + CRC”:
| 0xAA | 0x55 | LEN | DATA[0...N-1] | CRC8 |帧头0xAA 0x55用于找帧边界,LEN表示数据长度,CRC8 用于校验。接收侧的行为是:检测到0xAA 0x55后进入接收状态,连续读LEN字节,然后读 1 字节 CRC 并校验。注意 LEN 不能超过缓冲区的最大值,否则会产生越界,这是串口协议实现里最常见的漏洞之一。
CRC8 的 Verilog 实现用查表法最快,也可以用多项式除法逐位计算。对于 Zynq7020 这种资源充裕的芯片,查表法更省逻辑且逻辑清晰:
always @(*) begin case (crc_state) 8'h00: crc_next = 8'h00; // 实际工程填 256 项查找表 endcase end6.2 用异步 FIFO 隔离跨时钟域
Zynq7020 的常用场景是 PS 和 PL 各跑各的时钟,PS 把数据通过 AXI 总线写到 PL 的 FIFO,PL 的 UART 从 FIFO 取数发送。这时 FIFO 必须用 Xilinx 的axi_uartliteIP 或者自己例化一个异步 FIFO。Vivado 的 FIFO Generator IP 支持读写时钟不同,位宽不同,是标准做法。
自己写异步 FIFO 时,核心是格雷码指针同步。两个时钟域的读写指针不能直接比较,要把写指针同步到读时钟域,把读指针同步到写时钟域,满空标志的生成是关键。Xilinx 官方文档 PG057 描述了基础实现,工程上直接用 IP 更省事。
6.3 发完最后一帧再释放 RS485 总线:一个容易忽略的细节
RS485 多机通信时,“主动发送方发完最后字节后释放总线”的时序决定了下一个节点能否立刻接管总线。很多工程在发送状态机进入 IDLE 后立刻拉低 DE,但远程节点的收发器切换到接收模式也需要时间(通常几百纳秒)。在 DE 拉低之前插入一个 3~5us 的延时,能显著降低总线冲突概率。
实现方法很简单:方向控制模块里加一个延时计数器,当发送状态机的tx_busy结束信号到达后,启动延时计数,计满后再拉低dir_reg。这个延时的长度不需要太精确,只要能覆盖远程节点的切换时间即可。
最后还有一个容易被忽略的验证手段。做完三种总线的回环实验后,把波特率从 9600 一路调到 921600,观察每种总线的最大稳定速率。RS232 在长线缆上超过 115200 就会开始出错,RS422 在 10 米内跑到 921600 通常没问题,RS485 的表现则取决于终端电阻匹配——连线长度从 1 米换到 50 米,这个实验结果会和理论曲线吻合得很好。这些数据记录下来,就是你后续项目选型时最可靠的参考。
本文还有配套的精品资源,点击获取