三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

别再只会调用delay了!深入STM32 Systick定时器,从寄存器配置到实现精准us/ms延时的底层原理

别再只会调用delay了!深入STM32 Systick定时器,从寄存器配置到实现精准us/ms延时的底层原理

深入STM32 Systick定时器:从寄存器配置到精准延时实现

当你第一次接触STM32开发时,可能会发现几乎所有教程都在使用HAL库或标准库提供的延时函数。这些函数确实方便,但当你需要实现高精度定时、超长延时或在特殊场景(如Bootloader开发)下工作时,仅仅调用HAL_Delay()就显得力不从心了。本文将带你深入STM32的Systick定时器,从寄存器层面理解其工作原理,并实现精准的us/ms级延时。

1. Systick定时器基础

Systick是ARM Cortex-M内核提供的一个24位递减计数器,作为系统定时器,它独立于芯片厂商的外设定时器。与通用定时器相比,Systick有以下几个特点:

  • 集成在Cortex-M内核中:不同STM32系列芯片都包含此定时器
  • 轻量级设计:只有三个寄存器需要配置
  • 固定优先级中断:常用于实时操作系统(RTOS)的时钟节拍

Systick定时器的时钟源有两种选择:

  1. 外部时钟(HCLK/8)
  2. 内核时钟(HCLK)

时钟源的选择通过CTRL寄存器的第2位控制。理解这一点对后续精准延时的实现至关重要。

2. Systick寄存器详解

Systick定时器只有三个寄存器需要配置,理解每个寄存器每一位的作用是实现精准延时的关键。

2.1 CTRL (控制寄存器)

这个32位寄存器只有低16位有效,关键位如下:

位名称功能描述
0ENABLE计数器使能位(1=启动,0=停止)
1TICKINT中断使能位(1=计数到0时产生中断,0=不产生中断)
2CLKSOURCE时钟源选择(1=内核时钟,0=外部时钟)
16COUNTFLAG计数标志位(当计数器减到0时置1,读取后自动清零)

2.2 LOAD (重装载值寄存器)

24位寄存器,存储计数器递减的初始值。当计数器减到0时,会从这个寄存器重新加载值(如果使能了自动重装载)。

注意:实际写入的值应该是需要的计数值减1,因为计数器是从N减到0,共N+1个时钟周期。

2.3 VAL (当前值寄存器)

24位寄存器,读取时返回当前计数值,写入任何值都会将其清零(同时会清除COUNTFLAG标志)。

3. 精准延时实现原理

实现精准延时的核心是正确计算LOAD寄存器的值。计算公式如下:

LOAD值 = (延时时间 × 时钟频率) - 1

例如,要实现1ms延时:

  • 如果时钟源选择HCLK/8=9MHz:
    LOAD = (0.001s × 9,000,000Hz) - 1 = 8999
  • 如果时钟源选择HCLK=72MHz:
    LOAD = (0.001s × 72,000,000Hz) - 1 = 71999

3.1 不同时钟源下的延时实现

以下是两种时钟源配置的代码示例:

// 使用HCLK/8 (9MHz)作为时钟源 void SysTick_Init_HCLK_Div8(void) { SysTick->CTRL &= ~SysTick_CTRL_CLKSOURCE_Msk; // 选择外部时钟(HCLK/8) } // 使用HCLK (72MHz)作为时钟源 void SysTick_Init_HCLK(void) { SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // 选择内核时钟(HCLK) }

4. 实际应用中的四种实现方式

在实际开发中,不同厂商提供了不同的Systick延时实现方式。我们分析四种典型实现:

4.1 正点原子实现方式

特点:

  • 使用HCLK/8 (9MHz)时钟源
  • 直接操作寄存器
  • 延时时间有限制(最大约1864ms)

关键代码:

void delay_us(u32 nus) { u32 temp; SysTick->LOAD = nus * fac_us - 1; // fac_us = SystemCoreClock/8000000 SysTick->VAL = 0x00; SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1<<16))); SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; SysTick->VAL = 0X00; }

4.2 野火实现方式

特点:

  • 使用HCLK (72MHz)时钟源
  • 调用CMSIS提供的SysTick_Config函数
  • 延时时间不受限制(通过循环实现)

关键代码:

void SysTick_Delay_us(uint32_t us) { uint32_t i; SysTick_Config(72); // 配置1us延时 for(i=0; i<us; i++) { while(!((SysTick->CTRL) & (1<<16))); } SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; }

4.3 中断方式实现

小马飞控采用了中断方式实现延时,这种方式特别适合在RTOS中使用:

volatile uint32_t count; void SysTick_Handler(void) { if(count != 0) { count--; } } void delay_ms(uint32_t ms) { count = ms; SysTick->LOAD = 72000 - 1; // 1ms中断 SysTick->VAL = 0; SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk; while(count != 0); SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; }

5. 精度优化与常见问题

在实际应用中,我们需要考虑以下几个影响精度的因素:

  1. 中断延迟:如果使用中断方式,中断响应时间会影响精度
  2. 时钟偏差:晶振的实际频率可能与标称值有微小差异
  3. 代码执行时间:在启动和停止计数器时的指令执行时间

对于要求极高的应用,可以采用以下优化措施:

  • 校准系统时钟
  • 使用更高精度的外部晶振
  • 在关键代码段禁用中断
  • 测量实际延时并进行补偿

6. 特殊应用场景

6.1 Bootloader开发

在Bootloader中,通常不能依赖标准库,此时直接操作Systick寄存器是最可靠的方式:

void Bootloader_Delay(uint32_t ms) { SysTick->LOAD = (72000 * ms) - 1; SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); SysTick->CTRL = 0; }

6.2 超长延时实现

Systick的24位计数器在72MHz时钟下最大只能延时约233ms,要实现更长延时,可以结合软件计数器:

void Long_Delay(uint32_t sec) { uint32_t loops = sec * 1000 / 200; // 每次延时200ms for(uint32_t i=0; i<loops; i++) { SysTick_Delay_ms(200); } }

7. 性能对比与选择建议

下表对比了四种实现方式的特性:

特性正点原子野火中断方式直接寄存器
时钟源HCLK/8HCLK可配置可配置
最大延时有限制无限制无限制有限制
精度高高中高
适用场景裸机裸机RTOSBootloader
代码复杂度中低高中

选择建议:

  • 对于大多数裸机应用,野火的方式简单可靠
  • 需要超长延时时,选择带循环的实现
  • 在RTOS环境中,使用中断方式
  • Bootloader等特殊环境,直接操作寄存器最可靠
← 返回列表