3、传感器数据协议解析:Modbus RTU协议详解、CRC16校验算法实现、温湿度传感器(SHT30)数据读取

好,咱们进入第三章。这一章是硬骨头,也是整个系统的核心——你得让传感器开口说话。

我在做第一个农业项目时,最头疼的就是传感器数据读出来全是乱码。后来才发现,不是传感器坏了,是协议没搞对。说白了,传感器和单片机之间,得有一套双方都认可的「暗号」。这套暗号,在工业界就叫 Modbus RTU。

3.1 Modbus RTU 协议,到底是个啥?

Modbus 协议诞生于 1979 年,比我都大。但它至今还在用,为什么?因为它简单、可靠。

Modbus RTU 是 Modbus 的一种串行通信版本。它用两根线(A、B)传输数据,数据格式是二进制,效率很高。

一个完整的 Modbus RTU 数据帧长这样:

| 地址码 (1字节) | 功能码 (1字节) | 数据区 (N字节) | CRC16校验 (2字节) |

举个例子,你要读取地址为 0x01 的温湿度传感器:

  • 地址码:0x01 —— 告诉总线上的设备,我在叫谁
  • 功能码:0x03 —— 读取保持寄存器
  • 数据区:起始地址 + 读取数量
  • CRC16:校验数据是否传错

嗯,这里要注意:地址码不能重复。我在一个项目里,两个传感器都设成了 0x01,结果数据全混了。排查了整整一天。

核心要点:Modbus RTU 是主从协议。主机(你的单片机)发起请求,从机(传感器)响应。从机不会主动说话。

3.2 CRC16 校验算法,手写一个

CRC16 是 Modbus RTU 的「防伪标签」。数据在传输过程中,可能会受干扰,CRC 就是用来检查数据有没有被篡改的。

你想想看,如果传感器传回的温度是 25.3℃,但因为线路干扰变成了 85.6℃,你直接拿去用,大棚里的作物可就遭殃了。

CRC16 的计算原理不复杂,但手写起来有点绕。我直接给你一个 Python 实现,这是我项目里一直在用的:

def crc16_modbus(data: bytes) -> int:
    """
    计算 Modbus RTU 的 CRC16 校验值
    :param data: 待校验的字节数据
    :return: 16位 CRC 值
    """
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    # 注意:Modbus 的 CRC 是低字节在前,高字节在后
    return crc

这段代码里,0xA001 是多项式。你不需要背,直接复制用就行。

个人经验:我曾经在 CRC 的字节序上栽过跟头。Modbus 要求 CRC 低字节在前,高字节在后。如果你发现传感器不响应,先检查 CRC 的字节顺序对不对。

3.3 SHT30 温湿度传感器,怎么读?

SHT30 是 Sensirion 公司的产品,精度高、功耗低,农业气象站里很常见。它支持 I2C 和 Modbus 两种接口。咱们这里讲 Modbus 版本。

SHT30 的 Modbus 寄存器地址如下:

寄存器地址 内容 数据类型
0x0000 温度值 (原始) 16位无符号整数
0x0001 湿度值 (原始) 16位无符号整数

读取 SHT30 的完整流程:

  1. 主机发送请求帧:01 03 00 00 00 02 C4 0B
  2. 从机响应:01 03 04 [温度高字节] [温度低字节] [湿度高字节] [湿度低字节] [CRC低] [CRC高]
  3. 解析原始值,转换成实际温湿度

转换公式:

温度(℃) = -45 + 175 * (原始温度值 / 65535)
湿度(%RH) = 100 * (原始湿度值 / 65535)

举个例子,如果原始温度值是 0x1C4A(即 7242),那么:

温度 = -45 + 175 * (7242 / 65535) ≈ -45 + 19.34 ≈ -25.66℃

嗯,这个温度值明显不对。为什么会这样?

我遇到过这种情况。排查后发现,是传感器暴露在户外,冬天确实零下。但如果你在室内测试,温度应该在 20-30℃ 之间。如果读出来是负值,先检查传感器是不是真的在低温环境,还是接线有问题。

避坑指南:我曾经在读取 SHT30 时,发现湿度值一直显示 99.9%。后来发现是传感器被水汽包围了——因为我把传感器放在了加湿器旁边。所以,传感器安装位置很重要,别放在风口或水源附近。

3.4 完整代码示例:读取 SHT30 并解析

下面是一个完整的 Python 示例,通过串口读取 SHT30 的数据:

import serial
import struct

def crc16_modbus(data: bytes) -> int:
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc

def read_sht30(port: str, slave_addr: int = 0x01):
    """
    读取 SHT30 温湿度传感器
    :param port: 串口号,如 'COM3' 或 '/dev/ttyUSB0'
    :param slave_addr: 从机地址,默认 0x01
    """
    # 构建请求帧
    request = bytes([slave_addr, 0x03, 0x00, 0x00, 0x00, 0x02])
    crc = crc16_modbus(request)
    request += bytes([crc & 0xFF, (crc >> 8) & 0xFF])

    # 发送请求
    ser = serial.Serial(port, 9600, timeout=1)
    ser.write(request)

    # 读取响应(7个字节)
    response = ser.read(7)
    ser.close()

    if len(response) != 7:
        print("响应长度错误,可能通信失败")
        return None, None

    # 解析数据
    temp_raw = (response[3] << 8) | response[4]
    hum_raw = (response[5] << 8) | response[6]

    # 转换
    temperature = -45.0 + 175.0 * temp_raw / 65535.0
    humidity = 100.0 * hum_raw / 65535.0

    return temperature, humidity

# 使用示例
temp, hum = read_sht30('COM3')
print(f"温度: {temp:.2f}℃, 湿度: {hum:.2f}%RH")

这段代码我用了很多次,基本是「拿来就用」。你只需要改一下串口号就行。

小技巧:如果读出来的数据偶尔跳变,可以在代码里加一个「多次读取取平均」的逻辑。比如连续读 5 次,去掉最大值和最小值,再取平均。这样能有效滤除干扰。

3.5 本章小结

这一章我们干了三件事:

  • 搞懂了 Modbus RTU 的数据帧结构
  • 手写了 CRC16 校验算法
  • 用 Python 读取了 SHT30 温湿度传感器

说白了,传感器通信就是「发请求、收响应、做校验、算数值」这四步。你把这个流程走通了,换任何 Modbus 传感器都一样。

下一章,咱们要把这些数据存到数据库里,并搭建一个 Web 界面,让你在手机上就能看到大棚里的温湿度。敬请期待。