基于RT-Thread的STM32F407温湿度天气时钟设计与实现
2026/9/10 16:07:54 网站建设 项目流程

简介:面向嵌入式开发者的STM32F407+RT-Thread温湿度天气时钟完整工程包,将环境温湿度监测、网络天气获取与实时时钟显示集成于一体,适合学习RTOS移植、外设驱动、网络协议栈和物联网应用的开发者作为实战参考。压缩包共2000个文件,以C/C++源码为主(877个h与836个c),另有SConscript构建脚本、Kconfig配置、Python脚本、Markdown文档、演示视频与图片等,覆盖驱动、应用、中间件和文档多个层级。包体约14.93MB,文件类型丰富但结构清晰,代码模块化、注释统一,便于按需裁剪二次开发。项目已提供系统架构设计、软件模块说明、硬件接口定义以及演示视频,能帮助快速搭建开发环境、理解线程调度和传感器采集流程。已有332人浏览学习,从内容预览可见HAL库驱动、文件系统组件、网络协议栈等相关文件齐全,适合工业控制、智能家居、环境监测等场景的嵌入式开发者参考学习。

1. 从“功能机”到“任务调度”:为什么温湿度天气时钟要跑RT-Thread

STM32F407温湿度天气时钟,最常见的实现是裸机while循环加定时器中断,采集DHT11、读RTC、刷新LCD,看起来够用。但一旦把“天气”做真——通过ESP8266获取网络数据、JSON解析、断网重试,再把这些数据同时刷到TFT屏上,裸机的顺序执行就会出问题:网络recv等待期间,DHT11两秒采样周期被无限拉长;JSON解析卡住时,屏幕刷新也会跟着停顿。RT-Thread把系统拆成多个独立线程,由调度器按优先级和事件激活,网络阻塞只影响天气线程自身,温湿度采集和画面刷新照样稳定。这个标题适合从裸机过渡到RTOS的嵌入式工程师,也适合已经有RT-Thread基础、想看清线程拆分和传感器驱动封装细节的人。下面按工程搭建、传感器线程、RTC与天气、显示与调试四个阶段推进。

2. STM32F407的BSP适配与RT-Thread线程模型

2.1 从BSP拉取工程:RT-Thread Studio与menuconfig配置

常见做法是先打开RT-Thread Studio,新建基于STM32F407的RT-Thread工程。Studio会自动从rt-thread仓库的bsp/stm32f407xx目录拷贝BSP,这时搜索“stm32f407芯片包”发现找不到,多数是因为SDK Manager里芯片支持包版本和RT-Thread版本不匹配。处理办法是在SDK Manager里单独安装STM32F407的支持包,再选择rt-thread 4.1.x源码重新生成工程。如果习惯命令行,在env工具中进入bsp目录,执行scons --target=mdk5也能生成Keil工程,前提是环境变量里已经配置好ARMCC或GCC工具链。

生成工程后第一件事是检查rtconfig.h,确保下面几个宏符合预期:

#define RT_THREAD_PRIORITY_MAX 32 #define RT_USING_HEAP 1 #define RT_HEAP_SIZE 16384

RT_THREAD_PRIORITY_MAX默认是8,对天气时钟来说优先级分得太粗。设备里除了sensor、display、weather三个业务线程,还有finsh、idle、定时器线程,8个优先级很快就不够用。RT_HEAP_SIZE如果低于16KB,在创建消息队列或解析JSON时可能返回ENOMEM错误,这种错误不是直接崩溃,而是表现为后续rt_mq_send一直失败,排查周期很长。另一个容易忽略的点是链接脚本中堆空间要大于RT_HEAP_SIZE,Keil工程里需要同步调整startup文件的Heap_Size,否则堆分配irectly失败。

2.2 线程优先级与栈设计的参数表

天气时钟的线程划分建议如下表:

线程名优先级栈大小(字节)触发方式
rtc_sync101024启动和每天0点
sensor141024每2秒读取一次
display184096信号量/100ms轮询
weather222048每15分钟请求一次

RT-Thread里优先级数值越小越先执行,0到1留给调度器和中断。sensor线程设14,比display高,是为了保证DHT11的40微秒采样窗不被刷屏打断;display线程栈给4096,是因为GUI库在字符串格式化、坐标变换时局部缓冲区开销大;weather线程栈只有2048,因为它大部分时间阻塞在网络recv,实际使用率一般不会超过60%。

如果天气线程解析JSON后栈峰值接近极限,不要急着加栈,先检查是不是把cJSON解析用的缓冲区定义成了局部数组。那类char buf[1024]放在栈上,会立刻吃掉一半栈空间,改用静态数组或者从内存池分配更合理。

2.3 用INIT_APP_EXPORT创建线程的最小代码

线程创建的代码放在独立模块里,用INIT_APP_EXPORT自动注册:

#include <rtthread.h> static rt_thread_t sensor_tid = RT_NULL; static rt_thread_t display_tid = RT_NULL; static void sensor_entry(void *parameter) { while (1) { sensor_sample_and_publish(); rt_thread_mdelay(2000); } } static void display_entry(void *parameter) { while (1) { display_refresh(); rt_thread_mdelay(100); } } static int weatherclock_app_start(void) { sensor_tid = rt_thread_create("sensor", sensor_entry, RT_NULL, 1024, 14, 20); display_tid = rt_thread_create("display", display_entry, RT_NULL, 4096, 18, 20); if (sensor_tid != RT_NULL) rt_thread_startup(sensor_tid); if (display_tid != RT_NULL) rt_thread_startup(display_tid); return 0; } INIT_APP_EXPORT(weatherclock_app_start);

rt_thread_create的第三到第六个参数分别代表入口参数、栈大小、优先级、时间片。时间片只在同优先级线程之间轮转时有意义,给20个tick是通用值。INIT_APP_EXPORT会把weatherclock_app_start放到自动初始化段,系统启动到应用层时自动调用。这样后续新增按键扫描、日志线程,只需要再建一个INIT_APP_EXPORT,不会把main函数越堆越臃肿。

2.4 普遍会踩的栈溢出和hard fault排查

开启RT_USING_DEBUG后,在finsh输入list_thread能看到每个线程的“max used”值,这是栈峰值使用率。display线程如果max used超过90%,建议直接翻倍到8192字节。hard fault多出现在sensor线程,因为DHT11驱动的while等待循环没有超时判断,一旦传感器损坏或接线松了,代码会卡在死循环里,看起来像系统死机。正确写法是给等待电平加入超时计数,连续超时N次后返回错误,并让sensor线程正常进入下一个周期。

提示:线程栈不是设得越大越好,栈越大,SRAM留给消息队列和GUI buffer的空间就越小。用list_thread的max used数据来调参,比拍脑袋设一个8192更有效。

3. 温湿度采集线程:模拟I2C与单总线驱动的封装

3.1 传感器选型:DHT11、HTU21D与接口差异

天气时钟的温湿度来源通常是DHT11或者HTU21D。DHT11用单总线协议,一根线传数据,接线简单,精度正负2℃,适合做室内温度参考。HTU21D走I2C接口,精度高,适合想做室内外温湿度对比的场景。“stm32f407模拟i2c”这个热词说明很多人对硬件I2C不放心,实际在RT-Thread里可以用i2c-bit-bus组件把任意两个GPIO虚拟成I2C总线,注册成标准rt_i2c设备,应用层照常调rt_i2c_transfer,不用改代码。无论选哪种传感器,驱动层都建议做成独立模块,和应用线程通过消息队列交互。

3.2 DHT11读取代码与RTOS时序注意点

DHT11的读取步骤固定:主机拉低至少18毫秒再释放,传感器拉低80微秒响应,随后输出40位数据。在RT-Thread里实现时要注意区分毫秒级等待和微秒级等待:

#define DHT11_PIN GET_PIN(B, 8) static void dht11_start(void) { rt_pin_mode(DHT11_PIN, PIN_MODE_OUTPUT); rt_pin_write(DHT11_PIN, PIN_LOW); rt_thread_mdelay(20); rt_pin_write(DHT11_PIN, PIN_HIGH); rt_pin_mode(DHT11_PIN, PIN_MODE_INPUT_PULLUP); } static int dht11_wait_level(int level, int timeout_us) { while (rt_pin_read(DHT11_PIN) != level) { if (timeout_us-- <= 0) return -1; rt_hw_us_delay(1); } return 0; }

dht11_start里的rt_thread_mdelay(20)会让出CPU,这是允许的。但接下来的微秒级等待电平不能用rt_thread_mdelay,因为它最小延时是一个systick,通常是1毫秒或10毫秒,误差两个数量级。常见做法是用rt_hw_us_delay跑忙等,同时把sensor线程优先级调到比显示线程高,保证采样窗口不被屏幕刷新中断。

读到40位后还需要校验。DHT11数据帧格式是湿度整数加湿度小数加温度整数加温度小数加校验和,校验和等于前四个字节之和的低8位。校验失败直接丢弃本次数据,不要用上一次的值填充显示,否则用户看到温度恒定反而更难排查。

3.3 用消息队列发布温湿度数据

采集线程不要直接调用GUI刷新接口,而是把温度湿度打包成一条小消息塞进消息队列。display线程阻塞在rt_mq_recv上,收到数据后才更新界面。这样采集和显示解耦,显示卡顿时不会影响传感器的采样周期:

struct sensor_payload { int16_t temp_x10; /* 温度*10,避免浮点 */ int16_t humi_x10; }; static rt_mq_t sensor_mq = RT_NULL; static void sensor_sample_and_publish(void) { struct sensor_payload msg; if (dht11_read(&msg.temp_x10, &msg.humi_x10) == 0) { rt_mq_send(sensor_mq, &msg, sizeof(msg)); } } int sensor_thread_init(void) { sensor_mq = rt_mq_create("sensor", sizeof(struct sensor_payload), 8, RT_IPC_FLAG_FIFO); return 0; }

rt_mq_create第三个参数是队列深度8,最多缓存8条采样消息,满了就丢弃最旧的数据。显示线程本来就是可放弃帧的,队列满时丢旧数据比阻塞发送更合理。温度用int16_t存放大10倍后的数值,显示时再除以10,避免嵌入式GUI里频繁做浮点运算。

3.4 HTU21D的模拟I2C接入方式

如果换用HTU21D,接线从单总线变成SCL加SDA两根线。初始化时把GPIO挂到bit ops总线:

static struct rt_i2c_bit_ops htu21d_bit_ops = { .delay_us = 2, .timeout = 100, .scl = GET_PIN(A, 8), .sda = GET_PIN(C, 9), }; rt_device_t i2c2 = rt_i2c_bit_add_bus("i2c2", &htu21d_bit_ops);

执行rt_i2c_bit_add_bus后,RT-Thread的设备列表里就多了一个i2c2。接下来用rt_i2c_transfer发送0xE3启动温度转换,等待50毫秒后读3字节。实际调试时遇到读出来全是0xFF,多半是上拉电阻没接或者SCL和SDA接反,和RTOS无关。如果delay_us设成1,SCL频率接近500kHz,接近HTU21D的极限,建议用2us留出余量。

3.5 基于单总线和I2C的驱动统一设计

“基于stm32的温湿度检测”不只为天气时钟服务,后续可能接入云平台或者控制风扇。RT-Thread的Sensor框架可以把DHT11、HTU21D统一注册为temp_humi设备,业务层不区分协议。需要采集时rt_device_read直接拿到最新温湿度,Sensor框架内部自动管理采样周期。这样V1用DHT11,V2换SHT30,应用代码一行不用改,只替换驱动注册部分,是整个项目里性价比最高的封装。驱动注册前先确认BSP里已经打开了RT_USING_SENSOR和对应的sensor驱动框架,否则rt_device_find会返回空指针。

4. 天气时钟的核心:RTC校时与天气数据获取

4.1 STM32F407的RTC配置与掉电保持

RTC模块有独立电源域,主电源掉电后由VBAT电池继续走时。但很多开发板没焊VBAT电池,掉电后时间会重置。应用初始化时用备份寄存器BKP_DR0做“已初始化”标志:

void rtc_check_and_init(void) { if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0xA5A5) { RTC_TimeTypeDef time = {0}; RTC_DateTypeDef date = {0}; time.Hours = 9; time.Minutes = 0; time.Seconds = 0; date.Year = 25; /* 年偏移量,表示2025年 */ date.Month = 1; date.Date = 1; HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xA5A5); } }

这里最容易出错的是date.Year。HAL库期望的是年份偏移量,2025年传25,而不是2025。从正点原子stm32f407 freertos例程移植到RT-Thread时,经常看到例程里写的年份格式不统一,有人写BCD有人写BIN,混用之后显示年份总是错。建议统一用RTC_FORMAT_BIN,Year传25,避免在BCD和二进制之间来回换算。

4.2 用AT设备和ESP8266获取天气数据

天气数据用ESP8266的AT固件获取,不需要在STM32F407上跑完整LWIP协议栈。menuconfig里打开AT组件、选择ESP8266设备,指定串口号和波特率后,应用层直接用SAL socket访问HTTP:

int weather_fetch(char *buf, int len) { int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in srv; srv.sin_family = AF_INET; srv.sin_port = htons(80); srv.sin_addr.s_addr = inet_addr("120.25.91.66"); struct timeval tv = {5, 0}; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); if (connect(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) { closesocket(sock); return -1; } const char *req = "GET /v3/weather/now.json?key=xxx&location=beijing HTTP/1.1\r\n" "Host: api.seniverse.com\r\n" "Connection: close\r\n\r\n"; send(sock, req, rt_strlen(req), 0); int n = recv(sock, buf, len - 1, 0); buf[n] = '\0'; closesocket(sock); return n; }

代码里SO_RCVTIMEO设为5秒是关键。如果没有这行,ESP8266在WiFi断开时会让recv长时间阻塞,weather线程卡住,其他线程不受影响但天气数据不更新。5秒超时和重试逻辑配合,网络故障最多让weather线程阻塞5秒。这里用的IP是直连,省掉了DNS解析,但IP会变,量产版本建议改成gethostbyname解析域名。

4.3 JSON解析与天气数据缓存

HTTP响应里包含JSON数据,用cJSON解析:

cJSON *root = cJSON_Parse(buf + header_len); cJSON *now = cJSON_GetObjectItem(root, "now"); int temp = cJSON_GetObjectItem(now, "temperature")->valueint; int humi = cJSON_GetObjectItem(now, "humidity")->valueint; snprintf(weather_desc, sizeof(weather_desc), "%s", cJSON_GetObjectItem(now, "text")->valuestring); cJSON_Delete(root);

解析过程中要检查每个cJSON对象是否为空。天气API返回的字段随时可能变化,空指针解引用会直接hard fault。cJSON_Parse用的内存来自RT_HEAP_SIZE,如果堆设小了会返回NULL,表现为天气线程一跑就崩,但传感器和显示线程都正常。解析完成后把结果放入天气缓存结构体,用rt_sem_take加锁保护,防止display线程读取时写入一半。天气线程的执行周期设15分钟一次,连续3次拉取失败进入30分钟退避模式,避免频繁重试。

4.4 时间、天气和传感器数据的合并显示

这个模块负责把三个数据源合并成时钟面板结构体,供显示线程使用。结构体里加一个dirty_bits字段,哪一路更新了数据就置对应位:

#define DIRTY_TIME 0x01 #define DIRTY_WEATHER 0x02 #define DIRTY_SENSOR 0x04 typedef struct { uint8_t hour, minute, second; int16_t local_temp, local_humi; int16_t weather_temp; char weather_text[16]; uint8_t dirty_bits; } clock_panel_t;

dirty_bits由每个写入线程用rt_atomic_or置位,display线程读取后清除。刷新时只重绘dirty_bits里对应的区域:时间每秒变,温湿度每2秒变,天气每15分钟变,完全没有必要每秒全屏刷新。这个结构体配合分区域刷新策略,是天气时钟显示层保持流畅的关键,也让后续增加天气预警、时钟秒表等功能时改动范围最小。

5. TFT显示与调试:分区域刷新、触摸校准与复位溯源

5.1 FSMC接口与分区域刷新

TFT并口屏建议挂到FSMC Bank1区域,刷新速度远高于SPI屏。显示线程每100毫秒循环一次,但不应全屏重绘。LVGL场景下获取label坐标后向外扩8像素,调用lv_obj_invalidate_area让LVGL只把该矩形区域标为脏区:

lv_area_t tiny; tiny.x1 = label->coords.x1 - 8; tiny.y1 = label->coords.y1 - 8; tiny.x2 = label->coords.x2 + 8; tiny.y2 = label->coords.y2 + 8; lv_obj_invalidate_area(lv_scr_act(), &tiny);

温度数字变化时只重绘数字所在矩形,秒数变化时只重绘时间区域。实测在FSMC 25MHz写时序下,局部刷新能维持30帧左右,全屏刷新只有12帧左右,视觉差别明显。

5.2 TFT电阻触摸屏四点校准法的参数保存

电阻触摸屏的非线性较大,两点校准不够稳定。四点校准法在屏幕四角依次显示十字准星,记录触点原始ADC值和对应LCD坐标,求解仿射变换6参数:

typedef struct { float a, b, c; float d, e, f; } touch_calib_t; /* lcd_x = a * adc_x + b * adc_y + c */ /* lcd_y = d * adc_x + e * adc_y + f */

校准完成后把参数写入内部Flash末尾扇区,上电后直接读取。读ADC时连续采样两次取平均值,按压力度尽量和校准时保持一致,电阻屏的物理偏移会随手写压力变化,压力差异大了会出现点不准。

5.3 用list_thread和IWDG做复位溯源

调试阶段保持FINSH打开。出现异常时优先看list_thread输出,如果display线程max used接近100%,把栈翻倍;如果weather线程卡在RT_WAITING_FOREVER状态,去查socket超时是否真的设置成功。产品交付前加IWDG看门狗,天气线程每5秒喂一次,WiFi异常导致喂狗超时后系统自动复位。复位后在启动日志里打印复位原因:

uint32_t csr = RCC->CSR; if (csr & RCC_CSR_IWDGRSTF) { rt_kprintf("reset by iwdg\n"); RCC->CSR |= RCC_CSR_RMVF; }

这样能分辨是断网重试引起的死循环还是普通栈溢出。复位原因字段配合list_thread的栈峰值数据一起看,异常现场足够支撑一次完整的问题定位,不需要反复试错改超时时间。

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

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

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

立即咨询