使用文件 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 先体验:查看系统中的 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 的 gpiochipbase 值,然后加上偏移量。

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.hled.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
  1. led.hled.cpp 放入项目源码目录。

  2. .pro 文件中添加:

    qmake

    复制代码
    SOURCES += led.cpp
    HEADERS += led.h
  3. 由于 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 上机实验步骤

  1. 编译 Qt 程序,生成 ARM 可执行文件 LED_and_TempHumi

  2. 上传 到开发板:

    bash

    复制代码
    adb push LED_and_TempHumi /root
  3. 关闭 开发板上可能已经运行的旧版本 Qt 程序(否则设备节点被占用):

    bash

    复制代码
    adb shell
    ps | grep LED_and_TempHumi   # 查看 PID,例如 341
    kill -9 341
  4. 设置环境变量 并运行:

    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
  5. 点击按钮,观察 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.koled_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 上机实验

  1. 确保 dht11_drv.ko 和编译好的 LED_and_TempHumi 都在开发板的 /root 目录。

  2. 修改 /etc/init.d/rcS,添加加载 DHT11 驱动:

    bash

    复制代码
    insmod /root/led_drv.ko      # 如果使用驱动方式
    insmod /root/dht11_drv.ko
  3. 重启开发板,等待 Qt 界面出现,温湿度数值应该每秒刷新一次。

  4. 如果数值不更新,检查:

    • 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.cdht11_drv.c,学习字符设备驱动框架。
  • 使用设备树:了解如何在设备树中描述 GPIO 和 I2C 设备,让驱动自动匹配。
  • Qt 自定义控件:将温湿度数值用进度条或仪表盘显示,提升界面美观度。

🎉 恭喜!你已经学会在 Qt 中通过文件 I/O 控制 LED 和读取温湿度传感器。下一步,你可以将这些硬件操作封装成更友好的界面,或者通过 MQTT 将数据上传到云端。

相关推荐
雒珣4 小时前
Qt实现命令行参数功能示例:QCommandLineParser
开发语言·数据库·qt
史迪仔01126 小时前
[QML] 交互事件深度解析:鼠标、键盘、拖拽
前端·c++·qt
一晌小贪欢6 小时前
PyQt5 开发一个 PDF 批量合并工具
开发语言·qt·pdf
swift192216 小时前
Qt多语言问题 —— 静态成员变量
开发语言·c++·qt
用户805533698036 小时前
现代Qt开发教程(新手篇)1.4——容器
c++·qt
qq_466302457 小时前
u盘插入拔出,listView不显示盘符变化
c++·qt
blog.pytool.com8 小时前
ZLG USBCAN-II 接口使用
qt
秋月的私语9 小时前
遥感影像拼接线优化工具:基于Qt+GDAL+OpenCV的从一到二实践
开发语言·qt·opencv
雾岛听蓝9 小时前
Qt操作指南:信号与槽机制
开发语言·数据库·qt