直流电机与驱动:为什么不能直接接单片机
- 理解为什么电机要经驱动板(大电流 + 感性负载反电动势)
- 用 gpio_set_level 输出方向真值表控制正转/反转/刹车/滑停
- 用 ESP-IDF 的 LEDC 给使能脚发 PWM 调速
- 记住 TB6612 的 STBY 必须拉高、电源分离 + 共地这两条红线
- 选对 L298N 还是 TB6612
| 器材 | 数量 | 参考 |
|---|---|---|
| 直流减速电机 | 1 | — |
| TB6612 或 L298N 驱动板 | 1 | — |
价格随渠道波动,以购买页实时为准。
你想让一辆小车跑起来。手头有一颗直流减速电机、一块 ESP32-S3、照着点灯的经验,很自然会想:把电机的两根线接到一个 GPIO 和 GND 上,给个高电平,它不就转了?
别接。真这么接,轻则板子重启,重则烧引脚。直流电机和 LED 是两个量级的负载,中间必须隔一块「电机驱动板」。这一节把为什么讲透,再给你一份能直接跑的正反转加调速 ESP-IDF 代码,最后用一张排查表收尾——读完你能让小车真正动起来,而且知道每一步在防什么坑。
读这篇前,你最好已经会用 LEDC 发 PWM 调亮度。还没搞懂占空比是怎么回事?先看 PWM 调光那节,那里配 LEDC 定时器和通道的套路,这里给电机使能脚调速时会原样复用。
为什么不能直接接 GPIO
这不是「最好别」,是「不能」。两个硬道理,分开讲清楚。
一、电流太大,引脚扛不住
一个 GPIO 引脚能安全输出的电流只有几十毫安——ESP32-S3 单脚典型在 20mA 上下,这是给 LED 这种「吃几毫安就亮」的小负载设计的。
而一颗普通的直流减速电机,空转就要一两百毫安,带载或堵转的瞬间能冲到几百毫安甚至上安培。你拿一个只能给 20mA 的引脚去喂一个要几百 mA 的负载,结果只有一个:引脚被过流烧掉。这跟 LED 必须串限流电阻是同一类问题,只是电机这边的胃口大了一个数量级,靠电阻限流已经没意义——电机本来就需要那么大电流才转得动。
二、电机是感性负载,会反咬一口
电机内部是一圈圈线圈,线圈是典型的感性负载。感性负载有个脾气:电流不能突变。当你突然断电、或者电机换向的瞬间,线圈里的电流想维持原样,于是在两端「憋」出一个方向相反的高压尖峰——这叫反电动势(也叫续流电压)。
这个尖峰可能远超 3.3V,顺着线灌回单片机,足以击穿 GPIO 内部的保护结构。哪怕没当场烧掉,反复的电压冲击也会让芯片莫名其妙地重启、死机。LED 是纯电阻性负载,没这个问题;电机有,所以不能裸接。续流二极管就是干这个的——把这个尖峰泄掉。好消息是 TB6612、L298N 这类驱动板的 H 桥内部都集成了续流二极管,你不用自己外接,但要知道它在帮你挡这一刀。
很多人第一次是「先接上试试,转起来再说」。问题是电机裸接 GPIO 不一定当场冒烟,可能先能转几下、看着没事,引脚却已经在过流和电压尖峰里慢慢受损,过两天突然就不工作了。这种「延迟性损坏」最难排查。所以不是「能转就行」,是从第一次就走驱动板。
驱动板做的事:弱电控强电
既然引脚既给不出大电流、又扛不住反电动势,思路就清楚了:让单片机只负责「下命令」,让另一块电路负责「出力」。这块电路就是电机驱动板。
它的核心是一组功率开关(H 桥)。单片机给它几个毫安级的小信号——「正转」「反转」「多快」——驱动板就用自己接的独立大电源,按命令把大电流通断到电机上。同时它内部对反电动势做了处理(集成续流二极管),把电机的「反咬」挡在单片机之外。
这就是嵌入式里反复出现的标准套路:弱电控强电。单片机这边永远是干净的小信号,脏活累活(大电流、感性冲击、可能的高压)全交给中间的驱动级。你后面控制继电器、控制大功率灯、控制水泵,都是同一个模式。理解了这一层,半个 L2 的硬件你都通了。
以 TB6612 为例,它对每个电机给你三根控制脚,外加一根全局使能脚:
- 两根方向脚(A 路叫 AIN1 / AIN2):高低组合决定正转、反转、刹车、还是滑停。
- 一根速度脚(A 路叫 PWMA):给 PWM 占空比,决定转多快——这里就是你在 PWM 那节学的东西。
- 一根 STBY(待机)脚:整块芯片的总开关,必须拉成高电平芯片才工作。这是新手最爱漏的一根线,下面会专门强调。
完整代码:TB6612 控正反转 + 调速(ESP-IDF)
下面以 TB6612 驱动板的一路电机为例,全程用 ESP-IDF:方向脚走普通 gpio_set_level,速度脚走 LEDC PWM。先看接线,再看代码。
接线(一路电机):
ESP32-S3 GPIO4 ──→ 驱动板 AIN1 (方向脚 1)
ESP32-S3 GPIO5 ──→ 驱动板 AIN2 (方向脚 2)
ESP32-S3 GPIO6 ──→ 驱动板 PWMA (速度脚,走 LEDC)
ESP32-S3 GPIO15 ──→ 驱动板 STBY (待机脚,必须拉高)
驱动板 AO1 / AO2 ──→ 电机两根线
电机电源(电池组) ──→ 驱动板 VM 和 GND
ESP32-S3 GND ──→ 驱动板 GND ← 必须共地
驱动板 VCC ──→ ESP32-S3 3.3V (给芯片逻辑供电)
这些控制脚(AIN1/AIN2/PWMA/STBY)按你手上的板子可自由换,避开 strapping 脚(0/3/45/46)、USB 脚(19/20)和 26~37 段的 flash/PSRAM 脚即可,这里选的是 ESP32-S3 上安全的通用 IO。记住两条:①最后那根 GND 一定要把单片机的地和驱动板的地连到一起(共地),漏了它信号没有共同参考点,电机会乱转或干脆不动;②STBY 不拉高,电机一动不动,你会以为代码错了,其实是芯片在睡觉。
代码(放进工程 main/main.c):
// TB6612 单路电机:正转 → 停 → 反转 → 停,循环
// 方向脚用 gpio_set_level,速度脚用 LEDC PWM
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "motor";
#define AIN1 GPIO_NUM_4 // 方向脚 1 → 驱动板 AIN1
#define AIN2 GPIO_NUM_5 // 方向脚 2 → 驱动板 AIN2
#define PWMA GPIO_NUM_6 // 速度脚 → 驱动板 PWMA(走 LEDC)
#define STBY GPIO_NUM_15 // 待机脚 → 驱动板 STBY(必须拉高)
#define PWM_CH LEDC_CHANNEL_0
#define PWM_TIMER LEDC_TIMER_0
#define PWM_MODE LEDC_LOW_SPEED_MODE
// 配方向脚、STBY 为普通输出;速度脚交给 LEDC
static void motor_init(void)
{
// 方向脚 + 待机脚:普通数字输出
gpio_config_t io = {
.pin_bit_mask = (1ULL << AIN1) | (1ULL << AIN2) | (1ULL << STBY),
.mode = GPIO_MODE_OUTPUT,
};
gpio_config(&io);
// LEDC 定时器:8 位分辨率(占空比 0~255),与 PWM 调光那节同款
ledc_timer_config_t timer = {
.speed_mode = PWM_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = PWM_TIMER,
.freq_hz = 5000, // 5kHz,电机听不出啸叫
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer);
// 把 PWMA 那根 GPIO 绑到一个 PWM 通道
ledc_channel_config_t ch = {
.gpio_num = PWMA,
.speed_mode = PWM_MODE,
.channel = PWM_CH,
.timer_sel = PWM_TIMER,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ch);
gpio_set_level(STBY, 1); // 关键:唤醒 TB6612,不拉高电机不转
}
// 设速度:占空比 0~255,0=停转
static void set_speed(uint32_t duty)
{
ledc_set_duty(PWM_MODE, PWM_CH, duty);
ledc_update_duty(PWM_MODE, PWM_CH);
}
void forward(uint32_t speed) // 正转
{
gpio_set_level(AIN1, 1);
gpio_set_level(AIN2, 0);
set_speed(speed); // speed: 0~255,越大越快
}
void backward(uint32_t speed) // 反转
{
gpio_set_level(AIN1, 0);
gpio_set_level(AIN2, 1);
set_speed(speed);
}
void motor_stop(void) // 停(滑停)
{
gpio_set_level(AIN1, 0);
gpio_set_level(AIN2, 0);
set_speed(0);
}
void app_main(void)
{
motor_init();
while (1) {
ESP_LOGI(TAG, "正转 1.5s");
forward(180); vTaskDelay(pdMS_TO_TICKS(1500));
ESP_LOGI(TAG, "停 0.5s");
motor_stop(); vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI(TAG, "反转 1.5s");
backward(180); vTaskDelay(pdMS_TO_TICKS(1500));
ESP_LOGI(TAG, "停 0.5s");
motor_stop(); vTaskDelay(pdMS_TO_TICKS(500));
}
}
上面的 GPIO 编号、占空比、PWM 频率是参考实现,需按你的实际接线和电机自行验证。第一次跑务必让电机空载(别装到小车上),确认方向和调速正常再装轮子。
编译、烧录、看日志一条命令搞定:
idf.py build flash monitor
(第一次先 idf.py set-target esp32s3 选好芯片。)
你应该看到什么
- 串口里滚动打印
正转 1.5s/停 0.5s/反转 1.5s/停 0.5s,对应电机正转约 1.5 秒 → 停半秒 → 反转约 1.5 秒 → 停半秒 → 无限循环。 - 把
forward(180)里的 180 改大(比如 240),电机明显转得更快;改小(比如 90),慢下来甚至带载就转不动了——这就是 LEDC 占空比在控速度。 - 如果你接的两根电机线方向反了,「正转」看起来是反着转。没关系,把电机那两根线对调一下,或者把代码里
forward和backward的 AIN1/AIN2 互换即可,不影响安全。 - 如果电机纹丝不动、日志却照常打印——八成是 STBY 没拉高,或者那根线没接好。回头检查
gpio_set_level(STBY, 1)和接线。
AIN1 / AIN2 / PWM 方向真值表
把方向脚和速度脚的组合记成一张表,所有动作都从这里查:
| AIN1 | AIN2 | PWM 占空比 | 电机状态 |
|---|---|---|---|
| 1 | 0 | 1~255 | 正转,占空比越大越快 |
| 0 | 1 | 1~255 | 反转,占空比越大越快 |
| 0 | 0 | 任意 | 停(自由滑停,电机靠惯性滑停) |
| 1 | 1 | 任意 | 停(刹车,两端短接,立刻停住) |
| 任意 | 任意 | 0 | 停(无驱动) |
要点:方向由两根方向脚(AIN1/AIN2)用 gpio_set_level 输出的高低组合决定,速度由 PWMA 上的 LEDC 占空比决定,两者独立。AIN1 == AIN2 时电机停:0,0 是滑停(撒手让它自己慢慢停),1,1 是刹车(两端短接,急停)。代码里 motor_stop() 用的是滑停;想急停就把两根脚都置 1。
STBY 与供电共地:两条红线
电机这边的电、和单片机这边的电,处理方式完全不同;再加上 TB6612 那根容易漏的 STBY——这一段是整节最不能省的地方。
电机用独立电源供电(电池组 / 适配器),接到驱动板的 VM 脚,不要从单片机引脚或 USB 取电——电机的大电流会瞬间拉垮单片机供电,直接重启。驱动板电源地要和单片机共地(GND 连到一起),否则信号没有共同参考。电机启停有大电流冲击,建议在电源端加大电容稳压(H 桥内部已集成续流二极管,无需外接),进一步避免尖峰干扰单片机。
再强调四条,照做能避开八成翻车:
- STBY 必须拉高:TB6612 的 STBY 是芯片总开关,代码里
gpio_set_level(STBY, 1)一句没有,整块芯片就在待机,电机怎么都不转。这是最隐蔽的坑——日志一切正常,电机就是死的。 - 电源分两路:逻辑电(给 ESP32-S3)走 USB 或稳压,功率电(给电机)走电池组接 VM,两路各管各的,只在 GND 共地;驱动板的 VCC(逻辑供电)从单片机 3.3V 取。
- 先确认电压匹配:你的电机标称几伏,驱动板 VM 就给几伏(常见 6V/7.4V/12V),别拿 3.3V 去推 12V 电机,也别拿 12V 灌进只标 6V 的电机。
- 第一次上电先空载:电机别装到小车上,先让它空转确认方向和调速正常,再装上轮子,避免接错时小车直接窜出去。
L298N 还是 TB6612
两块都是常见的双路直流电机驱动板,选哪个:
| L298N | TB6612 | |
|---|---|---|
| 驱动技术 | 双极型晶体管,压降大(满载约掉 2V 多) | MOSFET,压降小、效率高 |
| 发热 | 大,常需散热片 | 小 |
| 体积 | 大 | 小巧 |
| 持续电流 | 单路约 2A,能扛更大电机 | 单路约 1.2A(峰值约 3A) |
| 控制脚 | IN1/IN2 + ENA(PWM 走 ENA) | AIN1/AIN2 + PWMA + STBY |
| 价格 | 老、便宜、好买 | 略贵一点 |
| 推荐 | 大电机/图便宜够用 | 小车轻负载更推荐 |
简单说:做小车、做轻负载,直接上 TB6612——它用 MOSFET、压降小、发热小、效率高、个头小,同样电池更耐用。L298N 是老方案,内部用双极型晶体管,压降大、发热明显,但能扛更大电流,如果你要驱动比较大的电机、或者手头只有它,也完全够用。
注意两点差异:①L298N 没有 STBY 脚,省掉 STBY 那根线和那句拉高;②L298N 的速度脚叫 ENA(使能),把上面代码里 LEDC 绑定的 PWMA 换成接 ENA 的 GPIO 即可,方向脚换成 IN1/IN2,其余 gpio_set_level + LEDC 的逻辑一模一样。照驱动板丝印对脚名就行。
故障排查:电机不对劲,按这张表查
| 现象 | 最可能的原因 | 怎么办 |
|---|---|---|
| 电机完全不转,日志正常 | STBY 没拉高 / 没共地 / VM 没接电源 / 占空比是 0 | 先确认 gpio_set_level(STBY, 1) 有执行且 STBY 接好;确认单片机 GND 和驱动板 GND 连到一起;确认 VM 接了电池;确认 set_speed 不是 0 |
| 只能往一个方向转 | 一根方向脚(AIN1 或 AIN2)没接好或代码写死 | 量一下 AIN1/AIN2 是否都接到位;核对 backward 分支确实切换了两根脚的 gpio_set_level |
| 一上电单片机就重启 | 电机从单片机取电 / 没独立电源 / 缺稳压电容 | 电机改走独立电池组接 VM;电源端加大电容;别让电机和单片机抢同一路电 |
| 驱动板或电机发烫 | 电压给高了 / 长时间堵转 / 电流超出驱动板能力 | 核对电机标称电压与 VM 是否匹配;别让电机长时间堵转;负载太大就换 L298N 或更大驱动 |
| 转速调不动,给多少都一样 | LEDC 没绑到 PWMA / 绑错了 GPIO / 没调 ledc_update_duty |
确认 ledc_channel_config 里 gpio_num 是接 PWMA 的脚;确认 set_speed 里调了 ledc_update_duty |
动手挑战:让小车前进和转向
会控一个电机,控两个就能做小车了。试试这个:
- 接第二路电机:TB6612 是双路,另一路用 BIN1 / BIN2 / PWMB(STBY 共用同一根)。给 LEDC 再配一个通道(
LEDC_CHANNEL_1)绑到 PWMB,照样写一组forward/backward/stop,参数换成第二个电机的脚。 - 前进:两个电机同向同速——小车直走。
- 转向:让一边电机比另一边慢(差速转向),或者一边正转、一边反转(原地打转)。试着写一个
turn_left():左电机慢、右电机快。
卡住了?把你的接线、想要的动作、和现在的 ESP-IDF 代码一起发给 AI,让它帮你补出双电机的转向函数和第二个 LEDC 通道——你已经掌握了单电机这一半,剩下的是复制和组合。
小结 · 你现在掌握了什么
- 你彻底搞懂了电机为什么不能裸接 GPIO:电流太大会烧引脚,感性负载的反电动势会击穿单片机,必须靠驱动板的 H 桥和续流二极管挡着。
- 你能用 ESP-IDF 控正反转和调速——方向靠 AIN1/AIN2 的
gpio_set_level真值表,速度靠 PWMA 上的 LEDC 占空比,两者独立。 - 你记住了 STBY 必须拉高 和 独立电源 + 共地 这两条红线,知道电机电和逻辑电要分开走。
- 你会选驱动板:小车轻负载上 TB6612(MOSFET、压降小、发热小),要大电流或图便宜用 L298N。
这套「单片机出小信号、驱动级出大力气」的弱电控强电思路,下一节会原样用到——只不过那次碰的不是电机,而是真正的强电。
下一步:学继电器,用同样的套路去控制 220V 这类强电负载。那一节的安全提示比这节还要紧,务必认真读。
本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。