简介:这是一套面向嵌入式初学者与电子爱好者的STM32综合实践项目源码,围绕实时时间与天气信息的串口屏显示展开,并附带桌面屏幕与倒计时报警功能。项目以STM32微控制器为核心,配合DS1302实时时钟芯片获取准确时间,再通过ESP8266模块联网调用天气API,将温度、湿度等数据实时呈现在串口屏上,用户还可设置倒计时提醒会议、约会或服药等重要事项,适合作为课程设计、毕业设计或DIY桌面时钟的参考方案。资源包共256个文件,约9.06MB,包含45个c源文件与47个h头文件构成的核心驱动与业务逻辑,另有uvprojx、uvoptx等Keil工程文件、axf与hex可执行产物、map与lst编译映射文件,以及若干crf、o、d中间文件,完整保留了从源码到烧录的工程结构。目前已有4163人学习下载,可帮助读者快速理解STM32外设驱动、ESP8266联网通信与串口屏交互的整合思路,并在此基础上二次开发。
1. 桌面时钟屏为什么总在“最后一公里”翻车
很多人做嵌入式小项目,卡住的地方往往不是主控,而是“把东西显示出来”这一步。基于 STM32、ESP8266 和 DS1302 的实时时间天气串口屏显示方案,核心目标很朴素:让一块桌面屏幕同时显示本地走时、网络天气和倒计时报警。它适合已经会点 C 语言、手里有开发板、想把零散模块串成一个完整小系统的人。真正难的不是某个模块单独跑通,而是三件事同时成立:DS1302 走时不能因主循环阻塞而漂,ESP8266 取天气不能把界面卡死,串口屏收指令不能因为字符串拼接越界而花屏。我见过太多方案在实验室亮一次就吃灰,问题基本都出在这三处。下面按“先能跑、再跑稳、最后能长期摆桌上”的顺序拆开讲。
2. 硬件选型与最小系统接线:为什么是这三颗芯片
2.1 STM32 做主机、ESP8266 做联网、DS1302 做走时的分工逻辑
这套组合的合理性在于各司其职。STM32 负责调度、串口协议解析和屏幕刷新;ESP8266 只做一件事——连上热点后发 HTTP 请求拿天气数据,通过串口把结果吐给 STM32;DS1302 用三线接口独立走时,主控复位或断电重启后时间不丢。常见做法是选 STM32F103C8T6 这类资源够用、资料多的型号,主频 72MHz,跑一个前后台框架绰绰有余。
需要提前想清楚的是通信口分配。我一般这样分:USART1 接串口屏,USART2 接 ESP8266,DS1302 用三个普通 GPIO 模拟时序。这样调试时互不干扰,拔掉任何一个模块都不影响另外两个先跑起来。串口屏选常见的 TJC 或同类型指令屏,波特率默认 9600 或 115200,具体以屏的规格为准,接线前先确认电平是 3.3V 还是 5V,避免直接怼上去。
| 模块 | 与 STM32 连接 | 关键参数 | 注意点 |
|---|---|---|---|
| 串口屏 | USART1 TX/RX | 波特率 115200,8N1 | 屏的 RX 接主控 TX,别接反 |
| ESP8266 | USART2 TX/RX | AT 固件,115200 | 供电要能扛 300mA 峰值 |
| DS1302 | 3 个 GPIO | 三线:CE、IO、SCLK | 备份电池别省,否则断电归零 |
2.2 供电与电平:最容易被忽略的翻车点
ESP8266 发射瞬间电流能冲到 300mA 左右,如果和 STM32 共用一颗小电流 LDO,联网时主控可能被拉复位,表现就是“一取天气就重启”。血泪经验是给 ESP8266 单独一路 3.3V,或者至少并一颗 470uF 电解加 0.1uF 陶瓷去耦。DS1302 的备份电池用 CR2032,注意正负极和限流电阻,接反了不仅不走时,还可能发热。
串口屏如果是 5V 供电、3.3V 逻辑,中间要确认它的 RX 是否耐 5V。不确定时串一颗 1k 电阻限流,或者用电平转换。别小看这一步,花屏、乱码、触摸失灵,一半以上是电平和地线没处理好。所有模块共地是底线,地线没接全,串口通信会时好时坏,这种玄学问题最难查。
2.3 最小验证顺序:先让三个模块各自“说话”
不要一上来就写完整逻辑。我的习惯是分三步验证:第一步,STM32 通过 USART1 发一条固定字符串给串口屏,屏幕上能出现文字,说明显示链路通;第二步,USART2 发 AT 指令,收到 OK,说明 ESP8266 通信通;第三步,读 DS1302 秒寄存器,串口打印出来,说明走时通。三步都过了再合并,否则出了问题你根本不知道是哪一段。
/* 分模块自检:依次验证屏幕、WiFi、时钟,任一失败就停在对应阶段 */ void module_self_test(void) { screen_send_string("t0.txt", "BOOT"); /* 屏幕能显示即通过 */ if (esp_at_test() != 0) { /* 发 AT 等 OK */ screen_send_string("t0.txt", "ESP FAIL"); while (1); } if (ds1302_read_sec() > 59) { /* 秒值非法说明时序错 */ screen_send_string("t0.txt", "RTC FAIL"); while (1); } screen_send_string("t0.txt", "ALL OK"); }这段代码的价值在于把故障定位提前。screen_send_string是封装的串口屏指令发送,esp_at_test返回 0 表示收到 OK,ds1302_read_sec读回秒值用于判断时序是否正确。参数上,屏幕控件名要和工程里实际命名一致,AT 等待超时建议给 2 秒,DS1302 读秒后要判断范围,超过 59 基本就是 SCLK 时序或接线问题。
3. DS1302 走时与串口屏刷新:把时间显示做稳
3.1 DS1302 三线时序与 BCD 码转换
DS1302 的数据是 BCD 码,读写都要转换,这是新手最容易写错的地方。写寄存器前要把十进制转 BCD,读回来再转十进制。三线时序里,CE 拉高开始传输,数据在 SCLK 上升沿写入、下降沿读出,低位先出。时序不对的典型现象是读出来全是 0 或全是 0xFF。
/* 十进制转 BCD,DS1302 写入前必须转换 */ static uint8_t dec_to_bcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); } /* BCD 转十进制,读回后使用 */ static uint8_t bcd_to_dec(uint8_t val) { return ((val >> 4) * 10) + (val & 0x0F); } /* 写一个字节到 DS1302,低位先出 */ void ds1302_write_byte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { DS_IO = dat & 0x01; /* 先送最低位 */ DS_SCLK = 1; /* 上升沿写入 */ DS_SCLK = 0; dat >>= 1; } }dec_to_bcd和bcd_to_dec是配套的,缺一个就会显示乱码时间。ds1302_write_byte里先置数据再拉时钟,顺序反了数据就错。SCLK 频率别太高,普通 GPIO 模拟给个几微秒延时足够,太快反而容易丢位。写完后建议回读一次校验,确认写入生效。
3.2 串口屏指令格式与控件绑定
串口屏的显示靠发指令改控件属性,常见格式是控件名.属性=值,指令末尾要跟三个 0xFF 作为结束符。比如给文本控件赋值就是t0.txt="12:30",后面补FF FF FF。控件名必须和屏幕工程里设计的一致,大小写敏感,写错了屏幕没反应但不报错,这种静默失败最坑。
/* 向串口屏发送文本,末尾必须补三个 0xFF 结束符 */ void screen_send_string(const char *obj, const char *str) { char buf[64]; int len = snprintf(buf, sizeof(buf), "%s=\"%s\"", obj, str); HAL_UART_Transmit(&huart1, (uint8_t *)buf, len, 100); uint8_t end[3] = {0xFF, 0xFF, 0xFF}; HAL_UART_Transmit(&huart1, end, 3, 100); }snprintf的缓冲区给 64 字节,足够放常规文本,但天气描述如果很长要评估是否溢出。HAL_UART_Transmit的超时给 100ms,阻塞时间过长会影响主循环,所以屏幕刷新不要放在高频任务里。三个 0xFF 是协议要求,少一个屏幕就把后续数据当内容,直接花屏。
3.3 主循环调度:别让刷新把走时拖漂
如果主循环里又是读时钟又是发屏幕又是等 WiFi,时间显示会明显卡顿。常见做法是用一个 1 秒的软件节拍,只在节拍到的时候读一次 DS1302 并刷新时间控件,天气请求放到更低频的任务里,比如每 10 分钟一次。这样主循环大部分时间在空转,走时和界面都顺。
/* 1 秒节拍刷新时间,10 分钟节拍请求天气,互不阻塞 */ void main_loop(void) { static uint32_t last_tick = 0, last_weather = 0; uint32_t now = HAL_GetTick(); if (now - last_tick >= 1000) { last_tick = now; rtc_update_display(); /* 读 DS1302 并刷屏 */ } if (now - last_weather >= 600000) { last_weather = now; weather_request_async(); /* 非阻塞发起天气请求 */ } esp_poll_response(); /* 轮询 ESP 返回,不干等 */ }HAL_GetTick提供毫秒计数,两个独立时间戳分别控制刷新频率。weather_request_async只负责发指令,不阻塞等待,esp_poll_response在每轮循环里检查有没有数据回来。这样即使网络慢,界面时间照走不误。参数上,天气间隔 600000ms 是 10 分钟,太频繁会被服务端限流,太慢天气没意义。
4. ESP8266 取天气与倒计时报警:把联网和交互接上
4.1 ESP8266 AT 指令联网与 HTTP 请求流程
ESP8266 用 AT 固件时,联网流程是固定几步:复位、设模式、连热点、建 TCP 连接、发 HTTP 请求、读响应、关连接。每一步都要等对应返回,不能一把梭全发出去。常见做法是封装一个带超时的等待函数,匹配到预期关键字才继续。
/* 等待 ESP8266 返回指定关键字,超时返回 -1 */ int esp_wait_for(const char *keyword, uint32_t timeout_ms) { uint32_t start = HAL_GetTick(); while (HAL_GetTick() - start < timeout_ms) { if (esp_rx_contains(keyword)) { return 0; } } return -1; /* 超时,调用方据此重试或报错 */ } /* 完整取天气流程,任一步失败就重来 */ int weather_fetch(void) { if (esp_send_cmd("AT+CIPSTART=\"TCP\",\"api.example.com\",80", 5000) != 0) return -1; if (esp_send_cmd("AT+CIPSEND=48", 2000) != 0) return -1; esp_send_raw("GET /weather?city=demo HTTP/1.1\r\nHost:api.example.com\r\n\r\n"); if (esp_wait_for("+IPD", 8000) != 0) return -1; parse_weather_from_rx(); /* 从响应里提取温度和描述 */ esp_send_cmd("AT+CIPCLOSE", 2000); return 0; }esp_wait_for是整套流程的骨架,超时时间按步骤给:建连接 5 秒,发数据 2 秒,等响应 8 秒。AT+CIPSEND后面的长度要和实际发送字节数一致,写少了数据被截断,写多了会等不到发送提示。parse_weather_from_rx负责从原始响应里抠出字段,建议用字符串查找而不是完整 JSON 解析,省资源也够用。
4.2 天气数据解析与显示字段映射
返回的数据里通常有温度、天气描述、湿度。解析时先定位关键字,再取后面的数值。注意响应里可能有多次+IPD分片,要拼完整再解析。显示上,温度放一个文本控件,描述放另一个,湿度可选。字段映射要和屏幕工程里的控件名一一对应,改一个地方要同步改另一个。
/* 从接收缓冲里提取温度,返回整数摄氏度 */ int parse_temperature(const char *rx) { const char *p = strstr(rx, "\"temp\":"); if (p == NULL) return -999; /* 未找到,返回非法值 */ p += 7; return atoi(p); /* 简单取整,够显示用 */ }strstr定位字段起点,偏移 7 是"temp":的长度,atoi直接转整数。返回 -999 作为非法标记,调用方判断后显示--而不是错误数字。如果天气接口字段名不同,改查找关键字即可,逻辑不变。这种解析方式对格式变化不敏感,比完整 JSON 库更省 Flash。
4.3 倒计时报警的状态机与蜂鸣器联动
倒计时报警要处理三个状态:设置、运行、触发。用一个状态变量加剩余秒数就能管住。运行时每秒减一,减到 0 触发蜂鸣器和屏幕提示,用户按键可复位。关键是倒计时不能和天气请求互相阻塞,所以减一操作放在 1 秒节拍里,和时钟刷新共用节拍。
/* 倒计时状态机:IDLE 设置,RUN 运行,ALARM 触发 */ void countdown_tick(void) { switch (cd_state) { case CD_RUN: if (cd_remain > 0) { cd_remain--; screen_send_number("n1.val", cd_remain); } else { cd_state = CD_ALARM; buzzer_on(); screen_send_string("t2.txt", "TIME UP"); } break; case CD_ALARM: if (key_pressed()) { /* 按键消警 */ buzzer_off(); cd_state = CD_IDLE; screen_send_string("t2.txt", "READY"); } break; default: break; } }cd_state控制流程,cd_remain存剩余秒数,screen_send_number更新数字控件。触发后进入CD_ALARM并开蜂鸣器,只有按键才退出,避免自动消失让人错过。蜂鸣器建议用三极管驱动,GPIO 直接带可能电流不够。倒计时和时钟共用节拍,代码简单也不打架。
5. 避坑与排查:这几处不处理,摆桌上三天就出问题
5.1 时间越走越慢或复位后归零
现象是运行几小时后时间比标准慢几分钟,或者断电重启后回到初始值。原因通常是主循环里有阻塞操作,读 DS1302 的节拍被拖长;归零则是备份电池没装或没电。解决是把耗时操作移出节拍任务,天气请求改成非阻塞轮询;同时检查电池电压,低于 2.5V 就换,电池座接触不良也会导致归零。
5.2 联网时屏幕卡住或主控重启
现象是一取天气界面就不动,严重时直接重启。原因是 ESP8266 峰值电流把电源拉垮,或者等待响应用了死循环阻塞。解决是给 ESP8266 独立供电并加大去耦电容,等待函数一律带超时,超时就放弃本次请求下轮再来。别用while(!flag)这种没有退路的写法,网络不稳时必翻车。
5.3 串口屏花屏、乱码或控件不更新
现象是屏幕出现乱码,或者发了指令控件没反应。原因有三类:指令末尾三个 0xFF 缺失,控件名和工程不一致,串口波特率不匹配。解决是抓串口波形确认结束符,逐字核对控件名大小写,确认主控和屏的波特率一致。花屏还可能是地线没共好,补一根地线往往就好了。
5.4 天气字段解析出错误数值
现象是温度显示成 0 或很大的数。原因是响应分片没拼完整就解析,或者字段关键字匹配到了别的位置。解决是先把多次+IPD的数据拼进同一个缓冲,确认收到完整响应再解析;关键字尽量带引号匹配,减少误命中。解析失败时显示占位符,不要显示错误数字误导自己。
5.5 倒计时触发后蜂鸣器不响或一直响
现象是到点没声音,或者响了停不下来。原因是蜂鸣器驱动电流不足,或者状态机没退出报警态。解决是加三极管驱动并确认极性,按键消警逻辑要确保能回到 IDLE。如果一直响,检查按键扫描是否被其他任务挡住,把按键检测放在高频轮询里。
6. 长期稳定运行的一个关键技巧:给系统加一层“看门狗式”自恢复
把东西做出来只是开始,能连续摆桌上几周不出问题才算成。我的习惯是在主循环里加一个轻量的自恢复机制:记录每个关键任务的最后完成时间,超时未完成就重新初始化对应模块,而不是整机重启。这样网络断了只重连网络,屏幕没响应只重发初始化,时间照走,体验好很多。
/* 任务心跳监控:超时则单独复位对应模块 */ typedef struct { uint32_t last_ok; /* 上次成功时间 */ uint32_t timeout; /* 允许的最大间隔 */ void (*recover)(void); /* 恢复函数 */ } task_monitor_t; static task_monitor_t monitors[] = { {0, 30000, esp_reconnect}, /* 网络 30 秒无成功就重连 */ {0, 5000, screen_reinit}, /* 屏幕 5 秒无刷新就重发初始化 */ }; void monitor_tick(void) { uint32_t now = HAL_GetTick(); for (int i = 0; i < 2; i++) { if (now - monitors[i].last_ok > monitors[i].timeout) { monitors[i].recover(); monitors[i].last_ok = now; /* 复位后重置计时 */ } } }这段代码把“哪个模块出问题”和“怎么恢复”解耦。last_ok在每次任务成功时更新,timeout按模块特性给:网络给 30 秒,屏幕给 5 秒。recover指向对应的重连或重初始化函数。注意恢复后要重置last_ok,否则会连续触发。这个结构不复杂,但能挡掉大部分偶发故障,比整机看门狗温和得多。
验证方法也简单:手动拔掉 ESP8266 电源,看系统是否在 30 秒后自动重连;把屏幕线拔了再插,看是否自动恢复显示。能通过这两项,基本可以放心长期运行。参数上,超时别设太短,网络波动是常态,设太短会频繁重连反而更不稳。
我踩过最深的坑是早期把所有逻辑塞进一个死循环,网络一卡整个系统就僵住,时间也不走了。后来改成节拍加超时加自恢复,同样的硬件,稳定性完全不一样。嵌入式项目里,留退路比跑得快重要得多。希望帮到你。
本文还有配套的精品资源,点击获取