news 2026/10/14 1:28:39

树莓派Qt工程DHT11温湿度采集实战源码与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Qt工程DHT11温湿度采集实战源码与避坑指南

简介:面向树莓派与Qt初学者,资源演示了在Qt工程中如何读取DHT11温湿度传感器,并将实时数据展示到窗口界面,适合希望掌握树莓派GPIO引脚操作、Qt信号槽机制以及单总线传感器数据解析的开发者参考。压缩包内共有13个文件,涵盖C++源文件(.cpp/.h)、界面设计文件(.ui)、Qt工程配置(.pro)、makefile以及编译生成的.o中间文件,整体大小仅614KB,结构清晰紧凑,便于对照源码理解工程组织方式,也方便二次修改与重新构建。资源基于树莓派3B、Ubuntu Mate系统(内核4.4.38)与Qt5.5.1环境编写,已实现DHT11温湿度的自动读取与窗口显示,目前已有1336人学习;通过源码可完整梳理从传感器初始化、数据时序解析到界面刷新的链路。该示例将硬件读取与GUI展示相结合,不仅适合入门嵌入式Qt开发,也可作为后续扩展更多传感器功能的参考模板,遇到接线或编译问题时还能依据工程配置快速定位。

1. 树莓派与 Qt 工程里的 DHT11:一份让温湿度采集少走弯路的实战资源

做嵌入式上位机开发的人大概都经历过这个场景:树莓派上跑着 Qt 写的界面程序,想实时显示环境温湿度,却在 DHT11 这个看似简单的小传感器上反复翻车。单总线协议时序要求苛刻,Qt 的 GUI 事件循环又容易干扰时序,再加上不同树莓派型号的 GPIO 编号差异,一个 10 块钱的传感器能折腾掉一整天。这份工程源码包解决的就是这个痛点——它把 DHT11 的底层时序读取、C++ 封装、Qt 跨线程刷新 UI 的完整链路都整理好了。适合手里有树莓派、想在 Qt 里加温湿度功能的开发者,无论是做智能家居小项目还是实验室环境监测,都能直接拿这套代码改着用。

2. DHT11 的读取原理与树莓派 GPIO 选型:先搞懂时序再写代码

2.1 单总线协议:40 bit 数据是怎么从传感器里挤出来的

DHT11 用的是单总线协议,一根数据线既传时钟又传数据。主机发起起始信号,传感器回响应信号,然后按顺序吐出 40 位数据:8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数、8 位校验和。校验和等于前四个字节之和的低 8 位,这个机制是排查数据错乱的第一道关卡。

时序细节决定了读取成败。主机先把总线拉低至少 18ms,再释放并延时 20-40us,传感器会把总线拉低 80us 再拉高 80us 作为响应。随后每个数据位的读取都是一次“下降沿开始、高电平持续决定 0 还是 1”的过程:50us 的低电平后,如果高电平持续 26-28us 就是 0,持续 70us 左右就是 1。代码里如果用睡眠函数来量这些微秒级的时间差,那基本就输了,因为系统调度的随机延迟会直接踩碎时序窗口。

常规做法是忙等待(busy-wait)+ 微秒级时钟读取。在 Qt 工程里我会把读取线程的优先级调高,并用clock_gettime拿单调时钟来测量高电平持续时间,而不是依赖usleep。源码包里已经写了这层逻辑,但理解它为什么这么写,比直接复制粘贴更重要。

2.2 树莓派型号与 GPIO 编号:BCM 还是 BOARD,选错就白接

树莓派全系列的 GPIO 物理引脚布局基本一致,但软件层编号方式有 BCM 和 BOARD 两套。BCM 编号是芯片 GPIO 号,BOARD 编号是物理引脚序号。源码包里用的是 BCM 编号,比如 GPIO17 对应物理引脚 11,这在树莓派 3B、4B、5 上都一致。但树莓派 5 的 IO 控制器做了调整,某些库的底层实现需要更新版本才能正常工作,如果你用的还是老版本的 wiringPi,会直接读不到数据。

我一般会建议用 sysfs 接口来读取 GPIO 的电平变化,但 sysfs 的延迟偏高,对 DHT11 这种微秒级协议不友好。更靠谱的是用libgpiod或者 pigpio 的实时采样接口。这份源码包里默认用的是自研的 mmap 方式直接操作 GPIO 寄存器,绕开了内核态到用户态的切换开销,时序精度能控制在 5us 以内。

2.3 引脚接线与上拉电阻:10 块钱的教训

DHT11 模块有两种常见形态:裸传感器和带 PCB 的模块。裸传感器四个引脚,VCC、DATA、NC、GND,DATA 必须外接一个 4.7kΩ 到 10kΩ 的上拉电阻到 VCC;带 PCB 的模块一般三根线,出厂已焊好上拉电阻,直接接树莓派即可。接线时 DATA 线尽量短,别跟电机线、继电器线并排走,否则时序波形会被干扰,你会看到读出来的温度忽高忽低。

还有一个容易踩的:树莓派的 GPIO 是 3.3V 逻辑电平,DHT11 的 VCC 可以接 3.3V 也可以接 5V。如果接 5V,DATA 线返回的高电平是 5V,树莓派引脚可能扛不住长时间直连;如果接 3.3V,读出来的数据也完全正常。稳妥方案是 VCC 接 3.3V,省去电平转换的麻烦,实测读取距离 1 米内的短跳线完全没问题。

3. 在 Qt 工程里集成 DHT11:C++ 封装与读取线程设计

3.1 工程结构与核心文件说明

这份源码包是一个完整的 Qt Widgets 工程,目录结构大致如下:

dht11_qt_demo/ ├── DHT11.pro # Qt 工程文件 ├── main.cpp # 入口 ├── mainwindow.h/cpp # 主窗口与 UI 槽函数 ├── dht11.h/cpp # DHT11 读取类封装 ├── dht11worker.h/cpp # 工作线程封装,QThread 子类 └── mainwindow.ui # 界面布局文件

.pro文件里需要确保有 QT += widgets 和 CONFIG += c++11。源码包用的是 Qt 5.15 和 Qt 6.4 都兼容的写法,没有用到过时的QSslSocket之类的东西。如果你要在自己的工程里加这套代码,直接把dht11.h/cpp和dht11worker.h/cpp拷贝过去,再在.pro里添加对应源文件即可。

3.2 读取类的核心实现:把时序翻译成 C++

dht11.cpp里最核心的函数是readData(),它完成了起始信号发送、响应等待和 40 位数据读取。下面这段代码是精简后的关键部分:

bool DHT11::readData(float &humidity, float &temperature) { // 1. 初始化 GPIO 为输出,拉低至少 18ms 发送起始信号 pinMode(_pin, OUTPUT); digitalWrite(_pin, LOW); delay(20); // 20ms,大于手册要求的 18ms // 2. 释放总线,转换为输入模式,等待传感器响应 pinMode(_pin, INPUT); waitForLow(20); // 等待传感器拉低总线,超时 20us waitForHigh(40); // 等待传感器拉高,超时 40us waitForLow(80); // 等待传感器拉低 80us,超时 80us // 3. 连续读取 40 位数据 uint8_t data[5] = {0}; for (int bit = 0; bit < 40; ++bit) { while (digitalRead(_pin) == LOW); // 等待数据位的低电平结束 uint64_t start = micros(); while (digitalRead(_pin) == HIGH); // 测量高电平持续时间 uint64_t duration = micros() - start; data[bit / 8] <<= 1; if (duration > 40) { data[bit / 8] |= 1; // 高电平超过 40us 视为逻辑 1 } } // 4. 校验和判断 if ((data[0] + data[1] + data[2] + data[3] & 0xFF) != data[4]) { return false; } humidity = data[0] + data[1] / 10.0f; temperature = data[2] + data[3] / 10.0f; return true; }

关于代码里的两个关键参数:delay(20)是起始信号的低电平时间,手册要求 18ms,留 2ms 余量应对系统调度抖动,但别拉太长,超过 30ms 传感器也可能不响应;duration > 40是判定逻辑 1 的阈值,标准里 0 的高电平约 26-28us,1 的高电平约 70us,取 40us 居中可以抗住大部分干扰。如果你发现读出来的数据全是 0,先检查这两个参数在当前树莓派型号上是否适用,尤其是树莓派 5 的 GPIO 读取速度变化可能导致实际测量值偏小。

3.3 线程设计:为什么不能在 UI 线程里直接读 DHT11

Qt 的 UI 线程处理的是事件循环,鼠标移动、定时器、绘制都会阻塞主循环。DHT11 的读取过程最快要 20ms,加上忙等待,期间 UI 会完全卡死。更麻烦的是,Linux 内核的 CFS 调度器可能随时打断你的忙等待循环,导致时序超时。所以源码包单独写了一个Dht11Worker继承QThread,在run()里做无限循环读取,每次读完通过信号把结果发给主窗口。

void Dht11Worker::run() { DHT11 sensor(17); // 使用 BCM 17 引脚 while (!_stop) { float h = 0.0f, t = 0.0f; bool ok = sensor.readData(h, t); emit dataReady(ok, h, t); QThread::msleep(2000); // DHT11 采样间隔必须大于 1 秒 } }

这里QThread::msleep(2000)不是随便写的。DHT11 手册明确要求两次读取间隔至少 1 秒,否则传感器无法稳定响应。实际测试中,1.5 秒比较极限,2 秒是个稳妥值,既能保证数据刷新率,又不会因为频繁握手导致传感器内部状态紊乱。在 UI 侧,用connect(worker, SIGNAL(dataReady(bool,float,float)), this, SLOT(updateUI(bool,float,float)))连接,Qt 的队列连接机制会自动把信号跨线程调度到 UI 线程,不用手动加锁。

3.4 mainwindow.ui 的显示逻辑

界面布局很朴素:两个 QLabel 分别显示温湿度,一个状态指示灯(用 QFrame 的样式表控制颜色)。updateUI槽函数里做了容错处理——如果ok为 false,显示“读取失败”,并把指示灯置红;如果成功则更新数值并置绿。这里值得注意的是,QLabel的setText本身是线程安全的,但跨线程调用依然建议走信号槽机制,而不是直接用指针调用,否则偶尔会出现概率性的崩溃。

4. 配置与运行:从交叉编译到树莓派部署的完整链路

4.1 本地编译还是板端编译

树莓派上直接装 Qt 开发工具链编译最简单,但速度慢,树莓派 4 编译一次全工程可能要两三分钟。交叉编译的配置过程比较繁琐,需要 x86 主机上的工具链、sysroot、Qt 库版本完全匹配,稍有偏差就编译不过。我的习惯是:如果只是改改 UI 和逻辑,直接 SSH 到树莓派上编译;如果要跑大项目或者频繁 clean build,再折腾交叉编译。

源码包的 README 里给了板端编译的命令:

cd dht11_qt_demo mkdir build && cd build qmake ../DHT11.pro make -j4 ./DHT11

qmake会在系统 PATH 里找到对应版本的 Qt 工具。如果系统装了多个 Qt 版本,用qmake -v确认当前默认版本。比如树莓派系统自带的 Qt 可能是 5.11,但你装的 Qt6 可能在/usr/lib/qt6/bin/qmake,这时候要么改 PATH,要么显式指定qmake的完整路径。

如果你的 Qt 工程要部署到没有开发环境的树莓派上,注意把以下运行库一起拷过去:

ldd ./DHT11

这个命令会列出所有动态库依赖。常见的缺失项是libQt5Widgets.so.5和libQt5Core.so.5,用scp把对应.so文件拷贝到目标机器的/usr/lib/下即可,或者干脆在目标机器上装libqt5widgets5这个 apt 包更省事。

4.2 GPIO 权限:非 root 用户运行时报错的排查

DHT11 直接操作 GPIO 寄存器需要/dev/mem的访问权限。在树莓派默认系统里,用户pi在gpio组里,但如果你的系统是新装的、或者用 root 之外的用户 SSH 登录,运行程序时会报permission denied。源码包里用了mmap直接映射物理内存,这在树莓派上需要 root 权限或者通过gpio组的设备文件访问。

解决方式是在/etc/udev/rules.d/下添加 udev 规则,赋予用户 GPIO 访问权限。常见的一条规则是:

SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660"

添加后重启或输入udevadm control --reload生效。如果你代码里用的是 wiringPi 或 pigpio,它们的守护进程会自己处理权限问题,但 mmap 方案一定要确认当前用户能读/dev/mem。

4.3 读取失败的日志排查思路

在调试阶段,源码包的dht11.cpp里预留了调试宏DHT11_DEBUG,开启后每次读取都会打印每个数据位的电平时序:

#ifdef DHT11_DEBUG qDebug() << "bit" << bit << "duration" << duration << "value" << (duration > 40); #endif

跑到树莓派上后,如果发现逻辑 1 的数据位全部判断为 0,大概率是测量函数的分辨率不足:micros()在树莓派上的实现精度够用,但如果有其他高优先级进程在跑,忙等待循环的采样间隔会被拉长到数十微秒,检测不到 26us 的短高电平。这时候可以把duration > 40阈值调到 20 再试,但不能低于 20,否则逻辑 0 和 1 的区分边界就消失了。

4.4 开机自启动:让程序在无显示器的情况下跑起来

做嵌入式项目通常希望树莓派插电就运行程序,不需要手动 SSH 再敲命令。源码包附带了一个dht11.service文件示例,可以放进/etc/systemd/system/:

[Unit] Description=DHT11 Qt Demo After=multi-user.target [Service] User=pi ExecStart=/home/pi/dht11_qt_demo/build/DHT11 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

Restart=always参数很关键——如果程序因为 GPIO 竞争或者传感器无响应崩了,systemd 会在 5 秒后自动拉起。注意这里的User=pi要改成你实际的用户,如果程序需要 root 权限操作/dev/mem,就得用User=root,但安全风险你自己权衡。执行sudo systemctl enable dht11.service后,开机自启就生效了。

5. 避坑指南:DHT11 + Qt 三小时实测踩坑记录

这一章把我在实际跑这套源码时遇到的高频问题列了出来,每条都按现象、原因、解决的格式整理,直接照方抓药。

5.1 读出来的温度一直是 0 或者负数

现象:程序编译运行正常,界面能刷新,但温度长期显示 0.0 或偶尔蹦出个 -20 之类的离谱值。

原因:大概率是data[2]和data[3]的处理逻辑错了。DHT11 返回的湿度整数位和温度整数位是有符号的,温度是整数部分 + 小数部分,但数据格式里如果温度是零下,最高位是符号位。源码包里做了正常解析,但如果你自己改过字节拼装逻辑,很容易把符号位当数据位。

解决:检查readData()里的temperature = data[2] + data[3] / 10.0f;。如果 data[2] 大于 127,说明可能是负温,需要额外处理符号扩展。正常室温下 data[2] 应该在 15 到 35 之间,如果在这个范围之外,优先排查传感器接线和时序参数而不是数据解析。

5.2 第一次读取成功,第二次开始持续失败

现象:程序刚启动时能读到几组正确数据,之后界面就停在“读取失败”不动了。

原因:最常见的原因是读取间隔太短。DHT11 内部是 RC 振荡电路,每次读数后需要时间恢复。如果你用msleep(500)甚至更短,传感器会进入一种“假死”状态,不回应主机的起始信号。

解决:把读取间隔调到 2000ms 以上,源码包里已经在Dht11Worker::run()里写死了QThread::msleep(2000),如果你自己改过,别低于这个值。如果已经是 2 秒还是失败,检查是不是在两次读取之间 GPIO 被其他程序占用了,比如同时跑着pigpiod就可能抢占引脚。

5.3 Qt 界面卡顿但程序不崩溃

现象:界面刷新变得很迟钝,拖动窗口时明显掉帧,但程序不会闪退。

原因:某些connect用了直连类型而不是队列连接。DHT11_DEBUG宏开启后,大量qDebug()输出会阻塞 IO,或者跨线程信号输入了高频率的dataReady信号导致 UI 线程排队处理不过来。另外,如果你额外在主线程里调了readData(),UI 线程被忙等待拖死,就会出现“帧率低但不退出”的症状。

解决:关闭DHT11_DEBUG宏,确认 UI 槽函数不做耗时的文件或网络操作。检查connect语句是否显式指定了Qt::QueuedConnection。最典型的翻车现场是在 worker 线程里调了QLabel::setText()并加了Qt::DirectConnection,这会让绘制操作在线程上下文里执行,轻则闪烁重则崩。

5.4 引脚改了但程序还是读旧引脚

现象:把传感器从 GPIO17 挪到 GPIO22,并修改了Dht11Worker里的DHT11 sensor(17)为22,重新编译后依然读到原来的引脚数据。

原因:有些移植的代码里引脚号硬编码在宏定义里,而源码包只改构造函数参数是不够的,dht11.h里的#define DHT11_PIN 17是默认值,构造函数里的参数如果被忽略,就会落回默认值。另一个可能性是旧进程还在后台跑,你启动的是第二个实例,GPIO 资源冲突导致新进程读不到。

解决:grep -n "DHT11_PIN" dht11.*检查所有硬编码位置,全部替换为 22;用ps aux | grep DHT11确认没有残留进程,sudo killall DHT11后再启动新编译的程序。

5.5 同一份源码在树莓派 4 正常、树莓派 5 上读不到数据

现象:代码在树莓派 4B 上跑得好好的,换到树莓派 5 上编译通过,但readData()一直返回 false。

原因:树莓派 5 的 GPIO 控制器换了新设计,老的mmap直接访问物理地址的方案在新硬件上可能失效或时序漂移。尤其是micros()在不同型号上的实现精度有差异,忙等待循环的临界耗时在树莓派 5 上整体变快,导致 40us 阈值判断失真。

解决:优先考虑用libgpiod替代裸寄存器操作,或者检查源码包是否提供了DHT11_USE_GPIO_CHIP这种基于新版硬件接口的实现。如果确实没有,就在读函数里把超时值和判定阈值在树莓派 5 上重新标定——实际测一下逻辑 0 和逻辑 1 的高电平时长再设定阈值,这是最终的后悔药。

6. 进阶玩法:把 DHT11 数据接到 QML 动态图表里

当你已经能稳定读取 DHT11 数据,下一步自然是想让界面更好看。QML 写图表比 Widgets 快得多,而且可以通过Q_PROPERTY把刚才的 C++ 数据模型暴露给 QML 层。在main.cpp里注册一个Dht11Model继承QObject,用Q_PROPERTY(float temperature READ temperature NOTIFY temperatureChanged)声明属性,然后在 QML 里用Repeater配合Canvas画折线图。

具体操作上,把Dht11Worker每次读到的数据存到一个环形缓冲区里,Dht11Model::appendRecord()更新缓冲区并发射temperatureChanged信号。QML 侧监听信号后重绘 Canvas:

Canvas { id: tempChart onPaint: { var ctx = getContext("2d"); ctx.reset(); ctx.strokeStyle = "red"; ctx.lineWidth = 2; ctx.beginPath(); for (var i = 0; i < tempHistory.length; i++) { var px = i * 2; var py = 100 - tempHistory[i] * 2; if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); } }

这段 QML 里tempHistory是 C++ 侧暴露给 QML 的QVariantList,每次更新后调用tempChart.requestPaint()。需要注意的坑是:Canvas 绘制在树莓派 GPU 环境上要确认打开了 OpenGL 加速,虚拟桌面上跑会慢得让你怀疑写错了代码。

最后说个我自己的习惯——从那以后每次做传感器相关的 Qt 工程,我都会强制走一遍这套流程:先在校验和上做反查,再确认 GPIO 权限和进程残留,最后才上线。这三个坑要是没排干净,后面所有界面开发都是在沙地上盖楼。这套方法也希望能帮到你,哪怕只是少踩一个 10 块钱传感器的坑,也算值了。

本文还有配套的精品资源,点击获取

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

长寿科学:健康寿命延长的生物学机制与生活方式

1. 长寿的本质&#xff1a;从科学认知到生活实践《超越百岁&#xff1a;长寿的科学与艺术》这本书彻底颠覆了我对"长寿"的认知。过去我以为长寿就是活得久&#xff0c;但书中揭示了一个更深刻的真相&#xff1a;真正的长寿是健康寿命的延长&#xff0c;而非单纯的时间…

作者头像 李华
网站建设 2026/10/14 1:28:24

地宫取宝题解:记忆化搜索与动态规划的经典结合

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:28:21

碳化硅全栈生态:从衬底到应用,跨越硅基时代的裂谷

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:27:59

ESP32舵机控制实战:从PWM原理到平滑无线控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:27:54

树莓派4B初始化避坑指南:供电、存储、散热与系统烧录全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:27:39

Matplotlib快速上手2:中文字体、布局与保存的完整出图指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华