STM32中的微秒级延时函数主要用于需要精确控制短时间间隔的场景。它通过让CPU原地执行特定次数的空操作(或基于硬件定时器计数)来实现短暂的暂停,从而满足外设、通信协议或传感器对精确时序的要求。

常见应用场景


1、通信协议驱动:

GPIO模拟通信:驱动I2C、SPI、1-Wire、UART(位碰撞)、WS2812(NeoPixel)等协议时,需要精确控制SCL/SDA的翻转时间、数据位的宽度、起始/停止条件的时间、复位脉冲的长度等。

2、传感器驱动:

初始化/复位脉冲:许多传感器(如DHT11/DHT22温湿度传感器、超声波测距模块HC-SR04的触发信号)需要特定宽度的启动或复位脉冲(通常是几十微秒)。

3、精确脉冲生成:

在GPIO上产生精确宽度的脉冲信号(例如,用于控制步进电机的步进脉冲、简单的PWM模拟、测试信号等),当所需脉冲宽度小于硬件定时器/PWM能方便设置的最小分辨率时,微秒延时结合GPIO翻转是常用方法。

实现方式

1、循环空指令实现:

固有缺陷


1、非线性误差:短延时误差大,长延时相对准确

2、阻塞式:延时期间CPU完全被占用

3、频率依赖:系统时钟变化时需重新校准

4、编译器依赖:不同编译器生成代码不同

5、不可中断:无法响应紧急事件

循环空指令的方式有很多,我给出我常用的一个

我测试发现可以满足大多数驱动程序,延时越长越精准,可能延时1微妙就误差特别大,慎用

#define LOOPS_PER_US 9  // 72MHz下典型值

void Delay_us(uint32_t us)
{
    uint32_t count = us * LOOPS_PER_US;
    while(count--);
}

2、使用通用定时器(如TIM2)——更高精度

误差分析:
1、函数调用开销:约0.1μs (72MHz下约7个时钟周期)

2、循环判断开销:约0.05μs

3、总误差:随延时时间增加基本保持恒定在0.1-0.2μs

具体配置:设置定时器每1us计数一次,以stm32f103c8t6演示

        Clock Source: Internal Clock

        Prescaler: 71 (72MHz / (71+1) = 1MHz → 1μs/计数)

        Counter Mode: Up

        Counter Period: 65535 (最大16位值)

        Auto-reload preload: Enable

        其他保持默认

注意定时器溢出问题,设置的时间既然能溢出也没必要使用微秒级。

void delay_us(uint32_t us)
{
    __HAL_TIM_SET_COUNTER(&htim1, 0);  // 重置计数器
    HAL_TIM_Base_Start(&htim1);        // 启动定时器
    
    while (__HAL_TIM_GET_COUNTER(&htim1) < us);  // 等待计数达到指定值
    
    HAL_TIM_Base_Stop(&htim1);         // 停止定时器
}
3、使用系统滴答定时器(SysTick)

如果不想占用硬件定时器,可以使用SysTick实现(精度稍低),像f103c8t6,只有四个定时器,资源有限,可以使用这种方式,虽然有缺点,但是影响都不大,这种方式比较推荐。

缺点:

        阻塞式延时:占用CPU资源,期间无法执行其他任务。

        中断干扰:若高优先级中断触发,可能导致延时误差增大。

        24位计数器限制:最大延时受限于LOAD值(如72MHz时最长约186ms)。

        低功耗模式失效:CPU休眠时SysTick停止,无法使用。

static uint32_t us_multiplier;  // 每微秒的时钟周期数

// 初始化函数(必须在系统时钟配置后调用)
void delay_init(void) {
    // 计算每微秒所需的时钟周期数
    us_multiplier = SystemCoreClock / 1000000;
    
    // 确保SysTick已经启用(CubeMX默认会配置)
    if (!(SysTick->CTRL & SysTick_CTRL_ENABLE_Msk)) {
        HAL_InitTick(0);  // 初始化SysTick
    }
}

// 微秒级延时函数
void delay_us(uint32_t us) {
    if (us == 0) return;
    
    uint32_t start_tick = SysTick->VAL;
    uint32_t ticks_required = us * us_multiplier;
    uint32_t elapsed_ticks;
    
    while (1) {
        uint32_t current_tick = SysTick->VAL;
        
        // 处理计数器回绕(24位递减计数器)
        if (current_tick > start_tick) {
            elapsed_ticks = start_tick + (SysTick->LOAD - current_tick) + 1;
        } else {
            elapsed_ticks = start_tick - current_tick;
        }
        
        if (elapsed_ticks >= ticks_required) {
            break;
        }
    }
}
4、使用DWT周期计数器实现高精度微秒延时 

DWT(Data Watchpoint and Trace)是ARM Cortex-M系列处理器中的调试组件,其中的CYCCNT(Cycle Counter)寄存器提供了极其精确的计时功能,是STM32上实现高精度延时的最佳方案。 

原理说明

DWT的CYCCNT是一个32位向上计数器,它在每个CPU时钟周期自动增加。在72MHz的STM32F103上:

  • 1秒 = 72,000,000个周期

  • 1微秒 = 72个周期

通过读取此计数器,我们可以精确测量代码执行时间,实现纳秒级延时精度。

方案优势
1、超高精度:

在72MHz下精度可达±0.014μs(约1个时钟周期)

实测误差小于0.05μs(50纳秒)

2、性能优异:

函数调用开销极小(约3个周期)

不占用任何硬件定时器资源

与SysTick、HAL_Delay完全兼容

3、自动适应:

使用SystemCoreClock自动适应不同时钟频率

在8MHz-72MHz范围内均可正常工作

// DWT初始化函数//先初始化才可以使用
void DWT_Init(void)
{
	// 使能DWT访问权限
	CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

	// 重置周期计数器
	DWT->CYCCNT = 0;

	// 使能周期计数器
	DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

// 微秒级延时函数
void Delay_us(uint32_t us)
{
	// 计算需要的时钟周期数
	uint32_t cycles = us * (SystemCoreClock / 1000000);

	// 记录起始周期计数
	uint32_t start = DWT->CYCCNT;

	// 等待达到指定周期数
	while ((DWT->CYCCNT - start) < cycles) {
		// 空循环
	}
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐