实时时钟与定时器
本文档详细介绍 AD24N GP-MCU SDK 中的实时时钟(RTC)与定时器子系统,涵盖虚拟 RTC(Virtual RTC)驱动、RTC 数据的 VM 持久化、系统滴答定时器(Tick Timer)的调度分发机制,以及基于 jiffies 的时间延时节流 API。
Purpose and Scope
本页面覆盖以下能力:
- RTC 驱动层:
rtc.c/rtc.h提供的设备初始化、时间/闹钟读写、唤醒配置与日历换算 API。 - RTC 数据持久化:通过 VM(虚拟机存储)保存/恢复 RTC 上下文。
- Tick Timer 系统滴答:
tick_timer_sys.c中由硬件定时器中断驱动的jiffies计数、节拍分发(sys_tick_timer/modules_tick_timer/app_timer_loop)与基于节拍的延时函数。 - 模块级节拍扩展:
tick_timer_modules.c与modules_tick_timer弱函数钩子。
以下相关主题属于其他页面,本文仅作指引:
- 硬件通用定时器(GPTimer)外设驱动与示例,参见
sdk/include_lib/cpu/gptimer.h、sdk/app/bsp/cpu/periph_demo/gptimer_demo.c对应的外设文档页。 - 电源管理(softoff 定时唤醒、
sys_power_down)与看门狗(wdt_close)机制属于系统电源/看门狗页面。 - VM 存储子系统本身(
vm_api.h)属于存储机制页面。
概述
嵌入式设备中,"时间"由两个互补的机制共同提供:
- **RTC(实时时钟)负责回答"现在是几点几分、星期几"这类日历问题。AD24N SDK 使用虚拟 RTC(VIRTUAL_RTC_EN)**方案:并非依赖独立的硬件 RTC 外设,而是基于内部低频时钟(LRC/32K/BTOSC)或电源域计数,把时间值保存在 VM 中,从而在软关机(softoff)期间仍能维持时间走时与闹钟唤醒能力。
- **Tick Timer(系统滴答)**负责回答"程序运行了多久、何时该做周期性任务"这类调度问题。硬件定时器以固定周期(如 2ms)触发中断,软件维护一个全局
jiffies计数器,并在每个节拍调用sys_tick_timer、app_timer_loop、modules_tick_timer三个分发入口,从而把单个中断源复用为多种周期任务(DAC 淡入淡出、100ms 事件、500ms 消息等)的时基。
这套设计的关键意图:
- 单一时间源:所有软件延时(
os_time_dly、delay_10ms)都基于jiffies单调递增计数,避免多个独立时基漂移不一致。 - 中断开销最小化:硬件 ISR 只做计数递增,周期任务通过取模分发(
cnt % N),把具体业务逻辑推迟到轮询循环中执行。 - 可扩展分层:
app_timer_loop与modules_tick_timer均声明为__attribute__((weak))弱函数,应用层与模块层可以各自覆盖,无需修改内核节拍代码。 - 低功耗友好:RTC 走时与闹钟依赖低频时钟而非主时钟,配合
vir_rtc_wakeup_enable可实现 softoff 下的定时唤醒。
架构
flowchart TD
subgraph sg_HW["硬件层"]
LRC["低频时钟源<br/>LRC / 32K / BTOSC"]
TIMER["硬件定时器中断<br/>(Tick 源,如 2ms)"]
end
subgraph sg_RTC["RTC 子系统"]
RTC_DEV["rtc_dev_init / rtc_dev_deinit"]
RTC_TIME["rtc_read_time / rtc_write_time"]
RTC_ALM["rtc_read_alarm / rtc_write_alarm<br/>rtc_alarm_switch"]
RTC_WK["vir_rtc_wakeup_enable / disable"]
CAL["日历换算<br/>ymd_to_day / day_to_ymd / leapyear"]
end
subgraph sg_VM["VM 存储"]
VM_WRITE["vm_write (rtc_save_api)"]
VM_READ["vm_read (rtc_read_api)"]
end
subgraph sg_TICK["Tick Timer 子系统"]
JIFFIES["jiffies 计数器"]
TICK_LOOP["tick_timer_loop"]
SYS_TICK["sys_tick_timer"]
APP_LOOP["app_timer_loop (weak)"]
MOD_TICK["modules_tick_timer (weak)"]
DELAY["os_time_dly / delay_10ms"]
end
subgraph sg_APP["应用层"]
APP["业务任务 / 消息循环"]
MODULES["功能模块<br/>(如 DAC 淡入淡出、100ms/500ms 任务)"]
end
LRC --> RTC_DEV
TIMER --> JIFFIES
RTC_DEV --> RTC_TIME
RTC_DEV --> RTC_ALM
RTC_DEV --> RTC_WK
RTC_TIME --> CAL
RTC_ALM --> CAL
RTC_TIME --> VM_READ
RTC_TIME --> VM_WRITE
RTC_DEV --> VM_READ
RTC_DEV --> VM_WRITE
JIFFIES --> TICK_LOOP
TICK_LOOP --> SYS_TICK
TICK_LOOP --> APP_LOOP
TICK_LOOP --> MOD_TICK
SYS_TICK --> APP
SYS_TICK --> MODULES
JIFFIES --> DELAY
DELAY --> APP
架构说明:
- RTC 子系统以
rtc.h声明的rtc_dev_init为入口,struct rtc_config_init携带默认时间、默认闹钟、闹钟回调、时钟源选择与闹钟使能标志。虚拟 RTC 的时间读写最终落在 VM 的vm_write/vm_read上(SYS_VM_EN使能时),从而在掉电/软关机后仍可恢复。 - Tick Timer 子系统以
tick_timer_init(声明于tick_timer_driver.h)初始化,硬件中断每次触发递增jiffies并调用tick_timer_loop。后者再按节拍序号g_tick_cnt做取模分发,驱动 DAC 渐变、100ms 事件、500ms 消息等周期任务;应用层与模块层通过弱函数钩子注入自己的周期逻辑。 - 两个子系统通过单一软件时基耦合:RTC 回答日历时间,Tick Timer 提供运行时长与周期调度;
os_time_dly/delay_10ms则基于jiffies提供不依赖操作系统的忙等待延时。
RTC 子系统详解
核心数据结构
RTC 层的时间表示使用 struct sys_time,字段按 _GNU_PACKED_ 紧凑打包,便于直接存入 VM:
struct sys_time {
u16 year;
u8 month;
u8 day;
u8 hour;
u8 min;
u8 sec;
} _GNU_PACKED_;
Source: rtc.h
初始化参数聚合在 struct rtc_config_init 中,一次性描述 RTC 设备的行为:
struct rtc_config_init {
const struct sys_time *default_sys_time;
const struct sys_time *default_alarm;
void (*cbfun)(void);
u32 rtc_clk;
u8 alm_en;
};
Source: rtc.h
default_sys_time/default_alarm:首次上电(VM 中无记录)时的默认时间与闹钟值;cbfun:闹钟触发时的回调函数;rtc_clk:RTC 走时时钟源,可取CLK_SEL_LRC(1)、CLK_SEL_32K(2)、CLK_SEL_BTOSC(3);alm_en:闹钟使能位。
#define CLK_SEL_LRC 1
#define CLK_SEL_32K 2
#define CLK_SEL_BTOSC 3
#define USE_VIR_RTC 1
Source: rtc.h
USE_VIR_RTC 置 1 表明本平台走虚拟 RTC 路径:rtc.c 在编译期检测 VIRTUAL_RTC_EN,使能时引入 asm/power/p33.h 与 asm/power/virtual_rtc.h,由虚拟 RTC 模块基于电源域/低频时钟维护时间,并借助 VM 持久化。
VM 持久化桥接
rtc.c 是 RTC 公共层与 VM 存储之间的桥接层。SYS_VM_EN 使能时,存取操作直接映射为 vm_write / vm_read;未使能时退化为返回 -1 的空操作,保证编译与链接不依赖 VM 子系统:
#if SYS_VM_EN
#include "vm_api.h"
#define rtc_save_api(p0, p1, p2) vm_write(p0, p1, p2)
#define rtc_read_api(p0, p1, p2) vm_read(p0, p1, p2)
#else
#define rtc_save_api(...) -1
#define rtc_read_api(...) -1
#endif
s32 rtc_mem_save(u32 index, u8 *data_buf, u16 len)
{
return rtc_save_api(index, data_buf, len);
}
s32 rtc_mem_read(u32 index, u8 *data_buf, u16 len)
{
return rtc_read_api(index, data_buf, len);
}
Source: rtc.c
设计意图:rtc_mem_save / rtc_mem_read 是虚拟 RTC 内核(virtual_rtc.c)保存/恢复上下文(走时计数、闹钟时间、唤醒标志)的底层通道。index 为 VM 存储槽位,len 为数据长度。通过宏抽象,VM 是否启用只影响运行行为,不影响上层 RTC 代码结构。
RTC 设备 API 全景
rtc.h 对外暴露的 API 覆盖设备的完整生命周期:
void rtc_dev_init(const struct rtc_config_init *rtc);
void rtc_dev_deinit(void);
void rtc_read_time(struct sys_time *time);
void rtc_write_time(const struct sys_time *time);
void rtc_read_alarm(struct sys_time *time);
void rtc_write_alarm(const struct sys_time *time);
void rtc_debug_dump(void);
void rtc_alarm_switch(u32 en);
void rtc_save_context_to_vm(void);
void rtc_reset_save_time(void);
u32 rtc_is_alarm_en(void);
u32 rtc_is_alarm_wkup(void);
u32 rtc_get_clk_sel(void);
Source: rtc.h
rtc_dev_init:以配置结构初始化 RTC,注册默认时间/闹钟并启动走时;重复调用需先rtc_dev_deinit。rtc_read_time/rtc_write_time:读写当前日历时间。rtc_read_alarm/rtc_write_alarm:读写闹钟时间。rtc_alarm_switch(en):运行时开关闹钟。rtc_save_context_to_vm:主动把当前 RTC 上下文刷入 VM(如关机前调用)。rtc_reset_save_time:清除已保存的走时记录,使下次上电回落到默认时间。rtc_is_alarm_en/rtc_is_alarm_wkup:查询闹钟使能状态与"本次开机是否为闹钟唤醒"。rtc_get_clk_sel:返回当前选用的走时时钟源。
此外还提供两个 softoff 定时唤醒控制接口:
void vir_rtc_wakeup_enable(u32 wkup_ms);//softoff定时唤醒配置,wkup_ms单位:ms
void vir_rtc_wakeup_disable(void);
Source: rtc.h
vir_rtc_wakeup_enable 以毫秒为单位配置软关机后的定时唤醒;vir_rtc_wakeup_disable 取消该唤醒。这是低功耗场景下"休眠到点自醒"的核心接口。
日历换算算法
rtc.h 声明了一组纯函数形式的日历换算工具,供虚拟 RTC 与上层应用共用:
bool leapyear(u32 year); //判断是否为闰年
u32 year_to_day(u32 year);
u32 month_to_day(u32 year, u32 month);
void day_to_ymd(u32 day, struct sys_time *sys_time);
u32 ymd_to_day(struct sys_time *time);
u32 caculate_weekday_by_time(struct sys_time *r_time); //计算当天为星期几
u32 get_day_of_month(u32 year, u32 month); //返回每月的天数
Source: rtc.h
设计意图:虚拟 RTC 内部通常只维护一个递增的"秒/日"计数值,读取时再换算成 year-month-day hour:min:sec 的日历形式;写入时反向换算。leapyear、month_to_day、get_day_of_month 处理大小月与闰年(2 月 29 天)边界,caculate_weekday_by_time 支持星期显示。
RTC 测试/示例路径(演示用法)
rtc.c 中保留了一段被 #if 0 屏蔽的测试代码,完整演示了虚拟 RTC 的用法:通过 rtc_simulate_init(&rtc_demo) 装载默认时间/闹钟与闹钟回调,随后用 read_sys_time / write_sys_time / read_alarm / write_alarm 读写并打印,最后进入走时打印循环:
static struct sys_time def_sys_time = { //初始一下当前时间
.year = 2050,
.month = 1,
.day = 1,
.hour = 1,
.min = 1,
.sec = 1,
};
const struct vir_rtc_data rtc_demo = {
.default_sys_time = &def_sys_time,
.default_alarm = &def_alarm,
.cbfun = alm_test_cbfun,
};
void rtc_test(void)
{
extern void wdt_close();
struct sys_time tmp_time;
rtc_simulate_init(&rtc_demo);
read_sys_time(&tmp_time); //读当前rtc时间
log_info("rtc_rtc_read_time_before: %d-%d-%d %d:%d:%d\n", tmp_time.year, tmp_time.month, tmp_time.day, tmp_time.hour, tmp_time.min, tmp_time.sec);
write_sys_time(&test_sys_time); //修改rtc时间
read_sys_time(&tmp_time); //读修改后rtc时间
read_alarm(&tmp_time); //读当前alarm时间
write_alarm(&test_alarm); //修改alarm时间
while (1) {
delay(50000);
read_sys_time(&tmp_time);
rtc_time_dump(&tmp_time);
}
}
Source: rtc.c
注意:该测试调用的是虚拟 RTC 层接口(rtc_simulate_init、read_sys_time、write_alarm 等,来自 virtual_rtc.h),与 rtc.h 声明的 rtc_dev_* 设备层接口是"实现—封装"的关系;实际产品初始化应走 rtc_dev_init。
Tick Timer 系统滴答详解
全局时基与中断开关
tick_timer_sys.c 定义了两个全局变量作为系统的软件时基:
volatile u32 jiffies = 0;
volatile u8 isr_tick_timer_close = 0;
Source: tick_timer_sys.c
jiffies:单调递增的系统节拍计数,由硬件定时器中断驱动,是os_time_dly/delay_10ms的时基。isr_tick_timer_close:节拍 ISR 的软开关标志。
配套的两个内联访问函数被刻意声明为 always_inline,注释明确警告"以下两个函数一定要内联",因为它们在中断关闭/打开路径上被高频调用,必须避免函数调用开销与可能的缓存抖动:
__attribute__((always_inline))
void tick_timer_set(bool on_off)
{
isr_tick_timer_close = on_off;
}
__attribute__((always_inline))
bool tick_timer_close(void)
{
return isr_tick_timer_close;
}
Source: tick_timer_sys.c
设计意图:在低功耗场景(如 flash 擦写、softoff 前后)需要短暂屏蔽节拍中断,tick_timer_set(true) 即可让 ISR 提前返回,避免在临界区中执行周期任务;由于是 volatile 标志 + always_inline,这个开关在中断上下文中的读写是原子、确定的。
节拍分发:sys_tick_timer
每个节拍都会调用 sys_tick_timer(cnt),它用取模方式把一个节拍源复用为多个周期任务:
void sys_tick_timer(u32 cnt)
{
if (0 == (cnt % 5)) {
dac_fade_api();
jiffies ++;
}
#if (FLASH_CACHE_ENABLE==1)
#if EXT_FLASH_EN
if (get_flash_cache_timer()) {
if (0 == (cnt % 50)) {//100ms
bsp_post_event(B_EVENT_100MS);
}
}
#endif
#endif
if (0 == (cnt % 250)) { //500ms
post_msg(1, MSG_500MS);
}
}
Source: tick_timer_sys.c
cnt % 5 == 0:每 5 个节拍执行一次 DAC 淡入淡出(dac_fade_api)并递增jiffies。若节拍周期为 2ms,则jiffies每 10ms 加 1。cnt % 50 == 0:约每 100ms 向系统投递B_EVENT_100MS事件;仅在使能 flash cache 且使用外部 flash(EXT_FLASH_EN)时生效,因为该事件用于驱动 flash 缓存定时刷新。cnt % 250 == 0:每 250 个节拍(约 500ms)通过post_msg(1, MSG_500MS)向消息循环投递 500ms 周期消息,是许多业务(LED 闪烁、电量刷新等)的通用节拍源。
节拍主循环:tick_timer_loop
tick_timer_loop 是 ISR 与轮询循环的公共入口,按固定顺序执行三层分发:
void tick_timer_loop()
{
g_tick_cnt ++;
sys_tick_timer(g_tick_cnt);
app_timer_loop();
modules_tick_timer(g_tick_cnt);
if (0 == (g_tick_cnt % 250)) { //500ms
g_tick_cnt = 0;
}
}
Source: tick_timer_sys.c
g_tick_cnt 是 u16 节拍序号,sys_tick_timer / modules_tick_timer 依赖它做取模周期判断;每 250 拍归零一次,避免溢出并保持与 500ms 周期对齐。app_timer_loop 与 modules_tick_timer 都是弱符号,默认空实现:
__attribute__((weak))
void app_timer_loop(void)
{
}
__attribute__((weak))
void modules_tick_timer(u32 cnt)
{
}
Source: [tick_timer_sys.c](https://gitee.com/Jieli-Tech/AD24N/blob/main/sdk/app/bsp/modules/timer/tick_timer_sys.c#L44-L48, L112-L116)
app_timer_loop():应用层周期钩子,无参数,适合不需要节拍序号的应用级周期逻辑。modules_tick_timer(cnt):模块层周期钩子,携带节拍序号cnt,供tick_timer_modules.c及各功能模块注册自己的周期任务(如按键扫描、UI 刷新等)。
弱函数机制是这套设计的核心扩展点:内核节拍代码只负责"按时调用",具体"每拍做什么"由各层覆盖实现决定,从而保持内核与业务解耦。
基于 jiffies 的延时
os_time_dly 与 delay_10ms 均基于 jiffies 做忙等待,二者行为等价,只是语义命名不同(一个是"OS 风格延时",一个是"10ms 粒度延时"):
void os_time_dly(u32 tick)
{
tick = tick + jiffies;
while (1) {
if (time_after(jiffies, tick)) {
break;
}
}
}
void delay_10ms(u32 tick)
{
tick = tick + jiffies;
while (1) {
if (time_after(jiffies, tick)) {
break;
}
}
}
Source: tick_timer_sys.c
设计意图:先取"目标绝对节拍 = 当前 jiffies + 相对延时",再循环等待 jiffies 超过目标值。使用 time_after(而非简单比较)是为了正确处理 32 位计数器回绕(wrap-around);jiffies 每 5 个节拍递增一次,因此 delay_10ms(n) 的实际粒度为 10ms × n(假设节拍 2ms、jiffies 每 10ms 递增)。这是忙等待,不释放 CPU,适用于短延时或无法使用任务调度的上下文。
头文件契约
tick_timer_driver.h 是 Tick Timer 子系统对外的唯一头文件契约:
extern volatile u32 jiffies;
extern void tick_timer_init(void);
void tick_timer_sleep_init();
void modules_tick_timer(u32 cnt);
Source: tick_timer_driver.h
jiffies:对外暴露的全局节拍计数,其他模块可读取以感知运行时长。tick_timer_init:初始化硬件定时器并启动节拍中断(由 BSP 启动流程调用)。tick_timer_sleep_init:低功耗睡眠场景下的节拍重新初始化。modules_tick_timer:模块层周期钩子,弱符号,可在任意模块中强覆盖。
核心流程
节拍分发流程
sequenceDiagram
participant HW as 硬件定时器中断
participant TL as tick_timer_loop
participant ST as sys_tick_timer
participant APP as app_timer_loop (weak)
participant MOD as modules_tick_timer (weak)
participant MSG as 消息循环
HW->>TL: 每节拍触发 (如 2ms)
TL->>TL: g_tick_cnt++
TL->>ST: sys_tick_timer(g_tick_cnt)
alt cnt % 5 == 0
ST->>ST: dac_fade_api(); jiffies++
end
alt cnt % 50 == 0 且 flash cache 使能
ST->>MSG: bsp_post_event(B_EVENT_100MS)
end
alt cnt % 250 == 0
ST->>MSG: post_msg(1, MSG_500MS)
end
TL->>APP: app_timer_loop()
TL->>MOD: modules_tick_timer(g_tick_cnt)
alt g_tick_cnt % 250 == 0
TL->>TL: g_tick_cnt = 0
end
虚拟 RTC 走时与闹钟唤醒流程
sequenceDiagram
participant APP as 应用
participant RTC as rtc_dev_* / 虚拟RTC
participant VM as vm_read / vm_write
APP->>RTC: rtc_dev_init(config)
RTC->>VM: 读取已保存上下文
VM-->>RTC: 走时/闹钟记录
RTC-->>APP: 完成初始化
APP->>RTC: rtc_write_time(&t)
RTC->>VM: rtc_mem_save 持久化
RTC-->>APP: ok
APP->>RTC: rtc_write_alarm(&a) / rtc_alarm_switch(1)
RTC-->>APP: ok
Note over RTC: 时间到达闹钟时刻
RTC-->>APP: 闹钟回调 cbfun()
APP->>RTC: vir_rtc_wakeup_enable(60000)
RTC-->>APP: 配置 softoff 定时唤醒
Note over APP: 系统进入 softoff<br/>唤醒后 rtc_is_alarm_wkup() 查询原因
流程要点:
- 上电后应用调用
rtc_dev_init,虚拟 RTC 从 VM 恢复走时与闹钟上下文,保证 softoff 期间时间连续。 - 时间/闹钟的每次写入都同步持久化到 VM,防止异常掉电丢失。
- 闹钟到达时由虚拟 RTC 触发
cbfun;若处于 softoff,则由vir_rtc_wakeup_enable配置的唤醒定时在指定毫秒后拉起系统,应用可通过rtc_is_alarm_wkup区分"闹钟唤醒"与"普通开机"。
使用示例
示例 1:RTC 设备初始化与时间读写
以下代码展示了设备层 RTC 的标准初始化流程(结构取自 rtc.h,调用序列依据 rtc.h 声明与 rtc.c 测试代码整理):
#include "asm/rtc.h"
static const struct sys_time default_time = {
.year = 2024, .month = 1, .day = 1,
.hour = 0, .min = 0, .sec = 0,
};
static const struct rtc_config_init rtc_cfg = {
.default_sys_time = &default_time,
.default_alarm = NULL,
.cbfun = NULL,
.rtc_clk = CLK_SEL_LRC, /* 低频时钟走时,低功耗 */
.alm_en = 0,
};
void app_rtc_init(void)
{
struct sys_time now;
rtc_dev_init(&rtc_cfg);
rtc_read_time(&now); /* 读取当前时间 */
log_info("now: %d-%d-%d %d:%d:%d\n",
now.year, now.month, now.day,
now.hour, now.min, now.sec);
}
Source: rtc.h
示例 2:基于 jiffies 的延时
os_time_dly / delay_10ms 可直接用于需要短时延时的业务路径,例如在消息处理中等待外设稳定:
#include "tick_timer_driver.h"
void demo_delay_usage(void)
{
extern void delay_10ms(u32 tick);
delay_10ms(20); /* 忙等约 200ms(10ms 粒度 × 20) */
os_time_dly(10); /* 忙等 10 个 jiffies(10ms 粒度) */
}
Source: tick_timer_sys.c
示例 3:模块级周期任务扩展
业务模块通过覆盖弱函数 modules_tick_timer 接入系统节拍,无需修改内核代码:
#include "tick_timer_driver.h"
/* 覆盖弱符号 modules_tick_timer,实现模块级周期逻辑 */
void modules_tick_timer(u32 cnt)
{
if (0 == (cnt % 10)) { /* 每 10 拍执行一次,约 20ms */
led_scan();
}
if (0 == (cnt % 50)) { /* 每 50 拍执行一次,约 100ms */
key_scan();
}
}
Source: tick_timer_sys.c
配置选项
以下宏/配置项决定了 RTC 与 Tick Timer 的编译期与运行期行为:
| 配置项 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
VIRTUAL_RTC_EN | 宏 | 0 / 1 | 是否启用虚拟 RTC;使能后 rtc.c 引入 virtual_rtc.h 实现走时 |
SYS_VM_EN | 宏 | 0 / 1 | 是否启用 VM 存储;决定 rtc_save_api/rtc_read_api 映射为 vm_write/vm_read 还是空操作返回 -1 |
CLK_SEL_LRC | 常量 | 1 | RTC 走时时钟源:内部 LRC |
CLK_SEL_32K | 常量 | 2 | RTC 走时时钟源:32K 晶振 |
CLK_SEL_BTOSC | 常量 | 3 | RTC 走时时钟源:蓝牙振荡器 |
USE_VIR_RTC | 宏 | 1 | 平台采用虚拟 RTC 路径(固定) |
FLASH_CACHE_ENABLE | 宏 | 0 / 1 | 使能 flash cache;影响 100ms 事件 B_EVENT_100MS 的投递 |
EXT_FLASH_EN | 宏 | 0 / 1 | 是否使用外部 flash;与 FLASH_CACHE_ENABLE 共同决定 100ms 刷新节拍 |
isr_tick_timer_close | volatile u8 | 0 | 节拍 ISR 软开关,运行时经 tick_timer_set 控制 |
jiffies | volatile u32 | 0 | 全局节拍计数,每 5 个节拍递增 1 |
g_tick_cnt | u16 | 0 | 节拍序号,每 250 拍归零 |
API 参考
RTC 设备层(rtc.h)
void rtc_dev_init(const struct rtc_config_init *rtc)
初始化 RTC 设备:装载默认时间/闹钟、设置时钟源与闹钟使能,并从 VM 恢复已保存上下文。
参数:
rtc:struct rtc_config_init *,初始化配置,含default_sys_time、default_alarm、cbfun、rtc_clk、alm_en。
返回: 无。
void rtc_dev_deinit(void)
反初始化 RTC 设备,停止走时并释放资源(重新初始化前必须调用)。
返回: 无。
void rtc_read_time(struct sys_time *time)
读取当前日历时间到 time。
参数: time(struct sys_time *):输出参数,填充 year/month/day/hour/min/sec。
返回: 无。
void rtc_write_time(const struct sys_time *time)
设置当前日历时间,并同步持久化到 VM。
参数: time(const struct sys_time *):输入参数,目标时间。
返回: 无。
void rtc_read_alarm(struct sys_time *time) / void rtc_write_alarm(const struct sys_time *time)
读写闹钟时间,语义与时间读写一致。
void rtc_alarm_switch(u32 en)
参数: en(u32):非 0 使能闹钟,0 关闭。
void rtc_save_context_to_vm(void)
将当前 RTC 上下文(走时/闹钟/唤醒标志)主动写入 VM。建议在进入 softoff 前调用。
void rtc_reset_save_time(void)
清除 VM 中的走时记录,下次上电回落到 default_sys_time。
u32 rtc_is_alarm_en(void)
返回: 1 表示闹钟已使能,0 表示未使能。
u32 rtc_is_alarm_wkup(void)
返回: 1 表示本次系统启动由闹钟唤醒,0 表示其他原因。
u32 rtc_get_clk_sel(void)
返回: 当前走时时钟源选择(CLK_SEL_LRC / CLK_SEL_32K / CLK_SEL_BTOSC)。
void vir_rtc_wakeup_enable(u32 wkup_ms) / void vir_rtc_wakeup_disable(void)
配置/取消 softoff 定时唤醒,wkup_ms 单位为毫秒。
日历换算工具(rtc.h)
| 函数 | 说明 |
|---|---|
bool leapyear(u32 year) | 判断 year 是否为闰年 |
u32 year_to_day(u32 year) | 公元 year 年之前(含)累计天数 |
u32 month_to_day(u32 year, u32 month) | year 年 month 月之前累计天数(处理闰年二月) |
void day_to_ymd(u32 day, struct sys_time *sys_time) | 累计天数换算为日历时间 |
u32 ymd_to_day(struct sys_time *time) | 日历时间反向换算为累计天数 |
u32 caculate_weekday_by_time(struct sys_time *r_time) | 计算指定日期为星期几 |
u32 get_day_of_month(u32 year, u32 month) | 返回该月天数(28/29/30/31) |
Tick Timer(tick_timer_driver.h)
| 函数/变量 | 说明 |
|---|---|
extern volatile u32 jiffies | 全局节拍计数(每 5 拍递增 1,约 10ms 粒度) |
void tick_timer_init(void) | 初始化硬件节拍定时器并启动中断 |
void tick_timer_sleep_init() | 低功耗睡眠场景下重新初始化节拍 |
void modules_tick_timer(u32 cnt) | 模块层周期钩子(weak,可覆盖),cnt 为节拍序号 |
void tick_timer_set(bool on_off) | 软开关节拍 ISR(always_inline) |
bool tick_timer_close(void) | 查询节拍 ISR 是否被关闭(always_inline) |
void sys_tick_timer(u32 cnt) | 系统级周期分发:DAC 渐变、100ms 事件、500ms 消息 |
void tick_timer_loop() | 节拍主循环:g_tick_cnt++ 后依次调用三层分发 |
void os_time_dly(u32 tick) | 基于 jiffies 的忙等待延时,单位:节拍(10ms 粒度) |
void delay_10ms(u32 tick) | 等价忙等待,语义为 10ms 倍数 |
void app_timer_loop(void) | 应用层周期钩子(weak,可覆盖),无参数 |
专业笔记
失败模式与边界情况
- VM 未使能(
SYS_VM_EN=0):rtc_mem_save/rtc_mem_read经宏展开为直接返回-1的空操作,RTC 时间不会持久化,softoff/掉电后时间丢失。上层代码必须对持久化失败有容忍度(时间回到默认值)。 - 首次上电无上下文:VM 中无走时记录时,虚拟 RTC 回落到
rtc_config_init.default_sys_time/default_alarm。产品必须保证default_sys_time合理,否则会显示错误时间。 #if 0测试代码不可用:rtc.c中的rtc_test与演示结构体被#if 0屏蔽,不能直接调用;它只是用法参考。真实入口是rtc_dev_init系列。- 忙等待阻塞:
os_time_dly/delay_10ms是忙等待循环,在调用期间不释放 CPU、不响应消息;若在节拍 ISR 被关闭(tick_timer_set(true))期间调用,jiffies不递增,会死循环。只应在节拍正常运行时用于短延时。 - 32 位回绕:延时实现使用
time_after处理jiffies回绕,业务代码比较jiffies差值时也应遵循同样的宏约定,避免直接a - b在回绕点出错。 - 取模周期与节拍周期耦合:
cnt % 5/% 50/% 250的周期取决于硬件节拍周期(如 2ms)。若芯片/时钟配置改变节拍周期,这些周期任务的实际间隔会成比例变化,需要同步调整取模值。
并发与一致性
jiffies、isr_tick_timer_close、g_tick_cnt均为volatile,在中断上下文与轮询上下文共享。tick_timer_set/tick_timer_close被强制内联以最小化读写窗口。jiffies只在sys_tick_timer的cnt % 5分支递增,读端(延时函数)在 while 循环中反复读取,无需显式临界区即可获得单调语义。g_tick_cnt每 250 拍归零,且与 500ms 消息周期对齐;模块层若自行维护计数,需注意cnt回绕到 0 的时刻,避免把"归零"误判为"新纪元"。
性能与运维考量
- 中断开销预算:
tick_timer_loop内的所有逻辑都会计入每个节拍的中断/主循环开销。dac_fade_api、modules_tick_timer等钩子应保持轻量;重负载业务应通过MSG_500MS/B_EVENT_100MS消息投递到消息循环中异步执行,而不是直接写在钩子里。 - 低功耗切换:softoff 前应调用
rtc_save_context_to_vm保存上下文,并通过vir_rtc_wakeup_enable设置唤醒;唤醒后查询rtc_is_alarm_wkup区分唤醒原因。tick_timer_sleep_init负责睡眠期间的节拍重初始化。 - 时钟源选择:
CLK_SEL_LRC(内部低频)功耗最低但精度一般;CLK_SEL_32K依赖外部晶振,精度更高;CLK_SEL_BTOSC复用蓝牙振荡器。走时精度要求高的产品应选 32K,并注意晶振起振时间。 - flash cache 联动:
B_EVENT_100MS仅在FLASH_CACHE_ENABLE && EXT_FLASH_EN时投递,用于外部 flash 缓存定时刷新;外部 flash 方案的代码路径依赖该节拍保持 cache 一致性。
扩展点
- 应用周期钩子:覆盖弱函数
app_timer_loop(),注入应用级无参周期逻辑。 - 模块周期钩子:覆盖弱函数
modules_tick_timer(u32 cnt),用cnt % N派生任意周期任务(tick_timer_modules.c即此类注册中心)。 - RTC 虚拟层:
VIRTUAL_RTC_EN使能后,virtual_rtc.h提供rtc_simulate_init、read_sys_time、write_alarm等底层实现;需要自定义走时策略(如外部 RTC 芯片)时,可替换该层并保持rtc_dev_*接口不变。 - 日历换算工具:
leapyear/day_to_ymd等纯函数可直接复用于业务侧(如日期计算、星期显示)。
测试覆盖
rtc.c内#if 0的rtc_test演示了时间/闹钟读写与走时打印的冒烟测试路径,涵盖:读时间→写时间→读回→读闹钟→写闹钟→读回→周期打印。- 示例中还展示了
sys_power_down/power_set_soft_poweroff与 RTC 联动的休眠测试思路(被注释,需按需启用)。 - 定时器侧无独立测试文件,但其节拍行为通过
B_EVENT_100MS/MSG_500MS的系统消息间接验证;gptimer_demo.c提供硬件 GPTimer 外设的参考测试。