STM32示例工程—请谨慎在其他中断服务函数中使用HAL_Delay()函数
STM32之HAL_Delay延时
STM32的HAL库的每个官方例程中,都会告诉用户,谨慎使用HAL_Delay()延时函数,下面来解释一下。
硬件原理
在使用STM32的HAL库时,经常会用到HAL_Delay()延时函数,该函数的参数是无符号32bit整数,除非被更高优先级打断,否则可实现毫秒级的阻塞式延时。该函数的底层是使用了CM4内核的滴答定时器,非ST厂商设计的定时器。从硬件和程序实现步骤上对该方法追根溯源。
滴答定时器是CM4内核中的一部分,内核的简化模型如下所示,可以看到,SysTick在内核中,该定时器是一个24bit的向下计数寄存器,使用CM4内核的资源。24bit可计数最大值为0x00FFFFFF,对应的十进制为16777215,当设置为1ms中断一次时,最大延时为279.62025分钟。
作为CM4系统内核,滴答定时器的设置通过几个寄存器即可完成参数设置。
寄存器名称 | 寄存器描述 |
---|---|
CTRL | 控制及状态位寄存器 |
LOAD | 重装载数值寄存器 |
VAL | 当前数值寄存器 |
CALIB | 标准数值寄存器 |
控制和状态位寄存器时对滴答定时器进行配置使用,如中断的使能、时钟源选择、是否重装载等;
重装载数值计数值是在滴答定时器初始化时便写进去数值。该数值表示当VAL寄存器减小到0时,自动将LOAD寄存器的数值赋值到VAL寄存器中,重新开始一次向下计数。可以这么理解,如果滴答定时器的时钟源频率是1us(1MHz),那么如果将LOAD设置为(1000-1),代表计数1000次,产生一次中断。即计数1ms,就产生一次中断。这是滴答定时器实现定时的本质原理。
VAL是当前数值寄存器,是一个24bit的递减计数器。
使用Keil仿真一个程序,查看内核寄存器的配置如下。
仿真中的系统内核寄存器配置
按照上面描述的寄存器说明,可以看到控制和状态寄存器是0x00010007,低3bit都为0,查看手册可知代表的意义为如下,即使能滴答定时器中断,使用FCLK系统时钟源。
自动重装载LOAD寄存器的值为0x0001387F,十进制为79999,为什么是这样的一个值呢?因为此次仿真的芯片系统时钟源为80MHz,系统配置为1ms中断一次,则需要对输入的时钟计数80000次,才能达到1ms。因此该寄存器的值设置为(80000-1)。普通单片机的内部时间也是以us来计算的,不能通过我们能够感知的秒级时间来思考芯片。1s的时间,CM4内核能够完成上万次计算,人间才一秒,CUP已千年。
当前数值寄存器显示的是当前值,仿真中可以看到,该值是不断递减的。减到0时重新装载。同时,CM4内核每1ms会进入一次滴答定时器中断服务函数。因为1ms就会执行一次滴答定时中断,在中断里面设置一个自增的全局变量,就可以记录系统走过的时间。这就是HAL延时函数的底层原理。
软件实现
HAL_Delay()函数原理
本部分参见STM32的HAL库函数,只是对HAL_Delay()函数的程序编写和调用关系进行梳理。
- 直接打开HAL_Delay()函数定义。(具体说明参见下面的注释)
/**
* @brief This function provides minimum delay (in milliseconds) based
* on variable incremented.
* @note In the default implementation , SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals where uwTick
* is incremented.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @param Delay specifies the delay time length, in milliseconds.
* @retval None
*/
__weak void HAL_Delay(uint32_t Delay)
{
// HAL_GetTick()函数功能只是返回uwTick值,uwTick是定义的全局变量,该值每次进入滴答定时中断自增1
uint32_t tickstart = HAL_GetTick();
//定义局部变量,等于需要延时的时间
uint32_t wait = Delay;
/* Add a period to guaranty minimum wait */
//HAL_MAX_DELAY宏定义展开为0xFFFFFFFFU(32bit最大值)
if (wait < HAL_MAX_DELAY)
{
//这里的uwTickFreq值为1,对传进来的Delay值加1,所以Delay = 1,实际延时约2ms
wait += (uint32_t)uwTickFreq;
}
//CPU不断的去内存中查询uwTick的值。除非此时被中断打断,否则CPU一直查询,阻塞状态
//注意:__IO uint32_t uwTick uwTick的属性为volatile,要求CPU每次都要真的去该变量所处的内存中取值,
//然后进行判断
while ((HAL_GetTick() - tickstart) < wait)
{
}
}
- 关键全局变量uwTick在滴答定时器的中断函数中处理
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
//在stm32xx_it.c文件中,可以看到官方注册的滴答定时器中断服务函数。该函数名称在启动文件中已经注册。该中断属于CM4内核中断,
//为硬中断
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick(); //在该中断中执行该函数
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
//HAL_IncTick()函数的定义,该函数执行功能为对uWTick变量执行自增操作
/**
* @brief This function is called to increment a global variable "uwTick"
* used as application time base.
* @note In the default implementation, this variable is incremented each 1ms
* in SysTick ISR.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval None
*/
__weak void HAL_IncTick(void)
{
uwTick += (uint32_t)uwTickFreq; //使用了强制转换方法
}
/*******看一下uwTickFreq的定义方法********/
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; /* 1KHz */
//uwTickFreq并不是uint32_t类型的,即uwTickFreq和uwTick并不是相同的类型,因此在执行算数运算时,需要进行强制转换操作
//HAL_TickFreqTypeDef是一种枚举类型,枚举类型可以是无符号8bit,也可以是无符号32bit,编译器一般是可以设置的
//HAL_TickFreqTypeDef枚举类型定义如下,在该定义中,第四个成员定义了一个值,该值可以等于上面三个成员中的一个
//这样定义的好处是,当需要对程序中的该变量进行修改时,不需要去查找或修改赋值语句,在定义的位置修改即可
//可以提高程序的健壮性
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;
- 继续查看滴答定时的初始化配置
/**
* @brief This function configures the source of the time base:
* The time source is configured to have 1ms time base with a dedicated
* Tick interrupt priority.
* @note This function is called automatically at the beginning of program after
* reset by HAL_Init() or at any time when clock is reconfigured by HAL_RCC_ClockConfig().
* @note In the default implementation, SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals.
* Care must be taken if HAL_Delay() is called from a peripheral ISR process,
* The SysTick interrupt must have higher priority (numerically lower)
* than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
* The function is declared as __weak to be overwritten in case of other
* implementation in user file.
* @param TickPriority Tick interrupt priority.
* @retval HAL status
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
HAL_StatusTypeDef status = HAL_OK;
/* Check uwTickFreq for MisraC 2012 (even if uwTickFreq is a enum type that doesn't take the value zero)*/
if ((uint32_t)uwTickFreq != 0U) //检查一下定时频率是否为0,由上面的枚举定义可知,取值为1/10/100
{
/*Configure the SysTick to have interrupt in 1ms time basis*/
//该函数用于配置滴答定时器的LOAD寄存器的值。返回0代表成功,可以执行下一步操作
//层层逻辑检查的目的是提高程序的健壮性,虽然不检查也不会报错,但C语言是自由又严谨的语言,
//特别是应用层代码的编写,需要遵循良好的规范
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / (uint32_t)uwTickFreq)) == 0U)
{
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
/* Return function status */
return status;
}
//看一下HAL_SYSTICK_Config配置函数的定义,该函数在stm32xx_hal_cortex.c文件中,内核的配置函数在该文件中,
//用户可以使用该函数进行配置
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
//继续查看SysTick_Config函数的定义,该函数是一个静态内联函数,用户务必禁止使用
/**
\brief System Tick Configuration
\details Initializes the System Timer and its interrupt, and starts the System Tick Timer.
Counter is in free running mode to generate periodic interrupts.
\param [in] ticks Number of ticks between two interrupts.
\return 0 Function succeeded.
\return 1 Function failed.
\note When the variable <b>__Vendor_SysTickConfig</b> is set to 1, then the
function <b>SysTick_Config</b> is not included. In this case, the file <b><i>device</i>.h</b>
must contain a vendor-specific implementation of this function.
*/
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
//寄存器的一些配置,此处为何减1???
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL;/* Load the SysTick Counter Value *///初始值设置为0
//使能滴答定时器中断,时钟源为内部系统时钟,开启内部中断
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
系统滴答定时器作为内核的心跳,在几乎所有的例程说明中,都会有如下的一段话:
@note Care must be taken when using HAL_Delay(), this function provides accurate delay (in milliseconds)
based on variable incremented in SysTick ISR. This implies that if HAL_Delay() is called from
a peripheral ISR process, then the SysTick interrupt must have higher priority (numerically lower)
than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
To change the SysTick interrupt priority you have to use HAL_NVIC_SetPriority() function.
当我们有一个中断的优先级很高,比如说在定时中断中,我们希望去控制LED灯闪烁,闪烁的这个延时调用了HAL_Delay()函数,从上面的分析中可知,HAL_Delay()原理是基于滴答定时器的中断,去判断全局变量的值。当定时器中断执行时,因为其优先级更高,因此此时此刻,滴答定时器是无法执行的,HAL_Delay()函数就会卡在这里。通过一个实例来加深对此问题的理解。
场景
使用ST电路板,创建一个工程。启动一个通用定时器,在定时器中断中使用HAL_Delay延时一段时间,然后通过串口输出数据。看一下程序的实际运行情况。
配置优先级如下:滴答定时器响应式优先级15,最低;串口优先级10,定时器优先级0,最高。
最简单的例子,设置定时器4s进入中断一次,在定时中断中,让LED等300ms毫秒频率闪烁。
void LED_Blink(void)
{
HAL_Delay(300);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
HAL_Delay(300);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
LED_Blink();
}
实际运行该函数,会发现LED不会闪烁,程序卡在了while循环这里。
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait)
{
}
}
原因很简单,滴答定时器的优先级低,在执行定时中断服务函数时,不会响应低优先级的中断,即SysTick_Handler中断捕获进入,则uwTick的值不会增加,那么HAL_Delay()函数则会一直卡住。导致出不去该中断服务函数,这种情况很可怕,会系统死机,因此,官方才会在每个工程的说明文档中,劝阻用户不要在其他中断中使用HAL_Delay()延迟函数,如果用,请提高它的优先级,谢谢。
作者:长春小征嵌入式