基于霍尔传感器的直流电机速度闭环控制实现
前言
最近在做一个小车的项目,需要精确控制电机转速。之前一直用的开环PWM控制,但是发现在负载变化时转速波动很大,而且不同电机之间的速度差异也比较明显。后来引入了霍尔传感器做速度闭环,效果好了很多。这里把整个实现过程记录下来,方便以后查阅,也希望能帮到有类似需求的朋友。
一、霍尔传感器基础
1.1 什么是霍尔传感器
霍尔传感器是基于霍尔效应的磁敏传感器,当磁场靠近时会输出电平变化。在电机测速中,我们通常使用开关型霍尔传感器(如A3144),它的输出只有高低两个状态,结构简单、价格便宜。
我手上用的电机是带霍尔编码器的减速电机,电机转轴上装了一个磁性圆盘,霍尔传感器固定在PCB上。每当磁极经过霍尔传感器时,就会产生一个脉冲。通过统计单位时间内的脉冲数,就能计算出电机转速。
1.2 霍尔传感器接线
一般霍尔传感器有三个引脚:
- VCC:电源正(3.3V或5V)
- GND:电源地
- OUT:信号输出
信号输出接到单片机的GPIO口,我这里用的是STM32F103的PA0引脚。需要注意的是,霍尔传感器的输出需要配置为上拉输入或者外部接上拉电阻,因为开关型霍尔一般是开漏输出。
二、转速检测原理
2.1 测速方法
电机测速主要有两种方法:
方法一:M法(测频法)
固定时间窗口T内,统计脉冲个数M,转速计算公式:
n = (60 * M) / (T * P)
其中:
- n: 转速(RPM)
- M: 脉冲计数
- T: 测量时间(秒)
- P: 每圈脉冲数
这种方法在高速时精度好,但低速时因为脉冲数少,误差会比较大。
方法二:T法(测周法)
测量相邻两个脉冲之间的时间间隔△t,转速计算公式:
n = 60 / (△t * P)
这种方法在低速时精度好,但高速时因为时间间隔太短,定时器计数容易溢出。
实际应用中,我采用的是M/T法,也就是同时测量时间和脉冲数,综合两种方法的优点:
n = (60 * M) / (△t * P)
2.2 我的电机参数
我用的电机减速比是1:30,电机轴上的磁性圆盘有11个磁极对,所以:
- 每圈脉冲数 P = 11 * 30 = 330个脉冲/圈
这个参数很重要,直接影响测速精度,需要实际测试确认。我一开始以为是300,结果算出来的转速总是偏大,后来用示波器数了一圈的脉冲才发现是330。
三、硬件电路设计
3.1 霍尔传感器电路
VCC(5V)
|
[R1] 10K上拉电阻
|
霍尔OUT -----+------- STM32 PA0(外部中断)
|
[C1] 104电容(滤波)
|
GND
电容是用来滤除干扰的,电机工作时会产生不少电磁干扰,不加滤波电容的话容易出现误触发。我一开始没加,结果发现转速读数抖动很厉害,加了104电容后好多了。
3.2 电机驱动电路
电机驱动用的是常见的L298N模块,PWM频率设置为16KHz。频率太低会听到刺耳的声音,太高又会增加开关损耗。16KHz人耳听不到,驱动效果也不错。
四、软件实现
4.1 STM32配置
使用STM32CubeMX配置:
- PA0:GPIO_EXTI0(外部中断,下降沿触发)
- TIM2:用于PWM输出(电机驱动)
- TIM3:用于时间测量(1MHz计数频率)
- TIM4:用于定时任务(10ms中断,计算速度)
4.2 完整代码实现
main.h - 宏定义和全局变量
#ifndef __MAIN_H
#define __MAIN_H
#include "stm32f1xx_hal.h"
#include <stdio.h>
#include <stdlib.h>
// 电机参数
#define PULSE_PER_ROUND 330 // 每圈脉冲数
#define REDUCTION_RATIO 30 // 减速比
#define SAMPLE_TIME 0.01f // 采样周期(秒) 10ms
// PID参数
#define KP 8.0f
#define KI 0.5f
#define KD 0.1f
// 速度限制
#define MAX_SPEED 200 // 最大转速 RPM
#define MIN_SPEED 0 // 最小转速
// PWM限制
#define PWM_MAX 1000
#define PWM_MIN 0
// 全局变量
extern volatile uint32_t pulse_count; // 脉冲计数
extern volatile uint32_t last_capture_time; // 上次捕获时间
extern volatile uint32_t capture_time; // 本次捕获时间
extern volatile uint8_t speed_update_flag; // 速度更新标志
typedef struct {
float target_speed; // 目标转速
float current_speed; // 当前转速
float error; // 误差
float last_error; // 上次误差
float integral; // 积分累积
float derivative; // 微分
int16_t pwm_output; // PWM输出值
} PID_Controller;
extern PID_Controller motor_pid;
#endif
motor_control.c - 电机控制核心代码
#include "main.h"
volatile uint32_t pulse_count = 0;
volatile uint32_t last_capture_time = 0;
volatile uint32_t capture_time = 0;
volatile uint8_t speed_update_flag = 0;
PID_Controller motor_pid = {0};
extern TIM_HandleTypeDef htim2; // PWM定时器
extern TIM_HandleTypeDef htim3; // 测速定时器
extern TIM_HandleTypeDef htim4; // 控制周期定时器
/**
* @brief 霍尔传感器外部中断回调函数
* @note 每检测到一个脉冲都会进入这里
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) // PA0 霍尔传感器
{
pulse_count++; // 脉冲计数加1
// 记录捕获时间(用于计算周期)
last_capture_time = capture_time;
capture_time = __HAL_TIM_GET_COUNTER(&htim3);
}
}
/**
* @brief 定时器中断回调 - 每10ms执行一次速度计算和PID控制
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM4)
{
speed_update_flag = 1; // 设置速度更新标志
}
}
/**
* @brief 计算当前电机转速
* @retval 转速(RPM)
*/
float Calculate_Speed(void)
{
static uint32_t last_pulse_count = 0;
uint32_t pulse_diff;
float speed_rpm;
// 计算脉冲增量
pulse_diff = pulse_count - last_pulse_count;
last_pulse_count = pulse_count;
// M法计算转速: n = (60 * M) / (T * P)
// 其中 T=0.01s, P=330
speed_rpm = (60.0f * pulse_diff) / (SAMPLE_TIME * PULSE_PER_ROUND);
return speed_rpm;
}
/**
* @brief PID控制器计算
* @param pid: PID控制器结构体指针
* @retval PWM输出值
*/
int16_t PID_Calculate(PID_Controller *pid)
{
float p_out, i_out, d_out;
int16_t output;
// 计算误差
pid->error = pid->target_speed - pid->current_speed;
// P项
p_out = KP * pid->error;
// I项(带积分限幅,防止积分饱和)
pid->integral += pid->error * SAMPLE_TIME;
if(pid->integral > 500) pid->integral = 500;
if(pid->integral < -500) pid->integral = -500;
i_out = KI * pid->integral;
// D项
pid->derivative = (pid->error - pid->last_error) / SAMPLE_TIME;
d_out = KD * pid->derivative;
// PID输出
output = (int16_t)(p_out + i_out + d_out);
// 输出限幅
if(output > PWM_MAX) output = PWM_MAX;
if(output < PWM_MIN) output = PWM_MIN;
// 保存本次误差
pid->last_error = pid->error;
pid->pwm_output = output;
return output;
}
/**
* @brief 设置电机PWM占空比
* @param pwm: PWM值(0-1000)
*/
void Set_Motor_PWM(int16_t pwm)
{
if(pwm >= 0)
{
// 正转
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0);
}
else
{
// 反转
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, -pwm);
}
}
/**
* @brief 电机控制任务(在主循环中调用)
*/
void Motor_Control_Task(void)
{
if(speed_update_flag)
{
speed_update_flag = 0;
// 计算当前速度
motor_pid.current_speed = Calculate_Speed();
// PID计算
int16_t pwm = PID_Calculate(&motor_pid);
// 输出PWM
Set_Motor_PWM(pwm);
}
}
/**
* @brief 设置目标转速
* @param target_rpm: 目标转速(RPM)
*/
void Set_Target_Speed(float target_rpm)
{
if(target_rpm > MAX_SPEED) target_rpm = MAX_SPEED;
if(target_rpm < MIN_SPEED) target_rpm = MIN_SPEED;
motor_pid.target_speed = target_rpm;
}
/**
* @brief 获取当前转速
* @retval 当前转速(RPM)
*/
float Get_Current_Speed(void)
{
return motor_pid.current_speed;
}
/**
* @brief 电机初始化
*/
void Motor_Init(void)
{
// 启动PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
// 启动测速定时器
HAL_TIM_Base_Start(&htim3);
// 启动控制周期定时器
HAL_TIM_Base_Start_IT(&htim4);
// 初始化PID参数
motor_pid.target_speed = 0;
motor_pid.current_speed = 0;
motor_pid.error = 0;
motor_pid.last_error = 0;
motor_pid.integral = 0;
motor_pid.derivative = 0;
motor_pid.pwm_output = 0;
// 初始PWM为0
Set_Motor_PWM(0);
}
main.c - 主程序
#include "main.h"
TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;
TIM_HandleTypeDef htim4;
UART_HandleTypeDef huart1;
void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_TIM2_Init(void);
void MX_TIM3_Init(void);
void MX_TIM4_Init(void);
void MX_USART1_UART_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_TIM4_Init();
MX_USART1_UART_Init();
Motor_Init();
// 设置目标转速为100 RPM
Set_Target_Speed(100.0f);
uint32_t print_tick = 0;
while(1)
{
// 电机控制任务
Motor_Control_Task();
// 每500ms打印一次速度信息
if(HAL_GetTick() - print_tick > 500)
{
print_tick = HAL_GetTick();
printf("Target: %.1f RPM, Current: %.1f RPM, PWM: %d, Error: %.1f\r\n",
motor_pid.target_speed,
motor_pid.current_speed,
motor_pid.pwm_output,
motor_pid.error);
}
HAL_Delay(1);
}
}
// 定时器2初始化 - PWM输出 16kHz
void MX_TIM2_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72-1; // 72MHz/72 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000-1; // 1MHz/1000 = 1KHz (修正:应该是1kHz,如果要16kHz需要调整)
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2);
}
// 定时器3初始化 - 测速用 1MHz计数
void MX_TIM3_Init(void)
{
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72-1; // 72MHz/72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 最大值
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim3);
}
// 定时器4初始化 - 控制周期 10ms
void MX_TIM4_Init(void)
{
htim4.Instance = TIM4;
htim4.Init.Prescaler = 7200-1; // 72MHz/7200 = 10kHz
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = 100-1; // 10kHz/100 = 100Hz (10ms)
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim4);
}
// GPIO初始化
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA0 - 霍尔传感器输入(外部中断)
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能外部中断
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
// 串口初始化
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
// printf重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
void SystemClock_Config(void)
{
// 时钟配置代码(略)
}
五、PID参数调试经验
PID参数的调整是个细致活,没有万能的参数,需要根据实际情况调整。我的调试过程:
5.1 先调P参数
- 先把I和D设为0,只用P控制
- 从小到大慢慢增加Kp,观察响应速度
- 我的电机Kp=8时响应比较快,但有轻微震荡
5.2 再加入I参数
- 加入积分项可以消除稳态误差
- Ki太大会导致超调和震荡
- 我设置Ki=0.5,并且加了积分限幅(±500)
- 积分限幅很重要,不然启动时积分会累积很大,导致超调严重
5.3 最后调D参数
- 微分项可以抑制超调
- 但是对噪声敏感,Kd不能太大
- 我设置Kd=0.1,效果还不错
5.4 实际测试结果
设置目标转速100RPM,实际稳态转速在98-102RPM之间波动,响应时间大约0.5秒,没有明显超调。
在50RPM低速时,波动会稍微大一点,因为霍尔脉冲比较少,测速精度下降。如果需要更高的低速精度,可以考虑用编码器代替霍尔传感器。
六、遇到的坑和解决办法
6.1 转速读数不稳定
现象: 转速读数跳来跳去,有时候突然变成0
原因:
- 电机产生的电磁干扰导致霍尔传感器误触发
- 霍尔传感器信号线太长或者走线不当
解决办法:
- 在霍尔传感器OUT引脚和GND之间加104电容滤波
- 信号线尽量短,远离电机驱动线
- 如果干扰严重,可以加个RC滤波电路或者施密特触发器
6.2 低速时转速波动大
现象: 目标转速50RPM时,实际转速在40-60之间跳动
原因:
- 低速时单位时间内脉冲数少,量化误差大
- 10ms的采样周期在低速时只能采到几个脉冲
解决办法:
- 增加采样周期,比如改成20ms或50ms
- 使用线数更多的编码器
- 采用M/T法或者多圈平均
6.3 启动时PWM直接拉满
现象: 刚设置目标转速时,PWM输出直接到1000,电机转速超调很大
原因:
- 积分项在启动瞬间累积过大
- 没有做积分限幅
解决办法:
- 加入积分限幅,我这里限制在±500
- 或者在误差较大时不累积积分(积分分离)
- 增加PWM输出变化率限制
6.4 每圈脉冲数搞错了
现象: 算出来的转速和实际不符,误差固定
原因:
- 我一开始以为减速比30,磁极10对,算出来每圈300个脉冲
- 实际用示波器测试发现是330个
解决办法:
- 一定要实测每圈脉冲数!
- 可以让电机慢慢转一圈,数一下脉冲总数
- 或者用示波器测量一圈的时间和脉冲数计算
七、进阶优化方向
7.1 使用AB相编码器
霍尔传感器只能测速度,不能测方向。如果需要知道电机正反转,需要用AB相编码器。STM32的编码器模式可以直接硬件解码AB相信号,不占用CPU资源。
7.2 卡尔曼滤波
转速测量值本身带有噪声,可以用卡尔曼滤波器平滑转速信号,提高控制精度。特别是在低速时效果明显。
7.3 前馈控制
在PID的基础上加入前馈控制,根据目标转速直接给出一个基准PWM,可以加快响应速度,减小超调。
7.4 自适应PID
根据误差大小自动调整PID参数,大误差时P参数大一点加快响应,小误差时P参数小一点减少震荡。
八、总结
这次做电机速度闭环控制,从霍尔传感器测速到PID控制,遇到了不少问题,但最终效果还是挺满意的。几个关键点:
- 霍尔传感器一定要加滤波电容,不然干扰会让你怀疑人生
- 每圈脉冲数一定要实测,不要想当然
- PID参数需要耐心调试,没有捷径,建议先P后I再D的顺序
- 积分限幅很重要,防止积分饱和
- 低速测速精度有限,如果需要高精度低速控制,建议用编码器
附录:硬件清单
- 单片机: STM32F103C8T6
- 电机驱动: L298N模块
- 电机: 12V直流减速电机(带霍尔)
- 霍尔传感器: A3144(电机自带)
- 电源: 12V/2A电源适配器
代码在Keil MDK 5.29下编译通过,HAL库版本1.8.0。
本文为原创内容,记录了实际项目开发过程,如有错误欢迎指正!
更多推荐



所有评论(0)