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校验错误:多为线路干扰或波特率不匹配,可降低波特率或检查终端电阻
- 数值异常:确认传感器寄存器地址与数据格式是否与本案例假设一致,不同厂商传感器可能使用不同寄存器地址或缩放系数