@[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 先体验:查看系统中的 GPIO
Linux 内核将 GPIO 控制器暴露在/sys/class/gpio下。执行以下命令查看:
bash
ls /sys/class/gpio/gpiochip* -d输出示例:
text
/sys/class/gpio/gpiochip0 /sys/class/gpio/gpiochip32 /sys/class/gpio/gpiochip64 ...每个gpiochipX代表一个 GPIO 控制器(Bank)。查看它的详细信息:
bash
cat /sys/class/gpio/gpiochip0/label # 显示硬件名称,如 "209c000.gpio" cat /sys/class/gpio/gpiochip0/ngpio # 显示该控制器有多少引脚查看所有 GPIO 的使用情况(需要内核开启 debugfs):
bash
cat /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_3:Bank=5,引脚=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.h
cpp
#ifndef LED_H #define LED_H void led_init(void); // 导出引脚并设为输出 void led_control(int on); // on=1 点亮, on=0 熄灭 #endif // LED_H2.4.2 代码:led.cpp(使用 sysfs)
cpp
#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文件中添加:qmake
SOURCES += led.cpp HEADERS += led.h由于
led.cpp中使用了系统头文件(fcntl.h等),它们位于交叉编译工具的 sysroot 下。如果编译时报错找不到头文件,需要在.pro中添加:qmake
INCLUDEPATH += /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():
cpp
int 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。上传到开发板:
bash
adb push LED_and_TempHumi /root关闭开发板上可能已经运行的旧版本 Qt 程序(否则设备节点被占用):
bash
adb shell ps | grep LED_and_TempHumi # 查看 PID,例如 341 kill -9 341设置环境变量并运行:
bash
export QT_QPA_GENERIC_PLUGINS=tslib:/dev/input/event1 export QT_QPA_PLATFORM=linuxfb: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 busy | GPIO 已被占用(如被其他驱动使用) | 检查/sys/kernel/debug/gpio,或卸载冲突驱动 |
编译时报fatal error: sys/types.h: No such file or directory | INCLUDEPATH 未设置或路径错误 | 确认交叉编译工具链的 sysroot 路径,并添加到 .pro |
| 按钮点击后 LED 无反应 | 硬件极性相反(1 灭 0 亮) | 修改led_control中的写入值 |
| 开发板运行后屏幕黑屏 | 屏幕保护触发 | 执行echo -e "\033[9;0]" > /dev/tty0 |
3. 通过专用驱动程序操作 LED —— 更简洁的接口
3.1 为什么要用驱动?
- 不需要知道 GPIO 编号和方向。
- 驱动可以封装更复杂的逻辑(如呼吸灯、闪烁频率)。
- 避免 sysfs 多步骤操作。
3.2 编译 LED 驱动
开发板厂家通常会提供 LED 驱动源码。进入驱动目录(如01_led_imx6ull),执行make编译:
bash
cd ~/Desktop/01_led_imx6ull makeMakefile 内容大致如下(根据你的开发板修改 KERN_DIR):
makefile
KERN_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 上传到开发板:
bash
adb 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,执行make:
bash
cd ~/Desktop/02_dht11_drv_imx6ull makeMakefile 关键部分:
makefile
KERN_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 测试驱动
bash
adb 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输出示例:
text
get Humidity: 76, Temperature : 31 get Humidity: 51, Temperature : 30 ...4.4 在 Qt 中集成 DHT11 —— 使用线程
因为温湿度需要每隔 1 秒读取一次,且不能阻塞 GUI 主线程,所以需要创建一个继承自 QThread 的线程类。
4.4.1 创建线程头文件 dht11_thread.h
cpp
#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.cpp
cpp
#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.cpp
dht11.h
cpp
#ifndef DHT11_H #define DHT11_H void dht11_init(void); int dht11_read(char *humi, char *temp); #endif // DHT11_Hdht11.cpp
cpp
#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 文件添加新文件
qmake
SOURCES += \ 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 驱动:bash
insmod /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/unexport |
5.2 驱动操作速查
| 设备 | 设备节点 | 驱动文件 | 测试命令 |
|---|---|---|---|
| LED 驱动 | /dev/100ask_led | led_drv.ko | led_test 0 on/off |
| DHT11 驱动 | /dev/mydht11 | dht11_drv.ko | dht11_test /dev/mydht11 |
5.3 Qt 线程要点
| 步骤 | 代码 |
|---|---|
| 继承 QThread | class MyThread : public QThread |
| 重写 run() | void run() override |
| 启动线程 | thread.start() |
| 线程中更新 UI | 通过信号槽或直接调用setText(注意线程安全) |
| 延时 | msleep(milliseconds) |
5.4 环境变量(开发板运行 Qt 程序)
| 变量 | 值 | 作用 |
|---|---|---|
QT_QPA_PLATFORM | linuxfb:fb=/dev/fb0 | 使用帧缓冲显示 |
QT_QPA_GENERIC_PLUGINS | tslib:/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 将数据上传到云端。