STMicroelectronics的STM32系列在电机控制项目里几乎是绕不开的选择,而TT马达(直流减速电机)又是小车、机械臂、智能窗帘这类入门项目里最常见的那颗“心脏”。很多新手在点亮LED之后,第一次卡住的地方往往就是“电机怎么不转”“怎么一转单片机就重启”“怎么调速”。这篇教程我从选型、驱动电路、PWM调速到问题排查一步步拆开讲,全是实际项目里验证过的经验,想给正在做STM32小车或者直流电机相关项目的朋友一个可以直接照着做的完整参考。
1. 先搞懂TT马达:从参数到驱动原理
1.1 TT马达是什么,为什么项目里总用它
TT马达并不是某个厂商的型号,而是国内创客圈对一类“130电机+塑料齿轮减速箱”组合的通俗叫法。它通常由一个空心杯直流电机和一个多级塑料齿轮减速箱组成,输出轴转速比电机本体转速低很多,但扭矩明显增大。这种马达广泛用在智能小车、循迹车、避障车、机械臂关节等入门项目里,价格便宜、结构简单、控制逻辑直白,非常适合用来学习电机驱动和PWM控制。
与步进电机、伺服电机相比,TT马达的优点是“只要给电就转、换向就反着转”,不需要编码器反馈(除非你自己加霍尔或者编码器版本),控制难度低。缺点是精度和定位能力基本为零,转速和负载关系也比较松。所以它适合做“让轮子转起来”“让窗帘拉起来”这类不需要精确位置控制的应用,而不适合做需要准确定位的关节控制。
1.2 关键电气参数:电压、转速、电流怎么看
市面上常见的TT马达分1:48、1:90、1:120等几种减速比,同一个电机本体在额定电压下,减速比越大输出转速越低、扭矩越大。选型时主要看三个参数:
- 额定电压:大多数TT马达额定电压3V-6V,最常用的是3V和6V两种。3V版本接3.3V-4.2V能跑,6V版本建议用6V-7.4V供电,超过额定电压太多会缩短寿命甚至烧毁。
- 空载转速:比如1:48减速比、6V额定电压的空载转速大约在200-300rpm左右,这个值决定了小车跑多快。
- 堵转电流:这是新手最容易忽略的参数。TT马达堵转电流通常在0.5A-2A之间,具体看规格书或者用万用表实测。堵转电流决定了驱动电路和电源的选型,如果选了个只能输出几百毫安的LDO去带电机,一堵转系统必崩。
补充一个我自己的经验:选马达不能只看空载参数,要看应用场景。如果做小型循迹小车,1:48的6V版本很合适;如果做爬坡车或者机械臂,扭矩优先,选1:120甚至更高减速比;如果做风扇、云台这类高速应用,可以选1:20以下低减速比甚至直接不加减速箱。
1.3 为什么STM32的GPIO不能直接驱动马达
很多新手第一次接电机都会想:STM32的GPIO不是能输出高电平吗?直接把电机接到引脚上不就行了?答案是不行,原因有两个。
第一是电流能力不够。STM32的单个GPIO输出电流一般只有20mA左右,而TT马达正常工作电流动辄几百毫安,堵转时更是超过1A。强行接上去的结果就是GPIO被拉垮、电压跌落、单片机复位甚至引脚烧毁。
第二是电机是感性负载。电机绕组在断电瞬间会产生反向电动势,电压可能冲到几十伏甚至更高,这种尖峰直接灌进GPIO引脚,非常容易把单片机干掉。所以电机驱动必须要有隔离、扩流和保护措施。
用生活化的类比来解释:GPIO就像一个只能背20斤东西的人,TT马达是一台需要背200斤货物的机器。你不能让这个人直接去扛,得给他配一台叉车,这“叉车”就是电机驱动模块或H桥电路。
2. 驱动方案选型:L298N、TB6612FNG还是分立H桥
2.1 常见驱动模块横评
给STM32配TT马达,主流方案有三大类:集成电机驱动模块、分立H桥电路、以及带逻辑的驱动芯片。我把最常见的几种放一起对比:
| 方案 | 典型代表 | 输入电压范围 | 最大持续电流 | 控制复杂度 | 适用场景 |
|---|---|---|---|---|---|
| L298N模块 | 经典L298N | 5V-12V | 2A(实际1A左右建议降额) | 低 | 学习、大电流电机、不介意压降 |
| TB6612FNG模块 | TB6612FNG | 2.7V-5.5V(逻辑)/6V-13.5V(电机) | 单路1.2A | 低 | 小车主驱、TT马达最佳搭档 |
| DRV8833模块 | DRV8833 | 2.7V-10.8V | 单路1.5A | 低 | 低压TT马达、锂电池供电 |
| 分立H桥 | 自己搭三极管/MOS管H桥 | 由元件参数决定 | 取决于元件 | 高 | 学习原理、特殊电压需求 |
| 集成PWM芯片 | L9110S | 2.5V-12V | 单路0.8A | 低 | 微型TT马达、方案预算有限 |
L298N是很多教材里的常客,但它的缺点是压降大(约1.5V-2V),6V供电到电机端只剩4V多,电机明显没力。而且模块体积大、功耗高,不太适合现在的小车项目。TB6612FNG的MOS管导通压降非常小,效率高,尺寸也小,是我做小车主驱时的首选。DRV8833则更适合3.3V逻辑直接控制,不需要额外电平转换。
2.2 我为什么推荐TB6612FNG作为入门首选
TB6612FNG是东芝出品的一颗双路H桥驱动芯片,一路H桥就能驱动一个TT马达,一颗芯片刚好驱动两个马达,完美匹配两轮差速小车。它的优势有几个:
- 导通压降小,6V输入、负载电流0.5A时压降约0.1V-0.3V,电机端电压几乎等于电源电压,扭矩损失小。
- 控制信号电平兼容3.3V和5V,STM32的GPIO可以直接驱动,不用额外加电平转换。
- 内置续流二极管,不需要外接反接保护二极管,省事很多。
- 待机电流极低,控制引脚拉低可以让芯片进入待机模式,降低整机功耗。
实际接线时TB6612FNG有四个控制引脚(AIN1、AIN2、PWMA、STBY)对应一路电机,另一路(BIN1、BIN2、PWMB)对应另一路。AIN1/AIN2控制旋转方向,PWMA控制PWM占空比调速,STBY需要置高才能工作。控制逻辑很简单:正转就是AIN1=1、AIN2=0,反转就是AI N1=0、AIN2=1,停止就是两个都为0,刹车是两路都置1也能实现急停。
2.3 关于续流保护和反电动势的坑
如果用电机的朋友自己搭H桥或者买裸芯片而不是模块,一定要记住续流二极管这件事。电机在断电瞬间会产生反向电动势,如果不加续流路径,这个电压尖峰可能击穿MOS管或三极管。集成驱动模块(TB6612、DRV8833、L298N)内部都做了处理,裸芯片或分立元件就必须在每个输出端对电源正极加一个快恢复二极管(或者用MOS管体二极管,但性能不如外置二极管)。
我自己踩过这个坑:第一次用两片L9110S裸芯片驱动TT马达,没加任何保护,结果马达正反切换时芯片三番五次烧掉,示波器一看尖峰直接干到20多伏。后来加装续流二极管和瓷片电容,问题彻底消失。
3. 硬件连接与电路设计细节
3.1 接线图和引脚分配
以STM32F103C8T6最小系统板 + TB6612FNG模块 + 两个TT马达为例,推荐这样接线:
| TB6612引脚 | STM32引脚 | 说明 |
|---|---|---|
| PWMA | PA0(TIM2_CH1) | A路电机PWM调速 |
| AIN1 | PA1 | A路方向控制1 |
| AIN2 | PA2 | A路方向控制2 |
| PWMB | PA3(TIM2_CH2) | B路电机PWM调速 |
| BIN1 | PA4 | B路方向控制1 |
| BIN2 | PA5 | B路方向控制2 |
| STBY | PA6 | 待机控制,置高使能 |
| VM | 7.4V或6V电源正极 | 电机电源 |
| VCC | 3.3V | 逻辑电源 |
| GND | GND(共地!) | 与STM32共地 |
关于引脚选择有个细节:PWM引脚必须选在定时器通道上,PA0对应TIM2_CH1、PA1对应TIM2_CH2(注意这里PA1同时是通道2,如果PWM和方向引脚冲突就容易出Bug)。我习惯把两路PWM放在同一个定时器的不同通道上,这样两个电机的PWM频率天然一致,调速同步性更好。
3.2 电源设计:为什么电机一转单片机就重启
这是新手问得最多的一个问题:“我的代码没问题,一上电程序能跑,但电机一转整个系统就重启”。十有八九是电源设计出了问题。TT马达启动瞬间电流非常大,如果STM32和电机共用一个LDO或一块小功率电池,电机的启动压降会把系统电压拉到单片机的复位门槛以下,从而触发复位。
解决办法有三条,建议都做:
- 给电机单独供电。单片机用USB的5V或者一路稳压,电机用电池直接供电,两边只在GND处连通。跳线帽那种两路共地的设计,其实是正确的,千万不要把电机电源和逻辑电源完全隔离,因为控制信号需要共同参考地。
- 在电机电源两端并联一个大容量电解电容(220uF-470uF,耐压高于电源电压),再并一个104瓷片电容。电解电容负责吸收瞬态的电流冲击,瓷片电容滤高频干扰。
- 尽量选内阻小的电池。普通碳性电池内阻大,带动电机瞬间电压跌落特别明显;镍氢、锂聚合物电池内阻小,表现就好很多。
3.3 逻辑电源的另一个隐藏问题:3.3V还是5V
TB6612FNG的逻辑电源引脚VCC可以接3.3V也可以接5V。接3.3V时,STM32的3.3V GPIO高电平可以直接识别;接5V时,如果STM32引脚是3.3V容限,GPIO输出的高电平不能被芯片正确识别为逻辑高。所以最简单稳妥的方案是:VCC直接接3.3V,与STM32的VD D一致,无需任何电平转换。
还有DRV8833这种芯片,逻辑电源范围比较宽,就更不用担心了。如果手头只有L298N模块,它的逻辑部分很多模块已经自带了5V稳压输出,直接用模块上的5V给逻辑部分供电也行,但注意别拿这个5V去给STM32供电,模块的5V稳压能力通常不够强。
4. 代码实现:从GPIO点动到PWM平滑调速
4.1 用GPIO实现正反转:最小可运行代码
先写一个最简单的控制程序,让马达正转1秒、停1秒、反转1秒。这里用HAL库演示,标准库思路一样,只是寄存器操作更直接。
// 定义方向引脚 #define AIN1_PIN GPIO_PIN_1 #define AIN2_PIN GPIO_PIN_2 #define AIN1_PORT GPIOA #define AIN2_PORT GPIOA void Motor_Init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } void Motor_Forward(void) { HAL_GPIO_WritePin(AIN1_PORT, AIN1_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(AIN2_PORT, AIN2_PIN, GPIO_PIN_RESET); } void Motor_Reverse(void) { HAL_GPIO_WritePin(AIN1_PORT, AIN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(AIN2_PORT, AIN2_PIN, GPIO_PIN_SET); } void Motor_Stop(void) { HAL_GPIO_WritePin(AIN1_PORT, AIN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(AIN2_PORT, AIN2_PIN, GPIO_PIN_RESET); } int main(void) { HAL_Init(); Motor_Init(); while (1) { Motor_Forward(); HAL_Delay(1000); Motor_Stop(); HAL_Delay(500); Motor_Reverse(); HAL_Delay(1000); Motor_Stop(); HAL_Delay(500); } }注意一个小细节:如果使用TB6612,还要把STBY引脚拉高,否则芯片始终处于待机状态,电机怎么都不转。很多新手在排查“代码没问题但电机不动”时就是卡在STBY没置高。
4.2 PWM调速:定时器配置与占空比计算
方向用GPIO控制后,速度就要靠PWM了。STM32的定时器可以输出PWM,核心是配置三个参数:预分频器(PSC)、自动重载值(ARR)和比较值(CCR)。PWM频率由PSC和ARR共同决定:
$$上 = PWM频率 = \frac{定时器时钟}{(PSC+1) \times (ARR+1)}$$
以STM32F103为例,TIM2挂在APB1上,时钟为72MHz。想得到20kHz的PWM频率(听不到噪声的频率),可以设PSC=71,ARR=49,计算得到:
$$上 = \frac{72MHz}{(71+1) \times (49+1)} = \frac{72MHz}{3600} = 20kHz$$
占空比就是CCR/(ARR+1)的比值。CCR=25时占空比50%,CCR=49时占空比接近100%。
HAL库的配置代码如下:
TIM_HandleTypeDef htim2; void PWM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // PSC=71, 72MHz/72=1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 49; // ARR=49, PWM频率=1MHz/50=20kHz htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 25; // CCR=25, 初始占空比50% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); }此时把PWMA引脚接到PA0(TIM2_CH1)上,写CCR值就能调速度:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 10); // 占空比约20% __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 40); // 占空比约80%4.3 两轮差速小车的转向逻辑:最简实现
两轮差速小车用两个TT马达分别驱动左右轮,转向原理很简单:左轮转右轮不转就向右转,左轮慢右轮快就向右转弯,两个轮子以不同速度转动即可实现不同半径的转向。整套逻辑就一行:左轮PWM、右轮PWM分别给不同CCR值。
我当时做小车时把速度抽象成一层接口,方便以后加调速或循迹:
void Set_LeftMotor(int speed) // speed: -100 ~ +100 { if (speed >= 0) { Motor_Left_Direction(1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed * 49 / 100); } else { Motor_Left_Direction(0); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (-speed) * 49 / 100); } }这里把CCR范围映射到-100到+100,负数就切换方向。然后转向就变成“左轮给70、右轮给30”这种直观操作,后续加红外循迹、超声波避障,只需要改speed参数,不用动底层硬件逻辑。
5. 常见问题与排查技巧实录
5.1 电机不转:从硬件到软件的系统排查
电机不转是最常见的问题,我一般按这个顺序排查,基本能在几分钟内定位:
| 排查项 | 操作 | 常见原因 |
|---|---|---|
| 供电电压 | 用万用表量VM和GND之间电压 | 电池没电、电源线断、降压板限流 |
| 使能引脚 | 确认STBY已接高电平 | STBY悬空或没配置,芯片待机 |
| 控制逻辑 | 单独给AIN1高电平、AIN2低电平,看是否转动 | IN1/IN2接线反了或者逻辑配反 |
| PWM引脚 | 确认PWM引脚选的是定时器通道对应引脚 | 引脚和定时器通道不匹配 |
| 共地 | 确认电机电源地、STM32地、模块地是否连在一起 | 地没共、控制信号回不了路 |
| 烧录状态 | 单独写个GPIO点灯程序验证引脚输出正常 | 引脚被复用、初始化顺序问题 |
如果芯片发热、有异响,多半是堵转或者输出短路,立即断电摸一下芯片温度,避免扩大损坏。
5.2 电机一转MCU就复位或者串口打印乱码
这个问题前面在电源设计里讲过,本质是电源跌落。但还有另一种可能:开关电源的纹波大,地线细,电机的电磁干扰串进了STM32的复位电路或串口线路。处理思路是:
- 电机线上加磁环或者绕几圈磁珠,滤掉高频干扰。
- STM32的复位引脚外部加一个104对地电容,增强抗干扰能力。
- 串口线用双绞线或者屏蔽线,缩短走线长度。
- 终极办法:把电机电源和逻辑电源的地星型连接,不要让大电流的地和小信号的地混在一起走。
我用充电池给六通道小车供电时,电机启动瞬间电压从7.4V拉到6.2V,系统没复位但PWM输出抖动明显,最后在电池两端加了470uF电解电容,问题彻底解决。
5.3 PWM调速时有啸叫声或者转速不稳
PWM频率太低(比如几百赫兹)会让人耳听到高频啸叫,同时也容易导致电机转速波动。推荐把PWM频率设在15kHz到20kHz以上,超过人耳听觉上限就不容易听到噪声。但要注意,频率太高时电机的感性阻抗变大,实际电流会下降,速度反而上不去。折中下来20kHz对TT马达是一个很稳的选择。
如果转速明显不稳定,比如周期性忽快忽慢,除了PWM频率问题,还要检查脉冲是否平滑。定时器CCR突变太快会导致电机电流突变,最好是做一个加速度限制,比如设定转速变化量每50ms只允许变化5%,这样启动更顺,对机械齿轮箱也友好很多。
int target_speed = 80; // 目标速度 int current_speed = 0; while (1) { if (current_speed < target_speed) { current_speed += current_speed + 5 > target_speed ? target_speed - current_speed : 5; Set_LeftMotor(current_speed); Set_RightMotor(current_speed); } HAL_Delay(50); }5.4 ST-Link 烧录时提示 no target found 的常见场景
不少人在做完电机接线后突然发现程序烧不进去了,提示类似“no stm32 target found”或者“error: no stm32 target found! if your product embeds debug authentication”。这种情况往往不是单片机坏了,而是你的复位电路或电源受到了电机干扰,导致调试接口不稳定。先断开电机电源、单独用USB给开发板供电再试一次;如果恢复了,就说明是供电问题。另外还要检查SWDIO和SWCLK两根调试线有没有被GPIO复用掉。
我自己就遇到过切换电机方向瞬间SWD断开,后来在SWDIO线上串了33欧电阻,同时在调试口和地之间加了一个104电容,再也没出现过烧录失败的问题。
5.5 关于IO驱动能力和PWM通道的误区
这里再强调一个很容易被忽视的点:STM32的GPIO驱动能力是有限的,但很多人以为“GPIO输出高电平就能驱动电机模块的逻辑输入端”。如果是TB6612这种CMOS输入,输入阻抗高,GPIO直接驱动没问题。但如果你的驱动模块是L298N的旧版,输入电路带了LED指示灯和大阻值上拉,单片机GPIO带载能力不够时电压会被拉低,造成逻辑不可靠。这时候可以用开漏输出加上拉电阻的方式提高驱动能力,或者加一个74HC245芯片做缓冲。
PWM通道的复用也非常容易翻车:PA0默认是TIM2_CH1,但如果你把PA0复用成了ADC或者普通GPIO,PWM信号根本出不来。遇到PWM输出异常,第一件事就是查复用功能配置和GPIO_AF是否正确。
最后分享一个我自己的工程习惯
TT马达听起来简单,但要把两个马达跑得协调、稳定,其实考验的是电源设计和代码架构。我现在做小车类项目,永远先把底层电机驱动抽象好,方向、速度、加速度限制都封装成独立函数,应用层只关心“往哪走、走多快”,不关心寄存器细节。这样后面加循迹、加遥控、加蓝牙,逻辑都是同一套代码,扩展起来非常快。
另外一个实用建议是:调试电机控制时不要直接用全速20kHz,先降到1kHz左右,用示波器看波形对不对,确认PWM频率和占空比匹配后再切回20kHz。否则在代码有Bug的情况下高频率PWM会对电机造成很激烈的冲击,长时间运转容易烧驱动。
TT马达配合STM32做小项目的上限其实很高,从两轮差速小车到小型机械臂、从鱼缸喂食器到智能窗帘,底层都是这一套“方向控制+PWM调速”的组合。把这套基础打牢,后续玩什么花样都不慌。