1. 基于Modbus RTU的温湿度数据采集

任务目标

本案例实现基于RS485总线的Modbus RTU温湿度数据采集。通过主机(网关)向从机(温湿度传感器)发送功能码03(读保持寄存器)请求,接收响应帧并解析出温度与湿度工程值。代码采用纯C语言编写,不依赖特定硬件平台,通过串口读写函数接口与底层驱动解耦,可直接移植至Linux或嵌入式RTOS环境。

环境与依赖

  • 编译器:GCC 4.8+ 或任意支持C99的编译器
  • 串口驱动:需提供串口打开、读取、写入、关闭四个底层函数(本案例以Linux termios为例给出参考实现)
  • 无第三方库依赖

完整实现

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>

/* Modbus RTU 帧最大长度:地址(1) + 功能码(1) + 数据(最多252) + CRC(2) */
#define MODBUS_RTU_MAX_FRAME 256
#define RESPONSE_TIMEOUT_MS  500

/* 设备地址与寄存器定义 */
#define SLAVE_ADDR          0x01
#define REG_TEMP_HUM_START  0x0000   /* 起始寄存器:温度(2字节) + 湿度(2字节) */
#define REG_COUNT           2        /* 读取2个寄存器 */

/* CRC16 (Modbus 多项式 0xA001) */
static uint16_t modbus_crc16(const uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;
}

/* 构建读保持寄存器请求帧 */
static uint16_t build_read_request(uint8_t *frame, uint8_t slave_addr,
                                   uint16_t start_reg, uint16_t reg_count) {
    frame[0] = slave_addr;
    frame[1] = 0x03;                    /* 功能码:读保持寄存器 */
    frame[2] = (start_reg >> 8) & 0xFF;
    frame[3] = start_reg & 0xFF;
    frame[4] = (reg_count >> 8) & 0xFF;
    frame[5] = reg_count & 0xFF;
    uint16_t crc = modbus_crc16(frame, 6);
    frame[6] = crc & 0xFF;
    frame[7] = (crc >> 8) & 0xFF;
    return 8;   /* 请求帧长度 */
}

/* 解析响应帧,提取温度与湿度(单位:0.01℃ / 0.01%RH) */
static int parse_response(const uint8_t *frame, uint16_t len,
                          int16_t *temperature, uint16_t *humidity) {
    if (len < 7) return -1;                     /* 最短响应:地址+功能码+字节数+数据+CRC */
    if (frame[0] != SLAVE_ADDR) return -2;      /* 从机地址不匹配 */
    if (frame[1] != 0x03) return -3;            /* 功能码错误 */

    uint8_t byte_count = frame[2];
    if (byte_count != 4 || len != (uint16_t)(3 + byte_count + 2))
        return -4;                              /* 字节数或帧长度错误 */

    /* 校验CRC */
    uint16_t recv_crc = frame[3 + byte_count] | (frame[3 + byte_count + 1] << 8);
    uint16_t calc_crc = modbus_crc16(frame, 3 + byte_count);
    if (recv_crc != calc_crc) return -5;        /* CRC校验失败 */

    /* 解析:温度寄存器(有符号16位,单位0.01℃),湿度寄存器(无符号16位,单位0.01%RH) */
    int16_t temp_raw = (int16_t)((frame[3] << 8) | frame[4]);
    uint16_t hum_raw = (uint16_t)((frame[5] << 8) | frame[6]);

    *temperature = temp_raw;
    *humidity = hum_raw;
    return 0;
}

/* ========== 串口底层函数(Linux termios 参考实现) ========== */
static int serial_fd = -1;

int serial_open(const char *dev, int baud) {
    serial_fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_fd < 0) return -1;

    struct termios opts;
    tcgetattr(serial_fd, &opts);
    cfsetispeed(&opts, baud);
    cfsetospeed(&opts, baud);
    opts.c_cflag |= (CLOCAL | CREAD);
    opts.c_cflag &= ~PARENB;    /* 无校验 */
    opts.c_cflag &= ~CSTOPB;    /* 1位停止位 */
    opts.c_cflag &= ~CSIZE;
    opts.c_cflag |= CS8;        /* 8位数据位 */
    opts.c_cflag &= ~CRTSCTS;   /* 禁用硬件流控 */
    opts.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    opts.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | ICRNL);
    opts.c_oflag &= ~OPOST;
    tcsetattr(serial_fd, TCSANOW, &opts);
    tcflush(serial_fd, TCIOFLUSH);
    return 0;
}

int serial_write(const uint8_t *data, uint16_t len) {
    return (int)write(serial_fd, data, len);
}

int serial_read(uint8_t *buf, uint16_t max_len, int timeout_ms) {
    fd_set fds;
    struct timeval tv;
    FD_ZERO(&fds);
    FD_SET(serial_fd, &fds);
    tv.tv_sec = timeout_ms / 1000;
    tv.tv_usec = (timeout_ms % 1000) * 1000;

    if (select(serial_fd + 1, &fds, NULL, NULL, &tv) <= 0)
        return 0;   /* 超时无数据 */
    return (int)read(serial_fd, buf, max_len);
}

void serial_close(void) {
    if (serial_fd >= 0) close(serial_fd);
}

/* ========== 主流程 ========== */
int main(int argc, char *argv[]) {
    const char *dev = (argc > 1) ? argv[1] : "/dev/ttyS0";
    int baud = (argc > 2) ? atoi(argv[2]) : B9600;

    if (serial_open(dev, baud) < 0) {
        fprintf(stderr, "无法打开串口 %s\n", dev);
        return 1;
    }

    uint8_t request[8];
    uint16_t req_len = build_read_request(request, SLAVE_ADDR,
                                          REG_TEMP_HUM_START, REG_COUNT);

    /* 发送请求 */
    if (serial_write(request, req_len) != req_len) {
        fprintf(stderr, "串口写入失败\n");
        serial_close();
        return 1;
    }

    /* 接收响应(含超时保护) */
    uint8_t response[MODBUS_RTU_MAX_FRAME];
    uint16_t resp_len = 0;
    uint32_t start_ms = 0;  /* 实际使用时替换为系统毫秒时间戳 */
    while (resp_len < sizeof(response)) {
        int n = serial_read(response + resp_len,
                            sizeof(response) - resp_len,
                            RESPONSE_TIMEOUT_MS);
        if (n > 0) {
            resp_len += n;
            /* 简单判断:收到完整帧(至少7字节且CRC匹配)即停止 */
            if (resp_len >= 7) {
                int16_t temp;
                uint16_t hum;
                if (parse_response(response, resp_len, &temp, &hum) == 0)
                    break;
            }
        } else {
            break;  /* 超时 */
        }
    }

    /* 解析并输出 */
    int16_t temperature;
    uint16_t humidity;
    int ret = parse_response(response, resp_len, &temperature, &humidity);
    if (ret == 0) {
        printf("温度: %.2f ℃\n", temperature / 100.0);
        printf("湿度: %.2f %%RH\n", humidity / 100.0);
    } else {
        fprintf(stderr, "解析失败,错误码: %d\n", ret);
    }

    serial_close();
    return (ret == 0) ? 0 : 1;
}

编译与运行

# 编译
gcc -o modbus_temp_hum modbus_temp_hum.c

# 运行(默认 /dev/ttyS0,波特率9600)
./modbus_temp_hum /dev/ttyS0 9600

# 若使用其他串口设备(如USB转RS485)
./modbus_temp_hum /dev/ttyUSB0 9600

验证方法

  • 硬件连接:将RS485转USB模块连接至网关(或PC),A/B线对应接入传感器RS485接口,确认传感器供电正常。
  • 从机地址确认:传感器出厂默认地址通常为0x01,若不同需修改代码中SLAVE_ADDR宏。
  • 寄存器地址确认:本案例假设温度寄存器偏移0x0000(有符号16位,单位0.01℃),湿度寄存器偏移0x0001(无符号16位,单位0.01%RH)。实际传感器可能不同,需查阅数据手册调整REG_TEMP_HUM_START及解析逻辑。
  • 预期输出:若传感器当前温度为25.50℃、湿度为60.00%RH,程序输出应为:
温度: 25.50 ℃
湿度: 60.00 %RH
  • 判定通过:程序退出码为0,且输出的温湿度数值与传感器本地显示(或已知环境值)偏差在合理范围内(通常温度±0.5℃,湿度±3%RH)。若输出错误码-5,说明CRC校验失败,检查波特率、接线或从机地址。

常见问题

  • 无响应(错误码-1或超时):检查RS485 A/B线是否接反、传感器供电是否正常、波特率是否匹配、串口设备名是否正确。
  • CRC错误(错误码-5):多为波特率不匹配或线路干扰导致数据帧错位,可尝试降低波特率或检查终端电阻(120Ω)。
  • 数值异常:确认寄存器字节序(本代码为大端模式,符合Modbus规范)及数据格式(有符号/无符号、缩放系数)。