十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA实现RS485通信:基于Verilog的UART收发与方向控制实战

FPGA实现RS485通信:基于Verilog的UART收发与方向控制实战 简介本资源是面向FPGA初学者与嵌入式通信开发者的实战型工程包聚焦Xilinx XC6SLX16平台上的RS485工业总线通信实现解决FPGA端串行协议硬件化设计难点。项目采用模块化Verilog HDL编写完整涵盖波特率发生器、双工切换控制DE/RE信号管理、发送/接收状态机、数据缓冲及基础错误检测逻辑可直接部署于XC6SLX16开发板适用于工业自动化、远程传感器组网等抗干扰长距通信场景。压缩包共67个文件含核心功能源码.v、约束文件.ucf、综合与实现报告.xrpt/.html/.log、位流文件.bit及ISE工程配置.xise/.prj总大小341KB结构清晰、层次分明便于理解编译流程与调试路径。已有192人学习下载提供即用型RTL代码与配套工程环境显著降低RS485协议在FPGA上落地的门槛助力快速掌握时序控制、异步接口协同与硬件驱动开发要领。 做FPGA开发的人早晚会遇到一个场景你的板子上明明有高速收发器、DDR3、PCIe这些猛料结果项目落地的第一步却是老老实实接一根RS485线和现场的PLC、变频器、电表去“对上话”。我当时拿到这颗Xilinx Spartan-6 XC6SLX16的时候就是这个情况。外围挂了一圈工业设备通信口全是RS485主控要求用FPGA直接出485总线不走单片机中转。于是就有了这个完整的工程基于Verilog HDL在XC6SLX16上从零实现RS485半双工通信包含UART收发、波特率生成、方向控制、仿真Testbench以及板级调试记录。这篇博文我尽量不写废话把工程拆开讲透。哪些是原理哪些是代码里真正该注意的细节哪些是仿真跑不到、只有上板才会踩的坑都会提到。适合刚接触FPGA串口通信的开发者也适合那种“明明代码没问题但就是收不到数据”正在挠头的朋友。1. 整体方案拆解为什么用FPGA做RS485以及XC6SLX16够不够用1.1 需求定位这个工程真正解决什么问题先说清楚RS485是什么。它在电气层上用的是差分信号A、B两根线之间的电压差表示逻辑1和逻辑0。协议层其实走的还是UART那一套——起始位、数据位、停止位。所以你在FPGA里做的核心工作第一是UART收发引擎第二是控制RS485收发器的方向引脚第三是处理总线仲裁和空闲状态。这个工程要解决的典型场景有几个工业控制板上需要直接对接RS485设备不想在FPGA外面再挂一颗MCU去转串口。需要FPGA作为主站轮询多个从站或者作为从站被动响应主站命令。板卡上RS485总线距离比较远、节点多用FPGA做底层收发可靠性更高时序也可控。我做的这个项目CPU侧或者说逻辑侧提供一个并行数据接口把要发送的字节写进去然后底层Verilog模块负责把字节打成UART帧、控方向、发到总线接收方向则是从485芯片RO引脚收到串行数据完整解析成字节后给到逻辑侧。整个过程全在FPGA内部完成不依赖外部处理器。1.2 平台选型XC6SLX16的资源与定位XC6SLX16是Spartan-6家族的一员逻辑单元大约15K左右内嵌DSP48A1和Block RAM。做RS485这种低速串口通信资源占用可以说是“九牛一毛”但选它做平台有实际考量Spartan-6系列工业级芯片在工控领域存量很大很多老项目都是它在扛。芯片支持3.3V IO直接可以接MAX3485、SP3485这类RS485收发器不用做电平转换。虽然跑不了Linux但做逻辑控制、协议解析非常稳没有操作系统调度带来的延迟抖动。整个工程下来我印象中逻辑资源消耗不到5%Block RAM基本没用。所以如果你是想在这个工程基础上继续扩展Modbus、自定义协议、帧解析、CRC校验资源完全够用。如果只是学习验证也可以用其他Spartan-6板卡管脚约束改一下就成。1.3 方案架构模块如何划分设计上我分了四层顶层模块rs485_top ├── 波特率生成模块baud_gen ├── UART发送模块uart_tx ├── UART接收模块uart_rx └── 方向控制逻辑dir_ctrl发送通路逻辑侧给出[7:0] tx_data和tx_start脉冲uart_tx按设定的波特率逐bit发出同时在发数据前拉高RS485收发器的DE使能也就是dir信号发完停止位后再延迟释放总线。接收通路RO引脚上的数据直接进uart_rx检测起始位下降沿然后按波特率在数据位中点采样收完8个bit后拼成字节输出rx_data和rx_valid脉冲。方向控制是485和普通UART最大的区别点。因为半双工同一个时刻只能收或只能发dir信号必须和发送时序严格配合。这里我在发送完停止位之后特意多等了一个bit周期再释放总线目的就是防止最后一个停止位还没发完就切回接收模式导致总线尾部被拉坏。2. UART协议细节与RS485物理层要点这部分不看后面全是坑2.1 UART帧格式RS485的协议核心就是UART一帧数据从左往右依次是空闲状态总线为高电平逻辑1。起始位一个bit周期的低电平接收方靠这个下降沿同步。数据位通常是8个bit低位在前LSB first。校验位可选项目里一般不用直接用无校验。停止位至少1个bit周期的高电平表示一帧结束。这里有个理解误区很多新手以为UART一帧就是“数据”实际上起始位和停止位占了额外的时间开销。比如115200波特率、8N1格式一帧总共10个bit有效数据是8个bit有效传输速率其实是波特率乘0.8。2.2 RS485物理层差分电平、方向控制、终端电阻RS485用A、B两根差分线传输A比B高表示逻辑1A比B低表示逻辑0。实际工程中总线空闲时一定要保证A比B高否则接收端会误判为持续的数据。这个靠什么保证两个手段总线两端各加一个120欧终端电阻匹配阻抗防止信号反射。在A线上加一个上拉电阻到电源B线下拉电阻到地保证空闲偏置在逻辑1状态。我用的收发器是SP34853.3V版本和MAX3485兼容引脚一样工作模式由DE和RE两个引脚控制DE高电平使能发送RE低电平使能接收。常规做法是把DE和RE短接用一个dir信号控制dir1时处于发送模式dir0时处于接收模式。FPGA的IO输出3.3V电平直接控制SP3485没问题。要注意的是SP3485的A、B引脚是差分对不是普通IO千万别接到FPGA的普通IO上。2.3 波特率发生分频计数而不是用PLL有人会问XC6SLX16有DCM数字时钟管理器为什么不直接用PLL出个115200的时钟我实测下来没必要而且有坑。RS485的波特率时钟是很低频的比如9600、115200如果直接用PLL生成115200Hz的时钟反而会让跨时钟域处理变复杂。更规范、更通用的做法是系统时钟比如50MHz跑主逻辑内部用一个计数器分频产生一个“波特率tick”脉冲每当tick有效时UART状态机前进一步。这样做的优势是明显的所有逻辑都在一个时钟域里仿真和调试都简单。改波特率只需要改一个分频参数不用重新生成时钟约束。采样逻辑可以用更高的时钟频率来做获得更精准的中点采样。分频值计算方式DIV 系统时钟频率 / 波特率如果系统时钟是50MHz波特率115200那么DIV 434.0278取整434实际波特率是115207.37误差只有0.006%完全满足UART“误差小于2%”的要求。3. Verilog核心模块设计与实现每一段代码都有讲究3.1 顶层模块接口规划先给出顶层模块的接口这样后续代码看着不迷路module rs485_top #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, // 发送接口 input wire [7:0] tx_data, input wire tx_start, output wire tx_busy, // 接收接口 output wire [7:0] rx_data, output wire rx_valid, // 外部RS485收发器接口 output wire uart_tx, // 接SP3485的DI input wire uart_rx, // 接SP3485的RO output wire rs485_dir // 接SP3485的DE/RE1发送0接收 );复位信号我惯例是低有效rst_n。tx_busy拉高期间外部逻辑不要发起新的发送请求rx_valid是单周期脉冲指示rx_data有效。3.2 波特率生成模块这个模块是整个通信的“节拍器”核心代码很简洁module baud_gen #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, output reg baud_tick ); localparam DIV CLK_FREQ / BAUD_RATE; reg [$clog2(DIV)-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; baud_tick 1b0; end else if (cnt DIV - 1) begin cnt 0; baud_tick 1b1; end else begin cnt cnt 1b1; baud_tick 1b0; end end endmodulebaud_tick是高电平一个时钟周期的脉冲频率等于波特率。需要注意$clog2是Verilog-2001的系统函数用来计算DIV需要的位宽ISE 14.7和Vivado都支持。对于50MHz、115200波特率DIV434计数器范围是0到433溢出时拉一个tick。3.3 UART发送模块设计发送状态机有四个状态空闲、起始位、数据位、停止位。module uart_tx #( parameter CLK_FREQ 50_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_busy, output reg txd ); localparam IDLE 2d0; localparam START_BIT 2d1; localparam DATA_BITS 2d2; localparam STOP_BIT 2d3; reg [1:0] state; reg [3:0] bit_cnt; reg [3:0] bit_timer; reg [7:0] tx_shift; wire baud_tick; baud_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_baud ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick) ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; txd 1b1; tx_busy 1b0; bit_cnt 0; bit_timer 0; end else begin case (state) IDLE: begin txd 1b1; if (tx_start) begin tx_busy 1b1; tx_shift tx_data; bit_cnt 0; state START_BIT; end end START_BIT: begin txd 1b0; if (baud_tick) begin bit_timer 0; state DATA_BITS; end end DATA_BITS: begin txd tx_shift[0]; if (baud_tick) begin tx_shift {1b0, tx_shift[7:1]}; bit_cnt bit_cnt 1b1; if (bit_cnt 3d7) state STOP_BIT; end end STOP_BIT: begin txd 1b1; if (baud_tick) begin bit_timer bit_timer 1b1; if (bit_timer 4d1) begin state IDLE; tx_busy 1b0; end end end endcase end end endmodule说明几点数据移位方向是LSB first先把低位移出去。tx_shift在停止位结束后才恢复不需要额外清成0。停止位我发了2个bit周期这是一种保护性设计。半双工切换方向需要时间停驻位长一点总线释放时不会把最后一位搞坏。tx_busy拉高的时间覆盖整个帧外部逻辑可以在tx_busy拉低后立刻发下一帧也可以通过延迟等待。3.4 UART接收模块设计接收模块比发送稍微复杂一点难点在起始位检测和采样点选择。module uart_rx #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, input wire rxd, output reg [7:0] rx_data, output reg rx_valid ); localparam IDLE 2d0; localparam START_BIT 2d1; localparam DATA_BITS 2d2; localparam STOP_BIT 2d3; reg [1:0] state; reg [3:0] bit_cnt; reg [7:0] shift_reg; reg [8:0] sample_cnt; // 用于在bit中间位置采样 reg rxd_d1, rxd_d2; wire baud_tick; baud_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_baud ( .clk(clk), .rst_n(rst_n), .baud_tick(baud_tick) ); // 同步打拍消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_d1 1b1; rxd_d2 1b1; end else begin rxd_d1 rxd; rxd_d2 rxd_d1; end end wire rxd_negedge rxd_d2 ~rxd_d1; // 检测下降沿 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; rx_data 0; rx_valid 1b0; shift_reg 0; bit_cnt 0; sample_cnt 0; end else begin rx_valid 1b0; case (state) IDLE: begin if (rxd_negedge) begin state START_BIT; sample_cnt 0; bit_cnt 0; end end START_BIT: begin if (baud_tick) begin sample_cnt sample_cnt 1b1; // 在起始位中点确认确实是低电平防止毛刺误触发 if (sample_cnt (CLK_FREQ/BAUD_RATE/2 - 1) rxd_d2 1b0) begin sample_cnt 0; state DATA_BITS; end else if (sample_cnt (CLK_FREQ/BAUD_RATE - 1)) begin // 起始位结束还没确认到低电平说明是毛刺回IDLE state IDLE; end end end DATA_BITS: begin if (baud_tick) begin sample_cnt sample_cnt 1b1; if (sample_cnt (CLK_FREQ/BAUD_RATE/2 - 1)) begin shift_reg {rxd_d2, shift_reg[7:1]}; bit_cnt bit_cnt 1b1; sample_cnt 0; if (bit_cnt 3d7) state STOP_BIT; end end end STOP_BIT: begin if (baud_tick) begin sample_cnt sample_cnt 1b1; if (sample_cnt (CLK_FREQ/BAUD_RATE/2 - 1)) begin if (rxd_d2 1b1) begin rx_data shift_reg; rx_valid 1b1; end state IDLE; end end end endcase end end endmodule这段代码有几个关键点必须讲起始位检测用两拍同步防止rxd输入带来的亚稳态。实际测试中输入信号没有经过时钟同步直接进状态机偶发会出现整个字节错位。起始位中间要再确认一次低电平。我遇到过总线上有尖峰毛刺导致状态机误入数据位的情况加了“中点确认”之后误触发概率大幅下降。数据采样点放在每个bit的中间位置这是UART通行的做法。中间位置抗干扰性最好偏离中点越远越容易在信号边沿附近采到不确定电平。3.5 RS485方向控制最容易翻车的地方方向控制其实就是一句话发送前拉高dir发完别立刻拉低等总线稳定后再释放。但我还是单独给它写了一个小节因为实际工程中在这个地方翻车的人太多了。module dir_ctrl ( input wire clk, input wire rst_n, input wire tx_busy, output reg rs485_dir ); reg [3:0] release_cnt; reg release_flag; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rs485_dir 1b0; // 默认接收状态 release_cnt 0; release_flag 0; end else begin if (tx_busy) begin rs485_dir 1b1; release_flag 1b1; release_cnt 0; end else if (release_flag) begin release_cnt release_cnt 1b1; // 延迟一小段时间再切回接收我习惯延迟4个时钟周期即可 if (release_cnt 4d3) begin rs485_dir 1b0; release_flag 1b0; end end end end endmodule为什么发送完不立刻切回接收因为如果tx_busy在停止位发送完毕的同拍拉低此时最后一个停止位可能还在线上传输方向控制立刻翻转会让总线电平还没恢复到空闲就被强拉成接收状态对端设备收到的是一个不完整的停止位极可能导致后续帧错位。我测试中遇到过对端返回数据最后一个字节偶尔丢失就是这个问题。4. 仿真验证与板级调试很多问题代码阶段发现不了4.1 Testbench设计与仿真要点仿真这一步千万不能偷懒。写一个覆盖发送、接收、回环的Testbench能提前发现大半问题。我当时的Testbench结构大致是module rs485_top_tb; reg clk 0; reg rst_n 0; reg [7:0] tx_data; reg tx_start; wire tx_busy; wire [7:0] rx_data; wire rx_valid; wire uart_tx; reg uart_rx; wire rs485_dir; // 生成50MHz时钟 always #10 clk ~clk; // 初始化 initial begin rst_n 0; tx_data 8h00; tx_start 0; uart_rx 1b1; repeat (10) (posedge clk); rst_n 1; // 发送字节 0x5A (posedge clk); tx_data 8h5A; tx_start 1; (posedge clk); tx_start 0; // 等待发送完成 wait (!tx_busy); #1000; $finish; end // 简单回环把发送数据立即回环到接收 always (*) uart_rx uart_tx; rs485_top u_top ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data), .tx_start(tx_start), .tx_busy(tx_busy), .rx_data(rx_data), .rx_valid(rx_valid), .uart_tx(uart_tx), .uart_rx(uart_rx), .rs485_dir(rs485_dir) ); endmodule仿真里最值得看的是这几处uart_tx波形是否符合UART帧格式起始位、数据位、停止位电平是否正确。rx_valid和rx_data是否在停止位中点之后出现数据是否和发送字节一致。rs485_dir是否在发送帧开始时拉高发送结束后延迟拉低。连续发送多个字节中间是否有丢帧。我会建议在Testbench里再加一个“连续发送10个字节”的用例因为单字节仿真通过不代表连续发送没问题。实测中连续发送最容易暴露状态机回不到IDLE、方向控制来不及释放这类bug。4.2 板级调试验证代码下载到XC6SLX16之后我习惯先做回环自测再做环回对接。回环自测就是把板上的SP3485 DI和RO直接用跳线连起来注意是数字侧不是A/B总线侧FPGA发一个字节看能不能正确收回来。这一步能确认FPGA内部逻辑通不通。如果回环自测过了再接真正的RS485总线和对端设备电脑USB转485、PLC、其他FPGA板通信。这里我列一下我实际测试时的连接方式测试对象连接方式验证内容板级回环DI接RO板内UART收发链路对PC通信板上485 A/B接USB转485与上位机收发对PLC通信A/B接PLC COM口主站轮询/从站应答双FPGA互联两块板A/B对接共地板间数据互传测试中发现一个非常隐蔽的问题两块FPGA板A/B对接时如果两边的地没有连在一起偶尔会出现整包乱码。RS485虽然是差分传输但它不是隔离的A/B线上的共模电压必须控制在-7V到12V范围内。两个设备的GND电位差太大SP3485的A/B引脚就会超出共模范围接收端看到的就是持续错误电平。我后来在A/B总线靠近板卡的地方并了一个TVS管情况好很多。4.3 常见问题排查速查表把调试过程中比较典型的问题整理成表格遇到类似情况可以按图索骥现象可能原因排查手段接收数据整帧错位波特率不匹配、起始位检测被毛刺干扰双通道示波器对比TX/RX检查波特率分频值第一个字节对后续字节乱码方向控制释放过早把停止位拉坏检查release_cnt延迟时间示波器看dir和TX时序板上回环正常接总线丢包终端电阻匹配问题或共模电压超限检查120欧电阻确认A/B接地参考一致总线空闲时对端一直收到0xFF或0x00偏置电阻缺失总线空闲状态不确定在A上加上拉、B上下拉电阻发送时对端偶尔收到错误帧发送结束立刻切接收停止位不完整增加停止位长度延迟切接收上电瞬间总线自动发出一包乱码FPGA复位前IO状态不定DE被意外拉高在SP3485的DE/RE脚加下拉电阻或FPGA配置期间置dir为低4.4 关于“无法通讯但波形看着正常”的一个心得这里分享一个真实的排查经历。有次调试示波器看A、B差分波形非常漂亮帧格式、电平都对但PC端就是收不到数据。我查了半小时才发现是USB转485模块的A、B接反了。RS485的A和B不能凭颜色判断不同厂家的标注会不一样。有些模块标A/B有些标D/D-有些干脆不标。最保险的办法是示波器确认正常发送时A相对B为正对应UART的空闲高电平。接反了之后UART帧完全翻转PC端只会收到一堆0xFF或者乱码。这种情况用万用表测“静态电压”不太容易看出来最好直接看差分波形。5. 工程扩展从“能通信”到“能干活”5.1 组网通信与多节点接入RS485天然支持多点组网标准情况下一条总线上可以挂32个节点现在很多收发器芯片支持更多。在FPGA实现上组网的核心是协议层的地址区分而不是物理层。我做的这个工程在物理层是透明的也就是说它不关心总线上跑的是Modbus、自定义协议还是裸串口。你想往上叠协议只需要在顶层外面建一个协议状态机根据收到的地址字节决定是丢弃还是处理。常见的做法是主站发一个地址字节所有从站都收从站判断地址是否匹配匹配则继续解析后续命令不匹配则忽略。FPGA从站还可以根据地址自动切换“收地址”和“收数据”两种状态。5.2 加CRC校验和帧超时处理裸UART传字节是没有完整性校验的如果总线距离长、环境干扰大建议在协议层加上CRC16或者CRC8。CRC模块建议用查表法或者LFSR实现。FPGA上做CRC的典型写法是用一个移位寄存器配合多路异或组合逻辑直接算出来不占用太多资源。帧超时也要做。比如主站发了帧头之后突然不发了从站如果一直等整个通信就卡住了。我一般是加一个超时计数器超过若干ms没收到下一个字节就清空接收缓冲区回到等待帧头的状态。5.3 集成到更大的系统比如给三菱PLC、变频器读数据很多工业场景下FPGA板卡要作为中控和多个PLC、变频器走RS485轮询。比如三菱PLC通常支持专用协议或者Modbus RTU变频器一般支持Modbus RTU。FPGA里做Modbus RTU主站完全可行资源消耗很小逻辑侧只需要按顺序发送请求帧、解析响应帧然后记录数据。这个时候工程里的UART收发模块就不用改了只需要在顶层外面加一个协议层。这一步也是为什么要强调“收发引擎和协议层分离”的原因——底层通讯做得通用上层业务随便换。5.4 性能优化思路RS485本身速度不高一般也就9600、19200、115200。如果项目要求更高的总线利用率有几件事可以做发送缓冲区用FIFO主逻辑可以先丢一帧进去就干别的不用等发送完。接收缓冲区用FIFO避免CPU在处理数据时接收来的字节被覆盖。对于Modbus这类半双工协议可以加一个“帧间静默时间”检测自动分离帧不需要主站额外拼接数据。XC6SLX16自带Block RAM做小深度的FIFO很容易。FIFO可以自己写也可以用IP核我这里是直接手工实现的因为只是数据缓冲不需要复杂功能。6. 总结成一句实操心得这个工程做完之后我最大的感触是RS485在FPGA上的实现难点不在UART协议而在方向控制、总线状态管理和系统工程思维。代码层面UART收发你可以在网上找到几十个版本但真正决定系统稳不稳定的是发送后释放总线的时间、空闲时总线电平的确定、以及和外部设备的共地处理这些细节。最后分享一个小技巧如果调试时总是“偶尔丢一帧”建议用示波器同时抓A/B差分波形和FPGA的dir信号单次触发触发条件设为dir下降沿。这样能看到发送结束切换到接收时总线电平是否瞬间出现了毛刺。这个毛刺就是丢帧的元凶解决它一般通过延长停止位或者增加发送结束后的空余时间就能搞定。本文还有配套的精品资源点击获取
返回列表