资讯动态

基于RT-Thread的STM32F407温湿度天气时钟设计与实现

发布时间:2026/9/10 16:07:55 来源:尧图企业网站定制
简介面向嵌入式开发者的STM32F407RT-Thread温湿度天气时钟完整工程包将环境温湿度监测、网络天气获取与实时时钟显示集成于一体适合学习RTOS移植、外设驱动、网络协议栈和物联网应用的开发者作为实战参考。压缩包共2000个文件以C/C源码为主877个h与836个c另有SConscript构建脚本、Kconfig配置、Python脚本、Markdown文档、演示视频与图片等覆盖驱动、应用、中间件和文档多个层级。包体约14.93MB文件类型丰富但结构清晰代码模块化、注释统一便于按需裁剪二次开发。项目已提供系统架构设计、软件模块说明、硬件接口定义以及演示视频能帮助快速搭建开发环境、理解线程调度和传感器采集流程。已有332人浏览学习从内容预览可见HAL库驱动、文件系统组件、网络协议栈等相关文件齐全适合工业控制、智能家居、环境监测等场景的嵌入式开发者参考学习。1. 从“功能机”到“任务调度”为什么温湿度天气时钟要跑RT-ThreadSTM32F407温湿度天气时钟最常见的实现是裸机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 --targetmdk5也能生成Keil工程前提是环境变量里已经配置好ARMCC或GCC工具链。生成工程后第一件事是检查rtconfig.h确保下面几个宏符合预期#define RT_THREAD_PRIORITY_MAX 32 #define RT_USING_HEAP 1 #define RT_HEAP_SIZE 16384RT_THREAD_PRIORITY_MAX默认是8对天气时钟来说优先级分得太粗。设备里除了sensor、display、weather三个业务线程还有finsh、idle、定时器线程8个优先级很快就不够用。RT_HEAP_SIZE如果低于16KB在创建消息队列或解析JSON时可能返回ENOMEM错误这种错误不是直接崩溃而是表现为后续rt_mq_send一直失败排查周期很长。另一个容易忽略的点是链接脚本中堆空间要大于RT_HEAP_SIZEKeil工程里需要同步调整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设成1SCL频率接近500kHz接近HTU21D的极限建议用2us留出余量。3.5 基于单总线和I2C的驱动统一设计“基于stm32的温湿度检测”不只为天气时钟服务后续可能接入云平台或者控制风扇。RT-Thread的Sensor框架可以把DHT11、HTU21D统一注册为temp_humi设备业务层不区分协议。需要采集时rt_device_read直接拿到最新温湿度Sensor框架内部自动管理采样周期。这样V1用DHT11V2换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_BINYear传25避免在BCD和二进制之间来回换算。4.2 用AT设备和ESP8266获取天气数据天气数据用ESP8266的AT固件获取不需要在STM32F407上跑完整LWIP协议栈。menuconfig里打开AT组件、选择ESP8266设备指定串口号和波特率后应用层直接用SAL socket访问HTTPint 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?keyxxxlocationbeijing 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的栈峰值数据一起看异常现场足够支撑一次完整的问题定位不需要反复试错改超时时间。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价