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

LowPower: DeepSleep QDEC Wakeup

1 功能概述

本例程演示如何使 SoC 进入 DeepSleep 状态,然后通过 QDEC 模块输入信号将其唤醒。

2 环境准备

  • 硬件设备与线材:

    • PAN1080 EVB 核心板 2 块,底板 1 块,其中:

      • 一块核心板1 + 底板用作正交编码信号发生器,具体操作请参照 LowPower: DeepSleep Quadrature Encoder 例程文档

      • 另一块核心板2用作正交解码器(QDEC),演示正交信号输入唤醒功能

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

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

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

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

  • 硬件接线:

    • 给 EVB 核心板2供电

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

      • 若核心板2芯片为 QFN32 或 LQFP64 封装,则使用杜邦线将 USB 转 UART 模块的 RX 引脚与核心板上的 P00 引脚相连,USB 转 UART 模块的 TX 引脚与核心板上的 P01 引脚相连

      • 若核心板2芯片为 QFN48 封装,则使用杜邦线将 USB 转 UART 模块的 RX 引脚与核心板上的 P30 引脚相连,USB 转 UART 模块的 TX 引脚与核心板上的 P31 引脚相连

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

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

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

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

    • 使用杜邦线将核心板1(底板)的:

      • P30 引脚(输出正交编码波形 Signal A)连接至核心板2的 P20 引脚(QDEC Z0)

      • P31 引脚(输出正交编码波形 Signal B)连接至核心板2的 P21 引脚(QDEC Z1)

  • PC 软件:

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

3 编译和烧录

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

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

4 例程演示说明

  1. 使用 ZAL 工具分别将 QENC 和 QDEC Wakeup 例程编译烧录至芯片,程序运行成功后可分别在 2 个串口终端上看到如下界面:

    image

    QENC 与 QDEC 初始 Log

    QENC Shell 界面下:输入 help 命令可以查看帮助信息;按 Tab 键可以列出所有实现的 Shell 命令。

  2. 在 QENC Shell 界面输入命令 qenc forward 100,输出 100 个正向相位变化的正交编码波形,观察 QDEC 唤醒与解码情况:

    image

    QENC 波形正向变化 100 个相位

    由 QDEC 端可看出,在 QENC 波形变化了 4 个相位的时候,SoC 成功被唤醒,并最终成功检测出 QENC 正向变化的 100 个相位。

  3. 在 QENC Shell 界面输入命令 qenc backward 150,输出 150 个反向相位变化的正交编码波形,观察 QDEC 唤醒与解码情况:

    image

    QENC 波形反向变化 150 个相位

    由 QDEC 端可看出,在 QENC 波形变化了 4 个相位的时候(由 100 变为 96),SoC 成功被唤醒,并最终成功检测出 QENC 反向变化的 150 个相位。

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

# 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_DCDC_PAN1080=y:使能芯片的 DCDC 供电模式,以降低芯片动态功耗

5.2 App DeviceTree 配置

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

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

其中:

  • 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)
{
	int16_t temp_cnt, curr_cnt, last_cnt, diff;

	/* 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 QDEC module and enable wakeup */
	qdec_module_init();

	while (1) {
		/* Record qdec step counter before entering deepsleep mode */
		last_cnt = QDEC_GetEventCnt(QDEC_CNT_IDX_Z);

		printk("Try to enter deepsleep mode..\n\n");
		k_sem_take(&wakeup_sem, K_FOREVER);
		printk("Waked up from deepsleep mode.\n");

		/* Wait until input waveform end for 100ms */
		temp_cnt = QDEC_GetEventCnt(QDEC_CNT_IDX_Z);
		for (size_t i = 0; i < 100; i++) {
			curr_cnt = QDEC_GetEventCnt(QDEC_CNT_IDX_Z);
			if (curr_cnt != temp_cnt) {
				temp_cnt = curr_cnt; /* Update temp_cnt */
				i = 0;	/* Reset time count value */
			}
			k_busy_wait(1000); /* Busy wait 1ms */
		}

		diff = curr_cnt - last_cnt;

		if (is_qdec_overflow && (diff < 0)) {
			is_qdec_overflow = false;
			diff += 32767;
		}

		if (is_qdec_underflow && (diff > 0)) {
			is_qdec_underflow = false;
			diff -= 32768;
		}

		if (diff > 0) {
			printk("Quardrature waveform go FORWARD %d steps.\n", diff);
		} else {
			printk("Quardrature waveform go BACKWARD %d steps.\n", -diff);
		}

		printk("Total step count: %d\n\n", curr_cnt + qdec_cntr_wrap_cnt);
	}
}
  1. 初始化一个名为 wakeup_sem 的信号量

    在 QDEC 唤醒中断触发时通过 give 此信号量可以激活 main 线程的调度

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

  3. 在 qdec_module_init() 函数中初始化 QDEC 硬件模块

  4. 使用 QDEC_GetEventCnt() 接口获取当前 QDEC 检测到的输入信号变化个数,此接口默认检测个数的范围为 -32768 ~ 32767

  5. 分别使用两个名为 is_qdec_overflowis_qdec_underflow 的变量记录输入计数是否发生上溢或下溢情况

5.3.2 QDEC 初始化程序

QDEC 初始化程序 qdec_module_init() 函数内容如下:

static void qdec_module_init(void)
{
	printk("Initialize hardware QDEC module..\n");

	/* Set pinmux of P20/P21 to QDEC Z0/Z1 */
	SYS_SET_MFP(P2, 0, QDEC_Z0);
	SYS_SET_MFP(P2, 1, QDEC_Z1);
	/* Enable digital input path of P20/P21 */
	GPIO_EnableDigitalPath(P2, BIT0 | BIT1);

	/* Enable clock of HW QDEC module */
	CLK_APB2PeriphClockCmd(CLK_APB2Periph_QDEC, ENABLE);

	/* Set QDEC clock source to the low speed 32K clock to make sure it can be wakeup from deepsleep mode */
	CLK_SetQdecClkSrc(CLKTRIM_QDEC_CLK_SEL_32K);
	/* Do not divide QDEC clock source (default) */
	CLK_SetQdecDiv(0);
	/* Set QDEC counter resolution to 4X */
	QDEC_SetCntResolution(QDEC_CNT_RESOLUTION_4X);
	/* Set QDEC input filter to 3 clock count (default) */
	QDEC_SetFilterThreshold(QDEC_FILTER_PERIOD_3);

	/* Enable Qdec counter overflow and underflow interrupt */
	QDEC_ClearIntMsk(QDEC_INT_CNT_OVERFLOW_Msk | QDEC_INT_CNT_UNDERFLOW_Msk, ENABLE);
	IRQ_DIRECT_CONNECT(QDEC_IRQn, 3, qdec_isr, 0);
	irq_enable(QDEC_IRQn);

	/* Enable QDEC wakeup feature */
	QDEC_Enable(ENABLE, QDEC_WAKEUP_EN_Msk);
	/* Enable QDEC module, filter feature, and channel Z */
	QDEC_Enable(ENABLE, QDEC_EN_Msk | QDEC_FILTER_EN_Msk | QDEC_CHANNEL_Z_EN_Msk);
}
  1. 此函数使用 Panchip HAL QDEC Driver 进行配置

  2. 配置 P20/P21 引脚的 PINMUX 为 QDEC 功能

  3. 使能两个引脚的数字输入功能

  4. 开启 QDEC 硬件模块的时钟

  5. 设置 QDEC 计数时钟为 32K 低速时钟

  6. 设置 QDEC 的时钟分频为不分频(硬件默认配置)

  7. 设置 QDEC 的计数分辨率为 4X,即输入信号的相位发生 90° 变化,计数值就会变化1

  8. 设置 QDEC 的中断功能,并使能上溢和下溢中断

  9. 使能 QDEC 的 Channel Z,并使能唤醒功能

5.3.3 QDEC 中断服务程序

QDEC 中断服务程序如下:

__ramfunc static void qdec_isr(void)
{
	if (QDEC_IsIntOccured(QDEC_INT_CNT_OVERFLOW_Msk)) {
		QDEC_ClearIntFlag(QDEC_INT_CNT_OVERFLOW_Msk);
		qdec_cntr_wrap_cnt += 32767;
		is_qdec_overflow = true;
		printk("QDEC overflow evt, curr cnt: %d\n", (int16_t)QDEC_GetEventCnt(QDEC_CNT_IDX_Z));
	}

	if (QDEC_IsIntOccured(QDEC_INT_CNT_UNDERFLOW_Msk)) {
		QDEC_ClearIntFlag(QDEC_INT_CNT_UNDERFLOW_Msk);
		qdec_cntr_wrap_cnt -= 32768;
		is_qdec_underflow = true;
		printk("QDEC underflow evt, curr cnt: %d\n", (int16_t)QDEC_GetEventCnt(QDEC_CNT_IDX_Z));
	}

	if (QDEC_IsIntOccured(QDEC_INT_WAKEUP_Msk)) {
		QDEC_ClearIntFlag(QDEC_INT_WAKEUP_Msk);
		printk("QDEC wakup evt, curr cnt: %d\n", (int16_t)QDEC_GetEventCnt(QDEC_CNT_IDX_Z));
		/* Trigger main thread reschedule */
		k_sem_give(&wakeup_sem);
	}

	/* To indicate zephyr to schedule new ready thread if there is */
	z_arm_int_exit();
}
  1. 检查是否发生 QDEC 计数上溢中断,如果是,则:

    • 清除中断标志位

    • 更新全局的 QDEC 总计数 qdec_cntr_wrap_cnt,并置位 is_qdec_overflow 标志

  2. 检查是否发生 QDEC 计数下溢中断,如果是,则:

    • 清除中断标志位

    • 更新全局的 QDEC 总计数 qdec_cntr_wrap_cnt,并置位 is_qdec_underflow 标志

  3. 检查是否发生 QDEC 唤醒中断,如果是,则:

    • 清除中断标志位

    • give 信号量,当中断服务程序返回后将会触发线程调度,而对于此例程来说则是重新调度至 main 线程的 main() 函数的 k_sem_take(wakeup_sem) 处继续执行

  4. 执行 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_P00, PAN1080_PIN_FUNC_P00_GPIO);
	pinmux_pin_set(pinmux, PAN_PIN_P01, PAN1080_PIN_FUNC_P01_GPIO);
	pinmux_pin_input_enable(pinmux, PAN_PIN_P01, 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_P00, PAN1080_PIN_FUNC_P00_UART0_TX);
	pinmux_pin_set(pinmux, PAN_PIN_P01, PAN1080_PIN_FUNC_P01_UART0_RX);
	pinmux_pin_input_enable(pinmux, PAN_PIN_P01, PAN_PIN_DIG_INPUT_PATH_ENABLE);
#endif
}
  1. 本例程在 z_power_hw_deep_sleep_enter_hook() 函数中,为防止 UART IO 漏电,编写了相关代码以确保在进入 DeepSleep 模式前:

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

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

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

  2. 本例程在 z_power_hw_deep_sleep_exit_hook() 函数中,编写了相关代码以恢复串口 Log 打印功能:

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

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

6 RAM/Flash资源使用情况

Memory region         Used Size  Region Size  %age Used
FLASH:       20060 B       384 KB      5.10%
SRAM:        7136 B        64 KB     10.89%