|
本帖最后由 cruelfox 于 2018-1-29 15:42 编辑
STEVAL-IDB007V1 玩到现在,基本上对它的低功耗模式了解了。这个SoC和STM32在低功耗模式方便的差异还是很大的。下面这个图是BlueNRG-1手册中的“状态”描述,左边三列都属于低功耗的模式。
注意,这里的 "SLEEP" 和一般ARM Cortex-m0 MCU所指的 SLEEP 模式含义不同。比如 STM32 的 SLEEP 是只把 CPU 停止(通过WFI/WFE指令),片上AHB和设备都继续运行。对于 BlueNRG-1,与之对应的模式则是软件层面上的 CPU_HALT,如下图。
熟悉 STM32 的朋友应该记得 STM32 的低功耗模式有 Sleep, Stop, Standby 这些,可以控制开启或关闭的部件要比 BlueNRG-1 丰富得多。只停止CPU的低功耗模式也只是节省ARM核心的部分功耗,对要求电池运行的这类系统是不够省电的。要进一步,需要降低CPU、总线的时钟频率,甚至关闭AHB时钟,依靠RTC等低功耗定时器来实现唤醒。
BlueNRG-1 在低功耗方面的选择明显不多。从手册上可以明确的是进一步省电,就必须关闭主时钟,关闭片上部分LDO,剩下极少部分的功能能运行。下面表格中,STANDBY 和 SLEEP 这两个模式的区别,也就是是否 32kHz 的时钟继续运行了。如果有 32kHz 时钟在,可以实现定时的唤醒操作,比如定时访问网络,这必须得支持。 从功耗上面可以大致看出来:
对上面这张表再补充一句:Reset 是最低功耗的模式,这和 STM32 也不一样。对于 BlueNRG-1, 把 Reset 接低它就彻底关闭了,相当于电源开关。
知道了有哪些低功耗模式,接下来需要知道如何进入低功耗模式,以及如何唤醒。
(1) 对于只停止CPU的模式,和一般的ARM单片机一样,BlueNRG-1 也是用 WFI 指令来休眠的,然后 CPU 就 Idle 了,等到中断来了再接着干活。
(2) 对于 Standy 和 Sleep 模式,BlueNRG-1 的手册上则没有说如硬件上是何进入的,写什么寄存器。在软件部分,说了是统一调用 BlueNRG_Sleep() 函数来实现。我对这样“语焉不详”的手册不是很喜欢,回想来也是,BLE部分库的代码都没开放,寄存器定义不开放也是意料之中的。用户如果自己随便操作,把MCU进入低功耗模式了,可能BLE部分就没法工作了。
好吧,先看看唤醒源有哪些。 出乎意料的,RTC居然不能在 Sleep 模式下工作和唤醒,尽管32kHz的时钟在。Watchdog 呢?说是如果有32k时钟可以运行的,但没说唤醒如何。GPIO用来唤醒,这个有,哪怕时钟都关掉了。
手册上透露出来的技术细节是意外的:这几个唤醒并不是接着 WFI 指令之后恢复,继续执行,尽管RAM的内容都在——而是系统 Reset 了,从Reset中断向量开始执行。另外,还有两个手册中完全没有描述的 BLE Timer1, BLE Timer2 可以唤醒,这才是低功耗运行蓝牙协议栈的基础。那么,唤醒之后都Reset岂不是重头再来了? 也不是,因为SRAM的内容是保持的,虽然所有硬件寄存器都复位了,除了个别的,比如判断Reset的原因是什么的寄存器。
似乎有意思了,依靠寄存器判断复位源,然后从RAM中去恢复进入低功耗模式之前的现场…… 不过还是比较复杂啊。
下面从分析 sleep.c 中 BlueNRG_Sleep() 函数的实现入手- uint8_t BlueNRG_Sleep(SleepModes sleepMode,
- uint8_t gpioWakeBitMask,
- uint8_t gpioWakeLevelMask)
- {
- SleepModes app_sleepMode, ble_sleepMode, sleepMode_allowed;
-
- /* Mask all the interrupt */
- __disable_irq();
- ble_sleepMode = (SleepModes)BlueNRG_Stack_Perform_Deep_Sleep_Check();
- app_sleepMode = App_SleepMode_Check(sleepMode);
- sleepMode_allowed = MIN(app_sleepMode, sleepMode);
- sleepMode_allowed = MIN(ble_sleepMode, sleepMode_allowed);
- #ifdef DEBUG_SLEEP_MODE
- sleepMode_selected[sleepMode_allowed]++;
- #endif
- if (sleepMode_allowed == SLEEPMODE_RUNNING) {
- /* Unmask all the interrupt */
- __enable_irq();
- return SUCCESS;
- }
-
- if (sleepMode_allowed == SLEEPMODE_CPU_HALT) {
- BlueNRG_IdleSleep();
- /* Unmask all the interrupt */
- __enable_irq();
- return SUCCESS;
- }
- /* Setup the Wakeup Source */
- SYSTEM_CTRL->WKP_IO_IS = gpioWakeLevelMask;
- SYSTEM_CTRL->WKP_IO_IE = gpioWakeBitMask;
- BlueNRG_InternalSleep(sleepMode_allowed, gpioWakeBitMask);
-
- return SUCCESS;
- }
复制代码
这函数里面,先是分别调用 BlueNRG_Stack_Perform_Deep_Sleep_Check() 和 App_SleepMode_Check(sleepMode) 来检查参数 sleepMode 指定的模式是否满足请求,比如 BLE stack 现在不允许 SLEEPMODE_NOTIMER 级别, 那么调用参数给 SLEEPMODE_NOTIMER 是达不到效果的。也就是说想用 BlueNRG_Sleep() 进入深度的休眠,未必实际就是深度休眠,它要询问软件其它部分是否可以(比如为了响应中断)。
当决定了实际的 sleepMode 之后,分情况处理了。如果是 RUNNING,则什么不做返回。如果是 CPU_HALT,则执行一下 WFI 指令。这里调用了 context_switch.s 中实现的 BlueNRG_IdleSleep() 函数,实际上就是执行 WFI 指令再返回而已。但是,我到现在都没明白为什么是先关了中断再执行WFI,再打开中断。中断关闭了WFI能退出休眠?
为了更低功耗的 Sleep 和 Standby 模式,后面要调用特殊函数 BlueNRG_InternalSleep() 来处理了。在这之前,要指定唤醒用的 GPIO.
BlueNRG_InternalSleep() 这个函数比较长,这里不全部贴出来了。在前面部分,把许多设备寄存器内容保存到局部变量中,如
- /* Save the peripherals configuration */
- /* System Control */
- SYS_Ctrl_saved = SYSTEM_CTRL->CTRL;
- /* FLASH CONFIG */
- FLASH_CONFIG_saved = FLASH->CONFIG;
- /* NVIC */
- NVIC_ISER_saved = NVIC->ISER[0];
- // Issue with Atollic compiler
- // memcpy(NVIC_IPR_saved, (void const *)NVIC->IP, sizeof(NVIC_IPR_saved));
- for (i=0; i<8; i++) {
- NVIC_IPR_saved[i] = NVIC->IP[i];
- }
- PENDSV_SYSTICK_IPR_saved = *(volatile uint32_t *)SHPR3_REG;
- /* CKGEN SOC Enabled */
- CLOCK_EN_saved = CKGEN_SOC->CLOCK_EN;
- /* GPIO */
- GPIO_DATA_saved = GPIO->DATA;
- GPIO_OEN_saved = GPIO->OEN;
- GPIO_PE_saved = GPIO->PE;
- GPIO_DS_saved = GPIO->DS;
复制代码
哎呀,几乎是想得到的寄存器都保存了,就是给硬件复位准备的。
Sleep 和 Standby 模式的区别在这里:
- // Enable the STANDBY mode
- if (sleepMode == SLEEPMODE_NOTIMER) {
- BLUE_CTRL->TIMEOUT |= LOW_POWER_STANDBY<<28;
- }
复制代码
然后,就是进入低功耗模式了
- //Enable deep sleep
- SystemSleepCmd(ENABLE);
- wakeupFromSleepFlag = 0; // Flag to signal if a wakeup from standby or sleep occurred
- //The __disable_irq() used at the beginning of the BlueNRG_Sleep() function
- //masks all the interrupts. The interrupts will be enabled at the end of the
- //context restore. Now induce a context save.
- void CS_contextSave(void);
- CS_contextSave();
复制代码
关键在于 CS_contextSave() 的实现,它是 context_switch.s 的汇编程序。
- EXPORT_FUNC(CS_contextSave)
- PUSH { r4 - r7, lr } /* store R4-R7 and LR (5 words) onto the stack */
- MOV R3, R8 /* mov thread {r8 - r12} to {r3 - r7} */
- MOV R4, R9
- MOV R5, R10
- MOV R6, R11
- MOV R7, R12
- PUSH {R3-R7} /* store R8-R12 (5 words) onto the stack */
- LDR R4, =savedMSP /* load address of savedMSP into R4 */
- MOV R3, SP /* load the stack pointer into R3 */
- STR R3, [R4] /* store the MSP into savedMSP */
-
- LDR R4, =0xE000ED04 /* load address of ICSR register into R4 */
- LDR R0, [R4] /* load the ICSR register value into R0 */
- LDR R4, =savedICSR /* load address of savedICSR into R4 */
- STR R0, [R4] /* store the ICSR register value into savedICSR */
- LDR R4, =0xE000ED24 /* load address of SHCSR register into R4 */
- LDR R0, [R4] /* load the SHCSR register value into R0 */
- LDR R4, =savedSHCSR /* load address of savedSHCSR into R4 */
- STR R0, [R4] /* store the SHCSR register value into savedSHCSR */
- LDR R4, =0xE000E200 /* load address of NVIC_ISPR register into R4 */
- LDR R0, [R4] /* load the NVIC_ISPR register value into R0 */
- LDR R4, =savedNVIC_ISPR /* load address of savedNVIC_ISPR into R4 */
- STR R0, [R4] /* store the NVIC_ISPR register value into savedNVIC_ISPR */
-
- DSB
- WFI /* all saved, trigger deep sleep */
- ENDFUNC
复制代码
它把几个通用寄存器和几个Cortex-m0的系统寄存器内容保存在RAM中,然后用 WFI 来进入深度休眠。在这之前,SystemSleepCmd(ENABLE) 实际上就是写 SCB->SCR 中的 SLEEPDEEP 位。如果成功的话,BlueNRG-1 将进入 Standby 或 Sleep 模式。
注意,唤醒之后可不是立即执行 那条 WFI 后面的指令. CPU复位,执行bootloader程序,如同我在 https://bbs.eeworld.com.cn/thread-609649-1-1.html 帖子中的分析。这次细看 Reset Handler 首先调用的 __low_level_init(),到底做了什么?
- int __low_level_init(void)
- {
- // If the reset reason is a wakeup from sleep restore the context
- if ((CKGEN_SOC->REASON_RST == 0) && (CKGEN_BLE->REASON_RST > RESET_WAKE_DEEPSLEEP_REASONS)) {
- void CS_contextRestore(void);
- wakeupFromSleepFlag = 1; //A wakeup from Standby or Sleep occurred
- CS_contextRestore(); // Restore the context
- //if the context restore worked properly, we should never return here
- while(1) { ; }
- }
- return 1;
- }
复制代码
好的,判断复位的原因,如果是低功耗休眠唤醒的话,就执行 CS_contextRestore()
- EXPORT_FUNC(CS_contextRestore)
- /* Even if we fall through the WFI instruction, we will immediately
- * execute a context restore and end up where we left off with no
- * ill effects. Normally at this point the core will either be
- * powered off or reset (depending on the deep sleep level). */
- LDR R4, =savedMSP /* load address of savedMSP into R4 */
- LDR R4, [R4] /* load the SP from savedMSP */
- MOV SP, R4 /* restore the SP from R4 */
- POP {R3-R7} /* load R8-R12 (5 words) from the stack */
- MOV R8, R3 /* mov {r3 - r7} to {r8 - r12} */
- MOV R9, R4
- MOV R10, R5
- MOV R11, R6
- MOV R12, R7
- POP { R4 - R7, PC } /*load R4-R7 and PC (5 words) from the stack */
复制代码
就是从内存中固定的地方取出保存的SP的值,初始化堆栈为 CS_contextSave() 时的状态,然后从堆栈中恢复通用寄存器和 PC,即跳到休眠前调用 CS_contextSave() 的返回地址去执行了,也就是从 BlueNRG_InternalSleep() 的中间开始执行了!这个很 tricky 吧。
BlueNRG_InternalSleep() 函数后面做的事情大概能猜到了,恢复保存的寄存器,以及再做一些额外的初始化工作,因为相当于重启了但没有执行 SystemInit(),需要补充一些工作。
所以,尽管进入 Standby 或者 Sleep 模式,BlueNRG-1 是相当于硬件复位了,还是可以通过软件上的技巧恢复到休眠之前的环境,继续执行。这里的前提是RAM不能掉电,而且有足够的空间保存硬件寄存器。
最后总结一下 BlueNRG-1 的低功耗模式
API级别 | 硬件状态 | 开启的时钟 | 唤醒事件 | SLEEPMODE_RUNNING | Running | 16MHz & 32kHz | -- | SLEEPMODE_CPU_HALT | CPU Idle | 16MHz & 32kHz | IRQ * | SLEEPMODE_WAKETIMER | Sleep | 32kHz | GPIO, BLE Timer (RESET) | SLEEPMODE_NOTIMER | Standby | N/A | GPIO (RESET) |
|
|