1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 低功耗的相关知识,
包括HAL 库基础时钟、FreeRTOS 基础时钟、低功耗处理和 Tickless 模式等知识

3、前提知识

3.1、HAL 库基础时钟

当我们使用 STM32CubeMX 软件配置一个基本的工程时,往往需要首先在 Pinout & Configuration 页面 RCC 中配置 HSE 和 LSE ,然后在 SYS 中配置 Debug 和 Timebase Source,这些都是必不可少的配置步骤,其中 Timebase Source 可以选择默认的 SysTick ,也可以选择任何一个定时器外设

3.1.1、使用 SysTick 定时器

学习 STM32 HAL 库开发,在 SYS 中配置 Timebase Source 时,一般将时基源保持默认的 SysTick 即可,
那么这个默认的 SysTick 是如何被初始化以及使用呢?

3.1.1.1、工作原理

打开 “
STM32CubeMX教程1 工程建立
” 文章配置的 STM32 空工程,找到 main.c 文件中的 main() 主函数,SysTick 在主函数第一个被执行的函数HAL_Init() 中得到初始化,具体如下图所示

其中滴答定时器频率
uwTickFreq
参数默认为 HAL_TICK_FREQ_DEFAULT(1KHZ) ,当然也可以根据需要修改为 10HZ 和 100HZ,如下述枚举类型定义

typedef enum
{
  HAL_TICK_FREQ_10HZ         = 100U,
  HAL_TICK_FREQ_100HZ        = 10U,
  HAL_TICK_FREQ_1KHZ         = 1U,
  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ
} HAL_TickFreqTypeDef;

当初始化完毕之后,滴答定时器就会以固定频率发生中断,然后进入中断回调函数 SysTick_Handler() 中,滴答定时器中断默认就会开启

3.1.1.2、中断处理

在 STM32CubeMX 软件的 NVIC 管理页面,可以发现默认开启的滴答定时器中断 Time base: System tick timer ,在软件上该中断不可关闭,但是可以设置中断优先级,具体如下图所示

在 stm32f4xx_it.c 文件中可以找到滴答定时器的回调函数 SysTick_Handler() ,其只调用了 HAL_IncTick() 函数,该函数只做了一件事情,就是每次发生滴答定时器中断的时候,将一个名为
uwTick
的全局变量加 1 (
uwTickFreq
参数默认为1),具体如下所示

根据这个全局变量的值我们就可以做一些延时的工作,比如常用到的 HAL_Delay() 延时函数就是通过滴答定时器中断来实现的,具体如下所述

/**
  * @brief  HAL 库延时函数
  * @param  Delay:延时时间,单位为ms
  * @retval None
  */
__weak void HAL_Delay(uint32_t Delay)
{
	uint32_t tickstart = HAL_GetTick();
	uint32_t wait = Delay;
	
	/* 最少等待一个频率时间 */
	if (wait < HAL_MAX_DELAY)
	{
		wait += (uint32_t)(uwTickFreq);
	}
	/* 空循环延时等待 */
	while((HAL_GetTick() - tickstart) < wait)
	{
	}
}

另外还有 HAL_SuspendTick() 和 HAL_ResumeTick() 两个控制滴答定时器中断停止和启动的函数,具体如下所述

/**
  * @brief  挂起滴答定时器中断
  * @retval None
  */
__weak void HAL_SuspendTick(void)
{
  /* 禁用 SysTick 中断 */
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
}

/**
  * @brief  恢复挂起的滴答定时器中断
  * @retval None
  */
__weak void HAL_ResumeTick(void)
{
  /* 使能 SysTick 中断 */
  SysTick->CTRL  |= SysTick_CTRL_TICKINT_Msk;
}

3.1.2、使用其他定时器

当 SysTick 被其他软件使用时(比如本系列教程的 FreeRTOS),STM32 还可以选择任何一个定时器外设作为其 HAL 库的时基源,比如选择基础定时器 TIM6

3.1.2.1、工作原理

当在 STM32CubeMX 软件中配置 SYS 中的 Timebase Source 为 TIM6 然后生成工程之后,与 “3.1.1、使用 SysTick 定时器” 小节不同的是,其首先会在 Core 文件夹下多出一个名为 stm32f4xx_hal_timebase_tim.c 的文件,该文件中涉及了所有关于 TIM6 作为 HAL 库系统嘀嗒定时器的配置程序,使用 TIM6 作为 HAL 库系统嘀嗒定时器的初始化步骤如下图所示

上图内容其实就是将基础定时器 TIM6 初始化为一个周期为 1ms 的定时器,并且启动其周期中断回调,如果对上述代码不了解可以阅读 “
STM32CubeMX教程5 TIM 定时器概述及基本定时器
” 实验

3.1.2.2、中断处理

当选择基础定时器 TIM6 作为 SysTick 时,在STM32CubeMX软件的 NVIC 管理中 TIM6 的中断就会被强制打开并且软件内不可关闭,但是同样可以修改优先级,如下图所示

同样可以在 stm32f4xx_it.c 文件中可以找到 TIM6 的中断回调函数 TIM6_DAC_IRQHandler() ,该函数调用了定时器的统一中断处理函数 HAL_TIM_IRQHandler() ,该函数根据使用的不同定时器功能最终调用不同的中断回调函数,这里读者只需要知道其调用了定时器周期回调函数 HAL_TIM_PeriodElapsedCallback() 即可,该函数由 STM32CubeMX 软件在 main.c 文件中自动生成,具体如下所示

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM6) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}

从函数体内内容可以看出其原理与 “3.1.1.2、中断处理” 小节所叙述的一致,故此处不再赘述

3.2、FreeRTOS 基础时钟

STM32CubeMX 软件配置使用 FreeRTOS 时,默认将 SysTick 滴答定时器分配给 FreeRTOS 使用,因此如果 HAL 库的时基源也为 SysTick 时,在生成工程代码时软件就会警告用户:“当使用 RTOS 时,强烈建议使用除 Systick 之外的 HAL 时基源,可以从 SYS 下的 Pinout 选项卡更改 HAL 时基源”,具体如下图所示

因此在 STM32 需要使用 FreeRTOS 时,一般将 SysTick 分配给 FreeRTOS 使用,而 HAL 库的时基源一般选择除 SysTick 之外的定时器外设
,同时如果用户明确自己不需要使用 HAL 库的 HAL_Delay() 延时函数,则可以关闭 HAL 库的时基源

3.2.1、工作原理

FreeRTOS 的 SysTick 系统时基源是在 vPortSetupTimerInterrupt() 函数中被初始化的
,在该函数中有一个名为
configUSE_TICKLESS_IDLE
参数用于设置是否使用 Tickless 模式,这是 FreeRTOS 中提供的一种低功耗模式,将在后面小节介绍,其对 SysTick 的初始化是直接对 SysTick 的寄存器进行操作的,其调用流程如下图所示

SysTick 的寄存器可以阅读
Arm® Cortex®-M4 Processor Technical Reference Manual
手册 “4.1 System control registers” 小节,如下图所示

3.2.2、中断处理

当将 SysTick 分配给 FreeRTOS 初始化并开启对应中断之后,SysTick 的中断会被定义在 cmsis_os2.c 文件中(从 FreeRTOS_v10.3.1 之后),该函数清除了中断标志然后调用了 FreeRTOS 定义的硬件接口文件中的 xPortSysTickHandler() 函数,在 xPortSysTickHandler() 函数中增加了 RTOS 滴答定时器计数量,然后挂起 PednSV 中断,请求上下文切换,具体如下所述

根据上面的分析我们知道了一件关于 FreeRTOS 很重要的事情,也就是:
FreeRTOS 的任务调度发起是在系统滴答定时器中断中发起的,然后真正进行上下文切换处理是在 PendSV 中断中执行的

3.3、低功耗处理

3.3.1、睡眠、停止和待机模式

在 “
STM32CubeMX教程25 PWR 电源管理 - 睡眠、停止和待机模式
” 文章中曾经介绍了关于 STM32 电源管理的睡眠、停止和待机三种低功耗模式,在一个由 FreeRTOS 管理的系统中,一般只使用其中的睡眠模式即可,因为停止和待机模式的唤醒条件相对较为苛刻,感兴趣的读者请自行阅读上述文章

3.3.2、低功耗思路

在一个 FreeRTOS 管理的多任务系统中,当所有任务处理完毕进入阻塞状态等待下次处理时机时,空闲任务会一直执行,如果同时使能了
configUSE_IDLE_HOOK
参数,则每当处理器将要进入空闲任务时,就会先进入空闲任务钩子函数中

因此我们可以在空闲任务钩子函数中设置处理器进入睡眠模式,但是同时也会存在一个问题,就是每次滴答定时器中断都会将处理器唤醒,这样其运行时序图应该如下图所示

3.3.3、Tickless 模式

上述低功耗思路中存在的一个问题:
“每次滴答定时器中断都会将处理器唤醒”
,FreeRTOS 提供了一个 Tickless 模式,当处理器空闲时会一直处于睡眠状态,然后在任务即将退出阻塞状态之前处理器提前被唤醒,理想的低功耗模式应该如下图所示

要使用 Tickless 模式只需要启用
configUSE_TICKLESS_IDLE
参数即可

,该参数可以通过 STM32CubeMX 软件设置,有三个可以配置的选项,选择 Built in functionality enabled 对应的参数值为 1 ,表示使用 FreeRTOS 内建的函数实现 Tickless 低功耗功能,选择 User defined functionality enabled 则对应的参数值为 2 ,表示使用用户自定义的函数实现 Tickless 低功耗功能,一般选择使用 FreeRTOS 现成的函数来实现 Tickless 低功耗功能,如下图所示

3.3.3.1、工作原理

当启用 Tickless 之后,系统满足以下两点时就会自动进入睡眠模式

  1. 空闲任务正在运行
  2. 可运行低功耗的时间大于参数 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 设定值时(默认为2)

用户需要注意的是进入睡眠的时间有最大值 xMaximumPossibleSuppressedTicks 限制
,该变量在设置滴答定时器中断 vPortSetupTimerInterrupt() 函数中被计算,当 MCU 频率为168MHz,FreeRTOS 频率为 1000Hz 时,该值为 99 ,也即单次最长进入睡眠时间的最大值为 99 个节拍,具体如下所示

3.3.3.2、vPortSuppressTicksAndSleep() 函数详解

vPortSuppressTicksAndSleep() 是 Tickless 模式实现的具体函数,该函数会在启用 Tickless 模式后在空闲任务中被调用,具体可以参考 “
freeRTOS 低功耗模式 和 空闲任务
” 文章

4、实验一:Tickless 模式的使用

4.1、实验目标

  1. 创建任务 Task_Main,在任务中实现 GREEN_LED 和 RED_LED 的闪烁程序
  2. 启用/关闭 Tickless 模式,对比两种不同情况下开发板的工作电流

4.2、CubeMX相关配置

首先读者应按照 "
FreeRTOS教程1 基础知识
" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 GREEN_LED 和 RED_LED 两个 LED 灯作为显示,具体配置步骤请阅读“
STM32CubeMX教程2 GPIO输出 - 点亮LED灯
”,注意虽开发板不同但配置原理一致,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,如下所示

然后在 Configuration 中单击 Config parameters 选项卡,在 Kernel settings 中找到 USE_TICKLESS_IDLE 参数,将其设置为 Disabled 或者 Built in functionality enabled,进行对比实验

最后配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

首先实现任务 Task_Main 使其每隔 500ms 改变一次 GREEN_LED 和 RED_LED 的状态,具体如下所述

void AppTask_Main(void *argument)
{
	/* USER CODE BEGIN AppTask_Main */
	/* Infinite loop */
	for(;;)
	{
		HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
		HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
		vTaskDelay(pdMS_TO_TICKS(500));
	}
	/* USER CODE END AppTask_Main */
}

然后再进入睡眠模式之前关闭系统滴答定时器,在退出睡眠模式之后开启系统滴答定时器,具体如下所述

__weak void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
	HAL_SuspendTick();
}

__weak void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
	HAL_ResumeTick();
}

4.4、烧录验证

在开启 Tickless 和关闭 Tickless 两种模式下读者可以自行测试开发板工作电流,对比开启和关闭两种模式下工作电流的变化

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

标签: none

添加新评论