简介:面向树莓派嵌入式开发与Qt界面编程初学者,这份资源以DHT11温湿度传感器为例,演示在Qt工程中读取数据并将温湿度实时显示到窗口的实现方法。运行环境为树莓派3B、Ubuntu Mate系统(Linux内核4.4.38)与Qt 5.5.1,代码可直接在此基础上编译运行。压缩包共13个文件,大小614KB,包含3个C++源文件、2个头文件、1个pro工程文件、1个ui界面文件,以及Makefile、o目标文件等构建中间产物,工程结构紧凑清晰。已有1336人浏览学习,适合想快速掌握传感器采集与Qt界面结合的开发者。通过学习可了解DHT11的初始化与数据读取时序、Qt下界面布局的搭建方式,以及完整工程文件的组织与构建配置,便于直接移植或二次开发。
1. 树莓派上给 Qt 工程接 DHT11:先搞明白难度到底在哪
树莓派教程里,凡是碰上 Qt 工程加 DHT11 传感器,十有八九不是 Qt 写不明白,而是被传感器的微秒级时序和线程模型绊住。DHT11 是个二十来块钱的温湿度传感器,一条数据线既收也发,看似简单,实际上它要求主机在微秒级别配合电平变化,而 Qt 工程又天然有界面线程和 worker 线程之分——这两件事凑在一起,就会产生“程序不崩但数据总是读不对”的经典问题。这篇笔记就围绕这个组合讲清楚:DHT11 的协议和接线、在 Qt 里用 QThread 封装读取线程、信号槽怎么做界面回传、以及我实际踩过的高频坑。适合手里有树莓派和 DHT11、想做一个带界面温湿度监控的读者,而不是只想跑命令行 Demo 的情况。
2. DHT11 的时序协议与树莓派接线:硬件不对,后面全白做
2.1 DHT11 单总线协议:一次完整读取的四个阶段
DHT11 没有时钟线,所有通信都靠一根 DATA 线上的电平跳变完成,所以通信双方必须严格按时间窗口来。一次完整读取可以拆成四个阶段:主机发起起始信号、从机应答、主机接收 40 位数据、总线释放。阶段之间环环相扣,任何一个阶段超时,这一次读取就作废。
第一阶段是起始信号。主机先把 DATA 线拉低,并且要持续至少 18ms。这个动作是为了让 DHT11 从休眠状态“醒”过来;短于 18ms 的话,传感器可能根本没准备好,后面就不会应答。拉低足够时间后,主机要立刻把线释放并拉高,但拉高只能保持 20~40us。这期间主机的任务是从输出模式切回输入模式,之后总线电平交给上拉电阻维持。
第二阶段是从机应答。DHT11 检测到起始信号结束后,会先拉低总线 80us,表示“收到命令”,然后再拉高 80us,表示“准备发数据”。这两个 80us 窗口可以说是整个协议里最容易出问题的地方:如果主机的释放动作不够干净,或者上拉电阻缺失,DHT11 根本不会开始应答,主机就会一直等在超时里。
第三阶段是 40 位数据。DHT11 会把 8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数、8 位校验和依次发出,每一位的格式都是“50us 低电平 + 一段高电平”。区别只在高电平的持续时间:高电平持续 26~28us 表示二进制 0,持续约 70us 表示二进制 1。主机在每位开始时检测到下降沿,然后测量高电平宽度,就能还原出这一位。
第四阶段是总线释放。40 位发完后,DHT11 会再拉低总线约 50us,然后彻底释放,把 DATA 交还给上拉电阻。至此一次读取结束。下表是我在实际代码里用的时间参数,可以直接照抄:
| 阶段 | 电平动作 | 时间参数 | 说明 |
|---|---|---|---|
| 主机起始 | DATA 拉低 | 超过 18ms | 代码里用 20ms |
| 主机释放 | DATA 拉高 | 20~40us | 代码里用 30us |
| 从机应答 | 拉低、再拉高 | 各 80us | 等待超时设 200us |
| 数据位 0 | 低 50us + 高 26~28us | 高电平短 | 阈值 40us 以下判 0 |
| 数据位 1 | 低 50us + 高约 70us | 高电平长 | 阈值 40us 以上判 1 |
| 总线释放 | DATA 拉低 50us | — | 至此一次读取完成 |
这里面最容易翻车的是“主机释放”那 20~40us。我见过有人用delay而不是delayMicroseconds,把拉高时间写成了 30ms,结果就是 DHT11 永远不应答,读出来的数据全是 0。记住一个口诀:起始信号要“长低短高”,低电平是毫秒级,高电平是微秒级。
2.2 树莓派接线和上拉电阻:杜邦线长度与 10kΩ 的取舍
接线看起来是三根线,但这里有两个坑:一是引脚编号体系,二是上拉电阻。先看引脚。DHT11 有四根脚,通常只用 VCC、DATA、GND 三根,NC 不接。VCC 接树莓派的 3.3V,GND 接 GND,DATA 接任意空闲 GPIO。下面是一组我常用的接法:
| DHT11 引脚 | 树莓派接口 | 说明 |
|---|---|---|
| VCC | 3.3V(物理脚 1) | 供电 3.3V,避免 5V 电平回灌 GPIO |
| DATA | GPIO17(BCM 编号) | 对应物理脚 11 |
| GND | GND(物理脚 9) | 共地 |
| NC | 不接 | — |
重点提醒:树莓派有三套引脚编号——物理编号、BCM 编号、wiringPi 编号。我上面写的是 BCM 编号 GPIO17,它在 wiringPi 体系里是编号 0,在物理排针上是第 11 脚。如果你在代码里写#define DHT11_PIN 17,但用的是 wiringPi 库,那实际操作的会是物理第 17 脚,根本不是你以为的那根线。新手一般都在这里卡一晚上。解决办法很简单:接通后先跑一遍gpio readall,对照表格确认 wiringPi 编号和你接的物理引脚对得上,再写进代码。
提示:同一根 DATA 线,wiringPi 编号、BCM 编号、物理脚号是完全不同的三个数字。查
gpio readall比背引脚图靠谱。
然后是上拉电阻。DHT11 的数据线是开漏输出,主机释放总线后,必须靠外部电阻把电平拉高,否则 DATA 线处于浮空状态,电平乱跳,读取必然失败。如果你买的是带 PCB 的 DHT11 模块,板子上一般已经焊好了一个 10kΩ 上拉电阻,三根杜邦线直接接就行。如果买的是裸传感器,就一定要在 DATA 和 VCC 之间自己加一个 4.7k~10kΩ 电阻。我习惯用 10kΩ,短线场景下非常稳。另外,杜邦线尽量控制在 20 厘米以内,不要和电机线、电源线捆在一起走线。线太长时分布电容会拖缓电平边沿,明明协议是对的,读数却时好时坏,这种问题用示波器才能看出来,没有示波器就只能靠缩短线来排查。
2.3 Qt 工程里驱动方案怎么选:wiringPi、pigpio、内核驱动
在 Qt 工程里访问树莓派 GPIO,常见方案有三类,我直接说结论:学习和小项目用老牌的 wiringPi,接口简单、资料最多;正经产品我更推荐 pigpio,时序更稳;追求极致稳定就直接上设备树加内核驱动,但工作量不在一个量级。
先看对比表:
| 方案 | 实现难度 | 时序精度 | 维护状态 | 适合场景 |
|---|---|---|---|---|
| wiringPi | 低 | 一般 | 已停止维护 | 原型验证、学习、个人小工具 |
| pigpio | 中 | 高 | 活跃 | 长时间运行、需要稳定读取的项目 |
| 设备树 + 内核驱动 | 高 | 最高 | 取决于驱动实现 | 生产级、开机自启的采集设备 |
wiringPi 的优势是 API 极其直观:pinMode、digitalWrite、digitalRead、delayMicroseconds、micros一套下来,做 DHT11 这种单总线协议非常顺手。缺点是官方已经不再维护,树莓派系统升级后偶尔会编译不过,这也是为什么很多新教程转向 pigpio。pigpio 的时序用守护进程加 DMA 实现,微秒级延时更可靠,但它要求先启动 pigpiod 服务,或者在自己的程序里调用gpioInitialise,对 Qt 工程来说多了一层部署成本。
我个人的选型习惯是:先拿 wiringPi 把协议和界面调通,因为调起来最快;如果发现数据丢失率偏高、需要 7x24 运行,再切换到 pigpio。下面第 3 章的示例代码以 wiringPi 为准,但代码里 GPIO 操作和延时都集中在几个函数里,换 pigpio 时只需要替换那几处底层调用,信号槽和线程模型完全不用动。这个思路很重要,否则一旦换了驱动,整个 Qt 工程都要重写。
3. 在 Qt 工程里实现 DHT11 读取线程:从 GPIO 到温湿度数据
3.1 为什么不能把传感器读取塞进 GUI 线程
很多新手拿到 Qt 工程,第一反应是在按钮的槽函数里直接调一个“readDHT11()”函数。这在语法上毫无问题,但运行起来会暴露两个致命问题。第一个问题是界面卡顿:DHT11 一次完整读取包含 20ms 起始信号和 40 位数据采集,整个过程里如果插在 GUI 线程,界面的重绘、按钮响应全部被阻塞,触摸屏上看起来就是“点了没反应”。第二个问题更隐蔽:Linux 的 GUI 线程经常被系统调度打断,而 DHT11 要求微秒级的高电平时长测量,只要线程被切走几百微秒,高电平宽度就会测错,数据自然全错。
所以在 Qt 工程里接 DHT11,第一步不是写读取代码,而是把读取动作挪进一个独立线程。常见做法是写一个继承QThread的传感器类,在run()里循环读取,读到的数据通过信号发回界面。虽然是老式写法,但对这个场景足够清晰,也最容易让新手理解“线程在跑、界面在等”的关系。下面 3.2 节就是这个类的完整实现。
3.2 Dht11Reader 线程类的完整实现:起始信号、应答与 40 位数据
先看头文件。这个类暴露两个信号给界面使用,一个表示读取成功并携带温湿度,一个表示读取失败并携带原因。构造函数接收 wiringPi 引脚号,这样同一个类可以复用在不同 GPIO 上,不必写死:
#ifndef DHT11READER_H #define DHT11READER_H #include <QThread> class Dht11Reader : public QThread { Q_OBJECT public: explicit Dht11Reader(int wiringPiPin, QObject *parent = nullptr); signals: void dataReady(float temperature, float humidity); void readFailed(const QString &message); protected: void run() override; private: bool readSensor(float &temperature, float &humidity); int waitForLevel(int level, int timeoutUs); int m_pin; }; #endif // DHT11READER_H然后是实现文件。核心逻辑是readSensor和waitForLevel两个函数:waitForLevel在超时时间内等待某一个电平出现,超时返回 -1;readSensor按协议四阶段依次执行。这里我给出关键代码:
#include "dht11reader.h" #include <wiringPi.h> Dht11Reader::Dht11Reader(int wiringPiPin, QObject *parent) : QThread(parent), m_pin(wiringPiPin) { } int Dht11Reader::waitForLevel(int level, int timeoutUs) { unsigned long start = micros(); while (digitalRead(m_pin) != level) { if (micros() - start > timeoutUs) { return -1; } } return 0; } bool Dht11Reader::readSensor(float &temperature, float &humidity) { unsigned char data[5] = { 0, 0, 0, 0, 0 }; // 起始信号:先拉低 20ms,再拉高 30us,然后释放总线 pinMode(m_pin, OUTPUT); digitalWrite(m_pin, LOW); delay(20); digitalWrite(m_pin, HIGH); delayMicroseconds(30); // 切回输入模式,靠外部上拉电阻维持高电平 pinMode(m_pin, INPUT); pullUpDnControl(m_pin, PUD_UP); // 等待从机应答:先出现 80us 低电平,再出现 80us 高电平 if (waitForLevel(LOW, 200) == -1) return false; if (waitForLevel(HIGH, 200) == -1) return false; // 读取 40 位数据 for (int i = 0; i < 40; i++) { if (waitForLevel(LOW, 150) == -1) return false; if (waitForLevel(HIGH, 150) == -1) return false; unsigned long highStart = micros(); if (waitForLevel(LOW, 150) == -1) return false; unsigned long highUs = micros() - highStart; data[i / 8] <<= 1; if (highUs > 40) { // 0 约 26~28us,1 约 70us,阈值取 40us data[i / 8] |= 1; } } // 校验和:前 4 字节相加,低 8 位必须等于第 5 字节 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),注意这是毫秒,20ms 比手册要求的最低 18ms 多留了一点余量;如果传感器老化或者低温环境,余量能提高应答成功率。拉高用delayMicroseconds(30),我这里没有把拉高时间写进一个变量,因为它在 20~40us 之间都可以工作,固定 30us 是中间值。应答等待超时设 200us 而不是 80us,是因为从拉高结束到 DHT11 真正开始拉低,还隔着 20~40us 的传感器内部处理时间,太紧容易误超时。数据位的高电平阈值取 40us,介于 28us 和 70us 之间,实测中 0 位一般在 26~32us,1 位在 65~75us,40us 作为分界非常稳。
run()函数里是一个循环,每读完一次就msleep(2000)。这个 2 秒间隔不是随手写的,DHT11 手册明确要求两次读取间隔至少 1 秒,间隔太短传感器会不响应。我实际测试时发现 1 秒偶发失败,改成 2 秒后连续跑一整天没掉过一次数据,所以推荐 2 秒作为默认值:
void Dht11Reader::run() { while (!isInterruptionRequested()) { float temp = 0.0f, hum = 0.0f; if (readSensor(temp, hum)) { emit dataReady(temp, hum); } else { emit readFailed(QStringLiteral("DHT11 read failed")); } msleep(2000); // DHT11 采样间隔建议 >= 1 秒,2 秒更稳 } }这里用isInterruptionRequested()作为循环退出条件,外界调用requestInterruption()就能让线程在下一轮循环结束时干净退出。注意msleep(2000)不会被打断,所以从请求停止到线程真正结束最多有 2 秒延迟,界面析构时wait(3000)就够等了。
3.3 用信号槽把数据交还界面:跨线程传递与刷新策略
线程类写好后,Qt 工程里的接入方式就很简单了。首先在main()里初始化 wiringPi,然后再创建主窗口。这里有个顺序要求:wiringPiSetup()必须先于一切 GPIO 操作调用,而且它需要 root 权限;如果你的程序是以普通用户身份启动的,会在这里直接失败。示例如下:
#include <QApplication> #include <QMessageBox> #include <wiringPi.h> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); if (wiringPiSetup() == -1) { QMessageBox::critical(nullptr, "Error", "wiringPi setup failed. Run with sudo."); return 1; } MainWindow w; w.show(); return app.exec(); }然后在主窗口构造函数里创建读取线程并连接信号。Qt 的跨线程信号槽默认使用队列连接,dataReady信号在传感器线程里发射,槽函数会自动排队到 GUI 线程执行,所以槽里可以直接更新界面,不用担心线程安全:
// mainwindow.cpp 构造函数中 m_dht11 = new Dht11Reader(0, this); // wiringPi 编号 0,即 BCM GPIO17 connect(m_dht11, &Dht11Reader::dataReady, this, &MainWindow::onDataReady); connect(m_dht11, &Dht11Reader::readFailed, this, &MainWindow::onReadFailed); m_dht11->start();主窗口析构时记得停线程,否则线程还在跑,而窗口对象已经被销毁,信号槽会变成向已删除对象发送信号,轻则崩溃,重则留下一个后台僵尸线程占用 GPIO:
MainWindow::~MainWindow() { m_dht11->requestInterruption(); m_dht11->wait(3000); }到这里,一个能在 Qt 工程里稳定读到 DHT11 数据的最小链路已经打通:线程循环读取,信号传回 GUI 线程,界面槽函数更新显示。接下来要解决的是显示侧的问题。
4. 界面轮询节奏与数据校验:让温湿度显示稳定不跳变
4.1 线程内自循环才是首选:为什么我不在 GUI 线程里放 QTimer 触发读
很多 Qt 新手看到“定时读取”四个字,第一反应就是QTimer,然后把它和读取函数绑在一起。这个思路本身没毛病,但它有一个隐含前提:读取操作必须足够快且不能被抢占。DHT11 不是这样,一次读取要精确测量微秒级高电平,放在 GUI 线程里就是给自己找麻烦。所以我的建议很直接:读取循环放在 worker 线程内部,用msleep(2000)控制节奏,GUI 线程完全不参与触发,只负责接收信号。
QTimer 在这个场景里并不是没用,而是岗位变了。我一般会让一个 1 秒的 QTimer 只做一件事:刷新状态栏里“最近一次成功读取”的时间戳,或者检测线程是否还活着。它不碰 GPIO,不触发读取,只是给用户一个“程序还在工作”的视觉反馈。这样 QTimer 的精度问题、线程抢占问题就都和我们无关了。下面是一个典型的状态监视定时器:
// mainwindow.cpp 构造函数中 m_statusTimer = new QTimer(this); m_statusTimer->setInterval(1000); connect(m_statusTimer, &QTimer::timeout, this, [this]() { statusBar()->showMessage( QString("last read: %1").arg(m_lastReadTime.toString("HH:mm:ss"))); }); m_statusTimer->start();m_lastReadTime在onDataReady槽里更新,这样状态栏永远显示最新读数时间。如果传感器连续读取失败,用户看到的“last read”时间会停留在很久以前,比单纯显示“已断开”更直观,也更方便定位问题。
4.2 校验和与异常跳变过滤:数据可信了再上屏
DHT11 的 40 位数据里最后 8 位是校验和,第 3 章的代码已经做了校验。但校验和通过不代表数据就一定是合理的,因为传感器本身可能工作在错误状态,输出一组“错得很有逻辑”的数值。我遇到过湿度从 60% 瞬间跳到 999% 的情况,校验和居然是对的。所以界面侧还要加一道异常值过滤。
常见做法是记录上一次成功读数,如果新读数与上次差值超过阈值,就视为毛刺丢弃。温度和湿度的阈值要分开设:温度正常变化比较慢,5 度以上的跳变基本可以判定异常;湿度相对活跃,但短时间超过 10% 的跳变也值得怀疑。下面是带过滤的槽函数:
void MainWindow::onDataReady(float temp, float hum) { // 第一层过滤:毛刺跳变 if (m_lastValid && (qAbs(temp - m_lastTemp) > 5.0f || qAbs(hum - m_lastHum) > 10.0f)) { m_jumpCount++; if (m_jumpCount < 3) { return; // 连续 3 次跳变才接受,防止偶然毛刺 } } else { m_jumpCount = 0; } // 第二层过滤:物理范围检查 if (hum < 0.0f || hum > 100.0f || temp < -40.0f || temp > 80.0f) { emit statusMessage("Invalid range"); return; } m_lastValid = true; m_lastTemp = temp; m_lastHum = hum; m_lastReadTime = QTime::currentTime(); m_tempLabel->setText(QString::number(temp, 'f', 1) + " °C"); m_humLabel->setText(QString::number(hum, 'f', 1) + " %"); }这里有个容易忽视的点:过滤代码别写在读取线程里。因为过滤需要依赖上一次成功值,这属于有状态的逻辑,放在 GUI 线程的槽函数里最合适,反正它处理的数据量很小,不会卡界面。m_jumpCount的设计是给“真跳变”留余量:如果连续 3 次都跳到同一个新值,说明环境确实变了,就接受它。这样既能滤掉偶发毛刺,又不会让真实变化永远被压制。
4.3 界面显示的最小实现:QLabel 与失败状态提示
显示端不需要复杂的东西,两个QLabel加一个状态栏就够了。温度用大号字体醒目一点,湿度在旁边,失败时把读数置成“--”并让状态栏显示错误原因。这里最大的问题反而不是显示,而是槽函数里不要做耗时操作,比如写文件、网络上报。如果一定要做,放在另一个线程或者至少用QtConcurrent丢到线程池,否则还是会拖 GUI。
我习惯把界面更新和失败处理拆成两个槽,因为它俩关心的东西不一样:
void MainWindow::onReadFailed(const QString &message) { m_tempLabel->setText("--"); m_humLabel->setText("--"); statusBar()->showMessage(message, 3000); }这个实现跑起来后,界面上温度湿度约每 2 秒刷新一次,失败时短暂显示错误信息后恢复“--”。整套逻辑下来几乎没有让 GUI 线程做过任何 GPIO 操作,出问题的概率就小了很多。
5. Qt 工程接 DHT11 常见问题排查:5 个高频踩坑现象
5.1 读出来全为 0 或持续一个固定值
现象很典型:界面稳定显示 0.0°C、0.0%,或者一直是一个不变化的固定数值。原因分三种,按概率排:接线错误、引脚编号用错、上拉电阻缺失。最常见的是把 BCM 编号和 wiringPi 编号混用了,代码里写的是 17,实际操作的是物理第 17 脚。解决办法是先跑gpio readall,对着表格确认你接的那根杜邦线对应哪一个 wiringPi 编号,然后把代码里的引脚号改成它。其次是检查 DATA 到 VCC 之间有没有上拉电阻,裸传感器没有模块上的电阻,最容易踩。
5.2 第一次读成功,第二次开始连续失败
这个现象几乎可以锁定为“读取频率过高”。DHT11 每次读完后需要时间完成内部测量,手册给出的建议是间隔 1 秒以上。我第一次调试时图省事,把msleep(200)当作轮询间隔,结果就是程序启动后第一条数据正常,后面全部超时。把间隔调到 2000ms 后问题消失。另外还有一种类似现象:起始信号低电平只有 10ms 左右,到了第二个周期传感器还没准备好,也会表现为“只有开头一次成功”。建议用delay(20)而不是delay(18),多一点余量。
5.3 编译报错找不到 wiringPi.h
Qt 的.pro文件里光写源码不写库,编译阶段就会报wiringPi.h: No such file or directory。需要在.pro里显式声明头文件路径和链接库,常见写法如下:
SOURCES += main.cpp mainwindow.cpp dht11reader.cpp HEADERS += mainwindow.h dht11reader.h LIBS += -lwiringPi INCLUDEPATH += /usr/local/include如果你的系统源里没有wiringPi软件包,安装时会报“package not found”,那就需要从源码编译安装。装完确认gpio readall能跑起来,再回到 Qt 工程里重新 qmake 和构建。这里提醒一句:wiringPi 已经不维护了,如果编译时遇到 API 变动,与其填坑,不如直接切到 pigpio,省下来的时间更值。
5.4 运行时提示 /dev/mem 打不开或权限不足
wiringPi 需要直接访问树莓派的物理内存映射,所以必须以 root 权限运行。常见表现是终端里用sudo跑没问题,一进 Qt Creator 就报 “Unable to open /dev/mem”。原因有两个:Qt Creator 不是 root 启动,或者当前用户不在gpio组里。临时验证用sudo ./dht11demo最快;如果想让普通用户也能跑,把用户加入gpio组后重新登录。注意:即使加了组,不同系统版本对/dev/mem的权限策略不同,我碰到过加了组仍然打不开的情况,兜底方案还是用sudo启动,或者换成 pigpio 的 socket 模式,那个走网络接口,不需要直接碰/dev/mem。
5.5 温度湿度偶尔跳变,读数像“毛刺”
现象是大部分时间正常,每隔几条数据会冒出一个离谱值,校验和通常还是对的。原因基本都在硬件环境:杜邦线太长、面包板接触不良、传感器和变频电机共电源。解决办法按成本从低到高排列:先把杜邦线剪短到 20 厘米以内,重新插一遍面包板,每次插到底;然后在 DHT11 的 VCC 和 GND 之间并一个 100nF 陶瓷电容,滤掉高频干扰;如果还不行,把 DHT11 的供电从 5V 降成 3.3V,或者反过来从 3.3V 升到 5V 试一次——我遇到过某批次传感器在 3.3V 下容易受干扰,换 5V 供电后反而稳定。注意换 5V 时要确认你的模块带电平转换,否则 DATA 线上的 5V 高电平有烧 GPIO 的风险。软件侧的过滤可以做,但不能作为唯一手段,硬件干净才是根。
6. 把 DHT11 封装成 Qt 可复用组件:先验证再进工程
6.1 用参数化构造把引脚和采样间隔暴露出来
第 3 章的Dht11Reader已经能在工程里正常工作了,但如果下次想换个 GPIO 口,或者把采样间隔从 2 秒改成 5 秒,还要改代码重新编译。我习惯把这类单总线传感器组件做得更“配置化”一点,构造函数直接接收引脚和间隔两个参数:
class Dht11Reader : public QThread { Q_OBJECT public: Dht11Reader(int wiringPiPin, int sampleIntervalMs, QObject *parent = nullptr); ... };对应的run()里把msleep(2000)改成msleep(m_intervalMs)。这样同一个组件可以在不同工程里复用,有的设备要省电,间隔设 10 秒;有的设备要实时监控,间隔设 1 秒上限。前端界面完全不需要知道这些差异,它只管接收dataReady。组件化之后,后面的项目接入成本就只有几行信号槽连接。
6.2 先验证传感器再进 Qt 工程
我吃了好几次“以为代码错了,其实是传感器也没接对”的亏,后来养成了一个固定习惯:在写 Qt 工程之前,先拿一个不带界面的最小程序验证传感器本身。不用额外写脚本,直接用 Qt 的qDebug打印结果,启动一个控制台应用跑 10 秒,看输出是否连续、稳定:
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); if (wiringPiSetup() == -1) { qCritical("wiringPi setup failed"); return 1; } Dht11Reader reader(0, 2000); QObject::connect(&reader, &Dht11Reader::dataReady, [](float t, float h) { qInfo("temp=%.1f hum=%.1f", t, h); }); QObject::connect(&reader, &Dht11Reader::readFailed, [](const QString &msg) { qWarning("failed: %s", qPrintable(msg)); }); reader.start(); QTimer::singleShot(10000, &app, &QCoreApplication::quit); return app.exec(); }这个验证程序如果 10 秒内输出稳定,说明硬件、接线、协议都没有问题,可以放心写界面;如果输出中断或乱跳,就别在 Qt 工程里反复折腾,先回头查硬件。这个习惯帮我避开了绝大多数的“假 bug”,很多时候问题的根源根本不在 Qt,而是杜邦线没插到底。
6.3 最后一个实用的调试手段
如果你实在没有示波器,又想确认高电平宽度对不对,可以在readSensor里临时把highUs的值通过信号打出来,对比一下 0 位和 1 位的分布。只要能看到两类明显聚在一起的时间值,说明时序是好的;如果时间值散成一片,那基本是系统调度或硬件电平问题。我最后一次调 DHT11,就是靠这个办法发现某根杜邦线接触不良,读数里的“1 位”从 70us 飘到了 100us 以上。那之后我给自己定了一条规矩:先确认传感器、再怀疑 Qt,数据打出来比什么都直观。希望这个流程能帮你少走我当年走过的弯路。
本文还有配套的精品资源,点击获取