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 例程演示说明¶
使用 ZAL 工具将编译后的例程烧录至芯片,程序运行成功后可在串口终端上看到如下 Shell 界面:
Shell 界面下:输入
help
命令可以查看帮助信息;按Tab
键可以列出所有实现的 Shell 命令。打开逻辑分析仪上位机软件,将通道 0 配置为上升沿触发,然后启动采样
在 Shell 界面输入命令
qenc forward 10
,输出 10 个正向相位变化的正交编码波形:由波形图可看出,正交编码波形输出了两路信号,其中 Signal A 比 Signal B 超前了 90° 相位(1/4 周期),且每 1ms 总有一路信号会发生一次相位变化,共产生了 10 次相位变化后停止。
在 Shell 界面输入命令
qenc backward 15
,输出 15 个负向相位变化的正交编码波形:由波形图可看出,正交编码波形输出了两路信号,其中 Signal A 比 Signal B 延后了 90° 相位(1/4 周期),且每 1ms 总有一路信号会发生一次相位变化,共产生了 15 次相位变化后停止。
当停留在 Shell 命令界面超过 5 分钟没有执行
qenc
命令后,系统自动进入 DeepSleep 低功耗状态:此时按 EVB 底板上的 WKUP 唤醒按键,可以将系统重新唤醒,唤醒后可以正常执行 Shell 命令:
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 机制相关的命令,如history
、resize
等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>;
};
其中:
将 uart1 的
status
属性配置为disabled
以禁止系统上电后初始化 uart1 设备,确保不会因为 uart1 的引脚配置产生 IO 漏电将 clk_xtl 的
status
属性配置为disabled
以禁用 32768Hz XTL 低速晶振(此为 SDK 默认配置)将 clk_rcl 的
status
属性配置为okay
以打开 32kHz RCL 内部低速时钟(此为 SDK 默认配置)将 dpll 的
clock-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);
}
获取 PINMUX 的设备指针,用于后面修改 pinmux 引脚配置的时候使用
在 wakeup_gpio_key_init() 函数中初始化唤醒按键对应的 GPIO 配置
在 quadrature_phase_signal_simulator_init() 函数中初始化模拟正交编码器的 IO 配置
分别使用 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);
}
此函数使用 Panchip HAL GPIO Driver 进行配置
使能 GPIO 中断
配置 P56 引脚的 PINMUX 为 GPIO 功能
将 P56 IO 配置为数字输入模式
开启 P56 引脚的内部上拉电阻
调用 SYS_delay_10nop() 接口,延时一段时间以确保上拉状态文档
关闭 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();
}
检查是否为 P56 引脚触发的中断,如果是,则:
清除 P56 中断标志位
重置 k_timer 计数
将 deepsleep_timer_expired 标志变量置为 flase
执行 z_arm_int_exit() 以通知 OS 当前中断执行完毕,允许 OS 在中断退出后立刻启动线程调度
5.3.4 创建 Shell 自定义命令¶
使用 Zephyr 提供的 Shell 命令注册接口,实现一个名为 qenc
的顶层命令,并在其下分别创建两个子命令 forware
和 backward
:
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);
使用 Zephyr 提供的 Shell 命令注册接口注册自定义 Shell 命令:
使用
SHELL_CMD_REGISTER
注册一个名为qenc
的顶层命令使用
SHELL_STATIC_SUBCMD_SET_CREATE
创建 Shell 子命令集使用
SHELL_CMD_ARG
分别创建两个带参数的子命令:forward
和backward
分别实现两个子命令功能:
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);
}
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
变量是否被置为,若是则允许系统进入低功耗流程,否则阻止系统进入低功耗。当程序执行到 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: 34396 B 384 KB 8.75%
SRAM: 12256 B 64 KB 18.70%