3233 字
16 分钟
一文带你搞懂直流电机驱动(新版)
TIP

旧版文章也发布在 CSDN 和本站的《一文带你搞懂电机驱动模块》。本版修正代码,并增加了互动实验。

单片机 GPIO 只能输出控制电平,无法承担电机电流。电机启动、堵转和换向时会吸取大电流;线圈断电时还会产生反向电动势。

控制系统分成两层:

  • 单片机给出方向、速度和启停命令;
  • 电机驱动模块承受电压与电流,把逻辑电平转换成功率输出。

对象是有刷直流电机。调节 5 个实验,把一个 -100% ~ +100% 的速度命令依次对应到 H 桥电流、PWM 波形、L298N 接线和单片机代码。

电压决定方向,也影响转速#

直流电机通电后开始旋转。对调两根电机线,电流方向和转向一起反转。

保持电源和负载不变,提高电机端的有效电压,稳定转速会随之升高。同一电压下,负载增加会拉低转速并抬高电流。

电压、方向与负载

拖动端电压,观察转向和转速。保持电压不变,再增加机械负载。

电源 → 电机 → 负载
可调电源+7.2 V机械负载25%正转 · 相对转速 49%
端电压7.2 V
方向正转
相对转速49%
出力负担36%
调节工作条件

动画只演示变化趋势。实测转速还取决于绕组参数、电源压降、摩擦和供电能力。

从实验中观察两件事:

  1. 电机两端电压的方向决定转向。
  2. 有效驱动影响速度,负载和供电能力也会改变结果。

手动对调电机线可以换向。H 桥让单片机用电平信号完成同样的操作。

H 桥:用四个开关改变电流方向#

四个开关分列电机两侧,电机位于中间,电路形状像字母 H,因此得名 H 桥(H-Bridge)

一组对角开关控制正转,另一组控制反转。停止方式分为两种:

  • 自由滑行(coast):断开电机,让转子靠惯性停下;
  • 快速制动(brake):把电机两端接到相同电位,让旋转中的电机产生制动力。

切换 H 桥的五种状态

选择一个状态,图中会标出导通开关和电流路径。桥臂直通用于展示错误接法。

H 桥电流路径
+VSGNDS1S3S2S4M左上与右下导通,电流从左向右穿过电机。
导通开关S1 + S4
电机端状态+VS
结果正转
选择开关状态
工作状态

正转和反转各使用一组对角开关。换向前先降低转速,避免反向电流冲击。

实验用机械开关表示电流路径。驱动芯片使用双极型晶体管或 MOSFET,并配置栅极或基极驱动、互锁、死区、过温保护和续流路径。

同一桥臂的上管和下管不能同时导通。 电流一旦绕过电机从电源流向地,就会发生桥臂直通。芯片保护只能降低损坏概率,控制逻辑仍需排除这个状态。

从 H 桥到电机驱动模块#

驱动芯片接收 IN1IN2ENA 等逻辑信号,再控制内部的四个功率开关。

以 L298 的 A 路 H 桥为例:

  • IN1IN2 决定两个输出端的电位;
  • ENA 为高时,输入命令生效;
  • ENA 为低时,整座桥被关闭,电机自由滑行。

切换 L298 输入,核对真值表

点击 ENA、IN1、IN2,查看 OUT1、OUT2 和电机状态。表中逻辑来自 L298 数据手册。

逻辑输入与功率输出
L298 · Bridge A逻辑译码 + 功率桥ENA = 1IN1 = 1IN2 = 0OUT1 OUT2 正转
OUT1
OUT2
电机动作正转
切换输入电平
ENAIN1IN2动作
110正转
101反转
100快速制动
111快速制动
0XX自由滑行

OUT1 高、OUT2 低,电流从左向右流过电机。 TB6612、DRV8833 等器件使用不同的真值表、待机脚和制动定义。

ST L298 数据手册 给出了双向直流电机真值表:ENA=1 时,两个输入不同对应正转或反转,两个输入相同对应快速制动;ENA=0 对应自由滑行。

这张表只适用于 L298。其他驱动器可能使用 AIN1/AIN2PH/ENSTBY,制动和休眠状态也有差异。换芯片时请按新器件的真值表改接线和代码。

PWM:用开关时间调节出力#

定时器让 GPIO 在高、低电平之间快速切换。37% 占空比表示每个周期有 37% 的时间导通:

  1. 导通一小段时间;
  2. 关闭一小段时间;
  3. 定时器在下一个周期重复这组动作。

电机线圈和转子的机械惯性会平滑这些开关脉冲。占空比升高后,电机获得的有效驱动随之增加。

把有符号速度命令转换成波形

正负号选择方向,绝对值转换为占空比。切换接线方案,比较两种波形。

三个引脚的时间波形
ENAIN1IN2一个示意周期 · 实际频率 10.0 kHz
速度命令+60%
方向正转
有效占空比60%
平均驱动方向+60%
速度接口与接线方案
控制方案

ENA 输出 PWM,IN1 和 IN2 在一个 PWM 周期内保持方向电平。

推荐的三线控制#

后文代码采用三线控制:

  • ENA 接定时器 PWM;
  • IN1IN2 接普通 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 路:

  • ENAIN1IN2 控制 OUT1OUT2
  • B 路对应 ENBIN3IN4OUT3OUT4

上电前检查接线

切换平台或断开连接,图中会标出缺失的电源、共地、信号线和电机线。

Arduino Uno ↔ L298N ↔ 电机
Arduino Uno5 V GPIOL298N 模块L298NMENA / PWMIN1IN2电机电源必须共地
控制器Arduino Uno
必要连接5 / 5
上电判断允许低速测试
平台与检查项
单片机平台
D11 → ENAD10 → IN1D9 → IN2

架空车轮或卸下机械负载,设置电源限流,再用 10% 至 20% 占空比测试方向。接线以模块丝印和原理图为准。

上电前检查#

  1. 给电机准备独立电源。 开发板的 5 V 引脚无法承担启动或堵转电流。
  2. 单片机 GND 与驱动模块 GND 共地。 否则逻辑电平没有共同参考。
  3. ENA 跳帽状态与接法一致。 使用 PWM 调速时,拔掉把 ENA 固定拉高的跳帽,再接 PWM 引脚。
  4. 确认模块的 5 V 逻辑供电方式。 各家 L298N 模块使用不同的稳压器、跳帽和丝印,请按手中模块的说明接线。
  5. 先架空负载并限流。 第一次测试从小占空比开始,先验证方向,再装回机械结构。

原厂 L298 把逻辑高电平门限最低值定为 2.3 V,因此 STM32F103 的 3.3 V GPIO 满足要求。成品模块还包含外围电路,接线前请检查模块原理图。

Arduino Uno 完整示例#

接线约定:

  • D11 → ENA
  • D10 → IN1
  • D9 → IN2
  • Arduino GND 与 L298N GND 共地。
Arduino Uno:motor_driver.ino
#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 的有效分辨率。

STM32F103 标准外设库:main.c
#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 ~ +1000
void 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 中完成以下配置:

  1. PA6 配为 TIM3_CH1 / PWM Generation CH1
  2. PB0PB1 配为推挽输出,标签分别设为 MOTOR_IN1MOTOR_IN2
  3. 若 TIM3 时钟为 72 MHz,设置 Prescaler = 0Counter Period = 7199
  4. PWM 极性选 High,初始 Pulse 设为 0。

CubeMX 生成时钟、GPIO 和 MX_TIM3_Init() 后,把以下驱动代码加入工程:

STM32F103 HAL:motor.h
#ifndef MOTOR_H
#define MOTOR_H
#include "main.h"
void Motor_Init(void);
void Motor_SetSpeed(int16_t speed);
#endif
STM32F103 HAL:motor.c
#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 ~ +1000
void 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 中调用:

STM32F103 HAL: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。

五个工程风险#

满速直接反转#

电机高速旋转时施加反向电压,会产生制动电流和反向电流。换向流程应按顺序执行:

  1. 逐步减小 PWM;
  2. 等待转速下降,必要时读取编码器;
  3. 关闭 ENA;
  4. 改变方向;
  5. 再逐步增加 PWM。

示例代码先减速到零再反转。Motor_SetSpeed() 不读取转速,系统需要通过等待时间或编码器判断电机是否停稳。

只看额定电流,不看堵转电流#

电机轴卡住后,反电动势消失,电流会远高于正常运行值。选择驱动器和电源时,请使用数据手册中的堵转电流,或通过限流测试取得实测值。空载电流不能用于功率选型。

忽略驱动器压降与散热#

L298 使用双极型晶体管。原厂数据手册给出的桥总饱和压降在 1 A 条件下典型值约为 1.8 V,最坏条件更高。驱动器把这部分压降转化为热量,电机得到的电压也随之降低。

L298N 可用于教学和已有模块维护。低压、电池供电或高效率设计可选择现代 MOSFET 驱动器。

忽略感性负载的续流路径#

开关关断后,电机线圈仍会维持电流,因此电路需要续流路径。常见 L298N 模块已安装续流二极管;裸 L298 芯片和自制功率板需要按数据手册配置外部二极管与电源退耦电容。

把保护电路当成正常工作方式#

过温保护、保险丝和限流电源会降低损坏概率。设计者仍需保证接线正确,留出电流余量,并处理软件状态。保护反复触发时,请检查负载、散热和换向流程。

首次上电顺序#

按以下顺序完成首次测试:

  1. 断开电机,确认控制器和驱动模块已经共地;
  2. 用万用表确认电机电源极性与电压;
  3. 保持 ENA=0 上电,检查没有异常发热;
  4. 架空电机或卸下机械负载;
  5. 给 10%~20% 占空比,只验证一个方向;
  6. 减速到零,再验证反方向;
  7. 逐步增加负载,同时观察电源电流与芯片温升;
  8. 最后再接入完整机械结构和自动控制流程。

小结#

GPIO 发送命令,驱动模块向电机提供功率。H 桥控制正转、反转、快速制动和自由滑行,PWM 控制有效出力。有符号速度命令进入驱动函数后,正负号选择方向,绝对值转换为占空比。

拿到一款新驱动器时,先从数据手册确认方向引脚、PWM 引脚和关闭状态,再检查堵转电流、压降、散热、共地和换向流程。

一文带你搞懂直流电机驱动(新版)
https://cloudsir.top/posts/how-to-use-motor-v2/
作者
CloudSir
发布于
2026-08-28
许可协议
CC BY-NC-SA 4.0
Linux 驱动是怎么工作的?设备、总线与驱动模型
从马赛克到彩色照片:拜耳去马赛克深度解析