(延迟有四样写法,你知道么?)
精确的微秒延迟具有极其广泛的应用,它不仅能满足复杂通讯协议的时序要求,还能实现精准的脉冲生成,是控制硬件行为不可或缺的一环。
那么,我们究竟是如何让微控制器“知道”时间流逝了 1 微秒的呢?答案很简单:数数!
是的,就像我们计算时间一样,微控制器也是通过“数数”来判断时间流逝的。人类以秒为基本单位,而秒的定义正是基于铯133原子基态的特定跃迁周期,这保证了时间的精确性。在微控制器中,我们则依赖其内部高速时钟的“滴答声”来计数。
下面,我们将深入探讨两种常见的微秒延迟实现方式,并一同经历一次真实的调试之旅,揭示其中隐藏的奥秘。
好了不水字数了,开始写代码
一、通过系统滴答时钟(SysTick)和中断实现微秒延迟
1. 设置系统初始化96MHz主频
首先,确保你的微控制器以正确的主频运行。以 APM32F103xx 为例,通常需要在系统配置文件中调整宏定义。
如图所示,解除 96MHz 宏定义的注释,并注释掉默认的 72MHz。这样,在 MCU 启动时,SystemInit 函数(它会在调用 main 函数之前自动执行)就会将主频设置为 96MHz。


2. 配置 SysTick 时钟与中断
- 新建 delay.c 和 delay.h 文件。
- 在 delay.h 中定义以下函数:
inline void Delay_Isr();
void Delay_Init();
void Delay(uint32_t us);
- 在 apm32f10x_int.c 的 SysTick_Handler 函数中添加对 Delay_Isr 的调用:
void SysTick_Handler(void) {
Delay_Isr();
}
3. 实现延迟函数
在 delay.c 中,我们将 count 变量声明为 volatile,以防止编译器对其进行不当优化,确保中断函数对其的修改能被主程序及时感知。
#include "delay.h"
volatile uint32_t count = 0; //volatile 防止编译器错误优化该变量导致出问题
void Delay_Isr() {
count--;
}
void Delay_Init() {
count = 0;
SysTick_Config(96); //1us
}
void Delay(uint32_t us) {
if (us == 0) {
return; // 0 微秒无需延时
}
count = us;
while (count != 0) {
// 插入空操作,防止编译器将整个 while 循环优化掉,确保延时行为
__NOP();
}
}
这是一个基于 SysTick 中断的简洁实现。Delay_Init() 配置 SysTick 每 1 微秒触发一次中断,Delay_Isr() 在中断中递减 count。Delay() 函数则简单地等待 count 归零。
4. 测试与结果
修改 main.c 来测试这个中断式延迟:
int main(void)
{
GPIO_Config_T configStruct;
RCM_EnableAPB2PeriphClock(LED2_GPIO_CLK);
configStruct.pin = LED2_PIN;
configStruct.mode = GPIO_MODE_OUT_PP;
configStruct.speed = GPIO_SPEED_50MHz;
GPIO_Config(LED2_GPIO_PORT, &configStruct);
LED2_GPIO_PORT->BC = LED2_PIN;
Delay_Init();
while(1)
{
LED2_GPIO_PORT->ODATA ^= LED2_PIN;// 翻转 LED 状态
Delay(1000*1000);// 延迟 1 秒 (1000 * 1000 微秒)
}
}
编译,烧录,上电,点亮mcu!


从逻辑分析仪的记录中,我们可以看到采集到的电平变化信号是每 999.98125ms 变化一次。相对于 1 秒的目标值,这产生了大约 0.001875% 的相对误差,这非常准确!


二、通过轮询 SysTick 计数器实现微秒延迟
前面中断式的延迟虽然精确,但需要维护中断服务程序。有没有一种更“直接”的方式呢?答案是轮询。但轮询的实现需要更精妙的计数逻辑。
1. 移除中断式延迟,实现轮询函数
我们注释掉旧的 Delay_Isr 和 Delay_Init 调用,并替换为以下轮询实现:
void Delay(uint32_t us) {
if (us == 0) {
return; // 0 微秒无需延时
}
SysTick_Config(96); //1us
// 计算总共需要延时的 SysTick 滴答数(96MHz下96个滴答为1us)
uint32_t total_ticks_to_wait = us * 96;
// 获取 SysTick 当前计数器的值 (SysTick 是向下计数)
uint32_t start_val = SysTick->VAL;
uint32_t elapsed_ticks = 0;
uint32_t current_val;
while (elapsed_ticks < total_ticks_to_wait) {
current_val = SysTick->VAL; // 读取当前计数器值
if (current_val > start_val) {
// 情况1: 计数器已经从 start_val 向下计数到 0,然后重载,再继续向下计数到 current_val
// 流逝的滴答数 = (start_val 到 0 的距离) + (重载值到 current_val 的距离)
// 巧妙地计算方式:将当前值想象成“跨越了一个周期”,将其和起始值拉到同一个“展开”的时间轴上。
// 公式: (start_val - current_val) + SysTick 的完整周期长度
elapsed_ticks += (start_val - current_val + SysTick_LOAD_RELOAD_Msk + 1);
} else {
// 情况2: 计数器在 start_val 和 0 之间正常向下计数
elapsed_ticks += (start_val - current_val);
}
start_val = current_val; // 更新起始点为当前值,为下一次迭代做准备
// 插入空操作,防止编译器将整个 while 循环优化掉,确保延时行为
__NOP();
}
}
2. 首次测试:诡异的 7.5us 延迟?
编译,烧录,上电,点亮mcu!

等等!目标是 1 秒,结果怎么变成了 7.5us?这完全不对劲!
3. 捉虫 (Debug) 大作战
本着“有问题就问 AI”的原则,我们先把问题抛给它:

AI 指出,可能是 SysTick_Config 的位置问题。根据 AI 的建议,我们把 SysTick_Config(96); 移到 main 函数中,只执行一次:

再次编译,烧录,上电。结果……

问题依旧!看来还是要亲自“动一动脑子”才能解决。
延迟时间不准,意味着我们“数数不准”。再仔细审视数数的逻辑:

我们预期的逻辑是:
- 获取起始 SysTick 计数 -> 获取当前 SysTick 计数 -> 计算两个计数的差值 -> 判断是否达到目标时间。
- 在计算差值时:
- 如果 current_val <= start_val (未发生重载):差值 = start_val - current_val。
- 如果 current_val > start_val (发生重载):差值 = start_val + 周期长度 - current_val。
现在问题是,我们的 while 循环过早结束了,这意味着 elapsed_ticks 在不该大的时候突然变得很大。那么,是谁让 elapsed_ticks 突然变大的呢?
排查可疑对象:
显然,第一行代码多了一个 SysTick_LOAD_RELOAD_Msk + 1,它很可能是罪魁祸首!它代表了 SysTick 计数器的一个完整周期长度,如果这个值不正确,就会导致计算错误。
在这些代码行上设置断点,Debug启动!:

我们抓到了!果然是 elapsed_ticks += (start_val - current_val + SysTick_LOAD_RELOAD_Msk + 1); 导致 elapsed_ticks 突然变得异常大。这意味着 SysTick_LOAD_RELOAD_Msk + 1 这个“周期长度”的值不对。
通过查看定义,我们发现 SysTick_LOAD_RELOAD_Msk 是一个很大的默认值 (通常是 0xFFFFFF,即 224 −1)。

但是,SysTick 的实际重载值是多少呢?我们跳转到 SysTick->LOAD 寄存器的地址,将其添加到监视窗口进行观察:



真相大白!实际运行时,SysTick->LOAD 的值是 95!

这个“95”是不是很眼熟?

破案了! 是我们调用 SysTick_Config(96) 时,内部将 SysTick->LOAD 寄存器设置为了 96-1=95。因此,SysTick 的实际计数周期是 96 滴答(从 95 倒数到 0),而不是默认的 224-1个滴答。
好了破案了,是我们修改了重载值
4. 最终修复与精确结果
既然知道了真实的周期长度,我们将计算公式中的 SysTick_LOAD_RELOAD_Msk + 1 替换为 SysTick->LOAD + 1,使用实际的重载值来计算周期:
// 修改前:elapsed_ticks += (start_val - current_val + SysTick_LOAD_RELOAD_Msk + 1);
// 修改后:
elapsed_ticks += (start_val - current_val + SysTick->LOAD + 1);
编译,烧录,上电,点亮mcu!

完美!延迟时间现在恢复了正常的 999.981667ms!
因为论坛没有预览和草稿功能,为方便编写分成上下两部分。
usDelay.zip MD5 44A039F99074D77E1139CC5353BCCD5D