理一理沁恒微 RISC-V 蓝牙芯片 CH58x 的滴答定时器,RTC 和通用定时器 ...... 矜辰所致
前言
在芯片是的使用过程中,我们有时候需要计算时间,那用什么来计时,我们很容易想到 SysTick、RTC、TMRx ,它们表面上都可以用来 "计时" 。但是我们使用哪一个,怎么用,对于很多小伙伴来说还是稀里糊涂的。
所以本文我们主要来说明一下 CH585 芯片上的 SysTick、RTC 的应用 以及 TMRx 的基础定时功能。
沁恒微 RISC-V 芯片学习系列博文:
【导航】沁恒微 RISC-V 蓝牙 入门教程目录 【快速跳转】.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 基础介绍](#一、 基础介绍)
-
- [1.1 基础定义](#1.1 基础定义)
- [1.2 区别](#1.2 区别)
- [1.3 官方代码应用](#1.3 官方代码应用)
- [二、 使用示例](#二、 使用示例)
-
- [2.1 SysTick 使用示例](#2.1 SysTick 使用示例)
- [2.2 RTC 使用示例](#2.2 RTC 使用示例)
-
- [2.2.1 时钟源选择](#2.2.1 时钟源选择)
- [2.2.2 RTC初始化](#2.2.2 RTC初始化)
- [2.2.3 RTC 中断配置](#2.2.3 RTC 中断配置)
- [2.2.4 RTC 唤醒功能](#2.2.4 RTC 唤醒功能)
- [2.2.5 示例演示](#2.2.5 示例演示)
- [2.3 TMRx 使用示例](#2.3 TMRx 使用示例)
- 三、相关问题说明
-
- [3.1 CH592 SysTick](#3.1 CH592 SysTick)
- [3.2 Ble 工程中的 RTC 和 SysTick](#3.2 Ble 工程中的 RTC 和 SysTick)
- 结语
写在最前面:在官方示例中,需要跑蓝牙低功耗计时、唤醒,就用 RTC ,要用高速(8K) RF 通信就用通用定时器。 SysTick 被用作给 BLE协议栈提供随机数种子,开启了,但是关闭了中断。
RTC 的时钟来源是 32.768KHZ 的晶振,蓝牙的 625us 基准时间来源也是通过 32.768KHZ 晶振硬件自动产生(外部时钟为 32.768K,而内部时钟默认是 32K ,使用内部需要校准)。
TMRx 的输入时钟就是系统主频(PCLK = HCLK)。
一、 基础介绍
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 区别
上面三者表面上都可以用来"计时",但设计初衷和应用场景完全不同。
SysTick(系统定时器)
设计初衷:
作为操作系统的"心脏"
------ 为RTOS提供精确的时间片轮转基础
------ 确保多任务公平获得CPU时间
------ 维持系统内部的时间秩序和节奏
在沁恒微 RISC-V 蓝牙芯片上,跑蓝牙的时候会有官方自己的 TMOS 调度机制,不太建议跑 RTOS ,因为要保证蓝牙稳定的工作,需要针对性的对 RTOS 的任务调度进行处理,相对复杂。
Systick 被蓝牙例程用来生成随机数种子。
RTC(实时时钟)
设计初衷:
作为系统的"日历时钟"
------ 记录真实的绝对时间(年月日时分秒)
------ 在系统断电后保持时间连续性
在官方示例中,RTC 是低功耗模式下的唤醒管理,可以提供真实的年月日时分秒,设备重启/睡眠后时间不丢失。
通用定时器(TMRx)
设计初衷:
作为硬件的 "精密工具包"
------ 精确控制外部硬件(PWM、输入捕获等)
------ 减轻CPU负担,硬件自动完成定时任务
------ 为特定应用提供定时
除了基本的定时器应用,在蓝牙或者 RF 例程中时钟,比如 2.4G RF 8K 收发,使用定时器,定时 125us 实现。
1.3 官方代码应用
我们可以查看一下官方蓝牙例程中的与时钟有关的部分(解释直接看代码注释):
在CH58x_BLEInit(); 函数中有:
c
#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(); 系统时钟初始化:
c
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 );
}
...
疑问:8K?示例是1K上面示例定时器参数设置为
GetSysClock() / 1000,速率只是1Khz,如果要跑 8K 应该还需要处理的。...
如果蓝牙示例使能了低功耗,会需要使用 RTC 唤醒,在 HAL_Init(); ---> HAL_SleepInit(); 配置睡眠唤醒的方式 - RTC唤醒,触发模式:
c
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 应用其他相关代码:
c
__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) 开启滴答定时器,参数为重装载值,会自动开启中断。
在中断函数中清除中断即可,示例如下:
c
#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 使用示例
RTC 时钟源:32.768 KHZ
精度
外部:20ppm 以内
内部:0.04%-0.5%(400ppm-5000ppm)
我们在上面 HAL_TimeInit 中已经看到过如何开启内部还是外部时钟源,方式可用,但是不够直观,官方提供了库函数给我们使用。
2.2.1 时钟源选择
时钟源选择使用 LClk32K_Select :
c
/*注意,,如果是切换,要先关闭之前用的时钟
sys_safe_access_enable();
R8_CK32K_CONFIG &= ~(RB_CLK_OSC32K_XT | RB_CLK_XT32K_PON);// 关闭外部时钟,和下面关闭内部选其一
R8_CK32K_CONFIG &= ~RB_CLK_INT32K_PON;//关闭内部时钟
sys_safe_access_disable();
*/
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();
这里有个疑问:需不需要R8_CK32K_CONFIG |= RB_CLK_INT32K_PON;给内部时钟上电这条语句呢?测试下来是不需要的。
RTC 时钟源系统默认是内部的,如果需要从内部切换到外部,官方手册有说明:

步骤就是按照上面HAL_TimeInit 中的来就可以,先关闭内部时钟,然后再使能外部32.768kHz晶振和外部晶振上电。
2.2.2 RTC初始化
初始化就一句话:
c
RTC_InitTime(2025, 11, 11, 0, 0, 0); //RTC时钟初始化当前时间
在蓝牙程序中,除初始化外,在程序运行中不可再调用此函数,否则会影响 tmos 和蓝牙的运行,可以将设置的时间值与当前时间作差,获取时间时加上这个差值。
2.2.3 RTC 中断配置
两种中断方式:
c
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);
使能中断:
c
PFIC_EnableIRQ(RTC_IRQn); //中断服务使能
中断处理:
c
__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 唤醒功能
配置代码如下:
c
PWR_PeriphWakeUpCfg( ENABLE, RB_SLP_RTC_WAKE, Edge_LongDelay ); //RTC使能唤醒功能
2.2.5 示例演示
示例我就直接上图:

外部也是一样用:
c
// 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 示例,我们简单修改一下示例:
c
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 CH592 SysTick
CH592 内核自带了一个64位计数器(SysTick),支持HCLK或者HCLK/8作为时基,具有较高优先级。
使用方式和 CH585 是一样的,只是在SysTick_Config 实现上有细节上的不同:

3.2 Ble 工程中的 RTC 和 SysTick
在 Ble 工程中,如果使用的是内部 RTC ,会开启 2 分钟的内部校准。
c
#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 计数值:
c
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 的使用和区别,单从定时功能的示例应用来说,是简单的。但是从如何合理的选择应用场合来说,是值得思考的。
我们也分析了官方示例的相关代码,如果自己比较模糊,官方示例的使用方式已经可以满足大部分的应用功能。如果后面有更多的使用细节,博主也会在遇到的时候更新博文。
好了,本文就到这里。谢谢大家!