嵌入式Linux应用开发:基于文件I/O与I2C的温湿度采集系统
嵌入式Linux应用开发:基于文件I/O与I2C的温湿度采集系统
前言
在嵌入式Linux开发中,用户空间应用程序直接操作硬件设备是最常见的开发场景之一。Linux的"一切皆文件"设计哲学,使得我们可以通过标准的文件I/O接口访问I2C、SPI、UART等总线设备。相比于编写内核驱动,用户空间驱动开发周期短、调试方便、安全性高——即使程序崩溃也不会导致整个系统宕机。本文以树莓派4B搭载SHT30温湿度传感器为例,完整演示如何在嵌入式Linux用户空间编写应用程序,通过I2C设备文件读取传感器数据,并实现数据日志记录和阈值告警功能。整套方案无需编写内核驱动,使用gcc交叉编译工具链即可完成,非常适合嵌入式Linux初学者入门。
硬件准备
| 元件 | 数量 | 参考价格 |
|---|---|---|
| 树莓派4B | 1块 | 约350元(可用友善NanoPi替代,约180元) |
| SHT30温湿度传感器模块 | 1个 | 约12元 |
| 杜邦线(母对母) | 4根 | 约2元 |
| MicroSD卡(16G+) | 1张 | 约30元 |
接线表:
| 树莓派4B | SHT30模块 |
|---|---|
| 3.3V(Pin 1) | VCC |
| GND(Pin 6) | GND |
| SDA(Pin 3, GPIO2) | SDA |
| SCL(Pin 5, GPIO3) | SCL |
SHT30的I2C地址为0x44(ADR引脚接GND时),默认I2C总线为/dev/i2c-1。
核心代码
/*
* sht30_reader.c - 嵌入式Linux I2C温湿度采集应用
* 交叉编译: arm-linux-gnueabihf-gcc -o sht30_reader sht30_reader.c
* 板端运行: ./sht30_reader [-i interval] [-l logfile]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <time.h>
#include <errno.h>
#define I2C_DEV "/dev/i2c-1" /* 树莓派I2C总线1 */
#define SHT30_ADDR 0x44 /* SHT30 I2C设备地址 */
#define MEAS_CMD_H 0x2C /* 单次测量命令高字节 */
#define MEAS_CMD_L 0x06 /* 单次测量命令低字节(高重复性) */
#define BUFFER_SIZE 6 /* SHT30返回6字节数据 */
/* 从原始数据计算温度 (公式来自SHT30数据手册) */
float calc_temperature(uint16_t raw_temp) {
return -45.0f + 175.0f * raw_temp / 65535.0f;
}
/* 从原始数据计算湿度 */
float calc_humidity(uint16_t raw_hum) {
return 100.0f * raw_hum / 65535.0f;
}
/* CRC校验 (SHT30使用多项式0x31) */
uint8_t crc8(const uint8_t *data, uint16_t len) {
uint8_t crc = 0xFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1);
}
}
return crc;
}
int main(int argc, char *argv[]) {
int fd, interval = 2;
FILE *log_fp = NULL;
int opt;
/* 解析命令行参数 */
while ((opt = getopt(argc, argv, "i:l:h")) != -1) {
switch (opt) {
case 'i': interval = atoi(optarg); break;
case 'l': log_fp = fopen(optarg, "a"); break;
case 'h':
printf("用法: %s [-i 采集间隔(秒)] [-l 日志文件路径]\n", argv[0]);
return 0;
}
}
/* 1. 打开I2C总线设备文件 */
if ((fd = open(I2C_DEV, O_RDWR)) < 0) {
perror("打开I2C设备失败");
return 1;
}
/* 2. 设置I2C从设备地址 */
if (ioctl(fd, I2C_SLAVE, SHT30_ADDR) < 0) {
perror("设置I2C从机地址失败");
close(fd);
return 1;
}
printf("✅ I2C设备初始化成功 (地址: 0x%02X)\n", SHT30_ADDR);
printf("📊 开始采集 (每%d秒一次, 按Ctrl+C停止)\n\n", interval);
/* 3. 主采集循环 */
while (1) {
uint8_t cmd[2] = {MEAS_CMD_H, MEAS_CMD_L};
uint8_t buf[BUFFER_SIZE];
struct timespec ts;
/* 发送测量命令 */
if (write(fd, cmd, 2) != 2) {
fprintf(stderr, "❌ 发送命令失败: %s\n", strerror(errno));
break;
}
/* 等待测量完成 (SHT30单次测量典型值15ms) */
usleep(50000); /* 50ms 足够安全 */
/* 读取6字节数据: [Temp_H, Temp_L, CRC, Hum_H, Hum_L, CRC] */
if (read(fd, buf, BUFFER_SIZE) != BUFFER_SIZE) {
fprintf(stderr, "❌ 读取数据失败: %s\n", strerror(errno));
break;
}
/* CRC校验 */
uint16_t raw_temp = (buf[0] << 8) | buf[1];
uint16_t raw_hum = (buf[3] << 8) | buf[4];
if (crc8(buf, 2) != buf[2]) {
fprintf(stderr, "⚠️ 温度CRC校验失败\n");
continue;
}
if (crc8(buf + 3, 2) != buf[5]) {
fprintf(stderr, "⚠️ 湿度CRC校验失败\n");
continue;
}
/* 计算并打印结果 */
float temp = calc_temperature(raw_temp);
float hum = calc_humidity(raw_hum);
clock_gettime(CLOCK_REALTIME, &ts);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S",
localtime(&ts.tv_sec));
printf("[%s] 🌡️ 温度: %.2f°C 💧 湿度: %.2f%%\n",
time_str, temp, hum);
/* 写入日志文件 */
if (log_fp) {
fprintf(log_fp, "%s,%.2f,%.2f\n", time_str, temp, hum);
fflush(log_fp);
}
sleep(interval);
}
close(fd);
if (log_fp) fclose(log_fp);
return 0;
}
代码解读
文件I/O操作核心逻辑:
-
设备文件打开(第50-54行):
open("/dev/i2c-1", O_RDWR)是嵌入式Linux应用开发的典型入口。I2C总线在用户空间被抽象为一个字符设备文件,应用程序通过标准POSIX接口与硬件交互,无需编写内核模块。在树莓派上,还需要用raspi-config启用I2C接口(Interfacing Options → I2C → Enable)。 -
I2C从机地址设置(第57-62行):
ioctl(fd, I2C_SLAVE, 0x44)告诉内核后续读写操作的目标设备。可以同时挂载多个I2C设备(如0x44的SHT30和0x38的AHT10),通过切换地址分别访问。如果总线挂载了多个传感器,可用i2cdetect -y 1命令扫描并确认设备地址。 -
信号完整性保障(第26-34行):SHT30的6字节返回数据中包含两个CRC校验字节。代码实现了CRC-8校验,防止总线噪声导致的数据损坏——这在工业现场环境中尤为重要。CRC多项式0x31是SHT30专用的,更换传感器时需要参考对应数据手册修改。
-
命令行参数解析(第41-47行):支持
-i设置采样间隔、-l输出日志文件,体现了嵌入式Linux应用"小而灵活"的设计风格。-h帮助信息让程序具有基本的自文档能力,这在命令行工具开发中是良好的工程实践。 -
日志文件写入(第117-121行):使用标准C库
fprintf搭配fflush实时落盘,确保意外断电时数据不丢失。日志采用CSV格式输出,每行包含时间戳、温度、湿度三个字段,方便后续用Python的pandas库或Excel导入分析。
交叉编译要点: 在PC上使用arm-linux-gnueabihf-gcc编译,编译命令:arm-linux-gnueabihf-gcc -static -o sht30_reader sht30_reader.c(-static参数避免目标板缺少动态库的问题),然后将二进制文件通过scp传输到树莓派:scp sht30_reader pi@192.168.1.100:~。如果使用Buildroot或Yocto构建的系统,还可以直接将此程序加入根文件系统的overlay目录。
实验效果
将编译好的sht30_reader拷贝到树莓派,赋予可执行权限后运行:
chmod +x sht30_reader
./sht30_reader -i 3 -l sensors.log
预期输出:
✅ I2C设备初始化成功 (地址: 0x44)
📊 开始采集 (每3秒一次, 按Ctrl+C停止)
[2026-06-10 14:30:01] 🌡️ 温度: 25.38°C 💧 湿度: 62.15%
[2026-06-10 14:30:04] 🌡️ 温度: 25.41°C 💧 湿度: 62.08%
[2026-06-10 14:30:07] 🌡️ 温度: 25.39°C 💧 湿度: 62.22%
用手靠近传感器,湿度值应在5秒内升高3~5%,温度读数稳定在±0.3°C范围内。日志文件sensors.log会以CSV格式记录所有采集数据,可直接导入Excel或Python做可视化分析。
常见问题
Q:打开/dev/i2c-1时提示Permission denied?
A:当前用户不在i2c用户组中。执行sudo usermod -aG i2c $USER && sudo reboot,或将程序设置为SUID:sudo chown root:root sht30_reader && sudo chmod +s sht30_reader。
Q:I2C通信时read返回-1且errno为121(Remote I/O error)?
A:常见原因有三种:接线松动(SDA/SCL接触不良)、I2C地址不对(检查模块ADR引脚电平,SHT30存在0x44和0x45两个可选地址)、总线电压不匹配(树莓派为3.3V,部分5V模块需电平转换)。
Q:采集到的温度持续偏高(>35°C)?
A:这通常是传感器自发热导致。SHT30在连续测量模式下工作电流约800μA,建议采用单次测量模式(本文已实现)并将采样间隔拉长到5秒以上。如果仍然偏高,可在代码中施加软件低通滤波:temp = 0.7 * temp_new + 0.3 * temp_prev。
Q:想改成其他传感器(如BMP280、AHT20)需要改哪里?
A:只需要修改SHT30_ADDR宏定义、测量命令(参考对应芯片数据手册)以及数据解析函数。I2C的open/ioctl/read/write流程完全通用,这正是嵌入式Linux"用户空间驱动"设计模式的优势。
更多推荐



所有评论(0)