STM32F767 delay函数的HAL库实现与优化

 本文介绍STM32F767 delay函数的HAL库实现与优化。

开发环境

硬件环境

  • 电脑:Windows 10 Home x64
  • Apollo STM32F767开发板(ST-LINK V2仿真器)

软件环境

  • Keil Version 5.24.1 (Pack Installer:Keil.STM32F7xx_DFP.2.9.0.pack)
  • STM32CubeMX Version 4.25.0(Packages Manager:STM32CubeF7)

HAL库实现方式

 首先了解HAL库中delay的实现方式。

SYSTICK初始化

 初始化函数如下所示。

1
2
3
4
5
6
7
8
9
10
/**Configure the Systick interrupt time
*/
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
/**Configure the Systick
*/
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

 初始化函数中将SYSTICK时钟源设置为HCLK,同时将SYSTICK中断周期设置为1ms。

SYSTICK中断函数

 中断函数如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
HAL_IncTick();
}
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; /* 1KHz */
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;

 中断服务函数的最终功能是对uwTick进行加1操作,即1ms中断则uwTick增加1。

delay函数

 函数实现如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__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)
{
}
}
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}

 delay函数的实现比较清晰,首先利用tickstart存储uwTick当前值,wait值为所需延时ms数,程序将轮询检测uwTick当前值与tickstart差值是否小于wait,直到达到所需延迟数则跳出循环。

delay函数的缺陷

 HAL库给出的delay函数实现了ms级别的延时,但是有2点严重不足。

  • 延时准确性难以保证:uwTick在SYSTICK中断中增加,但是SYSTICK本身的中断可能被更高优先级的中断打断,这将导致uwTick延迟增加。当前程序设置中SYSTICK的中断优先级设置到最高,但是实际系统中中断优先级需要根据任务的重要性来设置,通常SYSTICK不会是最高优先级的中断;
  • 延时分辨率不够:delay函数只能实现ms级别的延时,更短时间的延时无法实现。

 基于以上考虑,需要对delay函数进行优化。

Delay函数的优化

SYSTICK初始化

 初始化函数如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define SYSTICK_Frequency 1000 //in Hz
static uint32 systick_load = 0; // SysTick load value, period 1000/SYSTICK_Frequency ms
static uint32 systick_per_us = 0; // SysTick delta value per us
uint32 delay_us_max = 0; //max value in Delay_US
/**Configure the Systick interrupt time
*/
systick_load = HAL_RCC_GetHCLKFreq()/SYSTICK_Frequency;
HAL_SYSTICK_Config(systick_load);
systick_per_us = systick_load * SYSTICK_Frequency / 1000 / 1000;
//systick_per_us = HAL_RCC_GetHCLKFreq() / 1000 / 1000;
delay_us_max = UINT32_MAX / systick_per_us;
/**Configure the Systick
*/
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);

 针对各个变量的说明见下图。

变量 说明
SYSTICK_Frequency SYSTICK中断频率,单位为Hz。
systick_load SYSTICK reload值,reload等于SYSTICK时钟频率/SYSTICK_Frequency,使得中断频率为SYSTICK_Frequency。
1000/SYSTICK_Frequency SYSTICK中断周期,单位为ms。
systick_load * SYSTICK_Frequency / 1000 1ms SYSTICK计数器的变化值。
systick_per_us 1us SYSTICK计数器的变化值。
delay_us_max SYS_Delay_US()函数允许的最大延时us数,由UINT32_MAX与systick_per_us决定。

最后得到的delay_us_max值为:19884107,即延时最大周期不超过19884107us,约19.8s。

delay函数

 函数如下所示。

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
void SYS_Delay_US(uint32_t delay_us)
{
uint32 systick_pre = SysTick->VAL;
uint32 systick_now = 0;
uint32 delay_total = 0;
uint32 delay_sum = 0;
if (delay_us > delay_us_max)
{
while (1);
}
delay_total = systick_per_us * delay_us;
while (delay_total > delay_sum)
{
systick_now = SysTick->VAL;
if (systick_now < systick_pre)
{
delay_sum += systick_pre - systick_now;
systick_pre = systick_now;
}
else if (systick_now > systick_pre)
{
delay_sum += systick_load - (systick_now - systick_pre);
systick_pre = systick_now;
}
}
}

 程序的逻辑比较简明,核心思想为不断获取SysTick计数值的改变量,对改变量进行累加以推算延时时间;当延时时间达到预期则跳出循环。简单介绍如下。

  • 进入程序后立即保存当前SysTick计数值,便于后续比较;
  • 检测delay_us值是否超限,超限则停止运行告警;
  • delay_total为延时需要的SysTick计数值改变量;
  • 使用systick_now获取当前SysTick计数值,与systick_pre进行对比。SysTick->VAL为递减计数器,如果未发生reload,则改变值为systick_pre - systick_now;如果发生了reload,则改变值为systick_load - (systick_now - systick_pre);
  • delay_sum为SysTick总的改变量,当大于delay_total则跳出循环。

 注意,该函数成立的前提有一个基本假设,即2次使用systick_now获取当前SysTick计数值之间最多发生一次reload,如果发生2次及以上以上程序无法分辨,则只会计数一次,则延时值不准确。事实上SysTick的中断一般为ms级别,同时STM32F7的运算速度很高,systick_now获取当前SysTick计数值的实时性很高,不会出现假设中描述的异常。

实验验证

 main函数中对SYS_Delay_US()函数进行测试,程序如下。

1
2
3
4
5
6
7
while (1)
{
mainloop++;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
SYS_Delay_US(100);
}

 使用逻辑分析仪检测PB1电平变化,与预期一致。

文章目录
  1. 1. 开发环境
    1. 1.1. 硬件环境
    2. 1.2. 软件环境
  2. 2. HAL库实现方式
    1. 2.1. SYSTICK初始化
    2. 2.2. SYSTICK中断函数
    3. 2.3. delay函数
    4. 2.4. delay函数的缺陷
  3. 3. Delay函数的优化
    1. 3.1. SYSTICK初始化
    2. 3.2. delay函数
    3. 3.3. 实验验证
|