TIP旧版文章也发布在 CSDN 和本站的《一文带你搞懂电机驱动模块》。本版修正代码,并增加了互动实验。
单片机 GPIO 只能输出控制电平,无法承担电机电流。电机启动、堵转和换向时会吸取大电流;线圈断电时还会产生反向电动势。
控制系统分成两层:
- 单片机给出方向、速度和启停命令;
- 电机驱动模块承受电压与电流,把逻辑电平转换成功率输出。
对象是有刷直流电机。调节 5 个实验,把一个 -100% ~ +100% 的速度命令依次对应到 H 桥电流、PWM 波形、L298N 接线和单片机代码。
电压决定方向,也影响转速
直流电机通电后开始旋转。对调两根电机线,电流方向和转向一起反转。
保持电源和负载不变,提高电机端的有效电压,稳定转速会随之升高。同一电压下,负载增加会拉低转速并抬高电流。
电压、方向与负载
拖动端电压,观察转向和转速。保持电压不变,再增加机械负载。
动画只演示变化趋势。实测转速还取决于绕组参数、电源压降、摩擦和供电能力。
从实验中观察两件事:
- 电机两端电压的方向决定转向。
- 有效驱动影响速度,负载和供电能力也会改变结果。
手动对调电机线可以换向。H 桥让单片机用电平信号完成同样的操作。
H 桥:用四个开关改变电流方向
四个开关分列电机两侧,电机位于中间,电路形状像字母 H,因此得名 H 桥(H-Bridge)。
一组对角开关控制正转,另一组控制反转。停止方式分为两种:
- 自由滑行(coast):断开电机,让转子靠惯性停下;
- 快速制动(brake):把电机两端接到相同电位,让旋转中的电机产生制动力。
切换 H 桥的五种状态
选择一个状态,图中会标出导通开关和电流路径。桥臂直通用于展示错误接法。
正转和反转各使用一组对角开关。换向前先降低转速,避免反向电流冲击。
实验用机械开关表示电流路径。驱动芯片使用双极型晶体管或 MOSFET,并配置栅极或基极驱动、互锁、死区、过温保护和续流路径。
同一桥臂的上管和下管不能同时导通。 电流一旦绕过电机从电源流向地,就会发生桥臂直通。芯片保护只能降低损坏概率,控制逻辑仍需排除这个状态。
从 H 桥到电机驱动模块
驱动芯片接收 IN1、IN2、ENA 等逻辑信号,再控制内部的四个功率开关。
以 L298 的 A 路 H 桥为例:
IN1、IN2决定两个输出端的电位;ENA为高时,输入命令生效;ENA为低时,整座桥被关闭,电机自由滑行。
切换 L298 输入,核对真值表
点击 ENA、IN1、IN2,查看 OUT1、OUT2 和电机状态。表中逻辑来自 L298 数据手册。
| ENA | IN1 | IN2 | 动作 |
|---|---|---|---|
| 1 | 1 | 0 | 正转 |
| 1 | 0 | 1 | 反转 |
| 1 | 0 | 0 | 快速制动 |
| 1 | 1 | 1 | 快速制动 |
| 0 | X | X | 自由滑行 |
OUT1 高、OUT2 低,电流从左向右流过电机。 TB6612、DRV8833 等器件使用不同的真值表、待机脚和制动定义。
ST L298 数据手册 给出了双向直流电机真值表:ENA=1 时,两个输入不同对应正转或反转,两个输入相同对应快速制动;ENA=0 对应自由滑行。
这张表只适用于 L298。其他驱动器可能使用 AIN1/AIN2、PH/EN 或 STBY,制动和休眠状态也有差异。换芯片时请按新器件的真值表改接线和代码。
PWM:用开关时间调节出力
定时器让 GPIO 在高、低电平之间快速切换。37% 占空比表示每个周期有 37% 的时间导通:
- 导通一小段时间;
- 关闭一小段时间;
- 定时器在下一个周期重复这组动作。
电机线圈和转子的机械惯性会平滑这些开关脉冲。占空比升高后,电机获得的有效驱动随之增加。
把有符号速度命令转换成波形
正负号选择方向,绝对值转换为占空比。切换接线方案,比较两种波形。
ENA 输出 PWM,IN1 和 IN2 在一个 PWM 周期内保持方向电平。
推荐的三线控制
后文代码采用三线控制:
ENA接定时器 PWM;IN1、IN2接普通 GPIO;- 正负号决定
IN1/IN2; - 绝对值决定
ENA占空比。
方向 GPIO 和 PWM 各管一件事。反转时无需换算 PWM 比较值,方向与占空比可以分别核对。
两线 PWM + DIR 的占空比换算
如果为了节省一个控制引脚,把 IN1 接 PWM、IN2 接 DIR:
DIR=0时,PWM 高电平区间产生正向电压;DIR=1时,PWM 低电平区间才产生反向电压。
同样输出 70% 时,正转写入 70% 比较值,反转要写入 30%。这种接法还会改变 PWM 关闭阶段的制动或滑行状态。初次接线可先采用三线方案。
选择 PWM 频率
低频 PWM 可能让电机发出啸叫,并加大电流和转矩波动。提高频率可以避开部分可听范围,同时也会增加驱动器的开关损耗。老器件还可能限制最高开关频率。
示例使用 10 kHz。项目选型时请检查:
- 驱动芯片允许的开关频率;
- 电机的电感与噪声;
- 功率管或芯片温升;
- 定时器时钟和所需占空比分辨率。
L298N 实战接线
L298 内含两座 H 桥,可控制两台有刷直流电机或一台双极步进电机。接线示例只用 A 路:
ENA、IN1、IN2控制OUT1、OUT2;- B 路对应
ENB、IN3、IN4和OUT3、OUT4。
上电前检查接线
切换平台或断开连接,图中会标出缺失的电源、共地、信号线和电机线。
D11 → ENAD10 → IN1D9 → IN2架空车轮或卸下机械负载,设置电源限流,再用 10% 至 20% 占空比测试方向。接线以模块丝印和原理图为准。
上电前检查
- 给电机准备独立电源。 开发板的 5 V 引脚无法承担启动或堵转电流。
- 单片机 GND 与驱动模块 GND 共地。 否则逻辑电平没有共同参考。
- ENA 跳帽状态与接法一致。 使用 PWM 调速时,拔掉把 ENA 固定拉高的跳帽,再接 PWM 引脚。
- 确认模块的 5 V 逻辑供电方式。 各家 L298N 模块使用不同的稳压器、跳帽和丝印,请按手中模块的说明接线。
- 先架空负载并限流。 第一次测试从小占空比开始,先验证方向,再装回机械结构。
原厂 L298 把逻辑高电平门限最低值定为 2.3 V,因此 STM32F103 的 3.3 V GPIO 满足要求。成品模块还包含外围电路,接线前请检查模块原理图。
Arduino Uno 完整示例
接线约定:
D11 → ENA;D10 → IN1;D9 → IN2;- Arduino GND 与 L298N GND 共地。
#include <Arduino.h>
constexpr uint8_t MOTOR_ENA = 11;constexpr uint8_t MOTOR_IN1 = 10;constexpr uint8_t MOTOR_IN2 = 9;
// speed 范围:-255 ~ +255// 正数正转,负数反转,0 自由滑行停止。void motorSetSpeed(int16_t speed) { speed = constrain(speed, -255, 255);
if (speed == 0) { analogWrite(MOTOR_ENA, 0); digitalWrite(MOTOR_IN1, LOW); digitalWrite(MOTOR_IN2, LOW); return; }
if (speed > 0) { digitalWrite(MOTOR_IN1, HIGH);41 collapsed lines
digitalWrite(MOTOR_IN2, LOW); } else { digitalWrite(MOTOR_IN1, LOW); digitalWrite(MOTOR_IN2, HIGH); }
analogWrite(MOTOR_ENA, abs(speed));}
void setup() { pinMode(MOTOR_ENA, OUTPUT); pinMode(MOTOR_IN1, OUTPUT); pinMode(MOTOR_IN2, OUTPUT); motorSetSpeed(0);}
void loop() { // 约 2.5 秒内逐渐加速到正向全速。 for (int16_t speed = 0; speed <= 255; ++speed) { motorSetSpeed(speed); delay(10); }
// 先减到零,再换向,避免满速直接反转。 for (int16_t speed = 255; speed >= 0; --speed) { motorSetSpeed(speed); delay(10); } delay(300);
for (int16_t speed = 0; speed >= -255; --speed) { motorSetSpeed(speed); delay(10); }
for (int16_t speed = -255; speed <= 0; ++speed) { motorSetSpeed(speed); delay(10); } delay(800);}analogWrite() 使用开发板预设的 PWM 频率,Arduino Uno 的默认值并非 10 kHz。产品若对噪声或转矩纹波有要求,请配置定时器。
STM32F103 标准外设库完整示例
示例配置使用 72 MHz 系统时钟:
PA6 / TIM3_CH1 → ENA;PB0 → IN1;PB1 → IN2;TIM3计数周期为 7200 个计数,对应 10 kHz PWM。
speed 的范围为 -1000 ~ +1000,命令步进为 0.1%。定时器周期决定 PWM 的有效分辨率。
#include "stm32f10x.h"
#define MOTOR_PWM_TOP 7200U#define MOTOR_IN1_PORT GPIOB#define MOTOR_IN1_PIN GPIO_Pin_0#define MOTOR_IN2_PORT GPIOB#define MOTOR_IN2_PIN GPIO_Pin_1
static void Motor_GPIO_Init(void){ GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE );
// PA6 = TIM3_CH1 = ENA PWM gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_AF_PP;112 collapsed lines
gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio);
// PB0、PB1 = IN1、IN2 gpio.GPIO_Pin = MOTOR_IN1_PIN | MOTOR_IN2_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio);
GPIO_ResetBits(GPIOB, MOTOR_IN1_PIN | MOTOR_IN2_PIN);}
static void Motor_PWM_Init(void){ TIM_TimeBaseInitTypeDef timer; TIM_OCInitTypeDef pwm;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// TIM3 时钟 72 MHz:72 MHz / 7200 = 10 kHz timer.TIM_Prescaler = 0; timer.TIM_CounterMode = TIM_CounterMode_Up; timer.TIM_Period = MOTOR_PWM_TOP - 1U; timer.TIM_ClockDivision = TIM_CKD_DIV1; timer.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3, &timer);
pwm.TIM_OCMode = TIM_OCMode_PWM1; pwm.TIM_OutputState = TIM_OutputState_Enable; pwm.TIM_Pulse = 0; pwm.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &pwm); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);}
void Motor_Init(void){ Motor_GPIO_Init(); Motor_PWM_Init(); TIM_SetCompare1(TIM3, 0);}
// speed 范围:-1000 ~ +1000void Motor_SetSpeed(int16_t speed){ uint32_t pulse;
if (speed > 1000) speed = 1000; if (speed < -1000) speed = -1000;
if (speed == 0) { TIM_SetCompare1(TIM3, 0); // ENA 持续为低:自由滑行 GPIO_ResetBits(GPIOB, MOTOR_IN1_PIN | MOTOR_IN2_PIN); return; }
// 先关闭 ENA,再改变方向脚,避免切换瞬间产生错误状态。 TIM_SetCompare1(TIM3, 0);
if (speed > 0) { GPIO_SetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN); GPIO_ResetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN); pulse = ((uint32_t)speed * MOTOR_PWM_TOP) / 1000U; } else { GPIO_ResetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN); GPIO_SetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN); pulse = ((uint32_t)(-speed) * MOTOR_PWM_TOP) / 1000U; }
TIM_SetCompare1(TIM3, pulse);}
static void Delay(volatile uint32_t count){ while (count-- != 0U) { __NOP(); }}
int main(void){ int16_t speed;
Motor_Init();
while (1) { for (speed = 0; speed <= 1000; speed += 10) { Motor_SetSpeed(speed); Delay(50000); } for (speed = 1000; speed >= 0; speed -= 10) { Motor_SetSpeed(speed); Delay(50000); }
Delay(500000); // 确认减速到零后再反转
for (speed = 0; speed >= -1000; speed -= 10) { Motor_SetSpeed(speed); Delay(50000); } for (speed = -1000; speed <= 0; speed += 10) { Motor_SetSpeed(speed); Delay(50000); }
Delay(500000); }}空循环延时只负责放慢演示,无法提供精确定时。项目代码应改用 SysTick、硬件定时器或 RTOS 延时,并用状态机管理加减速和换向等待。
STM32F103 HAL 库完整示例
HAL 版本沿用同一组引脚。先在 STM32CubeMX 中完成以下配置:
- 把
PA6配为TIM3_CH1 / PWM Generation CH1; - 把
PB0、PB1配为推挽输出,标签分别设为MOTOR_IN1、MOTOR_IN2; - 若 TIM3 时钟为 72 MHz,设置
Prescaler = 0、Counter Period = 7199; - PWM 极性选 High,初始 Pulse 设为 0。
CubeMX 生成时钟、GPIO 和 MX_TIM3_Init() 后,把以下驱动代码加入工程:
#ifndef MOTOR_H#define MOTOR_H
#include "main.h"
void Motor_Init(void);void Motor_SetSpeed(int16_t speed);
#endif#include "motor.h"#include "tim.h"
#define MOTOR_PWM_TOP 7200U
void Motor_Init(void){ HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);}
// speed 范围:-1000 ~ +1000void Motor_SetSpeed(int16_t speed){ uint32_t pulse;
if (speed > 1000) speed = 1000; if (speed < -1000) speed = -1000;23 collapsed lines
if (speed == 0) { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET); return; }
// 先关闭 ENA,再更新方向。 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
if (speed > 0) { HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET); pulse = ((uint32_t)speed * MOTOR_PWM_TOP) / 1000U; } else { HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_SET); pulse = ((uint32_t)(-speed) * MOTOR_PWM_TOP) / 1000U; }
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);}在 CubeMX 生成的 main.c 中调用:
#include "motor.h"
int main(void){ int16_t speed;
HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); Motor_Init();
while (1) { for (speed = 0; speed <= 1000; speed += 10) { Motor_SetSpeed(speed); HAL_Delay(10); } for (speed = 1000; speed >= 0; speed -= 10) { Motor_SetSpeed(speed); HAL_Delay(10);16 collapsed lines
}
HAL_Delay(300);
for (speed = 0; speed >= -1000; speed -= 10) { Motor_SetSpeed(speed); HAL_Delay(10); } for (speed = -1000; speed <= 0; speed += 10) { Motor_SetSpeed(speed); HAL_Delay(10); }
HAL_Delay(800); }}APB1 分频和系统时钟会改变 TIM3 的计数时钟。请在 CubeMX 的 Clock Configuration 中确认定时器时钟,再计算 Prescaler 和 Period。
五个工程风险
满速直接反转
电机高速旋转时施加反向电压,会产生制动电流和反向电流。换向流程应按顺序执行:
- 逐步减小 PWM;
- 等待转速下降,必要时读取编码器;
- 关闭 ENA;
- 改变方向;
- 再逐步增加 PWM。
示例代码先减速到零再反转。Motor_SetSpeed() 不读取转速,系统需要通过等待时间或编码器判断电机是否停稳。
只看额定电流,不看堵转电流
电机轴卡住后,反电动势消失,电流会远高于正常运行值。选择驱动器和电源时,请使用数据手册中的堵转电流,或通过限流测试取得实测值。空载电流不能用于功率选型。
忽略驱动器压降与散热
L298 使用双极型晶体管。原厂数据手册给出的桥总饱和压降在 1 A 条件下典型值约为 1.8 V,最坏条件更高。驱动器把这部分压降转化为热量,电机得到的电压也随之降低。
L298N 可用于教学和已有模块维护。低压、电池供电或高效率设计可选择现代 MOSFET 驱动器。
忽略感性负载的续流路径
开关关断后,电机线圈仍会维持电流,因此电路需要续流路径。常见 L298N 模块已安装续流二极管;裸 L298 芯片和自制功率板需要按数据手册配置外部二极管与电源退耦电容。
把保护电路当成正常工作方式
过温保护、保险丝和限流电源会降低损坏概率。设计者仍需保证接线正确,留出电流余量,并处理软件状态。保护反复触发时,请检查负载、散热和换向流程。
首次上电顺序
按以下顺序完成首次测试:
- 断开电机,确认控制器和驱动模块已经共地;
- 用万用表确认电机电源极性与电压;
- 保持
ENA=0上电,检查没有异常发热; - 架空电机或卸下机械负载;
- 给 10%~20% 占空比,只验证一个方向;
- 减速到零,再验证反方向;
- 逐步增加负载,同时观察电源电流与芯片温升;
- 最后再接入完整机械结构和自动控制流程。
小结
GPIO 发送命令,驱动模块向电机提供功率。H 桥控制正转、反转、快速制动和自由滑行,PWM 控制有效出力。有符号速度命令进入驱动函数后,正负号选择方向,绝对值转换为占空比。
拿到一款新驱动器时,先从数据手册确认方向引脚、PWM 引脚和关闭状态,再检查堵转电流、压降、散热、共地和换向流程。