适用人群:刚能点亮 LED、能用 PWM 调个灯,但第一次想让"东西动起来"的同学。你会调
ledc,也知道 GPIO 有 3.3 V,可一接电机 MCU 就重启,甚至 IO 直接冒烟。没看过本专栏 01/02 篇也能读:前两篇讲的是传感器怎么"读数",这篇讲的是执行器怎么"出力"------传感器是耳朵,执行器是手脚。
读完你能得到:
① 彻底搞清 GPIO 为什么不能直接驱动电机;
② H 桥原理 + TB6612 / DRV8833 选型 + PWM 调速的 LEDC/MCPWM 代码;
③ 50 Hz 舵机:脉冲宽度怎么映射到角度,供电与共地怎么做;
④ 28BYJ-48(ULN2003)与 A4988 步进驱动代码,以及"为什么步进电机不能一下提速到最高";
⑤ 续流、栅极下拉、大电流走线、电流采样这些面试常问的工程常识;
⑥ 我踩过的 N 个坑,和若干故意试错实验。
一、为什么电机不能直接接 GPIO
先说我的真实翻车现场。
大三上我想做个自动喂食器,齿轮箱带一个小转盘。我拆了个旧的 5 V 直流减速电机,直接把红线接 ESP32 的 GPIO25,黑线接 GND,写了几句 gpio_set_level(25, 1)。按下复位键,电机"咔"地转了一下------然后开发板就黑了,再按复位没反应。我以为是板子坏了,换了一块,接上去又是一下黑屏。第二块 GPIO25 从那以后只能输出 1.6 V 左右,基本废了。
问题不在代码,在我把 GPIO 当电源用。
1.1 三条越界红线
| 红线 | GPIO 能给的 | 电机需要的 | 后果 |
|---|---|---|---|
| 电压 | 3.3 V(或 5 V USB 经 LDO) | 常见 3 V / 5 V / 12 V | 电压不够 → 转不动 |
| 电流 | 乐鑫手册:单个 IO 最大 40 mA 源出 / 28 mA 灌入 (GPIO_DRIVE_CAP_3,绝对最大值) |
普通 3 V 直流电机启动/堵转可达几百 mA | 电流一冲 → brownout 复位或 IO 烧坏 |
| 电感反冲 | GPIO 输出没有续流保护 | 电机线圈断电时 V = -L·di/dt,反冲几十伏 |
直接击穿 MCU 内部保护二极管 |
⚠️ 注意:40 mA / 28 mA 是 datasheet 的绝对最大值,不是推荐工作值。对 ESP32 而言,持续驱动几十 mA 以上就该考虑外部驱动。
这和
低功耗_03里那句"GPIO 是信号不是电源"是同一件事------那篇从功耗角度说,这篇从功率角度说。
1.2 反冲电压到底多吓人
电机线圈不是电阻,是电感。电感电流不能突变,你突然关断 GPIO,电流必须找地方走,于是线圈两端会感应出一个高压尖峰:
text
V = -L · (Δi / Δt)
L 可能只有几百 µH,但 Δt 是 ns 级
结果:尖峰轻松到几十伏,远超 GPIO 的 VCC+0.3 V 上限
除非你在电机两端并联一个续流二极管(flyback diode),给这股电流一条回路。GPIO 引脚里没有这个二极管,所以反冲直接打到芯片内部。
1.3 正确的角色分工
| 角色 | 谁干 | 走什么线 |
|---|---|---|
| 指挥官 | MCU GPIO | 细号线,只传"开/关/方向/转速" |
| 电源开关 | 驱动芯片 / MOSFET / 继电器 | 粗线或大铜皮,走电机电流 |
| 能量来源 | 独立电源或稳压器 | 电源按电机的功率设计 |
核心原则:信号和功率分开,但所有地必须共在一起。 不共地,信号就没有参考点,驱动芯片根本不知道你 GPIO 高电平是几伏。
二、直流电机(DC):H 桥、驱动芯片与 PWM 调速
2.1 H 桥:用四个开关控制方向和刹车
直流电机只有两根线,电流从 A→B 转一个方向,从 B→A 就反向。怎么做到双向?答案是把四个开关(MOSFET)摆成 H 形:
text
VM
│
┌────┴────┐
Q1 │ │ Q2
┌─────┤ ├─────┐
│ │ │ │
│ │ MOTOR │ │
│ │ │ │
│ │ │ │
└─────┤ ├─────┘
Q4 │ │ Q3
└────┬────┘
│
GND
正转:Q1、Q3 导通,电流 VM → Q1 → MOTOR → Q3 → GND
反转:Q2、Q4 导通,电流 VM → Q2 → MOTOR → Q4 → GND
刹车:Q3、Q4 导通,电机两端被短路到 GND,转子的动能快速耗掉
滑行:四个全关,电机自由转
绝对不能出现的是"同臂导通" :比如 Q1 和 Q4 同时打开,电源直接通过这两个 MOS 短路到地,瞬间电流只受 MOS 内阻限制,芯片会立刻炸。专业术语叫 shoot-through(直通/穿通)。
这也是驱动芯片比自己搭 H 桥更省心的地方------TB6612、DRV8833 内部都有死区时间 和互锁逻辑,从输入端就不可能让同臂 MOS 同时导通。
2.2 TB6612FNG 与 DRV8833 怎么选
这两块板在国产模块市场最常见,都是双 H 桥,都能同时或分别驱动两个直流电机。
| 参数 | TB6612FNG | DRV8833 |
|---|---|---|
| 电机供电 VM | 2.5 V ~ 13.5 V | 2.7 V ~ 10.8 V |
| 逻辑供电 VCC/VDD | 2.7 V ~ 5.5 V | 1.8 V ~ 7 V(逻辑) |
| 单桥连续电流 | 1.2 A | 1.5 A RMS(PWP/RTY 封装);0.5 A RMS(PW 小封装) |
| 单桥峰值电流 | 3.2 A | 2 A |
| 驱动 MOS 内阻 | 约 0.5 Ω(@5 V, 1 A) | 360 mΩ(HS+LS) |
| 过流/过温保护 | 无过流指示,有过热关断 | 有过流、短路、欠压、过温保护 |
| 睡眠模式 | STBY 低电平进入待机 | nSLEEP 低电平进入睡眠 |
| 电流限制 | 无内置电流环 | 有(通过 xISEN 采样电阻 + 200 mV 内部基准) |
💡 选型建议 :7.2 V / 12 V 小车用 TB6612;5 V 以下锂电池小项目用 DRV8833。但买 DRV8833 模块时一定看清封装/板子参数,有的模块用 PW 小封装,单桥只能 0.5 A,硬带 1 A 电机会发烫。
TB6612FNG 单通道控制真值表(以通道 A 为例;STBY 低电平时全部待机):
| IN1 | IN2 | PWM | STBY | OUT1 | OUT2 | 模式 |
|---|---|---|---|---|---|---|
| H | L | H | H | H | L | 正转(CW) |
| L | H | H | H | L | H | 反转(CCW) |
| H | H | H/L | H | L | L | 短刹车(Short brake) |
| L | L | H/L | H | OFF | OFF | 停止(Stop / 滑行) |
| X | X | X | L | OFF | OFF | 待机(Standby) |
⚠️ 注意:PWM 引脚低电平时,IN1/IN2 不同向也会进刹车(见表中 L/H + PWM=L)。所以实际调速时,IN1/IN2 只决定方向,PWM 决定"给多少能量"。
2.3 续流:为什么芯片没炸还能继续跑
直流电机 PWM 调速时,MOS 管每个周期都在开关。关断瞬间电机线圈里的电流必须有去处,否则就会感应出高压。H 桥里的处理办法通常叫 decay mode(衰减模式):
- 慢衰减(slow decay):电机两端被短接到地或电源,电流在线圈里自己慢慢耗掉。
- 快衰减(fast decay):电流通过 MOS 的体二极管流回电源或地,衰减快但纹波大。
TB6612FNG 内部输出级是 MOS,体二极管已经能完成续流,模块上通常不再额外加二极管;DRV8833 数据手册更是明确说电流通过 body diodes 回流。如果你的驱动芯片是继电器或晶体管 H 桥 ,那续流二极管就必须外接,否则第一个 PWM 周期就可能把器件打坏。
2.4 ESP32 LEDC 控制 TB6612:一份能编过的代码
先算分辨率。ESP32 的 LEDC 频率和分辨率互相牵制:
text
计数器最高位宽:ESP32 为 20 bit;ESP32-S3/C3 为 14 bit
频率 20 kHz、APB 80 MHz 时:
20 kHz 周期 = 4000 个 tick → 约 12 bit 可用
这里取 11 bit(2048 级)留点余量,让 ledc_timer_config 更容易成功
接线:
text
ESP32 GPIO18 ──→ TB6612 AIN1
ESP32 GPIO19 ──→ TB6612 AIN2
ESP32 GPIO21 ──→ TB6612 PWMA
ESP32 GPIO22 ──→ TB6612 STBY
TB6612 VM ────→ 电机电源(例 7.4 V 2S 锂电)
TB6612 VCC ────→ 3.3 V
共 GND(ESP32 GND ↔ TB6612 GND ↔ 电池负极)
代码:
c
// dc_motor.c ------ ESP-IDF v5.x,TB6612FNG 单路直流电机
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_log.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define MOTOR_AIN1 GPIO_NUM_18
#define MOTOR_AIN2 GPIO_NUM_19
#define MOTOR_PWMA GPIO_NUM_21
#define MOTOR_STBY GPIO_NUM_22
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define PWM_FREQ_HZ 20000
#define PWM_RES LEDC_TIMER_11_BIT // 2048 级
#define PWM_MAX_DUTY 2048
static const char *TAG = "dc_motor";
esp_err_t dc_motor_init(void)
{
gpio_config_t io = {
.pin_bit_mask = (1ULL << MOTOR_AIN1) | (1ULL << MOTOR_AIN2) | (1ULL << MOTOR_STBY),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&io), TAG, "gpio config failed");
/* STBY 拉高,否则芯片一直睡觉 */
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_STBY, 1), TAG, "stby high failed");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN1, 0), TAG, "ain1 init failed");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN2, 0), TAG, "ain2 init failed");
ledc_timer_config_t tcfg = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = PWM_RES,
.freq_hz = PWM_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_RETURN_ON_ERROR(ledc_timer_config(&tcfg), TAG, "ledc timer config failed");
ledc_channel_config_t ccfg = {
.gpio_num = MOTOR_PWMA,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0,
};
ESP_RETURN_ON_ERROR(ledc_channel_config(&ccfg), TAG, "ledc channel config failed");
return ESP_OK;
}
/* speed: -1000 ~ +1000;0 为停止 */
esp_err_t dc_motor_set(int16_t speed)
{
if (speed < -1000) speed = -1000;
if (speed > 1000) speed = 1000;
if (speed == 0) {
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN1, 0), TAG, "stop AIN1");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN2, 0), TAG, "stop AIN2");
ESP_RETURN_ON_ERROR(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0), TAG, "duty 0");
ESP_RETURN_ON_ERROR(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL), TAG, "update 0");
return ESP_OK;
}
/* 先刹车到 0 再换向,避免高速切换方向的大电流冲击 */
static int16_t s_last_speed = 0;
if ((s_last_speed > 0 && speed < 0) || (s_last_speed < 0 && speed > 0)) {
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN1, 1), TAG, "brake AIN1");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN2, 1), TAG, "brake AIN2");
ESP_RETURN_ON_ERROR(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, PWM_MAX_DUTY), TAG, "brake duty");
ESP_RETURN_ON_ERROR(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL), TAG, "brake update");
vTaskDelay(pdMS_TO_TICKS(50));
}
s_last_speed = speed;
uint32_t duty = (uint32_t)((uint32_t)PWM_MAX_DUTY * (uint32_t)__builtin_abs(speed) / 1000U);
if (speed > 0) {
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN1, 1), TAG, "fwd AIN1");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN2, 0), TAG, "fwd AIN2");
} else {
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN1, 0), TAG, "rev AIN1");
ESP_RETURN_ON_ERROR(gpio_set_level(MOTOR_AIN2, 1), TAG, "rev AIN2");
}
ESP_RETURN_ON_ERROR(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty), TAG, "set duty");
ESP_RETURN_ON_ERROR(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL), TAG, "update duty");
return ESP_OK;
}
⚠️
dc_motor_set()里的"先刹车再换向"不是强迫症。直流电机转起来有反电动势,高速时直接反接相当于电源 + 反电动势串联,启动电流能达到堵转电流的几倍,驱动芯片会过热保护甚至烧。正确流程:减速 → 停稳 → 换向 → 提速。
💡 如果你想用 ESP32 的 MCPWM(带死区)驱动 H 桥,或者做有刷电机闭环,需要看 MCPWM 那套 API。对入门项目,LEDC 够用,代码也更短。
💡 ESP-IDF 的 servo 例程或 Arduino 的Servo库底层也是 LEDC/MCPWM,只是帮你封装好了角度转换。如果你想搞懂脉宽怎么算出来的,建议先手写 LEDC;等调通了再换封装也不迟。
2.5 什么时候测电流?怎么测?
只要开始带电机,就必须关心电流。常用两种办法:
- 采样电阻 + 运放:在 H 桥下桥串一个 0.1 Ω 电阻,用差分运放放大后送 ADC。TB6612 没有内置采样脚,DRV8833 有 xISEN。
- 霍尔电流传感器:如 ACS712,隔离式、不串入功率地,适合学生实验。
没有电流数据,你就不知道电机是不是在堵转,也不知道驱动芯片有没有在热保护边缘。
三、舵机(Servo):不是角度传感器,是自带闭环的电机
舵机和直流电机的最大区别:舵机自己知道现在转到哪了。它内部有电机、减速齿轮、电位器和比较电路。你给它的不是"电压",而是一个 PWM 脉冲------它自己把脉冲宽度转成目标角度,然后驱动电机去追那个位置。
3.1 50 Hz PWM:周期 20 ms,脉宽才是命令
标准舵机信号:
text
频率:50 Hz(周期 20 ms)
高电平脉宽:0.5 ms ~ 2.5 ms
0.5 ms ────── 0°
1.0 ms ────── 45°
1.5 ms ────── 90°
2.0 ms ────── 135°
2.5 ms ────── 180°
⚠️ SG90 数据手册给的是 500 µs ~ 2400 µs 对应 0° ~ 180°。网上很多教程写 0.5 ms ~ 2.5 ms 也能用,但把舵机推到极限位置容易让它一直堵转、发热。建议程序里把范围限到 600 µs ~ 2300 µs,给机械结构留余量。
3.2 用 LEDC 产生舵机信号
LEDC 的分辨率怎么选?以 ESP32、50 Hz 为例:
text
目标:20 ms 周期里能分辨约 10 µs(0.1° 量级)
20 ms / 10 µs = 2000 级 → 11 bit(2048 级)就够
50 Hz 下 ESP32 可达 16 bit,ESP32-S3/C3 可达 13~14 bit,
这里选 13 bit(8192 级)兼容两者
代码:
c
// servo.c ------ ESP-IDF v5.x,LEDC 驱动 SG90 舵机
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_check.h"
#define SERVO_GPIO GPIO_NUM_27
#define SERVO_FREQ_HZ 50
#define SERVO_RES LEDC_TIMER_13_BIT // 8192 级
#define SERVO_CHANNEL LEDC_CHANNEL_1
#define SERVO_TIMER LEDC_TIMER_1
#define SERVO_MODE LEDC_LOW_SPEED_MODE
#define DUTY_MIN_US 600 // 安全下限
#define DUTY_MAX_US 2300 // 安全上限
#define PERIOD_US 20000
static uint32_t s_max_duty;
esp_err_t servo_init(void)
{
s_max_duty = (1U << SERVO_RES);
ledc_timer_config_t t = {
.speed_mode = SERVO_MODE,
.timer_num = SERVO_TIMER,
.duty_resolution = SERVO_RES,
.freq_hz = SERVO_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_RETURN_ON_ERROR(ledc_timer_config(&t), "servo", "timer cfg");
ledc_channel_config_t c = {
.gpio_num = SERVO_GPIO,
.speed_mode = SERVO_MODE,
.channel = SERVO_CHANNEL,
.timer_sel = SERVO_TIMER,
.duty = 0,
.hpoint = 0,
};
ESP_RETURN_ON_ERROR(ledc_channel_config(&c), "servo", "channel cfg");
/* 默认回到 90° */
return servo_write_us(1500);
}
/* 输入:脉宽 µs */
esp_err_t servo_write_us(uint32_t pulse_us)
{
if (pulse_us < DUTY_MIN_US) pulse_us = DUTY_MIN_US;
if (pulse_us > DUTY_MAX_US) pulse_us = DUTY_MAX_US;
uint32_t duty = (uint32_t)(((uint64_t)pulse_us * s_max_duty) / PERIOD_US);
ESP_RETURN_ON_ERROR(ledc_set_duty(SERVO_MODE, SERVO_CHANNEL, duty),
"servo", "set duty");
ESP_RETURN_ON_ERROR(ledc_update_duty(SERVO_MODE, SERVO_CHANNEL),
"servo", "update duty");
return ESP_OK;
}
/* 输入:0 ~ 180 */
esp_err_t servo_write_angle(uint8_t angle)
{
if (angle > 180) angle = 180;
uint32_t pulse = DUTY_MIN_US
+ ((DUTY_MAX_US - DUTY_MIN_US) * (uint32_t)angle) / 180U;
return servo_write_us(pulse);
}
3.3 供电与共地:最容易被忽略的两大杀手
供电:SG90 空载才几十 mA,但启动/堵转瞬间能到 200 mA 以上。ESP32 开发板上的 3.3 V LDO 或 USB 5 V 输出根本扛不住,舵机一转,板子电压被拉低,MCU 直接 brownout 重启。
正确接法:
text
独立 5 V/1 A 电源 ──→ 舵机 VCC(红线)
独立电源 GND ──────→ ESP32 GND(黑/棕线)← 必须共地
ESP32 GPIO27 ───────→ 舵机信号线(黄/橙线)
⚠️ 舵机信号线只走信号电流 ,μA 级;VCC 和 GND 才是功率回路。红线接 GPIO、黑线接 GND 是经典错误,会直接烧 IO。
共地:如果你用独立电源但不把电源地和 ESP32 地连起来,舵机内部比较器看到的"高电平"没有参考点,它会乱抖、尖叫,甚至根本不转。很多学生把电源分开后忘了这根线,舵机表现就像"程序有 bug",其实是硬件问题。
四、步进电机(Stepper):开环就能定位的秘密
步进电机的特点:一个脉冲走一步。你不用编码器也知道它转了多少------只要脉冲没丢、没超过转子能跟上的最大速度。
4.1 28BYJ-48 + ULN2003:新手最便宜的步进实验
28BYJ-48 是个 5 V 的四相五线单极性步进电机,参数:
| 参数 | 值 | 说明 |
|---|---|---|
| 额定电压 | 5 VDC | 实际 4~6 V 都能转 |
| 相数 | 4 相 | 但共用一个中心抽头 |
| 减速比 | 1/64 | 内部转子转 64 圈,输出轴转 1 圈 |
| 步距角 | 5.625° / 64 | 转子每步 5.625°,经减速后输出轴约 0.0879° |
| 单相电阻 | 50 Ω ± 7% | 5 V 下约 100 mA/相 |
ULN2003 是 7 路达林顿阵列,每路最大 500 mA,输出端内部带共阴极钳位二极管。用 4 路驱动 28BYJ-48 时,COM 脚必须接到电机电源正极(5 V),这样感性电流才有回路。
💡 3.3 V GPIO 能不能直接驱动 ULN2003A?28BYJ-48 单相约 100 mA,ULN2003A 在 250 mA 时的导通输入电压最大 2.7 V,所以 3.3 V 完全够用。如果你要推更大的电流(接近 300 mA 以上),建议测一下输入电流或加电平转换。
接线:
text
ESP32 GPIO13 ──→ ULN2003 IN1 ──→ 电机 Orange(A 相)
ESP32 GPIO12 ──→ ULN2003 IN2 ──→ 电机 Yellow(B 相)
ESP32 GPIO14 ──→ ULN2003 IN3 ──→ 电机 Pink (C 相)
ESP32 GPIO27 ──→ ULN2003 IN4 ──→ 电机 Blue (D 相)
5 V ──────────→ 电机 Red(公共端)
5 V ──────────→ ULN2003 COM(续流二极管公共端)
GND ───────────→ ULN2003 GND ↔ ESP32 GND
4.2 相序:整步 vs 半步
单极性 28BYJ-48 最常见的 8 步半步序列(顺时针):
text
A(Orange) B(Yellow) C(Pink) D(Blue)
Step1 1 0 0 1
Step2 1 0 0 0
Step3 1 1 0 0
Step4 0 1 0 0
Step5 0 1 1 0
Step6 0 0 1 0
Step7 0 0 1 1
Step8 0 0 0 1
"1" = GPIO 输出高 → ULN2003 导通 → 该相被拉低到地 → 线圈通电。
> 💡 有些数据手册里的相序表用"0"表示通电、"1"表示断电(从线圈电压角度看),和我这里"GPIO 输出电平"的写法正好相反。看表时先看清楚它定义的是哪个信号。
每走 8 步,内部转子转 5.625°;输出轴经过 64:1 减速,只转 5.625°/64 ≈ 0.0879°。所以一圈 360° 需要 4096 个半步。如果取整步序列(只走 Step1/3/5/7),一圈就是 2048 步,扭矩略小、抖动更大。
⚠️ 注意:网上有些相序表写的是"逆时针",或者线圈颜色对应关系不同。如果你的电机只抖不转,先把脉冲周期放慢到 100 ms/步,然后对调相邻两相 或把相序表反着跑------90% 的"相序错"问题这样解决。
半步驱动代码:
c
// stepper_28byj48.c ------ ESP-IDF v5.x
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define COIL_A GPIO_NUM_13
#define COIL_B GPIO_NUM_12
#define COIL_C GPIO_NUM_14
#define COIL_D GPIO_NUM_27
static const gpio_num_t s_coils[4] = { COIL_A, COIL_B, COIL_C, COIL_D };
/* 半步序列,一行是一步,顺序 1→8 顺时针 */
static const uint8_t s_half_step[8][4] = {
{1, 0, 0, 1},
{1, 0, 0, 0},
{1, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 1, 0},
{0, 0, 1, 1},
{0, 0, 0, 1},
};
void stepper28_init(void)
{
for (int i = 0; i < 4; i++) {
gpio_reset_pin(s_coils[i]);
gpio_set_direction(s_coils[i], GPIO_MODE_OUTPUT);
gpio_set_level(s_coils[i], 0);
}
}
void stepper28_step_once(uint32_t delay_ms)
{
static int s_step = 0;
for (int i = 0; i < 4; i++) {
gpio_set_level(s_coils[i], s_half_step[s_step][i]);
}
s_step = (s_step + 1) % 8;
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
⚠️ 这个演示用了
vTaskDelay,最慢只能到 10 ms/tick(默认 tick 1 ms 时实际是 1 ms 整数倍)。真正跑高速步进需要用硬件定时器或 esp_timer 做 µs 级延时,否则电机到 10 ms/步就上不去了。
4.3 A4988:两相四线步进的 step/dir 接口
A4988 驱动双极性两相四线步进电机(比如 42 步进电机、3D 打印机用的 NEMA17)。它把复杂的相序表封装在内部,你只需要给两个信号:
- DIR:方向(高/低)
- STEP:一个上升沿走一步
细分通过 MS1/MS2/MS3 设置:
| MS1 | MS2 | MS3 | 细分 |
|---|---|---|---|
| L | L | L | 整步 |
| H | L | L | 半步 |
| L | H | L | 1/4 步 |
| H | H | L | 1/8 步 |
| H | H | H | 1/16 步 |
A4988 关键规格:
- 电机供电 VBB:8 V ~ 35 V
- 逻辑供电 VDD:3 V ~ 5.5 V
- 输出电流:±2 A 绝对最大;不加散热片实际建议 ≤ 1 A,加散热片可达 2 A
- 每步电流:
ITripMAX = VREF / (8 × RS),RS 是采样电阻 - STEP 脉宽:高、低都至少 1 µs
- DIR 相对 STEP 的建立/保持时间:至少 200 ns
- RESET 和 SLEEP:必须拉高,否则不工作
⚠️ A4988 的电位器(VREF)决定电流上限。很多模块默认 VREF 约 0.8 V,RS 典型 0.1 Ω,电流约 1 A。如果你把电机换成小电流型号,必须重调 VREF,否则电机发烫、噪音大、寿命缩水。
接线:
text
ESP32 GPIO18 ──→ A4988 STEP
ESP32 GPIO19 ──→ A4988 DIR
ESP32 GPIO21 ──→ A4988 MS1(可选,接地=整步)
ESP32 GPIO22 ──→ A4988 MS2(可选)
ESP32 GPIO23 ──→ A4988 MS3(可选)
3.3 V ─────────→ A4988 VDD
12 V ──────────→ A4988 VMOT
GND ──────────→ A4988 GND ↔ ESP32 GND(共地)
A4988 1A/1B/2A/2B ──→ 步进电机四线
★ A4988 RESET 和 SLEEP 必须接高电平(可短接在一起)
基本驱动代码:
c
// a4988.c ------ ESP-IDF v5.x,整步驱动 NEMA17
#include "driver/gpio.h"
#include "esp_check.h"
#include "esp_rom_sys.h" // esp_rom_delay_us
#define A4988_STEP GPIO_NUM_18
#define A4988_DIR GPIO_NUM_19
void a4988_init(void)
{
gpio_config_t io = {
.pin_bit_mask = (1ULL << A4988_STEP) | (1ULL << A4988_DIR),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io));
gpio_set_level(A4988_STEP, 0);
gpio_set_level(A4988_DIR, 0);
}
/* dir=1 正转,dir=0 反转;steps 为 STEP 脉冲个数 */
void a4988_move(uint32_t steps, bool dir, uint32_t delay_us)
{
gpio_set_level(A4988_DIR, dir ? 1 : 0);
for (uint32_t i = 0; i < steps; i++) {
gpio_set_level(A4988_STEP, 1);
esp_rom_delay_us(delay_us > 0 ? delay_us : 1);
gpio_set_level(A4988_STEP, 0);
esp_rom_delay_us(delay_us > 0 ? delay_us : 1);
}
}
⚠️ 这里用
esp_rom_delay_us做 busy-wait,只是演示。实际跑高速、跑 RTOS 时,应该用硬件定时器中断或 MCPWM 输出方波,避免任务长时间占用 CPU。
4.4 为什么不能一步提到最高速?梯形加减速思路
步进电机转子有惯性,它不可能瞬间从 0 跳到 1000 step/s。你硬给高速脉冲,转子跟不上磁场,就会丢步,表现为电机"咔咔"叫、转不动甚至反转。
所以实际运动通常做 梯形速度曲线:
text
速度
▲
│ ┌──────────┐
│ / \
│ / \
│ / \
│ / \
└─/────────────────────\──────▶ 步数
加速段 匀速段 减速段
最经典的实时算法来自 Microchip 的 AVR446 应用笔记:
text
设定时器频率为 f_t,步距角 α,目标角加速度 ω̇
第一个间隔:
c0 = f_t · sqrt(2α / ω̇)
后续间隔用近似递推(避免开方):
cn = cn-1 - (2 · cn-1) / (4n + 1)
n 是第 n 步。加速时步间隔越来越小,电机越来越快;
减速时把公式反过来用,步间隔越来越大。
实现上不一定非要在中断里跑递推。简单项目可以这样做:
- 把加速段拆成 10~20 个速度台阶;
- 每个台阶跑固定步数,delay_us 按等差或等比递减;
- 匀速段按最快 delay_us 跑;
- 减速段把加速表反过来。
💡 如果你只是让小车走一段距离,没有实时位置要求,可以先把速度调到"能稳定转"的最大值,然后保持。真正做 3D 打印机、CNC 才需要精确的 S 曲线或梯形规划。
五、安全与功耗:别让执行器把 MCU 拉下水
5.1 续流与栅极下拉
这两件事在 项目实战_02 里已经强调过,这里系统总结一下:
| 位置 | 作用 | 不加的后果 |
|---|---|---|
| 续流二极管 | 给电感电流一条回路 | 反冲电压击穿 MOS/MCU |
| 栅极下拉电阻 | MCU 复位/未初始化时把 MOS 栅极固定到低电平 | 上电瞬间 MOS 半导通,继电器"咔"一下误吸合 |
| 栅极串联电阻 | 抑制振铃、限制充放电电流 | 开关过快产生 EMI,或 GPIO 灌电流过大 |
MOS 栅极下拉电阻典型值 10 kΩ ~ 100 kΩ。自己搭 H 桥或继电器低边驱动时,这个电阻必须接;用 TB6612/DRV8833 这类集成驱动芯片,输入端内部一般有下拉,外部可省略,但上电默认状态仍要确认。
5.2 大电流走线与电源功率预留
电机电流走的是功率回路,和 MCU 的 3.3 V 不是一回事。设计要点:
- 电源功率要留余量。电机启动电流是运行电流的 3~5 倍,电源额定电流至少按电机额定电流的 1.5 倍选。
- 地和电源线要粗。杜邦线带 1 A 以上电流会明显发热,长时间工作建议用更粗的线或 PCB 铜皮。
- 退耦电容靠近驱动芯片。在 VM 和 GND 之间放一个 100 µF 电解 + 100 nF 陶瓷,吸收电机换向时的电流纹波。
- MCU 的 3.3 V 和电机电源分开。除非电机非常小(<50 mA),否则不要从 MCU 的 LDO 取电。
5.3 电流采样:知道电机在干什么
没有电流反馈,你只能靠摸温度判断是否过载。采样方式前面提过:
text
1) 采样电阻 + 仪表/差分运放 → ADC
优点:成本低;缺点:引入地线干扰、需要放大电路
2) 霍尔电流传感器(如 ACS712-05B)→ ADC
优点:隔离、无需改地线;缺点:体积大、本身也耗电
DRV8833 内部已经做了电流限制(200 mV 基准),但那是"保护驱动芯片",不是"保护你的机械结构"。真正做力控或堵转检测,还是要外部采样。
六、新手必踩的 N 个坑
| # | 坑 | 后果(现象长什么样) | 正确做法 |
|---|---|---|---|
| 1 | GPIO 直接驱动电机 | 一接电机 MCU 重启,甚至 GPIO 烧坏 | GPIO 只发信号,电流走驱动芯片/MOSFET;电机独立供电 |
| 2 | TB6612 同侧 MOS 同臂导通(自建 H 桥时) | 电源直接短路,MOS 瞬间冒烟 | 用集成驱动芯片或加硬件死区;若用互补 PWM,必须插 MCPWM 死时间 |
| 3 | 忘了续流二极管/未接 COM | 继电器或晶体管 H 桥第一次 PWM 就坏;ULN2003 发热 | 继电器必加 1N4007/1N4148;ULN2003 的 COM 必须接电机电源正 |
| 4 | 舵机从开发板 3.3 V/5 V 取电 | 舵机一动 ESP32 重启,或板载 LDO 过热 | 舵机独立 5 V 电源,信号线接 GPIO,必须共地 |
| 5 | 舵机不共地 | 舵机抽搐、尖叫、不响应 | 把独立电源 GND 和 MCU GND 连起来 |
| 6 | 步进相序接错/表反了 | 电机只抖不转、异响、发热 | 放慢到 100 ms/步,对调相邻两相或反着跑相序表 |
| 7 | A4988 的 RESET/SLEEP 悬空 | 模块通电但不转,STEP 信号没反应 | RESET 和 SLEEP 短接到 3.3 V |
| 8 | A4988 VREF 不调 | 小电机烫手、噪音大、力矩不稳 | 按 ITripMAX = VREF/(8×RS) 计算并测量 VREF |
| 9 | 步进电机起步直接给高速 | 电机"咔咔"叫、丢步、甚至不转 | 加梯形加减速,先加速到目标速度 |
| 10 | 电机电源和 MCU 电源不隔离、地线乱走 | MCU 复位、ADC 跳变、通信丢包 | 单点接地,大功率回路不经过 MCU 地平面 |
坑 2 展开:集成驱动芯片真的不会同臂导通吗?
TB6612FNG 数据手册明确写了:"To prevent penetrating current, dead time t2 and t4 is provided in switching to each mode in the IC." 它内部已经插了死区,所以你从 IN1/IN2/PWM 端不可能让它同臂导通。把 IN1=H、IN2=H 写进去,它执行的是"短刹车",不是短路。
但如果你是自己用 4 个 MOS 搭 H 桥,或用两个半桥驱动芯片,同臂导通是真实存在的。做这类实验时一定要在示波器上先看上下桥 PWM 有没有重叠,再通电。
七、动手练一练
练习 1|先测电流再让它转(20 分钟)
不要先写方向切换。把 TB6612 接好,电机固定不让它转(或轻轻捏住轴),用万用表电流档串在 VM 回路里:
dc_motor_set(100)时电流多大?dc_motor_set(1000)时电流多大?- 从 1000 直接切到 -1000,电流峰值比匀速时大多少?
这个练习会让你对"反接制动电流"有直观感受,也会让你理解为什么代码里要"先刹车再换向"。
练习 2|【故意试错】舵机不共地(5 分钟)
舵机红线、棕线先接独立 5 V 电源,但电源负极暂时不接 ESP32 GND ,只把信号线接 GPIO。上电后调用 servo_write_angle(90),观察现象:舵机会抖、会叫、或者完全不理你。
然后关掉电源,把独立电源 GND 和 ESP32 GND 连上,再上电------大概率立刻正常。
练习 3|【故意试错】步进相序反了(10 分钟)
把 28BYJ-48 的 A/B 两相线对调,然后跑 stepper28_step_once()。你会看到电机只抖不转。把线换回来,或把相序表反着遍历,电机就正常转了。这个错很常见,见过一次以后 5 秒钟就能定位。
练习 4|用 A4988 让 42 步进转一圈,比较整步和 1/16 细分(20 分钟)
- 整步:MS1=MS2=MS3=0,发 200 个脉冲转一圈;
- 1/16 细分:MS1=MS2=MS3=1,发 3200 个脉冲转一圈。
用手摸或听声音:整步抖动明显,1/16 细分平稳很多。这会让你理解"细分不是为了精度,是为了减振"这句话。
练习 5|【故意试错】起步直接给高速 vs 梯形加速(20 分钟)
用 A4988 控制 42 步进:
- 方法 A:
delay_us直接设 500 µs(约 1000 step/s),看电机能不能启动; - 方法 B:从 2000 µs 起步,每 100 步把 delay_us 减 50 µs,直到 500 µs。
方法 B 能启动的速度,方法 A 可能直接丢步。这就是梯形加速的价值。
练习 6|观察 PWM 死区(需要示波器,选做)
如果你有离散 MOS H 桥或两个半桥驱动,用 MCPWM 输出互补 PWM。先用 0 ns 死区,在示波器上观察上下桥栅极信号------很可能有几十到几百纳秒的重叠。然后把 posedge_delay_ticks / negedge_delay_ticks 加到 1 µs 以上,重叠消失。记住:集成驱动芯片内部已经做了这一步,这个实验只是让你理解"为什么不能直接反相两个 PWM"。
小结
三类执行器,三种控制方式:
| 执行器 | 控制量 | 核心电路 | 最容易翻车的地方 |
|---|---|---|---|
| 直流电机 | 电压方向 + PWM 占空比 | H 桥(TB6612/DRV8833) | GPIO 直接驱动、反接制动电流、不换向直接倒车 |
| 舵机 | 50 Hz PWM 脉宽 | 内部自带闭环 | 供电不足、不共地、脉宽推到底 |
| 步进电机 | 脉冲个数 + 方向 | 驱动器(ULN2003/A4988) | 相序错、RESET/SLEEP 悬空、起步太猛 |
四句话带走:
- GPIO 是信号不是电源,电机电流必须走驱动芯片或 MOS。
- 感性负载必须有续流回路,不然反冲电压会打坏芯片。
- 舵机/电机独立供电,但所有地必须共在一起。
- 步进电机想快,得先让它慢慢加;梯形加减速是开环定位不丢步的关键。
各驱动参数都来自对应芯片的 Datasheet(TB6612 / DRV8833 / A4988 / 28BYJ-48 / ULN2003A / SG90 / ESP-IDF LEDC·MCPWM 等),选型时以你手上的型号版本为准。
- 本站相关:
项目实战_02(继电器/舵机/MOSFET 联动执行)、低功耗_03(GPIO 不是电源)
下一篇:嵌入式Linux基础_01_交叉编译与开发环境实战