2026/7/30 6:30:57

STM32定时器PWM输出与输入捕获全解析:从呼吸灯到信号测量

STM32定时器PWM输出与输入捕获全解析:从呼吸灯到信号测量 1. 项目概述从信号生成到信号测量的闭环在嵌入式开发尤其是电机控制、LED调光、舵机驱动这些场景里PWM脉冲宽度调制绝对是绕不开的核心技术。简单说PWM就是通过快速开关来控制一个信号的平均电压或功率开关时间的长短即占空比决定了最终输出的“力度”。但很多朋友在玩转STM32的PWM输出后往往就止步了。你有没有想过如何让单片机不仅能“发出”PWM信号还能“读懂”外部设备发来的PWM信号呢比如你想用遥控器控制自己的小车遥控器发出的就是PWM信号或者你想测量一个风扇的转速风扇的转速传感器输出的往往也是频率和占空比变化的PWM。这时候PWM输入捕获功能就派上用场了。这个项目就是基于STM32的HAL库手把手带你打通PWM的“输出”与“输入”这两个环节。我们会用同一个定时器先把它配置成PWM输出模式驱动一个LED实现呼吸灯效果直观感受占空比的变化。然后我们再把这个定时器重新配置成输入捕获模式专门用来测量外部输入的PWM信号的频率和占空比。通过这一个完整的闭环实验你不仅能掌握HAL库操作定时器的核心API更能深刻理解定时器在不同工作模式下的底层逻辑这对于构建复杂的控制系统至关重要。2. 核心硬件与原理剖析2.1 STM32定时器多面手的核心STM32的定时器Timer功能之强大堪称外设中的“瑞士军刀”。对于PWM相关应用我们主要关注通用定时器如TIM2, TIM3, TIM4, TIM5和高级定时器如TIM1, TIM8。它们都内置了专门的PWM生成和输入捕获电路。PWM输出原理定时器就像一个精准的秒表计数器它从0开始向上计数到我们设定的一个值自动重装载值ARR然后清零重新开始如此循环。我们设定另一个值比较寄存器CCRx当计数器的值小于CCRx时输出高电平大于等于CCRx时输出低电平。这样在一个计数周期内高电平的时间占比即占空比就等于CCRx / (ARR 1)。通过改变CCRx的值我们就能动态调整输出波形的占空比。ARR1决定了PWM的频率频率 定时器时钟源 / (ARR 1)。PWM输入捕获原理这是测量外部PWM信号的高级模式。它需要用到定时器的两个输入通道通常是CH1和CH2并将其配置为“PWM输入模式”。在此模式下两个通道一个如CH1被设置为上升沿触发另一个如CH2被设置为下降沿触发并且它们共同映射到同一个输入信号上。当信号上升沿到来时计数器当前值被捕获到CCR1寄存器并清零计数器当下降沿到来时当前值被捕获到CCR2寄存器。这样CCR1的值就代表了信号的一个完整周期频率的倒数CCR2的值则代表了高电平的时间。通过计算我们就能得到频率和占空比。STM32的硬件直接完成了最关键的边沿捕获和计数器锁存大大减轻了CPU负担并提高了测量精度。2.2 硬件连接规划为了完成这个闭环实验我们需要进行简单的硬件连接。假设我们使用STM32F103C8T6核心板蓝色药丸板这是一个非常普及的型号。PWM输出部分我们选择定时器TIM3的通道1对应引脚PA6作为PWM输出引脚。将一个LED的正极通过一个220Ω的限流电阻连接到PA6负极连接到GND。这样通过改变PWM占空比就能控制LED的亮度。PWM输入部分我们需要一个外部PWM信号源。这里有一个巧妙的“自环”测试方法将刚才的PWM输出引脚PA6用一根杜邦线直接连接到另一个配置为输入捕获的引脚上。我们选择定时器TIM2的通道1对应引脚PA0作为输入捕获引脚。这样单片机自己产生的PWM信号又被自己测量回来非常适合验证功能。注意更真实的场景是PA0连接外部信号源如遥控接收机、传感器等。自环测试时务必确保输出引脚和输入引脚属于不同的定时器因为一个定时器在同一时刻只能工作于一种主模式输出或输入。所需材料清单STM32F103C8T6核心板 1个USB转串口模块用于程序下载和调试打印 1个LED颜色不限 1个220Ω 电阻 1个杜邦线 若干面包板可选方便连接 1块3. 开发环境搭建与工程创建3.1 工具链选择与配置我习惯使用STM32CubeIDE它是ST官方推出的免费集成开发环境集成了CubeMX图形化配置工具和基于Eclipse的IDE对HAL库支持最好一站式解决所有问题。当然你也可以使用Keil MDK或者IAR配合独立的STM32CubeMX软件进行配置步骤是相通的。首先打开STM32CubeIDE新建一个STM32项目。选择你的芯片型号例如STM32F103C8Tx。在项目初始化阶段CubeMX的图形界面会自动打开。3.2 时钟树配置要点系统时钟是定时器精准工作的基石。对于STM32F103我们通常使用外部8MHz晶振HSE通过PLL倍频到72MHz作为系统时钟SYSCLK。在Pinout Configuration标签页转到System Core-RCC。将High Speed Clock (HSE)设置为Crystal/Ceramic Resonator。转到Clock Configuration标签页。你会看到一个复杂的时钟树图。在输入频率部分将HSE设置为8MHz。找到PLL相关设置将PLL源选择为HSEPLL倍频因子设置为9。这样PLL输出就是 8MHz * 9 72MHz。将SYSCLK的来源选择为PLL这样系统时钟就是72MHz。关键一步找到定时器的时钟源APB1和APB2。对于F103TIM2、3、4挂在APB1总线上TIM1挂在APB2上。APB1的预分频器默认是2所以APB1的时钟是36MHz。但是当APB1预分频系数不为1时连接到APB1的定时器会获得一个x2的时钟因此TIM2的时钟实际是72MHz。同理APB2时钟是72MHzTIM1的时钟也是72MHz。这个细节直接影响后续ARR和PSC的计算务必弄清楚。注意很多朋友计算出的PWM频率不对问题十有八九出在时钟树配置上。一定要在Clock Configuration界面确认最终到达你所用定时器的“Timer clocks”频率是多少。CubeMX会帮你计算并显示出来仔细核对。3.3 GPIO与定时器外设初步配置配置PWM输出引脚PA6在芯片引脚图上找到PA6左键点击选择TIM3_CH1。这表示将PA6复用为定时器3的通道1功能。在左侧分类中找到Timers-TIM3。将Clock Source设置为Internal Clock内部时钟源。将Channel1设置为PWM Generation CH1。下方参数配置中Prescaler (PSC - 16 bits value)预分频器设置为71。定时器时钟 72MHz / (711) 1MHz。这个1MHz就是计数器递增的频率。Counter ModeUp向上计数。Counter Period (AutoReload Register - 16 bits value)自动重装载值ARR设置为999。这样PWM周期 (ARR1) / 定时器时钟 1000 / 1MHz 1ms即PWM频率为1kHz。auto-reload preload设置为Enable这可以避免在更新ARR时产生毛刺。切换到Parameter Settings选项卡下的PWM Generation Channel 1子菜单Pulse (16 bits value)脉冲宽度即初始的CCR1值先设为500即50%占空比。CH PolarityHigh表示有效电平为高。即当计数器值小于CCR1时输出高电平。配置PWM输入捕获引脚PA0在芯片引脚图上找到PA0左键点击选择TIM2_CH1。在左侧找到Timers-TIM2。将Clock Source设置为Internal Clock。将Channel1设置为Input Capture direct mode。但先别急这不是PWM输入模式。STM32的“PWM输入模式”是一种特殊的映射模式需要在代码中配置。我们暂时先将其设为直接输入捕获模式用于验证引脚功能。更复杂的PWM输入模式配置我们留在代码部分详细说明。配置串口用于调试打印可选但强烈推荐找到USART1通常PA9为TXPA10为RX将其模式设置为Asynchronous异步通信。在Parameter Settings中设置波特率为115200。配置完成后点击右上角的“GENERATE CODE”让CubeIDE生成初始化代码。4. PWM输出功能实现详解4.1 代码生成与工程结构解析代码生成后我们主要关注三个文件main.c主程序循环所在。tim.c包含了TIM3和TIM2的初始化代码MX_TIM3_Init等。usart.c包含了串口初始化代码。首先在main.c的main函数中在MX_TIM3_Init()函数调用之后我们需要启动PWM输出。/* 启动TIM3的PWM输出 */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);这一行代码至关重要它启动了定时器3的计数器并开启了通道1的PWM输出。没有它引脚上不会有任何波形。4.2 呼吸灯效果实现动态调整占空比我们的目标是让LED像呼吸一样渐亮渐暗。这需要在一个循环中动态地改变CCR1即脉冲宽度的值。在main.c的while(1)主循环中我们添加以下代码uint16_t pwmVal 0; uint8_t dir 1; // 方向1为增加0为减少 while (1) { // 更新PWM占空比 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, pwmVal); // 根据方向更新pwmVal if(dir) { pwmVal; if(pwmVal 1000) { // ARR是999所以最大值为999这里用1000作为上限判断 dir 0; } } else { pwmVal--; if(pwmVal 0) { dir 1; } } // 延时控制呼吸速度 HAL_Delay(1); // 1ms延时这样一次完整的呼吸周期大约2秒 }代码解析__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, pwmVal)这是一个HAL库的宏用于安全地更新通道1的比较寄存器CCR1的值。直接操作htim3.Instance-CCR1在某些情况下可能有问题使用这个宏更稳妥。我们让pwmVal从0递增到999再递减回0循环往复。pwmVal直接作为CCR1的值因此占空比就在0%到100%之间平滑变化。HAL_Delay(1)控制每次变化的间隔。1ms的间隔结合1000个步进完成一次呼吸大约需要2秒。你可以调整这个延时来改变呼吸的快慢。实操心得HAL_Delay()函数依赖于系统滴答定时器Systick它会阻塞整个CPU。在简单的演示中没问题但在实际产品中这种需要精准定时或需要同时处理其他任务的情况应该使用定时器中断来非阻塞地更新PWM值。例如可以配置一个基本定时器如TIM6每1ms产生一次中断在中断服务程序里更新pwmVal和CCR1。4.3 示波器观测与问题排查将代码编译下载到单片机后你应该能看到LED开始呼吸。此时用示波器探头连接到PA6引脚和GND可以观察到标准的1kHz PWM波形其占空比在缓慢地周期性变化。如果看不到波形或波形异常请按以下步骤排查检查硬件连接LED和电阻是否接对PA6是否确实连接到了示波器探头检查时钟配置回顾3.2节确认TIM3的时钟频率是否正确应为72MHz。在tim.c的MX_TIM3_Init函数中检查htim3.Init.Prescaler和htim3.Init.Period的值是否与CubeMX设置一致。检查PWM启动函数确认HAL_TIM_PWM_Start()是否被调用。检查GPIO复用确认PA6是否被正确配置为复用推挽输出AF_PP。在生成的tim.c初始化代码里可以看到GPIO的配置。检查ARR和CCR值确保pwmVal没有超过ARR的值999。超过的话占空比将永远为100%。5. PWM输入捕获功能实现详解接下来是重头戏测量外部PWM信号的频率和占空比。我们将使用TIM2的通道1和通道2来实现完整的PWM输入模式。5.1 输入捕获模式配置解析在CubeMX中我们只将TIM2_CH1设为了直接输入捕获。要实现PWM输入我们需要手动修改代码。PWM输入模式本质上是将输入信号同时映射到两个捕获通道TI1和TI2并配置从模式、触发源和交叉映射。打开tim.c找到MX_TIM2_Init函数。我们需要在HAL库初始化结构体的基础上添加PWM输入模式的配置。修改后的MX_TIM2_Init函数关键部分static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_IC_InitTypeDef sConfigIC {0}; TIM_SlaveConfigTypeDef sSlaveConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; // 预分频72MHz / (711) 1MHz计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFF; // 作为输入捕获ARR设为最大防止溢出 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; //输入捕获通常不使能预装载 if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } // 配置时钟源内部时钟 sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig) ! HAL_OK) { Error_Handler(); } // --- 关键配置PWM输入模式 --- // 1. 配置通道1为输入捕获映射到TI1即PA0引脚 sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿捕获 sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; // 直接映射到TI1 sConfigIC.ICPrescaler TIM_ICPSC_DIV1; // 不分频每个有效边沿都捕获 sConfigIC.ICFilter 0; // 不滤波 if (HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1) ! HAL_OK) { Error_Handler(); } // 2. 配置通道2为输入捕获映射到TI1交叉映射 sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_FALLING; // 下降沿捕获 sConfigIC.ICSelection TIM_ICSELECTION_INDIRECTTI; // 交叉映射映射到TI1 if (HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_2) ! HAL_OK) { Error_Handler(); } // 3. 配置从模式复位模式触发源为TI1FP1 sSlaveConfig.SlaveMode TIM_SLAVEMODE_RESET; sSlaveConfig.InputTrigger TIM_TS_TI1FP1; // 触发信号为经过滤波后的TI1 sSlaveConfig.TriggerPolarity TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿触发复位 sSlaveConfig.TriggerFilter 0; sSlaveConfig.TriggerPrescaler TIM_ICPSC_DIV1; if (HAL_TIM_SlaveConfigSynchro(htim2, sSlaveConfig) ! HAL_OK) { Error_Handler(); } // 主模式输出触发配置本例不需要可省略或保持默认 // sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; // sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; // if (HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig) ! HAL_OK) // { // Error_Handler(); // } }配置逻辑解读通道1TIM_CHANNEL_1配置为在TI1即PA0引脚的上升沿进行捕获捕获值存入CCR1。同时这个上升沿被设置为触发源TIM_TS_TI1FP1。通道2TIM_CHANNEL_2配置为交叉映射到TI1TIM_ICSELECTION_INDIRECTTI在TI1的下降沿进行捕获捕获值存入CCR2。从模式Slave Mode设置为TIM_SLAVEMODE_RESET复位模式。这意味着当触发事件即TI1的上升沿发生时定时器2的计数器CNT会立刻清零。这是实现周期测量的关键。工作流程外部PWM信号从PA0输入。第一个上升沿到来时触发两件事a) 计数器CNT被清零b) 当前CNT值0被捕获到CCR1。然后计数器开始从0递增。当下降沿到来时当前的CNT值被捕获到CCR2。当下一个上升沿到来时CNT再次被清零并被捕获到CCR1此时CCR1中的值就是上一个完整周期的计数值。如此循环。5.2 测量逻辑与代码实现初始化完成后我们需要启动输入捕获并定期读取CCR1和CCR2的值来计算频率和占空比。在main.c的main函数中TIM2初始化后启动输入捕获/* 启动TIM2的输入捕获通道1和通道2 */ HAL_TIM_IC_Start(htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start(htim2, TIM_CHANNEL_2);然后在while(1)循环中可以与呼吸灯代码并存但为了清晰我们先注释掉呼吸灯部分添加测量和打印逻辑uint32_t frequency 0; uint32_t dutyCycle 0; uint32_t ic1Value 0, ic2Value 0; char msg[100]; while (1) { // 读取输入捕获值 ic1Value __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_1); // 周期值 ic2Value __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_2); // 高电平值 // 计算频率和占空比 (定时器时钟为1MHz即每秒计数1,000,000次) if(ic1Value 0) { // 频率 时钟频率 / 一个周期的计数值 frequency 1000000 / ic1Value; // 单位 Hz // 占空比 (高电平计数值 / 周期计数值) * 100% dutyCycle (ic2Value * 100) / ic1Value; } // 通过串口打印结果 sprintf(msg, Freq: %lu Hz, Duty: %lu%%\r\n, frequency, dutyCycle); HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), 100); HAL_Delay(200); // 每200ms打印一次避免串口数据刷屏 }计算原理ic1Value两个上升沿之间的计数值代表信号周期T。定时器计数频率我们之前配置为1MHzF_cnt。因此信号频率F_signal F_cnt / ic1Value。ic2Value上升沿到下降沿之间的计数值代表高电平时间T_high。因此信号占空比Duty (T_high / T) * 100% (ic2Value / ic1Value) * 100%。5.3 自环测试与结果验证现在进行关键的“自环测试”用一根杜邦线将PWM输出引脚PA6连接到PWM输入引脚PA0。将程序下载到单片机记得在main函数里只开启PWM输出和输入捕获暂时注释掉呼吸灯循环里动态改变CCR1的代码让PWM输出固定占空比比如50%。打开串口助手如Putty、SecureCRT等设置正确的串口号和波特率115200。你应该能看到串口持续打印出测量结果。由于PA6输出的是1kHz1000Hz、50%占空比的PWM波串口打印的结果应该非常接近“Freq: 1000 Hz, Duty: 50%”。可能会存在微小误差这是由于定时器时钟分频、中断处理延迟等造成的在允许范围内。测试进阶重新启用呼吸灯的动态占空比代码。此时PA6输出的PWM频率固定为1kHz但占空比在0%~100%之间变化。观察串口打印你会发现频率值基本稳定在1000Hz左右而占空比值会跟随呼吸灯节奏从0到100再回到0循环变化。这就完美验证了PWM输入捕获功能的正确性。6. 高级话题与深度优化6.1 高精度测量与溢出处理上面的例子中我们将定时器的ARR设置为最大值0xFFFF65535计数频率为1MHz。这意味着我们能无溢出测量的最长周期是 65535 / 1000000 ≈ 65.5ms对应最低频率约为15.26Hz。如果输入信号频率低于此值计数器就会在下一个上升沿到来前溢出归零从0重新开始导致测量错误。解决方案降低计数频率增大预分频器PSC的值。例如将PSC设为7199则计数频率降为72MHz / 7200 10kHz。此时最长可测周期为6.55秒最低频率0.15Hz。但这会降低测量分辨率。使用定时器溢出中断开启定时器的更新溢出中断。在中断服务函数中对一个全局变量如overflowCount进行加一操作。在计算周期时总计数 overflowCount * (ARR 1) ic1Value。这样可以极大地扩展测量范围但编程复杂度增加。使用32位定时器如果你的STM32型号拥有32位通用定时器如TIM2/TIM5在某些系列是32位的直接使用它其最大计数值约42.9亿在1MHz计数频率下可测量长达71分钟的单周期完全不用担心溢出。实操心得在项目规划阶段就要根据待测信号的频率范围来选择合适的定时器和配置参数。对于频率变化范围大的应用如测速溢出中断方案是更稳健的选择。在中断服务程序里记得要清除更新中断标志位__HAL_TIM_CLEAR_IT(htimx, TIM_IT_UPDATE)。6.2 抗干扰与数字滤波配置在实际工业环境中输入信号可能带有毛刺噪声导致错误的边沿触发和捕获值。STM32的输入捕获通道提供了数字滤波器ICFilter功能。在TIM_IC_InitTypeDef结构体中ICFilter可以设置为0到15之间的值。它不是一个简单的软件延时而是基于采样时钟的硬件滤波器。例如当ICFilter设为4时输入信号会被以f_DTS由ClockDivision决定的频率采样只有当连续4个采样点都是有效电平时边沿检测才被确认。配置建议对于低速、噪声明显的信号可以适当增加ICFilter值如设为6或8。对于高速信号过大的滤波值可能导致边沿检测延迟甚至错过窄脉冲此时应设为0或较小的值如2。f_DTS的频率可以在ClockDivision中设置。TIM_CLOCKDIVISION_DIV1表示f_DTS f_CK_INT定时器输入时钟。分频后可以降低采样频率配合ICFilter实现更宽的滤波窗口。6.3 使用DMA传输捕获数据在需要连续高速测量PWM参数例如用于高性能闭环控制时频繁进入中断读取CCR寄存器会增加CPU开销并可能丢失数据。此时可以使用DMA直接存储器访问功能。STM32的定时器支持在捕获事件发生时自动将CCR寄存器的值通过DMA传输到指定的内存数组中。你可以配置DMA循环模式连续存储多个周期的捕获值。然后CPU只需要定期比如每100ms去处理这块内存中的数据即可计算平均频率和占空比或者进行更复杂的分析。配置步骤概要在CubeMX中启用TIM2的通道1和/或通道2的DMA请求。配置DMA为循环模式从外设TIM2-CCR1到内存。在代码中启动DMA传输HAL_TIM_IC_Start_DMA(htim2, TIM_CHANNEL_1, buffer, BUFFER_SIZE)。可以开启DMA半传输完成和传输完成中断在中断中处理数据。这种方法将CPU从频繁的捕获中断中解放出来极大地提高了系统效率和实时性。7. 常见问题排查与调试技巧在实际操作中你可能会遇到各种问题。下面是一个快速排查指南现象可能原因排查步骤与解决方案PWM无输出1. GPIO未正确复用。2. 定时器未启动。3. 时钟未使能。1. 检查tim.c中GPIO初始化代码模式应为GPIO_MODE_AF_PP。2. 确认HAL_TIM_PWM_Start()已被调用。3. 在main()开头使用__HAL_RCC_TIM3_CLK_ENABLE()显式开启时钟CubeMX通常已做。PWM频率不对1. 系统时钟配置错误。2. PSC或ARR计算错误。1. 使用CubeMX的Clock Configuration界面反复核对或用示波器测量一个已知频率的引脚如MCO输出。2. 复核公式F_pwm F_tim_clk / ((PSC1)*(ARR1))。输入捕获值始终为01. 输入引脚配置错误。2. 输入信号电压不匹配。3. 从模式或触发源配置错误。1. 确认引脚配置为浮空输入或上拉输入且复用功能正确。2. 确保输入信号是标准的3.3V电平用示波器确认信号已到达引脚。3. 仔细检查5.1节中的从模式(SlaveMode)和触发源(InputTrigger)配置必须为RESET模式和TI1FP1/TI2FP2。捕获值跳动大不稳定1. 信号有噪声。2. 计数器溢出。3. 中断或主循环处理太慢。1. 尝试增加输入捕获滤波器的值(ICFilter)。在信号线上加一个小电容如10-100pF到地。2. 测量低频率信号时确保ARR足够大或启用溢出计数。3. 优化代码避免在中断服务程序中进行复杂运算。考虑使用DMA。占空比测量接近100%或0%1. 输入捕获极性配置反了。2. 通道映射错误。1. 检查sConfigIC.ICPolarity通道1应为RISING通道2应为FALLING。2. 检查sConfigIC.ICSelection通道1应为DIRECTTI通道2应为INDIRECTTI。串口无打印数据1. 串口引脚接错。2. 波特率不匹配。3. 代码未进入打印循环。1. 检查TX/RX线是否接反USB转串口模块是否驱动正常。2. 确认代码和串口助手设置的波特率、数据位、停止位、校验位完全一致。3. 在sprintf前加一个LED闪烁或设置一个GPIO翻转确认程序运行到了这里。调试技巧善用仿真器如果使用ST-Link等调试器可以在IDE中设置断点实时查看CCR1、CCR2、CNT等寄存器的值非常直观。GPIO翻转调试法在关键代码段如中断服务程序开始和结束处用一条HAL_GPIO_TogglePin()语句翻转一个空闲的GPIO引脚然后用示波器观察该引脚波形可以精确测量代码段的执行时间判断是否超时。分步验证不要试图一次性写完所有功能。先确保PWM输出正常再用信号发生器给输入引脚一个标准方波验证输入捕获功能最后再做“自环”或连接真实信号源。