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

LowPower: Multiple Wakeup Source

1 功能概述

本例程演示多种唤醒源、多种低功耗模式的切换,包括以下功能:

  • DeepSleep 状态下,通过 UART Rx 引脚唤醒芯片,并完成 UART 数据接收(对应 SoC GPIO 唤醒)

  • DeepSleep 状态下,通过 Zephyr Kernel Timer 定时唤醒(对应 SoC LP Timer 唤醒)

  • Standby Mode 1 状态下,通过按键唤醒芯片(对应 SoC GPIO 唤醒)

2 环境准备

  • 硬件设备与线材:

    • PAN1080 EVB 核心板底板各一块

    • JLink 仿真器(用于烧录例程程序)

    • USB 转 UART 模块(用于查看 QDEC 唤醒例程的串口 Log)

    • USB-TypeC 线一条(用于底板供电和查看串口打印 Log)

    • 杜邦线数根(用于连接各个硬件设备)

  • 硬件接线:

    • 将 EVB 核心板插到 EVB 底板上

    • 使用 USB-TypeC 线,将 PC USB 插口与 EVB 底板 USB->UART 插口相连

    • 使用杜邦线或跳线帽将 EVB 底板上的 TX0 引脚与核心板上的 P00 引脚相连, EVB 底板上的 RX0 引脚与核心板上的 P01 引脚相连,用于 UART0 输出 Log

    • 使用杜邦线将 USB 转 UART 模块的 RX 引脚与核心板上的 P06 引脚相连,USB 转 UART 模块的 TX 引脚与核心板上的 P07 引脚相连,用于 UART1 接收 PC 发送的数据

    • 使用杜邦线将 JLink 仿真器的:

      • SWD_CLK 引脚与 EVB 核心板的 P46 引脚相连

      • SWD_DAT 引脚与 EVB 核心板的 P47 引脚相连

      • SWD_GND 引脚与 EVB 核心板的 GND 引脚相连

  • PC 软件:

    • 串口终端工具(SecureCRT),波特率 921600(用于接收 SoC UART0 发送的 Log)

    • 串口调试助手(UartAssist),波特率 9600(用于发送串口数据至 SoC UART1)

3 编译和烧录

例程位置:zephyr\samples_panchip\low_power\multiple_wakeup_source

使用 ZAL 工具可以对其进行编译、烧录、打开 VS Code 调试等操作。关于 ZAL 工具的详细介绍请参考:Zephyr APP Launcher 工具介绍

4 例程演示说明

  1. 使用 ZAL 工具将编译后的例程烧录至芯片,并确认两个 PC 串口工具配置正确:

    image

    串口初始界面

    由 Log 可知程序初始化后进入了 DeepSleep 模式,等待唤醒

  2. 以 9600 的波特率向 SoC UART1 Rx 引脚发送十六进制数据 00 11 22 33 44

    image

    芯片通过 UART1 Rx 引脚的数据唤醒并成功接收到后续数据

    由 Log 可知芯片成功被 UART1 Rx 引脚接收到的数据唤醒,并成功收到预期数据:

    • 第一个数据 0x00 是唤醒前导码,在 9600 的波特率下,实际上是一个持续时间为 937us (1/9600*9s) 的低电平

    • 在接收第二个数据前,程序将 P06/P07 引脚的 PINMUX 从 GPIO 重新切回 UART1 功能,用于接收后面的数据(即 0x11 0x22 0x33 0x44)

  3. 等待 30 秒,可观察到芯片再次被唤醒并立刻进入 Standby Mode 1:

    UART1 Rx (P07) GPIO wakeup triggered..
    wakeup_sem is taken.
    standby_mode_enter_work is canceled.
    uart actual_read_len: 4, rx_index: 0
    0x11 0x22 0x33 0x44
    Try to take wakeup_sem..
    
    Soc has stayed in deepsleep mode more than 30s, now try to enter standby mode 1..
    
  4. 按下 EVB 底板上的 WKUP 按键,可以看到芯片复位,并提示复位原因为 Standby Mode 1 GPIO Wakeup

    Try to load HW calibration data.. DONE.
    - Chip Type         : 0x80
    - Chip CP Version   : None
    - Chip FT Version   : 7
    - Chip MAC Address  : D0000C06FB74
    - Chip Flash UID    : 31373237304A6D074330FFFFFFFFFFFF
    - Chip Flash Size   : 1024 KB
    *** Booting Zephyr OS build zephyr-v2.7.0-1653 ***
    
    Reset Reason: Standby Mode 1 GPIO Wakeup.
    Try to take wakeup_sem..
    

5 开发者说明

5.1 App Config 配置

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

# Low Power
CONFIG_PM=y
CONFIG_BT_CTLR_SLEEP_CLOCK_SOURCE=1

# Enable GPIO Input Sentinel
CONFIG_PM_GPIO_INPUT_SENTINEL=y

# Enable DC/DC
CONFIG_SOC_DCDC_PAN1080=y

其中:

  • CONFIG_PM=y:使能低功耗流程

  • CONFIG_BT_CTLR_SLEEP_CLOCK_SOURCE=1:低功耗时钟相关配置(目前必须固定配置为1)

  • CONFIG_PM_GPIO_INPUT_SENTINEL=y:开启 GPIO 输入电平检测功能,开启后当系统试图进入 DeepSleep 模式时,会先检查当前是否有使能中断的 GPIO,若有,则接着检查其输入电平是否与中断配置冲突,若冲突则阻止系统进入 DeepSleep 模式

    此处 “输入电平与中断配置冲突” 的含义是:中断配置为下降沿触发而此时输入电平已经为低电平,或者中断配置为上升沿触发而此时输入电平已经为高电平; PAN1080 SoC 的低功耗模式不支持这种冲突情况,因此在配置 GPIO 唤醒的时候为安全起见可将此开关打开。

  • CONFIG_SOC_DCDC_PAN1080=y:使能芯片的 DCDC 供电模式,以降低芯片动态功耗

5.2 App DeviceTree 配置

本例程的 App DeviceTree(对应 app.overlay 文件)配置如下:

&uart1 {
	current-speed = <9600>;
	pinctrl-0 = <&p0_6_uart1_tx &p0_7_uart1_rx>;
	status = "okay";
};

&clk_xtl {
	/* Frequency of XTL clock, DO NOT CHANGE THIS ITEM */
	clock-frequency = <32768>;
	/* Enable/Disable the external 32768 Hz low speed crystal oscillator */
	status = "disabled";
};

&clk_rcl {
	/* Frequency of RCL clock, DO NOT CHANGE THIS ITEM */
	clock-frequency = <32000>;
	/* Enable/Disable the internal 32 KHz Low Speed RC */
	status = "okay";
};

&rcc {
	clocks = <&dpll &clk_rcl>;
	clock-frequency-slow = <32000>;
};

其中:

  • uart1 的:

    • current-speed 属性配置为 9600 以将其初始波特率配置为 9600

    • pinctrl-0 属性配置为 <&p0_6_uart1_tx &p0_7_uart1_rx> 以将其 tx 重定向到 P06 引脚,rx 重定向到 P07 引脚

    • status 属性配置为 okay 以在系统初始化阶段使能 UART1 模块

  • clk_xtlstatus 属性配置为 disabled 以禁用 32768Hz XTL 低速晶振(此为 SDK 默认配置)

  • clk_rclstatus 属性配置为 okay 以打开 32kHz RCL 内部低速时钟(此为 SDK 默认配置)

  • rcc 的:

    • clocks 属性的第 1 项(clk_system)配置为 dpll 以将系统高速时钟(AHB 总线时钟)源选择为 DPLL 时钟(此为 SDK 默认配置)

    • clocks 属性的第 2 项(clk_slow)配置为 clk_rcl 以使系统使用 32kHz RCL 内部低速时钟(此为 SDK 默认配置)

    • clock-frequency-slow 属性配置为 32000 以告诉系统当前使用的低速时钟频率为 32000 Hz(此为 SDK 默认配置)

5.3 程序代码

5.3.1 主程序

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

void main(void)
{
	uint8_t rst_reason;

	/* Get the last reset reason */
	printk("\nReset Reason: ");
	rst_reason = soc_reset_reason_get();
	switch (rst_reason) {
	case SOC_RST_REASON_PIN_RESET:
		printk("nRESET Pin Reset.\n");
		break;
	case SOC_RST_REASON_SYS_RESET:
		printk("NVIC System Reset.\n");
		break;
	case SOC_RST_REASON_STBM1_GPIO_WAKEUP:
		printk("Standby Mode 1 GPIO Wakeup.\n");
		break;
	default:
		printk("Unhandled Reset Reason, refer to more reason define in soc.h!\n");
	}

	/* Initialize wakeup semaphore */
	k_sem_init(&wakeup_sem, 0, 1);

	/* Init specific GPIOs to input mode for wake up use */
	wakeup_gpio_init();

	/* Init uart1 device */
	if (!zephyr_uart1_device_init()) {
		printk("Error: Failed to init uart1 device.\n");
		return;
	}

	/* Init a delayable work for switching from deepsleep mode to standby mode */
	k_work_init_delayable(&standby_mode_enter_work, standby_mode_enter_work_handler);

	while (1) {
		/* Busy wait a while to simulate cpu busy status */
		k_busy_wait(1000000);

		/* Try to take wakeup_sem */
		printk("Try to take wakeup_sem..\n");
		k_sem_take(&wakeup_sem, K_FOREVER);
		printk("wakeup_sem is taken.\n");

		/* Cancel the previous scheduled delayable standby-mode-enter work */
		k_work_cancel_delayable(&standby_mode_enter_work);
		printk("standby_mode_enter_work is canceled.\n");
	}
}

  1. 获取本次系统复位的原因,本例程中仅检测 3 种情况(更多复位情况请参考 soc.h 文件中的相关定义):

    • 芯片 nRESET 引脚复位(nRESET 按键按下)

    • NVIC System Reset 软件复位(JLink 烧录后自动触发,或者软件调用 sys_reboot(0) 触发)

    • Standby Mode 1 GPIO Wakeup 唤醒复位

  2. 初始化一个名为 wakeup_sem 的信号量

    在系统从 DeepSleep 状态下唤醒时通过 give 此信号量可以激活 main 线程的调度

  3. 在 wakeup_gpio_init() 函数中初始化 GPIO 配置

  4. 在 zephyr_uart1_device_init() 函数中初始化 UART1 配置

  5. 使用 k_work_init_delayable() 接口初始化一个 Delayable Work,用于定时触发二级休眠特性(从 DeepSleep 状态下唤醒后立刻进入 Standby 状态)

  6. while (1) 循环中等待 wakeup_sem 信号量,然后立刻取消正在排队的 Delayable Work

5.3.2 GPIO 初始化程序

GPIO 初始化程序 wakeup_gpio_init() 函数内容如下:

static void wakeup_gpio_key_init(void)
{
	/* Set pinmux func as GPIO */
	SYS_SET_MFP(P5, 6, GPIO);

	/* Set GPIOs to input mode */
	GPIO_SetMode(P5, BIT6, GPIO_MODE_INPUT);
	CLK_Wait3vSyncReady(); /* Necessary for P56 to do manual 3v sync */

	/* Enable internal pull-up resistor path */
	GPIO_EnablePullupPath(P5, BIT6);
	CLK_Wait3vSyncReady(); /* Necessary for P56 to do manual 3v sync */

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

	/* Disable P56 interrupt at this time */
	GPIO_DisableInt(P5, 6);
}

static void wakeup_gpio_uart_rx_pin_init(void)
{
	/* Set GPIO to input mode */
	GPIO_SetMode(P0, BIT7, GPIO_MODE_INPUT);

	/* Enable internal pull-up resistor path */
	GPIO_EnablePullupPath(P0, BIT7);

	/* Disable P07 interrupt before entering deepsleep */
	GPIO_DisableInt(P0, 7);
}

static void wakeup_gpio_init(void)
{
	/* Enable customized GPIO ISR to override the default one written in zephyr gpio driver */
	IRQ_DIRECT_CONNECT(GPIO_IRQn, GPIO_IRQ_PRIO, gpio_irq_handler, 0);
	irq_enable(GPIO_IRQn);

	/* Configure gpio wakeup key (P56) */
	wakeup_gpio_key_init();

	/* Configure gpio wakeup pin for uart rx */
	wakeup_gpio_uart_rx_pin_init();
}
  1. 此函数使用 zephyr 的 GPIO Driver 对 GPIO 进行配置

  2. 此部分包括两部分:GPIO 唤醒按键(P56)初始化,以及 GPIO UART Rx 引脚(P07)初始化

5.3.3 GPIO 中断服务程序

GPIO 中断服务程序如下:

static void gpio_irq_handler(void)
{
	/* Check if WKUP (P56) button pressed */
	if (GPIO_GetIntFlag(P5, BIT6)) {
		GPIO_ClrIntFlag(P5, BIT6);
		printk("\nWKUP key (P56) pressed..\n");
	}

	/* Check if UART1 Rx (P07) GPIO wakeup triggered */
	if (GPIO_GetIntFlag(P0, BIT7)) {
		/* Clear gpio pin irq flag */
		GPIO_ClrIntFlag(P0, BIT7);
		printk("\nUART1 Rx (P07) GPIO wakeup triggered..\n");
		while (P07 == 0) {
			/* Busy wait until P07 pulled high, which means the 1st wakup
			 * trigger character (0x00) send done.
			 */
		}
		/* Resume UART1 PIN configs after P07 pulled high */
		SYS_SET_MFP(P0, 6, UART1_TX);
		SYS_SET_MFP(P0, 7, UART1_RX);
		GPIO_DisableInt(P0, 7);
	}

	/* To indicate zephyr to schedule new ready thread if there is */
	z_arm_int_exit();
}
  1. 判断 WKUP(P56)按键是否按下,若是则:

    • 清除此引脚的中断 Flag

    • 打印按键按下的 Log

  2. 判断是否为 P07(UART1 Rx)引脚产生的中断,若是则:

    • 清除此引脚的中断 Flag

    • 打印 UART1 Rx Trigger 的log

    • 等待 P07 引脚拉低(即 P07 接收的唤醒前导数据 0x00 接收完成)

    • 恢复 P06/P07 引脚为 UART1 功能

  3. 执行 z_arm_int_exit() 以通知 OS 当前中断执行完毕,允许 OS 在中断退出后立刻启动线程调度

5.3.4 UART1 中断响应回调函数

UART1 中断响应回调函数如下:

static void uart_rx_isr(const struct device *dev)
{
	static uint8_t rx_index;
	int actual_read_len;

	/* Handle received data */
	actual_read_len = uart_fifo_read(dev, uart_data + rx_index, UART_DATA_BUFF_SIZE - rx_index);

	printk("uart actual_read_len: %d, rx_index: %d\n", actual_read_len, rx_index);

	for (size_t i = 0; i < actual_read_len; i++) {
		printk("0x%x ", uart_data[rx_index + i]);
	}
	printk("\n");

	rx_index += actual_read_len;

	if (rx_index >= UART_DATA_BUFF_SIZE) {
		rx_index = 0;
	}
}

static void app_uart_callback(const struct device *dev, void *user_data)
{
	ARG_UNUSED(user_data);

	/* Update uart event status */
	if (!uart_irq_update(dev)) {
		printk("%s(): Error occurs\n", __func__);
		return;
	}

	/* Check if uart rx ready */
	if (uart_irq_rx_ready(dev)) {
		uart_rx_isr(dev);
	}
}

此函数用于判断 UART1 Rx 数据是否 Ready,若是则将 Rx FIFO 数据读空,并将接收到的数据打印出来

5.3.5 Kernel Delayable Work 回调函数

当 Delayable Work 超时时间到来,会触发对应的回调函数:

static void standby_mode_enter_work_handler(struct k_work *work)
{
	printk("\nSoc has stayed in deepsleep mode more than %ds, now try to enter standby mode 1..\n\n",
		INTERVAL_FOR_SWITCH_LOWPOWER_MODE);

	/* Wait until all UART0 data sending done before entering standby mode */
	while (!(UART_GetLineStatus(UART0) & UART_LINE_TXSR_EMPTY)) {
		/* Busy wait */
	}

	/* Enable P56 interrupt before entering standby mode for wakeup */
	GPIO_EnableInt(P5, 6, GPIO_INT_FALLING);

	/* Try to enter standby mode 1 */
	soc_enter_standby_mode_1(STBM1_WAKEUP_SRC_GPIO, STBM1_RETENTION_SRAM_NONE, 0);

	printk("WARNING: Failed to enter SoC standby mode 1 due to unexpected GPIO IO level detected.\n");
	printk("         The wakeup level of GPIO is configured as low-level-wakeup, however the\n");
	printk("         actual GPIO level right before entering standby mode is already low. The\n");
	printk("         SoC does not support this case, and the API soc_enter_standby_mode_1()\n");
	printk("         just returns when this case is detected.\n");

	while (1) {
		/* Busy wait */
	}
}
  1. 等待 UART0 Tx FIFO 空,防止进入 Standby Mode 之前 UART Tx FIFO 还有数据

  2. 使能 P56 中断,确保进入 Standby Mode 1 后可以通过 P56 按键唤醒系统

  3. 调用 soc_enter_standby_mode_1() 接口,进入 Standby Mode 1 低功耗模式

5.3.6 与低功耗相关的 Hook 函数

目前有 3 个与低功耗密切相关的 Hook 函数:

/*
 * Idle thread hook function for doing something before pm flow.
 * return true to avoid entering the following pm (low power) flow
 * return false otherwise.
 */
bool k_idle_thread_hook(void)
{
	/* Schedule the delayable standby-mode-enter work */
	k_work_schedule(&standby_mode_enter_work, K_SECONDS(INTERVAL_FOR_SWITCH_LOWPOWER_MODE));

	/* Allow entering pm flow */
	return false;
}

__ramfunc void z_power_hw_deep_sleep_enter_hook(void)
{
	/* Handle logging uart (UART0) */

	/* Wait until all UART0 data sending done before entering deepsleep mode */
	while (!(UART_GetLineStatus(UART0) & UART_LINE_TXSR_EMPTY)) {
		/* Busy wait */
	}
	/*
	 * Reset UART0 PINs to GPIO function and disable digital input path of UART Rx PIN
	 * to avoid possible current leakage.
	 */
	SYS_SET_MFP(P0, 0, GPIO);
	SYS_SET_MFP(P0, 1, GPIO);
	GPIO_DisableDigitalPath(P0, BIT1);

	/* Handle communication uart (UART1) */

	/* Wait until all UART1 data sending done before entering deepsleep mode */
	while (!(UART_GetLineStatus(UART1) & UART_LINE_TXSR_EMPTY)) {
		/* Busy wait */
	}
	/* Reset UART1 PINs to GPIO function and enable gpio interrupt for uart rx pin. */
	SYS_SET_MFP(P0, 6, GPIO);
	SYS_SET_MFP(P0, 7, GPIO);
	GPIO_EnableInt(P0, 7, GPIO_INT_FALLING);

	/* Enable P56 interrupt before entering deepsleep mode for wakeup */
	GPIO_EnableInt(P5, 6, GPIO_INT_FALLING);
}

__ramfunc void z_power_hw_deep_sleep_exit_hook(void)
{
	/* Resume UART0 PIN Configurations to reenable UART function */
	SYS_SET_MFP(P0, 0, UART0_TX);
	SYS_SET_MFP(P0, 1, UART0_RX);
	GPIO_EnableDigitalPath(P0, BIT1);

	/* Resume UART1 PIN Configurations here to reenable UART function if is not waked up by P07 */
	if (!GPIO_GetIntFlag(P0, BIT7)) {
		SYS_SET_MFP(P0, 6, UART1_TX);
		SYS_SET_MFP(P0, 7, UART1_RX);
		GPIO_DisableInt(P0, 7);
	}

	/* Re-disable P56 interrupt after deepsleep wakeup */
	GPIO_DisableInt(P5, 6);

	/* Trigger main thread reschedule after wakeup from deepsleep mode */
	k_sem_give(&wakeup_sem);
}
  1. Zephyr 有一个优先级最低的 idle 线程,当系统调度到此线程后会立刻触发 k_idle_thread_hook() 钩子函数。系统默认实现了一个 weak 属性的 k_idle_thread_hook() 函数,而在 App 中可以重新实现此函数。若令此函数返回 false,则表示允许程序 run 进 idle 线程后续的低功耗流程中; 而若令此函数返回 true,则表示阻止系统执行 idle 线程的低功耗流程(此情况下程序在 idle 线程中空转)。

    对于本例程来说,在 main 线程中 take 信号量之后,由于系统没有其他的 ready 线程等待调度,因此会直接调度至 idle 线程中。在 Idle Hook 函数中, 我们直接 schedule 了一个 delayable work,当设定时间到来后,会触发对应的 handler,使系统进入 standby mode 1 状态

  2. 当程序执行到 idle 线程中低功耗流程的 DeepSleep 子流程中后,会在 SoC 进入 DeepSleep 模式之前执行 z_power_hw_deep_sleep_enter_hook() 函数,在 SoC 从 DeepSleep 模式下唤醒后执行 z_power_hw_deep_sleep_exit_hook() 函数。

    • 本例程在 z_power_hw_deep_sleep_enter_hook() 函数中:

      • 为防止 UART0 IO 漏电,在进入 DeepSleep 状态前:

        • 确保串口 Log 数据都打印完毕(即 UART0/UART1 Tx FIFO 应为空)

        • 将 P00 引脚 Pinmux 功能由 UART0 Tx 切换回 GPIO

        • 将 P01 引脚 Pinmux 功能由 UART0 Rx 切换回 GPIO,并将其数字输入功能关闭

      • 为使得 UART1 Rx 收到数据后能唤醒,在进入 DeepSleep 状态前:

        • 将 P06 引脚 Pinmux 功能由 UART0 Tx 切换回 GPIO

        • 将 P07 引脚 Pinmux 功能由 UART0 Rx 切换回 GPIO,并使能此引脚的中断(下降沿触发)

      • 为确保 P56 按键唤醒功能正常,使能 P56 引脚的中断(下降沿触发)

    • 本例程在 z_power_hw_deep_sleep_exit_hook() 函数中:

      • 恢复串口 Log 打印功能:

        • P00 引脚 Pinmux 功能由 GPIO 重新切换成 UART0 Tx

        • P01 引脚 Pinmux 功能由 GPIO 重新切换成 UART0 Rx,并将其数字输入功能重新打开

      • 当不是 P07 引脚 GPIO 唤醒(即 UART1 Rx)时,重新设置 P06/P07 引脚为 UART1 功能

      • 关闭 P56 引脚的 GPIO 中断功能

      • 调用 k_sem_give() 接口,释放 wakeup_sem 信号量,使程序从低功耗状态下唤醒后,能够调度到 main() 函数中

6 RAM/Flash资源使用情况

Memory region         Used Size  Region Size  %age Used
FLASH:       22284 B       384 KB      5.67%
SRAM:        8792 B        64 KB     13.42%