资讯动态

FPGA XC6SLX16实现RS485通信:Verilog HDL从UART到差分总线

发布时间:2026/10/27 7:35:56 来源:尧图企业网站定制
简介这是一份面向FPGA开发者的RS485通信完整工程基于Xilinx XC6SLX16芯片使用Verilog HDL实现解决了FPGA与工业串行总线对接的关键问题。压缩包共67个文件约341KB以.v源文件、.ucf管脚约束、.bit配置文件及ISE工程文件为主包含顶层模块、UART收发、按键消抖、LED控制等Verilog代码以及综合实现报告和仿真脚本结构清晰可直接在XC6SLX16上编译运行。工程实现了半双工多机通信支持长距离传输与高噪声免疫并具备发送/接收使能切换、波特率发生器和奇偶校验等核心机制。目前已吸引197人学习交流。通过该工程可掌握RS485控制器设计、状态机编写与错误处理思路适合正在学习FPGA串口通信或需要快速搭建工业通信链路的学生与工程师参考复用。1. 为什么用XC6SLX16做RS485通信从串口到485的差距拿到一个“FPGA XC6SLX16实现RS485通信Verilog HDL实现”的工程包先别急着看代码。这个包解决的是工业现场最常见的需求FPGA板卡和外部设备仪表、PLC、传感器之间做远距离、多节点的数据交换。XC6SLX16是Spartan-6家族里的中低端型号逻辑资源不算多但几十个用户IO、内置串行收发器用来做UART转RS485这种协议桥接正好合适。RS485不是一种全新的协议它只是把UART的TTL电平转成差分信号再加一根方向控制线。工程包的核心就三件事Verilog怎么写UART收发、怎么控制收发器方向、怎么在XC6SLX16上跑通。适合刚入门FPGA但写过串口的开发者也适合做设备联网改造的老手——直接拿模块改个波特率就能用。2. 先从UART讲起RS485的物理层和Verilog设计起点2.1 RS485本质上还是串口什么时候用485而不是232RS485和RS232在逻辑层都是UART数据帧、波特率、校验位完全一样。区别在电气层RS232用单端电平地线共参考通信距离通常不超过15米RS485用差分信号A/B两线之间的电压差表示0和1共模抑制能力强距离能到1200米一条总线上还能挂32个节点标准负载。所以你在FPGA里看到的字节流、起始位、停止位跟串口一模一样唯一多出来的是方向控制。用XC6SLX16做RS485不用像写协议栈那样重造轮子。FPGA内部跑一个UART收发核外部接一颗RS485收发器芯片常见的是SP3485、MAX3485、ISL83485FPGA的UART_TXD接到收发器的DIUART_RXD接到RO再给DE/RE引脚一个方向信号。半双工模式下发送时拉高DE接收时拉低RE/DE。就这么简单。工程包里如果有“uart_tx.v”“uart_rx.v”这类文件那就是基础部分真正决定通信稳定性的是后面的波特率时钟和方向切换时机。2.2 收发使能控制只有半双工才需要方向引脚RS485有半双工和全双工两种。四线制全双工需要两对差分线收发互不干扰不需要方向控制两线制半双工是主流A/B共用一对线发送和接收必须分时复用。普通RS485收发器芯片的DEDriver Enable和REReceiver Enable通常可以连在一起用一个IO控制高电平时发送低电平时接收。Verilog里这个信号就叫rs485_dir或de。方向切换是新手最容易翻车的地方。发送完最后一个停止位后如果立刻把de拉低收发器内部还有几个纳秒的关断延时而总线上的信号可能还没完全释放紧接着就开始接收会把自己刚发出去的尾巴当成数据收回来。更稳的做法是发送使能保持到停止位发送完成后再延时一段时间比如一个波特率位周期或者干脆等发送FIFO空并且移位寄存器完全结束再拉低DE。工程包里如果只是简单的assign de tx_active就要注意这个尾巴问题。2.3 波特率时钟生成从50MHz分频到115200的误差计算RS485的波特率由UART时钟决定。XC6SLX16上常见的系统时钟是50MHz或100MHz直接拿系统时钟做UART采样时钟没问题只要分频准确。以115200bps为例用50MHz时钟每个bit的计数值是50_000_000 / 115_200 ≈ 434.0278。取整434实际波特率是50_000_000 / 434 / 2? 不对UART采样一般是16倍过采样或直接用1倍波特率时钟。工程包里常见的做法是生成一个波特率使能脉冲每计数到434产生一个tick用这个tick作为移位寄存器的移位节拍。计算误差实际波特率50_000_000 / 434 115207.37误差0.0064%完全没问题。但如果时钟是33.333MHz115200对应289.35取整289后误差约0.12%偏大但一般还能用再高波特率比如921600误差会放大就要考虑小数分频或改用PLL产生专门的时钟。代码里建议把波特率参数化parameter CLK_FREQ 50_000_000; parameter BAUD_RATE 115_200; localparam BAUD_DIV CLK_FREQ / BAUD_RATE; reg [15:0] div_cnt; wire baud_tick (div_cnt 1); always (posedge clk or negedge rst_n) begin if (!rst_n) div_cnt BAUD_DIV; else if (baud_tick) div_cnt BAUD_DIV; else div_cnt div_cnt - 1; end这里baud_tick每BAUD_DIV个时钟周期拉高一次作为UART发送和接收的移位时钟。注意BAUD_DIV是整数误差不可避免如果工程包里有uart_clk_div.v类似文件重点看它的计数初值和半周期处理。接收端如果采用16倍过采样再在每一bit中间采样抗干扰会好很多但组合逻辑会更复杂。工程包如果没做16倍过采样在总线短、波特率低的情况下也能跑只是长线传输时误码率会高。3. 用Verilog HDL实现RS485收发可复用的模块设计3.1 UART TX/RX核这是RS485的基础RS485的Verilog实现本质上就是标准的UART核加一个方向控制。先写好发送和接收模块再在外面包一层RS485顶层。发送模块的核心是一个状态机空闲、起始位、数据位、停止位。数据位可以是8位无校验或奇偶校验。常用参数是8数据位、无校验、1停止位8N1。在XC6SLX16上跑资源占用很小几十个LUT就够。下面是一个简化的发送模块module uart_tx #( parameter DATA_WIDTH 8, parameter PARITY 0 )( input clk, rst_n, input [DATA_WIDTH-1:0] tx_data, input tx_start, output reg txd, output reg tx_busy, input baud_tick ); localparam IDLE0, START1, DATA2, PARITY_BIT3, STOP4; reg [2:0] state; reg [2:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; txd 1b1; tx_busy 1b0; end else if (baud_tick) begin case (state) IDLE: if (tx_start) begin state START; txd 1b0; tx_busy 1b1; end START: begin state DATA; bit_cnt 0; shift_reg tx_data; end DATA: begin txd shift_reg[0]; shift_reg {1b0, shift_reg[DATA_WIDTH-1:1]}; if (bit_cnt DATA_WIDTH-1) state STOP; else bit_cnt bit_cnt 1; end STOP: begin txd 1b1; state IDLE; tx_busy 1b0; end default: state IDLE; endcase end end endmodule这个模块在baud_tick上升沿驱动每个tick发送一个bit。注意shift_reg的移位方向先发送最低位。tx_busy信号可以用来阻止新的发送请求也可以用来控制RS485的方向引脚。如果工程包的发送模块是用计数器枚举bit位来产生输出也是一样的效果但状态机方式更容易扩展奇偶校验和停止位长度。3.2 方向控制逻辑自动切换DE/RE的两种做法RS485的方向控制信号de最常见的写法是assign de tx_busy;。发送期间拉高空闲拉低。这样最简单但存在上面说的尾巴问题。另一种做法是用一个独立的延时计数器发送完成后继续保持DE高电平若干微秒让总线稳定再拉低开始接收。我一般会在顶层模块里做方向控制而不是在UART核内部处理这样方便调试。参考工程里常见的模式module rs485_top #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115_200, parameter TX_DELAY_US 10 )( input clk, rst_n, input [7:0] data_in, input send_en, output rs485_ro, // FPGA接收来自收发器RO output rs485_di, // FPGA发送接收发器DI output rs485_de // 方向控制1发0收 ); localparam BAUD_DIV CLK_FREQ / BAUD_RATE; localparam DELAY_CNT TX_DELAY_US * (CLK_FREQ / 1_000_000); reg [15:0] baud_div_cnt; wire baud_tick (baud_div_cnt BAUD_DIV); always (posedge clk or negedge rst_n) begin if (!rst_n) baud_div_cnt BAUD_DIV; else if (baud_tick) baud_div_cnt BAUD_DIV; else baud_div_cnt baud_div_cnt - 1; end wire tx_busy; reg de; reg [15:0] delay_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) de 1b0; else if (send_en !tx_busy) begin de 1b1; delay_cnt DELAY_CNT; end else if (tx_busy) begin de 1b1; delay_cnt DELAY_CNT; end else if (delay_cnt 0) begin de 1b1; delay_cnt delay_cnt - 1; end else de 1b0; end uart_tx u_tx (.clk(clk), .rst_n(rst_n), .tx_data(data_in), .tx_start(send_en), .txd(rs485_di), .tx_busy(tx_busy), .baud_tick(baud_tick)); endmodule这段代码的关键是delay_cnt当发送请求到来时拉高DE发送过程中保持DE为高发送结束后继续延时TX_DELAY_US微秒这里算的是时钟周期数注意DELAY_CNT TX_DELAY_US * (CLK_FREQ / 1_000_000)如果TX_DELAY_US1050MHz下就是500个周期延时结束后才拉低DE进入接收状态。延时时间太短总线余波未平太长影响响应速度尤其是主站轮询时每轮都多出几十微秒。这个参数需要根据实际收发器关断时间和总线长度来调。3.3 接收滤波与抗干扰处理485总线上的毛刺RS485用在工业现场总线容易受到电机、变频器干扰波形上会出现毛刺。UART接收端如果直接在波特率tick采样可能在数据位中间采到毛刺。工程包里如果只有浅层设计接收乱码时基本只能靠换更粗的屏蔽线。实际上Verilog里可以做简单的数字滤波用高频时钟连续采样RXD连续若干个采样值相同才认为电平稳定。比如用50MHz系统时钟每个时钟周期采样一次RXD做三级移位寄存器只有连续三个周期都是高电平才输出高连续三个都是低才输出低。这样能滤掉几十纳秒的毛刺。代价是信号边沿被延后约一个采样周期的三倍对115200波特率bit宽度约8.7微秒来说完全可忽略。如果是1Mbps毛刺滤波就要谨慎设计尽量用更短的滤波器。接收数据恢复通常用16倍过采样。即每个bit周期内产生16个采样脉冲在bit中心附近采样。标准做法是检测到起始位下降沿后等8个采样周期即半个bit然后每16个采样周期采一次数据。这样比直接在波特率tick上采样抗抖动能力强很多。工程包里如果有rx_sample_cnt这类计数变量多半就是16倍过采样如果没有建议自己在接收端加上成本不高但稳定性提升明显。4. 在XC6SLX16上跑通引脚约束、仿真和上板验证4.1 引脚约束把FPGA的IO映射到RS485收发器XC6SLX16有超过一百个用户IO但不是所有IO都适合接RS485收发器。Spartan-6的IO bank支持LVCMOS、LVDS等电平标准RS485收发器芯片一般是3.3V供电FPGA侧使用LVCMOS33电平。约束文件里要把RS485的TXD、RXD、DE分别映射到具体引脚并设置IOBUF或直接SLEW等属性。常见的UCF约束写法Spartan-6用UCF或XDCISE项目常用UCF如下NET clk LOC P123 | IOSTANDARD LVCMOS33; NET rst_n LOC P124 | IOSTANDARD LVCMOS33; NET rs485_di LOC P125 | IOSTANDARD LVCMOS33; NET rs485_ro LOC P126 | IOSTANDARD LVCMOS33; NET rs485_de LOC P127 | IOSTANDARD LVCMOS33;三个信号分别接到收发器芯片的DI、RO、DE引脚。注意rs485_ro是收发器输出到FPGA的输入rs485_di是FPGA输出到收发器输入方向不要搞反。如果板子上有LED可以再接一个led_tx、led_rx方便观察。引脚约束之后还要检查一个容易忽略的点收发器芯片的RO输出通常是开漏或推挽FPGA引脚要配置为输入不需要上拉但DE信号如果悬空有些收发器内部有下拉会处于接收状态。工程包里如果给出的是ISE工程直接用约束文件综合如果是Vivado需要转成XDC。XC6SLX16是Spartan-6老工程更多是ISE但用Vivado也能通过导入网表方式处理不过建议直接用ISE 14.7或兼容版本打开少走弯路。4.2 用Testbench验证收发时序没有硬件之前的第一个验证步骤是在ModelSim/QuestaSim或Vivado Simulator里跑仿真。RS485本身是模拟差分信号但仿真时我们可以用数字信号模拟收发器行为把DE信号作为三态控制RO端在DE为高时进入三态DE为低时反馈DI模拟环回。简单的testbench可以验证发送模块输出的波形是否符合UART时序。下面是一个基本的testbench模拟发送一个字节0x5501010101module rs485_top_tb; reg clk 0; reg rst_n 0; reg [7:0] data_in 8h55; reg send_en 0; wire rs485_di, rs485_de; wire rs485_ro; always #10 clk ~clk; // 50MHz initial begin repeat (10) (posedge clk); rst_n 1; repeat (10) (posedge clk); send_en 1; repeat (1) (posedge clk); send_en 0; // 等待发送完成 repeat (1000) (posedge clk); $finish; end rs485_top dut ( .clk(clk), .rst_n(rst_n), .data_in(data_in), .send_en(send_en), .rs485_di(rs485_di), .rs485_de(rs485_de), .rs485_ro(rs485_ro) ); // 模拟收发器DE1时RO三态DE0时RODI环回自收发 assign rs485_ro rs485_de ? 1bz : rs485_di; initial begin $dumpfile(rs485_tb.vcd); $dumpvars(0, rs485_top_tb); end endmodule仿真时重点看几个波形baud_tick是否按分频周期出现rs485_di的起始位、数据位、停止位是否完整rs485_de是否在发送结束后延时一段时间才拉低。如果发送完立即拉低DE仿真里会看到rs485_ro马上跟随rs485_di的最后一拍这对应后面会讲的尾巴问题。如果rs485_de延时过长则仿真时间会很长但只要功能对就可以。4.3 上板点灯最简单的自环测试仿真通过后可以上板做自环测试。把RS485收发器的A和B之间不接外部设备直接让FPGA发送数据然后在接收侧把RO环回。不过更简单的自环是在FPGA内部把发送数据直接环回给接收模块验证UART核本身没问题再通过外部短路A/B验证差分收发。具体步骤是先把rs485_de强制为高只发送用示波器看A/B之间的差分波形。把A和B短接或者接一个120欧终端电阻然后设置DE自动切换让FPGA发送一个字节后立即转为接收把收到的数据再通过UART发送到电脑调试工具上。如果收到的是自己发出的0x55说明收发链路基本通。如果收不到用逻辑分析仪比如板载的ILA核抓rs485_di、rs485_de和rs485_ro信号看哪一段丢了。XC6SLX16内部没有硬核UART都是逻辑实现所以综合后资源占用少。在ISE里综合后可以看到FF和LUT使用情况如果工程包带了RS485_TOP.ucf检查一下引脚是否和你的板子匹配。不同开发板上RS485芯片的DE极性可能不同——有的板子用PNP三极管反相DE高电平反而使能接收所以一定要看原理图确认极性。5. RS485通信中常见的三个坑现象、原因、解决5.1 发送正常接收乱码收发的极性接反了现象FPGA发送到电脑调试助手的字符是对的但电脑发送给FPGAFPGA收到后打印出来全是乱码比如发0x55收到0xAA高位低位完全反转。原因RS485的A和B接反了。差分信号靠A-B压差区分0和1A/B接反所有电平逻辑正好翻转。UART空闲时为高电平翻转后变成低电平接收端可能一直检测不到起始位或者起始位之后采到的数据全是反码。解决把板子上RS485接线端子A/B对调或者在FPGA接收逻辑里加一个极性反转的开关。如果你不想动硬件可以在Verilog接收端加一句assign rx_polar_inv ~rx_pin;但这不是好习惯因为整个总线上所有节点的极性都会受影响最好还是改接线。工程包里如果包含“A接AB接B”的注释那就是默认接法实际以硬件原理图为准。5.2 总线空闲时出现错误帧没有接终端电阻或偏置现象电脑调试助手在没有数据通信时时不时收到0x00、0xFF这种错误帧频率不高但很烦人。示波器看A/B差分电压空闲时波形在0附近跳动没有稳定的逻辑电平。原因RS485总线空闲时A/B之间需要保持一个确定的差分电压通常A高于B为1。如果总线上没有偏置电阻或终端电阻并且所有节点的收发器都进入接收状态高阻总线就处于悬浮状态外界电磁干扰耦合进来就会产生毛刺被接收端误判为起始位。解决在总线两端各接一个120欧终端电阻匹配传输线阻抗在一端通常是主站给A接上拉电阻到5V或3.3VB接下拉电阻到GND让空闲时A-B的电压差大于200mV。具体阻值根据总线节点数和收发器类型选常见用390欧或1k欧。FPGA里也可以对接收数据做空字节过滤连续收到多个0x00或0xFF就判断为总线空闲但不解决根本问题只能算临时措施。5.3 一接上设备就死机隔离地没做好现象FPGA板卡和外部RS485设备连接后系统复位、跑飞、数据全部错误拔掉RS485线就正常。用手摸一下金属接头偶尔会改善。原因RS485是差分传输但两端的收发器芯片都需要参考地。如果两边的地电位不相等共模电压超出收发器的-7V~12V范围芯片会闩锁或过压导致通信失败甚至损坏FPGA引脚。很多RS485收发器的A/B脚内部有保护二极管但共模电压过大时保护也不够。解决在靠近FPGA板卡的RS485收发器一侧加隔离电源和数字隔离器例如用ISO3082或ADM2587E自带隔离。如果工程包里没有隔离方案至少保证两端的地通过双绞线中一根线连接。还有一点XC6SLX16的IO承受电压有限RS485收发器损坏后可能会把高压串到FPGA所以建议在FPGA与收发器之间串联33欧电阻并在FPGA引脚旁加TVS管。这个坑在工业现场很常见实验室里不一定复现等接到现场就疼了。6. 进阶技巧用状态机做多机寻址和自动方向切换以及如何验证RS485最大的价值是能挂多台设备。前面那个简单的收发模块只解决了点对点通信要做主机轮询从机还得在UART字节流之上加一层协议地址、命令、校验。通常的套路是在FPGA里实现一个小型状态机解析从RS485收到的帧匹配本机地址后才产生响应。这样做的好处是一条总线上挂多个FPGA节点主机下发地址帧只有地址匹配的从机才拉高DE并回数据其他从机继续处在接收状态避免总线冲突。地址匹配状态机一般这样工作空闲时接收字节收到帧头比如0xAA 0x55后进入地址接收状态接下来一个字节是地址如果等于本机地址可以在顶层模块里用参数配置就继续接收长度、数据、校验不匹配就直接回空闲。这也是工程包里经常出现“frame”相关代码的原因。注意帧之间的间隔RS485是半双工从机要等主机发送完并释放总线后再开始回复所以在状态机里加一个总线空闲检测计数器连续多少个bit没有检测到起始位就认为总线空闲。如果主机发送一个广播帧所有从机都会进入接收但只有地址匹配的从机会拉高DE其他从机不动作避免抢总线。验证这个方法有没有用我一般会做三个层面的测试。第一是仿真把两个rs485_top实例背靠背相连一个作主机一个作从机同时用scoreboard比对发出的数据和回收的数据。第二是上板用单板自环验证状态机的地址字节能被正确匹配。第三是两板互联用一根双绞线A-A、B-B对接加120欧终端电阻实际跑1000帧每帧64字节的连续传输统计误码率。前面遇到的极性接反、终端电阻、地电位问题在这一步基本都会暴露出来。我自己做这类工程时习惯把rs485_de延时做成可调参数放在顶层模块的define文件里调试时用chipscope或ILA实时改不用反复重新综合。XC6SLX16的资源足够塞一个MicroBlaze软核但RS485这种场景用纯Verilog状态机更省资源也更容易保证时序。如果你手头有示波器或逻辑分析仪建议在调方向切换参数时抓一次DE和DI的波形亲眼看到发送停止位结束后DE再拉低的动作那种放心感比什么都强。希望这里写的东西能帮你少走几个弯路把自己的RS485工程一次跑通。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑