使用文件 I/O 操作硬件 —— 从 LED 到温湿度传感器
[TOC] 使用文件 I/O 操作硬件 —— 从 LED 到温湿度传感器写给急于控制硬件的你本章教你在 Qt 图形界面中控制 LED通过两种方法sysfs 和专用驱动以及读取温湿度传感器 DHT11。我们不讲复杂的驱动编写只讲如何调用已有的接口让你快速实现硬件交互。每个知识点都有白话解释、生活化类比、完整代码和避坑指南。1. 硬件操作的两条路 —— 用户态与内核态1.1 一句话白话在 Linux 中操作硬件有两条路用户态直接操作通过/sys或/dev下的文件用open/read/write控制硬件如 GPIO sysfs。内核驱动中转驱动程序提供专用的设备节点如/dev/100ask_led应用层同样用文件接口调用。1.2 生活化类比 GPIO sysfs就像去政府柜台办事流程公开但步骤繁琐先 export再设方向再写值。专用驱动就像找了代办中介你只需要说“开灯”中介帮你搞定一切封装好的接口。1.3 两种方法对比表特性GPIO sysfs专用驱动需要硬件知识需要知道引脚编号、方向不需要操作步骤多步export → direction → value一步write /dev/xxx中断支持不支持支持适用场景简单输出/输入复杂外设如传感器、LED 灯带可移植性依赖内核配置依赖驱动是否编译2. GPIO sysfs 操作 LED —— 用户态直接控制2.1 先体验查看系统中的 GPIOLinux 内核将 GPIO 控制器暴露在/sys/class/gpio下。执行以下命令查看bashls /sys/class/gpio/gpiochip* -d输出示例text/sys/class/gpio/gpiochip0 /sys/class/gpio/gpiochip32 /sys/class/gpio/gpiochip64 ...每个gpiochipX代表一个 GPIO 控制器Bank。查看它的详细信息bashcat /sys/class/gpio/gpiochip0/label # 显示硬件名称如 209c000.gpio cat /sys/class/gpio/gpiochip0/ngpio # 显示该控制器有多少引脚查看所有 GPIO 的使用情况需要内核开启 debugfsbashcat /sys/kernel/debug/gpio输出中会列出每个引脚的当前方向和值。白话gpiochip就像一排排的插座每个插座有编号。你要用的 LED 插在哪个插座上就需要知道它的全局编号。2.2 确定 LED 的 GPIO 编号以 IMX6ULL 为例开发板 LED 通常连接在某个 GPIO 引脚上。例如原理图中 LED 使用GPIO5_3。计算公式对于 IMX6ULL 这类 32 引脚 per Bank 的芯片text编号 (Bank号 - 1) × 32 引脚号GPIO5_3Bank5引脚3 → 编号 (5-1)×32 3 4×32 3 131⚠️注意不同芯片公式可能不同请查阅数据手册。最可靠的方法是找到对应 Bank 的gpiochip的base值然后加上偏移量。2.3 通过 sysfs 控制 LED 的步骤命令行验证bash# 1. 导出引脚让内核创建对应的文件 echo 131 /sys/class/gpio/export # 2. 设置方向为输出 echo out /sys/class/gpio/gpio131/direction # 3. 输出高电平点亮 LED取决于硬件极性 echo 1 /sys/class/gpio/gpio131/value # 4. 输出低电平熄灭 echo 0 /sys/class/gpio/gpio131/value # 5. 使用完后解除导出可选 echo 131 /sys/class/gpio/unexport2.4 在 Qt 程序中封装 GPIO 操作我们需要在 Qt 项目中添加两个文件led.h和led.cpp封装初始化和控制函数。2.4.1 代码led.hcpp#ifndef LED_H #define LED_H void led_init(void); // 导出引脚并设为输出 void led_control(int on); // on1 点亮, on0 熄灭 #endif // LED_H2.4.2 代码led.cpp使用 sysfscpp#include sys/types.h #include sys/stat.h #include fcntl.h #include stdio.h #include errno.h #include string.h #include unistd.h #include QDebug #define GPIO_NUM 131 void led_init(void) { int fd; // 1. 导出 GPIO fd open(/sys/class/gpio/export, O_WRONLY); if (fd 0) { qDebug() open /sys/class/gpio/export failed; return; } char buf[16]; snprintf(buf, sizeof(buf), %d\n, GPIO_NUM); write(fd, buf, strlen(buf)); close(fd); // 2. 设置方向为输出 char path[64]; snprintf(path, sizeof(path), /sys/class/gpio/gpio%d/direction, GPIO_NUM); fd open(path, O_WRONLY); if (fd 0) { qDebug() open path failed; return; } write(fd, out\n, 4); close(fd); } void led_control(int on) { static int fd -1; // 保持打开避免每次重复 open char path[64]; if (fd -1) { snprintf(path, sizeof(path), /sys/class/gpio/gpio%d/value, GPIO_NUM); fd open(path, O_RDWR); if (fd 0) { qDebug() open path failed; return; } } // 注意根据实际硬件可能 1 是灭0 是亮此处假设 1 为亮 if (on) write(fd, 1\n, 2); else write(fd, 0\n, 2); }2.4.3 在 Qt 项目中添加文件并配置 .pro将led.h和led.cpp放入项目源码目录。在.pro文件中添加qmakeSOURCES led.cpp HEADERS led.h由于led.cpp中使用了系统头文件fcntl.h等它们位于交叉编译工具的 sysroot 下。如果编译时报错找不到头文件需要在.pro中添加qmakeINCLUDEPATH /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include路径根据你的开发板 SDK 实际位置修改为什么需要 INCLUDEPATH因为 Qt Creator 默认不会自动添加交叉编译工具链的标准头文件路径需要手动指定。2.4.4 在 mainwindow 中调用在mainwindow.cpp的按钮槽函数中调用cpp#include led.h void MainWindow::on_pushButton_clicked() // 点亮按钮 { led_control(1); qDebug() LED on; } void MainWindow::on_pushButton_2_clicked() // 熄灭按钮 { led_control(0); qDebug() LED off; }别忘了在main()中调用led_init()cppint main(int argc, char *argv[]) { led_init(); // 初始化 GPIO QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }2.5 上机实验步骤编译Qt 程序生成 ARM 可执行文件LED_and_TempHumi。上传到开发板bashadb push LED_and_TempHumi /root关闭开发板上可能已经运行的旧版本 Qt 程序否则设备节点被占用bashadb shell ps | grep LED_and_TempHumi # 查看 PID例如 341 kill -9 341设置环境变量并运行bashexport QT_QPA_GENERIC_PLUGINStslib:/dev/input/event1 export QT_QPA_PLATFORMlinuxfb:fb/dev/fb0 export QT_QPA_FONTDIR/usr/lib/fonts/ /root/LED_and_TempHumi点击按钮观察 LED 亮灭。2.6 常见错误与解决错误现象可能原因解决方法open /sys/class/gpio/export: Permission denied权限不足用 root 用户运行程序或chmod 666相关文件write: Device or resource busyGPIO 已被占用如被其他驱动使用检查/sys/kernel/debug/gpio或卸载冲突驱动编译时报fatal error: sys/types.h: No such file or directoryINCLUDEPATH 未设置或路径错误确认交叉编译工具链的 sysroot 路径并添加到 .pro按钮点击后 LED 无反应硬件极性相反1 灭 0 亮修改led_control中的写入值开发板运行后屏幕黑屏屏幕保护触发执行echo -e \033[9;0] /dev/tty03. 通过专用驱动程序操作 LED —— 更简洁的接口3.1 为什么要用驱动不需要知道 GPIO 编号和方向。驱动可以封装更复杂的逻辑如呼吸灯、闪烁频率。避免 sysfs 多步骤操作。3.2 编译 LED 驱动开发板厂家通常会提供 LED 驱动源码。进入驱动目录如01_led_imx6ull执行make编译bashcd ~/Desktop/01_led_imx6ull makeMakefile 内容大致如下根据你的开发板修改 KERN_DIRmakefileKERN_DIR /home/book/100ask_imx6ull-sdk/Buildroot_2020.02.x/output/build/linux-origin_master all: make -C $(KERN_DIR) M$(pwd) modules $(CROSS_COMPILE)gcc -o led_test led_test.c clean: make -C $(KERN_DIR) M$(pwd) modules clean rm -rf modules.order led_test obj-m led_drv.o⚠️注意如果使用 Mini 开发板需要修改KERN_DIR为对应路径。3.3 测试驱动将生成的led_drv.ko和led_test通过 ADB 上传到开发板bashadb push led_drv.ko /root adb push led_test /root在开发板上执行bash# 先停止可能占用引脚的 Qt 程序 mv /etc/init.d/S99myqt /root # 备份自启动脚本 reboot # 重启后加载驱动 insmod /root/led_drv.ko ls /dev/100ask_led # 应该看到设备节点 # 测试 /root/led_test 0 on # 点亮 LED /root/led_test 0 off # 熄灭3.4 修改 Qt 程序使用驱动只需要修改led.cpp把 sysfs 操作替换为打开/dev/100ask_led并写入数据。代码led.cpp使用驱动cpp#include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include QDebug static int fd -1; void led_init(void) { fd open(/dev/100ask_led, O_RDWR); if (fd 0) { qDebug() open /dev/100ask_led failed; } } void led_control(int on) { if (fd 0) return; char buf[2] {0, 0}; // buf[0] 保留buf[1] 为 0 亮 1 灭取决于驱动定义 if (on) buf[1] 0; else buf[1] 1; write(fd, buf, 2); } 驱动定义的协议write(fd, buf, 2)第二个字节表示状态。不同驱动可能不同请参考led_test.c。3.5 开机自动加载驱动修改开发板启动脚本/etc/init.d/rcS在开头添加bash#!/bin/sh insmod /root/led_drv.ko # 加载 LED 驱动 # ... 原有内容重启后Qt 程序就可以直接使用/dev/100ask_led。4. 温湿度传感器 DHT11 —— 多线程实时读取4.1 DHT11 简介DHT11 是一款单总线数字温湿度传感器一次通信读取 40 位数据16 位湿度、16 位温度、8 位校验。内核驱动已经帮我们完成了复杂的时序应用层只需要读/dev/mydht11即可获得两个字节湿度0100%和温度050°C。4.2 编译 DHT11 驱动进入驱动目录02_dht11_drv_imx6ull执行makebashcd ~/Desktop/02_dht11_drv_imx6ull makeMakefile 关键部分makefileKERN_DIR /home/book/100ask_imx6ull-sdk/Buildroot_2020.02.x/output/build/linux-origin_master all: make -C $(KERN_DIR) M$(pwd) modules $(CROSS_COMPILE)gcc -o dht11_test dht11_test.c obj-m : dht11_drv.o4.3 测试驱动bashadb push dht11_drv.ko /root adb push dht11_test /root adb shell insmod /root/dht11_drv.ko ls /dev/mydht11 /root/dht11_test /dev/mydht11输出示例textget Humidity: 76, Temperature : 31 get Humidity: 51, Temperature : 30 ...4.4 在 Qt 中集成 DHT11 —— 使用线程因为温湿度需要每隔 1 秒读取一次且不能阻塞 GUI 主线程所以需要创建一个继承自 QThread 的线程类。4.4.1 创建线程头文件 dht11_thread.hcpp#ifndef DHT11_THREAD_H #define DHT11_THREAD_H #include QThread #include QLabel class DHT11Thread : public QThread { Q_OBJECT public: void run() override; void SetLabels(QLabel *labelHumi, QLabel *labelTemp); private: QLabel *labelHumi; QLabel *labelTemp; }; #endif // DHT11_THREAD_H4.4.2 线程实现 dht11_thread.cppcpp#include dht11_thread.h #include dht11.h // 封装了对 /dev/mydht11 的读写 #include QDebug void DHT11Thread::run() { char humi, temp; char buf[20]; dht11_init(); // 打开设备 while (1) { if (0 dht11_read(humi, temp)) { // 更新湿度标签 snprintf(buf, sizeof(buf), %d%%, (unsigned char)humi); labelHumi-setText(buf); // 更新温度标签 snprintf(buf, sizeof(buf), %d, (unsigned char)temp); labelTemp-setText(buf); } msleep(1000); // 每秒读取一次 } } void DHT11Thread::SetLabels(QLabel *labelHumi, QLabel *labelTemp) { this-labelHumi labelHumi; this-labelTemp labelTemp; }4.4.3 封装 DHT11 设备操作 dht11.h 和 dht11.cppdht11.hcpp#ifndef DHT11_H #define DHT11_H void dht11_init(void); int dht11_read(char *humi, char *temp); #endif // DHT11_Hdht11.cppcpp#include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include QDebug static int fd -1; void dht11_init(void) { fd open(/dev/mydht11, O_RDWR | O_NONBLOCK); if (fd 0) { qDebug() open /dev/mydht11 failed; } } int dht11_read(char *humi, char *temp) { char buf[2]; if (fd 0) return -1; if (read(fd, buf, 2) 2) { *humi buf[0]; *temp buf[1]; return 0; } return -1; }4.4.4 修改主窗口类提供获取 Label 的方法在mainwindow.h中添加两个成员变量和 Getter 函数在mainwindow.cpp的构造函数中从 UI 中找到对应的 Label 控件并保存注意label和label_2是在 UI 设计时给控件设置的objectName请根据实际名称修改。4.4.5 在 main.cpp 中启动线程4.5 修改 .pro 文件添加新文件qmakeSOURCES \ led.cpp \ dht11.cpp \ dht11_thread.cpp \ main.cpp \ mainwindow.cpp HEADERS \ led.h \ dht11.h \ dht11_thread.h \ mainwindow.h # 如果之前添加了 sysroot 路径保留 INCLUDEPATH /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include4.6 上机实验确保dht11_drv.ko和编译好的LED_and_TempHumi都在开发板的/root目录。修改/etc/init.d/rcS添加加载 DHT11 驱动bashinsmod /root/led_drv.ko # 如果使用驱动方式 insmod /root/dht11_drv.ko重启开发板等待 Qt 界面出现温湿度数值应该每秒刷新一次。如果数值不更新检查DHT11 模块是否正确连接DATA 引脚接在开发板指定 GPIO 上驱动已配置好。执行ls /dev/mydht11确认设备节点存在。手动运行dht11_test测试驱动是否正常。5. 完整速查表 5.1 GPIO sysfs 常用操作操作命令导出引脚echo N /sys/class/gpio/export设置方向echo out /sys/class/gpio/gpioN/direction写高电平echo 1 /sys/class/gpio/gpioN/value写低电平echo 0 /sys/class/gpio/gpioN/value读取输入cat /sys/class/gpio/gpioN/value解除导出echo N /sys/class/gpio/unexport5.2 驱动操作速查设备设备节点驱动文件测试命令LED 驱动/dev/100ask_ledled_drv.koled_test 0 on/offDHT11 驱动/dev/mydht11dht11_drv.kodht11_test /dev/mydht115.3 Qt 线程要点步骤代码继承 QThreadclass MyThread : public QThread重写 run()void run() override启动线程thread.start()线程中更新 UI通过信号槽或直接调用setText注意线程安全延时msleep(milliseconds)5.4 环境变量开发板运行 Qt 程序变量值作用QT_QPA_PLATFORMlinuxfb:fb/dev/fb0使用帧缓冲显示QT_QPA_GENERIC_PLUGINStslib:/dev/input/event1触摸屏支持QT_QPA_FONTDIR/usr/lib/fonts/字体目录6. 扩展学习建议 深入学习 sysfs研究/sys/class/gpio下的其他文件active_low,edge等实现按键中断检测。编写自己的驱动参考led_drv.c和dht11_drv.c学习字符设备驱动框架。使用设备树了解如何在设备树中描述 GPIO 和 I2C 设备让驱动自动匹配。Qt 自定义控件将温湿度数值用进度条或仪表盘显示提升界面美观度。 恭喜你已经学会在 Qt 中通过文件 I/O 控制 LED 和读取温湿度传感器。下一步你可以将这些硬件操作封装成更友好的界面或者通过 MQTT 将数据上传到云端。