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规范)及数据格式(有符号/无符号、缩放系数)。