树莓派5 + Qt 6 读取DHT11温湿度传感器实战方案
2026/9/9 5:49:33 网站建设 项目流程

简介:这是一份面向树莓派初学者的Qt工程示例资源,演示在Ubuntu Mate系统(内核4.4.38)下,基于Qt 5.5.1读取DHT11温湿度传感器数据,并将温湿度实时显示在窗口界面中。资源以zip包形式提供,共13个文件,主要包含C++源文件(cpp)、头文件(h)、Qt工程文件(pro)、界面文件(ui)以及Makefile和编译生成的中间文件(o)等,整体大小仅614KB,适合快速下载学习。目前已有1334人浏览学习。通过该资源,读者可以获得一个可直接运行的DHT11采集显示例程,了解在Qt工程中如何编写传感器驱动、设计UI界面及完成工程配置,对入门树莓派外设开发与Qt图形界面编程具有很好的参考价值。 去年我在树莓派5上做一个环境数据采集的小项目,核心需求很简单:用Qt写一个桌面程序,实时读取DHT11温湿度传感器的数值并显示曲线。按理说这类教程在网上已经烂大街了,但真上手才发现,大量现成教程还在用wiringPi那一套,而wiringPi早就停止维护,在树莓派5的新内核和新引脚控制器上根本跑不起来。这篇文章就是我在树莓派5 + Ubuntu 22.04 + Qt 6 环境下完整踩坑后整理出的方案,适合准备在树莓派上做Qt上位机、智能家居小项目,或者想搞懂DHT11到底怎么读的开发者参考。

1. 项目整体思路与方案选型

1.1 这个项目解决什么问题

DHT11是入门级温湿度传感器,成本几块钱,一个引脚就能输出数据,非常适合做环境监测演示。但在Qt工程里用DHT11,难点从来不在Qt本身,而在于底层数据怎么读上来。树莓派的GPIO口不能像单片机那样直接操作寄存器,数据得经过Linux内核这一层,而不同板卡、不同内核版本,处理方式差异非常大。

我的目标很明确:做一个长期稳定跑的桌面程序,而不是那种"终端里能打印一次温湿度就算成功"的demo。这就要求数据读取必须可靠,Qt界面不能卡顿,程序重启后能自动恢复读取。围绕这三个目标,我对比了几种主流方案,下面细说。

1.2 为什么放弃老教程里的通用GPIO库

网上能找到的树莓派DHT11教程,90%会让你先装wiringPi,然后用digitalWritedigitalRead这类函数去拼时序。这套玩法在树莓派4B及以前确实能跑,但在2023年之后的新环境里,基本是坑:

  • wiringPi早在2019年就停止维护了,作者自己都宣布项目终结,新内核新板卡根本没有适配。
  • 树莓派5的IO系统发生了大变化,改用RP1芯片管理GPIO,很多老库直接操作寄存器地址的方式全部失效。
  • Linux内核的sysfs GPIO接口(/sys/class/gpio)在较新内核里被标记为弃用,Ubuntu 22.04的树莓派镜像里默认就是用不了的。
  • 树莓派5上用户可用的GPIO控制器名称也变了,老程序硬编码的gpiochip0在树莓派5上往往对应的是系统内部引脚,真正能操作的是gpiochip4,这个差异让很多移植代码直接报错。

我并不是说这些老库没有学习价值,但从工程角度讲,选型时要优先考虑"还能不能被官方支持、有没有长期维护"。DHT11的数据读取对时序要求很高,靠一个停更多年的库去搞定这件事,风险太大。

2. 硬件接线与协议基础

2.1 引脚连接与注意事项

DHT11模块通常是三根引脚:VCC、DATA、GND,有些裸传感器是四根,但第四根NC脚不用管。接线特别简单,我直接列在下面:

DHT11引脚树莓派GPIO说明
VCC3.3V(第1脚)推荐3.3V供电,和GPIO逻辑电平一致
DATAGPIO17(第11脚)数据引脚,可换成任意空闲GPIO
GNDGND(第6脚)共地必须接

有两点要注意。第一,尽量买带PCB板的模块,板上一般集成了4.7kΩ到10kΩ的上拉电阻,直接接就能用。如果是裸传感器,DATA引脚到3.3V之间必须自己加一个4.7kΩ上拉电阻,否则读取大概率失败。第二,DHT11官方手册写供电范围是3.3V到5.5V,但树莓派GPIO输入最高只能承受3.3V,模块如果接了5V供电,数据脚可能会把电平抬高到5V,长期使用有烧引脚的风险。稳妥起见,我统一用3.3V供电。

2.2 DHT11通信协议与时序拆解

DHT11用的是单总线协议,一根数据线既要主机发指令又要传感器回数据,时序全看电平翻转的时间长度。完整通信过程分三个阶段:

  • 空闲状态:数据线保持高电平。
  • 主机发起起始信号:数据线拉低至少18ms(推荐20ms),然后释放拉高20-40us,等待传感器响应。
  • 传感器响应:DHT11检测到起始信号后,先拉低80us再拉高80us,表示"我准备好了,开始传数据"。
  • 数据回传:连续发送40位数据,每一位都以50us低电平开头,随后如果高电平持续26-28us表示逻辑0,如果高电平持续约70us表示逻辑1。

40位数据的含义是:8位湿度整数 + 8位湿度小数 + 8位温度整数 + 8位温度小数 + 8位校验和。校验算法很简单,把前四个字节相加,取低8位,如果和第五个字节相等就说明数据有效。比如湿度45%、温度26.5°C,那前四字节就是0x2D 0x00 0x1A 0x05,校验和就是0x2D + 0x00 + 0x1A + 0x05 = 0x4C

理解了协议就知道,DHT11读取的难点在于微秒级的时间判断。26us和70us的高电平宽度要区分清楚,而普通用户态程序在Linux非实时系统里做这种精准计时,难度相当大。这就直接引出了下一节要讲的两种方案。

3. 读取方案一:内核DHT11驱动,稳定压倒一切

3.1 用设备树overlay配置DHT11

这是我最推荐的生产级方案:Linux内核里早就自带了DHT11驱动(drivers/iio/humidity/dht11.c),只是一般没有默认绑定到GPIO上。我们只要通过设备树告诉内核"某个GPIO上接了一个DHT11",驱动就会自动负责所有时序细节,用户程序只需要从sysfs读文件。

在树莓派官方系统(Raspberry Pi OS)上,配置方式是在/boot/config.txt(部分新系统在/boot/firmware/config.txt)末尾加一行:

dtoverlay=dht11,gpiopin=17

gpiopin=17表示我接的GPIO17。保存后重启。在Ubuntu 22.04的树莓派镜像上,配置文件同样在/boot/firmware/config.txt,基本一致。

3.2 从IIO接口读取温湿度数据

重启后检查一下设备是否注册成功:

ls /sys/bus/iio/devices/

正常会看到类似iio:device0的目录,进去以后有in_humidityrelative_inputin_temp_input这两个文件。读取方式:

cat /sys/bus/iio/devices/iio:device0/in_humidityrelative_input cat /sys/bus/iio/devices/iio:device0/in_temp_input

返回的数值单位很特殊,湿度文件返回的是千分之百分比,比如45000表示45.0%;温度文件返回的是千分之一摄氏度,比如26500表示26.5°C。要除以1000才是常用单位。我在Qt工程里封装了一个简单函数:

double readDHT11Value(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return 0.0; } QString data = QString::fromUtf8(file.readAll()).trimmed(); file.close(); bool ok = false; double raw = data.toDouble(&ok); return ok ? raw / 1000.0 : 0.0; }

用的时候分别传入温度和湿度文件的路径就行。这种方案最大的好处是,整个读取过程由内核驱动完成,里面用的是高精度时钟和gpiolib接口,时序非常稳定,Qt程序永远不会因为读取冲突而卡死。我实测连续跑了三天,数据一次都没读丢过。

4. 读取方案二:用户态直接读引脚(理解原理的最佳路径)

4.1 libgpiod的基本操作

如果你用的是树莓派4B或者更老的板子,想自己把时序全部写出来理解一遍,那也不难。现在的标准做法是用libgpiod,它是内核gpiolib的用户态接口,官方维护,替代了废弃的sysfs方式。安装依赖:

sudo apt install libgpiod-dev gpiod

gpiod命令行工具先验证一下硬件:

gpioinfo gpiochip0

在树莓派4B上,用户GPIO一般在gpiochip0;树莓派5上要换成gpiochip4。查到的结果里可以看到每路GPIO的方向和状态。libgpiod的C API支持通过gpiod_line_request_output把引脚设为输出并拉低,再通过gpiod_line_request_input切回输入,完全足够完成DHT11的起始信号和后续采样。

4.2 一个可运行的C++读取函数

下面这段代码我精简过,保留了核心逻辑。它演示了起始信号和响应读取的流程,数据位部分用计算形参高电平持续时间的思路判断0或1:

#include <gpiod.h> #include <cstdio> #include <cstring> #include <thread> #include <chrono> static int readBit(struct gpiod_line *line, long *duration) { auto start = std::chrono::steady_clock::now(); while (gpiod_line_get_value(line) == 0) { // 等待低电平结束 } auto lowEnd = std::chrono::steady_clock::now(); while (gpiod_line_get_value(line) == 1) { // 等待高电平结束 } auto highEnd = std::chrono::steady_clock::now(); *duration = std::chrono::duration_cast<std::chrono::microseconds>(highEnd - lowEnd).count(); return gpiod_line_get_value(line); } int readDHT11() { struct gpiod_chip *chip = gpiod_chip_open_by_name("gpiochip0"); if (!chip) { perror("open chip"); return -1; } struct gpiod_line *line = gpiod_chip_get_line(chip, 17); if (!line) { perror("get line"); return -1; } // 发起起始信号 gpiod_line_request_output(line, "dht11", 0); std::this_thread::sleep_for(std::chrono::milliseconds(20)); gpiod_line_request_input(line, "dht11"); // 等待响应信号:先低后高 // 后面循环40次读数据位,用高电平时间判断 gpiod_line_release(line); gpiod_chip_close(chip); return 0; }

这段代码里gpiod_line_request_input这个切换操作本身就有不小的开销,我在树莓派4B上测试,单次切换大约耗时几十微秒,对于26us和70us的分辨来说已经接近极限了。所以如果你真的要跑用户态方案,建议装pigpiod或者用带DMA的库来保证微秒级精准度。这个方案的真正价值不在工程效率,而在于帮你把DHT11的时序彻底跑明白。

5. Qt工程集成:传感器数据进界面

5.1 线程设计:读取不能让UI卡顿

回到Qt部分。DHT11的读取无论走哪种方案,都不应该直接放在主界面线程里。原因有两个:一是sysfs文件读取在极端情况下可能阻塞,二是每次读取之间的等待和计算会拖慢界面刷新。我选择了经典的QThread + QObject工作线程模式:创建一个DHT11Reader类,把它moveToThread到一个专门的工作线程,读取完通过信号槽把结果发回主线程刷新UI。

头文件里的核心成员:

class DHT11Reader : public QObject { Q_OBJECT public: explicit DHT11Reader(QObject *parent = nullptr); public slots: void startReadLoop(); void stopReadLoop(); signals: void dataReady(double temperature, double humidity); void readError(const QString &message); private: std::atomic<bool> m_running{false}; };

工作线程里的循环逻辑:

void DHT11Reader::startReadLoop() { m_running = true; while (m_running) { double temp = 0.0, hum = 0.0; // 方案一:从IIO sysfs读取 temp = readDHT11Value("/sys/bus/iio/devices/iio:device0/in_temp_input"); hum = readDHT11Value("/sys/bus/iio/devices/iio:device0/in_humidityrelative_input"); if (temp > 0 && hum > 0) { emit dataReady(temp, hum); } else { emit readError("DHT11 读取失败或校验错误"); } // DHT11采样周期约1Hz,频繁读反而容易出错 std::this_thread::sleep_for(std::chrono::seconds(2)); } }

在主窗口里,启动线程的代码是:

m_reader = new DHT11Reader; m_worker = new QThread(this); m_reader->moveToThread(m_worker); connect(m_worker, &QThread::started, m_reader, &DHT11Reader::startReadLoop); connect(m_reader, &DHT11Reader::dataReady, this, &MainWindow::onDataReady); connect(m_reader, &DHT11Reader::readError, this, &MainWindow::onReadError); connect(this, &MainWindow::destroyed, m_reader, &DHT11Reader::stopReadLoop); m_worker->start();

这里有个我踩过的小坑:connect(this, &MainWindow::destroyed, m_reader, &DHT11Reader::stopReadLoop)这种写法在窗口析构时可能不够安全,因为工作线程可能还在循环里没有退出。我在closeEvent里手动先置m_running=false,再quit()wait()线程,最后才删除对象,这样退出时程序不会崩溃。

5.2 用qcustomplot把温湿度画成曲线

显示部分我用了qcustomplot,这是一个很成熟的Qt绘图库,单文件就能集成,非常适合嵌入式仪表盘场景。把qcustomplot.hqcustomplot.cpp加入工程,在UI上放一个QCustomPlot控件,然后初始化两个曲线图层,分别显示温度和湿度:

ui->plot->addGraph(); ui->plot->graph(0)->setPen(QPen(QColor(31, 119, 180))); ui->plot->graph(0)->setName("温度(°C)"); ui->plot->addGraph(); ui->plot->graph(1)->setPen(QPen(QColor(255, 127, 14))); ui->plot->graph(1)->setName("湿度(%)"); ui->plot->legend->setVisible(true); ui->plot->xAxis->setLabel("时间"); ui->plot->yAxis->setLabel("数值");

onDataReady槽里追加数据并更新横轴范围:

void MainWindow::onDataReady(double temp, double hum) { double now = QDateTime::currentMSecsSinceEpoch() / 1000.0; ui->labelTemp->setText(QString::number(temp, 'f', 1) + " °C"); ui->labelHum->setText(QString::number(hum, 'f', 1) + " %"); ui->plot->graph(0)->addData(now, temp); ui->plot->graph(1)->addData(now, hum); ui->plot->xAxis->setRange(now - 60, now); ui->plot->replot(); }

这样界面上就会实时滚动最近一分钟的温湿度曲线。qcustomplot的addData在数据量上来以后要注意清理历史数据,我每30秒清一次超过半小时的旧数据点,否则内存会缓慢增长。另外,qcustomplot的replot()在QThread线程里不能直接调,我所有绘图操作都放在主线程槽函数里,这是最稳妥的做法。

6. 常见问题与排查技巧

6.1 DHT11数据一直显示0或固定值

这是最常见的现象。先确认硬件接线:供电是不是3.3V、GND有没有共地、DATA引脚有没有插错。我用万用表量DATA引脚电压,正常空闲状态应该在3.3V左右,如果量到0V,说明模块没上电或者上拉电阻没接好。

接下来检查设备树有没有生效,用dmesg | grep dht11看内核日志,如果出现failed to request GPIO之类的提示,多半是overlay里指定的引脚和实际接线不一致,或者引脚被别的设备占用了。我一开始接GPIO2,怎么都不出数,后来才发现GPIO2被默认分配给了I2C功能,改成GPIO17立刻就好了。选引脚时尽量避开I2C、UART这些默认复用的功能脚。

6.2 用户态方案频繁读取失败怎么办

如果你坚持用用户态方案,出现读取失败先看两个地方。第一,起始信号的低电平时间是否够长,我遇到过有人写usleep(1000)只等了1ms,DHT11根本没被唤醒,至少要18ms,建议20ms。第二,每次读取间隔不能太短,DHT11内部采样周期大约是1Hz,你如果每200ms读一次,驱动会频繁返回错误。我一般设置2秒读一次,成功率接近100%。第三个建议是检查busy-wait循环里有没有被其他进程抢占,如果系统负载高,可以考虑把读取线程的优先级调高,或者干脆换内核驱动方案。

6.3 树莓派5 + Ubuntu 22.04下的特殊问题

树莓派5的用户GPIO控制器是gpiochip4,不是老教程里的gpiochip0,用libgpiod和命令行工具的时候要格外注意名称。另外,Ubuntu 22.04的apt源在国内访问很慢,建议先换成清华源或其他国内镜像,/etc/apt/sources.list里把archive.ubuntu.com替换成镜像地址,速度能快一个量级。还有个细节:Ubuntu的树莓派镜像里默认跑的是NetworkManager,频繁在弹出的网络弹窗会占用系统资源,如果你做嵌入式无人值守项目,记得先停掉不需要的GUI服务。

最后再分享一个小技巧:不管用哪种方案,调试阶段都要在Qt里加日志输出,把每次读取的原始值和耗时记录下来。DHT11本身精度有限,温度误差在±2°C、湿度误差在±5%RH左右,如果你的数据跳动在这个范围内,别怀疑程序有问题,传感器本身就只能达到这个精度。真需要更准的数据,就得考虑SHT30或BME280这类数字传感器了。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询