
简介这份资源面向STM32嵌入式开发者尤其是使用STM32F1与STM32F4系列进行项目开发、需要实现精确延时控制的工程师与学生。资源围绕延时函数这一常见需求提供了一套可直接集成到工程中的独立延时库帮助开发者省去从零编写与调试底层延时逻辑的时间。压缩包内共2个文件包含1个c源文件与1个h头文件分别承载延时函数的具体实现与函数原型、宏定义等接口声明整体包体仅848B轻量易用。该延时库支持毫秒与微秒级延时描述中提及延时上限可达477218ms与477218588us覆盖从短延时循环到长延时定时器处理的广泛范围。目前已有3011人学习下载适合希望快速为STM32F1或STM32F4工程补充可靠延时能力的开发者参考使用。1. 延时函数这件事比你想的更值得较真很多人第一次在 STM32 上写delay_ms(500)的时候都觉得这玩意儿太简单了——不就是空转吗可真到了项目里你会发现延时函数是那种「平时不起眼、出事就要命」的模块。比如某开发者用 STM32F1 调一个 DHT11 温湿度传感器时序要求微秒级结果用 HAL 库自带的HAL_Delay死活读不出数据换成自己写的delay_us立马就通了。再比如用 STM32F4 跑 WS2812 灯带对延时精度要求更高差几百纳秒颜色就全乱了。这份资源就是围绕 STM32F1 和 STM32F4 两个系列把延时函数的几种实现方式、精度差异、移植方法讲清楚适合正在做裸机开发、需要精确时序控制、或者想搞明白 SysTick 到底怎么用的嵌入式开发者。2. 先搞清楚STM32F1 和 F4 的延时到底差在哪2.1 内核不同延时实现的天花板就不同STM32F1 用的是 Cortex-M3 内核STM32F4 是 Cortex-M4F。两者都带 SysTick 定时器但 F4 的主频可以跑到 168MHz 甚至 180MHzF1 一般跑 72MHz。主频差了一倍多同样的循环延时函数在 F1 上跑 1ms 的循环次数放到 F4 上可能只有 0.4ms 左右。这就是为什么很多从 F1 转到 F4 的开发者会发现原来调好的延时突然不准了。SysTick 是一个 24 位的递减计数器挂在内核的私有外设总线上不占用任何外设资源。它的时钟源可以选内核时钟或者内核时钟的 8 分频。F1 和 F4 在 SysTick 的配置上基本一致区别主要在于时钟频率和重装载值的计算。常见的延时实现有三种软件循环延时、SysTick 中断延时、SysTick 轮询延时。软件循环延时最粗糙但胜在简单不需要配置任何外设SysTick 中断延时精度高但会占用中断资源SysTick 轮询延时不需要开中断精度也不错是比较推荐的做法。2.2 三种延时方式的代码实现与对比先看最基础的软件循环延时这种写法在 F1 上很常见// 软件循环延时基于 STM32F1 72MHz // 这种写法精度差但不需要任何外设配置 void delay_ms_loop(uint16_t ms) { uint32_t i; while(ms--) { // 下面的循环次数是经验值不同优化等级下差异很大 for(i 0; i 7200; i) { __NOP(); // 空操作防止编译器优化掉循环 } } }这段代码的逻辑很简单外层循环控制毫秒数内层循环空转。__NOP()是 CMSIS 提供的内联函数对应汇编的 NOP 指令。参数ms是延时的毫秒数类型用uint16_t是因为一般不会延时超过 65535ms。但这里有个大坑内层循环的次数 7200 是在特定编译器优化等级下测出来的。如果你把优化等级从-O0改成-O2编译器可能直接把整个循环优化掉延时直接变成 0。所以这种写法只适合对精度要求不高的场景比如 LED 闪烁。再来看 SysTick 轮询延时这是更靠谱的做法// SysTick 轮询延时适用于 STM32F1 和 STM32F4 // 需要先配置 SysTick 的时钟源和重装载值 volatile uint32_t delay_tick 0; // SysTick 中断服务函数每 1ms 调用一次 void SysTick_Handler(void) { if(delay_tick 0) { delay_tick--; } } // 毫秒级延时基于 SysTick 中断 void delay_ms_systick(uint32_t ms) { delay_tick ms; while(delay_tick ! 0); // 等待计数减到 0 } // 微秒级延时基于 SysTick 轮询 // 假设 SysTick 时钟为 72MHz重装载值为 71 void delay_us_systick(uint32_t us) { uint32_t start SysTick-VAL; uint32_t ticks us * (SystemCoreClock / 1000000); uint32_t elapsed 0; while(elapsed ticks) { uint32_t current SysTick-VAL; if(current start) { elapsed start - current; } else { // 计数器发生了回绕 elapsed start (SysTick-LOAD - current); } start current; } }delay_ms_systick依赖 SysTick 中断每 1ms 中断一次在中断里递减delay_tick。主循环里不断检查这个变量直到它变成 0。这种方式的精度取决于中断的响应时间一般误差在几个微秒以内。delay_us_systick则是直接读取 SysTick 的当前值寄存器SysTick-VAL通过计算差值来实现微秒级延时。SystemCoreClock是 CMSIS 提供的全局变量表示当前内核时钟频率。SysTick-LOAD是重装载值当计数器减到 0 时会自动重载这个值。这里有个细节需要注意SysTick-VAL是递减的所以当前值小于起始值时说明没有发生回绕直接相减即可如果当前值大于起始值说明计数器已经回绕了一次需要加上重装载值再计算。2.3 F1 和 F4 的 SysTick 配置差异在 F1 上配置 SysTick 通常是这样// STM32F1 72MHz配置 SysTick 每 1ms 中断一次 void SysTick_Init(void) { // 重装载值 72000000 / 1000 - 1 71999 if(SysTick_Config(SystemCoreClock / 1000)) { // 配置失败死循环 while(1); } // 设置中断优先级为最低 NVIC_SetPriority(SysTick_IRQn, 0x0F); }SysTick_Config是 CMSIS 提供的函数参数是重装载值。它会自动配置 SysTick 的时钟源为内核时钟使能中断并启动计数器。返回值非 0 表示配置失败通常是重装载值超过了 24 位的最大值。在 F4 上代码几乎一样只是SystemCoreClock的值不同// STM32F4 168MHz配置 SysTick 每 1ms 中断一次 void SysTick_Init(void) { // 重装载值 168000000 / 1000 - 1 167999 if(SysTick_Config(SystemCoreClock / 1000)) { while(1); } NVIC_SetPriority(SysTick_IRQn, 0x0F); }看起来只是数字变了但这里有个容易翻车的地方F4 的SystemCoreClock默认可能是 16MHzHSI 时钟如果你没有先调用SystemInit或者自己配置 PLLSystemCoreClock的值就是错的算出来的重装载值也是错的延时自然就不准了。我一般会在main函数开头先调用SystemInit()然后再配置时钟最后才初始化 SysTick。3. 把延时函数跑起来从工程搭建到实测验证3.1 工程文件结构怎么组织拿到这份资源后你会看到里面按 F1 和 F4 分成了两个文件夹每个文件夹里都有对应的延时函数实现和测试工程。常见的目录结构是这样的STM32_Delay/ ├── STM32F1/ │ ├── Delay/ │ │ ├── delay.c │ │ └── delay.h │ ├── User/ │ │ ├── main.c │ │ └── stm32f10x_it.c │ └── Project/ │ └── MDK-ARM/ │ └── Project.uvprojx ├── STM32F4/ │ ├── Delay/ │ │ ├── delay.c │ │ └── delay.h │ ├── User/ │ │ ├── main.c │ │ └── stm32f4xx_it.c │ └── Project/ │ └── MDK-ARM/ │ └── Project.uvprojx └── README.mdDelay文件夹放延时函数的实现User文件夹放主函数和中断服务函数Project文件夹放 Keil 工程文件。这种结构比较清晰移植的时候只需要把Delay文件夹拷过去然后在工程里添加对应的.c文件即可。3.2 在 Keil 里添加延时函数并测试以 F1 工程为例打开Project.uvprojx后在左侧的工程树里右键点击User组选择Add Existing Files to Group把delay.c添加进去。然后在main.c里包含头文件#include delay.h #include stm32f10x.h int main(void) { // 初始化系统时钟 SystemInit(); // 初始化延时函数 delay_init(); // 初始化 LED 引脚假设接在 PA5 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); while(1) { GPIO_SetBits(GPIOA, GPIO_Pin_5); // LED 亮 delay_ms(500); // 延时 500ms GPIO_ResetBits(GPIOA, GPIO_Pin_5); // LED 灭 delay_ms(500); // 延时 500ms } }delay_init()是延时函数的初始化函数内部会配置 SysTick。delay_ms(500)就是调用我们前面讲的毫秒级延时。烧录后如果 LED 以 1Hz 的频率闪烁说明延时基本正常。但这里有个问题LED 闪烁只能看出大概的延时看不出精度。要测精度得用示波器或者逻辑分析仪。我一般会在延时前后翻转一个 GPIO然后用逻辑分析仪抓波形看高电平持续时间是不是准确的 500ms。3.3 用 GPIO 翻转法实测延时精度下面这段代码可以用来测试延时精度// 延时精度测试代码 // 用 PA5 输出方波逻辑分析仪抓波形 int main(void) { SystemInit(); delay_init(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); while(1) { GPIO_SetBits(GPIOA, GPIO_Pin_5); delay_us(100); // 延时 100us GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_us(100); // 延时 100us } }这段代码会在 PA5 上输出一个 5kHz 的方波。用逻辑分析仪抓波形如果高电平时间是 100us说明延时准确如果偏差超过 10%就要检查 SysTick 的配置是否正确。常见的问题是SystemCoreClock的值不对。比如在 F4 上如果你用的是外部晶振 8MHz经过 PLL 倍频到 168MHz但SystemCoreClock没有更新它可能还是默认的 16MHz这样算出来的重装载值就是错的延时会是实际值的 10 倍左右。4. 避坑指南延时函数最常见的 5 个问题4.1 延时不准偏差固定倍数现象调用delay_ms(1000)实际延时是 500ms 或者 2000ms偏差是一个固定倍数。原因SystemCoreClock的值和实际内核时钟不一致。比如实际跑 72MHz但SystemCoreClock还是 8MHz算出来的重装载值就偏大延时变长。解决在main函数开头调用SystemInit()或者手动更新SystemCoreClock变量。如果是自己配置的时钟配置完后要调用SystemCoreClockUpdate()来刷新这个变量。4.2 延时函数在中断里卡死现象在中断服务函数里调用delay_ms程序卡死不动。原因delay_ms依赖 SysTick 中断来递减计数但如果在中断里调用而 SysTick 中断的优先级低于当前中断SysTick 中断就无法触发delay_tick永远不会减到 0导致死循环。解决不要在中断里使用依赖中断的延时函数。如果必须在中断里延时用delay_us_systick这种轮询方式的延时或者用简单的软件循环延时。4.3 优化等级改变后延时失效现象在-O0优化下延时正常改成-O2后延时变成 0 或者大幅缩短。原因编译器把空循环优化掉了。软件循环延时里的for循环如果没有volatile修饰或者没有实际副作用编译器会认为它是无用代码直接删除。解决在循环里加__NOP()或者把循环变量声明为volatile。更好的做法是直接用 SysTick 延时不依赖软件循环。4.4 F1 和 F4 代码直接移植后延时错误现象把 F1 的延时函数直接拷到 F4 工程里延时偏差很大。原因F1 和 F4 的主频不同SystemCoreClock的值不同重装载值计算方式虽然一样但代入的数值不一样。如果直接拷贝而没有修改时钟配置延时就会出错。解决移植时确认目标芯片的SystemCoreClock值确保SysTick_Config的参数是基于当前主频计算的。F1 一般是 72MHzF4 一般是 168MHz但具体取决于你的时钟配置。4.5 微秒延时在低主频下不准现象在 F1 上跑 72MHzdelay_us(1)实际延时可能是 2us 甚至更多。原因delay_us_systick函数本身有执行时间包括读取SysTick-VAL、计算差值、循环判断等。当延时时间很短时函数本身的开销就占了很大比例。解决对于 1us 级别的延时建议用硬件定时器或者直接写汇编NOP指令。如果非要用 SysTick可以把delay_us的最小值限制在 10us 以上或者对函数进行优化减少不必要的计算。5. 二次开发把延时函数改成自己顺手的样子5.1 用宏定义切换 F1 和 F4如果你同时维护 F1 和 F4 两个项目每次改延时函数都要改时钟配置很麻烦。我一般会在delay.h里用宏定义来区分// delay.h #ifndef __DELAY_H #define __DELAY_H #include stm32f10x.h // 或者 stm32f4xx.h // 根据芯片型号自动选择时钟频率 #if defined(STM32F10X_MD) || defined(STM32F10X_HD) #define DELAY_SYSCLK 72000000 #elif defined(STM32F40_41xxx) || defined(STM32F429_439xx) #define DELAY_SYSCLK 168000000 #else #error 请先定义芯片型号 #endif void delay_init(void); void delay_ms(uint32_t ms); void delay_us(uint32_t us); #endif这样在delay.c里就可以用DELAY_SYSCLK来计算重装载值不用每次都手动改数字。STM32F10X_MD和STM32F40_41xxx是 Keil 工程里预定义的宏一般在Options for Target的C/C选项卡里可以看到。5.2 加一个延时补偿系数实际项目中由于中断响应、函数调用开销等因素延时往往会比设定值稍长。我一般会加一个补偿系数在初始化的时候根据实测结果调整// 延时补偿系数默认 1.0 static float delay_compensation 1.0f; // 设置补偿系数 void delay_set_compensation(float factor) { if(factor 0.5f factor 2.0f) { delay_compensation factor; } } // 带补偿的毫秒延时 void delay_ms_compensated(uint32_t ms) { uint32_t actual (uint32_t)(ms * delay_compensation); delay_ms(actual); }delay_compensation默认是 1.0表示不补偿。如果实测发现延时偏长 5%就把它设成 0.95。这个系数可以根据实际测试结果来调整比如用逻辑分析仪测出实际延时然后反推系数。5.3 用硬件定时器替代 SysTickSysTick 虽然方便但它只有一个如果项目里还有其他地方要用 SysTick就会冲突。这时候可以用一个普通的硬件定时器来做延时比如 TIM2// 用 TIM2 做微秒延时适用于 STM32F1 void delay_tim_init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_InitStructure.TIM_Period 0xFFFF; // 自动重装载值 TIM_InitStructure.TIM_Prescaler 72 - 1; // 预分频72MHz / 72 1MHz TIM_InitStructure.TIM_ClockDivision 0; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_Cmd(TIM2, ENABLE); } // 微秒延时基于 TIM2 void delay_us_tim(uint32_t us) { uint16_t start TIM_GetCounter(TIM2); uint16_t target us; uint16_t elapsed 0; while(elapsed target) { uint16_t current TIM_GetCounter(TIM2); if(current start) { elapsed current - start; } else { elapsed (0xFFFF - start) current; } } }TIM_Prescaler设为 71这样定时器的计数频率就是 1MHz每个计数代表 1us。TIM_Period设为 0xFFFF让计数器在 0 到 65535 之间循环。delay_us_tim通过读取计数器的值来计算经过的时间。这种方式的优点是精度高不占用 SysTick缺点是占用一个硬件定时器。如果项目里定时器资源紧张还是要用 SysTick。5.4 验证改造后的延时函数改完延时函数后一定要用逻辑分析仪或者示波器验证。我一般会写一个测试函数输出不同频率的方波然后逐个检查// 延时测试函数 void delay_test(void) { // 测试 1ms 延时 GPIO_SetBits(GPIOA, GPIO_Pin_5); delay_ms(1); GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_ms(1); // 测试 100us 延时 GPIO_SetBits(GPIOA, GPIO_Pin_5); delay_us(100); GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_us(100); // 测试 10us 延时 GPIO_SetBits(GPIOA, GPIO_Pin_5); delay_us(10); GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_us(10); }用逻辑分析仪抓 PA5 的波形看每个脉冲的宽度是否和设定值一致。如果偏差超过 5%就要检查时钟配置和补偿系数。从那以后我每次改延时函数都会先用逻辑分析仪抓一遍波形确认精度没问题再往下写业务代码。这个习惯帮我省了很多调试时间毕竟延时不准导致的问题往往要到项目后期才会暴露出来那时候再回头查就麻烦了。希望这份资源能帮到你少走一些弯路。本文还有配套的精品资源点击获取