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

任务目标

本案例实现一个运行于Linux主机(或嵌入式Linux网关)上的C语言程序,通过RS485串口以Modbus RTU协议读取温湿度传感器(寄存器地址0x0000和0x0001)的原始值,并转换为实际的温度(℃)和相对湿度(%RH)工程值,打印到标准输出。

环境与依赖

  • Linux环境,具备标准串口设备(如/dev/ttyUSB0或/dev/ttyS0)
  • RS485转USB适配器(或板载RS485接口),传感器A/B线正确连接
  • 传感器从站地址:0x01,波特率:9600,8数据位、无校验、1停止位(8N1)
  • 编译依赖:仅标准C库(termios、sys/ioctl等),无需第三方库

实现原理

程序通过Linux串口驱动发送Modbus RTU功能码0x03(读保持寄存器)请求帧,从地址0x0000开始连续读取2个寄存器。接收响应后校验从站地址、功能码和CRC16,将两个16位寄存器值分别转换为温度与湿度工程值。本案例假设传感器数据格式为:温度寄存器值为实际温度×10(有符号整数),湿度寄存器值为实际湿度×10(无符号整数)。

关键完整代码

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

#define SERIAL_PORT "/dev/ttyUSB0"
#define SLAVE_ADDR  0x01
#define FUNC_READ   0x03
#define REG_START   0x0000
#define REG_COUNT   0x0002
#define BUF_SIZE    256

// CRC16 (Modbus)
unsigned short crc16(unsigned char *data, int len) {
    unsigned short crc = 0xFFFF;
    for (int i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;
}

// 配置串口为8N1, 9600bps
int config_serial(int fd) {
    struct termios tty;
    memset(&tty, 0, sizeof(tty));
    if (tcgetattr(fd, &tty) != 0) return -1;
    cfsetospeed(&tty, B9600);
    cfsetispeed(&tty, B9600);
    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
    tty.c_cflag |= (CLOCAL | CREAD);
    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CRTSCTS;
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    tty.c_oflag &= ~OPOST;
    tty.c_cc[VMIN] = 8;   // 等待至少8字节
    tty.c_cc[VTIME] = 10; // 1秒超时
    tcflush(fd, TCIOFLUSH);
    return tcsetattr(fd, TCSANOW, &tty);
}

// 发送Modbus RTU请求并接收响应
int modbus_read(int fd, unsigned char *resp, int *resp_len) {
    unsigned char req[8];
    req[0] = SLAVE_ADDR;
    req[1] = FUNC_READ;
    req[2] = (REG_START >> 8) & 0xFF;
    req[3] = REG_START & 0xFF;
    req[4] = (REG_COUNT >> 8) & 0xFF;
    req[5] = REG_COUNT & 0xFF;
    unsigned short crc = crc16(req, 6);
    req[6] = crc & 0xFF;
    req[7] = (crc >> 8) & 0xFF;

    if (write(fd, req, 8) != 8) return -1;
    usleep(50000); // 等待传感器响应

    int n = read(fd, resp, BUF_SIZE);
    if (n < 5) return -1;
    *resp_len = n;

    // 校验CRC
    unsigned short recv_crc = resp[n-2] | (resp[n-1] << 8);
    unsigned short calc_crc = crc16(resp, n-2);
    if (recv_crc != calc_crc) return -1;
    // 校验地址和功能码
    if (resp[0] != SLAVE_ADDR || resp[1] != FUNC_READ) return -1;
    // 校验字节数
    if (resp[2] != REG_COUNT * 2) return -1;
    return 0;
}

int main() {
    int fd = open(SERIAL_PORT, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        perror("open serial");
        return 1;
    }
    if (config_serial(fd) != 0) {
        fprintf(stderr, "serial config failed\n");
        close(fd);
        return 1;
    }

    unsigned char resp[BUF_SIZE];
    int resp_len = 0;
    if (modbus_read(fd, resp, &resp_len) != 0) {
        fprintf(stderr, "Modbus read failed\n");
        close(fd);
        return 1;
    }

    // 解析寄存器值(大端)
    short temp_raw = (resp[3] << 8) | resp[4];
    unsigned short hum_raw = (resp[5] << 8) | resp[6];

    float temperature = temp_raw / 10.0f;
    float humidity = hum_raw / 10.0f;

    printf("Temperature: %.1f C\n", temperature);
    printf("Humidity: %.1f %%RH\n", humidity);

    close(fd);
    return 0;
}

编译与运行

gcc -o modbus_temp_hum modbus_temp_hum.c
sudo chmod 666 /dev/ttyUSB0   # 或使用udev规则授权
./modbus_temp_hum

验证方法

程序执行后,若串口连接正确且传感器在线,标准输出将显示两行数据。判定通过条件:

  • 输出格式为Temperature: xx.x C和Humidity: xx.x %RH,数值在传感器量程内(如温度-40~80℃,湿度0~100%RH)
  • 数值与传感器当前环境读数一致(可用其他已知良好工具或传感器自带显示屏比对)
  • 若传感器未连接或地址错误,程序输出Modbus read failed并返回退出码1

预期输出示例(假设环境温度25.3℃,湿度60.5%RH):

Temperature: 25.3 C
Humidity: 60.5 %RH

注意:上述示例仅为逻辑推导的预期值,实际数值取决于传感器真实读数。

常见问题

  • 打开串口失败:确认设备节点存在且当前用户有读写权限
  • 读取超时无响应:检查RS485 A/B线是否接反、传感器地址是否匹配、波特率是否一致
  • CRC校验错误:多为线路干扰或波特率不匹配,可降低波特率或检查终端电阻
  • 数值异常:确认传感器寄存器地址与数据格式是否与本案例假设一致,不同厂商传感器可能使用不同寄存器地址或缩放系数