社区导航

 
查看: 1393|回复: 5

[蓝牙BLE] BlueNRG-1 的低功耗模式浅析

[复制链接]

969

TA的帖子

1

TA的资源

纯净的硅(高级)

Rank: 6Rank: 6

发表于 2018-1-29 08:49:01 | 显示全部楼层 |阅读模式
本帖最后由 cruelfox 于 2018-1-29 15:42 编辑

STEVAL-IDB007V1 玩到现在,基本上对它的低功耗模式了解了。这个SoC和STM32在低功耗模式方便的差异还是很大的。下面这个图是BlueNRG-1手册中的“状态”描述,左边三列都属于低功耗的模式。
modes.PNG
注意,这里的 "SLEEP" 和一般ARM Cortex-m0 MCU所指的 SLEEP 模式含义不同。比如 STM32 的 SLEEP 是只把 CPU 停止(通过WFI/WFE指令),片上AHB和设备都继续运行。对于 BlueNRG-1,与之对应的模式则是软件层面上的 CPU_HALT,如下图。 psave.PNG

熟悉 STM32 的朋友应该记得 STM32 的低功耗模式有 Sleep, Stop, Standby 这些,可以控制开启或关闭的部件要比 BlueNRG-1 丰富得多。只停止CPU的低功耗模式也只是节省ARM核心的部分功耗,对要求电池运行的这类系统是不够省电的。要进一步,需要降低CPU、总线的时钟频率,甚至关闭AHB时钟,依靠RTC等低功耗定时器来实现唤醒。

BlueNRG-1 在低功耗方面的选择明显不多。从手册上可以明确的是进一步省电,就必须关闭主时钟,关闭片上部分LDO,剩下极少部分的功能能运行。下面表格中,STANDBY 和 SLEEP 这两个模式的区别,也就是是否 32kHz 的时钟继续运行了。如果有 32kHz 时钟在,可以实现定时的唤醒操作,比如定时访问网络,这必须得支持。 从功耗上面可以大致看出来:
current.PNG
对上面这张表再补充一句: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的原因是什么的寄存器。
wakeup.PNG

似乎有意思了,依靠寄存器判断复位源,然后从RAM中去恢复进入低功耗模式之前的现场……  不过还是比较复杂啊。

下面从分析 sleep.c 中 BlueNRG_Sleep() 函数的实现入手
  1. uint8_t BlueNRG_Sleep(SleepModes sleepMode,
  2.                       uint8_t gpioWakeBitMask,
  3.                       uint8_t gpioWakeLevelMask)
  4. {
  5.   SleepModes app_sleepMode, ble_sleepMode, sleepMode_allowed;
  6.   
  7.   /* Mask all the interrupt */
  8.   __disable_irq();

  9.   ble_sleepMode = (SleepModes)BlueNRG_Stack_Perform_Deep_Sleep_Check();
  10.   app_sleepMode = App_SleepMode_Check(sleepMode);
  11.   sleepMode_allowed = MIN(app_sleepMode, sleepMode);
  12.   sleepMode_allowed = MIN(ble_sleepMode, sleepMode_allowed);

  13. #ifdef DEBUG_SLEEP_MODE
  14.   sleepMode_selected[sleepMode_allowed]++;
  15. #endif

  16.   if (sleepMode_allowed == SLEEPMODE_RUNNING) {
  17.     /* Unmask all the interrupt */
  18.     __enable_irq();
  19.      return SUCCESS;
  20.   }
  21.   
  22.   if (sleepMode_allowed == SLEEPMODE_CPU_HALT) {
  23.     BlueNRG_IdleSleep();
  24.     /* Unmask all the interrupt */
  25.     __enable_irq();
  26.     return SUCCESS;
  27.   }

  28.   /* Setup the Wakeup Source */
  29.   SYSTEM_CTRL->WKP_IO_IS = gpioWakeLevelMask;
  30.   SYSTEM_CTRL->WKP_IO_IE = gpioWakeBitMask;

  31.   BlueNRG_InternalSleep(sleepMode_allowed, gpioWakeBitMask);
  32.   
  33.   return SUCCESS;
  34. }
复制代码

这函数里面,先是分别调用 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() 这个函数比较长,这里不全部贴出来了。在前面部分,把许多设备寄存器内容保存到局部变量中,如
  1.   /* Save the peripherals configuration */
  2.   /* System Control */
  3.   SYS_Ctrl_saved = SYSTEM_CTRL->CTRL;
  4.   /* FLASH CONFIG */
  5.   FLASH_CONFIG_saved = FLASH->CONFIG;
  6.   /* NVIC */
  7.   NVIC_ISER_saved = NVIC->ISER[0];

  8.   // Issue with Atollic compiler
  9. //  memcpy(NVIC_IPR_saved, (void const *)NVIC->IP, sizeof(NVIC_IPR_saved));
  10.   for (i=0; i<8; i++) {
  11.           NVIC_IPR_saved[i] = NVIC->IP[i];
  12.   }


  13.   PENDSV_SYSTICK_IPR_saved = *(volatile uint32_t *)SHPR3_REG;
  14.   /* CKGEN SOC Enabled */
  15.   CLOCK_EN_saved = CKGEN_SOC->CLOCK_EN;
  16.   /* GPIO */
  17.   GPIO_DATA_saved = GPIO->DATA;
  18.   GPIO_OEN_saved = GPIO->OEN;
  19.   GPIO_PE_saved = GPIO->PE;
  20.   GPIO_DS_saved = GPIO->DS;
复制代码

哎呀,几乎是想得到的寄存器都保存了,就是给硬件复位准备的。

Sleep 和 Standby 模式的区别在这里:
  1.   // Enable the STANDBY mode
  2.   if (sleepMode == SLEEPMODE_NOTIMER) {
  3.     BLUE_CTRL->TIMEOUT |= LOW_POWER_STANDBY<<28;
  4.   }
复制代码


然后,就是进入低功耗模式了
  1. //Enable deep sleep
  2.   SystemSleepCmd(ENABLE);
  3.   wakeupFromSleepFlag = 0; // Flag to signal if a wakeup from standby or sleep occurred
  4.   //The __disable_irq() used at the beginning of the BlueNRG_Sleep() function
  5.   //masks all the interrupts. The interrupts will be enabled at the end of the
  6.   //context restore. Now induce a context save.
  7.   void CS_contextSave(void);
  8.   CS_contextSave();
复制代码


关键在于 CS_contextSave() 的实现,它是 context_switch.s 的汇编程序。
  1. EXPORT_FUNC(CS_contextSave)
  2.                  PUSH   { r4 - r7, lr }       /* store R4-R7 and LR (5 words) onto the stack */
  3.                  MOV    R3, R8                /* mov thread {r8 - r12} to {r3 - r7} */
  4.                  MOV    R4, R9
  5.                  MOV    R5, R10
  6.                  MOV    R6, R11        
  7.                  MOV    R7, R12        
  8.                  PUSH   {R3-R7}                 /* store R8-R12 (5 words) onto the stack */
  9.                  LDR    R4, =savedMSP           /* load address of savedMSP into R4 */
  10.                  MOV    R3, SP                  /* load the stack pointer into R3 */
  11.                  STR    R3, [R4]                /* store the MSP into savedMSP */
  12.                                  
  13.                  LDR    R4, =0xE000ED04         /* load address of ICSR register into R4 */
  14.                  LDR    R0, [R4]                /* load the ICSR register value into R0 */
  15.                  LDR    R4, =savedICSR          /* load address of savedICSR into R4 */
  16.                  STR    R0, [R4]                /* store the ICSR register value into savedICSR */

  17.                  LDR    R4, =0xE000ED24         /* load address of SHCSR register into R4 */
  18.                  LDR    R0, [R4]                /* load the SHCSR register value into R0 */
  19.                  LDR    R4, =savedSHCSR         /* load address of savedSHCSR into R4 */
  20.                  STR    R0, [R4]                /* store the SHCSR register value into savedSHCSR */

  21.                  LDR    R4, =0xE000E200         /* load address of NVIC_ISPR register into R4 */
  22.                  LDR    R0, [R4]                /* load the NVIC_ISPR register value into R0 */
  23.                  LDR    R4, =savedNVIC_ISPR     /* load address of savedNVIC_ISPR into R4 */
  24.                  STR    R0, [R4]                /* store the NVIC_ISPR register value into savedNVIC_ISPR */
  25.                  
  26.                  DSB
  27.                  WFI                          /* all saved, trigger deep sleep */
  28.                  ENDFUNC                 
复制代码

它把几个通用寄存器和几个Cortex-m0的系统寄存器内容保存在RAM中,然后用 WFI 来进入深度休眠。在这之前,SystemSleepCmd(ENABLE) 实际上就是写 SCB->SCR 中的 SLEEPDEEP 位。如果成功的话,BlueNRG-1 将进入 Standby 或 Sleep 模式。


注意,唤醒之后可不是立即执行 那条 WFI 后面的指令. CPU复位,执行bootloader程序,如同我在 http://bbs.eeworld.com.cn/thread-609649-1-1.html 帖子中的分析。这次细看 Reset Handler 首先调用的 __low_level_init(),到底做了什么?
  1. int __low_level_init(void)
  2. {
  3.   // If the reset reason is a wakeup from sleep restore the context
  4.   if ((CKGEN_SOC->REASON_RST == 0) && (CKGEN_BLE->REASON_RST > RESET_WAKE_DEEPSLEEP_REASONS)) {

  5.   void CS_contextRestore(void);
  6.   wakeupFromSleepFlag = 1; //A wakeup from Standby or Sleep occurred
  7.   CS_contextRestore(); // Restore the context
  8.   //if the context restore worked properly, we should never return here
  9.   while(1) { ; }
  10. }
  11.   return 1;
  12. }
复制代码

好的,判断复位的原因,如果是低功耗休眠唤醒的话,就执行 CS_contextRestore()
  1. EXPORT_FUNC(CS_contextRestore)
  2.                 /* Even if we fall through the WFI instruction, we will immediately
  3.                  * execute a context restore and end up where we left off with no
  4.                  * ill effects.  Normally at this point the core will either be
  5.                  * powered off or reset (depending on the deep sleep level). */
  6.                 LDR    R4, =savedMSP         /* load address of savedMSP into R4 */
  7.                 LDR    R4, [R4]              /* load the SP from savedMSP */
  8.                 MOV    SP, R4                /* restore the SP from R4 */
  9.                 POP   {R3-R7}                /* load R8-R12 (5 words) from the stack */
  10.                 MOV    R8, R3                /* mov {r3 - r7} to {r8 - r12} */
  11.                 MOV    R9, R4
  12.                 MOV    R10, R5
  13.                 MOV    R11, R6
  14.                 MOV    R12, R7
  15.                 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 & 32kHzIRQ *
SLEEPMODE_WAKETIMER Sleep 32kHzGPIO, BLE Timer (RESET)
SLEEPMODE_NOTIMER Standby N/AGPIO  (RESET)



回复

使用道具 举报

5

TA的帖子

0

TA的资源

一粒金砂(中级)

Rank: 2

发表于 2018-3-1 11:40:13 | 显示全部楼层
请问下BlueNRG-1低功耗模式三SLEEPMODE_WAKETIMER,串口发AT指令不能回送信息,该怎么解决呢?
主函数轮询调用BlueNRG_Sleep(SLEEPMODE_NOTIMER, wakeup_source, wakeup_level)函数,唤醒源设置如下(未调用前AT指令功能正常):
uint8_t wakeup_source = WAKEUP_IO13|WAKEUP_IO11;
uint8_t wakeup_level = (WAKEUP_IOx_LOW << WAKEUP_IO13_SHIFT_MASK)| (WAKEUP_IOx_LOW << WAKEUP_IO11_SHIFT_MASK);

点评

SLEEPMODE_WAKETIMER模式下,串口是不工作的,不能收到外部发送的数据了。  详情 回复 发表于 2018-3-1 12:02


回复

使用道具 举报

969

TA的帖子

1

TA的资源

纯净的硅(高级)

Rank: 6Rank: 6

 楼主| 发表于 2018-3-1 12:02:40 | 显示全部楼层
zhangxpid 发表于 2018-3-1 11:40
请问下BlueNRG-1低功耗模式三SLEEPMODE_WAKETIMER,串口发AT指令不能回送信息,该怎么解决呢?
主函数轮询 ...

SLEEPMODE_WAKETIMER模式下,串口是不工作的,不能收到外部发送的数据了。


回复

使用道具 举报

5

TA的帖子

0

TA的资源

一粒金砂(中级)

Rank: 2

发表于 2018-3-1 13:27:34 | 显示全部楼层
cruelfox 发表于 2018-3-1 12:02
SLEEPMODE_WAKETIMER模式下,串口是不工作的,不能收到外部发送的数据了。

串口发AT指令时应该可以临时唤醒吧,主要是想问下模式三不能通过串口唤醒(pio11)的方式来使用AT指令吗

点评

串口RXD不具备唤醒功能,硬件上只有通过PIO来唤醒。如果发送端先用一个电平唤醒BlueNRG-1,然后BlueNRG-1的程序等待串口的数据是可以的。要注意延迟,不能在BlueNRG-1重新初始化串口的时候RXD上数据已经出来了。  详情 回复 发表于 2018-3-1 15:12


回复

使用道具 举报

969

TA的帖子

1

TA的资源

纯净的硅(高级)

Rank: 6Rank: 6

 楼主| 发表于 2018-3-1 15:12:54 | 显示全部楼层
zhangxpid 发表于 2018-3-1 13:27
串口发AT指令时应该可以临时唤醒吧,主要是想问下模式三不能通过串口唤醒(pio11)的方式来使用AT指令吗

串口RXD不具备唤醒功能,硬件上只有通过PIO来唤醒。如果发送端先用一个电平唤醒BlueNRG-1,然后BlueNRG-1的程序等待串口的数据是可以的。要注意延迟,不能在BlueNRG-1重新初始化串口的时候RXD上数据已经出来了。


回复

使用道具 举报

5

TA的帖子

0

TA的资源

一粒金砂(中级)

Rank: 2

发表于 2018-3-2 09:32:26 | 显示全部楼层
cruelfox 发表于 2018-3-1 15:12
串口RXD不具备唤醒功能,硬件上只有通过PIO来唤醒。如果发送端先用一个电平唤醒BlueNRG-1,然后BlueNRG-1 ...

嗯,好的,谢谢了


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

  • 论坛活动 E手掌握

    扫码关注
    EEWORLD 官方微信

  • EE福利  唾手可得

    扫码关注
    EE福利 唾手可得

Archiver|手机版|小黑屋|电子工程世界 ( 京ICP证 060456 )

GMT+8, 2018-12-12 20:19 , Processed in 0.194370 second(s), 16 queries , Gzip On, MemCache On.

快速回复 返回顶部 返回列表