• 基于STM32L431的Liteos低功耗Runstop模式的实现


    Liteos源码
    以下涉及到的源码为Liteos的develop支线版本。

    Liteos的低功耗模式分为Tickless模式和Runstop模式。Tickless模式是通过改变Systick的重装载值(reload)来延长每个Tick的时间,最后进入sleep模式,具体的源码分析可以参考基于STM32L431的Liteos低功耗Tickless模式的使用。Tickless模式下系统的功耗是mA级的,这对于使用电池供电的设备来说,功耗是达不到要求的。而Runstop模式在develop版本的源码中相关的代码并没有几行,也就是说是不支持的。在master版本中是支持的,但是需要用LiteOS_Studio这个IDE来开发,使用LiteOS_Studio开发由于是交叉编译,生成的BIN文件比较大,这对于STM32L431来说还是有点吃不消的。既然Liteos有Tickless模式,那就仿照Tickless模式的源码来实现Runstop模式。

    首先要解决的就是系统时基的问题,在运行模式下Liteos的时基是通过Systick来实现的,但是在stop模式下就用不了。但是stm32的L系列单片机会有一个低功耗定时器LPTIM,这个定时器是一个16位向上计数器,时钟源也可以有很多种,而且可以像RTC一样把MCU从stop模式中唤醒。
    在这里插入图片描述
    那么在stop模式下就可以通过LSI或者LSE给lptim提供时钟,以下使用的时钟为LSI。STM32L431的LSI是32KHz的,通过32分频后可以得到1ms的tick,之所以这么设置是为了计算方便。在此分频下可以通过lptim的超时中断来记录系统过去了多长时间,在唤醒之后需要把过去的这段时间补偿给系统,这样系统的任务延时或者软件定时器才不会乱。lptim的超时中断可以通过HAL_LPTIM_TimeOut_Start_IT函数来实现,函数有3个参数,第一个参数为定时器句柄,第二个参数为重装载值,第三个参数为超时时间,超时时间会被写入比较寄存器。重装载值要大于超时时间,超时时间最大值为0xFFFF,也就是说32分频下最大可以设置(0xFFFF-1)ms的超时时间。当然如果不想用这个函数来实现,可以用HAL_LPTIM_Counter_Start_IT函数来实现,区别就是没有超时时间参数,在Reload的时候会产生中断。

    LPTIM初始化:

    LPTIM_HandleTypeDef hlptim1;
    void MX_LPTIM1_Init(void)
    {
      hlptim1.Instance = LPTIM1;
      hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC;
      hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV32;
      hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
      hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH;
      hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE;
      hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
      hlptim1.Init.Input1Source = LPTIM_INPUT1SOURCE_GPIO;
      hlptim1.Init.Input2Source = LPTIM_INPUT2SOURCE_GPIO;
      if (HAL_LPTIM_Init(&hlptim1) != HAL_OK)
      {
        Error_Handler();
      }
    }
    
    void HAL_LPTIM_MspInit(LPTIM_HandleTypeDef* lptimHandle)
    {
    
      RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
      if(lptimHandle->Instance==LPTIM1)
      {
        PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_LPTIM1;
        PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSI;
        if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
        {
          Error_Handler();
        }
        __HAL_RCC_LPTIM1_CLK_ENABLE();
    //    HAL_NVIC_SetPriority(LPTIM1_IRQn, 0, 0);
    //    HAL_NVIC_EnableIRQ(LPTIM1_IRQn);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    由于我使用的是接管中断方式,所以要把HAL_NVIC_SetPriority和HAL_NVIC_EnableIRQ函数替换为LOS_HwiCreate。中断服务函数为LPTIM_IRQHandler,由于使用lptim的目的只是为了唤醒MCU并在需要的时候获取计数值,因此不需要在中断服务函数中做特殊处理,只调用HAL库的中断处理函数清除中断标记即可。

    LOS_HwiCreate(LPTIM1_IRQn,0,(HWI_MODE_T)0,(HWI_PROC_FUNC)LPTIM_IRQHandler,(HWI_ARG_T)0);
    
    void LPTIM_IRQHandler(void)
    {
    	HAL_LPTIM_IRQHandler(&hlptim1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    获取计数值可以通过HAL库的HAL_LPTIM_ReadCounter函数获得:

    uint32_t HAL_LPTIM_ReadCounter(const LPTIM_HandleTypeDef *hlptim)
    
    • 1

    解决了时基来源的问题接下来要做的就是仿照Tickless模式来实现Runstop模式。主要实现的内容是:
    ①在空闲任务时判断要不要进入stop模式。
    ②如果需要进入stop模式,那么需要知道什么时候唤醒,并把这个时间设置为lptim的超时时间。
    ③同时在进入stop模式之前需要关闭systick,任务调度上锁,同时做一些低功耗前的准备,比如说关闭外设,设置IO口等。
    ④低功耗唤醒之后首先需要做的是初始化系统时钟,初始化需要用到的外设。
    ⑤唤醒之后与OS相关的要开启systick,任务调度解锁,系统的tick补偿。

    首先在进入stop模式之前,可以像Tickless模式下,获取到最近一个任务切换或者软件定时器到达的时间,并以此设置为lptim的超时时间,也就是MCU的唤醒时间。

    static inline UINT32 osStopTicksGet(VOID)//获取要stop的tick数
    {
        UINT32 uwTskSortLinkTicks = 0;
        UINT32 uwSwtmrSortLinkTicks = 0;
        UINT32 uwStopTicks = 0;
    
        /** Context guarantees that the interrupt has been closed */
        uwTskSortLinkTicks  = osTaskNextSwitchTimeGet();
        uwSwtmrSortLinkTicks = osSwTmrGetNextTimeout();
    
        uwStopTicks = (uwTskSortLinkTicks < uwSwtmrSortLinkTicks) ? uwTskSortLinkTicks : uwSwtmrSortLinkTicks;
        return uwStopTicks;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    systick的开启关闭,或者重载值设置可以直接用Liteos提供的函数:

    LITE_OS_SEC_TEXT_MINOR static inline VOID LOS_SysTickStart(VOID)
    LITE_OS_SEC_TEXT_MINOR static inline VOID LOS_SysTickStop(VOID)
    LITE_OS_SEC_TEXT_MINOR VOID LOS_SysTickReload(UINT32 uwCyclesPerTick)
    
    • 1
    • 2
    • 3

    任务调度上锁和解锁可以用:

    LITE_OS_SEC_TEXT_MINOR VOID LOS_TaskLock(VOID)
    LITE_OS_SEC_TEXT_MINOR VOID LOS_TaskUnlock(VOID)
    
    • 1
    • 2

    剩下最关键的就是tick补偿了。在Tickless模式下是通过osTickHandlerLoop函数进行补偿的,这个函数入参为要补偿的tick个数,然后通过for循环来执行和systick中断服务函数中同样的内容。但是对于stop模式来说,可能stop的时间非常久,那么要补偿的tick个数就特别多,显然不能再用这个函数来补偿。进入stop模式的时间是通过最近一个任务切换或者软件定时器到达的时间来确定的,那么缺失的tick当然对任务切换和软件定时器来说是必须的。

    通过分析源码,软件定时器可以通过osSwTmrAdjust函数来一次性补偿多个tick。因为Liteos的每个软件定时器之间不是相互独立的,而是互相之间有关联,按照时间到来的先后顺序体现在链表中。比如先后两个软件定时器,一个是5s,一个是7s,那么在软件定时器的链表中体现的就是一个是5s,一个是2s(5+2 = 7)。因此只用改变链表的头部节点值就可以。


    以下关于任务tick补偿部分理解有误,最新的可以查看:(二)基于STM32L431的Liteos低功耗Runstop模式的实现优化(退出stop2模式后任务相关Tick补偿优化)


    而任务延时如何一次补偿多个tick没有体现在源码中,那么就需要自己来构建。首先在运行的时候,每次systick中断都会执行任务扫描osTaskScan函数,函数中关键部分如下(其余部分可以自己查阅源码):

        for (pstTaskCB = LOS_DL_LIST_ENTRY((pstListObject)->pstNext, LOS_TASK_CB, stTimerList);&pstTaskCB->stTimerList != (pstListObject);) /*lint !e413*/
        {
            usTempStatus = pstTaskCB->usTaskStatus;
            if (UWROLLNUM(pstTaskCB->uwIdxRollNum) > 0)
            {
                UWROLLNUMDEC(pstTaskCB->uwIdxRollNum);
                break;
            }
    
            LOS_ListDelete(&pstTaskCB->stTimerList);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到在遍历任务的时候,当uwIdxRollNum的值大于0的时候,就减1后直接退出(也就是不为0则一个tick减1),扫描下一个任务,当uwIdxRollNum值为0的时候会把任务从延时队列中删除,然后才执行后续内容。同样的在获取最近的任务切换时间的时候,执行的函数是osTaskNextSwitchTimeGet,判断的也是uwIdxRollNum的值:

    LITE_OS_SEC_TEXT_MINOR UINT32 osTaskNextSwitchTimeGet(VOID)
    {
        LOS_TASK_CB *pstTaskCB;
        UINT32 uwTaskSortLinkTick = 0;
        LOS_DL_LIST *pstListObject;
        UINT32 uwTempTicks = 0;
        UINT32 uwIndex =0;
    
        for (uwIndex = 0; uwIndex < OS_TSK_SORTLINK_LEN; uwIndex++)
        {
            pstListObject = g_stTskSortLink.pstSortLink + (g_stTskSortLink.usCursor + uwIndex)%OS_TSK_SORTLINK_LEN;
            if (pstListObject->pstNext != pstListObject)
            {
                pstTaskCB = LOS_DL_LIST_ENTRY((pstListObject)->pstNext, LOS_TASK_CB, stTimerList);
                uwTempTicks = (uwIndex == 0) ? OS_TSK_SORTLINK_LEN : uwIndex;
                uwTempTicks += (UINT32)(UWROLLNUM(pstTaskCB->uwIdxRollNum) * OS_TSK_SORTLINK_LEN);
                if(uwTaskSortLinkTick == 0 || uwTaskSortLinkTick > uwTempTicks)
                {
                   uwTaskSortLinkTick = uwTempTicks;
                }
            }
        }
    
        return uwTaskSortLinkTick;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    那么既然后办法获取到这个值,那么反过来就可以把这个值减去相应的tick数,以下是我自己仿照osTaskNextSwitchTimeGet函数来实现的任务延时tick补偿:

    LITE_OS_SEC_TEXT_MINOR VOID osIdxRollNumDec(UINT32 Tick)
    {
        LOS_TASK_CB *pstTaskCB;
        LOS_DL_LIST *pstListObject;
        UINT32 uwTempTicks = 0;
        UINT32 uwIndex =0;
    
        for (uwIndex = 0; uwIndex < OS_TSK_SORTLINK_LEN; uwIndex++)
        {
    		UINT32 uwTaskTick = Tick;
            pstListObject = g_stTskSortLink.pstSortLink + (g_stTskSortLink.usCursor + uwIndex)%OS_TSK_SORTLINK_LEN;
            if (pstListObject->pstNext != pstListObject)
            {
                pstTaskCB = LOS_DL_LIST_ENTRY((pstListObject)->pstNext, LOS_TASK_CB, stTimerList);
                uwTempTicks = (uwIndex == 0) ? OS_TSK_SORTLINK_LEN : uwIndex;
    			uwTaskTick -= uwTempTicks;
    			uwTaskTick /= OS_TSK_SORTLINK_LEN;
    		
    			if(uwTaskTick > UWROLLNUM(pstTaskCB->uwIdxRollNum))
    			{
    				uwTaskTick = UWROLLNUM(pstTaskCB->uwIdxRollNum);
    			}
    			UWROLLNUMDECMULT(pstTaskCB->uwIdxRollNum,uwTaskTick);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    其中UWROLLNUMDECMULT这个宏需要自己写,Liteos提供的是UWROLLNUMDEC:

    #define UWROLLNUMDEC(UWNUM)  \
                UWNUM = (UWNUM - 1)
    #define UWROLLNUMDECMULT(UWNUM,X)  \
                UWNUM = (UWNUM - X)
    
    • 1
    • 2
    • 3
    • 4

    以上关于任务tick补偿部分理解有误,最新的可以查看:(二)基于STM32L431的Liteos低功耗Runstop模式的实现优化(退出stop2模式后任务相关Tick补偿优化)


    除以上部分,还需要更改的值是g_ullTickCount,这个是系统的tick计数值。上面提到的这些实现之后要实现的就是进入stop之前和退出stop之后要做的一些处理。

    VOID PreStopProcessing(VOID)
    {
    	LOS_TaskLock();
    	HAL_UART_DeInit(&huart1);
    	HAL_LPTIM_TimeOut_Start_IT(&hlptim1, 0xFFFF, g_LptimCount);
    }
    	
    VOID PostStopProcessing(VOID)
    {
    	UINTPTR uvIntSave = 0;
    	UINT32 ElapseCount = 0;
    	HAL_GPIO_WritePin(GPIOB, LED_RED_Pin, GPIO_PIN_SET);
    	uvIntSave = LOS_IntLock();
    	SystemClock_Config();//系统时钟初始化
    	MX_USART1_UART_Init();
    	PRINT_INFO("Wakeup\r\n");
    	ElapseCount = HAL_LPTIM_ReadCounter(&hlptim1);//获取lptim的计数值(过去了多少时间(ms))
    	HAL_LPTIM_TimeOut_Stop_IT(&hlptim1);
    	osWakeupUpdateKernelTick(ElapseCount);//tick补偿
    	LOS_SysTickReload(g_SystickCycles);//进入stop模式之前的计数值
    	LOS_TaskUnlock();
    	LOS_IntRestore(uvIntSave);
    	HAL_GPIO_WritePin(GPIOB, LED_RED_Pin, GPIO_PIN_RESET);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    PreStopProcessing和PostStopProcessing函数就像Freertos中configPRE_STOP_PROCESSING和
    configPOST_STOP_PROCESSING函数一样。在PreStopProcessing函数中要关闭外设,正确的设置IO口,以达到更低功耗,上面没有体现出来IO口配置是因为开始的时候在其他地方设置了。

    进入stop模式直接调用HAL库函数:

    HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
    
    • 1

    如果使用Freertos的话,可以参考一下固件库en.stm32cubel4_v1-17-0\STM32Cube_FW_L4_V1.17.0\Projects\B-L475E-IOT01A\Applications\FreeRTOS\路径下的FreeRTOS_LowPower_LPTIM工程,移植到自己的工程中,那么就容易的多了。

    PostStopProcessing函数中开关灯的动作是不需要的,我这边是为了测试这个过程中的时间,实际测试下来时间是1.91ms。
    在这里插入图片描述
    因此通过osStopTicksGet函数获取到要stop的tick数需要进行判断,如果太小的话(比如小于2),那就不能进入stop模式。而我通过上面的方法进入和退出stop的话,osStopTicksGet函数获取到要stop的tick数至少需要大于50,进出stop模式才不会有问题,具体原因还未找到。

    测试:
    创建两个任务:

    static void test_task1(void)
    {
    	while(1)
    	{
    		LOS_TaskDelay(LOS_MS2Tick(11000));//11s
    		PRINT_INFO("Task1 running\r\n");
    	}
    }
    static void test_task2(void)
    {
    	while(1)
    	{
    		LOS_TaskDelay(LOS_MS2Tick(16000));//16s
    		PRINT_INFO("Task1 running\r\n");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    创建一个软件定时器,定时周期7s:

    UINT16	test_swtmr_handle;
    static void test_swtmr_callback(UINT32 arg)
    {
    	PRINT_INFO("test_swtmr_callback\r\n");
    }
    
    uwRet = LOS_SwtmrCreate(LOS_MS2Tick(7000),LOS_SWTMR_MODE_PERIOD,(SWTMR_PROC_FUNC)test_swtmr_callback,&test_swtmr_handle,0);
    if(uwRet != LOS_OK)
    {
    	PRINT_ERR("Swtmr Error Code:0x%X\n",uwRet);
    	return uwRet;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行效果:
    在这里插入图片描述
    数据手册中stop模式下功耗:
    请添加图片描述
    实际运行stop模式下功耗:
    请添加图片描述

  • 相关阅读:
    SpringCloudAlibaba-微服务-注册中心之Nacos安装启动与集群配置
    ubuntu设置开机自启服务脚本
    JavaScript 夯实基础第一课:初学者必须要了解的 JavaScript 发展历程及语言规范特性
    听GPT 讲Istio源代码--pkg(5)
    北理工嵩天Python语言程序设计笔记(6 函数和代码复用)
    Android上传私有插件到私有MAVEN-PUBLISH
    SpringBoot入门教程:浅聊POJO简单对象(VO、DTO、Entity)
    【Java技术专题】「原理分析系列」分析反射底层原理及基础开发实战
    运动耳机哪种佩戴方式好?佩戴稳固舒适的运动耳机
    复制东方甄选?顺丰再战直播电商
  • 原文地址:https://blog.csdn.net/sinat_42731525/article/details/127414311