OpenHarmony Liteos-m 内核低功耗调试
内核移植
最近参考一个大神的博客在keil环境下移植了OpenHarmony开源鸿蒙的liteos-m内核到STM32L4 MCU。参考链接如下,很详细,大家按照文章说明很容易移植成功。
基于STM32F103RCT6移植LiteOS-M-V5.0.2-Release - 追寻编程的真谛 - 博客园
需要注意以下几点:
1.文章移植的是M3内核,用了发布版本5.0.2。我在移植的时候用的是master分支(最新提交哈希值为4972e69,如下图),MCU为M4内核。当前分支刚好提交了关于m4内核的keil支持文件。需要注意的是,移植完成后,会提示OsSignalTaskContextRestore没有定义。这个函数位于components/signal/los_signal.c中。但其实由于宏LOSCFG_KERNEL_SIGNAL没有启用,这就是个空函数。加入los_signal.c后,又提示文件找不到。我的做法是直接将los_signal.c中的#include \"los_signal.h\"注释掉即可。(其实我的做法还是繁琐了,直接自己定义一个OsSignalTaskContextRestore的空函数最简单)
2.文章中移植了一个简易的日志组件,可根据自己需求决定是否移植。
3.文章中移植了cmsis2标准接口,同样根据自己需求决定是否移植。
到这里,就可以开始移植功耗管理组件了。
功耗管理组件移植
提前声明一下,以下内容都是我个人理解,没有官方认证,可能存在错误,大家需要辩证看待理解。
功耗管理组件位于components/power。只有los_pm.c和los_pm.h两个文件,加入工程即可。
需要关注的宏定义
要启用功耗管理功能,需要使能对应的宏,一般定义在target_config.h中。
#define LOSCFG_KERNEL_PM 1
除此之外,还有两个宏跟功耗管理模块息息相关。
LOSCFG_KERNEL_PM_IDLE
这个宏主要用在功耗管理的入口函数里:
STATIC VOID OsPmNormalSleep(VOID){#if (LOSCFG_KERNEL_PM_IDLE == 1) (VOID)LOS_PmSuspend(0);#else UINT32 intSave; LosPmCB *pm = &g_pmCB; intSave = LOS_IntLock(); OsPmCpuSuspend(pm); OsPmCpuResume(pm); LOS_IntRestore(intSave);#endif}
我对这个宏的理解:源码中注释的解释是:在空闲任务中使能内核功耗管理组件。但是其实无论这个宏使能与否,OsPmNormalSleep这个接口都是在空闲任务中调用。不一样的是,这个宏使能后,将会调用完整的功耗管理机制(后面会说)。如果禁用,则是简单在当前空闲任务调度周期内调用OsPmCpuSuspend进行休眠,不能跨节拍休眠,也就是说,即使接下来10个节拍调度的都是空闲任务,也不能一口气休眠10个节拍,而是每个节拍都会唤醒一次然后再休眠。一般来说,我们使能这个宏。如果禁用,那么看到这里就可以结束了,因为后面的机制都不会调用。
另一个需要关注的宏:
LOSCFG_BASE_CORE_TICK_WTIMER
这个宏在源码中的解释是:系统定时器是一个64或128位定时器。其实就是定义系统定时器是否是一个宽定时器。我本次使用的STM32L4显然不是。大家了解完后面休眠的补偿机制后,就会意识到,宽定时器真是个好东西,不用考虑溢出的问题,简单易用,代码都变简单了。可惜,我用的低端MCU没有这个特性。
低功耗模式的核心机制
要实现最低的功耗,我们需要一个低功耗定时器的支持。这个定时器必须要能在MCU休眠状态下工作,一般用RTC或者STM32L的lpTIM都可以。
裸机状态下,业务代码空闲时,可以调用库函数提供的低功耗接口,直接进入低功耗状态,然后等待事件(中断)唤醒即可。
有操作系统时,原理不变,只是更复杂一点。我们需要在各个任务都空闲的时候才能进入休眠模式。那么如何判断各个任务都空闲了呢?一般来说,操作系统都会提供一个接口,用于获取下次有意义的任务调度时间。我们可以利用这个时间进行休眠。但是休眠后,会导致一个问题,系统节拍无法计数,从而影响系统正常工作。此时,就需要上面提到的低功耗定时器了,休眠的同时,开启低功耗定时器。被唤醒后(定时器唤醒或外部中断唤醒)读取定时器计数值,对系统节拍进行补偿。这样系统才能继续正常工作。
源码的实现过程
源代码中将功耗模式分为了4种:
typedef enum { LOS_SYS_NORMAL_SLEEP = 0, //正常休眠模式 LOS_SYS_LIGHT_SLEEP, //轻度休眠模式 LOS_SYS_DEEP_SLEEP, //深度休眠模式 LOS_SYS_SHUTDOWN, //关机模式} LOS_SysSleepEnum;
其实这4种模式并不是都要实现,我们只实现自己需要的模式即可。而且这几种模式跟MCU支持的休眠模式也没有严格的对应关系。要怎么用,全凭我们如何去实现。
除了这4种模式,比较重要的还有3个结构体:
typedef struct { UINT32 (*suspend)(UINT32 mode); VOID (*resume)(UINT32 mode); } LosPmDevice;typedef struct { UINT32 freq; VOID (*timerStart)(UINT64); VOID (*timerStop)(VOID); UINT64 (*timerCycleGet)(VOID); VOID (*tickLock)(VOID); VOID (*tickUnlock)(VOID);} LosPmTickTimer;typedef struct { UINT32 (*early)(UINT32 mode); VOID (*late)(UINT32 mode); VOID (*suspendCheck)(UINT32 mode); UINT32 (*normalSuspend)(VOID); VOID (*normalResume)(VOID); UINT32 (*lightSuspend)(VOID); VOID (*lightResume)(VOID); UINT32 (*deepSuspend)(VOID); VOID (*deepResume)(VOID); UINT32 (*shutdownSuspend)(VOID); VOID (*shutdownResume)(VOID);} LosPmSysctrl;
这3个结构体定义了很多函数指针,用于完成休眠机制。但是大家不要看到这么多函数指针就觉得很复杂,其实里面有些指针是不需要实现的。这里虽然看着复杂,但是实现的就是上面核心机制章节里说的那一点事情。不过这里分了3个层次(设备,定时器,系统)去实现,结构更清晰,也更合理。
源码的功耗管理核心入口是OsPmNormalSleep函数。具体的调用关系,我做了一张图,如下:
从这幅图中,大家就可以看出,源码对低功耗的实现过程,就是依照一定的顺序,调用了上面3个结构体中定义的各种指针函数。如果想要简便的话,那修面前和休眠后可以各只实现一个函数,完成所有的工作。想要结构清晰的话,就把大部分指针函数都实现。
移植过程
其实移植过程,主要就是实现那3个结构体的定义。
在源码中,也有测试代码可供参考,位于testsuites / sample / kernel / power.
首先在target_config.h中宏定义如下
#define LOSCFG_KERNEL_PM 1#define LOSCFG_KERNEL_PM_IDLE 1#define LOSCFG_BASE_CORE_TICK_WTIMER 0
新建一个.C文件,实现需要定义的函数
#include \"los_pm.h\"#include \"Platform_lptim.h\"/*------------------------ 设备操作接口 ------------------------*//** * @brief STM32L4设备挂起函数 * @param mode LiteOS-M低功耗模式 * @return LOS_OK * @details 关闭外设时钟,保留必要唤醒源(如RTC) */static UINT32 STM32L4_DeviceSuspend(UINT32 mode){ switch (mode) { case LOS_SYS_NORMAL_SLEEP: break; case LOS_SYS_LIGHT_SLEEP: // 进入Stop模式前关闭外设。 //__HAL_RCC_GPIOA_CLK_DISABLE(); //__HAL_RCC_GPIOB_CLK_DISABLE();//__HAL_RCC_GPIOC_CLK_DISABLE(); //__HAL_RCC_GPIOD_CLK_DISABLE(); //__HAL_RCC_GPIOE_CLK_DISABLE(); //__HAL_RCC_GPIOF_CLK_DISABLE(); //__HAL_RCC_GPIOG_CLK_DISABLE(); //__HAL_RCC_GPIOH_CLK_DISABLE(); //__HAL_RCC_GPIOI_CLK_DISABLE(); break; case LOS_SYS_DEEP_SLEEP: break; } return LOS_OK;}/** * @brief STM32L4设备恢复函数 * @param mode LiteOS-M低功耗模式 * @details 重新初始化外设时钟 */static VOID STM32L4_DeviceResume(UINT32 mode){ switch (mode) { case LOS_SYS_NORMAL_SLEEP: break; case LOS_SYS_LIGHT_SLEEP://__HAL_RCC_GPIOA_CLK_ENABLE();//__HAL_RCC_GPIOB_CLK_ENABLE();//__HAL_RCC_GPIOD_CLK_ENABLE();//__HAL_RCC_GPIOE_CLK_ENABLE();//__HAL_RCC_GPIOF_CLK_ENABLE();//__HAL_RCC_GPIOG_CLK_ENABLE();//__HAL_RCC_GPIOH_CLK_ENABLE();//__HAL_RCC_GPIOI_CLK_ENABLE(); break; case LOS_SYS_DEEP_SLEEP: break; }}/* 设备操作结构体 */static LosPmDevice stm32l4_pm_device = { .suspend = STM32L4_DeviceSuspend, .resume = STM32L4_DeviceResume};/*------------------------ 低功耗定时器接口 ------------------------*//** * @brief 锁定系统滴答定时器(SysTick) */static VOID SysTick_Lock(VOID){SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;}/** * @brief 解锁系统滴答定时器 */static VOID SysTick_Unlock(VOID){SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;}/* 低功耗定时器结构体 */static LosPmTickTimer stm32l4_pm_timer = { .freq = LPTIM_FREQ, .timerStart = LPTIM_statrCounter, .timerStop = LPTIM_stopCounter, .timerCycleGet = LPTIM_CycleGet, .tickLock = SysTick_Lock, .tickUnlock = SysTick_Unlock};/*------------------------ 系统控制接口 ------------------------*//** * @brief 系统低功耗前调用函数 * @param mode LiteOS-M低功耗模式 * @details 低功耗前准备工作 */static UINT32 SystemPmEarly(UINT32 mode){ return LOS_OK;}/** * @brief 系统低功耗退出后调用函数 * @param mode LiteOS-M低功耗模式 * @details 低功耗退出后工作 */static VOID SystemPmLate(UINT32 mode){}/** * @brief 轻度睡眠模式挂起函数 * @return LOS_OK * @details 进入轻度休眠模式 */static UINT32 STM32L4_LightSuspend(VOID){HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI); return LOS_OK;}/** * @brief 轻度睡眠模式恢复函数 * @return LOS_OK * @details 退出轻度休眠模式 */static VOID STM32L4_LightResume(VOID){}/* 系统控制结构体 */static LosPmSysctrl stm32l4_pm_sysctrl = { .early = SystemPmEarly, .late = SystemPmLate,.lightSuspend = STM32L4_LightSuspend,.lightResume = STM32L4_LightResume,};/*------------------------ 初始化函数 ------------------------*//** * @brief 注册STM32L4的PM组件 */VOID PmInit(VOID){ // 注册设备、定时器、系统控制节点 LOS_PmRegister(LOS_PM_TYPE_DEVICE, &stm32l4_pm_device); LOS_PmRegister(LOS_PM_TYPE_TICK_TIMER, &stm32l4_pm_timer); LOS_PmRegister(LOS_PM_TYPE_SYSCTRL, &stm32l4_pm_sysctrl); // 设置默认低功耗模式为深睡眠 LOS_PmModeSet(LOS_SYS_LIGHT_SLEEP);}
以上代码仅供参考,休眠模式仅实现了LOS_SYS_LIGHT_SLEEP(轻度休眠)。因为是测试,大部分函数都是空的。实际使用时,可在系统层面SystemPmEarly函数中完成一些系统层面休眠工作,如关闭电源,关闭子系统等。 设备层面STM32L4_DeviceSuspend函数中完成一些外设时钟关闭,中断配置,GPIO配置等操作。LPTIM_statrCounter等低功耗定时器相关函数,需要大家自己实现,具体是使用lptim还是rtc或者其他定时器,完全由大家自己决定。需要注意的是,LPTIM_statrCounter的入参是定时器的计数周期,有可能大于你的定时器最大周期,所以一定要处理一下,不要溢出,否则会有意想不到的错误发生(这里就能体现出宽定时器的优势了)。还有,PmInit函数最好在系统内核初始化之后,开始运行之前调用,也就是在LOS_KernelInit()和LOS_Start()之间调用。因为LOS_KernelInit()里面初始化一组默认值,PmInit放在前面的话,我们初始化的内容会被覆盖调。
实战检验
光说不练假把式,移植好了咱们检验一下成果。
测试代码来一段
VOID task1(VOID) {while (1) {HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);LOS_TaskDelay(3000);}}
测试方案依旧传统,点灯。亮3秒,灭3秒。接入功耗测试仪,供电3.3V,电流消耗如下:
大体来说,效果尚可。休眠时功耗约为3.7uA(仅MCU,数值略高于数据手册),1.4mA(LED亮,功耗大部分为LED)。唤醒的瞬间,可以发现功耗有个突变,主要是高频时钟启动和MCU自身的消耗。如果不用HSE和PLL,仅用MSI时钟,这个突变会小不少(实际数据后续有空我会在测试一下)。
避坑指南
1.测试低功耗时,下载完程序最好重启一下(我是掉电复位),否则总有500uA左右的功耗降不下去(我怀疑和调试口有关)。
2.如果使用STM的HAL库,低功耗的时候最好关闭HAL库初始化的那个定时器,否则可能影响系统无法休眠。理论上高频时钟关闭时,HAL的定时器是不运行的,但是实测确实会影响休眠。我分析是因为休眠前HAL的定时器已经产生中断,系统已经置位,但是没来的及处理。中断标志一旦挂起,系统就无法休眠。因为执行WFI指令时,只要有挂起的中断,就不会休眠。使用keil开发的朋友,调试的时候,可以打开NVIC调试窗口查看已经产生的中断,如下图所示。哪些中断在触发,导致不能休眠,这里看的一清二楚。
3.注意你的低功耗定时器的溢出问题。
4.注意STM32L4(其他型号也是一样)从低功耗唤醒后,默认使用内部MSI时钟。如果你使用的时HSE时钟,记得要重新初始化时钟。