任务介绍
本项目实现了2025贸泽电子M-Design创意设计竞赛的方向三,使用STM32F407-Discovery开发板、AtomS3-Lite开发板与VFD显示屏,实现了基于VFD显示屏与LVGL的自动校时电子钟。
硬件平台
首先介绍本次用到的开发板:STM32F407-Discovery开发板,这是一块对于ST蝴蝶粉来说非常熟悉的开发板,它的主控是STM32F407,虽然推出的时间已经比较久,但仍然凭借着丰富的片内资源和板载外设,成为一个非常适合做原型验证的利器。开发板集成了比较丰富的外设,例如加速度计、数字麦克风、音频输出、按键、LED、USB接口。在软件方面,由于板卡非常出名,生态已经适配好了Arduino IDE、PlatformIO、RT-Thread和MicroPython等多种开发平台,上手开发会很方便。本次我会使用这块STM32F407-Discovery开发板,做一个基于VFD显示屏与LVGL的自动校时电子钟。
- 主控设备:STM32F407-Discovery开发板
- 搭载ARM Coretex-M4微处理器
- 硬件串口通信接口
- LVGL图形库驱动
- 网络模块:AtomS3-Lite
- 基于ESP32-S3FN8
- 可编程按键
- MicroPython平台支持
- 显示模块:GU256X128C
- VFD显示
- 高可见范围
- 256x128单色像素
任务分析与实现
这次主办方出了四种方向,我选择的是方向三:无线通信、物联网。
方案框图:
- 时间源层:ESP32 NTP授时
- 基于pool.ntp.org公共时钟源
- 5秒同步周期(可配置)
- 三重握手校验机制
- 时间字符串定时同步
- 主控系统
- 校准时间串口解析
- 时钟维护
- VFD屏幕驱动
代码详解
本次项目涉及到STM32主控代码与ESP32网络时间代码,接下来分别讲解:
STM32主控代码
主控软件流程图:
流程图关键节点说明:
- 硬件初始化阶段
- 配置Serial2串口波特率(115200bps)
- 分配LVGL显示缓冲区(WIDTH*HEIGHT/8=4KB)
- 注册自定义显示驱动
disp_flush
- 界面构建流程
void oledClockDisplay() {
// Create status label
status_label = lv_label_create(lv_scr_act());
lv_obj_align(status_label, LV_ALIGN_TOP_LEFT, 5, 5);
lv_obj_set_style_text_font(status_label, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(status_label, lv_color_white(), 0);
// Create time label
time_label = lv_label_create(lv_scr_act());
lv_obj_align(time_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_font(time_label, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(time_label, lv_color_white(), 0);
// Create date label
date_label = lv_label_create(lv_scr_act());
lv_obj_align(date_label, LV_ALIGN_CENTER, 0, 40);
lv_obj_set_style_text_font(date_label, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(date_label, lv_color_white(), 0);
// Create week label
week_label = lv_label_create(lv_scr_act());
lv_obj_align(week_label, LV_ALIGN_TOP_RIGHT, -5, 5);
lv_obj_set_style_text_font(week_label, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(week_label, lv_color_white(), 0);
} - 创建四级文本标签(时间/日期/星期/状态)
- 设置Montserrat系列字体(48pt/14pt)
- 对齐方式优化(居中/边角)
- 主循环逻辑
- 串口协议处理
- 32字节环形缓冲区
- ASCII可打印字符过滤
- 自动时区补偿(UTC+8)
void parseAndSetTime() {
struct tm tm;
if (sscanf(timeBuffer, "%d-%d-%d %d:%d:%d",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) {
tm.tm_year -= 1900; // tm_year是从1900开始的年数
tm.tm_mon -= 1; // tm_mon范围0-11
// 转换为时间戳(时区处理)
time_t t = mktime(&tm);
if (t != -1) {
setTime(t); // 设置系统时间
Serial.println("Time updated successfully");
}
}
}
- 显示刷新机制
void loop() {
// 处理串口数据
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || bufferIndex >= sizeof(timeBuffer)-1) {
timeBuffer[bufferIndex] = '\0';
parseAndSetTime();
bufferIndex = 0;
} else if (c >= 32 && c <= 126) { // 只接受可打印ASCII字符
timeBuffer[bufferIndex++] = c;
}
}
static uint32_t last_update = 0;
if(millis() - last_update >= 1000) {
update_clock_display();
last_update = millis();
}
lv_timer_handler();
delay(5);
} - NTP自动对时
- 5ms自动刷新
- 显示刷新机制
ESP32网络时间代码
主控软件流程图:
流程图关键节点解析
1. 网络连接模块
def connect_wifi():
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("Connecting to WiFi...")
sta_if.active(True)
sta_if.connect(WIFI_SSID, WIFI_PASSWORD)
for _ in range(20): # 20秒连接超时
if sta_if.isconnected():
break
utime.sleep(1)
if sta_if.isconnected():
print("Connected! Network config:", sta_if.ifconfig())
else:
print("Connection failed!")
return sta_if
2. NTP同步核心
def sync_ntp():
for retry in range(3): # 最多重试3次
try:
ntptime.settime()
print("NTP sync successful")
return True
except Exception as e:
print("NTP sync failed, retrying...", retry+1)
utime.sleep(2)
return False
3. 通信协议层
UART数据帧结构:
def format_time(time_tuple):
# 格式化时间为YYYY-MM-DD HH:MM:SS
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
time_tuple[0], time_tuple[1], time_tuple[2],
time_tuple[3], time_tuple[4], time_tuple[5]
)
4. 北京时间计算
def beijing_time():
# 获取调整时区后的时间元组
utc_epoch = utime.time()
adjusted_epoch = utc_epoch + TIME_ZONE_OFFSET * 3600
time_tuple = utime.localtime(adjusted_epoch)
return time_tuple
效果展示
系统复位(网络校时前)
系统复位(网络校时后)
遇到的难题与解决办法
RT-Thread方案的AT联网异常
本项目最初使用的是 RT-Thread STM32F407 + ESP8266 AT指令联网的方式获取NTP时间,但由于主线RT-Thread在STM32F407上运行AT指令的代码存在缺陷,频繁报错。因此选择直接在ESP32模块本地运行NTP时间获取程序,通过串口把时间定时传给STM32F407主控,这样将联网与NTP获取隔离出去的方式,大大简化了主控代码,此时主控代码只需监听并解析串口数据即可获取最新时间。
活动感想
本项目的一个亮点在于实现了使用LVGL驱动VFD显示屏的能力,其实本次项目过程中,我原期望使用U8g2库进行驱动,但U8g2的新屏幕适配似乎更复杂一些,而LVGL只需要实现一个画点或区域刷新方法即可,这一点充分证明了LVGL的适应性。期望本次LVGL驱动VFD屏幕代码的开源,可以抛砖引玉,方便网友们使用自己手上的VFD屏幕显示出更绚丽的UI。
感谢贸泽电子和硬禾科技和联合举办的竞赛,祝硬禾的活动越办越好!