F429-HAL-RS485(回环/双机实验)(2026/8/16)

目录

[一:RS485 理论知识](#一:RS485 理论知识)

1:为什么需要RS485

[2. 差分信号------RS485 的核心](#2. 差分信号——RS485 的核心)

[3. 半双工 + 方向控制(昨天学的 PB8)](#3. 半双工 + 方向控制(昨天学的 PB8))

4:为什么必须"发完立刻切回接收"

[5. RS485 网络拓扑](#5. RS485 网络拓扑)

[6. RS485 vs CAN 对比(工控两兄弟)](#6. RS485 vs CAN 对比(工控两兄弟))

[7. Modbus RTU(RS485 最常见的上层协议,后面学)](#7. Modbus RTU(RS485 最常见的上层协议,后面学))

8:和can不同在哪

二:硬件连接:

1:GPIO控制读和写

2:tx和rx跳帽选择

三:RS485回环实验

1:BSP_485_Init初始化函数

2:HAL_UART_RxCpltCallback中断回调

四:实验总结

1、硬件层(本质)

2、初始化流程

[3、发送流程(按 KEY1 触发)](#3、发送流程(按 KEY1 触发))

4、接收流程(中断自动,藏在后台)

5、一句话总结


一:RS485 理论知识

一句话:RS485 = 串口 + 差分电平 + 半双工方向控制

1:为什么需要RS485

普通串口(TTL 电平)的痛点:

复制代码
TTL 串口:0V=0, 3.3V=1
  缺点:
  ① 距离短——超过 1 米就丢数据(电压衰减 + 干扰)
  ② 只能点对点——一个 TX 只能接一个 RX
  ③ 抗干扰差——工厂里电机一启动,5V 的 1 可能被干扰成 0
​
RS485 差分:用两根线(A/B)的电压差表示数据
  A-B 压差 > +0.2V  = 逻辑 1
  A-B 压差 < -0.2V  = 逻辑 0
  优点:
  ① 距离长——1200 米(低速时)
  ② 多节点——一根总线上挂 32~128 个设备
  ③ 抗干扰强——干扰同时叠加在 A 和 B 上,压差不变

2. 差分信号------RS485 的核心

复制代码
单端信号(TTL):一根线对地传
  TX ──────── 信号线
  GND ────── 参考地
  干扰直接叠加在信号线上,电平就变了 ❌
​
差分信号(485):两根线传反相的信号
  A ────────┐
            ├─ 接收端看 A-B 的差
  B ────────┘
  干扰同时打在 A 和 B 上(共模干扰),A-B 的差不变 ✅

这就是"差分抗干扰"的本质------干扰是共模的,两边一起涨一起跌,差值不受影响

3. 半双工 + 方向控制(昨天学的 PB8)

复制代码
RS485 是半双工:同一时刻只能一个方向传
​
485 芯片有两个控制脚:
  DE(Driver Enable)—— 高 = 使能发送
  RE(Receiver Enable)—— 低 = 使能接收(注意是低有效)
​
很多板子把 DE 和 RE 接一起,用一根 GPIO 控制(你的 PB8):
  PB8 = 高 → 发送模式(MCU 往总线发数据)
  PB8 = 低 → 接收模式(MCU 从总线听数据)
​
对讲机逻辑:按着按钮说话,松开听别人说

4:为什么必须"发完立刻切回接收"

这是 485 最容易踩的坑:

复制代码
BSP_485_TX_EN();          // PB8 高,开始发
HAL_UART_Transmit(...);   // 发数据
BSP_485_RX_EN();          // PB8 低,切回收 ← 必须做!
​
如果不切回收:
  你的 485 芯片一直处于发送模式
  → 别人发来的数据你听不到
  → 而且你的发送器一直占着总线,干扰别人

5. RS485 网络拓扑

复制代码
主机 (你的 STM32)
  │
  ├─ 120Ω 电阻(总线起点)
  │
  │  A ────────────────┐
  │  B ────────────────┤
  │                    │
  │               ┌────┼────┐  ┌────┼────┐
  │               │ 从机1   │  │ 从机2   │
  │               │ A     B │  │ A     B │
  │               └────┴────┘  └────┴────┘
  │                    │
  │               ┌────┼────┐
  │               │ 从机3   │
  │               │ A     B │
  │               └────┴────┘
  │                    │
  └─ 120Ω 电阻(总线终点)
  • 两根线:A 和 B(有时还有 GND 参考地)

  • 终端电阻:总线两端各 120Ω,防止信号反射。你的底板已经焊好了

  • 菊花链:设备串行接,不能星形分叉

  • 最多节点:32 个标准收发器,实际看芯片

6. RS485 vs CAN 对比(工控两兄弟)

RS485 CAN
底层 USART(串口) 专用 CAN 控制器
线数 2 根(A/B) 2 根(CAN_H/L)
节点寻址 无(靠协议) ID 广播
多主竞争 无(软件仲裁) 硬件自动仲裁
方向控制 需要(PB8) 不需要
错误检测 CRC+ACK+重发
上层协议 Modbus RTU CANopen/J1939
成本 低(几块钱芯片)

一句话总结:RS485 便宜但"笨"------没有地址、没有仲裁、没有纠错,全靠软件(Modbus)补;CAN 贵但"聪明"------硬件把这些全做了。

7. Modbus RTU(RS485 最常见的上层协议,后面学)

复制代码
Modbus RTU 帧格式:
┌────────┬──────────┬──────────┬─────┬─────┬─────┐
│ 地址   │ 功能码    │ 数据      │ CRC │ 结束 │
│ 1字节  │ 1字节     │ N字节     │ 2字节│ 3.5T │
└────────┴──────────┴──────────┴─────┴─────┘
​
主机:发"地址1,读保持寄存器"→ 从机1 应答"地址1,数据是xxx"
从机靠地址字节区分"是不是叫我",跟 CAN 的 ID 广播是两种思路

8:和can不同在哪

CAN 也是差分,但电平定义和叫法不同:

复制代码
CAN 用两根线:CAN_H 和 CAN_L(对应 485 的 A 和 B)
​
隐性电平(逻辑 1,总线空闲):
  CAN_H ≈ 2.5V
  CAN_L ≈ 2.5V
  压差 ≈ 0V  → 表示 1
​
显性电平(逻辑 0,有数据):
  CAN_H ≈ 3.5V
  CAN_L ≈ 1.5V
  压差 ≈ 2V  → 表示 0

485 和 CAN 的关键区别

RS485 CAN
逻辑 1 A-B > +0.2V(正压差) 压差 ≈ 0(隐性)
逻辑 0 A-B < -0.2V(负压差) 压差 ≈ 2V(显性)
空闲状态 由软件决定 固定隐性(1)
谁规定 0/1 看 A-B 正负 看压差大小

CAN 为什么这样设计------还记得之前学的仲裁吗?

复制代码
显性(0)压隐性(1):
​
  节点A发 0 → CAN_H=3.5V, CAN_L=1.5V(强驱动,压差2V)
  节点B发 1 → CAN_H=2.5V, CAN_L=2.5V(弱驱动,压差0V)
​
  两个节点同时在总线上:
    一个想把压差拉到 2V(显性0)
    一个想保持 0V(隐性1)
    → 显性赢!总线变成 0
    → 发 1 的节点发现自己输了,自动退避
​
  这就是 CAN 仲裁的物理基础——显性压隐性

二:硬件连接:

1:GPIO控制读和写

复制代码
PB8控制读和写
// PB8 = 低 → 接收模式(435 芯片"听"总线,你的 MCU 准备收数据)
#define _485_RX_EN()   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
​
// PB8 = 高 → 发送模式(435 芯片"喊"向总线,你的 MCU 往外发数据)
#define _485_TX_EN()   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
    
//跟对讲机一样——按着按钮(PB8=高)才能喊话,松开按钮(PB8=低)只能听。
   UART 天生全双工(发收各一根线),而 485 是半双工(发和收共用一根差分线)
   必须靠 PB8 告诉 485 芯片"现在我是要说话还是要听话"
   CAN 不需要这一脚——CAN 控制器硬件自动处理收发仲裁。

2:tx和rx跳帽选择

芯片视角命名的------TXD 是被写入,RXD 是从芯片读出:

复制代码
STM32 ←──────────────→ 485 芯片 ←────────→ A/B 总线
PD5(TX) →   TXD(被 MCU 写,MCU 想发的数据给它)
PD6(RX) ←   RXD(从 MCU 读,芯片总总线上听来的数据送出来)
 PB8    →   DE/RE(高=发、低=收)

实物跳帽是这么连的:

pin1和pin2短接,给RS485芯片供电,pin3和pin4是485TX和485RX,需要接到我们单片机的TX和RX,PD5为f429对应的usart_tx,PD6为f429对应的usart_rx

PD5连485TX,PD6连485RX

复制代码
普通串口直连(两个 MCU 直接对插,必须交叉):
  MCU_A TX ──────→ RX MCU_B
  MCU_A RX ←────── TX MCU_B
  因为"我的发送"要接到"你的接收",所以 TX 接 RX
​
MCU 接 485 芯片(直连,不交叉):
  MCU PD5(TX) ──→ 485_TX(485 芯片的"数据输入脚")                                            //485_TX 其实是"芯片接收数据的输入脚"(DI)
  MCU PD6(RX) ←── 485_RX(485 芯片的"数据输出脚")                                            //485_RX 其实是"芯片接收数据的输入脚"(RO)
  芯片的命名是"从芯片视角":485_TX = 芯片要发出去的数据(所以是 MCU 给它),485_RX = 芯片收进来的数据(所以给 MCU)

因为 CAN 外设内部自带回环路径,RS485 用的 USART 没有:

复制代码
CAN 回环(硬件内置):
  ┌──── bxCAN 外设 ────┐
  │  发送引擎 ──┐       │
  │            ↓ (内部绕回) │
  │  接收引擎 ←─┘       │
  └────────────────────┘
  TX/RX 引脚悬空,不经过外部
​
USART 回环(没有内部绕回电路):
  ┌──── USART2 ────┐
  │  TX → PD5 引脚  │  ← 信号从这里出去了
  │  RX ← PD6 引脚  │  ← 必须从外面接回来
  └────────────────┘
        │       ↑
        └───线───┘   ← 只能外部短接

CAN 外设在芯片设计时就在数字逻辑里加了一条内部绕回路径,CAN_MODE_LOOPBACK 一开就行。USART 没设计这个功能------TX 发出去必须从外部把 PD5 和 PD6 连起来才能回环。RS485 的 485 芯片回环(接 485_D─485_R)同理------芯片内部也没绕回路,只能靠外部跳帽短接。

三:RS485回环实验

实验简介:

剥开那层 485 的皮,本质就是 USART2 收发------你昨天回环实验用杜邦线短接 PD5-PD6,压根没经过 485 芯片,纯粹是 STM32 的串口自己发自己收。

复制代码
RS485 完整链路:
  USART2 发送 → 485芯片转差分 → 双绞线 → 对面485芯片转回来 → USART2 接收
                ↑ 这层"皮"才是 485 跟串口的区别
​
你昨天做的:
  USART2 发送 ──杜邦线直连── USART2 接收
  (完全没碰 485 芯片,就是纯串口回环)

所以 RS485 学习成本极低------串口你会了,485 就只多一个 PB8 方向脚 + 一颗差分芯片。真正要花心思的不是 485 本身,是跑在它上面的 Modbus RTU 协议(地址、功能码、CRC 校验)。这也是为什么路线图里把 RS485 和 Modbus 放一起。

等你 USB 转 485 模块到了,把跳帽 Pin1-Pin2 接上给芯片供电,螺丝端子 A 接 A、B 接 B,就能验证真正的差分通信了。

1:BSP_485_Init初始化函数

复制代码
g.Pull = GPIO_PULLUP;      ← 配置"上拉电阻",不是"输出高电平"
BSP_485_RX_EN();           ← 拉低 = 设置输出电平为低

关键:Pull 和"输出电平"是两码事。

配置 控制什么 对推挽输出的影响
g.Mode = GPIO_MODE_OUTPUT_PP 引脚当推挽输出用 决定引脚能输出 0/1
g.Pull = GPIO_PULLUP 内部上拉电阻 推挽输出模式下其实没用
HAL_GPIO_WritePin() 输出电平(ODR 寄存器) 决定引脚实际输出 0 还是 1

HAL_GPIO_Init 只负责"把这个引脚配成推挽输出",它不设置输出电平 。所以 HAL_GPIO_Init 之后引脚电平是不确定的(保持复位默认值),你必须显式调一次 HAL_GPIO_WritePin 来明确拉低。

至于 g.Pull = GPIO_PULLUP 这行------对推挽输出模式来说是多余的,它只在输入模式(或开漏模式)才真正起作用。野火代码习惯性写上,不影响功能。

一句话:初始化只定"模式",拉低要单独写 ODR 寄存器 。所以 BSP_485_RX_EN() 这一步不能省------它明确告诉芯片"开机默认接收模式"。

2:HAL_UART_RxCpltCallback中断回调

复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        if (usart1_rx_len < sizeof(usart1_rx_buf)) /* 防止溢出 */
        {
            usart1_rx_buf[usart1_rx_len++] = rx_byte; /* 存字节,计数 +1 */
        }
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 继续收下一个 */
    }
    else if (huart->Instance == USART2)                                 /* 485 接收 */
    {
        if (bsp_485_rx_len < sizeof(bsp_485_rx_buf))
            bsp_485_rx_buf[bsp_485_rx_len++] = bsp_485_rx_byte;        /* 存字节 */
        if (bsp_485_rx_len == 256) bsp_485_rx_flag = 1;
        HAL_UART_Receive_IT(&bsp_485_huart, (uint8_t *)&bsp_485_rx_byte, 1); /* 继续收 */
    }
}
复制代码
bsp_485_rx_flag = 1 在第 158 行——当 bsp_485_rx_len == 256(收满 256 字节)时置 1。
​
为什么它在 Usart.c 而不是 bsp_485.c?因为 HAL_UART_RxCpltCallback 是整个工程只能有一个的全局回调——USART1 和 USART2 共用它。之前两个文件各自写了一份,链接报 multiply defined。所以我把它统一放在 Usart.c,用 huart->Instance 区分是哪个串口:
​
​
HAL_UART_RxCpltCallback(huart)
  ├─ huart == USART1 → 存到 usart1_rx_buf(调试串口)
  └─ huart == USART2 → 存到 bsp_485_rx_buf,收满256字节置 bsp_485_rx_flag=1(485)
这样设计有个小缺点:485 的接收逻辑散落在 Usart.c 里,不够内聚。如果你觉得乱,更好的做法是回调里只调一个 bsp_485_rx_byte 相关的处理函数,但功能上现在这样跑通了。

四:实验总结

1、硬件层(本质)

复制代码
STM32 USART2
  │
  ├─ PD5(TX) ──杜邦线短接──→ PD6(RX)    ← 数据通路(绕过 485 芯片)
  │
  └─ PB8(DE/RE) → 485 芯片方向脚        ← 本实验没接芯片,空转

2、初始化流程

复制代码
main()
  ├─ HAL_Init()              HAL 库初始化
  ├─ SysTick_Init()          180MHz PLL + 10us 时基
  ├─ LED_Init()              RGB LED
  ├─ Usart1_Init(115200)     调试串口(打印用)
  ├─ Key_Init()              KEY1 GPIO
  └─ BSP_485_Init()
       ├─ 配 PD5(TX)/PD6(RX) 为 AF7 推挽
       ├─ 配 PB8 为推挽输出,默认拉低(接收模式)
       ├─ 配 USART2 115200 8-N-1
       └─ 启动首次 HAL_UART_Receive_IT(收 1 字节到 bsp_485_rx_byte)

3、发送流程(按 KEY1 触发)

复制代码
main() while(1)
  │
  ├─ 检测 KEY1 按下(消抖)
  ├─ 清空接收缓冲:rx_len=0, rx_flag=0
  │
  ├─ 循环发 256 字节(0x00~0xFF):
  │     for (i = 0; i < 256; i++)
  │       BSP_485_SendByte(i)
  │         ├─ PB8 拉高(发送模式)  ← 本实验空转
  │         ├─ HAL_UART_Transmit 发 1 字节
  │         └─ PB8 拉低(接收模式)  ← 本实验空转
  │
  ├─ HAL_Delay(500) 等接收完成
  │
  └─ 检查 bsp_485_rx_flag:
        = 1 → 打印收到的 256 字节
        = 0 → 提示"检查 PD5-PD6 是否短接"

4、接收流程(中断自动,藏在后台)

复制代码
发出去的字节从 PD5 → PD6 流回来
  │
  ├─ USART2 RXNE 中断触发
  │     └─ USART2_IRQHandler → HAL_UART_IRQHandler
  │           └─ HAL_UART_RxCpltCallback(在 Usart.c)
  │                 ├─ huart == USART2 分支
  │                 ├─ bsp_485_rx_buf[rx_len++] = bsp_485_rx_byte  ← 存字节
  │                 ├─ 收满 256 字节 → bsp_485_rx_flag = 1
  │                 └─ 重新 HAL_UART_Receive_IT 收下一字节
  │
  └─ main 里检测到 flag=1 → 打印缓冲区

5、一句话总结

复制代码
写:HAL_UART_Transmit 把字节塞进 USART2 发送寄存器 → 从 PD5 出去
读:PD6 收到字节 → RXNE 中断 → 回调存进 bsp_485_rx_buf
回环:杜邦线让 PD5 的数据直接流回 PD6
PB8:485 芯片的方向控制,本实验芯片未参与,空转

本质就是 USART2 的收发 + 中断缓冲,套了层 485 的皮(PB8 方向控制 + 差分芯片),回环阶段只验证了 USART2 软件部分

相关推荐
木白CPP1 小时前
Linux 驱动UART子系统源码分析
linux·运维·服务器·驱动开发·嵌入式硬件·开源软件
zlinear数据采集卡2 小时前
数据采集卡从入门到精通(10):采样率与分辨率的核心关系——反比律、架构分布与过采样
arm开发·嵌入式硬件·算法·fpga开发·架构·开源
白狐_7983 小时前
408 数据结构|线索二叉树两题详解:先序线索化后的空链域 + 中序前驱/后继判断
c语言·数据结构·链表
GeekZHR4 小时前
C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“
java·c语言·算法·指针
水果里面有苹果4 小时前
33-TPS62130RGTR-3A降压器
嵌入式硬件
振南的单片机世界5 小时前
推挽vs开漏:一个“强拉硬灌”,一个“只拉不管”
arm开发·stm32·单片机·嵌入式硬件
caimouse5 小时前
ReactOS 图形系统分析(36):位块传输 — bitblt.c
c语言·reactos
小心亦新6 小时前
STM32学习23--I2C-1
stm32·嵌入式硬件·学习
Tyler_TXZ6 小时前
C++C语言之——树
c语言·c++·算法·