简介:一份基于霍尔传感器与STM32的直流电机调速系统课程设计报告,适合自动化、电气类本科生及嵌入式初学者参考,用于完成电力拖动类课程设计或理解直流电机闭环调速实现思路。资源为PDF格式,共1个文件,压缩包大小约762KB,内容完整涵盖霍尔传感器电路制作、转速采样电路设计、数码管显示电路、STM32微控制器编程以及系统总体方案与测试优化等环节,并从控制器选型、电源方案、键盘与显示模块、驱动模块到PWM控制逐一展开,提供一套可复现的硬件与软件设计流程。全文基于Cortex-M3内核的STM32平台,给出测量范围200rpm-3000rpm、误差±10rpm的设计指标及结构框图,便于读者对照学习信号转换、数字转速换算和嵌入式程序编写方法。该资源已有800余人学习,适合需要快速掌握课程设计结构、硬件电路搭建思路和STM32调速系统框架的读者作为参考资料。
1. 用霍尔传感器给直流电机调速:为什么闭环比调占空比更值钱
手里有一台直流电机,做小型风机、泵、转台或者桌面输送带,你会发现一个很现实的问题:开环给固定 PWM 占空比,电机空载转速和带载转速能差出 20% 甚至更多。负载一变化,转速就掉,控制不了。这时候最便宜的解法不是换编码器,而是用霍尔传感器做转速反馈,配合 STM32 实现闭环调速。这套系统的骨架是:电机轴上装磁钢,霍尔传感器感应转速输出脉冲,STM32 用定时器捕获脉冲计算实际转速,PID 根据目标转速修正 PWM 占空比。
很多人误以为调速系统的核心是 PWM 生成,其实真正把精度拉起来的是霍尔传感器那一环。没有转速反馈,码盘精度再好的 PWM 也只是开环,负载一变化就露馅。这套方案适合正在做嵌入式课程设计、毕设或者产品样机验证的人,也适合想把产线上开环电机改成闭环的老工程师。霍尔传感器比光电编码器便宜,比测速发电机好安装,在粉尘和振动环境里也更扛得住。下面从传感器选型开始,一步步把整个系统拆开讲。
2. 霍尔测速方案选型:磁钢、传感器与信号调理怎么做才稳
2.1 开关型与锁存型霍尔:两类传感器的选用边界
霍尔传感器在有刷直流电机调速系统里,绝大多数情况下不是用来换相的,而是当作测速反馈元件。它的输出经过内部比较器整形后就是方波,STM32 直接捕获上升沿就能数脉冲。市面上常见的霍尔元件分两类:开关型和锁存型,两者输出行为差别很大,选错型号后面会非常难受。
开关型霍尔(例如常见 A3144 这类)输出随着磁场强度的变化而翻转,磁场超过工作点输出低电平,低于释放点输出高电平。它只响应磁场的「有没有」,不区分极性。好处是单极磁钢就能驱动,安装自由度大;缺点是如果电机轴上有多个磁极且磁场强度变化不够陡峭,输出容易抖动。
| 对比项 | 开关型 | 锁存型 |
|---|---|---|
| 输出行为 | 磁场强弱决定输出电平 | 靠磁场极性翻转锁存输出状态 |
| 测速适用 | 单磁钢、低速、安装空间大 | 多极磁环、需要高分辨率 |
| 抗抖动 | 一般,需配合滤波 | 较好,输出状态稳定 |
| 安装要求 | 宽松,贴合面即可 | 对气隙和极性方向敏感 |
| 典型场景 | 入门样机、单脉冲测速 | 高速电机、每转多脉冲需求 |
锁存型霍尔需要磁场方向反转才会翻转输出,正好匹配多极磁环:N 极来一下输出翻到一端,S 极来一下再翻回来。这样每转就能产生 4 个、8 个甚至更多的脉冲,测速分辨率明显提升。我一般建议,只要能保证磁钢和霍尔的距离稳定,优先选锁存型加多极磁环,尤其当电机额定转速只有几百转的时候,每转一个脉冲会让测速刷新慢到没法看。
2.2 磁钢安装与极对数:测速分辨率的源头
磁钢安装是整个系统里最容易被忽略的玄学环节。霍尔传感器与磁钢之间的气隙,直接决定输出信号的幅值和波形质量。气隙太大,磁场强度不够,开关型霍尔可能根本翻不了转;气隙太小,磁钢旋转时如果有轻微偏心,霍尔输出脉冲宽度会忽宽忽窄,测速值跟着跳。
磁钢的极对数用 P 表示,每转脉冲数等于 2 × 极对数。比如一个 4 对极磁环,每转产生 8 个脉冲。P 越大,单位转数的脉冲越多,相邻脉冲时间间隔越短,测速的实时性越好。但 P 不是越大越好:磁钢极对数多,要求霍尔安装位置对准磁环的圆周,任何偏心都会造成脉冲间隔不均匀,也就是常说的「占空比漂移」,最终体现为测速值周期性跳动。
安装磁钢时,我习惯先在电机轴端粘一个同轴磁环,用千分表打一下径向跳动,控制在 0.1mm 以内再固定霍尔支架。霍尔元件贴在磁环切线方向,距离 1~3mm 比较稳妥。这个环节不要图省事,后面测速跳动排错会节省大量时间。
2.3 信号调理:从霍尔输出到 STM32 捕获引脚的 3 个环节
霍尔元件输出一般需要外部上拉电阻才能形成可靠的数字电平,很多开关型霍尔是开漏输出。上拉电阻常见取 4.7kΩ 到 10kΩ,取值偏小会增大静态功耗,偏大则上升沿变缓。STM32 的捕获引脚建议配成浮空输入,因为霍尔输出侧已经有上拉电阻了,不要在 MCU 侧再启用内部上拉,两个上拉并联会让信号边沿变得很拖沓。
第二环节是 RC 滤波。霍尔点在电机轴转动时,如果磁钢表面有油污或安装偏心,输出会产生毛刺,一个上拉电阻硬扛不住。常用做法是加一个 1kΩ 电阻串联加 10nF 电容到地,构成低通滤波,截止频率大约在 16kHz,把几十纳秒的窄脉冲滤掉。注意滤波电容不能太大,否则边沿变缓,单片机捕获边沿的时机产生抖动,测速精度反而下降。
第三环节其实是可选的施密特整形。如果霍尔信号边沿在低转速下明显变缓,或者线缆比较长引入了噪声,可以在霍尔输出和 STM32 引脚之间加一级施密特缓冲器。大多数情况下,STM32 定时器的输入滤波器(ICFilter)就能起到类似作用,把捕获引脚内部滤波打开,外部就不需要额外整形电路了。这样硬件最简,成本最低。
3. STM32 定时器测速:M 法、T 法的代码实现与切换
3.1 输入捕获初始化:把霍尔脉冲变成可计算的计数
霍尔信号接入 STM32 的定时器输入捕获引脚,比如 PA0 复用为 TIM2_CH1。核心思路是配置定时器工作在输入捕获模式,每检测到一个上升沿,硬件自动把当前计数值锁存到捕获寄存器,并触发中断。这样软件只需在中断里读一下捕获值,就能知道相邻两次脉冲的时间间隔或者累计脉冲数。
下面这段初始化代码基于 HAL 库,以 72MHz 系统时钟为例:
// 定时器输入捕获初始化,PA0 -> TIM2_CH1 void MX_TIM2_Init(void) { TIM_IC_InitTypeDef sConfigIC = {0}; htim2.Instance = TIM2; htim2.Init.Period = 0xFFFF; // 16位计数器,计满回绕 htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz,计数周期 1us htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(&htim2); sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 捕获不分频,每个边沿都捕获 sConfigIC.ICFilter = 0x0F; // 输入滤波器,滤除窄脉冲毛刺 sConfigIC.Channel = TIM_CHANNEL_1; sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿捕获 HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // 使能捕获中断 }分频系数 72-1 把计数频率降成 1MHz,每个计数代表 1 微秒,这个口径在 T 法测速时非常方便。Period 设为 0xFFFF 是因为 TIM2 是 16 位定时器,计数到 65535 后回绕。ICFilter 取值对应内部数字滤波器采样频率,0x0F 大约能滤掉几微秒宽的窄脉冲,但又不会影响真实霍尔信号的边沿。如果霍尔线很长、干扰大,可以适当再调大一点,但注意过大会把低速时的真实脉冲也滤掉。
捕获中断回调里做两件事:累计脉冲数,同时记录相邻两次捕获的计数值差值。这两件事分别服务于 M 法和 T 法,代码在同一个中断里维护:
volatile uint32_t hall_pulse_cnt = 0; // 脉冲累计数 volatile uint32_t cap_period_us = 0; // 相邻脉冲间隔,单位 us volatile uint32_t last_cap = 0; // 上次捕获计数值 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { uint32_t cur = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算与上次捕获之间的计数值,注意处理 16 位回绕 if (cur >= last_cap) { cap_period_us = cur - last_cap; } else { cap_period_us = cur + 0xFFFF - last_cap; } last_cap = cur; hall_pulse_cnt++; } }回绕处理是必须要写的逻辑。计数器从 65535 回绕到 0 的时候,直接做减法会得到一个巨大的错误值,必须先判断当前值是否小于上次值。这种边界情况在低转速下特别容易触发,因为相邻脉冲间隔可能超过 65535 微秒,也就是 65ms。
3.2 M 法测速:固定闸门内数脉冲,适合中高速
M 法测速的原理是固定一个时间闸门,统计闸门内来了多少个霍尔脉冲,然后用脉冲数除以闸门时间再除以每转脉冲数,得到转速。闸门时间一般取 50ms 到 200ms 之间,太短则低速时脉冲数量少、误差大,太长则转速响应迟钝。我常用 100ms,兼顾实时性和精度。
#define PULSES_PER_REV 8 // 磁环 4 对极,每转 8 个脉冲 #define GATE_MS 100 // 闸门 100ms volatile float speed_rpm = 0; // 最终转速,单位 RPM // 在 1ms 周期任务中调用,累计到闸门时间就计算一次转速 void Speed_Task_1ms(void) { static uint32_t cnt_prev = 0; static uint16_t gate_cnt = 0; if (++gate_cnt >= GATE_MS) { gate_cnt = 0; uint32_t delta = hall_pulse_cnt - cnt_prev; // 闸门内脉冲数 cnt_prev = hall_pulse_cnt; // 60000 = 60 秒 * 1000 毫秒 speed_rpm = (float)delta * 60000.0f / (PULSES_PER_REV * GATE_MS); } }公式里 60000 是把毫秒口径的闸门时间换算成每分钟的脉冲频率。举一个直观的数字:如果 delta 是 80 个脉冲,PULSES_PER_REV 是 8,闸门 100ms,转速就是 80 × 60000 / (8 × 100) = 6000 RPM。
M 法的误差来源在低速时暴露得很明显。转速 60RPM 加上 8 脉冲每转,闸门 100ms 内只有 8 个脉冲,少一个脉冲就偏差 12.5%,这精度没法看。所以在低速区间必须引入 T 法。
3.3 T 法测速:用相邻脉冲间隔算周期,低速救星
T 法测速不数脉冲个数,而是测量相邻两个脉冲之间的时间间隔,用这个间隔反推转速。前面在捕获中断里已经维护了 cap_period_us 变量,它就是相邻脉冲间隔。有了它,低速测速精度会高很多。
// T 法转速计算,单位 RPM // cap_period_us 是相邻脉冲间隔,单位为微秒 float Speed_Calc_T_Method(uint32_t period_us) { if (period_us == 0) { return 0; } // 1 分钟 = 60,000,000 微秒 // 每转耗时 = period_us * PULSES_PER_REV // 转速 = 60,000,000 / 每转耗时 return 60000000.0f / ((float)period_us * PULSES_PER_REV); }注意 T 法的单位口径:每个脉冲对应的机械角度是 1/PULSES_PER_REV 转,那么一整转的时间就是脉冲间隔乘以 PULSES_PER_REV 微秒。一分钟是 6000 万微秒,除一下就得到 RPM。如果脉冲间隔是 12500 微秒,PULSES_PER_REV 是 8,每转耗时 100ms,转速就是 600 RPM。
实际项目中我不会把 M 法和 T 法做成两套独立逻辑,而是统一在捕获中断里维护数据,在闸门任务里判断:闸门时间内脉冲数 delta 大于等于 10 就用 M 法,小于 10 就用最近一次记录的 cap_period_us 走 T 法。切换阈值是一个经验值,太低则 T 法介入晚,太高则中速区间抖动。10 个脉冲对应在 8 脉冲每转时,100ms 闸门内转速约 750RPM 以下切 T 法,这个切换点实测比较顺滑。
4. PWM 驱动与速度闭环:增量式 PID 的参数怎么给
4.1 PWM 频率选择与 H 桥驱动配置:先跑通开环再谈闭环
有刷直流电机调速的功率级一般用 H 桥,STM32 输出 PWM 控制占空比,方向引脚控制正反转。PWM 频率不能拍脑袋定。常见做法是选 10kHz 到 20kHz,一方面避开人耳可听的噪声频段,另一方面保证电枢电流相对平滑。我一般默认 16kHz,如果电机电枢电感很小,可以降到 12kHz 提高电流连续度。
PWM 初始化时需要关注几个参数:计数频率、自动重载值、占空比寄存器,以及输出极性。以 72MHz 主频、16kHz PWM 为例,预分频选 72-1、自动重载值选 62,这样 PWM 频率 = 72MHz / 72 / 63 ≈ 16kHz。占空比寄存器从 0 到 62,对应 0% 到 100%,步进约 1.6%,对调速来说够用了。
H 桥驱动部分,常见的集成驱动芯片内部已经包含死区时间,软件侧只需要按顺序初始化:先把方向和 PWM 引脚拉到确定电平,再给驱动芯片使能引脚低电平,完成初始化后最后拉高使能。这个顺序很重要,否则上电瞬间 GPIO 默认电平不确定,H 桥上下桥臂可能同时导通,直接烧驱动。
开环调通是闭环的前提。先用固定占空比 50% 让电机转起来,确认霍尔测速读数正常、正反转方向可控,再进入 PID 环节。很多翻车现场都是跳过这一步直接上闭环,结果分不清是测速问题还是控制问题。
4.2 增量式 PID 实现与参数整定:从公式到能用的三组数
速度环用增量式 PID 是电机调速最常见的做法。增量式输出的是占空比修正量,而不是绝对占空比,天然带有记忆效果,而且不容易出现积分深度饱和导致的大幅超调。实现代码如下:
typedef struct { float kp, ki, kd; float prev_error; // 上一次误差 float prev_prev_error; // 上上次误差 float output; // 当前输出,即占空比加增量后的值 float out_limit; // 输出限幅,对应最大占空比 } PID_Handle_t; // 增量式 PID 计算,返回占空比,范围 -out_limit ~ +out_limit int16_t PID_Calc(PID_Handle_t *pid, int16_t setpoint, int16_t feedback) { float error = (float)setpoint - (float)feedback; float delta = pid->kp * (error - pid->prev_error) + pid->ki * error + pid->kd * (error - 2.0f * pid->prev_error + pid->prev_prev_error); pid->output += delta; // 输出累加,形成绝对占空比 if (pid->output > pid->out_limit) { pid->output = pid->out_limit; } if (pid->output < -pid->out_limit) { pid->output = -pid->out_limit; } pid->prev_prev_error = pid->prev_error; pid->prev_error = error; return (int16_t)pid->output; }增量式 PID 有三个值得注意的地方。第一是输出累加而不是直接赋 error 的比例,所以占空比有天然的「记忆」,负载突变时调整过程比较平滑。第二是 out_limit 必须设置,否则电机启动瞬间误差大,输出一路累加超过 100%,电机全压启动,电流冲击很大。第三是微分项 D 在这个系统里作用很小,很多情况下甚至可以设 0,因为霍尔测速本身带有量化噪声,微分项会把噪声放大成抖动的占空比。
参数整定顺序和经验值,我一般遵循先 P 后 I 再 D 的原则。先给一个保守的 P,让系统不振荡,然后加 I 消除稳态误差,最后根据超调决定要不要加 D。
| 工况 | kp | ki | kd | 备注 |
|---|---|---|---|---|
| 空载起步 | 0.3 | 0.05 | 0.01 | 先跑通,超调允许 |
| 轻载稳定 | 0.5 | 0.02 | 0.005 | 追求快速响应 |
| 重载带波动 | 0.8 | 0.08 | 0.02 | 负载波动大时加强比例 |
这些值不是万能的,伺服系统负载转动惯量变化很大,但我见过很多项目从这组初值开始微调都能收敛。
4.3 闭环联调:从开环占空比到速度闭环的验证路径
闭环联调的正确路径是先验证测速方向和控制方向的一致性。转速反馈是正的,目标转速也是正的,那么误差 = 目标 - 反馈。如果误差为正说明转速偏低,此时占空比增大,电机加速,系统才会收敛。如果接线接反,PID 输出增大后电机反而减速,误差进一步扩大,形成正反馈,几分钟内就能看到转速冲顶或者停转。
验证方向的方法很简单:给定一个固定占空比,手动把目标转速设得比实际转速高一点点,观察 PID 输出占空比应该从当前值缓慢上升。如果下降,说明反馈方向反了,把霍尔脉冲或者方向引脚取反即可。
闭环联调阶段建议把 PID 输出和实际转速同时通过串口或者调试器观察。先用空载小惯量做阶跃测试,给定一个目标转速,观察响应波形,记录超调量和稳定时间。这套路径跑通了再带真实负载,负载变化导致的问题基本集中在参数调整上,很少再出现方向性的问题。
5. 霍尔调速系统避坑:5 个常见问题与排查记录
5.1 测速值周期性跳动:先查磁钢与霍尔的气隙和同心度
现象:转速稳定在目标值附近,但串口打印的实时转速呈周期性波动,波动频率和电机转速接近,不是随机噪声。
原因:磁钢安装偏心,导致霍尔与磁钢的距离在旋转过程中不断变化。距离近磁场强,霍尔提前翻转;距离远磁场弱,翻转滞后。脉冲间隔忽长忽短,转速自然跟着跳。
解决:拆下磁钢重新安装,用千分表打径向跳动,控制在 0.1mm 以内。同时检查霍尔和磁钢的气隙,保持 1~3mm 且不随旋转变化。如果结构上没法调整,可以改用锁存型霍尔配多极磁环,锁存型对磁场幅度变化更不敏感。
5.2 低速测不准:M 法脉冲太少,误差拉满
现象:电机转速低于 100RPM 时,测速值跳动非常剧烈,甚至出现 0 RPM 和 200 RPM 交替跳变。
原因:M 法在闸门时间内只能数到几个脉冲,丢失任何一个脉冲都造成巨大百分比误差。我之前调试一台 4 对极磁环电机,目标转速 50RPM,闸门 100ms 内理论上只有约 3 个脉冲,少一个就是 33% 的误差。
解决:闸门内脉冲数少于 10 时切换 T 法,用相邻脉冲间隔计算转速。如果 T 法也抖动,检查是否低速下霍尔输出边沿抖动导致捕获不稳定,适当加大 ICFilter 滤波参数。
5.3 上电瞬间电机抖动或者驱动芯片发热:先查 GPIO 默认态和使能时序
现象:程序烧录后上电,电机在初始化完成前就猛地转了一下,或者驱动芯片很快发烫,但程序跑到主循环后一切正常。
原因:STM32 上电到 GPIO 初始化完成这段时间,引脚输出状态是默认浮空或者不确定电平,H 桥两个输入引脚出现任意组合。如果恰好是高电平加高电平,桥臂直通,电流飙升。
解决:H 桥使能引脚接一个下拉电阻,默认失能;程序初始化第一件事就是把 PWM 和方向引脚全部拉低,再初始化定时器,最后才拉高使能引脚。驱动芯片发烫严重说明直通已经发生,换芯片前先检查这个时序。
5.4 PID 整定振荡:积分限幅和输出限幅双保险必须有
现象:闭环投入后电机转速来回波动,甚至周期性振荡,把 kp 调小也不见明显改善。
原因:增量式 PID 输出累加后没有限幅,启动瞬间误差大,输出一路累加超限,电机全压加减速来回冲。另一个常见原因是微分项放大了测速量化噪声,kd 给得过大。
解决:代码里的 out_limit 不能省,根据电机额定电压和 PWM 分辨率设置,一般限制在 80%~90% 占空比。先只调 P,找到系统不振荡的临界增益,再减半做初始值,然后把 ki 从 0 慢慢加上去消除稳态误差。kd 在这类系统里通常不需要,除非负载惯量特别大。
5.5 高速时频繁中断拖慢主循环:用硬件计数减轻中断负担
现象:电机高速运行时,霍尔脉冲频率可能达到每秒几千个,每次进入捕获中断做处理,主循环任务明显卡顿,甚至影响 PWM 更新。
原因:中断频率过高,大部分 CPU 时间花在进出中断的压栈出栈上。我遇到过 8 脉冲每转、6000RPM 的情况,每秒脉冲 800 个,还能扛;但换成更高极对数和更高转速后,中断开销就很可观了。
解决:把捕获中断里最耗时的浮点运算全部移出去,中断里只维护计数值和捕获值。进一步优化方案是用定时器的从模式做硬件级联,让硬件直接完成计数,MCU 只负责周期读取。没有 DMA 或从模式配置经验的情况下,先把计算任务挪出中断是最快见效的做法。
6. 调 PID 的省力技巧:串口转速曲线与斜坡限速
调 PID 最怕的就是只能看到最终转速数字,看不到动态过程。串口打印一个轻量级的曲线数据出去,比盯着调试器变量舒服得多。我一般会做一根简单的数据帧,用 sprintf 把目标转速、实际转速、占空比拼成一行,波特率 115200,10ms 发一次,PC 端随便用一个串口绘图工具就能画出三条曲线。
// 串口输出转速曲线数据,格式:目标,实际,占空比 void Speed_Report(uint16_t setpoint, uint16_t actual, int16_t output) { char buf[48]; snprintf(buf, sizeof(buf), "%d,%d,%d\r\n", setpoint, actual, output); HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100); }看曲线调参数的逻辑其实很简单:超调大就把 kp 往下压,稳态偏差大就加 ki,波形高频抖动就减 kd 甚至清零。这三条经验能覆盖大多数调参场景,比背一整本控制理论管用。
另一个很实用的技巧是斜坡限速。直接把目标转速给 PID,启动会让电机全压加速,电流冲击大,机械结构也可能因为扭矩突变而抖动。给目标转速加一个斜坡发生器,让给定值逐步爬升到最终目标,能明显改善启动品质。代码很简单:
// 斜坡限速:每个周期向目标值逼近 step 步长 // 在主循环里每隔 10ms 调用一次 int16_t Speed_Ramp(int16_t target, int16_t current, int16_t step) { if (target > current + step) { return current + step; } if (target < current - step) { return current - step; } return target; }step 取 5~20 RPM 比较合适,根据系统需要的响应速度调整。这套系统从方案验证到现在,我在 PID 整定上翻车的次数远多于硬件烧毁的次数,类比重载启动时先把斜坡 step 调小、再看看曲线振荡周期决定动哪个参数,才算真正把霍尔调速这套闭环玩明白。希望帮到你。
本文还有配套的精品资源,点击获取