news 2026/4/23 2:04:30

使用文件 I/O 操作硬件 —— 从 LED 到温湿度传感器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用文件 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 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/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_ledled_drv.koled_test 0 on/off
DHT11 驱动/dev/mydht11dht11_drv.kodht11_test /dev/mydht11

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 13:47:29

Verilog递归优化:动态位宽加法器树的实现与性能分析

1. 动态位宽加法器树的设计动机 在数字电路设计中&#xff0c;加法器是最基础也最关键的运算单元之一。当我们需要对多个数据进行累加时&#xff0c;传统的做法是使用串行加法器链&#xff0c;但这种结构在数据量较大时会导致关键路径过长&#xff0c;严重影响电路的工作频率。…

作者头像 李华
网站建设 2026/4/17 23:33:45

10分钟掌握APK解析:Java开发者必备的Android应用深度分析工具

10分钟掌握APK解析&#xff1a;Java开发者必备的Android应用深度分析工具 【免费下载链接】apk-parser Apk parser for java 项目地址: https://gitcode.com/gh_mirrors/ap/apk-parser 在Android应用开发和安全分析领域&#xff0c;解析APK文件一直是一项复杂而繁琐的任…

作者头像 李华
网站建设 2026/4/18 17:12:17

如何用Mermaid-cli命令行工具快速生成专业图表:终极完整指南

如何用Mermaid-cli命令行工具快速生成专业图表&#xff1a;终极完整指南 【免费下载链接】mermaid-cli Command line tool for the Mermaid library 项目地址: https://gitcode.com/gh_mirrors/me/mermaid-cli 在当今技术文档和软件开发中&#xff0c;图表是沟通复杂概念…

作者头像 李华
网站建设 2026/4/18 18:28:53

实战指南:如何高效利用Malware-Bazaar进行恶意软件分析

实战指南&#xff1a;如何高效利用Malware-Bazaar进行恶意软件分析 【免费下载链接】malware-bazaar Python scripts for Malware Bazaar 项目地址: https://gitcode.com/gh_mirrors/ma/malware-bazaar Malware-Bazaar是由abuse.ch运营的专业恶意软件样本平台&#xff0…

作者头像 李华
网站建设 2026/4/18 13:35:16

5分钟掌握BilibiliDown:跨平台B站视频下载工具完整使用指南

5分钟掌握BilibiliDown&#xff1a;跨平台B站视频下载工具完整使用指南 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader &#x1f633; 项目地址: https://gitcode.com/gh_mirr…

作者头像 李华
网站建设 2026/4/18 3:06:00

mermaid之subgraph的进阶应用:构建复杂系统架构图

1. 为什么需要subgraph绘制复杂架构图 在分布式系统设计中&#xff0c;架构图就像建筑师的蓝图。我见过太多团队用矩形和箭头堆砌的"意大利面条图"——所有组件挤在一起&#xff0c;连线交叉得像一团乱麻。这种图发给新同事看&#xff0c;对方往往要花半小时才能搞清…

作者头像 李华