仪表盘代码优化

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

225.3.24 仪表盘代码优化

#include <stdio.h> // 声明 sprintf;否则编译器无法按正确的函数原型检查和生成调用代码

#define COLOR_RED 0xff,0x0,0x0 // 定义红色的 RGB 三个分量,供 lv_color_make 使用
#define COLOR_WHITE 0xff,0xff,0xff // 定义白色的 RGB 三个分量,供 lv_color_make 使用

lv_color_t color_value; // 保存 LVGL 颜色值,供四个告警图标共用
uint32_t time_count = 0; // 记录 LVGL 服务周期,用于控制图标红白切换
uint32_t needle_angle = 0; // 保存仪表盘指针角度;此处的数值单位是 1°
RTC_TimeTypeDef struct_rtc_time; // 保存从 RTC 读取出的时、分、秒
RTC_DateTypeDef struct_rtc_date; // 保存从 RTC 读取出的年、月、日
uint8_t time_buff[15] = {0}; // 保存格式化后的时间字符串,并预留字符串结束符空间
uint8_t date_buff[15] = {0}; // 保存格式化后的日期字符串,并预留字符串结束符空间
uint32_t timetick = 0; // 记录上一次刷新指针、时间和日期时的 HAL 毫秒滴答
uint32_t lvtick = 0; // 记录上一次调用 LVGL 定时器处理函数时的 HAL 毫秒滴答

while (1) // 进入裸机主循环,各项任务按自己的时间条件轮询执行
{ // 主循环开始
    if(HAL_GetTick() - lvtick > 5) // 距离上次 LVGL 服务超过 5 个毫秒滴答时进入;代码中的“>”使最早触发点约为第 6 ms
    { // LVGL 服务时间片开始
        lvtick = HAL_GetTick(); // 保存本次执行时刻,作为下一次时间差计算的基准
        lv_timer_handler(); // 运行 LVGL 的定时器、动画、重绘等待处理任务
        time_count++; // 只有服务 LVGL 时才把闪烁计数加一,避免计数速度取决于主循环空转速度
        time_count = time_count %400; // 把计数限制在 0~399,形成循环
    } // LVGL 服务时间片结束

    if(1 == time_count) // 计数为 1 时把四个图标改成红色
    { // 红色状态处理开始
        color_value = lv_color_make(COLOR_RED); // 把宏展开得到的 RGB 分量转换为 LVGL 颜色值
        lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); // 设置灯光图标的重着色颜色
        lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); // 把灯光图标的重着色不透明度设为完全生效
        lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); // 设置转向灯图标的重着色颜色
        lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); // 让转向灯图标完全采用重着色
        lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); // 设置水温图标的重着色颜色
        lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); // 让水温图标完全采用重着色
        lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); // 设置安全带图标的重着色颜色;对象名沿用课堂工程拼写
        lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); // 让安全带图标完全采用重着色
    } // 红色状态处理结束
    if(201 == time_count) // 计数为 201 时把四个图标改回白色
    { // 白色状态处理开始
        color_value = lv_color_make(COLOR_WHITE); // 把白色 RGB 分量转换为 LVGL 颜色值
        lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); // 设置灯光图标为白色重着色
        lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); // 让灯光图标完全采用白色重着色
        lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); // 设置转向灯图标为白色重着色
        lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); // 让转向灯图标完全采用白色重着色
        lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); // 设置水温图标为白色重着色
        lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); // 让水温图标完全采用白色重着色
        lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); // 设置安全带图标为白色重着色
        lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); // 让安全带图标完全采用白色重着色
    } // 白色状态处理结束

    if(HAL_GetTick() - timetick >500) // 距离上次刷新超过 500 个毫秒滴答时,统一执行低频任务
    { // 低频刷新时间片开始
        timetick = HAL_GetTick(); // 保存本次执行时刻,供下一轮判断使用
        needle_angle = needle_angle + 10; // 每次把指针增加 10°
        needle_angle = needle_angle%250; // 把课堂仪表盘的逻辑角度限制在 0~249°并循环
        lv_img_set_angle(ui_ImageNeedle, needle_angle*10); // LVGL 接口以 0.1°为单位,因此把逻辑角度乘 10
        HAL_RTC_GetTime(&hrtc, &struct_rtc_time, RTC_FORMAT_BIN); // 先读取时间;对 STM32F1 HAL 而言,这一步还会推动软件日期更新
        sprintf((char *)(time_buff),"%2u:%2u:%2u", struct_rtc_time.Hours, struct_rtc_time.Minutes, struct_rtc_time.Seconds); // 把无符号时间数值写成字符串,并显式转换目标指针类型
        lv_label_set_text(ui_LabelTime, (char *)time_buff); // 把时间缓冲区作为 C 字符串交给时间标签
        HAL_RTC_GetDate(&hrtc, &struct_rtc_date, RTC_FORMAT_BIN); // 在读取时间之后读取 HAL 当前维护的日期
        sprintf((char *)date_buff,"20%2u-%2u-%2u", struct_rtc_date.Year, struct_rtc_date.Month, struct_rtc_date.Date); // 把年、月、日格式化为课堂使用的日期字符串
        lv_label_set_text(ui_LabelDate, (char *)date_buff); // 把日期字符串显示到日期标签
    } // 低频刷新时间片结束
} // 主循环结束,程序实际会继续下一轮

课堂还在 rtc.c 的 MX_RTC_Init() 中,用备份寄存器标志包住 CubeMX 生成的首次时间、日期设置代码。下面保留课堂工程中的实际初始值;行尾注释用于解释,不是另写一套代码。

if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1)!=0xA0A0) // 读取备份寄存器 DR1;没有发现约定标志时才执行首次初始化
{ // 首次初始化条件成立,开始设置默认时间和日期
    sTime.Hours = 14; // 把默认小时设为 14
    sTime.Minutes = 22; // 把默认分钟设为 22
    sTime.Seconds = 16; // 把默认秒设为 16
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) // 以普通二进制数格式写入默认时间,并检查返回状态
    { // 写时间失败时进入错误分支
        Error_Handler(); // 调用工程统一的错误处理函数
    } // 写时间错误分支结束
    DateToUpdate.WeekDay = RTC_WEEKDAY_MONDAY; // 设置默认星期;F1 HAL 最终会依据日期重新计算星期
    DateToUpdate.Month = RTC_MONTH_MARCH; // 把默认月份设为 3 月
    DateToUpdate.Date = 22; // 把默认日设为 22 日
    DateToUpdate.Year = 25; // HAL 的年份是 0~99,这里的 25 表示课堂约定的 2025 年
    if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK) // 把默认日期写入 HAL 的日期状态,并检查返回状态
    { // 写日期失败时进入错误分支
        Error_Handler(); // 调用工程统一的错误处理函数
    } // 写日期错误分支结束
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA0A0); // 初始化完成后写入魔数,后续普通复位不再重写默认值
} // 首次初始化条件结束

代码范围说明:首个代码块把 main.c 中与本节优化直接相关的最终业务代码集中展示;第二个代码块对应 rtc.c 中被条件保护的初始化区。没有抄入 HAL/LVGL 库函数实现,也没有补写课堂未实现的通信协议。

一、代码优化

本节不是简单追求“警告消失”,而是同时处理三个不同层面的问题:让编译器拿到正确声明、让不同任务按合适周期运行、避免普通复位反复覆盖 RTC 时间。三类问题要分开理解:类型转换解决的是接口类型,滴答调度解决的是执行频率,备份寄存器标志解决的是是否重新初始化。

1.1 解决头文件问题

sprintf() 的声明位于标准头文件 <stdio.h>。课堂工程此前直接调用 sprintf(),却没有包含该头文件,因此编译器会报告函数未声明或隐式声明一类诊断。正确做法是在 main.c 的用户头文件区域加入:

#include <stdio.h> // 引入 sprintf 的标准函数声明,使编译器能够检查实参与返回类型

头文件不是把 sprintf() 的完整实现复制进当前文件,而是向编译器提供函数原型等声明。真正的库实现会在链接阶段由所选 C 运行库参与链接。对带尖括号的 #include <stdio.h>,预处理器通常从工具链配置的系统头文件路径查找;双引号形式通常还会优先查找当前工程目录。这里使用标准库头文件,尖括号写法合适。

把头文件放在“第一次使用之前”才是语义上的要求,不必机械理解成文件绝对第一行。放在 CubeMX 的 USER CODE BEGIN Includes 区域内还可避免重新生成代码时被覆盖。

1.2 解决类型不匹配问题

课堂中的缓冲区被声明为 uint8_t[],而 sprintf() 的第一个参数和 lv_label_set_text() 的文本参数使用 char */const char *。即使两种元素在本工程里都占一个字节,uint8_t * 与 char * 仍是不同的指针类型,编译器因此给出不兼容指针警告。

课堂最终代码采用显式强制转换:

sprintf((char *)(time_buff),"%2u:%2u:%2u", struct_rtc_time.Hours, struct_rtc_time.Minutes, struct_rtc_time.Seconds); // 把可写字节缓冲区的地址显式转换为 sprintf 需要的 char 指针
lv_label_set_text(ui_LabelTime, (char *)time_buff); // 把已经形成的以 \0 结尾字符串传给 LVGL 标签
sprintf((char *)date_buff,"20%2u-%2u-%2u", struct_rtc_date.Year, struct_rtc_date.Month, struct_rtc_date.Date); // 日期缓冲区作相同转换
lv_label_set_text(ui_LabelDate, (char *)date_buff); // 传入日期字符串

这里必须把两点区分开:

  • 强制转换告诉编译器“这里是有意按字符缓冲区使用”,从而消除类型诊断;它本身不会扩大数组,也不会自动保证格式化结果不越界。
  • 本例两个数组各有 15 字节,课堂格式产生的内容能够放下,并由 sprintf() 追加 \0。更通用的工程宜用带容量限制的 snprintf(),但课堂源代码没有改成该函数,笔记不擅自替换。

格式串中的 %2u 表示字段最小宽度为 2,位数不足时用空格补齐,例如 7 会显示为“ 7”;它不是固定两位。若需求是 07,格式应为 %02u,这属于显示规格选择,不是本节源代码的实际写法。

1.3 按复位后时间初始化问题

1.3.1 RTC 初始化设置时间问题

CubeMX 生成的 MX_RTC_Init() 原本每次启动都会调用 HAL_RTC_SetTime() 和 HAL_RTC_SetDate(),写入固定的 14:22:16 和 2025-03-22。这样一按复位键,程序重新从 main() 执行,RTC 又被固定值覆盖,看起来就像“时间没有保存”。

这与板上有没有纽扣电池不是同一个问题。VBAT 可以在 VDD 断电时继续给 RTC、LSE 和备份域供电,但若启动代码主动写入固定时间,仍会把继续运行的计数覆盖掉。先停止重复写入,电池保持才有意义。

1.3.2 避免按复位后时间初始化问题

课堂采用“备份寄存器魔数”方案:

  1. 启动时读取 RTC_BKP_DR1。
  2. 若它不是约定值 0xA0A0,认为这是首次有效初始化,执行默认时间、日期设置。
  3. 成功后把 0xA0A0 写入同一备份寄存器。
  4. 后续普通系统复位时标志仍在,于是跳过固定时间、日期设置。

0xA0A0 不是芯片规定的特殊值,只是本项目选择的“已初始化”标志。它不代表真实时间,也不会让 HAL 自动保存所有软件变量。正式产品还应考虑版本号、校验、恢复策略以及标志碰撞,而不只是判断一个常量。

备份域能够跨普通复位保持,但不是永远不丢。以下情况需要重新初始化:VDD 与 VBAT 都曾掉电、软件触发备份域复位、修改 RTC 时钟源导致备份域被复位,以及特定器件上的篡改事件清除备份寄存器。调试时若想重新执行默认时间设置,可以有意识地清除 DR1 或复位整个备份域;不要把“普通复位”和“备份域复位”混为一谈。

还有一个对本节结果至关重要的限制:STM32F1 所用的是 V1 RTC。硬件主要保存并运行秒计数器,HAL 的年、月、日由 RTC_HandleTypeDef 中的 DateToUpdate 软件状态维护。HAL_RTC_Init() 会把该软件日期初始化为 2000-01-01,HAL_RTC_SetDate() 只是更新这份状态;所以单独保存 0xA0A0,可以阻止程序覆盖仍在运行的硬件时间,却没有把日期状态持久化。课堂末尾出现“时间正常、日期不对”,正是这一限制的直接表现,而不是简单的显示格式错误。

若产品需要日期跨复位/掉电可靠保存,应把日期或完整时间戳另存到备份寄存器、外部 RTC 或其他非易失存储中,并在启动时恢复;也可以从可信外部时间源重新校时。

1.4 优化时间显示

1.4.1 时间显示问题与优化思路

上一版把读取 RTC、格式化字符串和设置 LVGL 标签直接放在高速 while(1) 中。主循环一秒可能运行成千上万次,但人眼看到的秒只需低频刷新。无意义的重复调用会增加格式化、对象更新和重绘负担,也会挤占图标动画、触摸响应等任务的执行时间。

优化目标不是“把主循环变慢”,而是让主循环继续快速轮询,只在某项任务的时间条件满足时执行该任务。这就是本节使用的非阻塞时间片思路。

1.4.2 优化时间显示的具体实现方法

先定义 uint32_t timetick = 0; 保存上一次执行时刻,再在主循环中比较当前滴答与该值的差:

if(HAL_GetTick() - timetick >500) // 只有经过超过 500 个 HAL 毫秒滴答时才执行
{ // 定时任务开始
    timetick = HAL_GetTick(); // 更新基准时刻
} // 定时任务结束

课堂把 RTC 读取、字符串格式化、标签设置和指针更新都放入这个分支。分支未满足时,CPU 会继续检查其他任务,不会像 HAL_Delay(500) 那样停在延时循环中等待。

由于源码写的是 >500 而不是 >=500,在默认 1 ms HAL 时基下,最早要到时间差为 501 时才成立;再加上主循环其他工作耗时,实际间隔是“约 0.5 秒”,不是严格的 500.000 ms。

1.4.3 滴答定时器与 HAL_GetTick() 函数介绍

在本课堂工程中,SysTick_Handler() 调用 HAL_IncTick(),默认每 1 ms 把全局无符号变量 uwTick 增加一次;HAL_GetTick() 只是返回该变量当前值。因此它给出的是系统启动后的相对毫秒计数,不是 RTC 的时分秒,也不是 Unix 时间戳。

更严谨地说,HAL 默认以 Cortex-M3 的 SysTick 作为时间基准,但 HAL 的相关函数是弱定义,工程也可以改用通用定时器等时间源。本项目的中断文件和链接结果表明实际使用的是默认 SysTick。

表达式 HAL_GetTick() - timetick 使用 uint32_t 无符号减法。即使 uwTick 大约每 49.7 天回绕一次,只要待判断的间隔远小于整个 32 位计数周期,这种“当前值减旧值”的写法仍能跨回绕正确工作。若先比较 HAL_GetTick() > timetick + 500,反而更容易在回绕附近出错。

HAL_Delay() 内部也会读取滴答并等待时间差达到要求,所以它是阻塞等待;本节只做一次条件判断,条件不满足就继续主循环,是协作式、非阻塞调度。若 HAL tick 被挂起,或某段代码长时间关闭中断,HAL_GetTick() 不再正常前进,相应任务也不会按预期触发。

1.4.4 优化时间显示代码实现与验证

课堂先把 RTC 时间显示代码移入 timetick 条件分支,并在分支入口更新 timetick。下载验证后,标签约每半秒刷新一次,秒值依然连续变化,说明显示不需要跟随主循环的最高速度刷新。

调用顺序仍应保持“先时间、后日期”。在 STM32F1 HAL 中,日期完全由软件维护,调用 HAL_RTC_GetTime() 会依据硬件秒计数更新软件日期;紧接着调用 HAL_RTC_GetDate() 才得到相匹配的日期状态。这和某些带硬件日历寄存器的 STM32 系列不是同一种实现。

1.4.5 优化后的问题与进一步调整

只降低时间标签刷新频率后,如果指针旋转仍在每次主循环执行,界面可能继续卡顿。原因是高频调用 lv_img_set_angle() 会不断让对象失效并请求重绘,图像旋转本身也有计算成本。课堂随后对指针和 LVGL 服务周期继续分流,最终把同为约 500 ms 的指针与时间任务合并。

这里的优化原则可以概括为:显示值的更新频率要由信息变化速度和交互需求决定,而不是由 CPU 能把主循环跑多快决定。

1.5 优化指针转动频率

1.5.1 指针转动频率的初步调整

课堂中间版本曾新建 needletick,先用 >100 的条件限制指针更新,并把每次逻辑角度增量由 1°改为 10°。这样做减少了旋转 API 的调用次数,又让指针在较少的更新中走过明显角度。

注意 lv_img_set_angle() 接受的是 0.1°单位:逻辑变量增加 10 后,再传入 needle_angle * 10,一次实际变化是 10°,不是 100°。

1.5.2 延时处理与指针转动优化

课堂没有通过在循环末尾加长 HAL_Delay() 来降频,因为阻塞延时会同时拖住 LVGL 处理、标签刷新和其他未来加入的通信任务。给每类工作保存自己的上次时刻,可以使多个周期不同的任务共享一个主循环。

这种方法仍不是实时操作系统:若某个分支内部执行很久,其他分支照样会被推迟。它适合当前裸机演示,后续加入 CAN、串口和更多传感器后,还要继续控制单次任务执行时间,必要时使用中断、DMA、状态机或 RTOS。

1.5.3 指针转动与 LVGL 定时器的整合

课堂把 lv_timer_handler() 也放入独立的 HAL 滴答条件:

if(HAL_GetTick() - lvtick > 5) // 用 HAL 毫秒滴答约束 LVGL 处理函数的调用频率
{ // LVGL 服务时间片开始
    lvtick = HAL_GetTick(); // 更新上一次服务时刻
    lv_timer_handler(); // 让 LVGL 处理到期的内部定时器、动画和重绘任务
} // LVGL 服务时间片结束

准确地说,这不是“创建了一个 LVGL 定时器”,而是用 HAL 时基定期调用 LVGL 的定时器处理器。LVGL 8.3 建议约每 5 ms 调用一次以保持响应,但这个周期并非硬实时要求。

本工程里还有另一条独立时基:TIM6 的周期回调调用 lv_tick_inc(1),给 LVGL 内部提供 1 ms 经过时间;SysTick 则调用 HAL_IncTick(),供 HAL_GetTick() 使用。前者告诉 LVGL“时间过去了多少”,后者决定主循环“何时调用处理器”,两者职责不同,不能只保留其中一个就假定功能等价。

time_count++ 被移到 LVGL 服务分支后,图标闪烁计数不再按主循环空转次数增长,而是按 LVGL 服务周期增长。代码用 >5,所以默认情况下每次计数的最短间隔约 6 ms;从 1 到 201 约为 1.2 s,而不是精确 1 s。

还有一个源代码层面的细节:红色和白色两个 if 位于 LVGL 服务分支之外。当 time_count 在数毫秒内保持为 1 或 201 时,对应的重着色语句会在主循环中重复执行多次。它比原先无条件持续重绘有所改善,但并非严格“每个事件只执行一次”。若要继续优化,应把状态切换放进计数更新分支,或记录上一次状态;本节按课堂最终源码记录,不擅自改写。

1.5.4 指针转动频率的进一步调整与测试

课堂观察指针仍显得偏快,又把中间版本的指针间隔改为 >500。最终效果取决于两个量:每次增加 10°,约每 0.5 s 更新一次,因此是离散跳动而非连续平滑动画。

逻辑角度序列为 10、20、……、240、0,共 25 次更新完成一次 250°循环。按最短约 501 ms 估算,一轮约 12.5 s;实际还会受到主循环负载影响。

1.5.5 最终调整与代码优化

由于时间标签和指针最终都采用相同的 >500 条件,课堂删除了中间使用的 needletick,把两组代码合并到 timetick 分支,减少重复判断和状态变量;未使用的 uint8_t buff[4] 也被删除。最终调度关系如下:

任务时间条件实际含义
LVGL 服务与闪烁计数HAL_GetTick() - lvtick > 5最早约每 6 ms 调用一次处理器并加一次计数
图标变红time_count == 1每 400 个服务周期的一处状态点
图标变白time_count == 201与红色状态相隔 200 个服务周期
指针、时间、日期HAL_GetTick() - timetick > 500最早约每 501 ms 统一刷新一次

把更新时间记录为“本次实际执行时刻”很直观,但如果某次执行被推迟,下一次基准也会随之推迟,长期可能产生相位漂移。当前仪表演示不要求精密周期;若是严格采样或通信时隙,应使用更合适的定时策略,不能照搬这段界面调度代码。

1.6 日期显示问题

1.6.1 日期显示错误原因

复位测试中,时分秒能够继续走,日期却回到错误值。直接原因不是“日期标签没有刷新”,而是 STM32F1 的 RTC 硬件没有其他系列那样的完整硬件日历寄存器,F1 HAL 把日期保存在 RAM 中的软件结构里。复位后 RAM 状态重新初始化为 2000-01-01,而 0xA0A0 又使默认日期设置被跳过,于是标签读到的不是复位前日期。

所以本节的备份标志方案只完成了一半:它正确避免了重复覆盖硬件时间,却没有设计日期持久化。学习时应把现象追到数据真正存放的位置,而不是靠多加几次 HAL_RTC_GetDate() 或改变显示格式碰运气。

1.6.2 数据传输与仪表盘校准

课堂提出后续用上位机校准仪表盘:PC 通过 USART2 向 STM32F103C8T6 节点发送时间数据,该节点再通过 CAN 转发给连接显示屏的 STM32F103ZET6 节点,由接收端更新 RTC 与界面。数据路径可写成:

PC → USART2 → C8T6 发送节点 → CAN 总线 → ZET6 显示节点 → RTC/LVGL

这只是本节提出的系统方向,本节源代码尚未实现具体校时协议。真正实现时至少要明确:年、月、日、时、分、秒各字段的字节布局,二进制还是 ASCII,CAN 标识符,分包规则,数据有效范围,校验或序号,超时处理,以及写 RTC 后如何更新备份标志。CAN 单帧经典数据区最多 8 字节,协议不能只说“把时间发过去”而不定义格式。

1.6.3 项目初衷与自主实践建议

本仪表盘项目的学习价值不只在画出界面,而在把 GUI、RTC、滴答调度、串口和 CAN 逐步组合起来。可以按以下顺序自行验证:

  1. 在调试器中观察 uwTick,确认 SysTick 每毫秒递增以及无符号时间差判断的结果。
  2. 分别做普通复位、断开 VDD 但保留 VBAT、VDD/VBAT 全断三组实验,记录 RTC 时间和 DR1 标志是否保持。
  3. 复位前后同时观察硬件时间和 hrtc.DateToUpdate,验证“时间保持、日期软件状态丢失”的区别。
  4. 给图标状态切换加计数或断点,确认当前两个外部 if 会在一个计数值保持期间重复执行,再尝试设计一次性状态切换。
  5. 最后自行定义并文档化校时帧,再实现串口接收、CAN 转发、范围检查和 RTC 更新;不要先抄一段未理解的协议代码。

二、本节易错点总结

  • 包含 <stdio.h> 解决的是函数声明问题;指针强制转换解决的是参数类型问题,两者不能互相替代。
  • (char *) 只改变编译器看待地址的类型,不会防止 sprintf() 越界。
  • HAL_GetTick() 是相对毫秒滴答,不是 RTC 时间;默认来自 SysTick,但允许被工程重写。
  • lv_tick_inc(1) 与 lv_timer_handler() 一个提供 LVGL 时间基准,一个执行到期任务,不是同一件事。
  • 源码使用 >,因此 >5 和 >500 的最早触发差分别是约 6 ms 和约 501 ms。
  • RTC_BKP_DR1 中的魔数是初始化标志,不是日期备份。
  • STM32F1 HAL 的日期由软件维护,跨复位保留日期必须另行持久化或重新校时。

三、资料依据