Standby Mode1 GPIO Key Wakeup¶
1 功能概述¶
本例程演示如何使 SoC 进入 Standby Mode 1 状态,然后通过 GPIO 按键将其唤醒。
2 环境准备¶
硬件设备与线材:
PAN107X EVB 核心板与底板各一块
JLink 仿真器(用于烧录例程程序)
电流计(本文使用电流可视化测量设备 PPK2 [Nordic Power Profiler Kit II] 进行演示)
USB-TypeC 线一条(用于底板供电和查看串口打印 Log)
杜邦线数根或跳线帽数个(用于连接各个硬件设备)
硬件接线:
将 EVB 核心板插到底板上
为确保能够准确地测量 SoC 本身的功耗,排除底板外围电路的影响,请确认 EVB 底板上的:
Voltage 排针组中的 VCC 和 VDD 均接至 3V3
POWER 开关从 LDO 档位拨至 BAT 档位(并确认底板背部的电池座内没有纽扣电池)
使用 USB-TypeC 线,将 PC USB 插口与 EVB 底板 USB->UART 插口相连
使用杜邦线将 EVB 底板上的 TX 引脚接至核心板 P16,RX 引脚接至核心板 P17
使用杜邦线将 JLink 仿真器的:
SWD_CLK 引脚与 EVB 底板的 P00 排针相连
SWD_DAT 引脚与 EVB 底板的 P01 排针相连
SWD_GND 引脚与 EVB 底板的 GND 排针相连
将 PPK2 硬件的:
USB DATA/POWER 接口连接至 PC USB 接口
VOUT 连接至 EVB 底板的 VBAT 排针
GND 连接至 EVB 底板的 GND 排针
PC 软件:
串口调试助手(UartAssist)或终端工具(SecureCRT),波特率 921600(用于接收串口打印 Log)
nRF Connect Desktop(用于配合 PPK2 测量 SoC 电流)
3 编译和烧录¶
例程位置:<PAN10XX-NDK>\01_SDK\nimble\samples\low_power\standby_m1_gpio_key_wakeup\keil_107x
双击 Keil Project 文件打开工程进行编译烧录,烧录成功后断开 JLink 连线以避免漏电。
4 例程演示说明¶
PC 上打开 PPK2 Power Profiler 软件,供电电压选择 3300 mV,然后打开供电开关
从串口工具中看到如下的打印信息:
Try to load HW calibration data.. DONE. - Chip Info : 0x1 - Chip CP Version : 255 - Chip FT Version : 7 - Chip MAC Address : E110000052E3 - Chip UID : 060300465454455354 - Chip Flash UID : 4250315A3538380B00CE12435603C678 - Chip Flash Size : 512 KB [I] App started.. [I] Reset Reason: nRESET Pin Reset. [I] Try to enter SoC sleep/deepsleep mode and wait for 3-times key pressing..
此时观察芯片电流波形,发现稳定在 4uA 左右(说明芯片成功进入了 DeepSleep 模式):
系统初始化后进入 DeepSleep 模式¶
芯片低功耗状态下的底电流(漏电流)与环境温度相关,温度越高,漏电流越大。
尝试按下 EVB 底板上的 3 个按键中的任意一个或几个:KEY1、KEY2 和 WKUP,由串口打印信息可知触发 3 次 DeepSleep 唤醒后,芯片会延时 500ms,然后进入 Standby Mode 1 状态:
[I] ==== KEY1 Pressed! (P0_6 falling edge detected) ==== [I] First key pressed, notify value = 1. [I] ==== KEY2 Pressed! (P1_2 falling edge detected) ==== [I] Second key pressed, notify value = 1. [I] ==== WKUP Pressed! (P0_2 falling edge detected) ==== [I] Third key pressed, notify value = 1. [I] Busy wait 500ms to keep SoC in active mode.. [I] Now try to enter SoC standby mode 1 (wakeup reset)..
此时再观察芯片电流波形,可以看到芯片触发了 3 次 DeepSleep 唤醒,最后一次唤醒持续了约 500ms 时间,随后进入 Standby Mode 1 状态等待下次按键唤醒:
使用按键将芯片从 DeepSleep 状态下唤醒 3 次后,芯片进入 Stnadby Mode 1 状态¶
由电流波形可知芯片前两次唤醒后均重新进入了 DeepSleep 模式,此模式下芯片电流保持在 4uA 左右;第三次唤醒后,芯片最终进入了 Standby Mode 1 模式,此模式下芯片电流保持在 1.2uA 左右。
再次尝试按下 EVB 底板上的 3 个按键中的任意一个(如 KEY1),由串口打印信息可知芯片从 Standby Mode 1 状态下唤醒并复位,复位的原因为 GPIO P06 唤醒(对应按键 KEY1),随后芯片重新进入 DeepSleep 状态继续等待按键唤醒:
Try to load HW calibration data.. DONE. - Chip Info : 0x1 - Chip CP Version : 255 - Chip FT Version : 7 - Chip MAC Address : E110000052E3 - Chip UID : 060300465454455354 - Chip Flash UID : 4250315A3538380B00CE12435603C678 - Chip Flash Size : 512 KB [I] App started.. [I] Reset Reason: Standby Mode 1 GPIO Wakeup (cnt = 1). [I] gpio wakeup src flag = 0x00000040 [I] SoC is waked up by GPIO P0_6. [I] Try to enter SoC sleep/deepsleep mode and wait for 3-times key pressing..
5 开发者说明¶
5.1 SDK Config 配置¶
与本例程相关的 SDK Config (sdk_config.h
) 配置有:
SoC Platform : Chip Power Mode
芯片供电选择 DCDC 模式,以降低芯片动态功耗
对应宏配置
CONFIG_SOC_DCDC_PAN1070 = 1
Power Management : Enable Low Power Mode
使能系统低功耗功能
对应宏配置
CONFIG_PM = 1
Power Management : Enable Low Power Mode : Continue Run After Standby M1 Wakeup
配置 Standby Mode 1 唤醒后 CPU 的行为:CPU 复位,而不是继续执行 WFI 后面的指令
对应宏配置
CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET = 0
Log & Debug Config : Enable App Log
使能 App Log 功能
对应宏配置
APP_LOG_EN = 1
Log & Debug Config : Enable App Log : Log to UART
将 App Log 输出至串口
对应宏配置
CONFIG_UART_LOG_ENABLE = 1
5.2 程序代码¶
5.2.1 主程序¶
本例程中,OS 为使能状态,因此主程序 main() 函数也是 OS Main Task 的入口函数,其内容如下:
int main(void)
{
/* Application initialization */
app_setup();
/* Application main infinite loop */
app_main_loop();
return 0;
}
5.2.2 App Setup 初始化¶
App 初始化 app_setup() 函数内容如下:
void app_setup(void)
{
APP_LOG_INFO("App started..\n\n");
uint8_t rst_reason;
/* Get the last reset reason */
APP_LOG_INFO("Reset Reason: ");
rst_reason = soc_reset_reason_get();
switch (rst_reason) {
case SOC_RST_REASON_POR_RESET:
APP_LOG("Power On Reset.\n");
break;
case SOC_RST_REASON_PIN_RESET:
APP_LOG("nRESET Pin Reset.\n");
break;
case SOC_RST_REASON_SYS_RESET:
APP_LOG("NVIC System Reset.\n");
break;
case SOC_RST_REASON_STBM1_GPIO_WAKEUP:
APP_LOG("Standby Mode 1 GPIO Wakeup (cnt = %d).\n", ++wkup_cnt);
parse_stbm1_gpio_wakeup_source();
break;
default:
APP_LOG("Unhandled Reset Reason (%d), refer to more reason define in soc_api.h!\n", rst_reason);
}
}
打印 App 初始化 Log
获取并打印本次芯片启动的复位原因
5.2.3 App Main Task Loop 任务循环¶
App Main Task 循环 app_main_loop() 函数内容如下:
void app_main_loop(void)
{
uint32_t ulNotificationValue;
/* Store the handle of current task. */
xTaskToNotify = xTaskGetCurrentTaskHandle();
if(xTaskToNotify == NULL) {
app_assert("Error, get current task handle failed!\n");
}
/* Enable 3 GPIO keys low-level wakeup for standby mode 1 */
wakeup_gpio_keys_init();
APP_LOG_INFO("Try to enter SoC sleep/deepsleep mode and wait for 3-times key pressing..\n");
/*
* Wait to be notified that gpio key is pressed (gpio irq occured). Note the first parameter
* is pdFALSE, which has the effect of decrease the task's notification value by 1, making
* the notification value act like a counting semaphore.
*/
ulNotificationValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
APP_LOG_INFO("First key pressed, notify value = %d.\n", ulNotificationValue);
ulNotificationValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
APP_LOG_INFO("Second key pressed, notify value = %d.\n", ulNotificationValue);
ulNotificationValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
APP_LOG_INFO("Third key pressed, notify value = %d.\n", ulNotificationValue);
APP_LOG_INFO("Busy wait 500ms to keep SoC in active mode..\n");
soc_busy_wait(500000);
#if !CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET
APP_LOG_INFO("Now try to enter SoC standby mode 1 (wakeup reset)..\n\n");
#if CONFIG_UART_LOG_ENABLE
/* Waiting for UART Tx done and re-set UART IO before entering standby mode 1 to avoid current leakage */
reset_uart_io();
#endif
/* Enter standby mode1 with all sram power off */
soc_enter_standby_mode_1(STBM1_WAKEUP_SRC_GPIO, STBM1_RETENTION_SRAM_NONE);
APP_LOG("WARNING: Failed to enter SoC standby mode 1 due to unexpected interrupt detected.\n");
APP_LOG(" Please check if there is an unhandled interrupt during the standby mode 1\n");
APP_LOG(" entering flow.\n");
while (1) {
/* Busy wait */
}
#else
while (1) {
APP_LOG_INFO("Now try to enter SoC standby mode 1 (wakeup continuous run)..\n\n");
#if CONFIG_UART_LOG_ENABLE
/* Waiting for UART Tx done and re-set UART IO before entering standby mode 1 to avoid current leakage */
reset_uart_io();
#endif
/* Enter standby mode1 with all common 48KB sram retention */
soc_enter_standby_mode_1(STBM1_WAKEUP_SRC_GPIO, STBM1_RETENTION_SRAM_BLOCK0 | STBM1_RETENTION_SRAM_BLOCK1);
/* Restore hardware peripherals manually */
restore_hw_peripherals();
APP_LOG_INFO("Waked up from SoC standby mode 1 and continue run (cnt = %d)..\n", ++wkup_cnt);
}
#endif /* CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET */
}
获取当前任务的 Task Handle,用于后续中断中给次任务发送通知使用
在 wakeup_gpio_keys_init() 函数中初始化按键 GPIO 配置
尝试获取任务通知(Task Task Notify)3 次,使系统 3 次进入 DeepSleep 状态并等待 GPIO 按键唤醒
第三次从 DeepSleep 状态下唤醒后,调用
soc_busy_wait()
接口使芯片在 Active 状态下全速运行 500ms根据
CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET
配置,决定进入 Standby Mode 1 的流程,这里由于我们将此配置设置为 0,因此会执行唤醒后复位的 Standby Mode 1 流程进入 Standby Mode 1 前,先将 Log UART IO 配置还原为 GPIO 模拟输入状态,以避免 Standby 模式下 IO 漏电
调用
soc_enter_standby_mode_1()
接口,使系统进入 Standby Mode 1 低功耗状态,此接口接受两个参数:第一个参数用于配置唤醒源,第二个参数用于配置低功耗下保电的 SRAM,这里我们将唤醒源配置为 GPIO,且低功耗状态下所有 SRAM 均掉电以节约功耗(由于当前低功耗配置为唤醒后复位,因此所有 SRAM 均没有保电的必要)
5.2.4 GPIO 初始化程序¶
GPIO 初始化程序 wakeup_gpio_keys_init() 函数内容如下:
static void wakeup_gpio_keys_init(void)
{
/* Configure GPIO P06 (KEY1) / P12 (KEY2) / P02 (WKUP) as Falling Edge Interrupt/Wakeup */
/* Set pinmux func as GPIO */
SYS_SET_MFP(P0, 6, GPIO);
SYS_SET_MFP(P1, 2, GPIO);
SYS_SET_MFP(P0, 2, GPIO);
/* Configure debounce clock to 32K clock */
GPIO_SetDebounceTime(GPIO_DBCTL_DBCLKSRC_RCL, GPIO_DBCTL_DBCLKSEL_4);
/*
* Construct GPIO init structure and Init GPIO P06/P12/P02:
* - Set IO to digital input mode
* - Enable internal pull-up resistor
*/
HAL_GPIO_InitTypeDef GPIO_InitStruct = {
.mode = HAL_GPIO_MODE_INPUT_DIGITAL,
.pull = HAL_GPIO_PULL_UP,
};
HAL_GPIO_Init(P0_6, &GPIO_InitStruct);
HAL_GPIO_Init(P1_2, &GPIO_InitStruct);
HAL_GPIO_Init(P0_2, &GPIO_InitStruct);
/* Construct GPIO interrupt init structure */
HAL_GPIO_IntInitTypeDef GPIO_IntInitStruct = {
.intMode = HAL_GPIO_INT_FALLING,
.debounce = ENABLE,
};
/* Init GPIO P06 interrupt */
GPIO_IntInitStruct.callbackFunc = gpio_p06_input_callback;
HAL_GPIO_InterruptInit(P0_6, &GPIO_IntInitStruct);
/* Init GPIO P12 interrupt */
GPIO_IntInitStruct.callbackFunc = gpio_p12_input_callback;
HAL_GPIO_InterruptInit(P1_2, &GPIO_IntInitStruct);
/* Init GPIO P02 interrupt */
GPIO_IntInitStruct.callbackFunc = gpio_p02_input_callback;
HAL_GPIO_InterruptInit(P0_2, &GPIO_IntInitStruct);
/* Wait for a while to ensure the internal pullup is stable */
soc_busy_wait(10000);
/*
* Clear possible GPIO pending IRQs to avoid unexpected GPIO isr run
* after NVIC GPIO IRQ enabled.
* [NOTE] GPIO pending IRQs would be generated right after SoC waking
* up from Standby Mode 1 by GPIO interrupt.
*/
NVIC_ClearPendingIRQ(GPIO0_IRQn);
NVIC_ClearPendingIRQ(GPIO1_IRQn);
/* Enable GPIO IRQs in NVIC */
NVIC_EnableIRQ(GPIO0_IRQn);
NVIC_EnableIRQ(GPIO1_IRQn);
}
使用 HAL GPIO Driver 对 GPIO 进行配置
由于 EVB 底板上有 3 个按键,分别对应核心板 P06/P12/P02 等 3 个引脚,因此这里仅配置这 3 个 GPIO 引脚:
配置引脚 Pinmux 至 GPIO 功能
选择去抖时钟源为 32K Clock,以支持边沿唤醒,并配置去抖时间
配置 GPIO 为输入模式,并使能内部上拉电阻(注意 EVB 底板按键没有外部上拉电阻)
使能 GPIO 中断,将其配置为下降沿触发中断(即下降沿唤醒),并使能去抖功能
使能 NVIC GPIO IRQs
5.2.5 GPIO 中断回调函数¶
static void gpio_p06_input_callback(HAL_GPIO_IntMode intMode)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (intMode == HAL_GPIO_INT_FALLING) {
/* Notify the task that KEY1 is pressed (P06 falling edge detected). */
vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
APP_LOG_INFO("==== KEY1 Pressed! (P0_6 falling edge detected) ====\n");
}
/* Yield if xHigherPriorityTaskWoken is true. */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
static void gpio_p12_input_callback(HAL_GPIO_IntMode intMode)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (intMode == HAL_GPIO_INT_FALLING) {
/* Notify the task that KEY1 is pressed (P06 falling edge detected). */
vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
APP_LOG_INFO("==== KEY2 Pressed! (P1_2 falling edge detected) ====\n");
}
/* Yield if xHigherPriorityTaskWoken is true. */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
static void gpio_p02_input_callback(HAL_GPIO_IntMode intMode)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (intMode == HAL_GPIO_INT_FALLING) {
/* Notify the task that KEY1 is pressed (P06 falling edge detected). */
vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
APP_LOG_INFO("==== WKUP Pressed! (P0_2 falling edge detected) ====\n");
}
/* Yield if xHigherPriorityTaskWoken is true. */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
由于使用 HAL GPIO Driver,因此无需实现 GPIO 中断服务程序,而是要为每个 GPIO 引脚实现自己的中断触发 callback
使用 FreeRTOS
vTaskNotifyGiveFromISR()
接口向 App Task 发送通知,表示有 GPIO 中断产生(对应按键按下),此接口中xHigherPriorityTaskWoken
变量被配置为pdTRUE
,表示当中断返回后将会触发线程调度,而对于此例程来说则是重新调度至 App Task 中的ulTaskNotifyTake()
处继续执行
5.2.6 与低功耗相关的 Hook 函数¶
本例程还用到了 2 个与低功耗密切相关的 Hook 函数:
CONFIG_RAM_CODE void vSocDeepSleepEnterHook(void)
{
#if CONFIG_UART_LOG_ENABLE
reset_uart_io();
#endif
}
CONFIG_RAM_CODE void vSocDeepSleepExitHook(void)
{
#if CONFIG_UART_LOG_ENABLE
set_uart_io();
#endif
}
FreeRTOS 有一个优先级最低的 Idle Task,当系统调度到此任务后会对当前状态进行检查,以判断是否允许进入芯片 DeepSleep 低功耗流程
若程序执行到 Idle Task 的 DeepSleep 子流程中,会在 SoC 进入 DeepSleep 模式之前执行
vSocDeepSleepEnterHook()
函数,在 SoC 从 DeepSleep 模式下唤醒后执行vSocDeepSleepExitHook()
函数本例程在
vSocDeepSleepEnterHook()
函数中,为防止 UART IO 漏电,编写了相关代码(reset_uart_io()
,具体实现见例程源码)以确保在进入 DeepSleep 模式前:串口 Log 数据都打印完毕(即 UART0 Tx FIFO 应为空)
P16 引脚 Pinmux 功能由 UART0 Tx 切换回 GPIO
P17 引脚 Pinmux 功能由 UART0 Rx 切换回 GPIO,并将其数字输入功能关闭
本例程在
vSocDeepSleepExitHook()
函数中,编写了相关代码(set_uart_io()
,具体实现见例程源码)以恢复串口 Log 打印功能:P16 引脚 Pinmux 功能由 GPIO 重新切换成 UART0 Tx
P17 引脚 Pinmux 功能由 GPIO 重新切换成 UART0 Rx,并将其数字输入功能重新打开
5.2.7 其他功能函数¶
本例程还编写了 2 个比较常用的功能函数:
static void parse_stbm1_gpio_wakeup_source(void)
{
uint32_t src;
src = soc_stbm1_gpio_wakeup_src_get();
printf("gpio wakeup src flag = 0x%08x\n", src);
for (size_t i = 0; i < 32; i++) {
if (src & (1u << i)) {
printf("SoC is waked up by GPIO P%d_%d.\n", i / 8, i % 8);
}
}
}
#ifdef CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET
void restore_hw_peripherals(void)
{
#if CONFIG_UART_LOG_ENABLE
UART_InitTypeDef Init_Struct = {
.UART_BaudRate = 921600,
.UART_LineCtrl = Uart_Line_8n1,
};
/* Re-init UART */
UART_Init(UART0, &Init_Struct);
UART_EnableFifo(UART0);
/* Set IO pinmux to UART again */
set_uart_io();
#endif
/* Re-enable NVIC GPIO IRQs */
NVIC_EnableIRQ(GPIO0_IRQn);
NVIC_EnableIRQ(GPIO1_IRQn);
/* Re-init GPIO keys */
wakeup_gpio_keys_init();
}
#endif /* CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET */
parse_stbm1_gpio_wakeup_source()
用于获取当前 GPIO 中断引脚,本例程中在初始化阶段检测到当前为 Standby Mode 1 GPIO 唤醒后,调用此接口即可获取并打印唤醒 IO 是哪个引脚当
CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET
配置被设置为 1 时, 芯片从 Standby Mode 1 下唤醒后,将不会触发复位,而是从睡眠之前的代码处接着执行,但由于 Standby Mode 1 状态下芯片内部大部分模块均已掉电,因此醒来后需通过函数restore_hw_peripherals()
重新初始化一些用到的硬件模块