CH58x 蓝牙芯片 SysTick、RTC、TMRx

复制代码
理一理沁恒微 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 的使用和区别,单从定时功能的示例应用来说,是简单的。但是从如何合理的选择应用场合来说,是值得思考的。

我们也分析了官方示例的相关代码,如果自己比较模糊,官方示例的使用方式已经可以满足大部分的应用功能。如果后面有更多的使用细节,博主也会在遇到的时候更新博文。

好了,本文就到这里。谢谢大家!

相关推荐
chen_song_5 天前
低时延迟流媒体之WebRTC协议
webrtc·rtc·流媒体
矜辰所致13 天前
CH585 高速 USB模拟 CDC串口应用示例
沁恒微·risc-v·usb·cdc串口·usb 模拟串口
嵌入式老牛21 天前
DrvBsp_I2C驱动_RTC(一)
单片机·嵌入式硬件·rtc
嵌入式老牛22 天前
【无标题】
单片机·嵌入式硬件·rtc
Felven24 天前
统信系统下设置RTC时间
linux·rtc·1024程序员节
普中科技1 个月前
【普中DSP28335开发攻略】-- 第 7 章 F28335时钟及控制系统
单片机·嵌入式硬件·时钟·dsp28335·普中科技
Silicore_Emma1 个月前
芯谷科技--I²C 串行实时时钟,为系统提供持久、精准的时间基准D1307
科技·实时音视频·低功耗·rtc·时间基准解决方案·双电源
macheria1 个月前
Qualcomm SM6115 平台RTC accuracy problem
rtc·qualcomm·sm6115·qcm4290
私人珍藏库1 个月前
[Android] Alarm Clock Pro 11.1.0一款经典简约个性的时钟
android·时钟