前言

最近在做一个小车的项目,需要精确控制电机转速。之前一直用的开环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参数

  1. 先把I和D设为0,只用P控制
  2. 从小到大慢慢增加Kp,观察响应速度
  3. 我的电机Kp=8时响应比较快,但有轻微震荡

5.2 再加入I参数

  1. 加入积分项可以消除稳态误差
  2. Ki太大会导致超调和震荡
  3. 我设置Ki=0.5,并且加了积分限幅(±500)
  4. 积分限幅很重要,不然启动时积分会累积很大,导致超调严重

5.3 最后调D参数

  1. 微分项可以抑制超调
  2. 但是对噪声敏感,Kd不能太大
  3. 我设置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控制,遇到了不少问题,但最终效果还是挺满意的。几个关键点:

  1. 霍尔传感器一定要加滤波电容,不然干扰会让你怀疑人生
  2. 每圈脉冲数一定要实测,不要想当然
  3. PID参数需要耐心调试,没有捷径,建议先P后I再D的顺序
  4. 积分限幅很重要,防止积分饱和
  5. 低速测速精度有限,如果需要高精度低速控制,建议用编码器

附录:硬件清单

  • 单片机: STM32F103C8T6
  • 电机驱动: L298N模块
  • 电机: 12V直流减速电机(带霍尔)
  • 霍尔传感器: A3144(电机自带)
  • 电源: 12V/2A电源适配器

代码在Keil MDK 5.29下编译通过,HAL库版本1.8.0。


本文为原创内容,记录了实际项目开发过程,如有错误欢迎指正!

更多推荐