当前位置:首页 > 技术学院 > 技术前线
[导读]在STM32嵌入式开发中,精确延时是非常基础但又极其关键的功能。无论是驱动单总线传感器(比如DS18B20)、控制LCD屏幕时序、还是生成精确的脉冲信号,都需要用到微秒级甚至纳秒级精度的延时。很多新手刚开始使用STM32,会直接用简单的空循环延时,或者随便用系统时钟分频做延时,结果实际使用中发现延时误差很大,温度传感器读不出数据,屏幕驱动时序不对点不亮,遇到问题还不知道为什么不准。

STM32嵌入式开发中,精确延时是非常基础但又极其关键的功能。无论是驱动单总线传感器(比如DS18B20)、控制LCD屏幕时序、还是生成精确的脉冲信号,都需要用到微秒级甚至纳秒级精度的延时。很多新手刚开始使用STM32,会直接用简单的空循环延时,或者随便用系统时钟分频做延时,结果实际使用中发现延时误差很大,温度传感器读不出数据,屏幕驱动时序不对点不亮,遇到问题还不知道为什么不准。

STM32有多种实现延时的方式,不同方式精度不同,适用场景也不同。本文从延时的基本需求出发,梳理几种常用精确延时的实现原理、代码实现和优缺点,帮开发者根据自己的场景选择最合适的方案,解决延时不准的问题。

一、STM32延时的核心需求:精度和资源占用平衡

我们说的精确延时,一般要求误差不超过10%,对于高精度时序要求,误差要控制在1%以内。和Linux、RTOS里的休眠不同,我们说的延时是总线阻塞式延时,也就是调用延时函数之后,CPU一直停在这里等待延时结束,不能做其他任务,因为很多外设时序要求总线必须等待延时完成,才能进行下一步操作。

阻塞式精确延时有两个核心要求:

精度足够:微秒级延时误差不能超过几微秒,满足绝大多数外设的时序要求;

实现简单,资源占用少:最好不要占用一个硬件定时器,浪费宝贵的硬件资源,能利用现有资源实现最好。

接下来我们从最简单的实现方式讲起,一步步分析不同实现方式的优劣。

二、最基础的空循环延时:原理简单,但精度差误差大

空循环延时是最容易想到的实现方式,就是写一个空的for循环,让CPU循环N次,靠循环消耗的时间实现延时。原理非常简单,不需要任何硬件资源,几行代码就能实现,我们最常见的写法是这样的:

void delay_us(uint32_t us) {

uint32_t i;

for (i = 0; i < us * (SystemCoreClock / 1000000); i++) {

__NOP();

}

}

这种方式看起来简单,实际精度却非常差,主要问题有四个:

不同优化等级下循环速度不一样:如果开启O2优化,编译器会把空循环优化掉,整个循环直接没了,根本不会产生延时,要是优化等级低,循环指令周期变长,延时就会变长,误差很大;

指令周期不固定:一个for循环包含了计数器递增、判断跳转、NOP多个指令,不同架构下指令周期不同,计算出来的循环次数很容易错,哪怕差一个时钟周期,微秒级延时误差就会超过10%;

中断会影响精度:如果延时过程中来了一个中断,CPU去处理中断,延时时间就会变长,误差几微秒到几十微秒不等,对时序要求高的场景直接就出错了;

主频变化就失效:如果代码换了一个主频,原来的循环次数就不对了,哪怕是同一项目,动态调整主频之后延时就不准了,需要重新计算循环次数。

空循环唯一的优点就是不需要硬件资源,代码简单,只适合对精度要求不高的场景,比如LED闪烁这种几毫秒级别的延时,不适合需要精确延时的外设驱动场景。如果想要更高的精度,需要用基于硬件的实现方式。

三、内核定时器SysTick实现延时:资源零占用,精度足够常用场景

SysTick(系统滴答定时器)是Cortex-M内核自带的一个24位向下计数的定时器,专门给操作系统提供系统时钟,也可以用来做精确延时,不需要占用额外的硬件定时器,是目前STM32裸机开发中最常用的精确延时实现方式。

1. 实现原理

SysTick的时钟一般直接接内核时钟(也就是系统主频),计数频率等于系统主频,计数周期是1个主频时钟周期,精度可以达到1个时钟周期,非常高。我们只需要配置SysTick为1计数周期1us,或者直接计算要延时多少个时钟周期,让计数器倒计数,等待计数完成就返回,就能得到精确的延时。

常见的实现方式有两种:

配置SysTick的周期为1ms,给系统提供心跳,然后延时的时候累计心跳计数,只能实现毫秒级延时,精度不够,不适合微秒级延时;

延时的时候临时设置SysTick的计数值,延时完成之后恢复原来的配置,这种方式可以实现微秒级精确延时,精度很高,是主流的实现方式。

2. 代码实现(标准库版本)

void delay_init(void) {

// SysTick时钟选择为内核时钟,不需要开启中断,只需要计数

SysTick_Config(SystemCoreClock / 1000); // 先配置1ms周期,给系统心跳用,如果不需要心跳也可以直接用的时候临时改

}

// 微秒级精确延时

void delay_us(uint32_t us) {

uint32_t ticks;

uint32_t told;

uint32_t tnow;

uint32_t cnt = 0;

uint32_t reload = SysTick->LOAD; // 获取原来的自动重装载值

ticks = us * (SystemCoreClock / 1000000); // 计算需要的时钟周期数

cnt = 0;

told = SysTick->VAL; // 获取当前的计数器值

while (1) {

tnow = SysTick->VAL;

if (tnow != told) {

if (tnow < told) {

cnt += told - tnow; // 计数器还没回绕,直接加差值

} else {

cnt += reload - tnow + told; // 计数器回绕,补全计数

}

told = tnow;

if (cnt >= ticks) {

break; // 计数够了,退出

}

}

}

}

// 毫秒级延时,基于微秒封装

void delay_ms(uint32_t ms) {

uint32_t i;

for (i = 0; i < ms; i++) {

delay_us(1000);

}

}

如果用HAL库开发,其实可以更简单,HAL已经提供了HAL_Delay()实现毫秒级延时,但HAL_Delay本质是靠SysTick心跳实现,只能实现毫秒级,而且依赖中断,精度不够,微秒级延时还是用上面的方法自己实现最好。

3. 这种方式的优缺点

优点:

不需要占用额外的硬件定时器,用Cortex-M内核自带的SysTick,资源零占用,对STM32来说任何型号都能用,兼容性好;

精度很高,可以达到1微秒以内的误差,满足绝大多数外设驱动的时序要求,比如DS18B20、OLED、单总线传感器都能正常工作;

主频变化的时候只需要重新计算计数值,不用修改代码,SystemCoreClock变量会自动更新,适配不同主频。

缺点:

如果延时过程中关闭了SysTick,就没法用,一般不会出现这种情况;

如果中断优先级比SysTick高,延时过程中被高优先级中断打断,还是会有误差,但绝大多数场景下误差都在可接受范围内;

是阻塞式延时,和所有阻塞延时一样,延时的时候CPU不能做其他任务,但这是阻塞式延时的需求决定的,不是这种实现方式本身的问题。

总的来说,SysTick实现精确延时是目前裸机开发中性价比最高的方式,精度足够,资源占用为零,代码简单,是绝大多数场景的首选。

四、定时器周期中断实现延时:更高精度,适合大延时

如果需要更长的延时,或者项目中已经有多余的硬件定时器,也可以用定时器来实现精确延时,原理就是配置定时器的计数频率为1MHz,这样一个计数周期就是1us,需要延时多少微秒就设置多少计数值,然后等待定时器计数完成,非常直观。

1. 实现原理

以通用定时器TIM2为例,系统主频72MHz,配置预分频器PSC为71,那么定时器的计数频率就是72MHz/(71+1) = 1MHz,每个计数就是1us,我们开启定时器的更新中断,需要延的时候设置自动重装载值为要延时的us数,然后等待计数完成,计数完成之后触发中断,唤醒等待,精度可以做到1us,误差比SysTick还小。

如果不需要中断,也可以用查询的方式,不断读取计数器的值,等计数器到了就返回,一样能实现精确延时,不需要占用中断资源。

2. 核心实现代码

void TIM_Delay_Init(void) {

// 配置TIM2,计数频率1MHz

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

TIM_TimeBaseStructure.TIM_Period = 1000; // 初始周期随便设

TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz -> 1MHz

TIM_TimeBaseStructure.TIM_ClockDivision = 0;

TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_Cmd(TIM2, ENABLE);

}

void TIM_Delay_us(uint32_t us) {

// 设置计数器值,从零开始计数

TIM_SetCounter(TIM2, 0);

// 等待计数到目标值

while (TIM_GetCounter(TIM2) < us);

}

void TIM_Delay_ms(uint32_t ms) {

while (ms--) {

TIM_Delay_us(1000);

}

}

这种查询方式的实现非常简单,不需要开中断,代码比SysTick还简洁,精度更高,因为定时器计数器独立于CPU,哪怕CPU被中断打断,计数器还是会继续计数,所以延时时间不会因为中断而变长,误差比SysTick更小,对精度要求特别高的场景更有优势。

3. 优缺点总结

优点:

精度最高,哪怕被中断打断,延时时间也不会变,误差不超过1微秒,适合高精度时序要求;

代码非常简单,逻辑清晰,不容易出问题;

可以实现很长的延时,只要定时器位数足够,32位定时器可以延时几个小时,都能精确控制。

缺点:

需要占用一个硬件定时器,STM32的定时器资源有限,很多项目中定时器都被用在PWM、输入捕获这些地方,没有多余的定时器给延时用,浪费资源;

查询方式会一直占用CPU,和其他阻塞延时一样,但这是阻塞延时本身的需求,不影响使用。

如果你的项目中刚好有多余的硬件定时器,对精度要求又很高,那么定时器查询方式是最好的选择,精度比SysTick更高。

五、更高阶:DWT周期计数器实现纳秒级延时,不需要占用任何资源

Cortex-M3/M4/M7内核都自带DWT(数据观察点和跟踪)模块,DWT里面有一个自由运行的周期计数器,每个时钟周期自动加1,我们可以直接读这个计数器的值来做延时,精度可以达到一个时钟周期,也就是纳秒级,而且不需要占用任何定时器资源,也不需要修改任何配置,直接就能用,是目前最高精度的免费延时实现方式。

1. 实现原理

DWT本来是用来做调试跟踪的,它的CYCCNT计数器从内核复位开始,每个时钟周期自动加1,只要开启之后就一直运行,不需要CPU干预,我们需要延时的时候,先读取当前的CYCCNT值,然后不断读新的CYCCNT值,差值等于我们需要的时钟周期数就返回,就能实现精确延时,精度就是一个时钟周期,比前面几种方式都高。

2. 代码实现

void DWT_Delay_Init(void) {

// 开启DWT计数器

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 开启DWT时钟

DWT->CYCCNT = 0; // 清零计数器

DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 开启周期计数

}

void DWT_Delay_us(uint32_t us) {

uint32_t start = DWT->CYCCNT;

uint32_t cycles = us * (SystemCoreClock / 1000000); // 计算需要的周期数

while ((DWT->CYCCNT - start) < cycles);

}

void DWT_Delay_ns(uint32_t ns) {

uint32_t start = DWT->CYCCNT;

uint32_t cycles = ns * (SystemCoreClock / 1000000000);

while ((DWT->CYCCNT - start) < cycles);

}

这种实现方式简直是完美,不需要占用任何硬件资源,不需要中断,精度直接到纳秒级,比前面所有方式都高,而且代码非常简洁,现在越来越多的开发者开始用这种方式做精确延时。

3. 注意事项和优缺点

注意事项:

只有Cortex-M3及以上内核才有DWT,Cortex-M0/M0+没有这个模块,没法用,STM32F1、F4、H7、G4系列都是M3/M4/M7内核,都能用,STM32F0系列就用不了;

有些禁用调试的场景下,DWT可能会被关闭,一般正常开发使用都没问题,量产产品也能正常用。

优点:

精度最高,纳秒级精度,比任何其他方式都准,完全满足最高精度的时序要求;

不占用任何硬件资源,不需要定时器,不需要SysTick配置,只要开一次就一直能用;

计数器是硬件自动计数,哪怕被中断打断,计数还是正确,延时时间不会变长,误差极小;

代码非常简洁,几十行代码就搞定,不需要复杂配置。

缺点:

只支持Cortex-M3及以上内核,不支持M0/M0+,兼容性差一点;

几乎没有其他缺点,是目前STM32精确延时的最优实现。

六、实际开发中常见问题和解决方法

不管用哪种方式,实际开发中经常会遇到延时不准的问题,我们整理了最常见的原因和解决方法:

1. 系统主频修改之后延时不准

原因:计算计数值的时候用了硬编码的主频,不是用SystemCoreClock变量,修改主频之后硬编码不对,就不准了。解决方法:所有计数值计算都用SystemCoreClock动态计算,修改主频之后SystemCoreClock会自动更新,不需要修改代码。

2. 短延时误差大

比如延时1us,实际出来是3us,一般是因为函数调用本身有开销,进入函数、返回都需要几个时钟周期,误差就出来了。解决方法:可以把函数改成内联函数,减少函数调用开销,或者调整计算的计数值,减去函数调用本身消耗的周期,就能修正误差。

3. 中断导致延时变长

SysTick和空循环很容易遇到这个问题,延时过程中来了中断,处理中断耽误了时间,延时就变长了。解决方法:如果对精度要求高,换成DWT或者硬件定时器延时,硬件定时器不管CPU有没有被打断,都会一直计数,延时时间不会变。如果一定要用SysTick,可以在延时之前关闭中断,延时完成再开,但是关闭中断会影响其他中断响应,不推荐。

4. 编译器优化把空循环优化没了

空循环延时常见问题,开启O2优化之后,编译器发现空循环没有用,直接把整个循环删掉了,就没有延时了。解决方法:循环里面加一个volatile变量,每次循环都修改这个变量,编译器就不会优化,或者直接不用空循环,用SysTick或者DWT更靠谱。

总结

STM32实现精确延时有四种主流方式,各自适合不同的场景:

对精度要求不高,随便用用:选空循环,代码简单,不用额外资源;

普通裸机开发,大多数外设驱动:选SysTick,不用占用额外硬件,精度足够,兼容性最好,所有Cortex-M内核都能用;

有多余硬件定时器,对精度要求高:选硬件定时器查询,精度比SysTick高,不怕中断打断;

M3及以上内核,追求最高精度零资源占用:选DWT周期计数器,纳秒级精度,不用占用任何资源,是目前最优的方案。

只要选对适合自己场景的实现方式,避开编译器优化、中断影响、硬编码主频这些常见的坑,就能实现稳定的精确延时,满足绝大多数嵌入式开发的需求。 以上是根据你的要求生成的内容,如需修改可继续提出。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

在STM32嵌入式开发中,RAM(随机存取存储器)是程序运行时存储动态数据、堆栈、全局变量的核心资源,直接决定了程序能实现的功能复杂度。很多开发者都遇到过莫名的程序崩溃、硬件异常,追根溯源往往是RAM分配错误、占用溢出:...

关键字: RAM STM32

在嵌入式STM32开发中,程序崩溃是开发者最常遇到也最头疼的问题之一:程序运行中突然跑飞、进入HardFault中断,开发者往往只能靠加打印猜位置,排查一个bug可能需要几天时间。这时候,Backtrace功能就像是嵌入...

关键字: 嵌入式STM32 STM32

随着半导体测试向更高复杂性与并行度演进,多工位自动测试设备(ATE)和SiC/GaN测试对电感、电容和电阻(LCR)测量的需求不断提升。然而,传统的外接台式LCR仪表和基于线缆的设置难以扩展,而且会降低可重复性。本文介绍...

关键字: 半导体 电阻 嵌入式

智能高尔夫球追踪系统是一项创新的嵌入式电子项目,旨在展示如何将紧凑型物联网硬件集成到体育科技应用中。在体育领域,高尔夫球扮演着主要角色,但在现代时代,所有设备都变得更加智能化,高尔夫球也由此演变为智能高尔夫球。本项目结合...

关键字: 嵌入式 物联网 NRF无线技术

当一个项目需要在STM32上运行FreeRTOS时,摆在工程师面前的不止一条路。STM32CubeMX图形化配置工具的出现,让RTOS的集成从“手工作坊”变成了“流水线作业”。但这是否意味着传统的手写移植已经过时?答案并...

关键字: STM32 FreeRTOS

在工业自动化、智能传感、嵌入式组网等分布式总线系统中,设备自动地址分配是实现节点互联互通、即插即用的核心技术。传统人工配置地址方式存在操作繁琐、扩展性差、地址冲突风险高、维护成本高等诸多问题,已无法适配大规模、动态化的总...

关键字: 总线 嵌入式 组网

2026年6月8日 – 专注于引入新品的全球电子元器件和工业自动化产品授权代理商贸泽电子 (Mouser Electronics) 正式宣布,首次荣获全球嵌入式应用安全连接解决方案知名供应商NXP® Semiconduc...

关键字: 物联网 移动设备 嵌入式

城市灯火通明、生活井然运转的背后,总有人在不被注意的地方,日复一日地坚持着。他们或许没有惊天动地的故事,却在漫长岁月里,用自己的方式守护着他人的生活。近日,乡村教师班爱花、爱心厨房运营者丫丫妈,以及“扛楼女工”云姐的故事...

关键字: 西门子家电 洗碗机 嵌入式

2026年5月15日,正值“世界无幽日”,一组数据再次引发公众关注:据《中国幽门螺杆菌感染防控》白皮书显示,我国幽门螺杆菌人群感染率已接近50%,涉及超过7亿人口,且家庭内传播特征极为显著——父母若感染,子女感染风险升高...

关键字: 洗碗机 AI 嵌入式

一块STM32G431、三颗IRFS7530 MOSFET、两个1mΩ采样电阻——这就是一台20A无人机电调的全部硬件。但从上电到电机平稳空转,中间隔着寄存器配置、ADC同步触发、Park变换、PI调参、SVPWM生成五...

关键字: STM32 无人机 FOC电调
关闭