1. 项目缘起:从想法到动手,为什么选择QT和DHT11?

大家好,我是老张,一个在嵌入式领域摸爬滚打了十来年的老码农。最近几年,物联网和智能家居的概念越来越火,身边不少朋友、学生都跑来问我:“张工,我想做个能实时显示温湿度的小玩意儿,放家里或者实验室用,用啥技术栈比较靠谱?” 每次被问到,我脑海里第一个蹦出来的组合就是 QT Chart 加上 DHT11 传感器。

为什么是它俩?我跟你唠唠。首先,DHT11这玩意儿太经典了,价格便宜(十来块钱一个)、接口简单(就一根数据线),精度对于日常环境监测(比如看看房间温湿度、温室大棚)完全够用,绝对是新手入门传感器的首选。其次,QT框架,特别是它的 QChart 模块,做数据可视化那叫一个方便。你想啊,辛辛苦苦从传感器读到数据,如果只是一串数字在终端里跳,多没意思。能有一条实时滚动的曲线图,直观地看到温度随时间怎么变化,那感觉就完全不一样了,这才是“监测系统”该有的样子。

而且,这个组合的扩展性很强。今天我们用ARM开发板(比如树莓派、香橙派或者各种国产派)做,明天你就能把它移植到更小的MCU平台,或者做成一个带网络功能的服务器,在手机、电脑上远程看曲线。所以,这个项目不仅仅是一个简单的Demo,它更像一个可生长的技术骨架,理解了它,你就能做出更多有意思的东西。

我这次打算带大家从零开始,手把手搭一个完整的系统。我们会从最基础的硬件接线和QT环境搭建讲起,然后一步步实现数据采集、曲线绘制、动态更新,最后还会聊聊怎么把这个程序“搬”到ARM开发板上去运行。过程中我会分享我踩过的一些坑,比如时序问题、界面卡顿、移植时的依赖库处理等等,保证你跟着做一遍,不仅能做出东西,更能明白背后的道理。

2. 硬件准备与软件环境搭建

工欲善其事,必先利其器。咱们先把“家伙事儿”备齐。

2.1 硬件清单与接线

你需要准备以下几样东西:

  1. DHT11温湿度传感器模块:注意是模块,不是裸传感器。模块上通常自带了一个上拉电阻和稳压芯片,用起来省心很多。
  2. 一块ARM开发板:树莓派3B/4B、香橙派Zero 2、或者Firefly的RK系列板子都行。我这次演示用的是树莓派4B,因为它资源丰富,社区支持好。
  3. 杜邦线若干(母对母)。
  4. 一台用于编程和交叉编译的电脑(Windows, Linux, macOS均可)。

接线非常简单,DHT11模块通常有三根针脚:

  • VCC:接开发板的 3.3V 电源引脚。切记不要接5V,虽然有些模块标称兼容5V,但为了稳妥和保护GPIO,一律用3.3V。
  • GND:接开发板的 GND(地)引脚。
  • DATA:接开发板的任意一个 GPIO 引脚(例如树莓派的GPIO17,对应物理引脚11)。

这里有个我踩过的坑:早期有些教程让DATA脚接5V的上拉电阻,但现在的模块基本都集成了,所以直接连接GPIO就行。如果发现数据读取不稳定,可以尝试在DATA脚和3.3V之间加一个4.7KΩ~10KΩ的上拉电阻。

2.2 QT开发环境与交叉编译链配置

在电脑上,我们需要安装QT开发环境。去QT官网下载 QT Creator,建议选择带有桌面和嵌入式开发组件的版本。安装时,记得勾选上 Qt Charts 模块,这是我们画图的核心。

接下来是重头戏:配置交叉编译工具链。我们要在x86的电脑上编译出能在ARM板子上运行的程序。以树莓派为例,步骤大致如下:

  1. 获取工具链:从树莓派官网或Linaro网站下载对应你开发板架构(通常是armhf或aarch64)的GCC交叉编译器。
  2. 在QT Creator中配置
    • 打开QT Creator,进入工具 -> 选项 -> 设备 -> 添加,选择“Generic Linux Device”。填写你开发板的IP地址、用户名(如pi)、密码,使用SSH密钥或密码验证。
    • 然后进入构建和运行
      • 编译器页签,手动添加你下载的交叉编译器(gcc和g++)。
      • QT版本页签,添加你为目标板编译的QT库中的qmake。这个QT库需要你事先用交叉编译工具链为ARM平台编译好,或者使用开发板系统自带的(如果版本匹配)。这是个稍显复杂但必须的过程。
      • 构建套件(Kit)页签,新建一个套件,取名如“Raspberry Pi”。选择刚才配好的交叉编译器、QT版本,设备类型选“Generic Linux Device”,并选择你刚添加的设备。

这个过程可能会遇到一些库路径问题,需要耐心调整。一个偷懒但有效的方法是:直接在树莓派上安装QT开发环境(sudo apt install qt5-default qtcharts5-dev),然后在QT Creator里用“远程Linux”的方式,直接在板子上编译运行。虽然编译速度慢点,但省去了配置交叉编译的麻烦,特别适合新手快速验证。

3. 核心原理:DHT11通信与QT Chart框架

3.1 DHT11的单总线协议解读

DHT11用的是单总线协议,也就是说,数据和指令都通过那一根DATA线来传输。它的通信流程很有意思,我把它比作两个人用摩斯电码对话,而且对话节奏必须严格同步。

一次完整的数据读取大约需要4毫秒,包含40位数据(8位湿度整数+8位湿度小数+8位温度整数+8位温度小数+8位校验和)。通信始于微处理器(我们的开发板)发送一个至少18毫秒的低电平“开始信号”,然后拉高并等待DHT11的响应。

DHT11会先拉低80微秒作为“应答信号”,再拉高80微秒,告诉你“我准备好了,数据要来了”。紧接着就是40位数据的传输。每一位数据都以一个50微秒的低电平起始,随后的高电平持续时间决定了这是“0”还是“1”:26-28微秒表示“0”,70微秒表示“1”。

所以,读取DHT11的关键就在于精确测量高电平的持续时间。在Linux下,我们通常用wiringPilibgpiod这样的库来操作GPIO,并通过delayMicroseconds()micros()这类函数进行微秒级延时和时间戳获取。这里最大的坑就是时序精度。如果系统负载高,或者用了像Python这样解释型语言(没有实时保证),很容易读错数据。所以用C++在接近硬件的层面操作,稳定性会好很多。读取成功后,记得把8位湿度整数、8位温度整数和校验和拿出来验证一下(前四个字节相加的低8位应该等于校验和字节)。

3.2 QT Chart架构与核心类剖析

搞定了数据来源,我们来看看怎么把它漂亮地展示出来。QT Chart模块的架构很清晰,它建立在强大的QGraphicsView框架之上。你可以把整个图表想象成一个舞台剧:

  • QChart: 这是舞台总监,也是整个图表的容器。它负责管理所有的演员(数据系列)和布景(坐标轴),设置舞台的标题、背景、动画效果等全局属性。
  • QAbstractSeries及其子类(如QLineSeries: 这些是演员,负责表演具体的数据。QLineSeries就是画折线的,QScatterSeries是画散点的。我们这次的主角就是QLineSeries,它用来描绘温度变化的曲线。
  • QAbstractAxis及其子类(如QValueAxis, QDateTimeAxis: 这些是舞台的标尺和背景网格,定义了坐标轴的范围、刻度、标签。QValueAxis用于数值轴,QDateTimeAxis特别适合我们这种随时间变化的曲线,它能直接显示时分秒。
  • QChartView: 这是剧院窗口,是QGraphicsView的派生类。它把QChart这个舞台呈现给观众(用户),并且自带了一些交互功能,比如用鼠标滚轮缩放图表、拖拽平移视图。

它们之间的关系是:QChartView 包含 QChartQChart 管理多个 QAbstractSeriesQAbstractAxis。我们编程时,就是创建这些对象,把它们关联起来,然后不断更新QLineSeries里面的数据点,图表就会自动重绘,实现动态效果。这种模型-视图的分离设计,让代码结构非常清晰。

4. 实战第一步:编写DHT11数据采集模块

理论说再多不如动手写一行代码。我们先在QT项目里创建一个专门负责和DHT11“对话”的类。

4.1 创建DHT11驱动类

在QT Creator里新建一个C++ Class,取名DHT11。头文件dht11.h大概长这样:

#ifndef DHT11_H
#define DHT11_H

#include <QObject>
#include <QTimer>

class DHT11 : public QObject
{
    Q_OBJECT
public:
    explicit DHT11(int gpioPin, QObject *parent = nullptr);
    ~DHT11();

    bool initialize(); // 初始化GPIO
    bool readData(float &temperature, float &humidity); // 读取一次数据

signals:
    void dataRead(float temperature, float humidity); // 读取成功信号
    void readError(const QString &error); // 读取失败信号

public slots:
    void startMonitoring(int intervalMs = 2000); // 开始定时监测
    void stopMonitoring(); // 停止监测

private:
    int m_gpioPin;
    QTimer *m_timer;
    bool m_isInitialized;

    // 底层GPIO操作函数
    bool setPinMode(int mode);
    bool digitalWrite(int value);
    int digitalRead();
    void delayMicroseconds(unsigned int us);
};

#endif // DHT11_H

这个类封装了DHT11的操作。构造函数需要指定GPIO引脚号。initialize()函数会调用wiringPiSetup()等初始化底层库。核心是readData()函数,它内部实现了我们前面讲的那套复杂的单总线协议,成功则返回true并填充温度和湿度值,失败则返回false

为了方便,我提供了自动监测的槽函数startMonitoring(),它内部启用一个QTimer,每隔一段时间(默认2秒,DHT11两次读取间隔建议大于1秒)调用一次readData(),并通过信号dataRead()把结果发射出去。这样,我们的界面逻辑只需要连接这个信号,就能轻松拿到最新数据了。

4.2 实现单总线协议与错误处理

readData()函数的实现是重点也是难点。这里我给出一个基于wiringPi的简化版伪代码逻辑,并强调几个关键点:

bool DHT11::readData(float &temperature, float &humidity) {
    int data[5] = {0};
    // 1. 发送开始信号:拉低至少18ms,然后拉高20-40us
    setPinAsOutput();
    digitalWrite(LOW);
    delayMicroseconds(18000);
    digitalWrite(HIGH);
    delayMicroseconds(30);

    // 2. 等待并检测DHT11响应
    setPinAsInput();
    // ... 等待低电平(应答信号)...
    // ... 等待高电平(准备发送)...

    // 3. 读取40位数据
    for(int i=0; i<40; i++) {
        // 等待低电平(起始位)结束
        while(digitalRead() == LOW);
        // 开始计时高电平持续时间
        auto start = micros();
        while(digitalRead() == HIGH);
        auto duration = micros() - start;

        // 根据持续时间判断是0还是1,存入data数组
        data[i/8] <<= 1;
        if(duration > 40) { // 阈值需要根据实际情况调整
            data[i/8] |= 1;
        }
    }

    // 4. 校验数据
    if(data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) {
        humidity = data[0] + data[1] / 10.0; // DHT11小数部分通常为0
        temperature = data[2] + data[3] / 10.0;
        return true;
    } else {
        emit readError("Checksum error!");
        return false;
    }
}

错误处理至关重要。除了校验和错误,还要处理超时(比如DHT11没响应)、电平持续时间异常等情况。一旦发生错误,应该通过readError信号通知上层,并可能进行重试。在实际项目中,我通常会加入一个简单的错误计数,连续失败多次后才判定为硬件故障,避免偶发干扰。

5. 实战第二步:设计QT界面与Chart动态曲线

数据有了,现在我们来打造展示数据的“仪表盘”。

5.1 使用QChartView构建图表界面

首先,在QT Designer里设计主界面。拖一个QWidget作为容器,然后提升为QChartView(需要右键点击,选择“提升为...”,填入类名QChartView,头文件QtCharts/QChartView)。这样我们就有了一个图表显示区域。再放几个QLabel用来显示当前温湿度的数值,以及“开始”、“停止”、“退出”按钮。

界面布局要考虑到美观和实用性。我把QChartView放在中央占大部分区域,当前数据标签放在上方或侧边,按钮放在底部。为了让曲线看起来更舒服,记得在代码里为QChartView设置抗锯齿渲染:

ui->chartView->setRenderHint(QPainter::Antialiasing);

5.2 配置动态曲线与坐标轴

接下来在代码里(比如主窗口的构造函数中)初始化图表。这是让曲线“动”起来的基础设置。

// 创建图表对象
QChart *chart = new QChart();
chart->setTitle("环境温湿度监测");
chart->setAnimationOptions(QChart::SeriesAnimations); // 添加系列动画,让曲线变化更平滑
ui->chartView->setChart(chart);

// 创建两条曲线系列
QLineSeries *tempSeries = new QLineSeries();
tempSeries->setName("温度 (°C)");
tempSeries->setColor(Qt::red);

QLineSeries *humSeries = new QLineSeries();
humSeries->setName("湿度 (%)");
humSeries->setColor(Qt::blue);

// 将系列添加到图表
chart->addSeries(tempSeries);
chart->addSeries(humSeries);

// 创建并配置坐标轴
QDateTimeAxis *axisX = new QDateTimeAxis();
axisX->setTitleText("时间");
axisX->setFormat("hh:mm:ss"); // 时间格式
axisX->setTickCount(6); // 刻度数量
axisX->setRange(QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(60)); // 初始显示60秒范围

QValueAxis *axisY = new QValueAxis();
axisY->setTitleText("数值");
axisY->setRange(0, 100); // 温湿度一般在这个范围
axisY->setTickCount(11);

// 为每条曲线关联坐标轴
chart->addAxis(axisX, Qt::AlignBottom);
chart->addAxis(axisY, Qt::AlignLeft);
tempSeries->attachAxis(axisX);
tempSeries->attachAxis(axisY);
humSeries->attachAxis(axisX);
humSeries->attachAxis(axisY);

这里我选择了QDateTimeAxis作为X轴,因为它能自动处理时间标签的显示,非常适合实时监测场景。Y轴用了QValueAxis,范围设为0-100,覆盖了DHT11的典型输出。

6. 实战第三步:数据绑定与实时更新逻辑

现在要把“数据采集模块”和“图表展示模块”连接起来,让曲线真正随着传感器数据舞动。

6.1 连接传感器信号与图表更新槽

在主窗口类中,我们实例化DHT11对象,并将其dataRead信号连接到一个自定义的槽函数上,比如叫onDataUpdated

// 在主窗口构造函数或初始化函数中
m_dht11 = new DHT11(17, this); // 假设接在GPIO17
connect(m_dht11, &DHT11::dataRead, this, &MainWindow::onDataUpdated);
m_dht11->startMonitoring(2000); // 每2秒读一次

onDataUpdated槽函数里,我们做三件事:

  1. 更新显示当前数值的QLabel
  2. 将新的数据点(时间戳, 温度值/湿度值)添加到对应的QLineSeries中。
  3. 管理数据点的数量,实现曲线滚动效果。

6.2 实现曲线滚动与数据点管理

如果无限制地往QLineSeries里添加点,内存会耗尽,而且曲线会越来越长,看不清近期变化。我们通常采用固定长度队列的方式,保持曲线上只显示最近N个数据点。

void MainWindow::onDataUpdated(float temperature, float humidity) {
    QDateTime currentTime = QDateTime::currentDateTime();
    qint64 msecs = currentTime.toMSecsSinceEpoch();

    // 1. 更新数值标签
    ui->labelTemp->setText(QString("温度: %1 °C").arg(temperature, 0, 'f', 1));
    ui->labelHum->setText(QString("湿度: %1 %").arg(humidity, 0, 'f', 1));

    // 2. 添加新数据点
    m_tempSeries->append(msecs, temperature);
    m_humSeries->append(msecs, humidity);

    // 3. 限制数据点数量,实现滚动(例如只保留最近30个点)
    const int maxPoints = 30;
    if (m_tempSeries->count() > maxPoints) {
        m_tempSeries->removePoints(0, m_tempSeries->count() - maxPoints);
        m_humSeries->removePoints(0, m_humSeries->count() - maxPoints);
    }

    // 4. 动态调整X轴范围,让曲线始终显示最新的一段
    if (m_tempSeries->count() >= 2) {
        qint64 firstTime = static_cast<qint64>(m_tempSeries->at(0).x());
        qint64 lastTime = static_cast<qint64>(m_tempSeries->at(m_tempSeries->count() - 1).x());
        m_axisX->setRange(QDateTime::fromMSecsSinceEpoch(firstTime),
                          QDateTime::fromMSecsSinceEpoch(lastTime));
    }
}

这样,曲线就会像心电图一样,从右向左平滑滚动,始终展示最新的30个数据点(大约1分钟的数据,假设2秒一个点)。视觉效果和实用性都非常好。

7. 进阶优化:提升系统稳定性与用户体验

一个基本的系统已经能跑了,但想做得更专业、更健壮,我们还得下点功夫。

7.1 图表视觉优化与交互功能

默认的图表样式可能比较朴素。我们可以通过QChartQLineSeries的方法进行美化:

  • 设置曲线样式tempSeries->setPen(QPen(Qt::red, 2, Qt::SolidLine)); 可以调整颜色、粗细和线型。
  • 添加数据点标记:对于关键点,可以使用QScatterSeries在折线上叠加散点,突出显示。
  • 启用图例:如果之前隐藏了,可以让图例显示出来,并调整其位置。
  • 利用QChartView的交互:用户可以通过鼠标滚轮缩放图表,按住鼠标拖拽平移视图。这些功能默认是开启的,你也可以通过setRubberBand()方法设置矩形缩放区域。

7.2 异常处理与数据持久化

稳定性是嵌入式系统的生命线。

  • 传感器断联处理:在DHT11类的readData函数中,增加超时检测。如果超过一定时间(如100毫秒)还没收到DHT11的响应,就判定为读取失败,发送readError信号。上层界面可以捕获这个信号,在界面上显示“传感器未连接”等提示,并可能自动尝试重新初始化。
  • 数据跳变滤波:物理传感器偶尔会有毛刺。可以在onDataUpdated槽函数中加入简单的软件滤波。例如,比较新读到的温度和上一次的温度,如果差值超过一个合理的阈值(如5°C/秒),则忽略这个值,或者取平均值。
  • 数据记录:有时我们需要回顾历史数据。可以添加一个“开始记录”按钮,点击后将每次读取的数据(时间戳、温度、湿度)追加写入到一个CSV文件或SQLite数据库中。这样即使程序关闭,数据也不会丢失,后续还可以进行分析。

8. 从PC到ARM:系统移植与部署要点

最后,我们要把这个在电脑上跑得好好的程序,搬到ARM开发板上去。

8.1 交叉编译与依赖库处理

如果你之前配置好了交叉编译套件,那么在QT Creator里直接切换构建套件到“Raspberry Pi”,点击编译即可。编译成功后,生成的可执行文件需要拷贝到开发板上运行。

这里最容易出问题的是动态库依赖。你的程序链接了Qt5Charts, Qt5Core等库。你需要确保这些库的ARM版本存在于开发板的系统路径中,或者将编译好的QT库一起打包到开发板上。通常,在开发板上通过包管理器安装QT运行时库是最简单的方法:

sudo apt update
sudo apt install libqt5charts5 libqt5core5a

使用ldd命令检查可执行文件的依赖关系,确保所有库都能找到。

8.2 在ARM板上的运行与调试

将可执行文件通过SCP拷贝到树莓派,然后通过SSH终端运行它。第一次运行可能会遇到权限问题,因为我们的程序需要访问GPIO。在树莓派上,通常需要将用户加入gpio组,或者直接使用sudo运行(不推荐长期使用)。

scp TemperatureMonitor pi@192.168.1.xxx:/home/pi/
ssh pi@192.168.1.xxx
cd /home/pi
sudo ./TemperatureMonitor # 或先执行 sudo usermod -a -G gpio pi,然后重新登录

调试:在ARM板上调试不如在PC上方便。可以大量使用qDebug()输出日志,观察程序运行状态。也可以考虑使用gdbserver进行远程调试,但设置稍复杂。一个实用的方法是:在关键逻辑处将状态信息写入一个日志文件,或者通过UDP发送到PC上的网络调试助手查看。

当程序在板子上稳定运行,屏幕上呈现出随着真实环境变化的温湿度曲线时,这个项目就算真正成功了。你会发现,从想法到代码,再到硬件上的实体运行,整个流程打通带来的成就感,是纯软件学习无法比拟的。这个项目里用到的技术点——硬件接口操作、数据可视化、跨平台开发——都是嵌入式GUI开发中的核心,掌握了它们,你就有了解决更复杂问题的能力。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐