在单片机上, delay函数只能通过忙等的机制来实现延时, 与PC机所实现的真正意义上的休眠不是一个概念.
代码实现:
delay.h
c
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32l4xx_hal.h"
/************************** RTOS自动适配(通用逻辑) **************************/
/* 自动识别 RT-Thread / FreeRTOS / uCOS */
#if defined(__RTTHREAD__) || defined(FREE_RTOS) || defined(USE_FreeRTOS) || \
defined(OS_CRITICAL_METHOD) || defined(CPU_CFG_CRITICAL_METHOD)
#define RTOS_USING 1
#else
#define RTOS_USING 0
#endif
/* 函数声明 */
/**
* @brief 初始化DWT计数器并自动计算频率偏移
*/
void delay_init(void);
/**
* @brief 微秒级延时
* @param nus: 延时微秒数 (最大支持约 53.6s @ 80MHz)
*/
void delay_us(uint32_t nus);
/**
* @brief 毫秒级延时 (RTOS模式下会释放CPU)
* @param nms: 延时毫秒数
*/
void delay_ms(uint32_t nms);
/**
* @brief 秒级延时 (建议大延时使用此函数)
* @param ns: 延时秒数
*/
void delay_s(uint32_t ns);
#endif /* __DELAY_H */
delay.c
c
#include "delay.h"
#include <stdint.h>
#include "core_cm4.h"
static uint32_t s_ticks_per_us = 0;
/**
* @brief 初始化DWT
*/
void delay_init(void)
{
// 获取当前 CPU 主频 (例如 80000000 Hz)
uint32_t cpu_freq = HAL_RCC_GetSysClockFreq();
s_ticks_per_us = cpu_freq / 1000000;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
// 部分内核需要解锁 DWT 访问权限
// DWT->LAR = 0xC5ACCE55;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
/**
* @brief 微秒级延时
*/
void delay_us(uint32_t nus)
{
if (nus == 0) return;
uint32_t start = DWT->CYCCNT;
uint32_t ticks = nus * s_ticks_per_us;
// 利用无符号减法,完美处理 CYCCNT 溢出回到 0 的情况
while ((DWT->CYCCNT - start) < ticks);
}
/**
* @brief 毫秒级延时 (智能适配 RTOS 与中断)
*/
void delay_ms(uint32_t nms)
{
if (nms == 0) return;
#if RTOS_USING == 1
// 检查是否在中断中 或 调度器是否未运行
if (__get_IPSR() != 0 || xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED)
{
for (uint32_t i = 0; i < nms; i++) delay_us(1000);
}
else
{
// 这里的 API 根据你的 RTOS 选择其一
vTaskDelay(pdMS_TO_TICKS(nms));
}
#else
for (uint32_t i = 0; i < nms; i++) delay_us(1000);
#endif
}
/**
* @brief 秒级延时
*/
void delay_s(uint32_t ns)
{
while (ns--)
{
delay_ms(1000);
}
}
为什么不用HAL库的HAL_Delay函数
因为HAL_Delay仅支持毫秒级别的延时, 并且依赖系统的滴答定时器SysTick.
当需要SPI/I2C时序, GPIO电平翻转, 传感器驱动等对微秒级时序要求严格的场景时, 需要延时微秒
代码逐行解释
1. 初始化打开内核跟踪, 启用DWT模块
// 全局启用DWT和ITM功能
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
对应文档 DDI0403E_Armv7-M Architecture Reference Manua.pdf
如下

// DWT计数器清零
DWT->CYCCNT = 0;
// DWT计数器使能
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

2. 利用忙等实现延时
uint32_t start = DWT->CYCCNT;
uint32_t ticks = nus * s_ticks_per_us;
// 利用无符号减法,完美处理 CYCCNT 溢出回到 0 的情况
while ((DWT->CYCCNT - start) < ticks); 通过不断查看计数器与初始值的差来确定是否结束等待
每1us的耗时确定逻辑如下:
uint32_t cpu_freq = HAL_RCC_GetSysClockFreq();
s_ticks_per_us = cpu_freq / 1000000;
先获取SYSCLK主时钟频率, 因为1s=1000,000us, 所以, 主时钟频率÷1000,000就得到每1usCPU能够运行的次数, 我的是80MHz, 相当于每1us就会运算80次, 所以s_ticks_per_us = 80
然后DWT->CYCCNT是CPU运算一次就累加1, 所以DWT->CYCCNT累加80次就是精确的1us