简介:这份资源是面向嵌入式初学者与STM32开发者的霍尔传感器实战DEMO,基于STM32F103微控制器设计,帮助读者理解磁场检测、位置与速度传感等场景下霍尔传感器的驱动与信号处理思路。压缩包共407个文件,约6.66MB,以C源码与头文件为主体,包含70个c、68个h文件,并配有Keil工程文件(uvprojx、uvoptx)、编译产物(hex、axf、map、lst)及若干汇编与说明文档,目录中可见红外接收、电机、LCD驱动、蓝牙、PS2、PID、CAN、串口等模块,便于按功能拆分学习。目前已有499人学习下载。资源覆盖霍尔传感器工作原理、STM32F103外设接口使用及完整开发流程,读者可据此掌握GPIO读取、中断与轮询机制、信号调理与串口调试等关键环节,并以此为模板扩展计数、测速等实际应用,适合作为课程设计、毕业设计或自学练手的参考工程。
1. 霍尔传感器 DEMO 在开发板上跑通:从“灯闪一下”到能测转速的那条最短路径
很多人第一次把霍尔传感器接到开发板上,烧完 DEMO 程序,盯着串口助手看半天,只看到一行行跳动的数字,心里其实没底——这玩意儿到底算不算跑通了?我见过太多人卡在这一步:代码编译过了,板子也亮了,但传感器一靠近磁铁,数据要么纹丝不动,要么乱跳得像玄学。问题往往不在传感器本身,而在于你没搞清楚这个 DEMO 到底在验证什么。
霍尔传感器实例 DEMO 的核心目标只有一个:把磁场变化转换成开发板能读到的电平跳变,再通过中断或轮询把它变成可观测的事件。它解决的是“物理世界到数字世界”的第一道接口问题,适合刚拿到开发板、想验证 GPIO 输入和外部中断是否正常工作的嵌入式新手,也适合需要快速评估霍尔器件选型是否匹配自己电机测速场景的工程师。你不需要先啃完数据手册,但必须知道 DEMO 里每一行代码在等什么信号。
2. 霍尔传感器选型与开发板接线:先搞清楚你手里的是开关型还是线性型
2.1 开关型与线性型的本质区别,以及 DEMO 为什么通常只用开关型
霍尔传感器分两大类:开关型输出的是数字高低电平,线性型输出的是随磁场强度连续变化的模拟电压。你拿到的 DEMO 程序,十有八九是给开关型写的,因为它的逻辑最简单——有磁铁靠近,输出翻转;磁铁离开,再翻转回去。开发板只需要一个 GPIO 就能读,不需要 ADC 采样,也不需要做电压校准。
开关型里又分单极、双极和锁存型。单极型是只对某一个磁极响应,比如只有南极靠近才翻转;双极型是南极和北极交替靠近都会翻转;锁存型则是南极翻转后保持,直到北极再来才翻回去。做电机测速时,锁存型最常用,因为转子每转一圈,磁极交替出现,锁存输出会给出干净的方波,不会在临界点抖动。如果你拿单极型去测转速,磁铁稍微偏离一点,输出就可能反复跳,计数直接翻倍。
我一般会先看传感器丝印上的型号,再去查它的输出类型。常见的 A3144 是单极开关型,OH137 是双极型,US1881 是锁存型。DEMO 程序里如果只写了一个 GPIO 中断,那它大概率是按开关型设计的,你换成线性型接上去,读到的永远是 0 或 1,因为线性输出在无磁场时可能停在 2.5V 左右,开发板的数字输入根本识别不了。
2.2 开发板接线:上拉电阻、去耦电容和那个容易被忽略的 VCC 电压
接线本身不复杂,但有几个细节决定了 DEMO 能不能稳定跑。开关型霍尔通常有三根线:VCC、GND、OUT。OUT 是开漏输出,意味着它只能把线拉低,不能主动拉高。所以你必须在外接一个上拉电阻,常见值是 4.7kΩ 到 10kΩ,接到和开发板 GPIO 同一电压域。如果你直接接 GPIO 而不加上拉,读到的电平会浮空,串口打印出来的就是随机 0 和 1。
开发板的 GPIO 电压通常是 3.3V,但有些霍尔传感器标称工作电压是 5V。这时候你要么选宽压型号,要么在 OUT 和 GPIO 之间加电平转换。我见过有人把 5V 供电的霍尔输出直接怼到 3.3V 的 STM32 引脚上,跑了几分钟没事,但长期用就是在赌命。稳妥做法是:先确认传感器数据手册里的 VCC 范围和输出高电平电压,再决定要不要分压或加转换芯片。
去耦电容也值得提一句。霍尔传感器内部有振荡器和比较器,电源上的纹波会直接耦合到输出。在 VCC 和 GND 之间并一个 0.1μF 的陶瓷电容,位置尽量靠近传感器引脚,能消掉大部分毛刺。这个电容不贵,但少了它,DEMO 跑起来可能每隔几秒就多出一个假中断。
// 以 STM32 HAL 库为例,GPIO 输入初始化片段 GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟 GPIO_InitStruct.Pin = GPIO_PIN_0; // 假设霍尔 OUT 接在 PA0 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发,对应磁铁靠近 GPIO_InitStruct.Pull = GPIO_PULLUP; // 启用内部上拉,外部可省电阻 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 配置 NVIC 中断优先级 HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn);这段代码做了三件事:把 PA0 设为下降沿触发的外部中断,启用内部上拉,然后打开中断通道。参数上,GPIO_MODE_IT_FALLING表示只在电平从高变低时触发,对应开关型霍尔输出被磁铁拉低的那一刻。如果你用的是锁存型,可能需要改成GPIO_MODE_IT_RISING_FALLING,让上升沿和下降沿都触发,这样一次磁极交替就能计两个数。Pull选GPIO_PULLUP是因为开漏输出需要上拉,如果你外部已经焊了 4.7kΩ,这里可以改成GPIO_NOPULL,避免并联后阻值偏低。
2.3 用轮询还是中断:DEMO 里两种写法的适用边界
DEMO 程序常见两种写法:一种是在while(1)里不断读 GPIO 电平,另一种是配中断,在回调里计数。轮询写法简单,但只适合磁铁变化很慢的场景,比如手动拿磁铁靠近离开。一旦你把它装到电机上,转速超过几百转每分钟,轮询就会漏掉脉冲,因为主循环里还有其他任务在跑。
中断写法更可靠,但要注意中断服务函数里别做耗时操作。我一般只在中断里给一个变量加一,然后置个标志位,主循环再去处理。如果你在中断里直接printf,串口输出会拖慢响应,高速时照样丢脉冲。另外,机械式霍尔开关在临界点会有抖动,中断触发一次可能实际跳变了好几次。硬件上可以加 RC 滤波,软件上可以在中断里做简单的延时确认,但延时不能太长,否则高速时又会漏。
3. 从 DEMO 到能测转速:定时器计数、消抖和串口输出的完整链路
3.1 用定时器做 1 秒闸门,把脉冲数换算成转速
DEMO 跑通之后,下一步就是让它真正能测转速。思路很直接:固定一个时间窗口,比如 1 秒,统计这 1 秒内霍尔中断触发了多少次,再根据电机每转一圈产生多少个脉冲,反推出转速。假设电机转子上贴了一块磁铁,霍尔每转一圈输出一个下降沿,那 1 秒内计到 50 个脉冲,转速就是 50 转每秒,也就是 3000 RPM。
实现上,用一个硬件定时器产生 1 秒中断,在定时器中断里读取霍尔计数值,清零,然后通过串口打印。霍尔中断只负责给一个volatile变量加一。注意这个变量要加volatile,否则编译器优化后可能一直读寄存器里的旧值。
volatile uint32_t hall_count = 0; // 霍尔脉冲计数 volatile uint8_t report_flag = 0; // 上报标志 // 霍尔外部中断回调 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { hall_count++; // 只做计数,不打印 } } // 定时器中断回调,1 秒触发一次 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { report_flag = 1; // 通知主循环上报 } } // 主循环 while (1) { if (report_flag) { report_flag = 0; uint32_t count = hall_count; // 读取当前计数值 hall_count = 0; // 清零,开始下一窗口 float rpm = count * 60.0f; // 每转 1 个脉冲时,RPM = 每秒脉冲数 × 60 printf("RPM: %.1f\r\n", rpm); } }这段代码里,hall_count在中断里加一,在定时器中断里置标志,主循环负责打印。参数上,TIM2需要配置成 1Hz 的更新中断,预分频和自动重载值根据你的系统时钟算。比如 72MHz 主频,预分频 7199,重载 9999,就是 1 秒。rpm的计算公式取决于每转脉冲数,如果你贴了两块磁铁,或者用的是锁存型且上升下降沿都触发,那每转脉冲数就是 2 或 4,公式里的系数要相应调整。
3.2 消抖:为什么你的转速读数会突然翻倍或归零
霍尔传感器在电机上跑的时候,输出边沿往往不是理想的方波。电机振动、磁铁安装偏心、电源噪声都会让输出在跳变瞬间产生多次抖动。如果中断配置成双边沿触发,一次机械跳变可能被计成 2 到 3 个脉冲,转速直接翻倍。如果抖动发生在下降沿附近,还可能让中断触发后电平又弹回去,下一次中断提前到来,计数偏大。
硬件消抖最有效:在霍尔 OUT 和 GND 之间并一个 10nF 到 100nF 的电容,再串一个 1kΩ 到 10kΩ 的电阻到 GPIO。这样边沿会变缓,高频抖动被滤掉。但电容不能太大,否则高速时边沿太慢,反而识别不了。我一般先用 10nF 试,如果转速上限够用就不再加。
软件消抖可以在中断里做:触发后延时一小段时间再读一次电平,确认状态真的变了才计数。但延时不能放在中断里死等,可以用定时器做个短延时,或者简单点,在中断里记录时间戳,如果两次中断间隔小于某个阈值就忽略。阈值根据你的最高转速算,比如最高 6000 RPM,每转 1 个脉冲,那脉冲间隔最小是 10ms,你可以把阈值设成 1ms,低于这个间隔的当成抖动丢弃。
3.3 串口输出与上位机观察:怎么确认你测到的不是假数据
串口打印是验证手段,但打印本身也会影响测量。如果你在 1 秒窗口里频繁打印,串口占用 CPU 时间,可能干扰中断响应。我一般只在每个窗口结束时打印一行,格式尽量简单,用printf重定向到串口。如果发现打印出来的 RPM 跳动很大,先别怀疑算法,用示波器或者逻辑分析仪抓一下霍尔输出波形,看看边沿是否干净、频率是否和电机实际转速对得上。
没有示波器的话,可以临时把霍尔输出接到开发板的一个 GPIO 上,用另一个定时器做输入捕获,直接测脉冲宽度和周期。输入捕获得到的是硬件级的时间戳,比中断计数更准,也能帮你判断抖动到底发生在哪里。这个法子我经常用来排查“计数翻倍”的问题,基本一抓一个准。
4. 避坑与排查:霍尔 DEMO 跑不通时先查这 5 个地方
4.1 现象:串口一直打印 0,磁铁靠近也没反应
原因通常是接线错了或者上拉没生效。开关型霍尔的开漏输出如果没有上拉,GPIO 读到的永远是低电平,中断配成下降沿就永远不触发。另一个可能是 VCC 没接对,有些传感器引脚顺序是 VCC-GND-OUT,有些是 OUT-GND-VCC,插反了不烧但也不工作。
解决:先用万用表量霍尔 OUT 对 GND 的电压,无磁铁时应该是高电平(接近 VCC),有磁铁时拉低。如果无磁铁时就是 0V,检查上拉电阻和 VCC。如果电压正常但中断不触发,检查 GPIO 模式是不是配成了模拟输入或输出。
4.2 现象:磁铁靠近一次,串口打印了几十个数
原因是中断抖动,或者你在中断里做了打印。机械式霍尔在临界点输出会振荡,下降沿触发一次后,电平弹回去又下来,中断连续触发。如果你在中断回调里直接printf,每次触发都打印,看起来就是一次靠近打了几十行。
解决:中断里只计数,不打印。硬件上加 RC 滤波,软件上加时间阈值过滤。如果用的是双边沿触发,改成单边沿,或者确认你的计数逻辑是否真的需要双边沿。
4.3 现象:电机一转,转速读数乱跳,有时翻倍有时减半
原因是每转脉冲数没算对,或者磁铁安装位置不对。单极型霍尔只对一个磁极响应,如果转子上只有一块磁铁,每转确实只产生一个脉冲。但如果你用的是锁存型且双边沿触发,每转就是两个脉冲。翻倍通常是多计了抖动,减半通常是漏了脉冲,比如中断被更高优先级任务阻塞。
解决:先确认传感器类型和触发方式,再确认转子上磁铁数量和极性排列。用示波器看实际波形,数一个机械周期里有多少个边沿。如果漏脉冲,检查中断优先级和主循环里有没有长时间关中断的操作。
4.4 现象:低速时正常,转速一高读数就偏小
原因是中断响应不过来,或者主循环里其他任务占用了太多时间。霍尔脉冲频率高了之后,如果中断服务函数太长,或者有更高优先级中断频繁打断,就会丢脉冲。另外,如果串口打印太频繁,也会拖慢系统。
解决:中断里只做最简操作,计数变量用volatile,主循环里处理打印。提高霍尔中断的优先级,确保它不被其他非关键中断打断。如果还是丢,考虑用定时器的输入捕获硬件自动记录边沿,完全不占 CPU。
4.5 现象:换了一块开发板,同样的代码读不到数据
原因是 GPIO 电压域或时钟配置不同。比如从 STM32 换到 ESP32-S3,GPIO 默认可能是 3.3V 但内部上拉阻值不同,或者外部中断的 API 完全不一样。ESP32 的霍尔传感器接口和 STM32 的 EXTI 不是一回事,直接移植代码肯定跑不通。
解决:先确认新开发板的 GPIO 电气特性,再查对应的外部中断配置方法。ESP32 用gpio_set_intr_type和gpio_isr_handler_add,STM32 用 HAL 库的 EXTI 回调。别指望复制粘贴就能跑,底层寄存器操作差异很大。
5. 把 DEMO 变成可复用的测速模块:状态机、滑动平均和阈值报警
DEMO 跑通之后,我一般不会停在“能打印 RPM”这一步。实际项目里,转速数据是要拿去做控制的,抖动大了控制器会疯。所以我会在 DEMO 基础上加两层处理:滑动平均滤波和状态机判断。
滑动平均很简单,开一个长度为 5 的数组,每次新 RPM 进来就替换最旧的值,然后算平均。这样单次抖动被平滑掉,输出稳定很多。但窗口不能太长,否则转速突变时响应会滞后。5 到 10 个点是比较平衡的选择,具体看你的控制周期。
状态机用来判断电机是否在转、是否堵转、是否超速。比如连续 3 个窗口 RPM 都低于 10,就认为电机停转;连续 3 个窗口 RPM 超过设定上限,就触发报警。这些逻辑放在主循环里,和中断完全解耦,改起来也方便。
#define RPM_WINDOW_SIZE 5 float rpm_buffer[RPM_WINDOW_SIZE] = {0}; uint8_t rpm_index = 0; uint8_t rpm_full = 0; float rpm_average(float new_rpm) { rpm_buffer[rpm_index] = new_rpm; rpm_index = (rpm_index + 1) % RPM_WINDOW_SIZE; if (rpm_index == 0) rpm_full = 1; uint8_t count = rpm_full ? RPM_WINDOW_SIZE : rpm_index; float sum = 0; for (uint8_t i = 0; i < count; i++) { sum += rpm_buffer[i]; } return sum / count; } // 在主循环里调用 float avg_rpm = rpm_average(rpm); if (avg_rpm < 10.0f) { // 电机停转 } else if (avg_rpm > 6000.0f) { // 超速报警 }这段代码里,rpm_buffer存最近几次的 RPM 值,rpm_index指向下一个写入位置,rpm_full标记缓冲区是否已经填满。rpm_average函数先写入新值,再根据是否填满决定平均几个数。参数上,RPM_WINDOW_SIZE越大越平滑但响应越慢,我一般从 5 开始试,如果控制周期是 100ms,5 个窗口就是 500ms 的滞后,对大多数电机测速够用。阈值 10 和 6000 要根据你的实际电机调整,别照抄。
还有一个技巧:如果你用的是锁存型霍尔,可以同时捕获上升沿和下降沿,这样每转脉冲数翻倍,低速时分辨率更高。但双边沿触发对抖动更敏感,必须配合硬件滤波。我一般先在单边沿下把转速测准,再考虑要不要切双边沿。
最后说个血泪教训:别在中断里调用任何可能阻塞的函数,包括printf、HAL_Delay、动态内存分配。我见过有人在霍尔中断里做malloc,跑几分钟就死机,查了半天才定位到堆碎片。中断里只做计数和置标志,其他全部丢给主循环。这个习惯一旦养成,后面做编码器、做串口协议解析都会少踩很多坑。希望帮到你。
本文还有配套的精品资源,点击获取