issue #30 » 一主一从RS485读取温度.md
# 一主一从 RS485 读取 DS18B20 温度说明
## 项目目标
本项目实现一个主机通过 RS485 总线读取从机 DS18B20 温度,并在主机端通过串口和 OLED 显示温度结果。
当前验证通过的结构是:
```text
主机 STM32 <-> 主机 RS485 模块 <==== A/B 差分总线 ====> 从机 RS485 模块 <-> 从机 STM32 <-> DS18B20
主机负责轮询从机,从机收到查询帧后读取 DS18B20 温度,并通过 RS485 返回温度数据。
系统功能
当前系统包含以下功能:
- 主机定时发送温度查询帧。
- 从机接收主机查询帧。
- 从机读取本地 DS18B20 温度。
- 从机将温度打包成协议帧并通过 RS485 返回。
- 主机解析从机回复帧。
- 主机通过串口打印温度。
- 主机通过 OLED 显示温度。
- 主机代码保留从机地址数组,后续可扩展多个从机地址。
硬件组成
主机端
主机端主要硬件包括:
STM32F407
RS485 模块
OLED 显示屏
调试串口
主机端作用:
发送查询命令
接收从机回复
解析温度数据
显示温度
从机端
从机端主要硬件包括:
STM32F407
RS485 模块
DS18B20 温度传感器
从机端作用:
等待主机查询
读取 DS18B20 温度
回复温度数据
RS485 通信原理
RS485 使用差分信号传输数据,主要依靠 A/B 两根线之间的电压差判断逻辑状态。
RS485 总线不是看 A 对地或 B 对地的单独电压,而是看:
A - B
或者:
B - A
只要 A/B 之间存在有效差分电压,就说明总线上有设备在驱动信号。
本项目使用 TTL 转 RS485 模块。STM32 的 UART 信号先进入 RS485 芯片,再由 RS485 芯片转换成 A/B 差分信号。
RS485 模块连接
主机 STM32 与主机 RS485 模块连接
主机使用 USART1 与 RS485 模块通信。
| STM32 主机引脚 | 功能 | RS485 模块 TTL 侧 |
|---|---|---|
PA9 |
USART1_TX |
RS485_IN_TX |
PA10 |
USART1_RX |
RS485_IN_RX |
3.3V |
电源 | 3V3 |
GND |
地 | GND |
注意:
RS485_IN_TX 接 MCU 的 TX
RS485_IN_RX 接 MCU 的 RX
这里不要按普通串口模块的“TX 接 RX、RX 接 TX”思路交叉接。
本模块的 RS485_IN_TX 表示“输入到 RS485 模块的 TX 信号”,所以应接 STM32 的 TX,也就是 PA9。
从机 STM32 与从机 RS485 模块连接
从机同样使用 USART1 与 RS485 模块通信。
| STM32 从机引脚 | 功能 | RS485 模块 TTL 侧 |
|---|---|---|
PA9 |
USART1_TX |
RS485_IN_TX |
PA10 |
USART1_RX |
RS485_IN_RX |
3.3V |
电源 | 3V3 |
GND |
地 | GND |
RS485 A/B 总线连接
| 主机 RS485 模块 | 从机 RS485 模块 |
|---|---|
A |
A |
B |
B |
GND |
GND |
建议主机和从机之间共地:
主机 GND 连接 从机 GND
虽然 RS485 是差分通信,但实际调试和短距离通信中,共地可以减少共模干扰和异常电平问题。
DS18B20 连接
当前从机 DS18B20 使用 PE6 作为数据引脚。
| DS18B20 引脚 | 连接 |
|---|---|
VCC |
3.3V |
GND |
GND |
DQ |
PE6 |
DS18B20 的 DQ 数据线需要上拉到 3.3V。
推荐:
DQ 通过 4.7kΩ 电阻上拉到 3.3V
如果代码中配置了内部上拉,也建议保留外部 4.7kΩ 上拉,DS18B20 通信会更稳定。
STM32 外设配置
主机 USART1
主机使用 USART1 与 RS485 模块通信。
配置如下:
| 参数 | 配置 |
|---|---|
| 串口 | USART1 |
| TX | PA9 |
| RX | PA10 |
| 波特率 | 115200 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | None |
| 模式 | TX_RX |
主机调试串口
主机使用另一个串口输出调试信息,例如:
Slave 1: 25.62 C
Slave 1 timeout, rx=0
当前代码中 _write() 重定向到了 huart2,所以主机调试打印使用 USART2。
主机 OLED
主机 OLED 用于显示温度结果。
显示内容示例:
RS485 Temp
S1: 25.6 C
如果通信失败,则显示:
S1: ----
从机 USART1
从机使用 USART1 与 RS485 模块通信。
配置如下:
| 参数 | 配置 |
|---|---|
| 串口 | USART1 |
| TX | PA9 |
| RX | PA10 |
| 波特率 | 115200 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | None |
| 模式 | TX_RX |
从机 TIM3
DS18B20 使用微秒级时序,因此从机工程中使用 TIM3 提供 delay_us()。
当前 TIM3 配置目标是:
1 个计数 = 1 us
典型配置:
系统时钟 168MHz
TIM3 在 APB1
Prescaler = 83
Counter Period = 65535
这样 delay_us(480) 才能产生接近 480us 的延时。
通信协议
主机和从机之间使用自定义帧格式。
帧格式
SOF + addr + func + data_len + data + crc16 + EOF
| 字段 | 长度 | 说明 |
|---|---|---|
SOF |
1 字节 | 帧头,固定 0xAA
|
addr |
1 字节 | 从机地址 |
func |
1 字节 | 功能码 |
data_len |
1 字节 | 数据长度 |
data |
N 字节 | 数据内容 |
crc16 |
2 字节 | Modbus CRC16,低字节在前 |
EOF |
1 字节 | 帧尾,固定 0x55
|
功能码
| 功能码 | 名称 | 说明 |
|---|---|---|
0x02 |
FUNC_QUERY_TEMP |
主机查询温度 |
0x01 |
FUNC_REPORT_TEMP |
从机上报温度 |
查询帧
主机查询温度时,数据长度为 0。
格式:
AA addr 02 00 crcL crcH 55
示例:
AA 01 02 00 21 E0 55
含义:
| 字段 | 值 |
|---|---|
| 帧头 | AA |
| 从机地址 | 01 |
| 功能码 | 02 |
| 数据长度 | 00 |
| CRC | 21 E0 |
| 帧尾 | 55 |
回复帧
从机回复温度时,数据长度为 2。
温度用 int16_t 表示,单位为 0.01℃。
例如:
25.34℃
会转换为:
2534
再拆成两个字节:
高字节 = 2534 >> 8
低字节 = 2534 & 0xFF
回复格式:
AA addr 01 02 tempH tempL crcL crcH 55
主机工作流程
主机启动后执行以下流程:
初始化 RS485 接收模式
初始化环形缓冲区
开启 USART1 接收中断
初始化 OLED
进入循环
循环中:
选择当前从机地址
发送查询帧
切换到接收状态
等待从机回复
解析回复帧
更新串口输出
更新 OLED 显示
切换到下一个从机
当前主机代码保留了从机地址数组:
static const uint8_t slave_addrs[] = {1, 2, 3};
如果以后要增加从机,只需要改这个数组:
static const uint8_t slave_addrs[] = {1, 2, 3, 4};
SLAVE_COUNT 会自动根据数组长度计算。
从机工作流程
从机启动后执行以下流程:
初始化 RS485 接收模式
初始化 DS18B20
开启 USART1 接收中断
进入循环
从机平时处于接收状态。
当从机收到主机查询帧后:
解析帧头
检查地址是否等于 DEVICE_ADDR
检查功能码是否为 FUNC_QUERY_TEMP
检查 CRC 是否正确
置位 pending_query
主循环检测到 pending_query 后:
读取 DS18B20 温度
打包回复帧
通过 RS485 发送给主机
回到接收状态
从机地址配置
从机地址通过编译宏 DEVICE_ADDR 指定。
例如第 1 个从机:
-DDEVICE_ADDR=1
第 2 个从机:
-DDEVICE_ADDR=2
第 3 个从机:
-DDEVICE_ADDR=3
从机代码中有默认地址:
#ifndef DEVICE_ADDR
#pragma message("DEVICE_ADDR not defined, defaulting to 1")
#define DEVICE_ADDR 1
#endif
如果编译时没有指定 DEVICE_ADDR,默认地址就是 1。
DS18B20 读取方式
当前一主一从测温中,每个从机只接一个 DS18B20,因此可以使用 Skip ROM 方式读取温度。
核心命令如下:
ds18b20_write_byte(0xCC); // Skip ROM
ds18b20_write_byte(0x44); // Convert T
读取温度寄存器时:
ds18b20_write_byte(0xCC); // Skip ROM
ds18b20_write_byte(0xBE); // Read Scratchpad
这种方式适合:
一块从机板上只有一个 DS18B20
如果一块从机板上挂多个 DS18B20,才需要使用 Search ROM 搜索每个传感器的 ROM ID,再通过 Match ROM 读取指定传感器。
当前实际结构如果是:
多个从机,每个从机一个 DS18B20
不需要 DS18B20 搜索 ROM。
主机通过从机地址区分不同从机即可。
RS485 方向控制
本项目中 RS485 模块使用 THVD1406DR 这类自动方向控制芯片时,软件中保留了接口:
void RS485_SetTxMode(void);
void RS485_SetRxMode(void);
但函数内部可以为空:
void RS485_SetRxMode(void)
{
/* THVD1406 自动方向控制,不需要 MCU 控制方向 */
}
void RS485_SetTxMode(void)
{
/* THVD1406 自动方向控制,不需要 MCU 控制方向 */
}
保留这两个函数的原因是:
方便兼容以后更换普通 MAX485 模块
如果以后换成普通 MAX485 模块,需要用 GPIO 控制 DE/RE,例如:
发送前拉高 DE
发送完成后拉低 DE
调试过程中的关键问题
PA9 和 PA10 接反
调试过程中曾出现:
MCU PA9 = 3.3V
模块 D = 0.6V
主机 rx=0
这说明 MCU 的 TX 没有真正接到 RS485 模块的 D / RS485_IN_TX。
最终通过交换 PA9 和 PA10 对应接线后,RS485 通信恢复正常。
正确连接是:
PA9 -> RS485_IN_TX
PA10 -> RS485_IN_RX
A/B 有差分信号但主机收不到
如果 A/B 有差分电压,只能说明 RS485 总线上有人在驱动信号。
但主机能否收到数据,还取决于:
RS485 模块 R 输出是否接到 MCU RX
MCU RX 引脚是否配置正确
串口接收中断是否开启
TX/RX 是否接反
所以 A/B 有信号不等于 MCU 一定收到数据。
屏幕显示 0℃
如果屏幕显示 0℃,通信不一定有问题。
常见原因:
DS18B20 没初始化
DS18B20 读取函数返回 0
DS18B20_SensorNum 为 0
DQ 没有上拉
DQ 引脚配置不对
DS18B20 线序接错
使用了不匹配的 Search ROM 版本
本次最终确认:
原始 DS18B20 单探头版本可以正常读数,搜索 ROM 版本不适合当前一块从机一个探头的结构。
常见现象和判断
| 现象 | 可能原因 | 判断方向 |
|---|---|---|
主机显示 timeout, rx=0
|
主机完全没有收到字节 | 查 PA9/PA10、RS485_IN_TX/RX、A/B、GND |
| A/B 有差分但 rx=0 | 总线有信号,但 MCU RX 没收到 | 查模块 R 到 PA10 |
屏幕显示 0℃
|
通信正常但温度为 0 | 查 DS18B20 |
| 固定 25℃ 能显示 | RS485 和显示正常 | 问题在 DS18B20 读取 |
| 捏 DS18B20 温度变化 | DS18B20 读取正常 | 系统完成 |
只显示 S1: ----
|
主机没有成功解析回复 | 查协议、CRC、从机地址 |
显示 Slave 1 timeout, rx=6
|
收到不完整帧 | 查帧尾、CRC、解析长度 |
| 显示乱码或偶发错误 | 串口参数或接线干扰 | 查波特率、GND、线长、终端电阻 |
当前推荐连接汇总
主机
| 主机 STM32 | 连接 |
|---|---|
PA9 |
主机 RS485 模块 RS485_IN_TX
|
PA10 |
主机 RS485 模块 RS485_IN_RX
|
3.3V |
主机 RS485 模块 3V3
|
GND |
主机 RS485 模块 GND
|
从机
| 从机 STM32 | 连接 |
|---|---|
PA9 |
从机 RS485 模块 RS485_IN_TX
|
PA10 |
从机 RS485 模块 RS485_IN_RX
|
3.3V |
从机 RS485 模块 3V3
|
GND |
从机 RS485 模块 GND
|
PE6 |
DS18B20 DQ
|
总线
| 主机 RS485 | 从机 RS485 |
|---|---|
A |
A |
B |
B |
GND |
GND |
DS18B20
| DS18B20 | 从机 STM32 |
|---|---|
VCC |
3.3V |
GND |
GND |
DQ |
PE6 |
当前代码思路
主机代码思路
主机核心逻辑:
1. 构造查询帧
2. 通过 USART1 发送到 RS485 模块
3. 接收从机回复
4. 根据 data_len 计算完整帧长度
5. 校验 SOF、EOF、CRC
6. 提取温度
7. 串口打印并刷新 OLED
温度打印使用整数方式格式化,避免 printf 浮点支持问题:
static void print_temp_2dec(uint8_t addr, float temp)
{
int32_t raw = (int32_t)(temp * 100.0f + (temp >= 0.0f ? 0.5f : -0.5f));
int32_t abs_raw = raw >= 0 ? raw : -raw;
if (raw < 0) {
printf("Slave %d: -%ld.%02ld C\r\n", addr, abs_raw / 100, abs_raw % 100);
} else {
printf("Slave %d: %ld.%02ld C\r\n", addr, abs_raw / 100, abs_raw % 100);
}
}
从机代码思路
从机核心逻辑:
1. USART1 中断接收主机查询帧
2. 根据帧长度接收完整协议帧
3. 校验地址和功能码
4. 收到查询后设置 pending_query
5. 主循环读取 DS18B20 温度
6. 构造回复帧
7. 通过 USART1 返回给主机
从机不建议在中断里直接读取 DS18B20,因为 DS18B20 读取需要延时。
正确做法是:
中断里只解析帧并置位 pending_query
主循环里读取温度并发送回复
后续扩展
扩展多个从机
如果后续从 1 个从机扩展为 3 个从机,主机只需要配置:
static const uint8_t slave_addrs[] = {1, 2, 3};
每个从机编译时分别设置:
-DDEVICE_ADDR=1
-DDEVICE_ADDR=2
-DDEVICE_ADDR=3
接线方式:
主机 A 接所有从机 A
主机 B 接所有从机 B
所有设备 GND 共地
扩展一个从机多个 DS18B20
如果一个从机板上挂多个 DS18B20,就不能再用 Skip ROM。
需要改成:
Search ROM 搜索所有 DS18B20
保存每个 DS18B20 的 64 位 ROM ID
读取时用 Match ROM 指定某个传感器
这种模式下,ds18b20_get_temp(index) 的 index 才有实际意义。
当前一主一从、每个从机一个 DS18B20 的结构不需要搜索 ROM。
最终结论
本系统的核心思路是:主机通过 RS485 查询,从机收到查询后读取本地 DS18B20,并把温度按自定义协议返回。调试中最关键的问题不是协议,而是 RS485 模块 TTL 侧 TX/RX 接线方向。修正 PA9/PA10 与模块 RS485_IN_TX/RS485_IN_RX 的连接后,通信恢复正常。
当前结构下,每个从机只接一个 DS18B20,因此使用 Skip ROM 方式读取最简单、最稳定。主机通过从机地址区分不同温度采集节点。