STM32霍尔传感器测速与PWM风扇控制:从A3144电路到闭环调速实战
1. 项目缘起:从“风扇不转”到“精准调速”的工程实践
最近在做一个嵌入式小项目,需要用一个STM32单片机去控制一个散热风扇,并且实时监测它的转速。风扇是那种常见的四线PWM调速风扇,带一根转速反馈线。听起来很简单,对吧?不就是给个PWM信号,然后读个脉冲嘛。但真上手了,发现坑还真不少。最开始的方案是用风扇的转速反馈线直接接MCU的GPIO,用输入捕获去测频率。结果发现,在低速或者启动瞬间,信号极其不稳定,干扰大,偶尔还会丢脉冲,导致转速计算完全不准,风扇控制也就成了“玄学调速”。
于是,我想到了霍尔效应传感器。手头正好有常见的A3144这类开关型霍尔传感器,它的原理是利用磁场变化输出高低电平,非常适合检测风扇叶轮上的磁铁。这样一来,转速信号就变成了一个干净、陡峭的方波,MCU处理起来就友好多了。这个项目的核心,就是基于STM32CubeMX和HAL库,搭建一个完整的系统:用定时器输出PWM驱动风扇,用另一个定时器的输入捕获功能来捕捉A3144传感器产生的脉冲,从而计算出实时转速,并可以根据目标转速进行闭环调节。
整个方案涉及STM32CubeMX的图形化配置、定时器PWM模式、输入捕获模式、中断处理以及转速计算算法,是一个综合性的嵌入式外设应用实例。无论你是刚接触STM32的新手,想学习CubeMX和HAL库的实战用法,还是正在为电机测速问题头疼的开发者,这篇从踩坑到填坑的完整记录,或许能给你一些直接的参考。
2. 核心器件选型与电路设计:为什么是A3144?
在动手写代码之前,硬件上的正确连接是基石。这里有几个关键点需要明确,它们直接决定了后续软件逻辑的复杂度和稳定性。
2.1 A3144霍尔传感器特性与工作模式
A3144是一款单极开关型霍尔效应传感器。所谓“单极”,是指它只对单一磁极(通常是S极)敏感。当S极靠近传感器标记面时,其输出管脚(通常为第3脚)会从高电平变为低电平;当磁场移除或减弱到一定阈值以下时,输出恢复高电平。对于“开关型”,意味着它的输出是干净的数字信号(开或关),不像线性霍尔传感器那样输出模拟电压,这大大简化了MCU端的接口设计。
在风扇测速应用中,我们通常在风扇的转子(比如某个扇叶的根部)贴上一小块钕铁硼磁铁。当风扇旋转时,磁铁每经过一次A3144传感器前方,就会产生一个从高到低再到高的脉冲信号。因此,风扇每旋转一圈,A3144就会输出一个完整的脉冲周期。测量这个脉冲的频率或周期,就能反推出风扇的转速。
注意:A3144的工作电压范围通常是4.5V到24V,而STM32的GPIO电平是3.3V。虽然A3144在3.3V供电下也可能工作,但为了确保其稳定性和驱动能力,建议使用5V供电。这就带来了电平转换的问题。
2.2 与STM32的接口电路设计
这是最容易出错的一环。A3144的输出是集电极开路(Open Collector)结构。这意味着它的输出管脚内部相当于一个连接到地的开关三极管,需要外部上拉电阻才能输出高电平。
错误的接法:直接将A3144的输出脚接到STM32的GPIO。当A3144输出低电平时,没问题,是0V。但当它输出高电平时,由于是开漏输出且没有上拉,这个引脚实际上处于浮空状态,STM32读到的可能是随机值,极易受干扰。
正确的接法:
- 电源:给A3144提供稳定的5V电压(Vcc接5V,GND接地)。
- 上拉电阻:在A3144的输出脚和5V电源之间连接一个上拉电阻,阻值通常在1kΩ到10kΩ之间,我常用4.7kΩ。这样,当A3144内部开关断开时,输出被上拉到5V高电平。
- 电平转换:由于上拉到了5V,而STM32的GPIO耐压一般为3.3V(部分引脚为5V容忍,但并非全部),直接连接有损坏MCU的风险。因此需要一个简单的电平转换。最经济可靠的方法是使用一个电压分压电路:在A3144输出端和STM32 GPIO之间串联两个电阻,例如一个1kΩ(接A3144端)和一个2kΩ(接地端),从中间连接点接到STM32。这样5V高电平经过分压后约为3.33V(5V * (2k/(1k+2k))),完美匹配3.3V逻辑。另一种更简洁的方案是,将上拉电阻直接接到STM32的3.3V电源上,而不是5V上。这样A3144输出高时,就是3.3V,可以直接接入STM32。但需要确认A3144在3.3V供电和3.3V上拉下的输出特性是否依然稳定可靠,经实测,多数情况下可以工作,但从器件手册的严谨性出发,分压方案更万无一失。
最终推荐电路:A3144的Vcc接5V,GND接地。OUT引脚通过一个4.7kΩ电阻上拉到5V,再通过一个1kΩ电阻连接到STM32的GPIO,同时在该GPIO引脚对地接一个2kΩ电阻。这样既完成了上拉,又实现了5V到3.3V的安全分压。
2.3 风扇驱动接口
我们控制的是四线PWM风扇,其引脚通常为:电源正(+12V)、电源地(GND)、转速反馈(TACH)、PWM控制(PWM)。STM32负责产生PWM信号,因此需要选择一个支持PWM输出的定时器通道对应的GPIO,连接到风扇的PWM引脚。风扇的电源(+12V)需要由外部电源提供,切勿从STM32取电!STM32的PWM引脚和风扇PWM引脚之间,如果风扇是5V PWM逻辑,可能也需要电平匹配,但多数12V风扇的PWM控制脚兼容3.3V逻辑,具体需查阅风扇数据手册。为简化,我们假设其兼容3.3V。
3. CubeMX工程配置详解:图形化搭建驱动框架
硬件连接好后,我们就可以打开STM32CubeMX,开始软件层面的配置。这里以STM32F103C8T6(蓝色药丸核心板)为例,使用内部RC振荡器(HSI)。
3.1 时钟树配置
虽然使用HSI,但为了得到准确的定时器时钟,建议在时钟配置(Clock Configuration)标签页,将系统时钟(SYSCLK)设置为HSI的倍频,最高到64MHz。确保给我们将要使用的定时器(如TIM1, TIM2, TIM3, TIM4)提供正确的时钟源(通常为APB1或APB2总线时钟)。清晰的时钟树是定时器精准计时的前提。
3.2 定时器1配置为PWM输出模式
假设我们使用高级定时器TIM1的通道1(PA8)来产生PWM驱动风扇。
- 在Pinout视图,找到PA8,将其功能设置为
TIM1_CH1。 - 在左侧分类视图中,找到
TIM1。 - 在
Mode部分,将Channel 1设置为PWM Generation CH1。 - 在
Parameter Settings部分进行关键配置:Prescaler (PSC - 16 bits value):预分频器。定时器时钟 = 系统时钟 / (PSC + 1)。假设系统时钟72MHz,我们希望定时器计数频率为1MHz(便于计算),则PSC = (72MHz / 1MHz) - 1 = 71。Counter Mode:Up(向上计数)。Counter Period (AutoReload Register - 16 bits value):自动重装载值(ARR)。这个值决定了PWM的频率。PWM频率 = 定时器时钟 / (ARR + 1)。如果我们想要一个25kHz的PWM频率(风扇常用的频率,高于人耳可闻范围,无噪音),ARR = (1MHz / 25kHz) - 1 = 39。Internal Clock Division (CKD):No Division。auto-reload preload:Enable。确保在更新ARR时缓冲,防止产生毛刺。- 下方
PWM Generation Channel 1子菜单:Mode:PWM mode 1(在向上计数时,当计数器小于比较寄存器CCR时,通道为有效电平)。Pulse (16 bits value):比较寄存器CCR的初始值。这个值决定了占空比。占空比 = (CCR / (ARR+1)) * 100%。初始可以设一个中间值,比如20。Output compare preload:Enable。同样是为了平滑更新CCR。Fast Mode:Disable。CH Polarity:High。这里需要根据风扇的PWM控制逻辑来定。有些风扇是“高电平有效”(PWM信号为高时风扇转),有些是“低电平有效”。常见的是高电平有效,即占空比越大,转速越高。我们先设为High,如果不符,后续在代码中反转极性或硬件上反接即可。
3.3 定时器2配置为输入捕获模式
假设我们使用通用定时器TIM2的通道1(PA0)来捕获A3144的脉冲信号。选择PA0是因为它通常默认是TIM2_CH1。
- 将PA0功能设置为
TIM2_CH1。 - 配置
TIM2:Mode部分:将Channel 1设置为Input Capture direct mode。Parameter Settings部分:Prescaler (PSC - 16 bits value):同样设置为71,让TIM2的计数时钟也是1MHz,这样捕获到的计数值单位就是1微秒,方便计算。Counter Mode:Up。Counter Period (AutoReload Register):设置为最大值65535(对于16位定时器)。因为我们是测量两个上升沿(或下降沿)之间的时间间隔(周期),只要这个间隔不超过定时器溢出时间即可。1MHz时钟下,65535对应65.535ms,对于转速不低于 (1 / 0.065535) * 60 ≈ 916 RPM的风扇,不会溢出。如果风扇转速可能更低,需要开启定时器溢出中断来处理。auto-reload preload:Disable(输入捕获模式通常不需要)。Internal Clock Division (CKD):No Division。
- 下方
Input Capture Channel 1子菜单:Polarity Selection:Rising Edge。我们选择捕获上升沿,即A3144输出从低(磁铁经过)恢复到高(磁铁离开)的瞬间。也可以选择下降沿,取决于你的磁极和电路初始状态,只要固定一种边沿并保持一致即可。IC Selection:Direct。IC Prescaler:No division。每一个有效边沿都捕获。IC Filter:可以设置一个滤波值,比如4,以消除可能的毛刺干扰。这取决于你的信号质量。
3.4 中断与DMA配置
- TIM2输入捕获中断:在
NVIC Settings标签页,找到TIM2全局中断,勾选Enabled。这样每次捕获到有效边沿时,都会进入中断服务程序,我们可以在这里读取捕获值并计算周期。 - TIM2溢出中断(可选但推荐):在低速测量时,为了防止两次捕获之间定时器溢出导致计算错误,最好也开启定时器更新中断(Update interrupt)。在TIM2的
Parameter Settings中勾选Update interrupt (UIE),并在NVIC中启用它。 - TIM1 PWM无需中断:PWM输出由硬件自动完成,通常不需要中断。
3.5 生成工程代码
配置完成后,点击Project Manager,设置好工程名称、路径、IDE(MDK-ARM V5等)。在Code Generator部分,建议选择“Copy only necessary library files”以节省空间,并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”让代码更模块化。最后点击GENERATE CODE。
4. 关键代码实现:从捕获到计算的完整逻辑
CubeMX生成了基础框架,但核心的测速算法和PWM控制逻辑需要我们自己在main.c或独立的模块文件中实现。
4.1 变量定义与初始化
在/* USER CODE BEGIN PV */区域定义全局变量:
/* Private variables ---------------------------------------------------------*/ // 用于转速计算 volatile uint32_t TIM2_Capture_Value_Last = 0; // 上一次捕获值 volatile uint32_t TIM2_Capture_Value_Current = 0; // 当前捕获值 volatile uint32_t TIM2_Period = 0; // 计算得到的周期(计数值) volatile uint32_t TIM2_Overflow_Count = 0; // 定时器溢出次数 volatile uint8_t TIM2_Capture_Flag = 0; // 捕获完成标志位 // 风扇转速相关 float Fan_RPM = 0.0; // 计算出的转速值 uint8_t Fan_Magnet_Num = 1; // 风扇转一圈产生的脉冲数,通常为1(一块磁铁) // 目标转速与控制 float Target_RPM = 2000.0; // 目标转速 float Current_PWM_Duty = 50.0; // 当前PWM占空比(百分比)在main()函数的/* USER CODE BEGIN 2 */部分,启动定时器和PWM输出:
/* USER CODE BEGIN 2 */ // 启动TIM2的输入捕获和溢出中断 HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_Base_Start_IT(&htim2); // 启动溢出中断 // 启动TIM1的PWM输出 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 设置初始PWM占空比(例如50%) __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (htim1.Init.Period + 1) * Current_PWM_Duty / 100.0); /* USER CODE END 2 */4.2 输入捕获中断服务程序
这是测速的核心。我们需要在stm32f1xx_it.c文件中找到TIM2_IRQHandler函数,并在其中调用HAL库的中断处理回调函数。但更关键的是,我们要实现自己的捕获逻辑。通常,我们会在main.c中重写HAL库的输入捕获回调函数HAL_TIM_IC_CaptureCallback。
在main.c的/* USER CODE BEGIN 4 */区域添加:
/** * @brief 输入捕获中断回调函数 * @param htim: 定时器句柄 */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) // 判断是TIM2 { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // 判断是通道1 { // 读取当前的捕获比较寄存器值 TIM2_Capture_Value_Current = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算周期(考虑定时器溢出的情况) if (TIM2_Capture_Value_Current >= TIM2_Capture_Value_Last) { // 没有发生溢出,或者溢出次数已通过更新中断记录 TIM2_Period = (TIM2_Overflow_Count * 65536) + (TIM2_Capture_Value_Current - TIM2_Capture_Value_Last); } else { // 当前值小于上次值,说明在两次捕获间定时器发生了溢出 // 此时TIM2_Overflow_Count已经记录了溢出次数 TIM2_Period = (TIM2_Overflow_Count * 65536) + ((65536 - TIM2_Capture_Value_Last) + TIM2_Capture_Value_Current); } // 更新上一次捕获值 TIM2_Capture_Value_Last = TIM2_Capture_Value_Current; // 重置溢出计数器(在本次计算后重置,为下一个周期准备) TIM2_Overflow_Count = 0; // 设置捕获完成标志 TIM2_Capture_Flag = 1; } } } /** * @brief 定时器溢出(更新)中断回调函数 * @param htim: 定时器句柄 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 定时器溢出一次,计数器加1 TIM2_Overflow_Count++; // 注意:如果溢出次数过多,可能意味着风扇已停转或转速极低,需要做超时处理 if (TIM2_Overflow_Count > 10) // 例如,超过10次溢出(约655ms)没收到信号 { // 可以在这里将转速设置为0,并采取相应措施(如提高PWM尝试启动) Fan_RPM = 0.0; TIM2_Capture_Flag = 0; // 清除标志,等待新信号 } } }4.3 转速计算与PWM控制逻辑
在main函数的while循环中,我们检查捕获标志,计算转速,并根据目标转速调整PWM。
/* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // 如果捕获完成标志被置位 if (TIM2_Capture_Flag) { // 清除标志 TIM2_Capture_Flag = 0; // 计算转速:RPM = (1 / 周期(秒)) * 60秒 / 每圈脉冲数 // 周期(秒) = TIM2_Period (计数次数) / 定时器时钟频率 (Hz) // 定时器时钟频率我们之前配置为1MHz = 1,000,000 Hz if (TIM2_Period > 0) // 防止除零错误 { float period_in_seconds = (float)TIM2_Period / 1000000.0; Fan_RPM = (1.0 / period_in_seconds) * 60.0 / Fan_Magnet_Num; } else { Fan_RPM = 0.0; } // 简单的比例控制算法(P控制)调整PWM float error = Target_RPM - Fan_RPM; // 比例系数Kp,需要根据实际系统调试,例如0.01 float Kp = 0.01; Current_PWM_Duty += Kp * error; // 限制PWM占空比在合理范围,例如5%到100%(有些风扇需要一定占空比才能启动) if (Current_PWM_Duty < 5.0) Current_PWM_Duty = 5.0; if (Current_PWM_Duty > 100.0) Current_PWM_Duty = 100.0; // 更新PWM输出 uint32_t new_compare_value = (htim1.Init.Period + 1) * Current_PWM_Duty / 100.0; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_compare_value); // 可以通过串口打印转速和占空比进行调试 // printf("RPM: %.1f, Duty: %.1f%%\r\n", Fan_RPM, Current_PWM_Duty); } // 可以添加其他任务,如按键扫描调整目标转速等 HAL_Delay(100); // 主循环延时,避免过于频繁的调整 } /* USER CODE END 3 */5. 调试技巧与常见问题排查
代码写完了,下载到板子,风扇可能不转,或者转速读数乱七八糟。别急,嵌入式开发的大部分时间都在调试。
5.1 硬件信号排查
这是第一步,也是最关键的一步。
- 确认A3144有输出:用示波器或者逻辑分析仪探头,直接测量A3144输出引脚(上拉电阻之后)。手动用磁铁靠近、离开,应该能看到清晰的3.3V(或5V)到0V的跳变。如果没有信号,检查:
- A3144的电源(5V)和地是否接好。
- 磁铁的极性是否正确(需要用S极去触发)。
- 上拉电阻是否接对、阻值是否合适。
- 电平分压电路(如果用了)连接是否正确。
- 确认信号已到达STM32引脚:将示波器探头移到STM32的PA0(或你使用的捕获引脚),重复磁铁动作。看到的波形应该和A3144输出端一致,且高电平在3.3V左右。如果电压不对,检查分压电路计算或连接。
- 确认PWM输出:测量STM32的PWM输出引脚(如PA8),在程序启动后,应该能看到一个频率为25kHz(根据你的配置),占空比可变的方波。如果没有,检查:
- CubeMX中定时器PWM配置是否正确,特别是GPIO复用功能是否开启。
- 代码中
HAL_TIM_PWM_Start是否被调用。 - 用
__HAL_TIM_SET_COMPARE设置的占空比值是否在合理范围内(0到ARR)。
5.2 软件逻辑调试
硬件信号无误后,问题就集中在软件。
- 验证输入捕获是否工作:在
HAL_TIM_IC_CaptureCallback函数内部,通过设置一个GPIO引脚(比如一个LED)翻转,或者通过串口打印一条信息。每当磁铁经过一次,这个LED就应该闪烁一次,或者串口收到一条消息。这能最直接地证明你的中断配置和捕获逻辑是通的。 - 检查周期计算:在回调函数中,将计算出的
TIM2_Period通过串口打印出来。用手匀速转动风扇,这个值应该相对稳定。如果值跳动巨大,可能是:- 中断嵌套或处理时间过长:确保中断服务函数尽可能短,只做最基本的读取和计算。复杂的浮点运算或打印操作放到主循环。
- 滤波未开启:回到CubeMX,将TIM2输入通道的
IC Filter值适当调大(比如4或8),可以滤除信号上的毛刺。 - 定时器时钟配置错误:再次核对
TIM2的预分频器(PSC)设置,确保你理解的定时器计数频率和代码中计算使用的频率一致。
- 转速计算异常:如果周期值看起来合理,但计算出的RPM天差地别。请检查公式:
- 单位换算是否正确?
1,000,000是1MHz的赫兹数。 Fan_Magnet_Num是否正确?如果你贴了2块磁铁,这个值应该是2。- 浮点数运算是否有问题?可以先将
TIM2_Period强制转换为float再参与计算。
- 单位换算是否正确?
- PWM控制无反应或震荡:
- 死区:检查PWM控制极性是否正确。如果风扇是低电平有效,而你配置了高电平有效,那么占空比越大,风扇反而越慢。尝试在CubeMX中反转
CH Polarity,或者在代码中设置占空比为100 - Current_PWM_Duty。 - 控制参数
Kp不合适:Kp太大,系统会剧烈震荡;Kp太小,响应太慢,永远追不上目标转速。需要耐心调试。可以先固定一个PWM占空比,观察稳态转速,然后改变目标值,观察系统响应来调整Kp。 - 风扇启动问题:很多风扇有一个最低启动占空比(比如20%)。低于这个值,风扇无法启动,读到的转速就是0。我们的代码中已将最小占空比限制在5%,你可能需要根据你的风扇手册调整这个值到20%或30%。
- 死区:检查PWM控制极性是否正确。如果风扇是低电平有效,而你配置了高电平有效,那么占空比越大,风扇反而越慢。尝试在CubeMX中反转
5.3 进阶优化考虑
当基本功能跑通后,可以考虑以下优化,让系统更稳健:
- 数字滤波:对计算出的
Fan_RPM进行软件滤波,如一阶低通滤波,可以平滑转速显示,减少跳动。float Filtered_RPM = 0.0; float alpha = 0.2; // 滤波系数,0~1,越小越平滑 Filtered_RPM = alpha * Fan_RPM + (1 - alpha) * Filtered_RPM; - 更高级的控制算法:简单的P控制可能存在静差。可以加入积分(I)项,构成PI控制器,以消除静差。
static float error_integral = 0.0; float Ki = 0.001; // 积分系数 error_integral += error; // 积分限幅,防止积分饱和 if(error_integral > 1000) error_integral = 1000; if(error_integral < -1000) error_integral = -1000; Current_PWM_Duty += Kp * error + Ki * error_integral; - 频率测量法替代周期测量法:对于高速旋转的物体,周期法(测两个边沿的时间)在低速时精度高,但高速时可能因为中断处理来不及而丢沿。另一种方法是频率法:在一个固定的时间窗口(比如1秒)内,统计捕获到的脉冲个数。这可以通过开启定时器更新中断来定义时间窗口,在输入捕获中断里累加脉冲计数器来实现。这种方法在高速时更准确,低速时误差大。可以根据你的转速范围选择或结合使用。
- 处理停转与超低速:代码中我们已经通过溢出中断做了简单的超时判断。更完善的做法是,在主循环里设置一个“最后有效捕获时间”的时间戳,如果超过一定时间(比如1秒)没有新的捕获事件,就判定风扇已停转,将转速置零,并可能触发一个“重启”程序,比如将PWM短暂设为100%再恢复,尝试“踢”一下风扇。