简介:本资源是一套基于RT-Thread实时操作系统的嵌入式温湿度监测与报警系统完整工程,面向物联网开发初学者、嵌入式工程师及高校实践教学人员,解决工业环境、仓储物流、医药冷链等场景中对温湿度实时感知、阈值预警与本地/远程响应的核心需求。压缩包共44个文件,涵盖6个C源文件(含传感器驱动、LVGL图形界面、OneNet云对接模块)、6个头文件(board.h、rtconfig.h等硬件与系统配置)、2个Keil工程文件(.uvprojx/.uvoptx)、1个LVGL界面截图(png)及SCons构建脚本、CubeMX配置、Kconfig菜单配置等关键开发要素,整体大小1.95MB,结构清晰,符合RT-Thread官方项目规范。已有1011人学习下载,读者可直接编译烧录至星火一号开发板,获得从DHT22数据采集、LCD/LVGL动态显示、蜂鸣器+LED本地报警到Wi-Fi联网告警的全链路可运行代码,并复用其多任务调度设计、中断处理逻辑与云平台通信模板。

相关服务:日本服务器租用

1. 项目整体设计与思路拆解

1.1 为什么做温湿度监测与报警系统

事情要从我手头的一个实际需求说起。工作室里有一批对环境要求比较敏感的电子元器件,夏天潮湿、冬天干燥,稍微不注意就会影响良品率。一开始我用的是那种十几块钱的温湿度计,但问题很明显:没法实时远程看数据,也没法在温湿度超限时自动报警。后来我翻了翻手上的开发板,发现RT-Thread官方的星火一号开发板正好适合做这个事——板载资源够用、支持RT-Thread操作系统、扩展接口齐全,于是就有了这个温湿度监测与报警系统的项目。

从技术角度看,这个项目的核心需求有三个:一是实时采集环境温湿度数据;二是将数据解析、显示并按照预设阈值进行报警判断;三是保留后续扩展的能力(比如数据上报、云端联动)。因为涉及传感器读取、多任务调度、外设控制,用裸机开发虽然也能实现,但代码耦合度高、扩展性差。所以我在方案选型时直接锁定了RT-Thread实时操作系统,配合星火一号开发板来跑多线程任务。

1.2 整体架构与核心方案选型

先说一下硬件层面的核心配置:

  • 主控:星火一号开发板(STM32F407ZGT6,主频168MHz,Cortex-M4内核)
  • 传感器:DHT11温湿度传感器(也可以用AHT20,我后面会做对比)
  • 显示:板载OLED显示屏(0.96寸,I2C接口,SSD1306驱动)
  • 报警:无源蜂鸣器 + LED指示灯
  • 交互:板载按键(用于设置报警阈值)
  • 调试:板载ST-Link + 串口

软件层面的核心架构是这样的:

  • 操作系统:RT-Thread Nano版本(也可以通过RT-Thread Studio直接创建完整版工程)
  • 传感器驱动:基于RT-Thread的传感器设备驱动框架,通过软件包方式加载DHT11驱动
  • 应用层:温湿度采集线程、数据显示线程、报警判断线程
  • 调试接口:FinSH控制台(这个真的强烈推荐,直接在终端里敲命令就能查看系统状态)

为什么选RT-Thread而不是FreeRTOS?我的理由主要有几点:第一,RT-Thread的组件生态比较完整,传感器驱动有现成的软件包可以复用;第二,FinSH控制台调试起来太方便了,不用反复烧录固件就能动态查看传感器数据;第三,星火一号开发板本身就是RT-Thread官方设计的评估板,芯片适配和BSP支持都已经做得很成熟,拿来就能直接用,基本不用自己适配驱动。

1.3 报警机制的设计思路

报警逻辑看起来简单,实际上要考虑的细节不少。我最初的方案是"温度超过阈值就触发报警",但实际跑起来后发现了一个问题:如果传感器读数在阈值附近抖动,蜂鸣器会频繁通断,非常影响体验。

所以我加了三层机制:

  1. 平滑滤波 :连续采集5次数据,去掉最大值和最小值后取平均,抑制偶发跳变。
  2. 滞回控制 :当温度超过上限值触发报警后,必须回落到上限值减2℃才解除报警,避免在阈值附近反复触发。
  3. 蜂鸣器间歇鸣响 :报警时蜂鸣器不是一直响,而是每500ms切换一次状态,既醒目又不至于太刺耳。

这套机制看起来简单,但是在实际使用中体验差距很明显。后面章节我会把具体实现代码和参数计算过程详细拆开讲。

2. 硬件准备与核心器件解读

2.1 传感器选型:DHT11、AHT20、SHT30怎么选

先聊聊传感器选型。现在市面上常见的数字温湿度传感器主要有三款:DHT11、AHT20、SHT30。

型号 温度精度 湿度精度 采样周期 通信接口 价格(约) 适合场景
DHT11 ±2℃ ±5%RH 1s 单总线 3~5元 入门学习、精度要求不高的场景
AHT20 ±0.3℃ ±2%RH 0.5s I2C 5~8元 精度要求较高的环境监测
SHT30 ±0.3℃ ±2%RH 0.5s I2C 8~15元 工业级应用,长期稳定性更好

我最终用的是DHT11,理由是项目定位是"快速搭建一套可用的环境监测报警系统",精度需求没那么苛刻。DHT11的价格便宜、驱动代码简单、网上资料多,踩坑时排查也方便。如果你做的是精度敏感的场景(比如药品存储、实验室环境),我建议直接上AHT20,I2C接口比单总线稳定得多。

这里要提醒一句:DHT11是单总线通信协议,对时序要求非常严格,主控IO口模式切换必须精确到微秒级。RT-Thread的软件包已经写好了驱动,但如果自己写裸机驱动,建议用定时器捕获的方式来读取,单纯靠延时函数有概率在优化等级不同的情况下出现时序错乱。

2.2 星火一号开发板的硬件亮点

星火一号是RT-Thread官方推出的开发板,主控芯片是STM32F407ZGT6。这块芯片的资源对做温湿度监测系统来说绰绰有余——168MHz的主频、192KB SRAM、1MB Flash,跑RT-Thread加几路传感器采集加OLED显示,CPU占用率基本在5%以下。

板载资源方面,我用到的有:

  • 板载ST-Link调试下载器,一根USB线搞定烧录和调试,不用外接J-Link
  • 板载OLED屏幕接口,通过I2C总线直接驱动SSD1306
  • 板载无源蜂鸣器和LED灯,报警输出不用额外接外设
  • 板载的按键用来切换报警阈值修改模式

开发板上引出的排针间距是2.54mm标准间距,用杜邦线连接非常方便。DHT11我接在GPIOB的PB12引脚上,OLED走I2C1(PB6和PB7),蜂鸣器用PA1,LED用PC13(板载),按键用了PB0和PB1。

2.3 硬件接线与供电注意事项

硬件接线图我直接说关键点,因为这种项目接线本来就是"查手册+用万用表"的事,但我还是要把几个容易出错的地方单独拎出来讲。

第一,DHT11的供电电压是3.3V~5.5V,但它的数据引脚输出高电平是跟着供电电压走的。如果开发板上的传感器电源是5V供电,那么数据引脚输出的高电平也是5V,这会超过STM32的容忍电压(严格说STM32的普通IO最大容忍电压是VDD+0.3V,很多引脚通过FT标记支持5V容忍)。最稳妥的做法是DHT11用3.3V供电,数据引脚直接接主控IO。如果因为走线原因必须用5V供电,数据线上串一个1kΩ的电阻做电平限制,或者用分压电路降到3.3V。

第二,OLED用的I2C接口要接上拉电阻。虽然星火一号板载了上拉,但如果你自己飞线扩展,I2C上拉电阻是必须的,一般用4.7kΩ到10kΩ都可以。我之前有块OLED模块本身带了上拉,结果还额外加了上拉,导致I2C总线上拉过强,反而出现通信不稳定的问题——这个细节值得注意。

第三,蜂鸣器如果用的是无源蜂鸣器,需要PWM信号驱动声音才正常;如果直接用GPIO高低电平驱动,只能听到"咔哒咔哒"声而不是连续的"滴滴"声。我在项目里偷了个懒,用GPIO翻转的方式驱动,实测下来报警声音虽然没有PWM那么好听,但通过500ms间隔的"滴滴"声也足够引起注意了。如果你对报警音调有要求,可以改用定时器PWM输出2kHz方波驱动。

3. 基于RT-Thread的软件实现全流程

3.1 工程创建与环境配置

我用的是RT-Thread Studio(IDE版本4.1+),创建工程流程很简单,但有几个关键配置点值得记录。

第一步,打开RT-Thread Studio,新建RT-Thread项目,选择"基于开发板"模板,芯片型号选STM32F407ZGTX,开发板选择"星火一号"。

第二步,等待工程创建完成后,在 rtconfig.h 中确认以下宏已经开启:

#define RT_THREAD_PRIORITY_MAX 32
#define RT_USING_HEAP 1
#define RT_USING_COMPONENTS_INIT 1
#define RT_USING_SERIAL 1
#define RT_USING_FINSH 1
#define RT_USING_I2C 1
#define RT_USING_SENSOR 1
#define RT_USING_PIN 1

第三步,打开menuconfig,在"硬件设备驱动"中确认I2C驱动、PIN驱动、串口驱动已开启。传感器软件包方面,我在RT-Thread Settings -> Sensor -> DHT11中勾选了DHT11软件包,并配置了读取的引脚号。

这一步有个小坑:RT-Thread Studio的menuconfig配置界面里,引脚号填的是 board 级别的引脚编号,不是F407的原始引脚编号。我第一次就填错了号导致传感器读取失败,后面在 board.h 里查了引脚映射表才搞定。

3.2 传感器驱动与数据采集实现

在RT-Thread中,使用传感器设备的标准流程是:查找设备 -> 打开设备 -> 读取数据。

先来看DHT11软件包接入后的核心代码:

#include <rtthread.h>
#include <rtdevice.h>
#include <sensor.h>
#include <sensor_dht11.h>

#define DHT11_DEVICE_NAME "dht11"

static rt_device_t dht11_dev = RT_NULL;
static struct rt_sensor_data sensor_data;

static int dht11_init(void)
{
    rt_err_t ret = RT_EOK;

    dht11_dev = rt_device_find(DHT11_DEVICE_NAME);
    if (dht11_dev == RT_NULL)
    {
        LOG_E("Can not find device: %s", DHT11_DEVICE_NAME);
        return -RT_ERROR;
    }

    ret = rt_device_open(dht11_dev, RT_DEVICE_FLAG_RDONLY);
    if (ret != RT_EOK)
    {
        LOG_E("Open device failed: %d", ret);
        return ret;
    }

    return RT_EOK;
}

static int dht11_read_data(float *temp, float *humi)
{
    rt_ssize_t size = rt_device_read(dht11_dev, 0, &sensor_data, 1);
    if (size != 1)
    {
        LOG_E("Read sensor data failed, size=%d", size);
        return -RT_ERROR;
    }

    *temp = sensor_data.data.temp;
    *humi = sensor_data.data.humidity;

    return RT_EOK;
}

这里有几个细节需要注意: rt_device_find 返回的是设备句柄,如果找不到设备,先检查驱动注册是否成功,可以用 list_device 命令在FinSH中查看。 rt_device_read 返回的实际读取长度,DHT11软件包一次读取返回一条数据,所以检查返回值是否为1。

DHT11的数据手册也值得认真看一遍。它的一次数据帧有40bit:16bit湿度数据、16bit温度数据、8bit校验码。驱动层已经帮你做了校验和数据解析,但了解这个格式有助于理解后面可能出现的异常数据问题。比如,如果读到的温度值是-4.0,湿度是0.0,多数情况下不是传感器坏了,而是数据线上有上拉/下拉干扰,导致数据帧中湿度高位字节清零了。

3.3 温湿度采集线程与滤波算法

我设计了一个温湿度采集线程,优先级设置为15,周期性每1秒执行一次采集并做滤波处理。选用这个周期是因为DHT11手册要求两次读取间隔至少1秒,同时1秒的刷新率对于环境监测来说足够快。

滤波方面,我采用了"取中值平均滤波":

#define FILTER_COUNT 5

static void data_filter(float *temp_buffer, float *humi_buffer, int count, float *temp_out, float *humi_out)
{
    // 对温度做冒泡排序(实际上可以只取最值)
    for (int i = 0; i < count - 1; i++)
    {
        for (int j = 0; j < count - 1 - i; j++)
        {
            if (temp_buffer[j] > temp_buffer[j + 1])
            {
                float tmp = temp_buffer[j];
                temp_buffer[j] = temp_buffer[j + 1];
                temp_buffer[j + 1] = tmp;

                float tmp_h = humi_buffer[j];
                humi_buffer[j] = humi_buffer[j + 1];
                humi_buffer[j + 1] = tmp_h;
            }
        }
    }

    // 去掉最大值和最小值,剩余3个取平均
    float temp_sum = 0.0f, humi_sum = 0.0f;
    for (int i = 1; i < count - 1; i++)
    {
        temp_sum += temp_buffer[i];
        humi_sum += humi_buffer[i];
    }
    *temp_out = temp_sum / (count - 2);
    *humi_out = humi_sum / (count - 2);
}

为什么用5个点去掉最值取均值而不是简单求均值?因为DHT11偶尔会因为时序干扰读到一个跳变值(比如实际温度25.4℃突然读到26.8℃),这种偶发毛刺对均值的影响可能比较大,而"去极值取中值平均"能在保留数据平滑性的同时把毛刺滤掉。

线程代码结构如下:

static void temp_humi_thread_entry(void *parameter)
{
    float temp_buffer[FILTER_COUNT];
    float humi_buffer[FILTER_COUNT];
    float temp = 0.0f, humi = 0.0f;
    int idx = 0;

    while (1)
    {
        float temp_raw, humi_raw;
        if (dht11_read_data(&temp_raw, &humi_raw) == RT_EOK)
        {
            temp_buffer[idx] = temp_raw;
            humi_buffer[idx] = humi_raw;
            idx++;

            if (idx >= FILTER_COUNT)
            {
                idx = 0;
                data_filter(temp_buffer, humi_buffer, FILTER_COUNT, &temp, &humi);
                // 数据更新回调
                on_env_data_updated(temp, humi);
            }
        }
        rt_thread_mdelay(1000);
    }
}

3.4 OLED显示与报警控制实现

OLED显示我用了RT-Thread的ssd1306软件包,直接查找设备 lcd 并调用rt_device_control设置显示内容。

核心显示逻辑并不复杂,关键是要注意显示刷新不能阻塞传感器采集线程。我单独建了一个显示线程,通过消息队列从采集线程接收最新的温湿度数据,然后更新屏幕。

屏幕布局我是这样设计的:

+------------------+
| TEMP: 25.4 C    |
| HUMI: 60.2 %    |
| [状态正常/报警] |
+------------------+

报警阈值默认设置为温度上限30℃,湿度下限30%RH。这个阈值在实际使用中是可以动态调整的,通过开发板上的两个按键实现:一个按键切换修改温度/湿度,另一个按键加/减数值。

报警判断放在采集线程里更直接,我加一个带滞回的判断函数:

typedef struct
{
    float temp_limit_high;
    float humi_limit_low;
    uint8_t alarm_state; // 0:正常,1:报警
} alarm_ctx_t;

static alarm_ctx_t g_alarm;

static void check_alarm(float temp, float humi)
{
    uint8_t new_state = 0;

    if (g_alarm.alarm_state == 0)
    {
        // 正常状态
        if (temp > g_alarm.temp_limit_high || humi < g_alarm.humi_limit_low)
        {
            new_state = 1; // 进入报警
        }
    }
    else
    {
        // 报警状态,滞回
        if (temp < (g_alarm.temp_limit_high - 2.0f) && humi > (g_alarm.humi_limit_low + 5.0f))
        {
            new_state = 0; // 恢复正常
        }
    }

    if (new_state != g_alarm.alarm_state)
    {
        g_alarm.alarm_state = new_state;
        if (new_state)
        {
            rt_pin_write(PIN_BUZZER, PIN_HIGH);
            rt_pin_write(PIN_LED, PIN_LOW);
        }
        else
        {
            rt_pin_write(PIN_BUZZER, PIN_LOW);
            rt_pin_write(PIN_LED, PIN_HIGH);
        }
    }
    
    // 报警状态下蜂鸣器间歇鸣响
    if (g_alarm.alarm_state)
    {
        if (rt_tick_get() % 1000 < 500)
            rt_pin_write(PIN_BUZZER, PIN_HIGH);
        else
            rt_pin_write(PIN_BUZZER, PIN_LOW);
    }
}

这里我用了 rt_tick_get() % 1000 来判断500ms翻转,这是一个比较粗糙但实现简单的方式。更精确的做法是给蜂鸣器单独开一个控制线程或使用软件定时器。如果你用的是实时性要求更高的场景,建议用软件定时器来翻转。

3.5 FinSH调试与串口数据上报

这一步我强烈推荐每一位做嵌入式开发的同行都好好利用。RT-Thread的FinSH本质上就是一个命令行工具,我在调试过程中反复用它来看传感器原始数据、修改阈值参数,不用改代码重新烧录,效率提升非常明显。

串口数据上报的代码我直接沿用了RT-Thread的日志系统:

#define LOG_TAG "env"
#include <ulog.h>

LOG_D("Temperature: %.1f°C, Humidity: %.1f%%", temp, humi);
if (g_alarm.alarm_state)
{
    LOG_W("ALARM: Temp=%.1f, Humi=%.1f", temp, humi);
}

经过实测,串口输出可以使用 RT-Thread Studio 自带的串口终端或任意串口工具连接。波特率是115200,8N1,日志输出帧结构大概是每2秒1行,方便后续接上位机或者用Python脚本做数据存储分析。

4. 核心功能模块的深入解析

4.1 DHT11驱动原理与时序解读

有朋友可能会问,直接用软件包不就行了吗,为什么还要去了解驱动原理?因为实际项目中,传感器的读取错误率和你对硬件协议的理解深度直接相关。出了问题如果没有原理层面的认知,排查效率会非常低。

DHT11的通信协议是单总线协议,以主机发送起始信号开始:

  1. 主机拉低总线至少18ms(唤醒传感器),然后释放总线,等待传感器响应。
  2. 传感器回应一个80us的低电平响应信号,然后拉高80us,准备发送数据。
  3. 每个数据位时序:50us低电平 + 26~28us高电平表示逻辑0;50us低电平 + 70us高电平表示逻辑1。
  4. 40位数据发送完毕后,传感器释放总线进入空闲状态。

所有位时序的定时精度要求在微秒级别,所以这也是上面提到的为什么驱动代码必须用定时器捕获或者关中断的方式做,不能依赖任务调度。

在RT-Thread的DHT11软件包中,它用的是 rt_hw_us_delay 来做微秒延时,同时操作PIN口时临时关闭了线程调度。这个驱动能工作,但有一个隐患:在极端情况下,如果系统正在处理高优先级中断且持续时间过长,单总线时序会被破坏。不过对于DHT11这种慢速传感器来说,这个概率极低,实测稳定运行了一周,偶尔还会出现校验失败的情况,软件包用重试机制解决了这个问题。

如果你用AHT20,它的I2C时序相对宽容很多,因为I2C有SCL时钟线做同步,不会出现单总线那样严格的时序要求。这就是为什么很多工业产品普遍用I2C接口传感器而不是单总线传感器。

4.2 报警阈值的参数计算与设计逻辑

报警系统的核心是阈值,但阈值不是随便拍脑袋定的。我设置的默认阈值依据是JIS Z 8703(日本工业标准)和一般电子产品仓储环境要求:温度不超过30℃,湿度不低于30%RH。这两个值对于普通民用场景比较有代表性。

阈值设计逻辑要考虑三个方面:

环境需求 :你保护的物品/设备对环境温湿度有什么要求。比如精密电子车间是22℃±2℃、45%~65%RH;一般仓库是15℃~30℃、35%~75%RH。要根据实际需求先定。

传感器精度的影响 :DHT11的温度精度±2℃,湿度精度±5%RH。这意味着当显示值接近阈值时,实际值可能已经超限了。所以阈值设定时要预留传感器的误差裕量。比如你的环境要求是温度不超过28℃,那报警阈值应该设置在26℃左右,留出2℃的误差空间。

滞回宽度的选择 :我在前面提到报警后温度回落2℃才解除。这个2℃是怎么来的?主要考虑两个因素:一是传感器本身的自然波动(DHT11在稳定环境中的读数波动大约是±0.5℃);二是空调/制冷设备的控温回差(一般空调的温度控制回差是±1℃)。如果设置的滞回值小于传感器波动范围,报警系统就会在临界点附近来回触发,所以取2℃既能避免抖动,又不会太迟钝。

湿度这边的滞回我用的是5%RH,也是类似的逻辑。DHT11的湿度测量波动比温度大一些,取5%左右比较合适。

4.3 多线程任务调度评估

整个系统运行中有几个线程在并发工作:

线程名称 优先级 周期 最大栈空间(估算) 功能
temp_humi_thread 15 1s 1024B 采集温湿度并滤波
display_thread 20 200ms 1024B 刷新OLED显示
alarm_thread 10 500ms 512B 报警判断与输出控制
idle_thread 31 - 512B 系统空闲
tshell_thread 5 - 4096B FinSH控制台

线程优先级的分配逻辑是:报警线程优先级最高(数值越小优先级越高),因为报警是安全相关的实时任务;采集线程其次;显示线程优先级最低,因为UI刷新晚几百毫秒感知不强烈;FinSH线程是RT-Thread自带的,优先级默认是5,这个值不用改。

为什么报警线程的周期不也用1秒而是用500ms?因为报警输出需要快速响应:蜂鸣器响和新数据到达之间的延迟如果太长,用户站在设备旁边会明显感觉"反应慢半拍"。500ms的周期配合报警函数的滞回判断,既能做到快速响应,又不会因为传感器更新慢导致报警线程空转。

内存占用方面,所有线程的栈加上系统核心数据结构,总内存占用大约16KB,星火一号的192KB SRAM剩余空间还非常充裕。如果你后续要加以太网、WIFI、文件系统,这个余量是完全够用的。

4.4 OLED显示驱动的优化

OLED显示这一块,看起来很简单,但实际有几个优化心得可以分享。

第一,I2C的通信速率默认是100kHz(标准模式),SSD1306这些OLED模块支持400kHz(快速模式),在RT-Thread的I2C驱动配置里可以直接改。实测从100kHz提到400kHz后,整屏刷新的等待时间从大约30ms降到了约10ms,对于1秒刷新一次的应用来说体验提升非常明显,但也注意I2C设备的实际走线质量,如果杜邦线较长(超过15cm),建议不要超频到400kHz,否则可能出现通信错误。

第二,不要整屏刷新。OLED模块本身不支持分区域刷新,但可以在内存中维护一个背板缓冲区,只把变化的部分写入显存。我在项目里只把温度和湿度数值更新,固定内容("TEMP:"和"HUMI:"标签)只在初始化时写入一次。这样每次刷新只需要发送约40字节的数据,显著减少了I2C通信量。

第三,字库选择。SSD1306模块支持内置的ASCII字符字库(8x6),直接调用软件包自带的中文显示也是可以的。但中文显示一般在主控中有取模软件生成的数组,本项目只显示英文和数字,用起来最方便。

5. 常见问题与排查技巧实录

5.1 传感器读取失败或者数值为0

这是我调试过程中遇到最多的一个问题。主要表现是:FinSH中查看DHT11设备存在,但读出来的数据一直是0.0或-4.0。

排查思路按优先级来:

  1. 检查传感器电源 :DHT11的VCC引脚是否稳定在3.3V~5V之间,用万用表量一下最直接。我之前有一次把杜邦线插错了,把VCC接到了GND上,数据全是0,第一时间没查出来。
  2. 检查数据线连接 :DHT11的DATA引脚和主控GPIO之间是否有虚接、短路、接触不良。杜邦线用久了会有氧化层,接触电阻大了会导致时序边缘变差。
  3. 检查引脚配置 :确认驱动配置里填的引脚号和实际接线一致。RT-Thread的PIN编号不是芯片引脚号,需要查开发板的引脚映射表。
  4. 检查上拉电阻 :DHT11的数据线上必须有4.7kΩ~10kΩ的上拉电阻到VCC,没有上拉的话数据读取会失败。很多DHT11模块已经板载了上拉电阻,但如果你直接买了裸传感器,记得自己接。

如果这些检查都做完了还是不行,可以在FinSH里用 list_device 命令确认设备的注册状态,然后用逻辑分析仪抓一下总线波形,看看有没有响应信号。没有响应信号多为接线问题,有波形但数据全是1或0多为上拉或者干扰问题。

5.2 OLED屏幕显示乱码或者不显示

OLED的问题主要集中在I2C地址不匹配、显示初始化失败和数据发送异常。

SSD1306的I2C地址一般是0x3C或者0x3D,取决于模块上SA0引脚的电平。我用的模块默认是0x3C,但有的模块是0x3D。RT-Thread的ssd1306软件包可以通过menuconfig修改I2C地址,默认是0x3C。如果你买到的模块是0x3D,不修改配置就会初始化失败,屏幕黑屏。

另外,有一个非常隐蔽的问题:I2C时序受上拉电阻和线缆电容影响的容忍度不一样。如果杜邦线超过20cm,线电容大了之后高频通信容易出错,表现为屏幕闪烁或者显示错乱。处理方法是缩短杜邦线,或者把I2C速度降到100kHz。

5.3 蜂鸣器不响或者声音异常

蜂鸣器不响,先分清是有源还是无源。有源蜂鸣器内部有振荡源,给高电平就响;无源蜂鸣器需要外部给PWM信号才能发声。

我用的是无源蜂鸣器,如果直接写 rt_pin_write(PIN_HIGH) ,只能听到一声"咔哒",不会有持续声音。所以在报警输出中,我用的是每500ms翻转一次电平,等效于产生了1Hz的方波信号,听感上就是"滴、滴、滴"的节奏。如果你希望发出更清脆的声音,应该使用定时器的PWM输出,配置为2kHz左右的方波,然后通过PWM占空比控制音量。

5.4 串口日志输出乱码

串口乱码是最简单的问题,但也是最常见的。出现乱码的排查顺序:

  1. 串口工具的波特率是否和代码里配置一致。RT-Thread默认调试串口是115200,如果串口助手设置成了9600,打印出来就会乱码。
  2. 系统时钟配置是否正确。如果外部晶振频率没配对(比如实际是8MHz但代码里配的是25MHz),串口波特率就会有偏差。
  3. 共地问题。USB转串口模块和开发板必须共地,否则信号电压参考点不一致,会出现间歇性乱码。

6. 系统扩展方向与实际使用心得

整个系统跑起来之后,实用价值已经超过了最初的预期。我在工作室里连续运行了两周,没有出现一次死机或者严重的传感器错误。30℃温度报警触发过两次,一次是下午阳光直射到传感器附近,一次是空调停机后的温度反弹,报警响应都很及时,蜂鸣器的声音在隔壁房间都能听到。

后续我计划做几个方向的扩展:

数据上云 :星火一号板载了以太网接口,可以很容易地接上RT-Thread的SAL套接字抽象层,通过MQTT协议把温湿度数据推送到云端。数据积累之后,还能做一些趋势分析,比如判断环境变化的规律。

多传感器接入 :在I2C总线上挂AHT20再加SHT30,做数据比对和冗余校验,提高系统在传感器故障时的健壮性。

远程报警联动 :报警触发时除了蜂鸣器和LED,还可以通过微信/短信通知。方案是接入ESP8266或者直接用开发板以太网加上一个简单的HTTP客户端,调用第三方消息推送接口。

最后分享一个我在实际测试中发现的细节: 传感器的放置位置比传感器本身的精度更重要 。一开始我把DHT11放在开发板旁边,由于芯片发热,读到的温度比环境实际温度高了将近3℃。后来我把传感器用杜邦线延长到离开发板约30cm的地方,数据才基本接近真实环境温度。做环境监测项目时,一定要让传感器远离热源,保持在通风的环境中,不然报出来的数据都是"假数据",后续的所有逻辑都会跟着出问题。

以上就是这个基于RTT星火一号开发板的温湿度监测与报警系统的完整实现过程。项目本身难度不高,但麻雀虽小五脏俱全,从传感器驱动到多线程调度再到报警策略设计,每个环节都有值得深入琢磨的地方。如果你也在做类似的嵌入式小项目,希望这篇文章能给你一些参考和启发。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif