
简介这是一份面向树莓派初学者的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然后用digitalWrite、digitalRead这类函数去拼时序。这套玩法在树莓派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脚数据引脚可换成任意空闲GPIOGNDGND第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末尾加一行dtoverlaydht11,gpiopin17gpiopin17表示我接的GPIO17。保存后重启。在Ubuntu 22.04的树莓派镜像上配置文件同样在/boot/firmware/config.txt基本一致。3.2 从IIO接口读取温湿度数据重启后检查一下设备是否注册成功ls /sys/bus/iio/devices/正常会看到类似iio:device0的目录进去以后有in_humidityrelative_input和in_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 gpiodgpiod命令行工具先验证一下硬件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_caststd::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::atomicbool 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_runningfalse再quit()和wait()线程最后才删除对象这样退出时程序不会崩溃。5.2 用qcustomplot把温湿度画成曲线显示部分我用了qcustomplot这是一个很成熟的Qt绘图库单文件就能集成非常适合嵌入式仪表盘场景。把qcustomplot.h和qcustomplot.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)只等了1msDHT11根本没被唤醒至少要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这类数字传感器了。本文还有配套的精品资源点击获取