1. 项目缘起为什么FreeRTOS下的us延时是个“坑”最近在做一个基于STM32和FreeRTOS的精密数据采集项目需要控制一个外部ADC的采样时序。ADC的芯片手册上明确写着两个关键控制信号之间的间隔需要至少5微秒us。在裸机环境下这活儿很简单一个DWT-CYCCNT循环或者精准的定时器就能搞定。但当我信心满满地把代码搬到FreeRTOS任务里问题就来了延时要么严重超时要么直接卡死系统调度变得一塌糊涂。我相信很多从裸机转向RTOS的STM32开发者都踩过这个坑。在FreeRTOS里我们最常用的延时是vTaskDelay()但这个函数的精度是以系统的时钟节拍Tick为单位的。比如你的configTICK_RATE_HZ设置为1000那么一个Tick就是1毫秒ms。你想延时10微秒对不起vTaskDelay(1)直接就给你延时了1000微秒误差高达100倍这显然无法满足像精确时序控制、高速通信协议解析如WS2812B灯带、超声波测距等对微秒级延时有着严苛要求的场景。所以“STM32在FreeRTOS下的us延时”这个需求本质上是在寻找一种方法能在多任务、可抢占的实时操作系统环境下实现高精度、低抖动的微秒级阻塞延时同时不能破坏FreeRTOS的调度器心脏——SysTick定时器的正常工作。网上有很多零碎的方案但要么不完整要么有隐藏的陷阱。今天我就结合自己的踩坑经历把几种主流方案的原理、实现、以及最关键的“避坑指南”彻底讲透。2. 核心原理SysTick、DWT与通用定时器的博弈在深入代码之前我们必须搞清楚STM32上能用于高精度延时的几种硬件资源以及它们在FreeRTOS环境下的“生存状态”。2.1 FreeRTOS的“心脏”SysTick定时器SysTick是Cortex-M内核自带的一个24位递减计数器。FreeRTOS移植的核心就是接管了SysTick的中断服务函数通常是xPortSysTickHandler。在这个中断里系统会更新任务调度所需的时钟计数检查是否有更高优先级的任务就绪从而决定是否进行任务切换。关键陷阱绝对不要在FreeRTOS运行后再去随意修改SysTick的加载值SysTick-LOAD或控制寄存器SysTick-CTRL。这会导致FreeRTOS的时钟节拍混乱轻则任务调度周期异常重则整个系统死锁。因此任何想通过“调整SysTick频率来实现us延时”的方案从根上就是错误的会直接摧毁RTOS的基石。2.2 被遗忘的“瑞士军刀”DWT周期计数器DWTData Watchpoint and Trace单元里有一个叫做CYCCNT的32位周期计数器。它紧随着内核时钟HCLK递增主频是多少MHz它一秒就累加多少百万次。如果你的STM32运行在72MHz那么CYCCNT每增加1就代表大约13.89纳秒ns的时间流逝。这个精度用来做us级延时绰绰有余。它的巨大优势是零开销纯硬件计数器无需配置中断。极高精度直接绑定内核时钟。使用简单通常只需使能一次然后直接读取其值。在裸机中它是实现delay_us函数的不二之选。但在FreeRTOS中我们需要注意它是一个全局共享的硬件资源如果多个任务同时使用它来做延时理论上不会冲突因为都是只读操作。真正的风险在于任务被抢占。2.3 灵活备胎通用定时器TIM几乎所有的STM32都有多个通用定时器TIM2, TIM3, TIM4等。我们可以配置其中一个定时器以1MHz1us计数一次或更高的频率运行然后通过查询其计数器CNT寄存器或者结合中断来实现延时。它的优点是独立资源与FreeRTOS的核心SysTick完全解耦互不影响。功能强大可以结合输入捕获、PWM等做更复杂的事情。可分配可以指定一个专用定时器给高精度延时模块使用。缺点是占用硬件资源需要牺牲掉一个定时器。略有开销需要额外的初始化配置。2.4 方案选型逻辑面对这三个选择我们的决策路径应该是SysTick直接排除。动了它等于给FreeRTOS“拔管”。DWT首选方案。只要解决任务抢占带来的误差问题它就是最简单、最节省资源的方案。适用于对资源极度敏感、且延时操作不那么频繁的场景。通用定时器保底方案。当DWT方案因某些原因如芯片不支持、或需要绝对隔离不可用时或者需要同时利用该定时器做其他精度计时如脉冲宽度测量时使用。接下来我们将重点深入DWT方案并剖析其最大的挑战——任务抢占然后给出通用定时器的实现作为备选。3. 方案一基于DWT周期计数器的实现与深度避坑这是最优雅的方案但坑也最多。一个天真的实现可能如下// 错误示范这是一个有严重缺陷的us延时函数 void delay_us_bad(uint32_t us) { uint32_t start_tick DWT-CYCCNT; uint32_t delay_ticks us * (SystemCoreClock / 1000000); while((DWT-CYCCNT - start_tick) delay_ticks); }在单任务或裸机下这个函数工作“完美”。但在FreeRTOS下它隐藏着致命问题。3.1 问题一任务抢占导致的“时间膨胀”假设你的任务A调用了delay_us_bad(100)期望延时100us。在while循环执行到第50us时一个更高优先级的任务B就绪了FreeRTOS调度器立刻抢占任务A切换到任务B执行。任务B执行了300us后才放弃CPU。此时调度器切回任务Awhile循环继续判断发现从start_tick到现在实际已经过去了350us远大于100us于是循环立刻退出。结果你期望的100us延时实际变成了350us以上。这个误差是不可预测的完全取决于系统中其他高优先级任务的行为。这对于需要精确时序的控制来说是灾难性的。3.2 问题二计数器溢出DWT-CYCCNT是一个32位无符号整数当你的主频是72MHz时它大约每 (2^32 / 72e6) ≈ 59.65秒就会溢出归零一次。上面的while循环判断条件(DWT-CYCCNT - start_tick) delay_ticks在发生溢出时会计算出错误的差值因为无符号数减法在溢出时会产生一个巨大的数导致循环可能永远无法退出或者延时时间极短。3.3 增强型DWT延时实现为了解决上述问题一个健壮的实现必须考虑抢占和溢出。但请注意在任务函数内我们无法完全“禁止”抢占因为那会破坏RTOS的实时性。我们只能减少被抢占的窗口并让延时逻辑适应被抢占的情况。核心思路是分段延时并实时检查是否超时。我们不一次性计算总的延时ticks然后死等而是将长时间延时拆分成多个极短的时间片在每个时间片开始时都重新检查“从最初开始到现在总时间是否已经达到目标”。如果中途被抢占回来之后检查会发现已经超时则立刻退出。// dwt_delay.h #ifndef __DWT_DELAY_H #define __DWT_DELAY_H #include “stm32f1xx_hal.h” // 根据你的芯片系列修改 // DWT初始化函数需要在FreeRTOS启动前调用例如在main函数开头创建任务之前 void DWT_Init(void); // 高精度微秒延时函数阻塞式 // 注意此函数执行期间任务可能被抢占但函数会补偿因此产生的误差确保总延时不少于指定时间。 void delay_us(uint32_t us); #endif // dwt_delay.c #include “dwt_delay.h” #include “FreeRTOS.h” #include “task.h” // DWT控制寄存器宏定义 #define DWT_CR *(volatile uint32_t *)0xE0001000 #define DWT_CYCCNT *(volatile uint32_t *)0xE0001004 #define DEM_CR *(volatile uint32_t *)0xE000EDFC #define DEM_CR_TRCENA (1 24) #define DWT_CR_CYCCNTENA (1 0) static uint32_t us_ticks; // 存储系统主频除以1M即1us对应的CPU周期数 void DWT_Init(void) { // 1. 使能DWT跟踪功能 DEM_CR | DEM_CR_TRCENA; // 2. 清零并使能DWT周期计数器 DWT_CYCCNT 0; DWT_CR | DWT_CR_CYCCNTENA; // 3. 计算1us对应的周期数 us_ticks SystemCoreClock / 1000000; // 如果SystemCoreClock不能被1MHz整除这里会丢失精度但对于us级延时影响很小。 } void delay_us(uint32_t us) { if (us 0) return; uint32_t start_tick DWT_CYCCNT; uint32_t target_ticks us * us_ticks; uint32_t elapsed_ticks; uint32_t delay_chunk; // 分段延时的“块”大小这里设为1us对应的周期数即每次循环检查1us。 // 这个值可以调整越小则对抢占响应越及时但循环开销略增。 const uint32_t chunk_ticks us_ticks; while (1) { // 计算从开始到现在经过的周期数处理计数器溢出 elapsed_ticks DWT_CYCCNT - start_tick; // 注意此处利用了无符号数溢出的特性即使CYCCNT溢出只要目标时间不超过溢出周期的一半计算就是正确的。 // 对于59秒的溢出周期和us级延时这个假设完全成立。 // 如果总耗时已经达到或超过目标则退出 if (elapsed_ticks target_ticks) { break; } // 计算本次需要等待的小块时间剩余时间与块大小的最小值 delay_chunk target_ticks - elapsed_ticks; if (delay_chunk chunk_ticks) { delay_chunk chunk_ticks; } // 等待这一小块时间 uint32_t chunk_start DWT_CYCCNT; while ((DWT_CYCCNT - chunk_start) delay_chunk) { // 此处可以插入任务YIELD但一般不推荐因为会主动引发调度。 // taskYIELD(); } } }这个实现的关键改进点溢出处理elapsed_ticks DWT_CYCCNT - start_tick;这条语句本身在无符号整数运算下可以正确处理非跨两次溢出的情况即start_tick和DWT_CYCCNT只跨越一次从最大值到0的翻转。对于微秒级延时这绝对安全。抢占适应通过将长延时分解为多个1us的chunk并在每个chunk开始前重新计算已过去的总时间elapsed_ticks我们实现了“超时检查”。即使任务在执行某个chunk的while循环时被长时间抢占当它恢复执行进入下一个while(1)循环时会立刻发现elapsed_ticks target_ticks从而立即退出整个延时函数。这保证了总延时时间至少为设定值可能更多但绝不会少。精度与开销平衡chunk_ticks设置为1us是一个很好的平衡点。检查频率足够高能及时响应超时循环开销又不会太大。重要提示DWT_Init()函数必须在FreeRTOS调度器启动vTaskStartScheduler()之前调用。因为一旦FreeRTOS运行它可能会在任务上下文切换时使用DWT单元尽管不常见提前初始化可以避免冲突。最安全的做法是在main()函数开头初始化了时钟系统之后立即调用。3.4 DWT方案的局限性尽管我们做了增强DWT方案仍有其固有局限依赖内核特性不是所有的Cortex-M芯片都支持DWT单元虽然绝大多数STM32都支持。在移植代码前需要确认。无法提供“绝对精确”的延时它只能保证“不少于”设定的时间。由于任务抢占实际延时可能更长抖动Jitter取决于系统负载。CPU占用这是忙等待Busy-Wait。在延时的整个过程中CPU核心被完全占用无法执行其他任务。因此它只适用于极短时间的延时通常建议在几十微秒以内。如果需要延时几百微秒甚至毫秒级请务必使用vTaskDelay()将CPU让给其他任务。4. 方案二基于通用定时器的独立时钟源当DWT不可用或者你需要一个完全独立、不受任何内核因素影响的精确时钟源时通用定时器是可靠的选择。我们以STM32最基础的TIM2为例。4.1 定时器初始化配置我们目标是让定时器以1MHz的频率计数这样计数器CNT每增加1就代表1us。// gptim_delay.h #ifndef __GPTIM_DELAY_H #define __GPTIM_DELAY_H #include “stm32f1xx_hal.h” // 根据你的芯片系列修改 // 使用TIM2作为高精度延时定时器可根据需要修改 #define DELAY_TIM TIM2 #define DELAY_TIM_CLK_ENABLE __HAL_RCC_TIM2_CLK_ENABLE void GPTIM_Delay_Init(void); void delay_us_tim(uint32_t us); #endif // gptim_delay.c #include “gptim_delay.h” #include “FreeRTOS.h” #include “task.h” // 假设系统主频APB1定时器时钟为72MHz #define DELAY_TIM_PRESCALER (72 - 1) // 预分频值使计数器时钟为1MHz #define DELAY_TIM_PERIOD (0xFFFF - 1) // 自动重装载值16位定时器最大值-1 static volatile uint32_t tim_overflow_count 0; void GPTIM_Delay_Init(void) { TIM_HandleTypeDef htim2 {0}; TIM_ClockConfigTypeDef sClockSourceConfig {0}; // 1. 使能时钟 DELAY_TIM_CLK_ENABLE(); // 2. 配置时基 htim2.Instance DELAY_TIM; htim2.Init.Prescaler DELAY_TIM_PRESCALER; // 72分频72MHz / 72 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period DELAY_TIM_PERIOD; // 自动重装载值 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; // 为了简化溢出处理先禁用预装载 if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } // 3. 配置时钟源为内部时钟 sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig) ! HAL_OK) { Error_Handler(); } // 4. 使能更新中断用于处理计数器溢出 __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); HAL_NVIC_SetPriority(TIM2_IRQn, 5, 0); // 设置一个较低的优先级不要高于FreeRTOS可管理的中断优先级 HAL_NVIC_EnableIRQ(TIM2_IRQn); // 5. 启动定时器 HAL_TIM_Base_Start(htim2); } // TIM2全局中断服务函数需要在stm32f1xx_it.c中实现或声明 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { if (__HAL_TIM_GET_IT_SOURCE(htim2, TIM_IT_UPDATE) ! RESET) { __HAL_TIM_CLEAR_IT(htim2, TIM_IT_UPDATE); tim_overflow_count; // 溢出次数加1 } } }4.2 微秒延时函数实现有了一个持续运行且记录溢出次数的定时器我们就可以实现一个能处理32位甚至64位扩展时间的延时函数。void delay_us_tim(uint32_t us) { if (us 0) return; uint32_t start_overflow; uint32_t start_count; uint32_t current_overflow; uint32_t current_count; uint64_t start_time_us; uint64_t target_time_us; uint64_t current_time_us; // 第一次读取获取起始的溢出次数和计数器值 start_overflow tim_overflow_count; start_count DELAY_TIM-CNT; // 计算起始的绝对时间单位微秒 // 注意此处假设定时器周期为65535每溢出一次代表65536us start_time_us (uint64_t)start_overflow * (DELAY_TIM_PERIOD 1) start_count; // 计算目标绝对时间 target_time_us start_time_us us; // 循环等待直到当前时间 目标时间 do { // 再次读取当前溢出次数和计数器值 current_overflow tim_overflow_count; current_count DELAY_TIM-CNT; // 计算当前的绝对时间 current_time_us (uint64_t)current_overflow * (DELAY_TIM_PERIOD 1) current_count; // 关键处理读取瞬间可能发生的计数器溢出 // 如果在读取current_overflow之后读取current_count之前定时器发生了溢出 // 那么current_count会是一个很小的值新周期的开始而current_overflow是旧的。 // 这会导致计算出的current_time_us远小于实际值。 // 一个简单的修正如果current_count非常小例如小于start_count的一半而我们认为不应该这么小 // 则假设溢出发生在两次读取之间将current_overflow加1。 // 更稳健的方法是使用临界段保护但会增加开销。对于us级延时发生此情况的概率极低。 // 这里采用一个启发式检查 if (current_count (DELAY_TIM_PERIOD / 4) (current_overflow start_overflow)) { // 这是一个粗略的检查在实际高负载系统中可能需要更严谨的保护。 // 为了代码简洁和性能此处仅作提示。 // 更安全的做法是进入临界段taskENTER_CRITICAL/taskEXIT_CRITICAL读取这两个值。 } } while (current_time_us target_time_us); }4.3 通用定时器方案的优缺点分析优点完全独立与FreeRTOS内核、SysTick、DWT均无干扰是最“干净”的方案。高可靠性只要中断优先级设置正确低于configMAX_SYSCALL_INTERRUPT_PRIORITY以确保FreeRTOS API安全其运行非常稳定。功能可扩展该定时器还可以同时用于输入捕获、PWM输出等其他用途只需注意资源共享。缺点与注意事项中断优先级定时器的更新中断优先级必须设置为低于configMAX_SYSCALL_INTERRUPT_PRIORITY如果你使用了configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。这是因为在delay_us_tim函数中我们读取了全局变量tim_overflow_count而这个变量在中断服务程序中被修改。虽然这个简单的volatile变量在大多数情况下是安全的但严格来说在极端情况下如32位机读64位变量可能存在原子性问题。更严谨的做法是在读取时暂时关闭中断但会增加延时函数的不确定性。对于STM3232位机读32位volatile变量通常是原子的因此这个风险很小。资源占用永久占用了一个定时器及其相关中断。潜在竞态条件如上文代码注释所述在do...while循环中分两次读取overflow和CNT存在极小的概率在两次读取之间发生溢出导致时间计算错误。对于微秒级延时这个时间窗口极短概率极低但在要求极高的场合可以使用临界段taskENTER_CRITICAL()/taskEXIT_CRITICAL()将这两次读取保护起来但这会短暂关闭中断可能影响系统实时性需权衡。CPU占用同DWT方案这也是忙等待。5. 实战场景与进阶优化策略掌握了基础实现我们来看看如何在真实项目中应用和优化。5.1 场景一驱动WS2812B RGB LED灯带WS2812B的0码和1码对时序要求极其严格误差通常需要控制在±150ns以内。使用FreeRTOS的vTaskDelay根本不可能。此时必须使用忙等待的delay_us或delay_ns。操作要点使用DWT方案因为需要纳秒级精度DWT是最佳选择。可以编写一个delay_ns(uint32_t ns)函数基于SystemCoreClock计算周期数。提升任务优先级执行WS2812B数据发送的任务应该设置为系统中最高优先级或尽可能高。目的是最大限度地减少在发送关键时序时被其他任务抢占的概率。虽然我们的增强型delay_us能保证最小延时但频繁被抢占会拉长整个数据帧的发送时间可能影响刷新率。关闭中断在发送一整帧数据如控制100个灯需要2400位 * 约1.25us 3ms的过程中如果被其他中断打断仍可能造成时序错误。对于极致的可靠性可以在发送整个数据帧前进入临界段taskENTER_CRITICAL()发送完毕后退出taskEXIT_CRITICAL()。但这会阻塞所有中断包括系统Tick所以时间必须非常短毫秒级否则会影响整个系统的实时性。使用DMAPWM/SPI对于大量LED的控制更专业的做法是使用DMA将颜色数据搬运到PWM产生脉冲波形或SPI用位拼接模拟时序的发送寄存器完全解放CPU。但这属于另一个话题。5.2 场景二超声波传感器HC-SR04的测距HC-SR04需要单片机给出一个至少10us的高电平触发信号然后等待回响高电平并测量其宽度。测量宽度需要微秒级精度。操作要点触发信号使用delay_us(10)产生10us高电平。用DWT或通用定时器方案均可。测量回响切勿在任务中用while循环等待引脚变高/变低来测量时间因为任务可能被抢占导致测量值严重偏大。正确做法是使用定时器的输入捕获功能。配置一个定时器可以和延时用的定时器分开也可以是同一个的另一个通道工作在输入捕获模式。将回响引脚连接到定时器的输入通道。设置上升沿和下降沿捕获。在中断中记录捕获值两者的差值即为高电平时间。这个中断服务程序是硬件触发的与任务调度无关精度极高。任务分工触发和读取结果放在一个低优先级任务中即可。触发后任务可以vTaskDelay一小段时间或等待一个信号量由输入捕获中断给出再去读取计算好的距离值。这样CPU利用率最高。5.3 进阶优化混合延时策略一个好的系统设计应该根据延时长短混合使用不同的策略延时需求推荐方法理由 50 us增强型DWT忙等待 (delay_us)精度高实现简单。短时间忙等待对系统整体影响微乎其微。50 us ~ 1 ms通用定时器忙等待 (delay_us_tim)如果DWT不可用或需要资源隔离。同样短时间忙等待可接受。 1 msvTaskDelay()/vTaskDelayUntil()必须使用此方法。将CPU让出供其他任务运行这是RTOS的核心优势。绝对不要用忙等待延时毫秒级时间在你的应用代码中可以这样组织// my_delay.h #define DELAY_US(us) \ do { \ if ((us) 1000) { \ vTaskDelay(pdMS_TO_TICKS((us) / 1000)); \ delay_us((us) % 1000); /* 处理余下的微秒部分 */ \ } else { \ delay_us(us); \ } \ } while(0)这个宏自动判断大于等于1ms的部分用vTaskDelay小于1ms的部分用高精度delay_us。但请注意vTaskDelay的精度是Tick所以pdMS_TO_TICKS(1)可能实际是1-2个Tick即1-2ms。因此对于需要严格时序的复合延时最好全部用高精度延时或者重新设计任务避免长延时阻塞。6. 调试、验证与常见问题排查实现之后如何验证你的delay_us真的准出了问题怎么查6.1 精度验证方法示波器法最直接写一个测试任务循环操作一个GPIO引脚先拉高调用delay_us(100)再拉低再delay_us(100)。用示波器测量高电平脉宽应该是100us ± 误差。误差主要来源于函数调用开销、循环判断开销以及任务抢占。void test_delay_task(void *pvParameters) { GPIO_InitTypeDef GPIO_InitStruct {0}; // ... 初始化一个GPIO为输出模式比如PA5 while(1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); delay_us(100); // 测试100us延时 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); delay_us(100); // 也可以加一个vTaskDelay(100)让波形间隔大一点方便观察 vTaskDelay(pdMS_TO_TICKS(100)); } }定时器捕获法使用另一个未使用的定时器配置在输入捕获模式测量上述GPIO脉冲的宽度。可以在调试阶段将数据通过串口打印出来。6.2 常见问题与排查清单问题延时时间严重不对比如长了几个数量级。排查检查SystemCoreClock的值是否正确。在SystemInit()和配置完时钟树后这个值应该被更新。如果它还是默认值比如16MHz而实际主频是72MHz那么你的us_ticks计算就错了4.5倍。排查确认DWT是否成功使能。可以在初始化后打印DWT-CYCCNT的值看看它是否在递增。排查对于通用定时器方案检查预分频器PSC和自动重装载值ARR计算是否正确。定时器时钟 APBx时钟 / (PSC 1)。确认定时器是否真的启动了__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE)。问题系统运行一段时间后死机或调度异常。排查首要怀疑对象是SysTick被修改。全局搜索代码看是否有其他地方如旧的裸机延时库、其他第三方库修改了SysTick-LOAD或SysTick-VAL。FreeRTOS启动后这些寄存器是禁区。排查通用定时器中断优先级是否高于configMAX_SYSCALL_INTERRUPT_PRIORITY如果是并且你在该中断中调用了FromISR版本的FreeRTOS API如xQueueSendFromISR可能会导致内核数据损坏。排查DWT或通用定时器的初始化是否在FreeRTOS启动之后才调用确保它们在vTaskStartScheduler()之前初始化。问题使用delay_us后系统响应变慢其他任务好像“卡”了。原因你正在长时间进行忙等待比如delay_us(5000)就是阻塞CPU 5ms。在这5ms内即使有更高优先级的任务就绪也无法被调度除非有中断打断。解决严格遵守“短延时用忙等长延时用vTaskDelay”的原则。重新设计你的任务将长延时操作分解或者使用状态机在等待期间主动调用taskYIELD()但注意在delay_us的循环里加taskYIELD会使其完全失去精度。问题测量脉冲宽度发现值跳动很大。排查你是否在任务中直接用while循环和delay_us测量这受任务调度影响极大。必须使用定时器输入捕获功能来测量外部信号宽度。排查GPIO引脚配置是否正确是否配置为上拉/下拉外部信号是否有毛刺最后记住一个核心原则在RTOS中凡是涉及时间的关键操作硬件永远是比软件循环更可靠的选择。能用定时器硬件模块输入捕获、输出比较、PWM完成的就不要用任务延时去模拟。delay_us这类函数只是一个在硬件资源无法满足时用于填补超短时间空白的工具要谨慎、节制地使用。