第28章:RS-485串口通信实验

本章对应原书第 660~685 页,属于「第五篇 实战篇」。

本章在讲什么?(先看这个)

在"UART 串口通信实验"里我们学会了用串口传数据,但普通串口(RS-232 电平)有个致命弱点:抗干扰差、传不远。这一章要把串口"升级"成工业现场常用的 RS-485 接口——它靠两根线的电压"差值"来表示数据,抗干扰能力极强,一根总线还能挂 128 个设备。

本章的实验任务是:用两块开发板通过 RS-485 互联,各自板上的两个按键控制对方开发板上两个 LED 的亮灭。你会学到差分传输原理、SP3485 电平转换芯片的用法,以及如何把程序固化到 Flash 里做双板联调。

核心概念拆解

28.1 RS-485简介:先搞懂"差分传输"

RS-485 是一种基于 UART 串口的接口标准,主要规定发送器和接收器的电气特性。注意一个容易混淆的点:RS-232、RS-422、RS-485 都是"接口标准"(管电平和接线),而 UART 是"串口协议"(管数据格式),两者是不同层面的概念,实际使用中常常组合在一起。

先看两种传输方式的差别:

  • 单端传输:用一根信号线对地线的电压表示逻辑 0 和 1(RS-232 就是这种)。
  • 差分传输:用两根信号线传一路信号,两根线上幅值相等、极性相反,用它们的差值表示逻辑。

图 28.1.1 差分传输方式

差分传输为什么抗干扰?打个比方:你和朋友在嘈杂的会场里,一人贴着另一人耳朵小声说话(单端),很容易被环境噪声污染;但如果你们俩用"你抬高音量、我压低音量"的相对变化来传递信息(差分),环境噪声同时叠加到两人身上,相减之后就抵消了。

图 28.1.2 差分传输抑制共模干扰

当信号线上叠加了频率、幅值、相位都相同的干扰(共模干扰)时,单端传输的信号会被污染;而差分传输两根线上的干扰在求差时互相抵消,大大提高了抗干扰能力——代价是多用一根信号线。

三种电气协议对比(记结论就行):

对比项 RS-232 RS-422 RS-485
工作方式 单端,3线全双工 差分,4线全双工 差分,2线半双工
节点数 1发1收 1发10收 多发多收,最大128个收发器
最大传输距离 约15米 约1219米 约1219米
最大传输速率 20Kbps 10Mbps 10Mbps
逻辑1 -3V~-15V 差值电压+(2~6)V 差值电压+(2~6)V
逻辑0 +3V~+15V 差值电压-(2~6)V 差值电压-(2~6)V

要点:RS-485 只有 2 根信号线(A 和 B),发送和接收共用,所以只能半双工通信(不能同时收发),需要靠使能信号切换收发方向。

28.2 硬件设计:SP3485 电平转换

图 28.2.1 RS485 接口

开发板板载了 RS485 接口,但它的通信引脚并没有直连 ZYNQ,而是需要跳帽选择。看下面的选择接口原理图:

图 28.2.2 RS232/RS485 选择接口

P1 口的 UART2_TX 和 UART2_RX 直连 ZYNQ 引脚。本次实验要把 3和5、4和6 两组引脚用跳帽连接,选择到 RS485:

图 28.2.3 P1 口跳帽连接图

ZYNQ PL 侧串口引脚输出的是 TTL 电平(3.3V 表示 1,0V 表示 0),而 RS-485 用差分电压表示逻辑,所以中间必须有一个电平转换芯片——SP3485

图 28.2.4 RS-485 串口原理图

SP3485 关键引脚:

引脚 说明
RO 接收数据输出(到 FPGA 的 RX)
RE 数据接收使能,低电平有效
DE 数据发送使能,高电平有效
DI 发送数据输入(来自 FPGA 的 TX)
A 差分传输线+(正向)
B 差分传输线-(反向)

芯片判断逻辑:A-B > +0.2V 表示逻辑 1,A-B < -0.2V 表示逻辑 0。A、B 引脚上挂的三个电阻(R1、R3、R4,阻值比 3:1:3)形成偏置,保证空闲时 A、B 之间有约 0.47V 的压差(A 线约 1.88V,B 线约 1.41V)。

图 28.2.5 A 和B 引脚的电路图

由于 RS-485 是半双工,收发方向怎么控制?RE 低有效、DE 高有效,两根使能线有效值相反,所以硬件上直接把它们连在一起,用一个信号 RS485_DE 控制:高电平发送,低电平接收。

图 28.2.6 RS485 收发方向自动控制电路

更妙的是这块板用三极管做了方向自动控制:RS485_DE 由 ZYNQ 输出的 UART2_TX 经三极管驱动。UART2_TX 空闲时为高,三极管导通,RS485_DE 被拉低,芯片稳定处于接收状态;发送数据 0 时三极管截止,RS485_DE 拉高进入发送状态,把 0 发出去;发送数据 1 时三极管导通、芯片处于接收态,但接收态下数据线默认为高电平,所以 1 也能"发"出去。于是在 FPGA 代码里,你完全不用管方向切换,把 RS485 当普通串口用即可。

28.3 实验任务

用两块开发板通过 RS485 端口互联,各自的两个按键控制对方开发板上两个 LED 的亮灭:按键按下时对方对应 LED 点亮,释放时熄灭。

28.4 程序设计

总体思路:RS-485 本质上只是接口标准,经电平转换芯片后,RS485 接口可以完全当串口用。所以本章工程就是在"按键 + 串口收发 + LED"的组合上搭起来的:

图 28.4.1 RS-485 串口通信简略架构图

两块开发板功能完全一样:本地按键经消抖后,通过串口发送模块发给对方;同时接收对方发来的按键值,取反后驱动本地 LED。

图 28.4.2 RS-485 串口通信系统架构图

顶层系统框图包含五个模块:

图 28.4.3 顶层系统框图

  • 顶层模块(rs485_key_led):例化和连接各子模块;
  • 按键触发模块(key_trig):消抖并产生发送使能;
  • 串口发送模块(uart_tx)、串口接收模块(uart_rx):直接复用"UART 串口通信实验"中的模块,一行不改;
  • LED 控制模块(led_ctrl):解析接收数据控制 LED。

按键触发模块

难点在于:什么时候启动一次发送?答案是按键状态发生改变时。判断方法是给消抖后的按键值打两拍(key_data_d0、key_data_d1),两拍值不相等说明数据变了,此时输出一个只持续一个时钟周期的高电平信号 key_data_valid 作为串口发送使能。

图 28.4.4 按键触发模块框图

两个按键各需一个消抖模块,所以例化两次"按键消抖模块"(来自按键控制蜂鸣器实验)。模块框图与时序如下:

图 28.4.5 按键触发模块端口波形图

核心代码:

module key_trig(
    input            clk           ,  //外部50M 时钟
    input            rst_n         ,  //外部复位信号,低有效
    input      [1:0] key           ,  //外部按键输入
    output reg       key_data_valid,  //按键数据有效标志
    output     [1:0] key_data         //按键数据
    );

//reg define
reg [1:0] key_data_d0;
reg [1:0] key_data_d1;

//对key_data 信号打两拍用于判断按键是否发生改变
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_data_d0 <= 2'b11;
        key_data_d1 <= 2'b11;
    end
    else begin
        key_data_d0 <= key_data;
        key_data_d1 <= key_data_d0;
    end
end

//当检测到按键数据发生改变时,key_data_valid 拉高
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        key_data_valid <= 1'b0;
    else if (key_data_d1 != key_data_d0)
        key_data_valid  <= 1'b1;
    else
        key_data_valid  <= 1'b0;
end

//按键0 消抖模块
key_debounce u_key_debounce_0(
    .sys_clk        (clk        ),
    .sys_rst_n      (rst_n      ),
    .key            (key[0]     ),
    .key_filter     (key_data[0])
    );

//按键1 消抖模块
key_debounce u_key_debounce_1(
    .sys_clk        (clk        ),
    .sys_rst_n      (rst_n      ),
    .key            (key[1]     ),
    .key_filter     (key_data[1])
    );

endmodule

代码分三部分:打两拍检测变化、产生单周期有效脉冲、例化两个消抖模块。复位时把寄存器初始化为 2'b11(按键空闲态,低电平按下)。

图 28.4.6 按键触发模块仿真波形图

仿真中模拟 key0 按下:按键数据变化时 key_data_valid 拉高且只保持一个时钟周期,与设计的时序图一致。

LED 控制模块

输入是串口接收模块给出的数据(取低 2 位有效数据 led_data)和接收完成信号 uart_done(作为使能 led_en),输出 2 位 LED 控制信号。

图 28.4.7 LED 控制模块框图

这里有个通用经验值得记住:找模块间的"握手"信号时,不一定都能找到高有效、单周期的完美同步信号。如果找到的是低有效信号就取反;持续多周期的信号就做取沿操作;异步信号就打拍同步到本模块时钟域。

图 28.4.8 LED 控制模块时序图

注意一个细节:按键按下是低电平,而 LED 是高电平点亮,所以接收数据要取反后再赋给 LED——这就是时序图中 led_data 为 2'b00 时 led 却输出 2'b11 的原因。

module led_ctrl(
    input              clk     ,    //外部50M 时钟
    input              rst_n   ,    //外部复位信号,低有效
    input              led_en  ,    //LED 控制使能
    input       [1:0]  led_data,    //LED 控制数据
    output reg  [1:0]  led          //LED 灯
    );

//控制LED 灯的变化
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        led <= 2'b00;
    else if(led_en)              //在led_en 使能时,改变LED 灯的状态
            led <= ~led_data;    //按键按下时为低电平,而LED 高电平时点亮
        else
            led <= led;
end

endmodule

图 28.4.9 LED 控制模块仿真波形图

顶层模块

顶层做各模块例化与信号连接。有一个"编码"细节:按键数据只有 2 位,而串口一帧数据是 8 位,所以发送前把 2 位按键数据放在低 2 位、高 6 位补 0(编码);接收时只取低 2 位有效数据(解码)。

module rs485_key_led(
    input           sys_clk       ,    //外部50M 时钟
    input           sys_rst_n     ,    //外部复位信号,低有效
    input  [1:0]    key           ,    //按键
    output [1:0]    led           ,    //LED 灯
    //uart 接口
    input           rs485_uart_rxd,    //rs485 串口接收端口
    output          rs485_uart_txd     //rs485 串口发送端口
    );

//parameter define
parameter  CLK_FREQ = 50000000;    //定义系统时钟频率
parameter  UART_BPS = 115200  ;    //定义串口波特率

//wire define
wire        uart_tx_en    ;    //UART 发送使能
wire        uart_rx_done  ;    //UART 接收完毕信号
wire [7:0]  uart_tx_data  ;    //UART 发送数据
wire [7:0]  uart_rx_data  ;    //UART 接收数据
wire [1:0]  key_data      ;    //按键数据

//将2 位按键数据编码为8 位的串口数据
assign   uart_tx_data = {6'b0 , key_data};

//串口接收模块
uart_rx #(
    .CLK_FREQ       (CLK_FREQ),
    .UART_BPS       (UART_BPS))
u_uart_rx(
    .clk              (sys_clk       ),
    .rst_n            (sys_rst_n     ),
    .uart_rxd         (rs485_uart_rxd),
    .uart_rx_done     (uart_rx_done  ),
    .uart_rx_data     (uart_rx_data  )
    );

//LED 控制模块
led_ctrl u_led_ctrl(
    .clk            (sys_clk           ),
    .rst_n          (sys_rst_n         ),
    .led_en         (uart_rx_done      ),  //LED 控制使能
    .led_data       (uart_rx_data[1:0] ),  //LED 控制数据
    .led            (led               )
);

//按键触发模块
key_trig u_key_trig(
    .clk             (sys_clk    ),
    .rst_n           (sys_rst_n  ),
    .key             (key        ),
    .key_data_valid  (uart_tx_en ),     //按键有效通知信号
    .key_data        (key_data   )      //按键数据
    );

//串口发送模块
uart_tx #(
    .CLK_FREQ       (CLK_FREQ),
    .UART_BPS       (UART_BPS))
u_uart_tx(
    .clk            (sys_clk      ),
    .rst_n          (sys_rst_n    ),
    .uart_tx_en     (uart_tx_en   ),
    .uart_tx_data   (uart_tx_data ),
    .uart_txd       (rs485_uart_txd)
    );

endmodule

仿真验证

TestBench 需要激励四个输入:系统时钟、复位、串口接收线、按键。串口和按键的激励写法分别来自之前的串口实验和按键实验,这里只强调一个新语法:

defparam u_rs485_key_led.u_key_trig.u_key_debounce_0.CNT_MAX=10;
defparam u_rs485_key_led.u_key_trig.u_key_debounce_1.CNT_MAX=10;

defparam(重定义参数)可以在编译时通过层次路径修改底层模块的参数值,格式为 defparam 例化模块名.参数名=参数值;。这里把消抖计数上限改成 10,大幅缩短仿真时间。注意:defparam 不可综合,只能用在仿真代码里,工程代码中千万别用。

`timescale  1ns/1ns

module  tb_rs485_key_led;

//parameter  define
parameter  T = 20;              // 时钟周期为20ns

defparam u_rs485_key_led.u_key_trig.u_key_debounce_0.CNT_MAX=10;
defparam u_rs485_key_led.u_key_trig.u_key_debounce_1.CNT_MAX=10;

//reg define
reg         sys_clk  ;
reg         sys_rst_n;
reg  [1:0]  key      ;
reg         uart_rxd ;

//给输入信号初始值
initial begin
   sys_clk            = 1'b0 ;
   sys_rst_n          = 1'b0 ;
   key                = 2'b11;
   uart_rxd           = 1'b1 ;     // 空闲为高
    #(T+10)
   sys_rst_n  = 1'b1;
    #(1000)
   uart_rxd           = 1'b0 ;     //起始位
   #(8680) uart_rxd = 1'b0;  //bit0 位
   #(8680) uart_rxd = 1'b0;  //bit1 位
   #(8680) uart_rxd = 1'b0;  //bit2 位
   #(8680) uart_rxd = 1'b0;  //bit3 位
   #(8680) uart_rxd = 1'b0;  //bit4 位
   #(8680) uart_rxd = 1'b0;  //bit5 位
   #(8680) uart_rxd = 1'b0;  //bit6 位
   #(8680) uart_rxd = 1'b0;  //bit7 位
   #(8680) uart_rxd = 1'b1;  //停止位
    #(1000)
   key = 2'b10;  // 模拟按键抖动
    #(20)
   key = 2'b11;
    #(60)
   key = 2'b10;
end

//50Mhz 时钟,每10ns 电平取反一次
always #(T/2) sys_clk = ~sys_clk;

//例化顶层模块
rs485_key_led u_rs485_key_led(
    .sys_clk          (sys_clk  ),
    .sys_rst_n        (sys_rst_n),
    .key              (key      ),
    .led              (),
    .rs485_uart_rxd   (uart_rxd ),
    .rs485_uart_txd   ()
);

endmodule

图 28.4.10 RS-485 串口通信仿真波形1

波形1验证了发送方向:按键数据变化 → key_data_valid 拉高 → 串口发送模块被使能发出数据。

图 28.4.11 RS-485 串口通信仿真波形2

波形2验证了接收方向:模拟收到 8'h00(对方两个按键都按下),uart_done 拉高后 LED 控制模块解析数据并驱动 LED。

28.5 生成比特流

引脚约束如下:

信号名 方向 管脚 说明 电平标准
sys_clk input U18 系统时钟 50MHz LVCMOS33
sys_rst_n input N16 复位,低有效 LVCMOS33
key[0] input L14 PL端按键0 LVCMOS33
key[1] input K16 PL端按键1 LVCMOS33
led[0] output H15 PL端LED0 LVCMOS33
led[1] output L15 PL端LED1 LVCMOS33
rs485_uart_rxd input K14 RS-485串口接收线 LVCMOS33
rs485_uart_txd output M15 RS-485串口发送线 LVCMOS33
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN L14 IOSTANDARD LVCMOS33} [get_ports {key[0]}]
set_property -dict {PACKAGE_PIN K16 IOSTANDARD LVCMOS33} [get_ports {key[1]}]
set_property -dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN L15 IOSTANDARD LVCMOS33} [get_ports {led[1]}]
set_property -dict {PACKAGE_PIN K14 IOSTANDARD LVCMOS33} [get_ports rs485_uart_rxd]
set_property -dict {PACKAGE_PIN M15 IOSTANDARD LVCMOS33} [get_ports rs485_uart_txd]

引脚分配完成后编译工程、生成比特流文件。

28.6 程序固化与下载验证

本次实验需要两块开发板通信,同一台电脑接两个下载器容易冲突,只接一个又免不了对 JTAG 热插拔(有损坏器件的风险),所以这次必须先把程序固化到 Flash,再断电脱离下载器运行。

固化脚本的使用

工程里的"zynq 纯PL 端固化脚本"文件夹包含三个文件:

  • bootbin.bif:引导镜像文件,指定用哪些文件生成 BOOT.bin;
  • create_bootbin.bat:批处理文件,调用 Vitis 的 bootgen(生成镜像)和 program_flash(烧写)功能,生成 BOOT.bin 并烧写到开发板 Flash;
  • zynq_fsbl.elf:固化的初始化文件(7010 和 7020 核心板 DDR 型号不同,要用对应版本的 elf)。

使用步骤:① 用文本编辑器打开 create_bootbin.bat,把其中路径替换为你电脑上 Vitis 安装路径下的 bin 文件夹路径;② 把工程生成的比特流文件复制到该文件夹;③ 打开 bootbin.bif,确认其中比特流文件名与实际一致(同文件夹下直接写文件名即可)。

然后:连接电源和下载器,把启动模式开关 BOOT_CFG 拨到 JTAG 模式(1、2 都拨到 ON),双击运行 create_bootbin.bat。看到"Bootimage generated successfully"说明 BOOT.bin 生成成功;不要关窗口,继续等待,出现"Flash Operation Successful"说明固化完成。之后断电、断开下载器,用同样方法固化第二块板。

注意:固化完成后要先断电,再把 BOOT_CFG 拨到 QSPI 启动模式(1 拨到 ON、2 拨到 OFF),下次上电才会运行 Flash 里的程序。

上板验证

  • 两块板的 RS485 接口用两根公对公杜邦线连接,A 接 A、B 接 B,不能接反;注意 CAN 接口与 RS485 接口外观很像,别插错。
  • 两块板的 P1 口都要用跳帽(K14 即 RX 跳帽连接 485_TX,M15 即 TX 跳帽连接 485_RX)选择到 RS485。
  • BOOT_CFG 拨到 QSPI 启动模式,上电。按下任一板上的按键,另一块板对应的 LED 点亮;松开熄灭,即验证成功。

另一种固化方式:把 BOOT.bin 拷贝到格式化为 FAT32 的 SD 卡,插卡后把 BOOT_CFG 拨到 SD CARD 启动模式(1、2 都 OFF),即可从 SD 卡启动。

初学者容易踩的坑

  • 接口标准 ≠ 串口协议:RS-232/422/485 是电气接口标准,UART 是数据协议,RS-485 实验里 FPGA 内部跑的仍然是 UART 协议,只是电平被 SP3485 转换了。
  • 杜邦线不能接反:两块板的 RS485 必须 A 接 A、B 接 B;而且板上 CAN 接口和 RS485 接口外观相似,插错口通信必失败。
  • 跳帽别忘接:RS485 引脚不直连 ZYNQ,P1 口必须用跳帽选择到 RS485(K14→485_TX、M15→485_RX),否则怎么烧程序都没反应。
  • 半双工别同时收发:RS-485 只有两根差分线,收发共用,同一时刻只能一个方向传输;不过本实验硬件已自动切换方向,代码里不用管。
  • defparam 只能用于仿真:它不可综合,只是缩短仿真时间(如把消抖参数改小)的工具,写进工程代码会导致综合失败。
  • 固化后要断电再换启动模式:Flash 烧写完成后必须先断电,再拨到 QSPI 模式,否则不会运行固化程序;且烧写窗口显示成功前不要关闭。

小结 & 下一步

本章核心是理解差分传输的抗干扰原理和 RS-485 的半双工、多点特性,并借助 SP3485 把"RS485 当串口用"——FPGA 内部逻辑与之前的 UART 实验完全一致,只是做了一次 2 位按键数据的编码/解码。同时你掌握了把纯 PL 程序固化到 QSPI Flash / SD 卡的完整流程。

动手建议:完成拓展训练——当解析到 11 时两灯熄灭、按下 key0(10)两灯点亮、按下 key1(01)两灯同闪、同时按下(00)两灯交替闪烁;再用固化脚本对旧例程做一次 QSPI 和 SD 卡启动实验,巩固固化流程。