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

LowPower: DeepSleep KSCAN Wakeup

1 功能概述

本例程演示如何使 SoC 进入 DeepSleep 状态,然后通过连接 KSCAN 模块的矩阵按键将其唤醒。

2 环境准备

  • 硬件设备与线材:

    • PAN1080 EVB 核心板底板各一块

    • PAN1080 EVB 配套 8x8 矩阵按键板(PAN1080X Development Kit Keyboard V2.1)一块

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

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

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

  • 硬件接线:

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

    • 将 Keyboard 按键板通过排线接到 EVB 底板上

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

    • 根据核心板芯片的型号,选择对应的 UART Log 线接法:

      • 确认核心板芯片为LQFP64 封装,然后使用杜邦线将 EVB 底板上的 TX0 引脚与核心板上的 P30 引脚相连, EVB 底板上的 RX0 引脚与核心板上的 P31 引脚相连

      • 由于 kscan 引脚限制,本例程不支持封装为 QFN32 和 QFN48 的核心板

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

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

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

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

  • PC 软件:

    • 终端工具(SecureCRT),波特率 921600(用于接收串口打印 Log)

3 编译和烧录

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

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

4 例程演示说明

  1. 使用 ZAL 工具将 KSCAN Wakeup 例程编译烧录至芯片,程序在初始化完成后立刻进入 DeepSleep 模式:

    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 ***
    Initialize hardware KSCAN module..
    main(): Try to enter deepsleep mode..
    
  2. 按一下 Keyboard 按键板上的任意一个按键(如 K11),可以看到芯片被成功从 DeepSleep 状态下唤醒,然后在 1s 以后重新进入 DeepSleep 模式:

    main(): Waked up from deepsleep mode.
    K11 pressed..
    K11 released..
    main(): Try to enter deepsleep mode..
    
  3. 再按住 Keyboard 按键板上的任意一个按键(如 K64),然后在 2s 以后松开,可以观察到芯片被按键按下的事件唤醒,稍后重新进入 DeepSleep 模式,然后又被按键松开的事件重新唤醒:

    main(): Waked up from deepsleep mode.
    K64 pressed..
    main(): Try to enter deepsleep mode..
    
    main(): Waked up from deepsleep mode.
    K64 released..
    main(): Try to enter deepsleep mode..
    

5 开发者说明

5.1 App Config 配置

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

# Enable Low Power Flow
CONFIG_PM=y
CONFIG_BT_CTLR_SLEEP_CLOCK_SOURCE=1

# Forcely calibrate 32K RCL Clock
CONFIG_SOC_FORCE_CALIB_RCL_CLK=y

# Increase Low power LDO calibrate code (can be 0/1/2/3)
CONFIG_SOC_INCREASE_LLDO_CALIB_CODE=1

# Enable DC/DC
CONFIG_SOC_DCDC_PAN1080=y

其中:

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

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

  • CONFIG_SOC_FORCE_CALIB_RCL_CLK=y:在系统初始化阶段强制校准内部 32K RCL Clock 时钟(若当前使用的芯片为校准后的芯片,则此开关也可以不开以节约系统启动时间)

  • CONFIG_SOC_INCREASE_LLDO_CALIB_CODE=1:强制提高芯片 LLDO(Lowpower LDO)的校准档位(当芯片使用 KSCAN 无法正常唤醒的时候可以尝试配置此功能)

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

5.2 App DeviceTree 配置

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

&gpioctrl {
	/* port, pin, pinmux_name, pinmux_sel [, flag1, ... ] */
	DT_PAN_PINS(p3, 1, uart0_rx, PAN1080_PIN_FUNC_P31_UART0_RX, input-enable);
};

&uart0 {
	current-speed = <921600>;
	pinctrl-0 = <&p3_0_uart0_tx &p3_1_uart0_rx>;
	status = "okay";
};

&uart1 {
	status = "disabled";
};

&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";
};

&dpll {
	/* f(dpll_output) = f(dpll_input) / clock_div * clock_mult */
	clocks = <&clk_xth>;
	clock-div = <2>;  /* Fixed to 2 */
	clock-mult = <4>; /* Can be 3 (48MHz) or 4 (64MHz) */
	status = "okay";
};

&rcc {
	clock-names = "clk_system", "clk_slow";
	clocks = <&dpll &clk_rcl>;
	clock-frequency-system = <DT_FREQ_M(64)>;
	clock-frequency-slow = <32000>;
	ahb-prescaler = <1>;
	apb1-prescaler = <1>;
	apb2-prescaler = <1>;
};

其中:

  • 更新 gpioctrl 中的引脚属性配置,将 P31(用作 UART Rx)引脚的 input-enable 功能打开,以使能此引脚的数字输入功能

  • uart0pinctrl-0 属性配置为 <&p3_0_uart0_tx &p3_1_uart0_rx>,以将 uart0 的 tx 重定向到 P30 引脚,rx重定向到 P31 引脚

  • uart1status 属性配置为 disabled 以禁止系统上电后初始化 uart1 设备,确保不会因为 uart1 的引脚配置产生 IO 漏电

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

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

  • dpllclock-mult 属性配置为 4 以将 DPLL 时钟输出配置为 64MHz(此为 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)
{
	/* Initialize wakeup semaphore */
	k_sem_init(&wakeup_sem, 0, 1);

	/* Get pinmux device structure */
	pinmux = device_get_binding("PINMUX");
	if (pinmux == NULL) {
		printk("get device PINMUX error!\n");
		return;
	}

	/* Init hardware KSCAN module and enable wakeup */
	kscan_module_init();

	while (1) {
		printk("%s(): Try to enter deepsleep mode..\n\n", __func__);
		k_sem_take(&wakeup_sem, K_FOREVER);
		printk("%s(): Waked up from deepsleep mode.\n", __func__);
		/* Busy wait 1s to avoid soc entering deepsleep mode again after wakeup */
		k_busy_wait(1000000);
	}
}
  1. 初始化一个名为 wakeup_sem 的信号量

  2. 获取 PINMUX 的设备指针,用于后面修改 pinmux 引脚配置的时候使用

  3. 在 kscan_module_init() 函数中初始化 KSCAN 硬件模块

  4. while (1) 循环中等待 wakeup_sem 信号量,然后 busy wait 1s 后重新通过 k_sem_take 的方式进入 DeepSleep 状态

5.3.2 KSCAN 初始化程序

KSCAN 初始化程序 kscan_module_init() 函数内容如下:

static void kscan_io_init(void)
{
	/* Set pinmux to enable kscan input function */
	SYS_SET_MFP(P2, 0, KSCAN_I0);
	SYS_SET_MFP(P2, 1, KSCAN_I1);
	SYS_SET_MFP(P2, 2, KSCAN_I2);
	SYS_SET_MFP(P2, 3, KSCAN_I3);
	SYS_SET_MFP(P1, 4, KSCAN_I4);
	SYS_SET_MFP(P1, 5, KSCAN_I5);
	SYS_SET_MFP(P1, 6, KSCAN_I6);
	SYS_SET_MFP(P1, 7, KSCAN_I7);
	/* Enable internal pull-down resistor on kscan input pads */
	GPIO_EnablePulldownPath(P2, BIT0 | BIT1 | BIT2 | BIT3);
	GPIO_EnablePulldownPath(P1, BIT4 | BIT5 | BIT6 | BIT7);
	/* Enable digital input path on kscan input pads */
	GPIO_EnableDigitalPath(P2, BIT0 | BIT1 | BIT2 | BIT3);
	GPIO_EnableDigitalPath(P1, BIT4 | BIT5 | BIT6 | BIT7);
	/* Enable kscan input pins */
	KS_InputEnable(KSCAN, ENABLE, 0xFF);

	/* Set pinmux to enable kscan output function */
	SYS_SET_MFP(P5, 0, KSCAN_O0);
	SYS_SET_MFP(P5, 1, KSCAN_O1);
	SYS_SET_MFP(P4, 0, KSCAN_O2);
	SYS_SET_MFP(P5, 3, KSCAN_O3);
	SYS_SET_MFP(P5, 5, KSCAN_O4);
	SYS_SET_MFP(P5, 7, KSCAN_O5);
	SYS_SET_MFP(P0, 0, KSCAN_O6);
	SYS_SET_MFP(P0, 1, KSCAN_O7);
	/* Enable kscan output pins */
	KS_OutputEnable(KSCAN, ENABLE, 0x0000FF);
}

static void kscan_module_init(void)
{
	printk("Initialize hardware KSCAN module..\n");

	/* Enable clock of hw kscan module */
	CLK_APB2PeriphClockCmd(CLK_APB2Periph_KSCAN, ENABLE);
	/* Select 32K clock as scan source */
	CLK_SetKeyscanClkSrc(CLKTRIM_KSCAN_CLK_SEL_32K);

	/* Init IO for kscan */
	kscan_io_init();

	/* Enable Interrupt */
	KS_IntEnable(KSCAN, ENABLE);
	IRQ_DIRECT_CONNECT(KSCAN_IRQn, KSCAN_HW_INTERRUPT_PRIORITY, kscan_isr, 0);
	irq_enable(KSCAN_IRQn);

	/* Set valid scan signal to high level */
	KS_SetKeyscanPolarity(KSCAN, ENABLE);
	/* Set scan interval between rows, t_clk_cnt = reg_val + 1 */
	KS_SetRowInterval(KSCAN, 0x2);
	/* Set scan interval between frames, t_clk_cnt = 2 ^ reg_val + 1 */
	KS_SetFrameInterval(KSCAN, 0x1);
	/* Set debounce time when key pressed, t_clk_cnt = 2 ^ reg_val + 1 */
	KS_SetDebounceTime(KSCAN, 0x2);

	/*
	 * Now the scan interval of each frame becomes:
	 * t_interval = t_row_interval * 24 + t_frame_interval * 24
	 *            = ((row_reg_val + 1) * 24 + (2 ^ frame_reg_val + 1) * 24) / 32000
	 *            = ((2 + 1) * 24 + (2^1 + 1) * 24) / 32000 s
	 *            = 0.0045s = 4.5ms
	 */

	/* Clear All Keys' Status Regs */
	KS_ClearKeyscanStatus(KSCAN, ENABLE);
	/* Start Scanning and wakeup feature */
	KS_Enable(KSCAN, ENABLE, KS_WAKEUP_AND_PERIPH_ENABLE);
}
  1. 此函数使用 Panchip HAL KSCAN Driver 进行配置

  2. 开启 KSCAN 硬件模块的时钟

  3. 设置 KSCAN 扫描时钟为 32K 低速时钟

  4. KSCAN 用到的 IO 较多,因此将 IO 相关的配置单独写在 kscan_io_init() 函数中

  5. 设置 KSCAN 的中断功能

  6. 设置 KSCAN 为高电平扫描

  7. 设置 KSCAN 扫描间隔参数

  8. 使能 KSCAN 及唤醒功能

5.3.3 KSCAN 中断服务程序

KSCAN 中断服务程序如下:

__ramfunc static void kscan_isr(void)
{
	static uint8_t last_key_status[24] = { 0 };
	uint8_t *ks_info_base = (uint8_t *)&KSCAN->KS_INFO0;
	uint8_t key_detect_flag;

	for (size_t row = 0; row < 24; row++) {
		key_detect_flag = ks_info_base[row] ^ last_key_status[row];
		if (key_detect_flag) {
			for (size_t col = 0; col < 8; col++) {
				if (key_detect_flag & BIT(col)) {
					if (ks_info_base[row] & BIT(col)) {
						printk("K%d pressed..\n", row + col * 8 + 1);
					} else {
						printk("K%d released..\n", row + col * 8 + 1);
					}
				}
			}
		}
		last_key_status[row] = ks_info_base[row];
	}

#ifdef KSCAN_EVENT_DEBUG
	printk("\nKey status change detected..\n");
	for (size_t i = 0; i < 24; i++) {
		if (i % 8 == 0) {
			printk("KSCAN ROW INFO %02d~%02d: ", i, i + 7);
		}
		printk("0x%02x ", ks_info_base[i]);
		if ((i + 1) % 8 == 0) {
			printk("\n");
		}
	}
#endif
	/* Clear all flags if there is */
	KS_ClearFlag(KSCAN, KS_INT_FLAG_Msk | KS_WK_FLAG_Msk | KS_FLAG_Msk);
	/* Re-enable kscan */
	KS_Enable(KSCAN, ENABLE, KS_PERIPHRAL_ENABLE);

	/* To indicate zephyr to schedule new ready thread if there is */
	z_arm_int_exit();
}
  1. 检查是哪个按键引起的中断,并判断按键是按下还是释放

  2. 清除 KSCAN 相关的中断标志位

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

5.3.4 与低功耗相关的 Hook 函数

本例程实现了 2 个与低功耗相关的 Hook 函数:

__ramfunc void z_power_hw_deep_sleep_enter_hook(void)
{
#ifdef CONFIG_SERIAL
	/* Wait until all UART0 data sending done before entering deepsleep mode */
	while (!(UART_GetLineStatus(UART0) & UART_LINE_TXSR_EMPTY)) {
		/* Busy wait */
	}

	/*
	 * Reset UART PINs to GPIO function and disable digital input path of UART Rx PIN
	 * to avoid possible current leakage.
	 */
	pinmux_pin_set(pinmux, PAN_PIN_P30, PAN1080_PIN_FUNC_P30_GPIO);
	pinmux_pin_set(pinmux, PAN_PIN_P31, PAN1080_PIN_FUNC_P31_GPIO);
	pinmux_pin_input_enable(pinmux, PAN_PIN_P31, PAN_PIN_DIG_INPUT_PATH_DISABLE);
#endif
}

__ramfunc void z_power_hw_deep_sleep_exit_hook(void)
{
#ifdef CONFIG_SERIAL
	/* Resume UART PIN Configurations to reenable UART function */
	pinmux_pin_set(pinmux, PAN_PIN_P30, PAN1080_PIN_FUNC_P30_UART0_TX);
	pinmux_pin_set(pinmux, PAN_PIN_P31, PAN1080_PIN_FUNC_P31_UART0_RX);
	pinmux_pin_input_enable(pinmux, PAN_PIN_P31, PAN_PIN_DIG_INPUT_PATH_ENABLE);
#endif
	/* Trigger main thread reschedule after wakeup from deepsleep mode */
	k_sem_give(&wakeup_sem);
}
  1. 本例程在 z_power_hw_deep_sleep_enter_hook() 函数中,为防止 UART IO 漏电,编写了相关代码以确保在进入 DeepSleep 模式前:

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

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

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

  2. 本例程在 z_power_hw_deep_sleep_exit_hook() 函数中:

    • 编写了相关代码以恢复串口 Log 打印功能:

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

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

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

6 RAM/Flash资源使用情况

Memory region         Used Size  Region Size  %age Used
FLASH:       20020 B       384 KB      5.09%
SRAM:        7120 B        64 KB     10.86%