
简介这是一套面向嵌入式开发者的STM32F407RT-Thread温湿度天气时钟完整工程包采用模块化设计实现环境温湿度采集、实时天气预报与时钟显示可应用于智能家居、环境监测、农业温室等场景。资源共2000个文件压缩包约15MB主体为C语言源码与头文件同时包含Sconscript/Kconfig构建配置脚本、Python辅助脚本、Markdown与TXT说明文档、GIF演示等目录结构清晰。已有332人学习浏览适合需要参考完整RTOS项目架构或在此基础上做功能扩展的开发者。借助文档可快速搭建开发环境示例代码演示了RT-Thread线程调度、传感器驱动及网络接口的实际用法演示动图直观展示LCD显示效果模块化代码便于后续替换传感器或接入更多天气数据源也可根据实际需求裁剪系统模块降低二次开发成本能够帮助开发者从零开始理解RT-Thread与STM32的整合方式。1. 基于 STM32F407 和 RT-Thread 的天气时钟这不是一条简单例程把“基于STM32F407的RT-Thread系统温湿度天气时钟”这串标题拆开你会发现它其实是三类工程问题的合体F407 做主控、RT-Thread 做实时内核、温湿度加天气数据做业务。很多人觉得桌面天气时钟就是把传感器读一下、屏刷一下但真正动手时DHT22 或 SHT30 的采样时序、ESP8266 网络包的闪烁到达、TFT 触摸屏按下时打断绘制这些不可控因素会让裸机轮询代码迅速膨胀成面条工程。RT-Thread 的价值在于用线程、信号量、消息队列把“实时采集”和“非阻塞网络”分开让每部分都能独立调试。这篇文章面向已经会点灯但想把 RTOS 落到小项目的工程师按从 BSP 移植到显示校准的完整路径把每个环节的命令、参数和坑位讲清楚读完就能照着拼出一台自己的天气时钟。2. 移植 RT-Thread 到 STM32F407BSP、scons 与最小线程2.1 选官方 BSP 而不是从零移植STM32F407 的 RT-Thread 移植难度比 L0 系列低很多因为官方仓库里已经有现成的bsp/stm32/stm32f407xx工程里面预置了芯片启动文件、时钟树、UART、GPIO、SPI、I2C 等基础驱动。这个 BSP 本质上就是 stm32f407 芯片包与驱动库的绑定产物你直接在这上面加业务比对着参考手册写寄存器更靠谱。我见过很多“基于正点原子 STM32F407 FreeRTOS 例程”改来的工程作者把 FreeRTOS 的队列和任务删掉再硬塞 RT-Thread不仅两个调度器的中断优先级冲突连SysTick都用不对。正规做法是下载 RT-Thread 源码在 BSP 目录下复制一份stm32f407xx改名后修改笼统管脚配置再开始应用开发。2.2 用 menuconfig 裁剪出最小系统先说环境准备Windows 上装 Env 工具Linux 上装arm-none-eabi-交叉编译器然后确保 Python 和scons可用。进入 BSP 目录后执行scons --menuconfig弹出的图形菜单里需要重点勾选这几项RT_USING_TIMER_SOFT用于网络超时重连RT_USING_SENSOR是温湿度传感器设备框架RT_USING_POSIX给 socket 操作提供底层支撑RT_USING_LIBC让printf、malloc工作正常。时钟频率、晶振、调试串口号在board.h里修改保存后执行scons --targetmdk5 -j4这会生成 Keil 工程文件。如果你习惯用 GCC就执行scons -j4直接编译。注意scons --menuconfig会重写rtconfig.h你自己加的宏如果没有写明依赖会被覆盖丢失所以客户化配置尽量用SConscript里的CPPDEFINES或独立头文件。2.3 创建第一个应用线程RT-Thread 的main只是一个特殊线程不该把业务全塞进去。更常见的做法是在main里只创建应用线程然后退出。代码如下#include rtthread.h static rt_thread_t app_tid; static void app_entry(void *parameter) { rt_kprintf(WeatherClock app running\r\n); while (1) { rt_thread_mdelay(1000); } } int main(void) { app_tid rt_thread_create(app, app_entry, RT_NULL, 2048, 10, 20); if (app_tid ! RT_NULL) { rt_thread_startup(app_tid); } return RT_EOK; }参数需要逐一说明app是线程名调试时用来检索线程app_entry是入口函数指针第三个参数为入口参数线程之间传结构体时用得上这里给空2048是线程专用栈大小字节为单位对初始化阶段足够但后续加网络、文件系统时得扩大到 4096 或更高10是线程优先级RT-Thread 中数字越小优先级越高主线程优先级默认是MAIN_THREAD_PRIORITY通常为 10最后一个20是时间片长度以系统节拍为单位同优先级下两个线程互让时的切换粒度就在这里体现。2.4 线程栈、堆和内存分配策略线程栈大小不是拍脑袋定的RT-Thread 官方调试手段里最有用的一个就是list_thread()。打开rtconfig.h中的RT_USING_HEAP、RT_USING_DEBUG启动后用串口终端执行list_thread()输出如下thread prio status sp stack size max used left tick error app 10 ready 0x00 0x00000800 0x00000214 0x00000000 0x000这里的max used是栈历史最高占用字节数如果接近stack size就把线程栈加大。千万不要把 RT-Thread 的堆只放在默认的 RAM 区域STM32F407 有 64KB 的 CCM RAM不能直接用于 DMA如果你把网络缓冲区分配到 CCMESP8266 或 LCD 的 DMA 传输会读到废数据。我一般把 CCM 留给快速运算变量而堆、线程栈全部放在常规 SRAM。配置项取值说明优先级5 ~ 30实时性高的线程值小普通显示线程值大时间片10 ~ 50同优先级线程轮转的 tick 数栈大小1024 ~ 4096以 4 字节对齐常备余量 20%另外一个关键点是空闲线程钩子rt_thread_idle_sethook。你可以在空闲钩子中做低功耗检查或按键扫描但钩子函数里不要调用会阻塞的 API否则系统就永远无法进入空闲态。3. 温湿度传感器接入模拟 I2C 与传感器驱动框架3.1 为什么模拟 I2C 比硬件 I2C 更受欢迎STM32F407 自带两个硬件 I2C 外设但社区里搜索“stm32f407 模拟 i2c”的热度远高于硬件 I2C原因在于硬件 I2C 在监听状态和非字节对齐中断组合下偶尔会卡死在 BUSY 状态看门狗不喂就死机。模拟 I2C 用两个 GPIO 引脚按标准时序翻转不依赖外设状态机出问题一眼就能从逻辑分析仪看到。对于 SHT30 这种只用到 I2C 标准模式或快速模式的传感器模拟方式完全足够。模拟 I2C 需要关注的是引脚编号与 GPIO 复用。推荐先给两条线接 4.7kΩ 上拉然后定义引脚#define SCL_PIN GET_PIN(A, 8) #define SDA_PIN GET_PIN(A, 9)驱动初始化时把两个引脚设置为开漏输出并打开内部上拉。这样在读取从机数据时不需要频繁切换输入模式开漏输出在释放总线后会被外部上拉拉高逻辑自然就变成了输入状态。3.2 模拟 I2C 的核心时序与读写函数起始信号是 SCL 高电平期间 SDA 从高变低停止信号是 SCL 高电平期间 SDA 从低变高。示例代码static void i2c_start(void) { rt_pin_write(SDA_PIN, PIN_HIGH); rt_pin_write(SCL_PIN, PIN_HIGH); rt_pin_write(SDA_PIN, PIN_LOW); rt_pin_write(SCL_PIN, PIN_LOW); } static void i2c_stop(void) { rt_pin_write(SDA_PIN, PIN_LOW); rt_pin_write(SCL_PIN, PIN_HIGH); rt_pin_write(SDA_PIN, PIN_HIGH); }注意时序之间的间隔普通 GPIO 翻转很快但 SCL 频率不要超过 400kHz如果主频 168MHz需要在每次电平翻转后插入 1~2 微秒延时。数据位在 SCL 低电平期间更新在 SCL 高电平期间采样务必保证 SDA 在 SCL 上升沿前至少 100ns 稳定。SHT30 的常用命令如下命令取值说明单次测量高重复性0x2C 0x06默认测量命令周期测量 10Hz0x21 0x30适合实时刷新软复位0x30 0xA2传感器无响应时恢复读取状态寄存器0xF3 0x2D检查上电状态位用上述命令发送后等待 15ms 再读取 6 字节数据其中前两个字节是温度第三个字节是检验字节第四第五字节是湿度。3.3 把 SHT30 挂到 RT-Thread sensor 框架直接写一个裸驱动函数sht30_read()也能用但不如接入 RT-Thread 的 sensor 框架因为上层可以统一用rt_device_find和rt_device_read读取不用管 I2C 还是 SPI。实现 sensor 设备时需要填充rt_sensor_ops结构体static rt_ssize_t sht30_polling_get_data(rt_sensor_t sensor, struct rt_sensor_data *data) { uint16_t temp_raw, humi_raw; sht30_measure(temp_raw, humi_raw); >esp8266_send_cmd(AT\r\n, OK, 1000); esp8266_send_cmd(ATCWMODE1\r\n, OK, 500); esp8266_send_cmd(ATCWJAP\ssid\,\pwd\\r\n, WIFI GOT IP, 10000); esp8266_send_cmd(ATCIPSTART\TCP\,\api.seniverse.com\,80\r\n, CONNECT, 5000);每个esp8266_send_cmd内部会等待期望回包等待期间不断从串口缓冲读取。重点在于响应不是一次性到达的例如连接 AP 会先返回WIFI CONNECTED然后过几秒才返回WIFI GOT IP。所以代码中匹配“WIFI GOT IP”时必须允许缓冲区内含有之前的 CONNECTED 文本采取“循环读取直到超时并检查关键字”的方式而不是简单的等完整行。连接 TCP 后发送 HTTP GET 请求static const char http_get[] GET /v3/weather/now.json?keyYOUR_KEYlocationbeijing HTTP/1.1\r\n Host: api.seniverse.com\r\n Connection: close\r\n\r\n; esp8266_send_cmd(ATCIPSEND..., SEND OK, 2000); esp8266_data_send(http_get, sizeof(http_get) - 1);注意ATCIPSEND后面需要填一个十进制长度值这里的长度是请求字符串实际的字节数不能写sizeof(http_get)因为里面有字符串终止符。发送完等待SEND OK之后 ESP8266 会主动推送接收到的服务器响应。AT 指令作用需要匹配的返回AT测试模块响应OKATCWMODE1设置为 STATION 模式OKATCWJAPssid,pwd连接 APWIFI GOT IPATCIPSTART...建立 TCP 连接CONNECT OKATCIPSENDn发送 n 字节数据SEND OK4.3 接收区拼接与 cJSON 解析HTTP 响应会分段到达不能用一次rt_device_read就把 payload 拿全。我通常在串口接收中断里往一个全局环形缓冲写字节天气网络线程每 200ms 检查一次环形缓冲用rt_strstr找\r\n\r\n找到后认为 HTTP 头结束后续数据就是 JSON 正文。如果正文不完整继续等到完整闭合大括号或检测到}。拿到 JSON 后使用 cJSON 库解析。cJSON 在 RT-Thread 中有软件包直接env添加cJSON即可。解析代码如下cJSON *root cJSON_Parse(payload); if (root RT_NULL) { log_e(weather parse error); return; } cJSON *now cJSON_GetObjectItem(root, now); cJSON *temp cJSON_GetObjectItem(now, temp); cJSON *text cJSON_GetObjectItem(now, text); rt_snprintf(weather_buf.temp_size, %s.%s, temp-valuestring, ℃); rt_snprintf(weather_buf.text_size, %s, text-valuestring); cJSON_Delete(root);这里有两个易错点一是temp-valuestring是char *它指向的是 cJSON 内部分配的内存cJSON_Delete(root)之后该指针立即失效所以必须在 Delete 前用rt_snprintf拷贝到自己的结构体二是要防止text字段为RT_NULL某些天气接口在极端情况下不返回该字段代码里要判断。4.4 天气刷新频率与断线重连公共天气 API 的免费额度一般不允许频繁请求30 分钟一次的刷新足够。断线重连不能无脑循环否则 WiFi 长时间连不上会让整个系统卡死。重连逻辑加状态机#define WEATHER_WAIT_AP (30 * RT_TICK_PER_SECOND) static void weather_thread_entry(void *p) { while (1) { if (esp8266_is_connected()) { fetch_weather_report(); } else { esp8266_reconnet_ap(); } rt_thread_mdelay(30 * 60 * 1000); } }等待 AP 连接时用信号量挂起而不是阻塞在数组循环里这样其他线程照常调度。如果你在串口调试里看到at thread卡住那多半是信号量永久挂起可以针对信号量设置一个最大等待 tick超时后返回错误并重新初始化串口。5. 显示线程与触摸校准TFT 上的天气时钟界面5.1 显示线程设计与局部刷新显示线程是“天气时钟”的门面负责把时间、温度、湿度、天气文字画到 TFT 上。我一般把显示线程优先级设为 20比温湿度线程低比空闲线程高。线程循环里用消息队列接收传感器和网络数据同时维护一个 1 秒计时器刷新时钟。while (1) { if (rt_mq_recv(display_mq, disp_data, 500) RT_EOK) { draw_weather_area(disp_data); } if (rt_tick_get() - last_tick RT_TICK_PER_SECOND) { draw_clock_now(); last_tick rt_tick_get(); } }rt_mq_recv的超时设为 500ms这样数据包没到时线程不至于一直阻塞还可以顺便处理按键轮询。绘制时不要整屏清色只需重画数字区域。例如秒位数字每一次只擦除原来显示old_sec的矩形再写入new_sec。否则 240x320 的全屏写点耗时约 30ms1 秒 60 次CPU 占用会高达 30% 以上。5.2 STM32F407 TFT 电阻触摸屏的四点校准法触摸屏校准是“STM32F407 TFT 电阻触摸屏 四点校准法”这个关键词背后的核心需求。电阻屏的 X、Y 采样值与屏幕像素坐标呈线性关系但存在平移、旋转和缩放偏差所以需要解一个六参数仿射变换Xs a * X b * Y c Ys d * X e * Y f用户依次点击屏幕四个角落比如左上、右上、左下、右下代码记录每次点击时的 X/Y ADC 值和预设的屏幕坐标 Xs/Ys。用线性代数求解系数。实际工程中为了减少抖动每点连续采样 8 次去掉最大最小值求平均。然后计算变换后的坐标int32_t screen_x calib_a * raw_x calib_b * raw_y calib_c; int32_t screen_y calib_d * raw_x calib_e * raw_y calib_f;校准完成后立即做一次验证在屏幕中心画一个十字用触摸笔点击看显示坐标是否落在中心 ±5 像素内。如果偏差大检查触摸面板的 X 轴与 Y 轴是否接反或者驱动内 AD 值的极性是否反向。校准系数要保存到backup域或 SPI Flash掉电后重新读取否则每次开机都校准会烦死人。5.3 显示区域的划分与内容更新策略建议把屏幕划分成固定区域避免相互覆盖。区域坐标 (x, y)尺寸 (w x h)刷新内容时间区域(0, 0)240 x 80时分秒每秒刷新温度区域(0, 80)120 x 50温度值天气更新时湿度区域(120, 80)120 x 50湿度值天气更新时天气文本(0, 130)240 x 50晴天/多云/雨等状态栏(0, 180)240 x 20WiFi 连接图标时间区域每秒刷新天气区域 30 分钟才刷新一次所以时间刷新闪屏不会影响天气。如果你的液晶是 SPI 接口尽量用 DMA 发送数据避免rt_spi_transfer在等待期间被中断打断。RT-Thread 的 SPI 设备框架支持 DMA在初始化时设置消息头带RT_DEVICE_FLAG_DMA即可但需要确保 DMA 描述符连续并排不然数据长度超过环形描述符长度会丢数据。6. 低功耗、看门狗与四项排错技巧6.1 基于 PM 组件的省电做法桌面天气时钟大多插电运行功耗不是重点但如果是电池版本RT-Thread 的 PM 组件是必备。使用rt_pm_request(RT_PM_STOP)可以让 MCU 进入 STOP 模式电流从几十毫安降到微安级。但要注意STOP 模式下内核 SysTick 也会停止所有依赖 tick 的线程都会被冻结所以省电代码必须设计成检测到无按键、无触摸、无网络数据时先挂起所有低优先级线程再请求 STOP有唤醒源时立即释放 PM 请求恢复运行。6.2 独立看门狗防止死机网络线程如果因为某种原因永远停在ATCIPSTART等待上整个时钟就会变成砖头。RT-Thread 看门狗设备可以解决。rt_device_t wdt rt_device_find(wdg); rt_device_init(wdt); rt_device_control(wdt, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, (void *)5); while (1) { net_process_blocking_step(); rt_device_control(wdt, RT_DEVICE_CTRL_WDT_KEEPALIVE, RT_NULL); }看门狗喂狗一定要放在最有可能卡死的地方。不要放在显示线程里因为显示线程很稳定而网络线程卡住时看门狗会被显示线程一次次喂饱永远复不了位。正确的喂狗位置是网络线程每一次完整轮询之后这样一旦 AT 等待超时死锁看门狗才能触发硬件复位。6.3 四个高频排查点和串口手段第一list_thread()查看线程栈是否溢出max used超过栈大小的 80% 就调大。第二list_device()检查sht30设备是否注册成功经常有人忘记在板级初始化里调用rt_hw_sht30_init()。第三串口输出乱码时优先查RT_CONSOLE_BAUDRATE是不是 115200而不是去查线路。第四cJSON 解析结构体导致 hard fault 时打开RT_USING_MEMTRACE看是不是在cJSON_Delete后再访问了原来的valuestring。调试状态下使用 Proteus8 仿真可以快速验证外设接线。搜索“proteus8 stm32f407 离线元件库”能下载到包含常见 LCD、SHT30、ESP8266 的模型在没有硬件时先把 I2C 读数和网络交互的逻辑跑通。这个方法能省掉一半的反复烧写时间。本文还有配套的精品资源点击获取