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

LowPower: DeepSleep Quadrature Encoder

1 功能概述

本例程是基于 Zephyr Shell 的正交编码波形发生器(Quadrature Encoder),用于通过 GPIO 输出 SoC QDEC 硬件模块可识别的正交编码波形,支持正向和反向两种波形。

2 环境准备

  • 硬件设备与线材:

    • PAN1080 EVB 核心板底板各一块

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

    • 逻辑分析仪(本文使用 Kingst LA1010 逻辑分析仪进行演示)

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

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

  • 硬件接线:

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

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

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

      • 若核心板芯片为 QFN32 或 LQFP64 封装,则使用杜邦线或跳线帽将 EVB 底板上的 TX0 引脚与核心板上的 P00 引脚相连, EVB 底板上的 RX0 引脚与核心板上的 P01 引脚相连

      • 本例程暂不支持封装为 QFN48 的核心板

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

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

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

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

    • 将 LA1010 硬件的:

      • USB 接口连接至 PC USB 接口

      • CH0 引脚连接至 EVB 底板的 P30 引脚(输出正交编码波形 Signal A)

      • CH1 引脚连接至 EVB 底板的 P31 引脚(输出正交编码波形 Signal B)

  • PC 软件:

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

    • Kingst VIS(LA1010 逻辑分析仪上位机软件)

3 编译和烧录

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

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

4 例程演示说明

  1. 使用 ZAL 工具将编译后的例程烧录至芯片,程序运行成功后可在串口终端上看到如下 Shell 界面:

    image

    Zephyr Shell 主界面

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

  2. 打开逻辑分析仪上位机软件,将通道 0 配置为上升沿触发,然后启动采样

  3. 在 Shell 界面输入命令 qenc forward 10,输出 10 个正向相位变化的正交编码波形:

    image

    正交编码波形正向变化 10 个相位

    由波形图可看出,正交编码波形输出了两路信号,其中 Signal A 比 Signal B 超前了 90° 相位(1/4 周期),且每 1ms 总有一路信号会发生一次相位变化,共产生了 10 次相位变化后停止。

  4. 在 Shell 界面输入命令 qenc backward 15,输出 15 个负向相位变化的正交编码波形:

    image

    正交编码波形负向变化 15 个相位

    由波形图可看出,正交编码波形输出了两路信号,其中 Signal A 比 Signal B 延后了 90° 相位(1/4 周期),且每 1ms 总有一路信号会发生一次相位变化,共产生了 15 次相位变化后停止。

  5. 当停留在 Shell 命令界面超过 5 分钟没有执行 qenc 命令后,系统自动进入 DeepSleep 低功耗状态:

    image

    操作超时进入 DeepSleep 低功耗状态

  6. 此时按 EVB 底板上的 WKUP 唤醒按键,可以将系统重新唤醒,唤醒后可以正常执行 Shell 命令:

    image

    将系统从 DeepSleep 低功耗状态下唤醒

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 Zephyr Shell
CONFIG_SHELL=y
CONFIG_SHELL_CMDS=n
CONFIG_KERNEL_SHELL=n
CONFIG_DEVICE_SHELL=n
CONFIG_DEVMEM_SHELL=n
# CONFIG_SHELL_MINIMAL=y
# CONFIG_SHELL_STACK_SIZE=1024

# 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_SHELL=y:使能 Zephyr Shell 框架

  • CONFIG_SHELL_CMDS=n:不使能 Zephyr Shell 机制相关的命令,如 historyresize

  • CONFIG_KERNEL_SHELL=n:不使能 Kernel 相关的 Shell 命令

  • CONFIG_DEVICE_SHELL=n:不使能 Device 相关的 Shell 命令

  • CONFIG_DEVMEM_SHELL=n:不使能 DevMem 相关的 Shell 命令

  • 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)
{
	/* Get pinmux device handle */
	pinmux = device_get_binding("PINMUX");
	if (pinmux == NULL) {
		printk("get device PINMUX error!\n");
		return;
	}

	/* Init WKUP button (GPIO P56) */
	wakeup_gpio_key_init();

	/* Init Simulated Quadrature Encoder IOs */
	quadrature_phase_signal_simulator_init();

	/* Init kernel timer to trigger soc entering deepsleep mode */
	k_timer_init(&deepsleep_trigger_timer, timer_expired_cb, NULL);
	k_timer_start(&deepsleep_trigger_timer, K_SECONDS(DPSLP_TIMEOUT_S), K_NO_WAIT);
}
  1. 获取 PINMUX 的设备指针,用于后面修改 pinmux 引脚配置的时候使用

  2. 在 wakeup_gpio_key_init() 函数中初始化唤醒按键对应的 GPIO 配置

  3. 在 quadrature_phase_signal_simulator_init() 函数中初始化模拟正交编码器的 IO 配置

  4. 分别使用 OS 提供的 k_timer_init() 和 k_timer_start() 接口,使能一个超时时间为 DPSLP_TIMEOUT_S 的 Kernel Timer,用于超时进入低功耗状态,当 k_timer 超时后,将会执行名为 timer_expired_cb 的回调函数,此函数中仅仅置为一个名为 deepsleep_timer_expired 的标志变量:

    static void timer_expired_cb(struct k_timer *timer)
    {
       deepsleep_timer_expired = true;
    }
    

5.3.2 GPIO 初始化程序

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

void wakeup_gpio_key_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);

	/* 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 to avoid trigger in SoC active mode */
	GPIO_DisableInt(P5, 6);
}
  1. 此函数使用 Panchip HAL GPIO Driver 进行配置

  2. 使能 GPIO 中断

  3. 配置 P56 引脚的 PINMUX 为 GPIO 功能

  4. 将 P56 IO 配置为数字输入模式

  5. 开启 P56 引脚的内部上拉电阻

  6. 调用 SYS_delay_10nop() 接口,延时一段时间以确保上拉状态文档

  7. 关闭 P56 引脚的输入中断使能

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 pressed, use <CTRL + C> to evoke the Prompt..\n");
		/* Restart kernel timer to schedule another deepsleep waiting flow */
		k_timer_start(&deepsleep_trigger_timer, K_SECONDS(DPSLP_TIMEOUT_S), K_NO_WAIT);
		deepsleep_timer_expired = false;
	}

	/* To indicate zephyr to schedule new ready thread if there is */
	z_arm_int_exit();
}
  1. 检查是否为 P56 引脚触发的中断,如果是,则:

    • 清除 P56 中断标志位

    • 重置 k_timer 计数

    • 将 deepsleep_timer_expired 标志变量置为 flase

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

5.3.4 创建 Shell 自定义命令

使用 Zephyr 提供的 Shell 命令注册接口,实现一个名为 qenc 的顶层命令,并在其下分别创建两个子命令 forwarebackward

static int cmd_qenc_forward(const struct shell *shell, size_t argc, char **argv)
{
	int step = atoi(argv[1]);

	shell_print(shell, "QENC go forward %d step(s)..", step);

	/* Reschedule dpslp trigger timer to avoid entering deepsleep while we are using this shell command */
	k_timer_start(&deepsleep_trigger_timer, K_SECONDS(DPSLP_TIMEOUT_S), K_NO_WAIT);

	for (size_t i = 0; i < step; i++) {
		quadrature_phase_signal_simulator_state(&sim_qsig_ctx_cntr, SIM_QSIG_STATE_GO_FORWARD);
		k_busy_wait(1000);
	}

	return 0;
}

static int cmd_qenc_backward(const struct shell *shell, size_t argc, char **argv)
{
	int step = atoi(argv[1]);

	shell_print(shell, "QENC go backward %d step(s)..", step);

	/* Reschedule dpslp trigger timer to avoid entering deepsleep while we are using this shell command */
	k_timer_start(&deepsleep_trigger_timer, K_SECONDS(DPSLP_TIMEOUT_S), K_NO_WAIT);

	for (size_t i = 0; i < step; i++) {
		quadrature_phase_signal_simulator_state(&sim_qsig_ctx_cntr, SIM_QSIG_STATE_GO_BACKWARD);
		k_busy_wait(1000);
	}

	return 0;
}

/* Statically init shell command for configuring the simulated Quadrature Encoder */
SHELL_STATIC_SUBCMD_SET_CREATE(qenc_sub_cmd,
	SHELL_CMD_ARG(forward, NULL,
		"Quadrature Encoder Go Forward.\nExample usage: [ qenc forward"
		" 100 ] will trigger the simulated QENC go forward 100 steps.",
		cmd_qenc_forward, 2, 0),
	SHELL_CMD_ARG(backward, NULL,
		"Quadrature Encoder Go Backward.\nExample usage: [ qenc backward"
		" 50 ] will trigger the simulated QENC go backward 50 steps.",
		cmd_qenc_backward, 2, 0),
	SHELL_SUBCMD_SET_END /* Array terminated. */
);
SHELL_CMD_REGISTER(qenc, &qenc_sub_cmd, "Quadrate Encoder used to simulate QDEC input waveform", NULL);
  1. 使用 Zephyr 提供的 Shell 命令注册接口注册自定义 Shell 命令:

    • 使用 SHELL_CMD_REGISTER 注册一个名为 qenc 的顶层命令

    • 使用 SHELL_STATIC_SUBCMD_SET_CREATE 创建 Shell 子命令集

    • 使用 SHELL_CMD_ARG 分别创建两个带参数的子命令:forwardbackward

  2. 分别实现两个子命令功能:

    • forward 命令,对应 cmd_qenc_forward 函数,实现根据输入参数输出正向的正交编码波形功能,并重置 k_timer 计数

    • forward 命令,对应 cmd_qenc_backward 函数,实现根据输入参数输出反向的正交编码波形功能,并重置 k_timer 计数

5.3.5 与低功耗相关的 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)
{
	if (deepsleep_timer_expired) {
		/* Allow entering deepsleep flow */
		printk("\n\n%ds timeout, enter deepsleep mode, press WKUP key to resume..\n", DPSLP_TIMEOUT_S);
		return false;
	} else {
		/* Prevent entering deepsleep flow */
		return true;
	}
}

__ramfunc void z_power_hw_deep_sleep_enter_hook(void)
{
	/* Enable P56 interrupt before entering deepsleep */
	GPIO_EnableInt(P5, 6, GPIO_INT_FALLING);
#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
	/* Disable P56 interrupt after waking up from deepsleep */
	GPIO_DisableInt(P5, 6);
}
  1. Zephyr 有一个优先级最低的 idle 线程,当系统调度到此线程后会立刻触发 k_idle_thread_hook() 钩子函数。系统默认实现了一个 weak 属性的 k_idle_thread_hook() 函数,而在 App 中可以重新实现此函数。若令此函数返回 false,则表示允许程序 run 进 idle 线程后续的低功耗流程中; 而若令此函数返回 true,则表示阻止系统执行 idle 线程的低功耗流程(此情况下程序在 idle 线程中空转)。

    对于本例程来说,在 main() 函数退出后,由于系统没有其他的 ready 线程等待调度,因此会直接调度至 idle 线程中,继而执行 k_idle_thread_hook() 函数,在此 hook 函数中,检查 deepsleep_timer_expired 变量是否被置为,若是则允许系统进入低功耗流程,否则阻止系统进入低功耗。

  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() 函数中:

      • 为使 WKUP 按键能正常唤醒系统,使能了 P56 引脚的中断,并将触发条件配置为下降沿

      • 为防止 UART IO 漏电,编写了相关代码以确保在进入 DeepSleep 模式前:

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

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

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

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

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

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

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

      • 重新禁止 P56 引脚的中断功能,以防止唤醒状态下按 WKUP 按键也触发 GPIO 中断

6 RAM/Flash资源使用情况

Memory region         Used Size  Region Size  %age Used
FLASH:       34484 B       384 KB      8.77%
SRAM:       12264 B        64 KB     18.71%