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