杰理 SDK 文档中心
首页
首页
  • 入门指南

    • SDK 概述与芯片平台
    • 环境搭建与开发工具链
    • 编译、烧录与快速开始
  • 应用层开发

    • 语音玩具应用 voice_toy
    • 扩音器应用 voice_enhanced
    • 语音功能状态机 voice_func
    • 应用公共框架与配置
  • 音频子系统

    • 音频解码器与 MIDI 播放
    • 音频编码与录音
    • 音效算法(ANS、变调、变声、混响)
    • 音频输出、功放与硬件重采样
  • 存储与文件系统

    • 文件系统层(FAT、NOR_FS、SYDF 等)
    • 存储设备与设备管理
    • 参数存储 VM 与保留区
  • 系统机制

    • 消息与事件机制
    • 电源管理与低功耗
    • 固件升级机制
    • 外设驱动(按键、红外、SPI、USB)
    • 实时时钟与定时器
  • 构建系统与工具

    • 构建系统(Makefile 与 Code::Blocks)
    • 编译后处理与语音资源打包
  • 硬件平台与文档

    • 芯片平台与启动流程
    • 硬件文档、规格书与原理图

实时时钟与定时器

本文档详细介绍 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)属于存储机制页面。

概述

嵌入式设备中,"时间"由两个互补的机制共同提供:

  1. **RTC(实时时钟)负责回答"现在是几点几分、星期几"这类日历问题。AD24N SDK 使用虚拟 RTC(VIRTUAL_RTC_EN)**方案:并非依赖独立的硬件 RTC 外设,而是基于内部低频时钟(LRC/32K/BTOSC)或电源域计数,把时间值保存在 VM 中,从而在软关机(softoff)期间仍能维持时间走时与闹钟唤醒能力。
  2. **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() 查询原因

流程要点:

  1. 上电后应用调用 rtc_dev_init,虚拟 RTC 从 VM 恢复走时与闹钟上下文,保证 softoff 期间时间连续。
  2. 时间/闹钟的每次写入都同步持久化到 VM,防止异常掉电丢失。
  3. 闹钟到达时由虚拟 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常量1RTC 走时时钟源:内部 LRC
CLK_SEL_32K常量2RTC 走时时钟源:32K 晶振
CLK_SEL_BTOSC常量3RTC 走时时钟源:蓝牙振荡器
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_closevolatile u80节拍 ISR 软开关,运行时经 tick_timer_set 控制
jiffiesvolatile u320全局节拍计数,每 5 个节拍递增 1
g_tick_cntu160节拍序号,每 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 一致性。

扩展点

  1. 应用周期钩子:覆盖弱函数 app_timer_loop(),注入应用级无参周期逻辑。
  2. 模块周期钩子:覆盖弱函数 modules_tick_timer(u32 cnt),用 cnt % N 派生任意周期任务(tick_timer_modules.c 即此类注册中心)。
  3. RTC 虚拟层:VIRTUAL_RTC_EN 使能后,virtual_rtc.h 提供 rtc_simulate_init、read_sys_time、write_alarm 等底层实现;需要自定义走时策略(如外部 RTC 芯片)时,可替换该层并保持 rtc_dev_* 接口不变。
  4. 日历换算工具: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 外设的参考测试。

相关链接

  • rtc.c — RTC 公共层与 VM 桥接
  • rtc.h — RTC 设备 API 与日历工具声明
  • tick_timer_sys.c — 系统滴答节拍分发
  • tick_timer_modules.c — 模块级节拍注册
  • tick_timer_driver.h — Tick Timer 头文件契约
  • gptimer_demo.c — 硬件 GPTimer 外设示例
  • gptimer.h / gptimer_hw_v5.h — GPTimer 外设驱动(硬件定时器页)
  • vm_api.h — VM 存储子系统
  • 电源管理(softoff 唤醒、sys_power_down)与看门狗机制,参见系统电源管理页面。
Prev
外设驱动(按键、红外、SPI、USB)