
简介面向FPGA工程师、嵌入式硬件设计人员以及对工业总线协议感兴趣的进阶学习者这是一份以Xilinx XC7A35T为目标器件的RS485串口通信驱动Verilog HDL实现工程。工程内部按功能划分波特率发生器、数据移位寄存器、收发方向控制与使能信号管理、奇偶校验/错误检测等模块并给出了与MAX485类电平转换芯片衔接的控制逻辑可帮助读者理解FPGA如何构建完整RS485物理层接口。压缩包内共包含113个文件整体约1.18MB除Verilog源文件v外还集成了Vivado工程文件xpr、引脚约束xdc、综合/实现报告rpt、仿真与脚本文件sh/tcl/bat以及综合和布局布线后的DCP网表和可直接下载到芯片的比特流文件bit目录结构清晰便于跟踪从设计输入到生成配置文件的每一步。该资源已有205人学习下载。学习后可掌握FPGA上RS485通信驱动的模块拆分方法、时序控制要点和标准Vivado工程组织方式借鉴其调试与验证思路快速移植到其他需要对串行接口做定制化设计的场合。1. 从XC7A35T到RS485串口通信驱动解决的是什么问题FPGA开发板上的UART引脚输出的是3.3V单端电平而RS485是差分信号两者不能直接对接。XC7A35T作为Artix-7系列中比较常用的逻辑资源芯片经常出现在工业通信、多机采集和仪器控制场景里。这时要做的事很明确用Verilog HDL在FPGA内部实现一个串口通信驱动再通过外部的TTL转RS485收发器把单端信号转成差分总线信号。这看起来只是“UART加一个收发器”但在RS485半双工链路上还需要额外处理一根方向控制信号DE。如果DE在最后一个停止位发送结束前就被拉低接收端会看到一截被截断的帧如果DE拉得太晚又会占用总线时间。很多项目最终卡在不是“发不出数据”而是“方向切换时机不对”上。这篇文章从链路设计、UART内核、DE控制到仿真验证把XC7A35T上RS485串口通信驱动的完整落地路径讲清楚。适合正在用Vivado做FPGA串口项目、或者想把MCU侧串口移植到FPGA上的工程师参考。2. RS485链路设计与XC7A35T的引脚约束2.1 为什么不能把FPGA的UART引脚直接接到RS485总线上RS485通讯协议详解里经常强调RS485只是物理层和链路层的电气规范不规定字节帧格式。字节与字节之间怎么拼仍然是UART的起始位、数据位、停止位结构。但RS485总线上A、B两根线的差分电压和FPGA引脚上的LVCMOS电压完全是两套电平体系。如果直接把FPGA的TXD接到RS485的A线上驱动能力不够也没有办法产生反向差分电压。收发器芯片的作用是把FPGA输出的单端数据转换成差分信号同时把总线上的差分信号还原成单端电平给FPGA的RXD读取。常见芯片有SP3485、MAX3485、ISO3082等它们内部通常有发送器和接收器两部分分别由DI、RO引脚连接FPGA并由DE和RE控制工作状态。半双工RS485下A、B共用一对线收发方向不能同时有效。发送时收发器驱动总线接收时收发器释放总线。这个方向切换必须由FPGA控制不能默认接死。FPGA侧最需要关注的引脚就是DE它是RS485链路可靠性的核心。所以在写Verilog之前先把物理链路画清楚FPGA的UART TX接到收发器DIFPGA的UART RX接到收发器ROFPGA一个普通GPIO接到收发器的DE/RE。这样FPGA才能同时管理字节流和总线方向。2.2 TTL转RS485模块的接口与FPGA引脚规划市面上常见的TTL转RS485模块会把收发器、保护电路和终端电阻都集成好。对FPGA来说只需要规划四类引脚串口发送、串口接收、方向控制、全局时钟。下面是一张典型的引脚规划表FPGA信号方向电平标准接收发器引脚说明uart_clkinputLVCMOS33—系统时钟常见50 MHzuart_txdoutputLVCMOS33DI发送数据线uart_rxdinputLVCMOS33RO接收数据线rs485_deoutputLVCMOS33DE/RE高电平发送低电平接收rst_ninputLVCMOS33—低有效复位在Vivado里引脚约束用XDC文件写。实际板卡的FPGA封装不同引脚编号要以原理图为准。下面是一段示例XDCset_property PACKAGE_PIN R7 [get_ports uart_clk] set_property PACKAGE_PIN T7 [get_ports uart_txd] set_property PACKAGE_PIN U7 [get_ports uart_rxd] set_property PACKAGE_PIN V7 [get_ports rs485_de] set_property IOSTANDARD LVCMOS33 [get_ports {uart_clk uart_txd uart_rxd rs485_de}] create_clock -period 20.000 -name sys_clk [get_ports uart_clk]IOSTANDARD一定要和板卡bank电压一致Artix-7的HR bank通常用LVCMOS33或LVCMOS25HP bank则可能是LVCMOS18。如果电压不匹配Vivado在实现阶段会报IO标准冲突或者板子工作时序完全乱套。create_clock不能省RS485的波特率误差和时钟约束没有直接关系但没有时钟约束会给时序分析带来很多虚假路径。2.3 组网、终端电阻与总线偏置RS485一主多从的组网结构里所有设备的A、B并接到同一条总线上距离稍长就会出现反射。标准做法是在总线两端各并联一个120Ω终端电阻吸收高频反射。终端电阻会增大收发器负载所以一条总线上能挂的节点数量有限一般按“单位负载”计算32个标准单位负载是常见上限。如果总线上没有任何设备发送数据A、B之间电压接近0接收端输出可能进入不定态表现为FPGA收到一堆随机0xFF或0x00。解决办法是在A线上加一个上拉电阻到VCC在B线上加一个下拉电阻到GND让空闲时A比B高。很多现成的TTL转RS485模块已经做了这层偏置自制电路则需要另外计算偏置电阻阻值通常在470Ω到1kΩ之间。3. Verilog HDL实现RS485驱动的UART收发内核3.1 波特率发生器分频计算和误差波特率发生器的本质是对FPGA系统时钟做分频。最简单可靠的写法是产生一个“每比特周期出现一次”的baud_tick脉冲作为UART收发状态机的节拍。代码里用参数把时钟频率和波特率分离便于后续切换不同的RS485速率。// baud_gen.v module baud_gen #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115_200 )( input wire clk, input wire rst_n, output reg baud_tick ); localparam DIV_MAX CLK_FREQ / BAUD_RATE; reg [15:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; baud_tick 1b0; end else if (cnt DIV_MAX - 1) begin cnt 16d0; baud_tick 1b1; end else begin cnt cnt 16d1; baud_tick 1b0; end end endmodulebaud_tick每DIV_MAX个时钟周期输出一个高脉冲。50 MHz时钟除以115200得到434也就是说每434个时钟周期产生一次脉冲。由于整数除法带来的误差实际波特率和理论值会有微小偏差但只要偏差小于2%UART在8位数据内不会出现采样点漂移过大的问题。系统时钟目标波特率DIV_MAX实际波特率误差50 MHz960052089600.610.006%50 MHz115200434115207.40.006%50 MHz460800108462962.90.47%如果要跑高波特率比如1 Mbps以上建议把分频器改成小数分频或者用FPGA内部的MMCM直接产生靠近目标频率的时钟。3.2 发送状态机起始位、数据和停止位发送模块按状态机思想写成四个状态空闲、起始位、数据位、停止位。平时TXD保持高电平FPGA收到发送请求后先把待发送字节锁存到移位寄存器再输出一个低电平起始位然后从低位到高位逐个发送最后输出停止位。// uart_tx.v module uart_tx #( parameter DATA_WIDTH 8 )( input wire clk, input wire rst_n, input wire baud_tick, input wire [DATA_WIDTH-1:0] tx_data, input wire tx_start, output reg tx, output wire tx_busy ); localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; reg [1:0] state; reg [3:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; assign tx_busy (state ! IDLE); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; tx 1b1; bit_cnt 4d0; shift_reg 8d0; end else if (baud_tick) begin case (state) IDLE: begin if (tx_start) begin shift_reg tx_data; tx 1b1; state START; bit_cnt 4d0; end end START: begin tx 1b0; state DATA; end DATA: begin if (bit_cnt DATA_WIDTH - 1) begin tx shift_reg[0]; bit_cnt 4d0; state STOP; end else begin tx shift_reg[0]; shift_reg shift_reg 1; bit_cnt bit_cnt 1b1; end end STOP: begin tx 1b1; state IDLE; end endcase end end endmodulebit_cnt从0计数到7每发完一个数据位就右移一次寄存器把下一位推到最低位。停止位状态里把TX拉高同时回到空闲状态。tx_busy可以供上层逻辑判断是否允许写入下一个字节避免发送过程中覆盖待发送数据。3.3 接收状态机下降沿检测和中点采样接收端不能直接使用自由运行的baud_tick去采样因为起始位到来时本地时钟和发送端时钟相位不一定对齐。稳一点的做法是检测RXD的下降沿然后用一个计数器等待半个比特周期再在数据位的中间点采样。// uart_rx.v module uart_rx #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115_200 )( input wire clk, input wire rst_n, input wire rxd, output reg [7:0] rx_data, output reg rx_done ); localparam IDLE 2d0; localparam HALF_WAIT 2d1; localparam SAMPLE 2d2; localparam STOP 2d3; localparam FULL_BIT CLK_FREQ / BAUD_RATE; localparam HALF_BIT FULL_BIT / 2; reg [1:0] state; reg [15:0] cnt; reg [3:0] bit_cnt; reg [7:0] shift_reg; reg rxd_d1; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_d1 1b1; state IDLE; cnt 16d0; bit_cnt 4d0; rx_done 1b0; rx_data 8d0; end else begin rxd_d1 rxd; case (state) IDLE: begin if (rxd_d1 (~rxd)) begin state HALF_WAIT; cnt 16d0; bit_cnt 4d0; end end HALF_WAIT: begin if (cnt HALF_BIT) begin state SAMPLE; cnt 16d0; end else begin cnt cnt 16d1; end end SAMPLE: begin if (cnt FULL_BIT) begin cnt 16d0; if (bit_cnt DATA_WIDTH - 1) begin rx_data {rxd, shift_reg[7:1]}; rx_done 1b1; state STOP; end else begin shift_reg {rxd, shift_reg[7:1]}; bit_cnt bit_cnt 1b1; end end else begin cnt cnt 16d1; end end STOP: begin rx_done 1b0; state IDLE; end endcase end end endmodule接收端在检测到下降沿后等FULL_BIT/2个时钟也就是理论上起始位的中间点然后按完整的比特周期采样8次。shift_reg用低位先到的方式拼接8次采样结束后rx_data里的bit0就是字节的最低位。3.4 把UART内核接到RS485收发器顶层信号分配在顶层模块里把uart_tx的tx输出接到uart_txduart_rx的rxd输入接uart_rxd再把rs485_de作为单独信号引出。发送开始时拉高DE发送结束后延迟一个比特周期再拉低DE具体逻辑在第4章展开。4. RS485方向控制与一主多从组网的关键参数4.1 半双工方向切换DE为什么要延迟一个停止位RS485半双工的特点决定了一根总线上同一时间只能有一个节点驱动总线。FPGA在发送数据时DE必须为高数据帧发送完成后DE必须拉低让总线回到接收状态。如果DE在停止位还没结束时就拉低收发器会提前释放总线停止位后半段变成高阻态对端接收到的电平可能被总线偏置电阻拉高从而把帧尾判断成空闲状态导致最后一位数据丢失。更隐蔽的问题是“收到自己发出的数据”。RS485总线上所有节点共享A、B线FPGA发送时收发器的RO也会把总线上的数据回送给RXD。如果驱动方向切换正常发送期间RXD上会出现自己发出的字节这对应用层一般无所谓但如果系统要求主机发送后立刻判断是否收到从机响应需要把发送期间的RXD数据屏蔽掉否则会和从机回包混在一起。4.2 用Verilog设计RS485方向控制模块方向控制模块的任务就是跟踪UART发送状态在DE拉高后保持足够时间发送结束再延迟一个比特周期关闭驱动器。下面的代码用tx_busy和tx_done组合出DE信号。// rs485_de_ctrl.v module rs485_de_ctrl #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115_200 )( input wire clk, input wire rst_n, input wire tx_busy, input wire tx_done, output reg rs485_de ); localparam ONE_BIT CLK_FREQ / BAUD_RATE; reg [15:0] hold_cnt; reg sending; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rs485_de 1b0; hold_cnt 16d0; sending 1b0; end else begin if (tx_busy) begin sending 1b1; hold_cnt 16d0; rs485_de 1b1; end else if (sending) begin if (tx_done) begin sending 1b0; hold_cnt 16d0; end else if (hold_cnt ONE_BIT) begin hold_cnt hold_cnt 16d1; end else begin rs485_de 1b0; end end end end endmodule这里借用tx_done确认UART已经进入停止位之后至少再等一个完整比特周期才拉低DE。hold_cnt的作用是补偿从FIFO读取数据到UART真正输出起始位之间的空档。如果把DE和tx_busy直接连在一起停止位结束后DE立刻拉低数据线的最后一位电压还没有维持满一个比特时间总线很容易被噪声干扰。4.3 一主多从的地址过滤与接收时序RS485一主多从中主机发送带有从机地址的帧所有从机都能收到但只有地址匹配的从机响应该帧。FPGA从机侧可以在UART接收完整字节后判断第一帧地址是否等于本机地址不匹配就直接丢弃后续字节。实现地址过滤时建议使用一个简单的状态寄存器reg addr_match; always (posedge clk or negedge rst_n) begin if (!rst_n) begin addr_match 1b0; end else if (rx_byte_valid (rx_byte 8h01)) begin addr_match 1b1; // 地址匹配等待完整命令 end else if (rx_done) begin addr_match 1b0; end end从机回包时也要遵循“先听后说”原则不能检测到总线空闲立即发送。比较好的做法是在总线空闲条件满足后继续等待3到5个字节时间避免两个从机同时抢占总线。主机发送命令和从机回包之间要预留足够的间隔一般至少1毫秒。4.4 自动收发电路的局限与FPGA控制的优势很多低成本的TTL转RS485模块使用自动收发电路不需要外接DE控制。自动电路通常用RC延时和三极管驱动DE节省一根信号线。但这种电路的RC时间常数固定波特率升高后发送结束的“释放延迟”不够精确往往出现停止位被提前截断或者收发切换震荡增多。控制方式控制精度波特率适应性典型问题自动收发电路RC延时固定9600以下尚可高波特率误码、停止位截断FPGA直接控制DE精确到一个比特周期可到10 Mbps以上需要多一根GPIOFPGA的优势在于波特率可参数化DE延时可依据时钟精确计算。对于大批量生产或者长效系统用FPGA直接控制DE比省一个引脚更可靠。5. 用Vivado和串口助手验证RS485驱动5.1 仿真testbench和时序波形检查先用Vivado Simulator做功能仿真。在testbench里生成50 MHz时钟把uart_tx输出的tx信号直接连回uart_rx的rxd不需要等上板就能验证UART内核的字节收发。下面是一个最小testbench骨架。module tb_rs485_uart; reg clk 0; reg rst_n 0; reg tx_start 0; reg [7:0] tx_data 8hA5; wire tx_busy, tx_line; wire [7:0] rx_data; wire rx_done; always #10 clk ~clk; initial begin #100 rst_n 1; #200 tx_data 8hA5; tx_start 1; #20 tx_start 0; wait (rx_done); $display(received %02x, rx_data); #500 $finish; end baud_gen #(.CLK_FREQ(50_000_000), .BAUD_RATE(115_200)) u_baud (.clk(clk), .rst_n(rst_n), .baud_tick(baud_tick)); uart_tx u_tx ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .tx_data(tx_data), .tx_start(tx_start), .tx(tx_line), .tx_busy(tx_busy) ); uart_rx u_rx ( .clk(clk), .rst_n(rst_n), .rxd(tx_line), .rx_data(rx_data), .rx_done(rx_done) ); endmodule仿真时重点看起始位低电平宽度是否等于波特率对应的比特周期看DE在tx_busy结束后是否延时一个比特再拉低。Vivado仿真默认的时间尺度是1ns#10 clk ~clk生成的就是50 MHz时钟这一点在检查波形时很容易确认。5.2 上板回环与常见故障排查上板时先做“FPGA内部回环”就是把uart_tx输出直接接到uart_rx输入这样通过串口助手发数据给FPGAFPGA原样返回能确认UART内核本身没问题。但内部回环会绕过RS485收发器不能证明物理链路正常。真正的RS485回环测试需要把FPGA的uart_txd接到收发器DI让总线A、B对接再把另一个TTL转RS485模块的A、B并联模块的RO接回来。串口助手发送一个测试帧FPGA收到后原样回发在串口助手上看到相同数据才算通过。常见的问题有三个。第一收到乱码优先检查波特率分频系数是否算错误差在460800这种高速率下会接近0.5%如果线路过长会出现采样点漂移第二能收不能发看DE是否拉高收发器DI、RO方向是否接反第三总线空闲时收到连续0xFF通常是A、B偏置电阻没接好总线电压没有稳定在确定的逻辑电平上。5.3 把驱动封装成AXI-Lite IP如果后续要接MicroBlaze软核或者用Vivado的Block Design做系统集成可以把UART和DE控制整体封装成一个AXI-Lite外设。使用Vivado的Create and Package New IP把顶层端口暴露给寄存器接口偏移地址映射可以这样设计偏移地址寄存器名读/写功能0x00TX_DATA写写入待发送字节0x04RX_DATA读读出接收到的字节0x08STATUS读bit0表示TX忙bit1表示RX完成封装完成后MicroBlaze只需要对上述寄存器做存储器读写就能控制RS485不需要关心底层的电平变化和DE时序。这种封装方式对后续项目复用很有价值换一个FPGA芯片时只要换引脚约束Verilog设计核心部分不需要改动。本文还有配套的精品资源点击获取