CH58x 蓝牙芯片 SysTick、RTC、TMRx
理一理沁恒微 RISC-V 蓝牙芯片 CH58x 的 SysTick,RTC 和通用定时器 ...... 矜辰所致
...... 优化时钟源说明部分 2025/12/6
...... 修改 RTC 时钟源选择部分说明 2025/12/8
...... 增加部分相关问题说明 2026/5/21
前言
在芯片是的使用过程中,我们有时候需要计算时间,那用什么来计时,我们很容易想到 SysTick、RTC、TMRx ,它们表面上都可以用来 ”计时“ 。但是我们使用哪一个,怎么用,对于很多小伙伴来说还是稀里糊涂的。
所以本文我们主要来说明一下 CH585 芯片上的 SysTick、RTC 的应用 以及 TMRx 的基础定时功能。
沁恒微 RISC-V 芯片学习系列博文:
【导航】沁恒微 RISC-V 蓝牙 入门教程目录 【快速跳转】
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
写在最前面:
在官方示例中,需要跑蓝牙低功耗计时、唤醒,使用 RTC 。
要用高速(8K) RF 通信就用通用定时器(本文只限定定时功能)。
SysTick 被用作给 BLE协议栈提供随机数种子,开启了,但是关闭了中断 。
一、 基础介绍
1.1 基本说明
基本说明直接引用官方芯片手册:
SysTick :内核节拍器
内核自带了一个32位计数器(SysTick),支持HCLK或者HCLK/8作为时基,具有较高优先级。
RTC:实时时钟
实时时钟(RTC)是一个独立的定时器,包含一组连续计数的计数器。在相应软件配置下,可提
供简单日历功能。修改计数器的值可以重新设置当前的时间和日期。
RTC寄存器与PMU一样常供电,在系统复位或从低功耗模式唤醒后,RTC的设置和时间维持不变。
TMRx:通用定时器
芯片提供了4 个26 位定时器,TMR0、TMR1、TMR2 和TMR3,最长定时时间为2^26 个时钟周期。
它适用于多种场合,包括测量输入信号脉冲长度(输入捕捉)或者产生输出波形(PWM),支持DMA功
能。每个定时器都是完全独立的,可以一起同步操作。
1.2 时钟源
RTC 的时钟来源是 32KHz 的低频晶振(内部或者外部)。
在官方工程中,有一个CONFIG.h文件,里面有一个CLK_OSC32K 宏定义,决定了 RTC 的时钟来源。
CONFIG.h文件里面的宏定义很重要,一定要熟悉一下,有很多需求其实只要了解这个头文件只需要简单的修改就可以实现。

SysTick 的以系统主频为时基,时钟来源就是 32 MHz 高速晶振,因为我们的芯片应用高速晶振是必须的,这里通常都是来自于外部的 32Mhz 晶振 (只有不使用蓝牙,不使用高速USB,不使用 NFC 等应用的时候,极小部分情况下才可以不带高速晶振,这里不做考虑)。
TMRx 的以系统主频为时基,时钟来源就是 32 MHz 高速晶振,同上。
1.3 区别
上面三者表面上都可以用来"计时",但设计初衷和应用场景完全不同。
SysTick(系统定时器)
设计初衷:
作为操作系统的"心脏"
—— 为RTOS提供精确的时间片轮转基础
—— 确保多任务公平获得CPU时间
—— 维持系统内部的时间秩序和节奏
在沁恒微 RISC-V 蓝牙芯片上,跑蓝牙的时候会有官方自己的 TMOS 调度机制,不太建议跑 RTOS ,因为要保证蓝牙稳定的工作,需要针对性的对 RTOS 的任务调度进行处理,相对复杂。
Systick 被蓝牙例程用来生成随机数种子。
RTC(实时时钟)
设计初衷:
作为系统的"日历时钟"
—— 记录真实的绝对时间(年月日时分秒)
—— 在系统断电后保持时间连续性
在官方示例中,RTC 是低功耗模式下的唤醒管理,可以提供真实的年月日时分秒,设备重启/睡眠后时间不丢失。
通用定时器(TMRx)
设计初衷:
作为硬件的 “精密工具包”
—— 精确控制外部硬件(PWM、输入捕获等)
—— 减轻CPU负担,硬件自动完成定时任务
—— 为特定应用提供定时
除了基本的定时器应用,在蓝牙或者 RF 例程中时钟,比如 2.4G RF 8K 收发,使用定时器,定时 125us 实现。
1.4 官方代码应用
我们可以查看一下官方蓝牙例程中的与时钟有关的部分(解释直接看代码注释):
在CH58x_BLEInit(); 函数中有:
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFFF)
...
__SysTick_Config(SysTick_LOAD_RELOAD_Msk);// 配置SysTick并打开中断,设置最大重装值
PFIC_DisableIRQ(SysTick_IRQn);//关闭 Systick 中断
...
cfg.srandCB = SYS_GetSysTickCnt; //给bleConfig_t做随机数种子
在 HAL_Init(); —> HAL_TimeInit(); 系统时钟初始化:
void HAL_TimeInit(void)
{
bleClockConfig_t conf; // 定义一个“时钟配置”结构体,给 BLE 协议栈用
// ==================== 第一部分:32K时钟源配置 ====================
// 根据CLK_OSC32K宏定义选择使用外部晶振还是内部RC振荡器
#if(CLK_OSC32K) // 如果宏 = 1 → 用“内部 32 k RC”;= 0 → 用“晶振 32.768 k”
sys_safe_access_enable(); /// 开启安全访问模式,允许修改关键寄存器
R8_CK32K_CONFIG &= ~(RB_CLK_OSC32K_XT | RB_CLK_XT32K_PON); // 清除外部晶振使能位和外部晶振上电位(先关闭外部晶振)
sys_safe_access_disable(); // 关闭安全访问模式
sys_safe_access_enable();
R8_CK32K_CONFIG |= RB_CLK_INT32K_PON; // 打开内部 32 k RC
sys_safe_access_disable();
LSECFG_Current(LSE_RCur_100); // RC 省电电流档
Lib_Calibration_LSI(); // 校准内部 RC 精度
#else // 用晶振分支
sys_safe_access_enable();
R8_CK32K_CONFIG &= ~RB_CLK_INT32K_PON; // 清除内部32K RC振荡器使能位(先关闭内部RC)
sys_safe_access_disable();
sys_safe_access_enable();
R8_CK32K_CONFIG |= RB_CLK_OSC32K_XT | RB_CLK_XT32K_PON; // 使能外部32.768kHz晶振和外部晶振上电
sys_safe_access_disable();
#endif
// ==================== 第二部分:RTC实时时钟初始化 ====================
// 初始化RTC时间为2020年1月1日0点0分0秒(这是一个默认起始时间)
// 在实际应用中,这里应该设置为当前真实时间
RTC_InitTime(2020, 1, 1, 0, 0, 0); // 给 RTC 设一个起点,后面 BLE 拿它当“绝对时间”
// ==================== 第三部分:BLE时钟配置 ====================
// 初始化BLE时钟配置结构体(全部清零)
tmos_memset( &conf, 0, sizeof(bleClockConfig_t) );
// 配置时钟精度:如果使用外部晶振精度为50ppm,内部RC为1000ppm
// 外部晶振精度高(50ppm),内部RC精度差(1000ppm)
conf.ClockAccuracy = CLK_OSC32K ? 1000 : 50; // 时钟精度(RC=1000 ppm,晶振=50 ppm)
conf.ClockFrequency = CAB_LSIFQ; //这里如果是外部32.768K ,如果是用内部就是32KHZ
//这个最大值是以1s=37268 记一天的值即0xA8C00000(32768 *3600 *24U)
//触发值设置范围为[1,0xA8C00000],设置其他值将会永远不触发,
conf.ClockMaxCount = RTC_MAX_COUNT; // 设置RTC最大计数值(防溢出)0xA8C00000
conf.getClockValue = SYS_GetClockValue; // 设置获取时钟值的回调函数指针,协议栈要“当前秒”时调这个
conf.SetPendingIRQ = SYS_SetPendingIRQ; // 设置中断挂起的回调函数指针,协议栈要“软中断”时调这个
#if RF_8K // ④ 如果定义了 RF_8K(蓝牙高速时基)
conf.Clock1Frequency = GetSysClock()/1000; // = 62.4 MHz ÷1000 = 62.4 kHz
conf.getClock1Value = SYS_GetClock1Value; // 读 TMR3 计数
conf.SetClock1PendingIRQ = SYS_SetClock1PendingIRQ; // 设置高速时钟中断回调,TMR3 软中断
conf.SetTign = SYS_SetTignOffest; // 微调 RF 时隙偏移 ,时间偏移校正回调函数
TMR3_ITCfg(ENABLE, TMR0_3_IT_CYC_END); // 使能TMR3的循环结束中断
PFIC_EnableIRQ(TMR3_IRQn); // 允许 TMR3 进中断
#endif
// ==================== 第五部分:TMOS定时器系统初始化 ====================
// 使用配置好的参数初始化TMOS(Timer Operating System)定时器
// 这是BLE协议栈的时间管理核心
TMOS_TimerInit( &conf );
}
疑问:
按照博主个人认知的流程应该是先给对应的晶振上电,等待稳定,再配置为对应时钟源,最后再选择性的关闭不用的时钟源的电。
示例上是先关闭不用的,然后再开启需要用的,电源和使能同时进行。
不过呢,因为这是是刚上电的初始化,还没有开启需要用到 32K 时钟源的设备,所以这里也不会导致什么问题。
…
上面示例定时器参数设置为GetSysClock() / 1000,定时时间为 1ms,速率只是1Khz,如果要跑 8K 应该还需要处理的。
…
如果蓝牙示例使能了低功耗,会需要使用 RTC 唤醒,在 HAL_Init(); —> HAL_SleepInit(); 配置睡眠唤醒的方式 - RTC唤醒,触发模式:
void HAL_SleepInit(void)
{
#if(defined(HAL_SLEEP)) && (HAL_SLEEP == TRUE)
sys_safe_access_enable();
R8_SLP_WAKE_CTRL |= RB_SLP_RTC_WAKE; // RTC唤醒
sys_safe_access_disable();
sys_safe_access_enable();
//触发模式就是一次性的“单发闹钟”
//硬件自动中断 + 自动唤醒,自动清除标志位,只触发一次
R8_RTC_MODE_CTRL |= RB_RTC_TRIG_EN; // 触发模式
sys_safe_access_disable();
PFIC_EnableIRQ(RTC_IRQn);
#endif
}
RTC 应用其他相关代码:
__HIGH_CODE
void RTC_SetTignTime(uint32_t time)
{
sys_safe_access_enable();
R32_RTC_TRIG = time;
sys_safe_access_disable();
RTCTigFlag = 0;
}
__INTERRUPT
__HIGH_CODE
void RTC_IRQHandler(void)
{
R8_RTC_FLAG_CTRL = (RB_RTC_TMR_CLR | RB_RTC_TRIG_CLR);
RTCTigFlag = 1;
}
官方的示例使用思路可以给大家提供参考,下面我们对这 3 种定时器分别做单独的 Demo 测试。
二、 使用示例
2.1 SysTick 使用示例
使用 SysTick_Config(uint32_t ticks) 开启滴答定时器,参数为重装载值,会自动开启中断。
在中断函数中清除中断即可,示例如下:
#include "CH58x_common.h"
int main()
{
HSECFG_Capacitance(HSECap_18p);
//SYSCLK_FREQ CLK_SOURCE_HSE_PLL_62_4MHz
SetSysClock(SYSCLK_FREQ);
GPIOA_SetBits(GPIO_Pin_14);
GPIOPinRemap(ENABLE, RB_PIN_UART0);
GPIOA_ModeCfg(GPIO_Pin_15, GPIO_ModeIN_PU);
GPIOA_ModeCfg(GPIO_Pin_14, GPIO_ModeOut_PP_5mA);
UART0_DefInit();
/*
FREQ_SYS 62400000 每秒中断一次
FREQ_SYS/1000 1ms中断一次
也可以使用 GetSysClock() 获取当前系统时钟
*/
SysTick_Config(FREQ_SYS); //每秒中断一次
while(1){
}
}
__INTERRUPT
__HIGH_CODE
void SysTick_Handler() /***嘀嗒定时器中断函数***/
{
SysTick->SR = 0; //清除中断标志
PRINT("systick IRQ!\r\n");
}
还有一个函数SYS_GetSysTickCnt 可以获取计数的值,因为 SysTick 的时钟为系统时钟,所以每 1 / 62.4 µs 计数器 +1。这个计数值,只要进入中断,就会清 0 !
比如我们修改一下测试代码,测试结果如下:

还要说明的是,SysTick 在经过睡眠唤醒后,计数会被清除。
2.2 RTC 使用示例
我们在上面 HAL_TimeInit 中已经看到过如何开启内部还是外部时钟源,除了上面直接操作寄存器,官方还提供了库函数给我们使用。
2.2.1 时钟源选择
手册中有:

时钟源选择使用 LClk32K_Select :
// 选择内部
sys_safe_access_enable();
R8_CK32K_CONFIG |= RB_CLK_INT32K_PON; //给外部32K上电
sys_safe_access_disable();
LClk32K_Select(Clk32K_LSI); //启用内部32K
// 选择外部
sys_safe_access_enable();
R8_CK32K_CONFIG |= RB_CLK_XT32K_PON; //给外部32K上电
sys_safe_access_disable();
LClk32K_Select(Clk32K_LSE); //启动外部时钟源
RTC 时钟源系统默认是内部的,如果需要从内部切换到外部,官方手册有说明:

同时需要配置 CLK_OSC32K 宏定义。
关于晶振的稳定时间,在手册中也有说明:

对于官方示例代码来说,选择完低频时钟来源,在RTC_InitTime 里面有一个晶振是否起振的 “弱检测”,确保晶振至少已经起振了。
2.2.2 RTC初始化
初始化就一句话:
RTC_InitTime(2025, 11, 11, 0, 0, 0); //RTC时钟初始化当前时间
在蓝牙程序中,除初始化外,在程序运行中不可再调用此函数,否则会影响 tmos 和蓝牙的运行,可以将设置的时间值与当前时间作差,获取时间时加上这个差值。
同时 RTC 寄存器是掉电保持的,如果想用 RTC 做万年历,那么在shutdown 唤醒复位后不能再次对 RTC 进行初始化,可以获取一下芯片复状态寄存器判断是否需要进行初始化RTC。
2.2.3 RTC 中断配置
两种中断方式:
typedef enum
{
Period_0_125_S = 0, // 0.125s 周期
Period_0_25_S, // 0.25s 周期
Period_0_5_S, // 0.5s 周期
Period_1_S, // 1s 周期
Period_2_S, // 2s 周期
Period_4_S, // 4s 周期
Period_8_S, // 8s 周期
Period_16_S, // 16s 周期
} RTC_TMRCycTypeDef;
//定时模式 共八档可配置
RTC_TMRFunCfg(Period_2_S);
//触发方式,参数为相对当前时间的触发间隔时间,基于LSE/LSI时钟周期数
//32768为1s
RTC_TRIGFunCfg(32768*1);
使能中断:
PFIC_EnableIRQ(RTC_IRQn); //中断服务使能
中断处理:
__INTERRUPT
__HIGH_CODE
void RTC_IRQHandler(void)
{
if (RTC_GetITFlag(RTC_TRIG_EVENT)) {
//...
RTC_ClearITFlag(RTC_TRIG_EVENT);
}
if (RTC_GetITFlag(RTC_TMR_EVENT)) {
//...
RTC_ClearITFlag(RTC_TMR_EVENT);
}
//或者直接参考官方
//R8_RTC_FLAG_CTRL = (RB_RTC_TMR_CLR | RB_RTC_TRIG_CLR);
}
2.2.4 RTC 唤醒功能
配置代码如下:
PWR_PeriphWakeUpCfg( ENABLE, RB_SLP_RTC_WAKE, Edge_LongDelay ); //RTC使能唤醒功能
2.2.5 示例演示
示例我就直接上图:

外部也是一样用:
// LClk32K_Select(Clk32K_LSI); //启用内部32K
LClk32K_Select(Clk32K_LSE); //启动外部时钟源
sys_safe_access_enable();
R8_CK32K_CONFIG |= RB_CLK_XT32K_PON; //给外部32K上电
sys_safe_access_disable();
RTC_InitTime(2025, 11, 11, 0, 0, 0);
RTC_TMRFunCfg(Period_2_S);
PFIC_EnableIRQ(RTC_IRQn);
while(1){
}
...
__INTERRUPT
__HIGH_CODE
void RTC_IRQHandler(void)
{
UINT16 py; UINT16 pmon; UINT16 pd; UINT16 ph; UINT16 pm; UINT16 ps;
RTC_GetTime(&py,&pmon,&pd,&ph,&pm,&ps);
if (RTC_GetITFlag(RTC_TRIG_EVENT)) {
}
if (RTC_GetITFlag(RTC_TMR_EVENT)) {
PRINT("%d年%d月%d日%d时%d分%d秒\r\n",py,pmon,pd,ph,pm,ps);
PRINT("RTC_IRQ_TEST\r\n");
RTC_ClearITFlag(RTC_TMR_EVENT);
}
}
休眠唤醒

说明一下,上面前面两种低功耗模式正常唤醒,sleep 模式会操作 RTC ,导致了异常,大家可以自行查看PM_LowPower_Sleep 函数实现,这里不过多深入讨论,如果后期确实遇到问题,我们再来详细探讨。
2.3 TMRx 使用示例
本文我们只做基础的定时功能使用示例,具体 TMRx 的其他功能在其他地方用到的时候会有对应的说明, 比如 PWM 输出应用 :
我们还是来看 TMR 示例,我们简单修改一下示例:
TMR0_TimerInit( GetSysClock()); // 1S一次定时
TMR0_ITCfg(ENABLE, TMR0_3_IT_CYC_END); // 开启中断,周期结束标志
PFIC_EnableIRQ(TMR0_IRQn);
__INTERRUPT
__HIGH_CODE
void TMR0_IRQHandler(void) // TMR0 定时中断
{
if(TMR0_GetITFlag(TMR0_3_IT_CYC_END))
{
TMR0_ClearITFlag(TMR0_3_IT_CYC_END); // 清除中断标志
PRINT("TMR_IRQ\r\n");
}
}
测试结果就是 1s 一次产生 TMR 中断。
其中通过 TMR0_TimerInit 函数设定定时时间,定时器的时钟来源系统时钟 HCLK,所以 TMR0_TimerInit 定时间计算如下:
定时时间 = 参数 / 系统时钟 (秒)
.
比如想要中断时间位 125us:
TMR0_TimerInit( GetSysClock() / 8000); // 定时时间1/8000 秒
三、相关问题说明
本小节记录一下系列芯片中的一些关于这3种定时器的对应问题,以作记录(保持更新)。
3.1 RTC 做万年历说明
在官方手册中有介绍:

RTC 寄存器复位可以保持(除了掉电情况),但是要注意,我们如果使用 shutdown 模式的话,需要注意复位的时候不要再重新初始化 RTC 了。
我们示例中,在初始化阶段的 HAL_Init() --> HAL_TimeInit(); 就是对 RTC 初始化的代码。我们要做万年历可以根据复位状态寄存器判断是否需要进行初始化 RTC。
if(只有是上电复位才执行下面的初始化)
{
RTC_InitTime(2020, 1, 1, 0, 0, 0); //RTC时钟初始化当前时间}
}
还要注意,做万年历,需要将内部 32K 配置为 32768,配置后精度可以达到千分之一的参数要求 。
3.2 使用外部 HSE 作为时钟源
这里只是记录方法,使用了 HSE 作为时钟源的话,只是因为比内部 32K 相对准确一点,但是无法休眠,不能低功耗,RTC 没有初始化,无法正常计数,RTC 的时钟来源必须是 32K 时钟源(内/外部)。
将 CLK_OSC32K 配置为 0x82 和TMOS_TimerInit( NULL); 函数传NULL的作用都是将 HSE 配置为tmos和蓝牙的时基。
可以查看 TMOS_TimerInit 函数决定用哪种方式,如果函数注释如下:
/**
* @brief tmos system timer initialization
*
* @note must initialization before call tmos task
*
* @param fnGetClock - system clock select extend input,if NULL select HSE as the clock source
*
* @return SUCCESS if successful, FAILURE if failed.
*/
extern bStatus_t TMOS_TimerInit( bleClockConfig_t *pClockConfig );
使用 TMOS_TimerInit( NULL); 方式实现(CH584/5 CH592 )。
如果 TMOS_TimerInit 函数如下:
/**
* @brief tmos system timer initialization
*
* @note must initialization before call tmos task
*
* @param fnGetClock - 0:system clock select RTC timer
* valid:system clock select extend input
*
* @return SUCCESS if successful, FAILURE if failed.
*/
extern bStatus_t TMOS_TimerInit( bleClockConfig_t *pClockConfig );
使用 CLK_OSC32K=0x82 的方式实现(CH583/2 CH573/1)。
3.3 CH592 SysTick
CH592 内核自带了一个64位计数器(SysTick),支持HCLK或者HCLK/8作为时基,具有较高优先级。
使用方式和 CH585 是一样的,只是在SysTick_Config 实现上有细节上的不同:

3.4 Ble 工程中的 RTC 和 SysTick
在 Ble 工程中,如果使用的是内部 RTC ,会开启 2 分钟的内部校准。
#define BLE_CALIBRATION_PERIOD 120000
if(events & HAL_REG_INIT_EVENT)
{
uint8_t x32Kpw;
#if(defined BLE_CALIBRATION_ENABLE) && (BLE_CALIBRATION_ENABLE == TRUE) // 校准任务,单次校准耗时小于10ms
#ifndef RF_8K
BLE_RegInit(); // 校准RF,会关闭RF并改变RF相关寄存器,如果使用了RF收发函数需注意校准后再重新启用
#endif
#if(CLK_OSC32K)
Lib_Calibration_LSI(); // 校准内部RC
#elif(HAL_SLEEP)
x32Kpw = (R8_XT32K_TUNE & 0xfc) | 0x01;
sys_safe_access_enable();
R8_XT32K_TUNE = x32Kpw; // LSE驱动电流降低到额定电流
sys_safe_access_disable();
#endif
tmos_start_task(halTaskID, HAL_REG_INIT_EVENT, MS1_TO_SYSTEM_TIME(BLE_CALIBRATION_PERIOD));
return events ^ HAL_REG_INIT_EVENT;
#endif
}
如果没有定义 HAL_SLEEP=1 ,没开启低功耗,RTC 在运行但是不会产生中断,可以获取 RTC 的时间,如果开启了低功耗才会产生中断,用来唤醒设备,TMOS 任务会自动关联 RTC ,唤醒时间即为下一次任务的执行时间。
在 Ble 工程中,SysTick 是一起运行的,只是没有中断,前文已经有过说明,我们还可以确认一下 。
在例程中新建一个事件,定时读一下 RTC 时钟和 RTC 计数值:
if(events & RTC_EVT)
{
UINT16 py; UINT16 pmon; UINT16 pd; UINT16 ph; UINT16 pm; UINT16 ps;
RTC_GetTime(&py,&pmon,&pd,&ph,&pm,&ps);
printf("%d年%d月%d日%d时%d分%d秒\r\n",py,pmon,pd,ph,pm,ps);
printf("test systick%d\r\n",SYS_GetSysTickCnt());
tmos_start_task(Peripheral_TaskID, RTC_EVT, 1600);
return (events ^ RTC_EVT);
}

结语
本文研究了一下CH58x 蓝牙芯片 SysTick、RTC、TMRx 的使用和区别,单从定时功能的示例应用来说,是简单的。但是从如何合理的选择应用场合来说,是值得思考的。
我们也分析了官方示例的相关代码,如果自己比较模糊,官方示例的使用方式已经可以满足大部分的应用功能。如果后面有更多的使用细节,博主也会在遇到的时候更新博文。
好了,本文就到这里。谢谢大家!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)