当前文档版本为 NDK-v0.6.0,您可以访问当前页面的 开发中 版本以获取最近可能的更新。

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 例程演示说明

  1. PC 上打开 PPK2 Power Profiler 软件,供电电压选择 3300 mV,然后打开供电开关

  2. 从串口工具中看到如下的打印信息:

    Try to load HW calibration data.. DONE.
    - Chip Info         : 0x1
    - Chip CP Version   : 255
    - Chip FT Version   : 6
    - Chip MAC Address  : E11000000FF8
    - Chip UID          : 6D0001465454455354
    - Chip Flash UID    : 4250315A3538380B005B7B4356037D78
    - Chip Flash Size   : 512 KB
    APP version: 255.255.65535
    
    Reset Reason: nRESET Pin Reset.
    
    Try to enter SoC sleep/deepsleep mode and wait for 3-times key pressing..
    
  3. 此时观察芯片电流波形,发现稳定在 4uA 左右(说明芯片成功进入了 DeepSleep 模式):

    image

    系统初始化后进入 DeepSleep 模式

    芯片低功耗状态下的底电流(漏电流)与环境温度相关,温度越高,漏电流越大。

  4. 尝试按下 EVB 底板上的 3 个按键中的任意一个或几个:KEY1、KEY2 和 WKUP,由串口打印信息可知触发 3 次 DeepSleep 唤醒后,芯片会延时 500ms,然后进入 Standby Mode 1 状态:

    P0_6 INT occurred.
    First key pressed, notify value = 1.
    P1_2 INT occurred.
    Second key pressed, notify value = 1.
    P0_2 INT occurred.
    Third key pressed, notify value = 1.
    
    Busy wait 500ms to keep SoC in active mode..
    
    Now try to enter SoC standby mode 1 (wakeup reset)..
    
  5. 此时再观察芯片电流波形,可以看到芯片触发了 3 次 DeepSleep 唤醒,最后一次唤醒持续了约 500ms 时间,随后进入 Standby Mode 1 状态等待下次按键唤醒:

    image

    使用按键将芯片从 DeepSleep 状态下唤醒 3 次后,芯片进入 Stnadby Mode 1 状态

    由电流波形可知芯片前两次唤醒后均重新进入了 DeepSleep 模式,此模式下芯片电流保持在 4uA 左右;第三次唤醒后,芯片最终进入了 Standby Mode 1 模式,此模式下芯片电流保持在 1.2uA 左右。

  6. 再次尝试按下 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   : 6
    - Chip MAC Address  : E11000000FF8
    - Chip UID          : 6D0001465454455354
    - Chip Flash UID    : 4250315A3538380B005B7B4356037D78
    - Chip Flash Size   : 512 KB
    APP version: 255.255.65535
    
    Reset Reason: Standby Mode 1 GPIO Wakeup (cnt = 1).
    gpio wakeup src flag = 0x00000040
    SoC is waked up by GPIO P0_6.
    P0_6 INT occurred.
    
    Try to enter SoC sleep/deepsleep mode and wait for 3-times key pressing..
    First key pressed, notify value = 1.
    

5 开发者说明

5.1 App Config 配置

本例程的 App Config(对应 app_config_spark.h 文件)配置如下:

image

App Config File

其中,与本例程相关的配置有:

  • Enable DCDC (CONFIG_SOC_DCDC_PAN1070 = 1):使能芯片 DCDC 供电模式,以降低芯片动态功耗

  • Log Enable (CONFIG_LOG_ENABLE = 1):使能串口 Log 输出

  • Low Power Enable (CONFIG_PM = 1):使能系统低功耗流程

  • Continue Run After Standby M1 Wakeup (CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET = 0):配置 Standby Mode 1 唤醒后芯片行为是复位(而不是继续执行 WFI 后面的指令)

5.2 程序代码

5.2.1 主程序

主程序 app_main() 函数内容如下:

void app_main(void)
{
    BaseType_t r;

    print_version_info();

    /* Create an App Task */
    r = xTaskCreate(app_task,               // Task Function
                    "App Task",             // Task Name
                    APP_TASK_STACK_SIZE,    // Task Stack Size
                    NULL,                   // Task Parameter
                    APP_TASK_PRIORITY,      // Task Priority
                    NULL                    // Task Handle
    );

    /* Check if task has been successfully created */
    if (r != pdPASS) {
        printf("Error, App Task created failed!\n");
        while (1);
    }
}
  1. 打印 App 版本信息

  2. 创建 App 主任务 “App Task”,对应任务函数 app_task

  3. 确认线程创建成功,否则打印出错信息

5.2.2 App 主任务

App 主任务 app_task() 函数内容如下:

void app_task(void *arg)
{
    uint32_t ulNotificationValue;
    uint8_t rst_reason;

    /* Store the handle of current task. */
    xTaskToNotify = xTaskGetCurrentTaskHandle();
    if(xTaskToNotify == NULL) {
        printf("Error, get current task handle failed!\n");
        while (1);
    }

    /* Get the last reset reason */
    printf("\nReset Reason: ");
    rst_reason = soc_reset_reason_get();
    switch (rst_reason) {
    case SOC_RST_REASON_POR_RESET:
        printf("Power On Reset.\n");
        break;
    case SOC_RST_REASON_PIN_RESET:
        printf("nRESET Pin Reset.\n");
        break;
    case SOC_RST_REASON_SYS_RESET:
        printf("NVIC System Reset.\n");
        break;
    case SOC_RST_REASON_STBM1_GPIO_WAKEUP:
        printf("Standby Mode 1 GPIO Wakeup (cnt = %d).\n", ++wkup_cnt);
        parse_stbm1_gpio_wakeup_source();
        break;
    default:
        printf("Unhandled Reset Reason, refer to more reason define in os_lp.h!\n");
    }

    /* Enable 3 GPIO keys low-level wakeup for standby mode 1 */
    wakeup_gpio_keys_init();

    printf("\nTry 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);
    printf("First key pressed, notify value = %d.\n", ulNotificationValue);
    ulNotificationValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
    printf("Second key pressed, notify value = %d.\n", ulNotificationValue);
    ulNotificationValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
    printf("Third key pressed, notify value = %d.\n", ulNotificationValue);

    printf("\nBusy wait 500ms to keep SoC in active mode..\n");
    soc_busy_wait(500000);

#if !CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET
    printf("\nNow try to enter SoC standby mode 1 (wakeup reset)..\n\n");
#if CONFIG_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);

    printf("WARNING: Failed to enter SoC standby mode 1 due to unexpected interrupt detected.\n");
    printf("         Please check if there is an unhandled interrupt during the standby mode 1\n");
    printf("         entering flow.\n");

    while (1) {
        /* Busy wait */
    }
#else
    while (1) {
        printf("\nNow try to enter SoC standby mode 1 (wakeup continuous run)..\n\n");
#if CONFIG_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();
        printf("Waked up from SoC standby mode 1 and continue run (cnt = %d)..\n", ++wkup_cnt);
    }
#endif /* CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET */
}
  1. 获取当前任务的 Task Handle,用于后续中断中给次任务发送通知使用

  2. 获取并打印本次芯片启动的复位原因,若复位原因为 Standby Mode 1 GPIO 唤醒,则额外解析并打印唤醒 IO 是哪个引脚

  3. 在 wakeup_gpio_keys_init() 函数中初始化按键 GPIO 配置

  4. 尝试获取任务通知(Task Task Notify)3 次,使系统 3 次进入 DeepSleep 状态并等待 GPIO 按键唤醒

  5. 第三次从 DeepSleep 状态下唤醒后,调用 soc_busy_wait() 接口使芯片在 Active 状态下全速运行 500ms

  6. 根据 CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET 配置,决定进入 Standby Mode 1 的流程,这里由于我们将此配置设置为 0,因此会执行唤醒后复位的 Standby Mode 1 流程

  7. 进入 Standby Mode 1 前,先将 Log UART IO 配置还原为 GPIO 模拟输入状态,以避免 Standby 模式下 IO 漏电

  8. 调用 soc_enter_standby_mode_1() 接口,使系统进入 Standby Mode 1 低功耗状态,此接口接受两个参数:第一个参数用于配置唤醒源,第二个参数用于配置低功耗下保电的 SRAM,这里我们将唤醒源配置为 GPIO,且低功耗状态下所有 SRAM 均掉电以节约功耗(由于当前低功耗配置为唤醒后复位,因此所有 SRAM 均没有保电的必要)

5.2.3 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 */
    GPIO_SetDebounceTime(GPIO_DBCTL_DBCLKSRC_RCL, GPIO_DBCTL_DBCLKSEL_4);
    /* Enable input debounce function of specified GPIOs */
    GPIO_EnableDebounce(P0, BIT6);
    GPIO_EnableDebounce(P1, BIT2);
    GPIO_EnableDebounce(P0, BIT2);

    /* Set GPIOs to input mode */
    GPIO_SetMode(P0, BIT6, GPIO_MODE_INPUT);
    GPIO_SetMode(P1, BIT2, GPIO_MODE_INPUT);
    GPIO_SetMode(P0, BIT2, GPIO_MODE_INPUT);
    CLK_Wait3vSyncReady(); /* Necessary for P02 to do manual aon-reg sync */

    /* Enable internal pull-up resistor path */
    GPIO_EnablePullupPath(P0, BIT6);
    GPIO_EnablePullupPath(P1, BIT2);
    GPIO_EnablePullupPath(P0, BIT2);
    CLK_Wait3vSyncReady(); /* Necessary for P02 to do manual aon-reg sync */

    /* Wait for a while to ensure the internal pullup is stable before entering low power mode */
    soc_busy_wait(10000);

    /* Enable GPIO interrupts and set trigger type to Falling Edge */
    GPIO_EnableInt(P0, 6, GPIO_INT_FALLING);
    GPIO_EnableInt(P1, 2, GPIO_INT_FALLING);
    GPIO_EnableInt(P0, 2, GPIO_INT_FALLING);

    /* Enable GPIO IRQs in NVIC */
    NVIC_EnableIRQ(GPIO0_IRQn);
    NVIC_EnableIRQ(GPIO1_IRQn);
}
  1. 此函数使用 Panchip Low-Level GPIO Driver 对 GPIO 进行配置

    实际上也可使用更上层的 Panchip HAL GPIO Driver 进行配置,具体可参考GPIO Digital Input Interrupt例程中的相关介绍

  2. 由于 EVB 底板上有 3 个按键,分别对应核心板 P06/P12/P02 等 3 个引脚,因此这里仅配置这 3 个 GPIO 引脚:

    • 配置引脚 Pinmux 至 GPIO 功能

    • 使能去抖功能(并配置去抖时间)

    • 使能 GPIO 数字输入模式

    • 使能内部上拉电阻(按键没有外部上拉电阻)

    • 使能 GPIO 中断,将其配置为下降沿触发中断(即下降沿唤醒),并使能相关 NVIC IRQ

5.2.4 GPIO 中断服务程序

GPIO P0 和 P1 的中断服务程序分别如下:

void GPIO0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdTRUE;

    for (size_t i = 0; i < 8; i++) {
        if (GPIO_GetIntFlag(P0, BIT(i))) {
            GPIO_ClrIntFlag(P0, BIT(i));
            printf("P0_%d INT occurred.\r\n", i);
            /* Notify the task that gpio key is pressed. */
            vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
        }
    }
}

void GPIO1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdTRUE;

    for (size_t i = 0; i < 8; i++) {
        if (GPIO_GetIntFlag(P1, BIT(i))) {
            GPIO_ClrIntFlag(P1, BIT(i));
            printf("P1_%d INT occurred.\r\n", i);
            /* Notify the task that gpio key is pressed. */
            vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
        }
    }
}
  1. 每个 GPIO Port 均需编写自己的中断服务函数,其内部可通过 GPIO_GetIntFlag() 接口判断触发中断的是当前 port 的哪根 pin

  2. 在中断服务函数中需注意调用 GPIO_ClrIntFlag() 接口清除中断标志位

  3. 使用 FreeRTOS vTaskNotifyGiveFromISR() 接口向 App Task 发送通知,表示有 GPIO 中断产生(对应按键按下),此接口中 xHigherPriorityTaskWoken 变量被配置为 pdTRUE,表示当中断返回后将会触发线程调度,而对于此例程来说则是重新调度至 App Task 中的 ulTaskNotifyTake() 处继续执行

5.2.5 与低功耗相关的 Hook 函数

本例程还用到了 2 个与低功耗密切相关的 Hook 函数:

CONFIG_RAM_CODE void vSocDeepSleepEnterHook(void)
{
#if CONFIG_LOG_ENABLE
    reset_uart_io();
#endif
}

CONFIG_RAM_CODE void vSocDeepSleepExitHook(void)
{
#if CONFIG_LOG_ENABLE
    set_uart_io();
#endif
}
  1. FreeRTOS 有一个优先级最低的 Idle Task,当系统调度到此任务后会对当前状态进行检查,以判断是否允许进入芯片 DeepSleep 低功耗流程

  2. 若程序执行到 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.6 其他功能函数

本例程还编写了 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_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 */
  1. parse_stbm1_gpio_wakeup_source() 用于获取当前 GPIO 中断引脚,本例程中在初始化阶段检测到当前为 Standby Mode 1 GPIO 唤醒后,调用此接口即可获取并打印唤醒 IO 是哪个引脚

  2. CONFIG_PM_STANDBY_M1_WAKEUP_WITHOUT_RESET 配置被设置为 1 时, 芯片从 Standby Mode 1 下唤醒后,将不会触发复位,而是从睡眠之前的代码处接着执行,但由于 Standby Mode 1 状态下芯片内部大部分模块均已掉电,因此醒来后需通过函数 restore_hw_peripherals() 重新初始化一些用到的硬件模块