【WCH蓝牙系列芯片】-基于CH592开发板—多种唤醒方式,唤醒四种睡眠模式
------------------------------------------------------------------------------------------------------------------------------------
从CH592的芯片手册中,也可以看到四种低功耗模式和对应的唤醒事件的要求,在唤醒事件中,都有I/O 或 RTC 或 BAT这三种方式唤醒,但是PM程序中只有GPIO中断唤醒的一个程序,所以这次将其他的唤醒的方式做一个补充说明。

1. 四种低功耗模式
| 模式 | 睡眠接口程序 | 程序是否继续运行 | 核心特征 | 典型用途 |
|---|---|---|---|---|
| Idle | LowPower_Idle() |
是 | 主要停止 CPU 内核,唤醒最快,省电程度最低 | 很短的空闲等待 |
| Halt | LowPower_Halt() |
是 | 进入深睡,高速时钟可停止,外设供电保留较多 | 需要较快恢复的间歇任务 |
| Sleep | LowPower_Sleep() |
是 | 按 POWER_PLAN 保留指定 RAM/模块,主 LDO 可关闭 |
电池设备周期采样 |
| Shutdown | LowPower_Shutdown() |
否 | 掉电范围最大;唤醒后复位并重新进入 main() |
长时间待机、运输模式 |
Idle、Halt、Sleep 在唤醒后,从低功耗函数调用的下一行继续运行,但是Shutdown 唤醒后会复位重启,重新执行程序。
2. 四种唤醒源
| 唤醒源 | 触发条件 |
|---|---|
| RTC 定时 | 固定周期事件 |
| RTC 触发 | 当前 RTC 计数加相对 tick 后匹配 |
| GPIO | GPIO中断触发唤醒 |
| BAT 低压 | VDD 低于低功耗监控门限 |
3、RTC定时模式和触发模式唤醒
在RTC唤醒中,有两个方式,一种就是定时模式唤醒,还有一种就是触发模式唤醒,在程序中利用宏来选择这两种方式测试这个RTC唤醒睡眠得过程。
int main(void) { uint8_t resetStatus; uint8_t resetKeep; /* 读取上一次复位原因 */ resetStatus = SYS_GetLastResetSta(); /* 读取复位保持寄存器 */ resetKeep = R8_GLOB_RESET_KEEP; /* 读取后清除保持寄存器 */ SYS_ResetKeepBuf(0); /* 设置系统时钟为PLL 60MHz */ SetSysClock(CLK_SOURCE_PLL_60MHz); /* 使能DC-DC */ PWR_DCDCCfg(ENABLE); //将没有使用的GPIO配置为上拉输入,避免引脚悬空导致额外功耗。 GPIOA_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); GPIOB_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); /* 初始化调试串口 */ DebugInit(); /* 初始化RTC低速时钟 */ RTCClockInit(); PRINT("\r\n"); PRINT("CH592 RTC wake-up demo, ChipID=%02x\r\n",R8_CHIP_ID); /* * 判断是否为Shutdown唤醒后产生的复位。 */ if((resetKeep == SHUTDOWN_WAKE_MARK) && ((resetStatus == RST_FLAG_GPWSM) || (resetStatus == RST_FLAG_SW))) { PRINT("Wake reason: shutdown wake-up reset\r\n"); } else { PRINT("Reset status: %u\r\n", resetStatus); } while(1) { #if (RTC_WAKE_MODE == RTC_WAKE_MODE_TIMER) PRINT("RTC timer wake mode, low-power mode=%lu\r\n", (uint32_t)DEMO_LOW_POWER_MODE); #elif (RTC_WAKE_MODE == RTC_WAKE_MODE_TRIGGER) PRINT( "RTC trigger wake after %lu seconds, " "low-power mode=%lu\r\n", (uint32_t)RTC_WAKE_SECONDS, (uint32_t)DEMO_LOW_POWER_MODE); #endif WaitDebugTxComplete(); //等待串口日志数据完全发送完毕。 RTCArmSelectedWakeup(); //根据RTC_WAKE_MODE配置RTC唤醒源。 EnterSelectedLowPowerMode(); //进入选择的低功耗模式。 /* * Idle、Halt和Sleep唤醒后从这里继续执行。 * Shutdown唤醒后会复位,不会执行下面的代码。 */ RestoreClockAfterDeepSleep(); ReportRTCWakeReason(); //打印RTC唤醒原因 PRINT("RTC wake-up, irq_flag=%u\r\n", rtcWakeFlag); DelayMs(10); } }
在主程序中,利用RTCArmSelectedWakeup()函数来选择RTC的触发模式或者定时模式唤醒,

通过EnterSelectedLowPowerMode()进入选择的低功耗模式。这里选择的是Sleep睡眠模式。

在定时模式中,利用配置RTC_TMRFunCfg(RTC_TIMER_PERIOD); 开启RTC定时模式,但是在这个定时模式中,时间的选择都是固定的,只有0.125s、0.25s、0.5s、1s、2s、4s、8s、16s周期。
RTC_TIMER_PERIOD = Period_8_S 表示每8秒产生一次RTC定时事件。


RTC 定时模式和触发模式共用 RTC 外设及 RTC 中断入口。为了防止上一次的配置影响本次睡眠,程序每一轮都调用:RTCPrepareWakeup()程序;配置使能 RTC 低功耗唤醒。

在RTC触发唤醒的模式中,在RTCArmSelectedWakeup函数中调用RTCArmTriggerWakeup函数,传入触发唤醒的时间是60秒

在RTCArmTriggerWakeup函数中,将ticks = 60UL * 32768UL;这样结果为ticks = 1,966,080;然后调用RTC_TRIGFunCfg(1966080);然后RTC 会根据当前 RTC 计数值生成新的触发目标值。达到目标值以后产生 RTC_TRIG_EVENT,从而进行唤醒。

在触发模式中,RTC的最大计数为0xA8C00000,
所以RTC 一天的计数值为 32768 × 86400 = 2,831,155,200 = 0xA8C00000 ( 86400 秒 = 24 小时);所以 CH592 的 RTC 32 kHz 计数范围正好对应约 24 小时,之后会回绕。
在RTC中断服务函数中,RTC 定时模式和 RTC 触发模式共用同一个中断入口。

发生定时事件RTC_TMR_EVENT后,判断 RTC_TMR_EVENT 是否有效,清除硬件事件标志。在软件变量中记录 RTC_WAKE_FLAG_TIMER。
发生触发目标匹配后:判断 RTC_TRIG_EVENT 是否有效。清除硬件事件标志。在软件变量中记录 RTC_WAKE_FLAG_TRIGGER。
通过串口日志和电流分析仪来观察这两种RTC睡眠唤醒的过程
RTC定时模式-8秒定时


RTC触发模式-60秒触发


在低功耗唤醒事件中也是有包含对低电压的状态的检测,这样如果是在电池供电场景或者电表的场景下,可能会出现电池电压过低需要更换电池时,突然停电需要保存数据。此时低电压监测就起到了作用

在电池电压检测配置寄存器(R8_BAT_DET_CFG)中,可以设置电压阈值参数配置,来应对不同的电压范围参数。在电池状态寄存器(R8_BAT_STATUS)中可以指示电池电压处于低电压状态或者是超低电压状态,也就是一个高精度监控和低精度监控模式。
#define BAT_MONITOR_LEVEL LPLevel_1V8 //电压门限设置
#define BAT_STATUS_MASK RB_BAT_STAT_LOW //选择低功耗电压监控档位
通过宏定义来选择使用的高精度监控的电压范围,或者是低功耗监控的电压范围

在程序中,先配置打开低功耗BAT比较器,在配置BAT为Sleep唤醒源。在ArmBATWake()函数中,先清楚标志位,开启新一轮的电压监控,
PWR_PeriphWakeUpCfg(ENABLE, RB_SLP_BAT_WAKE, Long_Delay); //开启BAT事件唤醒
/* * 返回1:当前电压正常,BAT唤醒已经设置。 * 返回0:当前已经低压,不进入Sleep,进行唤醒。 */ static uint8_t ArmBATWake(void) { batWakeFlag = 0; //清楚标志 PFIC_DisableIRQ(WDOG_BAT_IRQn); //关闭中断 PFIC_ClearPendingIRQ(WDOG_BAT_IRQn); //清除PFIC中遗留的中断待处理状态 PWR_PeriphWakeUpCfg(DISABLE, RB_SLP_BAT_WAKE, Long_Delay); //关闭BAT唤醒使能 PowerMonitor(DISABLE, BAT_MONITOR_LEVEL); //关闭上一次的电源监控 PowerMonitor(ENABLE, BAT_MONITOR_LEVEL); //开启电源监控 DelayMs(2); /* BAT是状态判断。检查当前是否已经低压;入睡前已经低压时不能直接执行WFI睡眠 */ if(BATIsLow()) { return 0; } PWR_PeriphWakeUpCfg(ENABLE, RB_SLP_BAT_WAKE, Long_Delay); //开启BAT事件唤醒 PFIC_ClearPendingIRQ(WDOG_BAT_IRQn); //清除PFIC中遗留的中断待处理状态 PFIC_EnableIRQ(WDOG_BAT_IRQn); //开启中断 return 1; }
//禁止芯片再次进入BAT中断。
static void FinishBATCycle(void)
{
PFIC_DisableIRQ(WDOG_BAT_IRQn); //关闭中断
PWR_PeriphWakeUpCfg(DISABLE, RB_SLP_BAT_WAKE, Long_Delay); //关闭BAT唤醒使能
PowerMonitor(DISABLE, BAT_MONITOR_LEVEL); //关闭电源监控
PFIC_ClearPendingIRQ(WDOG_BAT_IRQn); //清除PFIC中遗留的中断待处理状态
}
在BATIsLow函数返回值中读取硬件状态,判断当前是低电压状态,还是正常电压的状态。
//读取当前硬件状态, 1:当前低压; 0:当前电压正常。
static uint8_t BATIsLow(void)
{
return (R8_BAT_STATUS & BAT_STATUS_MASK) ? 1U : 0U;
}
在主函数中,先上电判断当前的电压状态,如果ArmBATWake()返回0,即当前已经低压,就不进入睡眠状态,等待电源电压稳定,禁止芯片再次进入BAT中断。
如果上电正常情况下,在睡眠期间发发生了低电压状态,就会触发低电压的中断,触发唤醒。
在中断函数中,WDOG与BAT共用中断,需要先确认是不是一般精度BAT低压事件,if(R8_BAT_STATUS & RB_BAT_STAT_LOW),然后再置标志位状态batWakeFlag = 1。
int main(void) { SetSysClock(CLK_SOURCE_PLL_60MHz); //主频设为60MHz PWR_DCDCCfg(ENABLE); //DC-DC功能 //GPIO配置为上拉输入,避免漏电 GPIOA_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); GPIOB_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); DebugInit(); PRINT("\r\nCH592 BAT low-power monitor wake demo\r\n"); PRINT("Path: LPLevel_2V5 -> WDOG_BAT_IRQn\r\n"); while(1) { uint8_t enteredSleep = 0; //进入睡眠的标志位 uint8_t wasBATEvent; if(!ArmBATWake()) //上电时已经低压[ArmBATWake() 返回0,即当前已经低压] { PRINT("VDD is already below the LP threshold; raise VDD.\r\n"); WaitDebugTxComplete(); //确保在睡眠之前,串口能正常打印日志 WaitBATRecovery(); //低电压状态,程序就一直在这个状态下 FinishBATCycle(); //禁止芯片再次进入BAT中断。 DelayMs(100); continue; } PRINT("BAT armed; enter Sleep, then lower VDD.\r\n"); WaitDebugTxComplete(); //确保在睡眠之前,串口能正常打印日志 /* * ISR只关中断请求、不关monitor,因此WFI会被仍有效的BAT条件立即释放。 */ if((batWakeFlag == 0) && !BATIsLow()) //正常电压状态 { enteredSleep = 1; LowPower_Sleep(RB_PWR_RAM24K | RB_PWR_RAM2K); //睡眠,//只保留24+2K SRAM 供电 } wasBATEvent = (batWakeFlag || BATIsLow()) ? 1U : 0U; // WaitBATRecovery(); //低电压状态,程序就一直在这个状态下 RestoreClockAfterWake(); //重新开启DC-DC和系统时钟重新切换到PLL 60MHz。 FinishBATCycle(); //禁止芯片再次进入BAT中断。 if(wasBATEvent && enteredSleep) { PRINT("Wake reason: BAT low-power monitor; VDD recovered.\r\n"); } else if(wasBATEvent) { PRINT("BAT became low before WFI; Sleep was skipped.\r\n"); } else { PRINT("Unexpected wake: BAT status was not active.\r\n"); } DelayMs(500); } } /* 低电压中断函数;WDOG与BAT共用中断,先确认是不是一般精度BAT低压事件 */ __INTERRUPT __HIGH_CODE void WDOG_BAT_IRQHandler(void) { if(R8_BAT_STATUS & RB_BAT_STAT_LOW) //判断是否为一般精度BAT低压事件 { uint8_t ctrl = R8_BAT_DET_CTRL; sys_safe_access_enable(); //使能安全访问寄存器保护, R8_BAT_DET_CTRL = ctrl & ~(RB_BAT_LOW_IE | RB_BAT_LOWER_IE); //清楚中断标志 sys_safe_access_disable(); //关闭安全访问 batWakeFlag = 1; //置标志位 } }
如果是使用高精度的电压检测,得利用NMI_Handler的NMI中断处理函数
__INTERRUPT
__HIGH_CODE
void NMI_Handler(void)
{
PRINT("R8_BAT_STATUS=%x. \n",R8_BAT_STATUS);
while(R8_BAT_STATUS&RB_BAT_STAT_LOWER) //等电压恢复
{
batWakeFlag = 1; //置标志位
}
}
在这个低功耗睡眠低电压唤醒的程序中,在LowPower_Sleep函数中,需要在睡眠函数将关闭电压监控屏蔽,才能正常使用这个低电压检测唤醒的功能。

通过设置2.5V的门限电压设置,通过功耗分析仪和串口打印的方式分别看正常电压时和低于门限电压时的两种状态。
1、低于设置的2.5V电压门限

2、正常3.3V电压供电状

5、GPIO中断唤醒
PA5 高电平 → 进入 Sleep → 按键使 PA5 高→低
→ GPIO 事件唤醒 PMU
→ 执行 GPIOA_IRQHandler,清中断标志
→ 从 LowPower_Sleep 返回
→ 恢复 HSE 偏置 → 下一轮 Sleep
#if 0 #include "CH59x_common.h" /********************************************************************* * @fn DebugInit * * @brief 调试初始化 * * @return none */ void DebugInit(void) { GPIOA_SetBits(GPIO_Pin_9); GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeOut_PP_5mA); UART1_DefInit(); } /********************************************************************* * @fn main * * @brief 主函数 * * @return none */ int main() { SetSysClock(CLK_SOURCE_PLL_60MHz); PWR_DCDCCfg(ENABLE); GPIOA_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); GPIOB_ModeCfg(GPIO_Pin_All, GPIO_ModeIN_PU); /* 配置串口调试 */ DebugInit(); PRINT("Start @ChipID=%02x\n", R8_CHIP_ID); DelayMs(200); #if 1 /* 配置唤醒源为 GPIO - PA5 */ GPIOA_ModeCfg(GPIO_Pin_5, GPIO_ModeIN_PU); GPIOA_ITModeCfg(GPIO_Pin_5, GPIO_ITMode_FallEdge); // 下降沿唤醒 PFIC_EnableIRQ(GPIO_A_IRQn); PWR_PeriphWakeUpCfg(ENABLE, RB_SLP_GPIO_WAKE, Long_Delay); #endif #if 0 PRINT("IDLE mode sleep \n"); DelayMs(1); LowPower_Idle(); PRINT("wake.. \n"); DelayMs(500); #endif #if 0 PRINT("Halt mode sleep \n"); DelayMs(2); LowPower_Halt(); HSECFG_Current(HSE_RCur_100); // 降为额定电流(低功耗函数中提升了HSE偏置电流) DelayMs(2); PRINT("wake.. \n"); DelayMs(500); #endif #if 0 PRINT("sleep mode sleep \n"); DelayMs(2); // 注意当主频为80M时,Sleep睡眠唤醒中断不可调用flash内代码。 LowPower_Sleep(RB_PWR_RAM24K | RB_PWR_RAM2K | RB_XT_PRE_EN); //只保留24+2K SRAM 供电 HSECFG_Current(HSE_RCur_100); // 降为额定电流(低功耗函数中提升了HSE偏置电流) PRINT("wake.. \n"); DelayMs(500); #endif #if 0 PRINT("shut down mode sleep \n"); DelayMs(2); LowPower_Shutdown(0); //全部断电,唤醒后复位 /* 此模式唤醒后会执行复位,所以下面代码不会运行, 注意要确保系统睡下去再唤醒才是唤醒复位,否则有可能变成IDLE等级唤醒 */ HSECFG_Current(HSE_RCur_100); // 降为额定电流(低功耗函数中提升了HSE偏置电流) PRINT("wake.. \n"); DelayMs(500); #endif PRINT("sleep mode sleep \n"); while(1) { #if 1 DelayMs(2); // 注意当主频为80M时,Sleep睡眠唤醒中断不可调用flash内代码。 LowPower_Sleep(RB_PWR_RAM24K | RB_PWR_RAM2K | RB_XT_PRE_EN); //只保留24+2K SRAM 供电 HSECFG_Current(HSE_RCur_100); // 降为额定电流(低功耗函数中提升了HSE偏置电流) // PRINT("wake.. \n"); // DelayMs(500); #endif } } /********************************************************************* * @fn GPIOA_IRQHandler * * @brief GPIOA中断函数 * * @return none */ __INTERRUPT __HIGH_CODE void GPIOA_IRQHandler(void) { GPIOA_ClearITFlagBit(GPIO_Pin_5); //R16_PA_INT_IF = GPIO_Pin_5; } #endif
浙公网安备 33010602011771号