STM32F103定时器输入捕获实现多路风扇转速测量实战指南

📅 2026/7/30 4:34:17 👁️ 阅读次数 📝 编程学习
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)就不再是普通的输出口了,而变成了一个“信号监听员”。

它的工作流程是这样的:

  1. 监听边沿:我们首先告诉定时器,我们要监听什么样的信号变化。对于风扇测速,风扇的霍尔传感器或驱动芯片的TACH(转速计)引脚输出的通常是方波脉冲。我们一般选择“上升沿”或“下降沿”作为捕获触发条件。比如,我们设置捕获上升沿。
  2. 启动秒表:定时器内部有一个计数器(CNT),它在一个稳定的时钟驱动下不停地向上计数。这个时钟源可以是内部系统时钟(经过分频),精度很高。
  3. “抓住”瞬间:当被监听的GPIO引脚上出现一个我们指定的边沿(比如上升沿)时,硬件会立即做两件事:
    • 将当前定时器计数器(CNT)的值,瞬间“拷贝”到一个叫做“捕获/比较寄存器”(CCR)的专属寄存器里。这个动作是硬件自动完成的,速度极快,不受软件中断延迟影响。
    • 可以产生一个“捕获中断”或者触发一个DMA请求,通知CPU:“嘿,我抓住了一个边沿,当时的时刻值保存在CCRx里了!”
  4. 计算时间差:在程序中,我们会在捕获中断里,读取这次保存的CCRx值。如果我们连续捕获两个上升沿,那么用第二次捕获的CCRx值减去第一次捕获的CCRx值,再乘以计数一个数所代表的时间(由定时器时钟频率决定),就得到了这两个上升沿之间的时间间隔,也就是脉冲的周期(T)。
  5. 换算转速:风扇的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风扇)会引出四根线:VCC(+12V或+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的引脚分配图,找到如PA0(TIM2_CH1)、PA1(TIM2_CH2)、PA6(TIM3_CH1)、PB6(TIM4_CH1)等引脚。
  • 多路连接:如果你有4个风扇,就需要4个具有输入捕获功能的引脚。它们可以来自同一个定时器的4个通道(最节省定时器资源),也可以来自多个定时器。规划时要统筹考虑。

3. 滤波与抗干扰:风扇电机是感性负载,启停和PWM调速时会产生较大的电气噪声,可能耦合到TACH信号线上,造成毛刺,导致输入捕获误触发。

  • 硬件滤波:在信号线上串联一个小的磁珠(如600Ω@100MHz),并在靠近MCU引脚处放置一个对地的小电容(如10pF~100pF),可以滤除高频噪声。
  • 软件滤波:STM32的定时器输入通道本身支持数字滤波功能,可以配置为需要连续采样到N次相同的电平才确认有效边沿,这能极大地抑制毛刺。这是我们后续在CubeMX里要配置的一个重要参数。

注意:在面包板或飞线阶段,干扰问题可能不明显,但一旦装入金属机箱,或者靠近电机、电源等噪声源,问题就会暴露。良好的硬件设计是稳定测速的基础,不要指望全部用软件来弥补硬件缺陷。

4. 软件环境搭建与CubeMX配置

我们使用STM32CubeMX进行初始化配置,它能直观地配置时钟、引脚和外设,生成基础代码框架,极大提高效率。这里以使用一个定时器(TIM3)的四个通道同时测量四路风扇为例。

4.1 系统与时钟树配置

  • 选择芯片:在CubeMX中选定你的具体型号,如STM32F103C8T6。
  • 配置时钟(RCC)
    • HSE(高速外部时钟):选择“Crystal/Ceramic Resonator”,如果你的板子有外部晶振(通常8MHz)。
    • 进入Clock Configuration标签页。将HSE输入作为PLL源,通过PLL倍频到72MHz,作为系统时钟(SYSCLK)。APB1总线时钟(定时器2-4的时钟源)最高36MHz,APB2总线时钟最高72MHz。确保定时器的时钟源是你期望的频率。对于TIM2-TIM5,如果APB1预分频器不为1,则定时器时钟是APB1时钟的2倍。这里我们目标是将定时器时钟配置得尽可能高,以提高测量精度,比如达到72MHz。

4.2 定时器(TIM3)输入捕获配置这是最核心的步骤。

  1. 启用TIM3:在Pinout & Configuration标签页左侧,找到Timers->TIM3
  2. 时钟源:选择“Internal Clock”作为定时器的主时钟源。
  3. 分频与周期
    • Prescaler (PSC - 16 bits value):预分频器。定时器实际时钟 = 定时器输入时钟 / (PSC + 1)。为了高精度,我们希望计数频率高。假设定时器时钟为72MHz,如果我们设置PSC=71,则计数频率为 72MHz / (71+1) = 1MHz。即计数器每1微秒加1。
    • Counter Mode:向上计数“Up”。
    • Counter Period (AutoReload Register - 16 bits value):自动重装载值(ARR)。设置计数器从0计数到的最大值。对于16位定时器,最大65535。这里我们设置为最大值65535,让计数器自由运行。也可以设置一个较小的值,配合溢出中断来扩展测量范围,但测速通常周期较短,设为最大值更简单。
    • auto-reload preload:使能。
  4. 配置通道1为输入捕获
    • 点击Channel 1,选择“Input Capture direct mode”。
    • ICPolarity:选择“Rising Edge”,捕获上升沿。
    • IC Selection:选择“Direct”,信号直接连接到捕获单元。
    • IC Prescaler:分频器。选择“No prescaler”,每个边沿都捕获。如果风扇转速很高,脉冲很密集,可以设置每2个或4个边沿捕获一次,以降低中断频率。
    • IC Filter数字滤波器。这是抗干扰的关键!设置一个值,比如“6”。这表示输入信号必须连续8个采样周期保持相同电平,才被认为有效。采样频率由内部时钟决定。这个值能有效滤除窄毛刺。
  5. 重复步骤4:依次将Channel 2,Channel 3,Channel 4都配置为“Input Capture direct mode”,参数类似。这样TIM3的四个通道就独立配置好了。
  6. 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 i=0; i<FAN_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脉冲/转(PPR=2)。

// 在main.c的while(1)循环中 while (1) { // 每隔100ms计算一次转速 HAL_Delay(100); for(int i=0; i<FAN_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); // PPR=2 // 可选:添加滤波,比如简单移动平均 // ... // 输出转速,可以通过串口打印或显示在屏幕上 printf("Fan%d RPM: %.1f\n", i+1, fan[i].rpm); } else { // 没有捕获到有效脉冲,可能风扇停转或转速极低 fan[i].rpm = 0.0f; printf("Fan%d: STOPPED or ERROR\n", i+1); } } }

6. 精度提升、抗干扰与进阶优化

基本的测速功能已经实现,但在实际项目中,我们还需要考虑精度、稳定性和资源占用。

6.1 提高测量精度

  • 提高定时器时钟频率:这是最直接的方法。在时钟树配置中,尽量提高定时器的输入时钟。对于TIM2-TIM5,确保APB1预分频器不为1,以获得倍频后的时钟(最高72MHz)。计数频率越高,测量一个脉冲周期得到的计数值越大,相对误差越小。
  • 使用定时器溢出中断扩展量程:当风扇转速很低时,脉冲周期可能很长,计数器可能会溢出多次。仅仅依靠两次捕获值的差值无法计算。此时,需要开启定时器的更新(溢出)中断(HAL_TIM_Base_Start_IT(&htim3)),并在HAL_TIM_PeriodElapsedCallback()中维护一个溢出计数器。计算周期时,公式变为:总周期 = 溢出次数 * (ARR+1) + 本次捕获值 - 上次捕获值。这能极大地扩展可测量的最低转速范围。
  • 使用输入捕获的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 > 0is_first_capture标志来判断。但更好的做法是结合“超时判断”。可以开启一个基本定时器(如TIM6)产生1秒的中断,在该中断里检查每个风扇的“最后一次捕获时间戳”(在捕获中断里更新)。如果某个风扇的时间戳与当前时间差超过阈值(如1.5秒),则判定其停转。

7. 实测调试与常见问题排查

代码写完了,下载到板子上,风扇也接好了,但转速显示不对或者为0?别急,按照以下步骤排查。

7.1 信号通路确认

  • 示波器/逻辑分析仪是王道:首先用示波器测量风扇TACH引脚的实际波形。确认:
    1. 是否有脉冲输出?风扇是否真的在转?(给PWM信号或直接接VCC)。
    2. 脉冲幅度是否达到MCU可识别的电平(3.3V左右)?
    3. 脉冲是否干净?有无明显毛刺?
    4. 脉冲频率是否与预期转速匹配?(例如,3000 RPM,2 PPR,则频率 = (3000/60)*2 = 100Hz,周期10ms)。
  • 检查电平转换电路:如果使用分压电阻,用万用表测量连接到MCU引脚的实际电压,确保高电平>2.0V(STM32的VIH),低电平<0.8V(VIL)。
  • 检查上拉电阻:确认已启用内部上拉或正确连接了外部上拉电阻。

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引脚,测试你的代码是否能正确测量这个频率。如果信号发生器测试通过,但接风扇不行,那问题肯定出在风扇信号或前端电路上。这种分步排查的思路,能帮你快速定位问题所在。