STM32F103定时器输入捕获实现多路风扇转速测量实战指南
1. 项目缘起为什么需要多路风扇测速在嵌入式开发尤其是涉及散热控制、环境监控或者设备状态诊断的项目里风扇转速是一个关键参数。它直接反映了散热效率、系统负载甚至是设备健康度。比如一个工控机柜里可能有多个风扇分别给CPU、电源模块、硬盘阵列散热我们需要同时监控它们的转速一旦某个风扇停转或转速异常就要立刻报警防止设备过热损坏。STM32F103作为一款经典的ARM Cortex-M3内核微控制器以其丰富的外设和极高的性价比在工控、消费电子等领域应用广泛。它内置了多个高级/通用定时器这些定时器自带的“输入捕获”功能正是实现高精度脉冲信号测量的利器。相比于用外部中断配合软件计时或者用ADC去测量风扇的模拟电压霍尔传感器输出通常是脉冲输入捕获是硬件级别的测量不占用CPU时间精度高抗干扰能力强特别适合这种需要实时、多路监测的场景。我最近就在一个数据采集终端项目里遇到了这个需求需要同时监测4个机箱风扇的转速。最初尝试用外部中断发现当风扇转速较高比如超过3000 RPM且多路同时工作时频繁的中断严重影响了主程序的实时性。后来切换到定时器的输入捕获模式问题迎刃而解。这篇文章我就来详细拆解一下如何基于STM32F103利用其定时器的输入捕获功能稳定、高效地实现多路风扇的转速测量。整个过程会从原理、硬件连接、CubeMX配置、代码实现到实际调试中的坑点一一讲清楚。2. 核心原理定时器输入捕获如何“抓住”脉冲在动手写代码之前我们必须先搞明白定时器的输入捕获到底是怎么工作的。这决定了我们后续的配置逻辑和代码写法。你可以把它想象成一个高度专业化的“秒表”团队。一个STM32的通用定时器如TIM2, TIM3, TIM4通常有4个独立的通道CH1-CH4。每个通道都可以被配置为输入捕获模式。在这个模式下该通道对应的GPIO引脚比如TIM2_CH1对应PA0就不再是普通的输出口了而变成了一个“信号监听员”。它的工作流程是这样的监听边沿我们首先告诉定时器我们要监听什么样的信号变化。对于风扇测速风扇的霍尔传感器或驱动芯片的TACH转速计引脚输出的通常是方波脉冲。我们一般选择“上升沿”或“下降沿”作为捕获触发条件。比如我们设置捕获上升沿。启动秒表定时器内部有一个计数器CNT它在一个稳定的时钟驱动下不停地向上计数。这个时钟源可以是内部系统时钟经过分频精度很高。“抓住”瞬间当被监听的GPIO引脚上出现一个我们指定的边沿比如上升沿时硬件会立即做两件事将当前定时器计数器CNT的值瞬间“拷贝”到一个叫做“捕获/比较寄存器”CCR的专属寄存器里。这个动作是硬件自动完成的速度极快不受软件中断延迟影响。可以产生一个“捕获中断”或者触发一个DMA请求通知CPU“嘿我抓住了一个边沿当时的时刻值保存在CCRx里了”计算时间差在程序中我们会在捕获中断里读取这次保存的CCRx值。如果我们连续捕获两个上升沿那么用第二次捕获的CCRx值减去第一次捕获的CCRx值再乘以计数一个数所代表的时间由定时器时钟频率决定就得到了这两个上升沿之间的时间间隔也就是脉冲的周期T。换算转速风扇的TACH信号通常是每转输出2个或4个脉冲具体看风扇型号两线制风扇常见是2个脉冲/转。假设是2 Pulses Per Revolution (PPR)。那么测量到的周期T就是一个脉冲的宽度。一转包含2个脉冲所以一转的时间是 2 * T。那么转速RPM转/分钟 60 / (2 * T)。公式为RPM 60 / (PPR * T)其中T的单位是秒。这里有一个关键点定时器计数器是循环计数的。比如一个16位的定时器最大值是65535计到65535后下一个数就回到0重新开始。因此在计算两个捕获值之间的差值时必须考虑计数器溢出的情况。如果第二次捕获值比第一次小我们就要加上一个周期65536再相减。这是输入捕获编程中的一个经典坑点后面代码部分会重点处理。理解了原理我们就知道要实现多路测速本质上就是配置多个定时器通道可以是同一个定时器的不同通道也可以是不同定时器让它们各自独立地“抓住”对应风扇引脚上的脉冲边沿。3. 硬件设计与连接避开噪声与干扰原理通了接下来看硬件。这一步如果没做好软件调得再完美也白搭。风扇测速的硬件连接看似简单但细节决定成败。1. 风扇接口选择常见的4线PWM风扇如电脑CPU风扇会引出四根线VCC12V或5V、GND、PWM_IN控制转速、TACH_OUT转速反馈。我们关注的就是TACH_OUT这根线。它是一个开漏Open-Drain或推挽输出会输出与转速成比例的方波脉冲。两线制风扇则只有VCC和GND其转速信息是通过测量电流纹波等方式间接获取不适用此方法本文讨论的是带TACH信号线的风扇。2. 与STM32的连接信号电平匹配风扇TACH信号通常是5V或12V电平而STM32F103的GPIO引脚绝大多数是3.3V容忍不能直接接入5V。必须进行电平转换最简单的办法是使用一个分压电阻网络例如将风扇的TACH信号通过一个1kΩ电阻连接到STM32引脚同时在该引脚到地之间接一个2kΩ电阻这样5V信号就会被分压到约3.3V。更稳妥的方法是使用电平转换芯片如TXS0108E或光耦进行隔离尤其在工业噪声环境下。上拉电阻由于风扇TACH输出通常是开漏结构必须在STM32的输入引脚上启用内部上拉电阻或者外接一个上拉电阻如4.7kΩ到3.3V以确保信号在空闲时有确定的高电平状态。GPIO引脚选择必须连接到具有定时器输入捕获功能的引脚上。不能随便选一个IO口。需要查阅STM32F103的数据手册或CubeMX的引脚分配图找到如PA0TIM2_CH1、PA1TIM2_CH2、PA6TIM3_CH1、PB6TIM4_CH1等引脚。多路连接如果你有4个风扇就需要4个具有输入捕获功能的引脚。它们可以来自同一个定时器的4个通道最节省定时器资源也可以来自多个定时器。规划时要统筹考虑。3. 滤波与抗干扰风扇电机是感性负载启停和PWM调速时会产生较大的电气噪声可能耦合到TACH信号线上造成毛刺导致输入捕获误触发。硬件滤波在信号线上串联一个小的磁珠如600Ω100MHz并在靠近MCU引脚处放置一个对地的小电容如10pF~100pF可以滤除高频噪声。软件滤波STM32的定时器输入通道本身支持数字滤波功能可以配置为需要连续采样到N次相同的电平才确认有效边沿这能极大地抑制毛刺。这是我们后续在CubeMX里要配置的一个重要参数。注意在面包板或飞线阶段干扰问题可能不明显但一旦装入金属机箱或者靠近电机、电源等噪声源问题就会暴露。良好的硬件设计是稳定测速的基础不要指望全部用软件来弥补硬件缺陷。4. 软件环境搭建与CubeMX配置我们使用STM32CubeMX进行初始化配置它能直观地配置时钟、引脚和外设生成基础代码框架极大提高效率。这里以使用一个定时器TIM3的四个通道同时测量四路风扇为例。4.1 系统与时钟树配置选择芯片在CubeMX中选定你的具体型号如STM32F103C8T6。配置时钟RCCHSE高速外部时钟选择“Crystal/Ceramic Resonator”如果你的板子有外部晶振通常8MHz。进入Clock Configuration标签页。将HSE输入作为PLL源通过PLL倍频到72MHz作为系统时钟SYSCLK。APB1总线时钟定时器2-4的时钟源最高36MHzAPB2总线时钟最高72MHz。确保定时器的时钟源是你期望的频率。对于TIM2-TIM5如果APB1预分频器不为1则定时器时钟是APB1时钟的2倍。这里我们目标是将定时器时钟配置得尽可能高以提高测量精度比如达到72MHz。4.2 定时器TIM3输入捕获配置这是最核心的步骤。启用TIM3在Pinout Configuration标签页左侧找到Timers-TIM3。时钟源选择“Internal Clock”作为定时器的主时钟源。分频与周期Prescaler (PSC - 16 bits value)预分频器。定时器实际时钟 定时器输入时钟 / (PSC 1)。为了高精度我们希望计数频率高。假设定时器时钟为72MHz如果我们设置PSC71则计数频率为 72MHz / (711) 1MHz。即计数器每1微秒加1。Counter Mode向上计数“Up”。Counter Period (AutoReload Register - 16 bits value)自动重装载值ARR。设置计数器从0计数到的最大值。对于16位定时器最大65535。这里我们设置为最大值65535让计数器自由运行。也可以设置一个较小的值配合溢出中断来扩展测量范围但测速通常周期较短设为最大值更简单。auto-reload preload使能。配置通道1为输入捕获点击Channel 1选择“Input Capture direct mode”。ICPolarity选择“Rising Edge”捕获上升沿。IC Selection选择“Direct”信号直接连接到捕获单元。IC Prescaler分频器。选择“No prescaler”每个边沿都捕获。如果风扇转速很高脉冲很密集可以设置每2个或4个边沿捕获一次以降低中断频率。IC Filter数字滤波器。这是抗干扰的关键设置一个值比如“6”。这表示输入信号必须连续8个采样周期保持相同电平才被认为有效。采样频率由内部时钟决定。这个值能有效滤除窄毛刺。重复步骤4依次将Channel 2,Channel 3,Channel 4都配置为“Input Capture direct mode”参数类似。这样TIM3的四个通道就独立配置好了。NVIC设置在NVIC Settings中勾选“TIM3 global interrupt”使能定时器全局中断。同时为了能在捕获事件时及时响应我们需要使能捕获中断。更关键的是在Code Generation标签页找到TIM3的配置确保“Generate IRQ handler”和“Enable interrupt for all TIM3 events”被勾选。这样CubeMX才会为我们生成TIM3_IRQHandler中断服务函数并在其中处理包括捕获中断在内的各种事件。4.3 GPIO引脚检查配置完成后在图形化界面上你会看到PA6, PA7, PB0, PB1这是TIM3 CH1-CH4的默认引脚具体以你的芯片为准被自动配置为复用功能Alternate Function并且旁边有“TIM3_CHx”的标识。确认无误即可。4.4 生成代码点击Project Manager设置好项目名称、路径、IDE如MDK-ARM V5。在Code Generator里选择“Copy only necessary library files”以节省空间。最后点击“GENERATE CODE”生成初始化代码。5. 代码实现从捕获到转速计算CubeMX生成了硬件初始化代码HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1)等我们需要在工程中添加应用逻辑。核心任务是在中断服务函数中处理捕获事件并计算转速。5.1 定义全局变量与结构体为了管理多路风扇的数据我们最好定义一个结构体。// 在main.c开头或单独的头文件中定义 typedef struct { uint32_t last_capture_value; // 上一次捕获的计数器值 uint32_t capture_period; // 计算出的脉冲周期计数器差值 uint8_t is_first_capture; // 是否是第一次捕获的标志 float rpm; // 计算出的转速 } Fan_Speed_t; #define FAN_NUM 4 Fan_Speed_t fan[FAN_NUM]; // 四路风扇数据5.2 启动输入捕获在main()函数的初始化部分在MX_TIM3_Init()调用之后启动所有通道的输入捕获并开启中断。// 启动TIM3的四个通道的输入捕获中断模式 HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_2); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_3); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_4); // 初始化风扇数据结构 for(int i0; iFAN_NUM; i) { fan[i].last_capture_value 0; fan[i].capture_period 0; fan[i].is_first_capture 1; fan[i].rpm 0.0f; }5.3 编写中断回调函数HAL库采用了回调函数机制。当发生捕获中断时会调用HAL_TIM_IC_CaptureCallback()函数。我们需要重写这个函数。在main.c的/* USER CODE BEGIN 4 */和/* USER CODE END 4 */之间或者在一个独立的用户文件中添加以下代码// 重写输入捕获中断回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { uint32_t current_capture; uint8_t fan_index 0; // 判断是哪个定时器的哪个通道触发了中断 if (htim-Instance TIM3) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { fan_index 0; current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); } else if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_2) { fan_index 1; current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); } else if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_3) { fan_index 2; current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_3); } else if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_4) { fan_index 3; current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_4); } else { return; // 不是我们关心的通道 } // 处理捕获值计算周期 Fan_Speed_t *pFan fan[fan_index]; if (pFan-is_first_capture) { // 第一次捕获只记录值不计算 pFan-last_capture_value current_capture; pFan-is_first_capture 0; } else { // 计算两次捕获之间的计数器差值处理溢出 uint32_t diff; if (current_capture pFan-last_capture_value) { diff current_capture - pFan-last_capture_value; } else { // 计数器溢出从最大值绕回0 diff (0xFFFF - pFan-last_capture_value) current_capture 1; // 1是因为从0开始 // 更通用的写法diff current_capture (htim-Instance-ARR - pFan-last_capture_value) 1; // 因为我们ARR设置的是65535等同于0xFFFF } // 保存本次捕获值用于下次计算 pFan-last_capture_value current_capture; // 记录周期单位定时器计数次数 pFan-capture_period diff; } } }5.4 在主循环中计算并输出转速中断回调函数只负责记录脉冲周期计数器差值。我们将转速计算放在主循环或定时任务中避免在中断中进行浮点运算等耗时操作。假设我们配置的定时器计数频率是1MHz每计数一次代表1微秒风扇是2脉冲/转PPR2。// 在main.c的while(1)循环中 while (1) { // 每隔100ms计算一次转速 HAL_Delay(100); for(int i0; iFAN_NUM; i) { if(fan[i].capture_period 0 !fan[i].is_first_capture) { // 计算一个脉冲的周期单位秒 // capture_period 是计数次数 1MHz时钟下每次计数1e-6秒 float period_sec (float)fan[i].capture_period / 1000000.0f; // 计算转速 RPM 60 / (PPR * T) fan[i].rpm 60.0f / (2.0f * period_sec); // PPR2 // 可选添加滤波比如简单移动平均 // ... // 输出转速可以通过串口打印或显示在屏幕上 printf(Fan%d RPM: %.1f\n, i1, fan[i].rpm); } else { // 没有捕获到有效脉冲可能风扇停转或转速极低 fan[i].rpm 0.0f; printf(Fan%d: STOPPED or ERROR\n, i1); } } }6. 精度提升、抗干扰与进阶优化基本的测速功能已经实现但在实际项目中我们还需要考虑精度、稳定性和资源占用。6.1 提高测量精度提高定时器时钟频率这是最直接的方法。在时钟树配置中尽量提高定时器的输入时钟。对于TIM2-TIM5确保APB1预分频器不为1以获得倍频后的时钟最高72MHz。计数频率越高测量一个脉冲周期得到的计数值越大相对误差越小。使用定时器溢出中断扩展量程当风扇转速很低时脉冲周期可能很长计数器可能会溢出多次。仅仅依靠两次捕获值的差值无法计算。此时需要开启定时器的更新溢出中断HAL_TIM_Base_Start_IT(htim3)并在HAL_TIM_PeriodElapsedCallback()中维护一个溢出计数器。计算周期时公式变为总周期 溢出次数 * (ARR1) 本次捕获值 - 上次捕获值。这能极大地扩展可测量的最低转速范围。使用输入捕获的PWM输入模式如果风扇信号非常规整可以考虑使用一个定时器的两个通道如CH1和CH2设置为“PWM输入模式”。该模式能自动测量高电平时间和周期硬件自动完成更省心但一个定时器只能测量一路信号。6.2 软件抗干扰与滤波硬件滤波是基础软件滤波是补充。数字滤波器配置如前所述CubeMX中的IC Filter参数是首要的软件抗干扰手段。根据噪声情况调整这个值。周期值软件滤波在计算RPM前对capture_period进行软件滤波。例如限幅滤波如果本次计算的周期值与上次值相差超过某个阈值比如±20%则认为可能是干扰脉冲舍弃本次值使用上次值。滑动平均滤波维护一个周期值的历史数组每次取平均值。这能有效平滑转速显示但会引入延迟。filtered_period (old_period * 3 new_period) / 4。“无信号”超时判断在中断回调或主循环中增加一个超时机制。如果某个通道超过一定时间比如2秒没有发生捕获中断则判定该风扇停转将RPM强制设为0并报警。6.3 多路与资源优化单定时器多通道本文示例使用了TIM3的四个通道。这是最节省定时器资源的方式。需要注意同一个定时器的所有通道共享一个计数器CNT和预分频器PSC但各有独立的捕获比较寄存器CCRx。因此它们的测量基准时钟是完全同步的精度一致。多定时器如果一路风扇需要非常高的测量精度或特殊模式如PWM输入而其他路不需要可以分配不同的定时器。DMA方式对于转速极高、需要连续记录大量脉冲时间的场景频繁的中断可能成为负担。可以配置输入捕获触发DMA将捕获值自动搬运到内存数组中然后由主程序批量处理。这能极大减轻CPU中断负载。6.4 处理极低转速与零速当风扇完全停止时没有脉冲信号也就不会进入捕获中断。我们的代码中通过capture_period 0和is_first_capture标志来判断。但更好的做法是结合“超时判断”。可以开启一个基本定时器如TIM6产生1秒的中断在该中断里检查每个风扇的“最后一次捕获时间戳”在捕获中断里更新。如果某个风扇的时间戳与当前时间差超过阈值如1.5秒则判定其停转。7. 实测调试与常见问题排查代码写完了下载到板子上风扇也接好了但转速显示不对或者为0别急按照以下步骤排查。7.1 信号通路确认示波器/逻辑分析仪是王道首先用示波器测量风扇TACH引脚的实际波形。确认是否有脉冲输出风扇是否真的在转给PWM信号或直接接VCC。脉冲幅度是否达到MCU可识别的电平3.3V左右脉冲是否干净有无明显毛刺脉冲频率是否与预期转速匹配例如3000 RPM2 PPR则频率 (3000/60)*2 100Hz周期10ms。检查电平转换电路如果使用分压电阻用万用表测量连接到MCU引脚的实际电压确保高电平2.0VSTM32的VIH低电平0.8VVIL。检查上拉电阻确认已启用内部上拉或正确连接了外部上拉电阻。7.2 软件配置与代码排查GPIO复用功能确认CubeMX生成的代码正确配置了引脚为复用模式而不是普通的输入/输出。中断优先级与使能确认NVIC中TIM3的中断已使能并且优先级设置合理不要被更高优先级中断长时间阻塞。中断服务函数确认TIM3_IRQHandler函数存在并且内部调用了HAL_TIM_IRQHandler(htim3)。这个函数是HAL库处理所有定时器中断事件的中枢它会根据事件类型调用对应的回调函数如我们的HAL_TIM_IC_CaptureCallback。回调函数是否被调用在HAL_TIM_IC_CaptureCallback函数开头加一个翻转LED的代码或者通过串口打印一个字符。当风扇转动时观察LED是否闪烁或串口是否有输出。如果没有说明中断根本没进来。捕获值打印在回调函数中直接将current_capture值通过串口打印出来。观察这个值是否在规律变化。如果值不变可能是信号没进来如果值变化但无规律可能是干扰或滤波器设置不当。计数器溢出处理如果转速显示值周期性跳动或完全错误检查代码中对计数器溢出的处理是否正确。可以故意让定时器周期ARR设小一点比如1000让溢出频繁发生来测试你的溢出处理逻辑是否正确。7.3 性能与稳定性测试多路同时工作让所有风扇以不同转速运行观察各路测量值是否独立、准确。检查是否有某一路的测量会影响其他路通常不会因为硬件独立。转速突变测试突然改变某一路风扇的PWM占空比观察软件测得的转速响应是否快速、平滑。软件滤波会引入延迟需要根据实际需求权衡。长时间运行让系统连续运行数小时观察是否有数据跳变、死机或内存泄漏如果用了动态滤波数组。确保中断处理和主循环计算中没有阻塞操作。我个人的经验是输入捕获测速的稳定性七分靠硬件信号质量三分靠软件滤波与处理。第一次调试时务必用示波器确认信号波形。遇到问题先隔离用信号发生器产生一个已知频率的方波接到MCU引脚测试你的代码是否能正确测量这个频率。如果信号发生器测试通过但接风扇不行那问题肯定出在风扇信号或前端电路上。这种分步排查的思路能帮你快速定位问题所在。