@[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/unexport
2.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_H
2.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
make

Makefile 内容大致如下(根据你的开发板修改 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
make

Makefile 关键部分:
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.o
4.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_H
4.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_H
dht11.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/include
4.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 将数据上传到云端。