STM32标准库PWM驱动直流电机:从硬件连接到代码实现

📅 2026/7/30 3:01:56 👁️ 阅读次数 📝 编程学习
STM32标准库PWM驱动直流电机:从硬件连接到代码实现

1. 项目缘起:从“转起来”到“转得稳”

最近在整理工作室的物料,翻出来几个闲置的直流有刷电机和一块TB6612驱动模块。看着它们,突然想起很多朋友在入门STM32控制电机时,第一步往往卡在如何让电机“转起来”上。网上的教程要么是CubeMX+HAL库的,要么是寄存器版本的,对于还在使用标准库(Standard Peripheral Library)的朋友来说,总感觉隔了一层。特别是想用最基础的PWM实现一个简单的开环速度控制,好像资料都挺零散的。

所以,我决定用最“原始”也最“经典”的STM32标准库,搭配一个常见的电机驱动模块,写一篇手把手的指南。目标很简单:不扯复杂的闭环算法,就聚焦于如何用定时器的PWM功能,输出一个可调占空比的方波,让电机听话地变速旋转。这个过程,是理解更高级的FOC、PID等控制算法的基石。如果你手头正好有STM32F1系列(比如流行的F103C8T6)的开发板、一个电机驱动模块(如TB6612、L298N)和一个直流电机,那么跟着做一遍,你就能彻底掌握PWM开环控制的核心流程。

2. 硬件选型与连接:为什么是它们?

在动手写代码之前,理清硬件连接和选型逻辑至关重要。盲目接线不仅可能导致电机不转,更可能烧毁芯片或模块。

2.1 核心控制器:STM32与标准库

我们选用STM32F103C8T6(俗称“蓝桥杯”或“最小系统板”)作为主控。选择它和标准库的原因有三:

  1. 资源丰富且经典:F103系列拥有多个通用定时器(TIM2, TIM3, TIM4等),每个定时器都能产生4路PWM,完全满足基础电机控制需求。其标准库资料浩如烟海,社区支持极好,遇到问题几乎都能找到答案。
  2. 学习曲线平滑:相比于直接操作寄存器,标准库用结构体和函数封装了底层配置,代码可读性更高;相比于HAL库,它又更贴近硬件,能让你清晰地理解定时器、GPIO等外设是如何被配置起来的。这是从51单片机过渡到STM32的黄金学习路径。
  3. 成本与普及度:C8T6核心板价格低廉,几乎是人手一块的“标配”,保证了教程的复现性。

2.2 电机驱动模块:TB6612 vs L298N

为什么我更推荐TB6612而不是更古老的L298N?这里有一个简单的对比:

特性TB6612L298N
工作电压2.5V-13.5V (VM)4.5V-46V
输出电流1.2A(连续)/3.2A(峰值)2A(连续)/3A(峰值)
控制逻辑电平3.3V/5V兼容通常需要5V,对3.3V MCU可能不友好
功耗与发热MOSFET桥,效率高,发热小双H桥,导通压降大(约2V),发热严重
待机模式有(STBY引脚)
尺寸与集成度小巧,集成度高较大,可能需要外接散热片

核心优势:TB6612采用MOSFET,其导通内阻(Rds(on))远小于L298N的双极型晶体管,这意味着在驱动相同电机时,TB6612自身的功耗和发热要小得多。对于小型直流电机(比如玩具车电机、小型风扇电机),TB6612是更优、更现代的选择。它只需要两个控制信号(AIN1/AIN2或BIN1/BIN2)和一个PWM信号即可控制一个电机的方向和速度,逻辑清晰。

2.3 接线图与原理剖析

我们以驱动一个电机为例,使用TB6612的A通道。接线如下:

  1. 电源部分

    • VM:接电机电源(例如7.2V航模电池或6-12V的直流电源)。务必与给STM32供电的3.3V/5V电源隔离!这是保护MCU的关键。
    • VCC:接逻辑电源(3.3V或5V),与STM32共地。如果STM32是3.3V供电,这里就接3.3V。
    • GND至关重要!TB6612的GND必须与STM32的GND、电机电源的GND连接在一起,构成统一的参考地。
  2. 控制信号部分

    • AIN1:连接STM32的某个GPIO引脚(如PA1),用于控制电机方向之一。
    • AIN2:连接STM32的另一个GPIO引脚(如PA2),用于控制电机方向之二。
    • PWMA:连接STM32的某个定时器通道引脚(如PA8,对应TIM1_CH1),用于输入PWM速度信号。
    • STBY:使能引脚,接高电平(VCC)模块才工作。可以接STM32的GPIO控制,也可以直接接VCC。
  3. 电机输出部分

    • AO1AO2:分别接直流电机的两个电极。

方向控制逻辑(真值表):

STBYAIN1AIN2PWMA电机状态
101PWM正转(速度由PWM占空比决定)
110PWM反转(速度由PWM占空比决定)
100PWM刹车(电机快速停止)
111PWM刹车(电机快速停止)
0XXX待机(低功耗,电机自由停止)

注意AIN1AIN2决不能同时为1或同时为0并输入PWM,这会导致电机短路刹车,瞬间电流很大,对驱动芯片和电源都是考验。常规调速我们只用(0,1)(1,0)这两种状态。

3. 软件架构:定时器PWM生成全解析

理解了硬件如何响应信号,我们来看STM32如何产生这些信号。核心是利用通用定时器(如TIM3)的PWM输出模式。

3.1 定时器PWM原理简述

STM32的通用定时器有一个16位的自动重装载寄存器(TIMx_ARR)和一个16位的捕获/比较寄存器(TIMx_CCRx)。计数器(TIMx_CNT)从0开始向上计数。

  • ARR决定了PWM的频率:计数器计到ARR值后归零,重新开始,这个周期就是PWM波的周期。PWM频率 = 定时器时钟源 / (ARR + 1)
  • CCRx决定了PWM的占空比:我们设置定时器为“PWM模式1”。当计数器值小于CCRx时,输出有效电平(高或低,可配置);大于等于CCRx时,输出无效电平。这样,占空比 = CCRx / (ARR + 1)

通过修改CCRx的值,我们就能动态改变输出PWM的占空比,从而改变电机的平均电压,实现调速。

3.2 工程代码结构规划

一个清晰的项目结构有助于管理和维护。建议在Keil或你使用的IDE中这样组织:

Motor_PWM_OpenLoop/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c │ └── (其他用户文件) ├── Hardware/ │ ├── motor.c │ └── motor.h ├── Libraries/ │ ├── CMSIS/ │ ├── STM32F10x_StdPeriph_Driver/ │ └── (标准库文件) └── (工程配置文件)

我们把所有电机相关的操作封装在motor.c/h中,实现高内聚、低耦合。

3.3 motor.h 头文件设计

头文件定义了接口和关键参数,是代码的“使用说明书”。

#ifndef __MOTOR_H #define __MOTOR_H #include "stm32f10x.h" // 电机编号定义 typedef enum { MOTOR_A = 0, MOTOR_B } Motor_TypeDef; // 电机方向定义 typedef enum { MOTOR_FORWARD = 0, MOTOR_BACKWARD, MOTOR_BRAKE } Motor_DirTypeDef; // 电机初始化结构体(用于配置定时器和GPIO) typedef struct { TIM_TypeDef* TIMx; // 定时器,如TIM3 uint16_t TIM_Channel; // 定时器通道,如TIM_Channel_1 uint32_t RCC_APBxPeriph_TIM; // 定时器时钟,如RCC_APB1Periph_TIM3 GPIO_TypeDef* GPIOx; // PWM引脚所在GPIO组,如GPIOA uint16_t GPIO_Pin; // PWM引脚,如GPIO_Pin_6 uint32_t RCC_APB2Periph_GPIO; // GPIO时钟,如RCC_APB2Periph_GPIOA uint8_t GPIO_PinSource; // 引脚源,如GPIO_PinSource6 } Motor_PWMInitTypeDef; // 电机控制结构体(包含方向控制引脚) typedef struct { Motor_PWMInitTypeDef PWM_Config; // PWM配置 GPIO_TypeDef* DIR_GPIOx1; // 方向引脚1的GPIO组 uint16_t DIR_Pin1; // 方向引脚1 GPIO_TypeDef* DIR_GPIOx2; // 方向引脚2的GPIO组 uint16_t DIR_Pin2; // 方向引脚2 } Motor_InitTypeDef; // 函数声明 void Motor_Init(const Motor_InitTypeDef* Motorx); void Motor_SetSpeed(Motor_TypeDef Motorx, int16_t speed); // speed范围:-1000 ~ +1000 void Motor_Stop(Motor_TypeDef Motorx); #endif /* __MOTOR_H */

设计思路:这里我做了两层封装。Motor_PWMInitTypeDef专门管理PWM相关的硬件资源,而Motor_InitTypeDef在此基础上加入了方向控制引脚,形成一个完整的电机控制单元。使用int16_t类型的speed,正负代表方向,绝对值代表速度大小,这样上层控制逻辑会非常直观。

3.4 motor.c 源文件实现(核心)

这是重头戏,我们一步步拆解。

3.4.1 初始化函数Motor_Init

这个函数负责初始化控制一个电机所需的所有GPIO和定时器PWM通道。

// 假设我们定义了两个电机控制结构体 static Motor_InitTypeDef MotorA_Structure; static Motor_InitTypeDef MotorB_Structure; void Motor_Init(const Motor_InitTypeDef* Motorx) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 初始化方向控制GPIO引脚 RCC_APB2PeriphClockCmd(Motorx->DIR_GPIOx1 == GPIOA ? RCC_APB2Periph_GPIOA : Motorx->DIR_GPIOx1 == GPIOB ? RCC_APB2Periph_GPIOB : RCC_APB2Periph_GPIOC, ENABLE); // 实际项目中应对所有可能的GPIO组做判断,此处简化 GPIO_InitStructure.GPIO_Pin = Motorx->DIR_Pin1 | Motorx->DIR_Pin2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(Motorx->DIR_GPIOx1, &GPIO_InitStructure); // 注意:DIR_GPIOx1和DIR_GPIOx2可能是同一组,也可能是不同组。严谨的实现需要分别初始化。 // 初始化后,将方向引脚设为刹车状态(0,0)或(1,1) GPIO_ResetBits(Motorx->DIR_GPIOx1, Motorx->DIR_Pin1); GPIO_ResetBits(Motorx->DIR_GPIOx1, Motorx->DIR_Pin2); // 假设同组 // 2. 初始化PWM引脚 (复用推挽输出) RCC_APB2PeriphClockCmd(Motorx->PWM_Config.RCC_APB2Periph_GPIO, ENABLE); GPIO_InitStructure.GPIO_Pin = Motorx->PWM_Config.GPIO_Pin; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(Motorx->PWM_Config.GPIOx, &GPIO_InitStructure); // 3. 初始化定时器时基单元 // 使能定时器时钟(需要区分APB1和APB2) if (Motorx->PWM_Config.TIMx == TIM1 || Motorx->PWM_Config.TIMx == TIM8) { RCC_APB2PeriphClockCmd(Motorx->PWM_Config.RCC_APBxPeriph_TIM, ENABLE); } else { RCC_APB1PeriphClockCmd(Motorx->PWM_Config.RCC_APBxPeriph_TIM, ENABLE); } // 计算PWM频率。假设系统时钟72MHz,定时器预分频设为71,则计数器时钟为1MHz。 // 若ARR设为999,则PWM频率 = 1MHz / (999+1) = 1kHz。1kHz对于直流电机调速是常用频率。 TIM_TimeBaseStructure.TIM_Period = 999; // ARR值,决定周期 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频值,PSC TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(Motorx->PWM_Config.TIMx, &TIM_TimeBaseStructure); // 4. 初始化PWM输出比较模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 模式1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性:高电平有效 TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0,CCR值 // 根据不同的通道,调用不同的OC初始化函数 switch(Motorx->PWM_Config.TIM_Channel) { case TIM_Channel_1: TIM_OC1Init(Motorx->PWM_Config.TIMx, &TIM_OCInitStructure); TIM_OC1PreloadConfig(Motorx->PWM_Config.TIMx, TIM_OCPreload_Enable); // 使能预装载 break; case TIM_Channel_2: TIM_OC2Init(Motorx->PWM_Config.TIMx, &TIM_OCInitStructure); TIM_OC2PreloadConfig(Motorx->PWM_Config.TIMx, TIM_OCPreload_Enable); break; // ... 处理通道3和4 default: break; } // 5. 使能定时器的自动重装载预装载寄存器(ARR) TIM_ARRPreloadConfig(Motorx->PWM_Config.TIMx, ENABLE); // 6. 使能定时器 TIM_Cmd(Motorx->PWM_Config.TIMx, ENABLE); // 如果是高级定时器(TIM1/TIM8),还需要使能主输出 if (Motorx->PWM_Config.TIMx == TIM1 || Motorx->PWM_Config.TIMx == TIM8) { TIM_CtrlPWMOutputs(Motorx->PWM_Config.TIMx, ENABLE); } }

关键点解析

  1. GPIO模式:PWM引脚必须设置为GPIO_Mode_AF_PP(复用推挽输出)。这是很多新手容易忽略的地方,如果设成普通的推挽输出,定时器是无法控制该引脚输出的。
  2. 时钟使能:定时器时钟挂在不同的APB总线上。TIM1/TIM8在APB2,其他通用定时器在APB1。使能时钟前需要判断。
  3. PWM频率计算PWM频率 = TIMxCLK / ((PSC+1)*(ARR+1))。其中TIMxCLK是定时器的时钟源频率。如果APB1预分频器不为1,定时器时钟会倍频,需要查手册确认。对于F103,当APB1分频系数不为1时,挂在其上的定时器时钟是APB1时钟的2倍。一个稳妥的做法是在system_stm32f10x.c中查看SystemCoreClock,或者直接计算。我们这里假设系统时钟72MHz,APB1分频系数为2,则APB1时钟36MHz,TIM2-7的时钟为72MHz。设置PSC=71,则计数器时钟为1MHz。ARR=999,得到1kHz PWM波。
  4. 预装载寄存器TIM_OCxPreloadConfigTIM_ARRPreloadConfig使能了预装载功能。这意味着我们对CCR或ARR的修改,会在下一个更新事件时才生效,防止在PWM周期中间修改寄存器导致输出毛刺。
  5. 高级定时器:TIM1和TIM8有刹车、互补输出等高级功能,用于PWM输出时必须额外使能TIM_CtrlPWMOutputs
3.4.2 速度设置函数Motor_SetSpeed

这是控制电机的核心API,它将一个范围在-1000 ~ 1000的速度值,转换为具体的方向信号和PWM占空比。

void Motor_SetSpeed(Motor_TypeDef Motorx, int16_t speed) { Motor_InitTypeDef* pMotor; uint16_t dir_pin1_state, dir_pin2_state; uint16_t pwm_duty; // 1. 获取对应电机的控制结构体指针 if (Motorx == MOTOR_A) { pMotor = &MotorA_Structure; } else { pMotor = &MotorB_Structure; } // 2. 边界限制 if (speed > 1000) speed = 1000; if (speed < -1000) speed = -1000; // 3. 判断方向并设置方向控制引脚 if (speed > 0) { // 正转 dir_pin1_state = 0; dir_pin2_state = 1; pwm_duty = speed; // 速度值为正,直接作为占空比 } else if (speed < 0) { // 反转 dir_pin1_state = 1; dir_pin2_state = 0; pwm_duty = -speed; // 速度值为负,取绝对值作为占空比 } else { // 速度为零,刹车 dir_pin1_state = 0; dir_pin2_state = 0; pwm_duty = 0; } // 4. 应用方向控制信号(根据实际接线,可能需要对不同GPIO组单独操作) if (dir_pin1_state) { GPIO_SetBits(pMotor->DIR_GPIOx1, pMotor->DIR_Pin1); } else { GPIO_ResetBits(pMotor->DIR_GPIOx1, pMotor->DIR_Pin1); } if (dir_pin2_state) { GPIO_SetBits(pMotor->DIR_GPIOx1, pMotor->DIR_Pin2); // 假设同组 } else { GPIO_ResetBits(pMotor->DIR_GPIOx1, pMotor->DIR_Pin2); } // 5. 设置PWM占空比 (CCR值) // 我们的ARR是999,速度范围是0-1000,所以需要映射:CCR = (pwm_duty * 999) / 1000 // 使用整数运算避免浮点数,先乘后除减少精度损失 uint16_t ccr_value = (pwm_duty * 999) / 1000; switch(pMotor->PWM_Config.TIM_Channel) { case TIM_Channel_1: TIM_SetCompare1(pMotor->PWM_Config.TIMx, ccr_value); break; case TIM_Channel_2: TIM_SetCompare2(pMotor->PWM_Config.TIMx, ccr_value); break; // ... 处理通道3和4 default: break; } }

设计精妙之处

  1. 统一的控制接口:用一个speed参数同时控制方向和速度,上层应用逻辑非常简洁。想正转全速就传1000,反转半速就传-500,停止传0
  2. 死区与刹车处理:当speed为0时,我们将方向引脚都置0,这是TB6612的“刹车”模式之一,能让电机快速停止,而不是“滑行”。这是一种简单的开环制动。
  3. 占空比映射:将0-1000的线性速度值,线性映射到0-999的CCR值。这里使用了整数运算(a * b) / c的模式,在嵌入式系统中效率高于浮点数运算。注意,当pwm_duty=1000时,ccr_value=999,对应100%占空比(因为占空比=CCR/(ARR+1))。
3.4.3 停止函数Motor_Stop

这是一个便捷函数,用于快速停止电机。

void Motor_Stop(Motor_TypeDef Motorx) { Motor_SetSpeed(Motorx, 0); // 直接调用速度设置函数,设置为0即可刹车 }

3.5 main.c 主程序示例

最后,我们在主函数中将这些模块组装起来,并实现一个简单的加速-匀速-减速-反转的测试程序。

#include "stm32f10x.h" #include "motor.h" #include "delay.h" // 需要一个简单的延时函数,可以用SysTick实现 // 定义电机A的硬件配置 const Motor_InitTypeDef MotorA_Config = { .PWM_Config = { .TIMx = TIM3, .TIM_Channel = TIM_Channel_1, .RCC_APBxPeriph_TIM = RCC_APB1Periph_TIM3, .GPIOx = GPIOA, .GPIO_Pin = GPIO_Pin_6, .RCC_APB2Periph_GPIO = RCC_APB2Periph_GPIOA, .GPIO_PinSource = GPIO_PinSource6, }, .DIR_GPIOx1 = GPIOA, .DIR_Pin1 = GPIO_Pin_4, .DIR_GPIOx2 = GPIOA, .DIR_Pin2 = GPIO_Pin_5, }; int main(void) { // 初始化系统时钟(通常由SystemInit()在启动文件调用,这里假设已配置为72MHz) // 初始化延时函数 Delay_Init(); // 初始化电机A Motor_Init(&MotorA_Config); while(1) { // 测试1: 正转加速 for(int16_t i = 0; i <= 1000; i += 50) { Motor_SetSpeed(MOTOR_A, i); Delay_ms(100); // 每100ms增加5%速度 } Delay_ms(2000); // 全速正转2秒 // 测试2: 正转减速 for(int16_t i = 1000; i >= 0; i -= 50) { Motor_SetSpeed(MOTOR_A, i); Delay_ms(100); } Delay_ms(1000); // 停止1秒 // 测试3: 反转加速 for(int16_t i = 0; i >= -1000; i -= 50) { Motor_SetSpeed(MOTOR_A, i); Delay_ms(100); } Delay_ms(2000); // 全速反转2秒 // 测试4: 反转减速 for(int16_t i = -1000; i <= 0; i += 50) { Motor_SetSpeed(MOTOR_A, i); Delay_ms(100); } Delay_ms(1000); // 停止1秒 } }

4. 调试与排坑:让电机真正听话

代码写完,下载到板子,电机可能不转,或者行为异常。别急,这是嵌入式开发的常态。按照以下步骤排查,能解决99%的问题。

4.1 电源与地线:一切的基础

这是最核心也最容易出错的一步。

  • 现象:电机完全不转,驱动芯片发热严重,STM32可能复位或不工作。
  • 排查
    1. 共地!共地!共地!用万用表蜂鸣档,确保STM32的GND、TB6612的GND、电机电源的GND是直接连通的。任何一点悬空都会导致信号参考电平混乱。
    2. 电源隔离:电机电源(接VM)和单片机逻辑电源(接VCC)最好使用两个独立的电源。如果必须共用,务必在VM输入端加入一个大容量(如1000uF)的电解电容和一个104的瓷片电容进行滤波,并确保电源功率足够。电机启动瞬间的电流冲击(浪涌电流)足以让稳压芯片崩溃,导致单片机复位。
    3. 电压匹配:确认TB6612的VCC引脚接到了正确的逻辑电平(3.3V或5V),VM接到了电机额定电压范围内。

4.2 信号测量:用逻辑分析仪/示波器说话

没有仪器?一个简单的LED或万用表电压档也能做初步判断。

  • 现象:电机不转或一直全速转。
  • 排查
    1. 方向信号:在程序运行中,用万用表测量AIN1AIN2引脚对地的电压。当你调用Motor_SetSpeed(MOTOR_A, 500)时,应该测量到AIN1=0V,AIN2=3.3V(假设逻辑电平3.3V)。如果电压不对,检查GPIO初始化代码和Motor_SetSpeed函数中设置引脚的部分。
    2. PWM信号:这是关键。将示波器或逻辑分析仪探头接到PWMA引脚。
      • 是否有波形?如果没有,检查定时器时钟是否使能、GPIO是否配置为复用推挽输出、定时器是否被使能(TIM_Cmd)、对于TIM1/TIM8是否使能了主输出(TIM_CtrlPWMOutputs)。
      • 频率是否正确?测量波形的周期,是否接近我们计算的1ms(1kHz)?如果差很多,检查TIM_PeriodTIM_Prescaler的设置,以及系统主频是否正确。
      • 占空比是否变化?Motor_SetSpeed函数中设置断点,或让速度循环变化,观察示波器上脉冲宽度是否随之变化。如果占空比不变,检查TIM_SetComparex函数是否被正确调用,通道号是否匹配。

4.3 软件逻辑与常见错误

  • 现象:电机只能单向转,或者调速不线性。
  • 排查
    1. 方向逻辑错误:对照TB6612的真值表,确保你的(AIN1, AIN2)组合在正转、反转、刹车时是正确的。最常见的错误是把(1,0)(0,1)搞反了。
    2. 占空比映射错误:在Motor_SetSpeed函数中,打印或通过调试器观察计算出的ccr_value。当你传入speed=500时,ccr_value应该是(500*999)/1000=499。如果这个值不对,检查映射公式。
    3. ARR与CCR的关系:记住,有效占空比 = CCR / (ARR + 1)。如果你设置ARR=999,那么CCR=999时占空比是999/1000=99.9%,并非100%。如果你需要绝对的100%占空比,一种方法是设置CCR大于ARR(在PWM模式1下,输出会持续为有效电平),但更规范的做法是直接控制方向引脚让电机全速,同时将PWM占空比设为100%(即CCR=ARR+1?不,应该是CCR=ARR,然后使能输出比较快速模式?)。更简单的做法是,在代码中判断如果speed的绝对值大于某个阈值(如990),就直接将方向引脚设为全速状态,并关闭PWM输出(或设CCR=ARR)。不过对于开环控制,99.9%和100%的差别几乎感觉不到。
    4. 初始化顺序:确保先配置好GPIO和定时器,最后才使能定时器(TIM_Cmd)。如果顺序乱了,可能会在初始化完成前就产生不正确的PWM边沿。

4.4 进阶:电机启动与低速“死区”

  • 现象:电机在低速时(比如speed<100)不启动,或者抖动,一旦超过某个阈值就突然转起来。
  • 分析与解决:这是直流有刷电机的特性——静摩擦力矩。电机从静止到转动需要克服一定的静摩擦,所需的启动电压(即PWM占空比)比维持转动所需的电压高。
    • 解决方案1(软件):在Motor_SetSpeed函数中,对低速区进行非线性补偿。例如,当目标速度绝对值小于阈值DEAD_ZONE(比如150)时,实际输出的PWM占空比用一个更大的值。可以做一个简单的映射表:实际CCR = 补偿曲线(目标speed)。这被称为“启动助推”。
    • 解决方案2(硬件):选择启动扭矩更大的电机,或者给电机轴加装减速箱,降低启动负载。

    注意:这不是你的代码或驱动有问题,而是电机本身的物理特性。理解这一点,就从“让电机转”进阶到了“让电机转得好”。

5. 从开环到闭环:下一步该想什么?

当你成功让电机随着你的代码指令平滑地加速、减速、正反转时,你已经完成了开环控制。但开环控制有个致命问题:它无法抵抗干扰。如果负载突然变重,电机转速就会下降;如果电池电压降低,转速也会变慢。因为你只是“希望”电机以某个速度转,并没有去“检查”它是否真的以那个速度在转。

这就是闭环控制(比如PID)要解决的问题。你需要一个反馈传感器来告诉控制器电机的真实转速。常用的有:

  1. 编码器:安装在电机轴上,每转一圈输出固定数量的脉冲。通过定时器捕获这些脉冲的频率,就能精确计算转速。这是最常用、精度最高的方案。
  2. 霍尔传感器:有些电机内部集成了霍尔元件,每转输出几个脉冲(如MG310电机有3个霍尔),精度较低但成本也低。
  3. 测速发电机:输出与转速成正比的电压信号,需要ADC采样。

开环是闭环的基础。你现在已经搭建好了“执行机构”(PWM驱动电机)和“控制器”(STM32)。下一步,就是为这个系统装上“眼睛”(传感器),并设计一个“大脑算法”(PID控制器),让系统能够根据“期望速度”和“实际速度”的偏差,自动调整PWM占空比,从而在负载变化时也能保持转速稳定。

到那时,你可以沿用本文的硬件框架和软件架构,只需新增编码器读取的代码(通常使用定时器的编码器接口模式),并在主循环中增加一个PID计算函数,用其输出结果来调用Motor_SetSpeed。你会发现,有了清晰的模块化设计,功能的叠加会变得非常顺畅。

最后,分享一个我自己的小习惯:在调试电机这类大电流设备时,我总是在电源线上串联一个保险丝(比如2A),并在VM和GND之间并联一个大功率的续流二极管(如1N5822)。保险丝能在短路时保护你的电源和芯片,而续流二极管能吸收电机线圈在PWM关断时产生的反向电动势,保护驱动芯片的MOSFET。这些小投入,往往能避免整个实验板的“阵亡”。