做小车、做云台、做一个简单的机械臂,我遇到的第一类电机基本都是TT马达。它便宜、耐造、拆装方便,跟STM32搭配起来,刚好把GPIO、定时器、PWM和功率驱动这几个嵌入式核心外设一次过完一遍。这篇笔记不打算只讲“怎么接线、怎么敲代码”,我尽量把原理也揉进去——为什么电机不转、为什么发烫、为什么抖,这些坑我都踩过,写出来给后来人省点时间。不管你是刚点亮LED想进阶的新手,还是已经写过一些外设驱动、准备做两轮差速小车的同学,这篇内容都能让你少走弯路。
1. TT马达的核心特征与选型考虑
1.1 TT马达到底是个什么结构
TT马达从外观上看就是一个圆柱形电机加上一组减速齿轮箱,输出轴上还能直接套轮子或者码盘。很多人叫它“TT Motor”,其实这名字更多是来自智能小车套件里的习惯叫法,本质就是一台小功率有刷直流电机。内部结构上,它比无刷电机简单得多:永磁体提供定子磁场,转子上绕线圈,通过换向器和电刷把电流送进线圈,让转子持续旋转。减速齿轮箱的作用是把电机的高转速、低扭矩转换成低转速、高扭矩的输出,所以TT马达额定转速一般都在几十转到三百多转每分钟这个区间,搬动几十到两三百克的车身很轻松。
正是因为“有刷”这个属性,TT马达的控制特别直接:通上额定电压就转,反接两根线就反转,电压越高转速越快。代价是电刷和换向器之间会有机械磨损,寿命比无刷电机短。不过对学习和项目验证来说,这根本不是问题,几块钱一个,坏了直接换,肉不疼。
1.2 为什么TT马达是STM32入门首选
我见过不少新手上来就纠结“要不要直接上无刷电机配电调”,我的建议是:如果是第一次用单片机控制电机,老老实实从TT马达开始。原因有三点。
第一,驱动逻辑简单。有刷直流电机只需要控制电压大小和极性,一个PWM波加一个H桥直驱芯片就搞定,不涉及三相换相、反电动势检测这种复杂逻辑,适合把注意力放在STM32本身的配置和编程上。第二,减速箱带来的扭矩对DIY小车来说刚好够用,配合橡胶轮和电机支架,几个螺丝就能搭出一个能跑的底盘,成就感来得快。第三,TT马达有带编码器的版本(一般是霍尔编码器或光电码盘),后期做转速闭环、里程计、差速转向都够用。也就是说,从最基础的开关控制到PID调速,它都能一路撑下来。
拿STM32F103C8T6这种几十块钱的板子来说,TT马达刚好匹配它的外设资源:一个定时器输出PWM,一个定时器做编码器计数,剩下几个GPIO控制方向。整套驱动流程跑通之后,你其实已经把MCU控制物理世界的最基础闭环掌握了,再去看无刷、步进、伺服这些电机,思路会顺畅很多。
1.3 不要忽略的电气参数和实测数据
TT马达常见标称电压是3V到6V,我平时用的比较多的是6V版本,配两节或者四节镍氢电池都行。用实验室稳压电源实测,6V供电下空载电流大约100毫安上下,带个普通橡胶轮空转不会超过200毫安;但把输出轴捏住堵转,电流会瞬间飙到500毫安甚至更高。这个数据很重要——它直接决定了你不能拿STM32的GPIO去怼电机。
STM32的GPIO输出能力大概在3.3V、几毫安到二十毫安级别,连空载都喂不饱,更别说堵转。就算有人抱着“我直接从5V引脚取电给电机”的侥幸心理,堵转时那几百毫安的电流也会把开发板供电拉垮,轻则电压跌落导致复位,重则烧板载稳压芯片。所以我一直强调,驱动电路不是可选项,是必须项。
2. 驱动方案与原理:从H桥到TB6612FNG
2.1 为什么不能省掉驱动芯片
很多人第一次尝试是直接把电机两根线接在STM32引脚上,结果电机纹丝不动,甚至把引脚烧了。这里有两个原因。第一,电机的启动电流和堵转电流远超MCU的IO驱动能力。第二,电机本质是感性负载,通断瞬间会产生反向电动势,这个电压尖峰可能超过MCU耐压值,直接击穿引脚内部的保护二极管。
更麻烦的一个问题是“反转”。要让直流电机反转,必须让电流从反方向流过电枢,也就是把电机两端电压极性对调。一个独立的GPIO只能输出高或低,没法输出负电压,所以光靠单片机引脚根本无法实现双向转动。这些需求指向同一个解决方案:H桥驱动电路。
2.2 H桥的基本工作逻辑
H桥的电路结构只有四个开关管,电机接在桥臂中间,外观上像字母H,所以叫H桥。四个开关Q1、Q2在上半部分,Q3、Q4在下半部分,电机两端分别接在左桥臂和右桥臂之间。
想让电机正转,就打开左上角Q1和右下角Q4,电流从电源正极流过Q1、电机、Q4回到负极;想让电机反转,就换成右上角Q2和左下角Q3导通,电流方向反过来。如果让Q1和Q3、Q2和Q4这种同侧上下两个开关同时导通,就是直接把电源短路,这叫“直通”,轻则跳闸,重则烧管子,在实际驱动芯片里都会通过逻辑互锁来防止这种状态。除了正转和反转,H桥还能实现“刹车”和“滑行”:刹车是同侧两个管子导通,把电机两端短接,利用反电动势形成制动力矩;滑行则是四个管子全关,电机自由停转。
从控制角度讲,方向由两个开关组合决定,速度则由PWM占空比决定。而TB6612FNG这类驱动芯片,本质就是把这一整套H桥、死区逻辑、保护电路全部集成好的成品,你只需要通过两个GPIO选方向,一个PWM调速度。
2.3 TB6612FNG与L298N、分立MOS管的选型对比
做TT马达驱动,市面上主流的方案无非三种:L298N模块、TB6612FNG模块、自己搭分立MOS管H桥。我三个都用过,简单说说差别。
L298N是很多老教程里的常客,驱动能力强,但它是双极型晶体管方案,饱和压降大,1A以上电流时发热严重,而且模块巨大,放小车上很占地方。分立MOS管方案成本最低,但你需要自己处理逻辑电平转换、栅极驱动、防直通和散热问题,对新手不太友好。TB6612FNG是东芝的MOS管驱动芯片,压降只有零点几伏,静态电流低,体积小,驱动能力在1.2A左右,驱动TT马达这种小功率电机绰绰有余。它还有一个好处,逻辑电源只需要3.3V就能工作,刚好匹配STM32的电平。
如果你的电机功率更大,或者准备以后升级成带减速箱的较大直流电机,也可以考虑DRV8833或者A4950这类芯片,引脚逻辑类似,但具体细节还是要以数据手册为准。总体来说,STM32配TT马达,TB6612FNG是综合体验最好的选择。
2.4 TB6612FNG引脚说明与接线逻辑
TB6612FNG有14个引脚,正面看依次是:VM(电机电源)、VCC(逻辑电源)、GND、PWMA、AIN1、AIN2、STBY、BIN1、BIN2、PWMB、GND、GND、BO1、AO1、AO2、BO2,还有些模块会引出BO1/AO1这种电机输出口,不同模块丝印略有差异,接线前务必看清自己模块的引脚标注,不要想当然。
接线逻辑上,VM接电池正极;VCC接3.3V或者5V逻辑电源;GND必须和STM32GND、电池GND共地。PWMA接STM32定时器的PWM输出通道;AIN1和AIN2接两个普通GPIO;STBY是待机引脚,低电平时芯片停止工作,因此必须拉高才能正常输出。电机接AO1和AO2。ITE控制逻辑是一个真值表,我整理成了表格:
| STBY | AIN1 | AIN2 | PWMA | 电机状态 |
|---|---|---|---|---|
| L | x | x | x | 停止 |
| H | H | L | H | 正转(全速) |
| H | L | H | H | 反转(全速) |
| H | H | L | PWM | 正转(调速) |
| H | L | H | PWM | 反转(调速) |
| H | H | H | x | 刹车 |
| H | L | L | x | 滑行 |
这里要特别提醒一下,AIN1和AIN2同为高是刹车,同为低是滑行,这两个状态很容易被新手忽略。我在调试时一度以为输出低电平就是不转,结果电机是处于滑行状态,被外力推动还是会缓慢转动。区分清楚这两者,对做精确定位的小车很重要。
3. STM32侧PWM和GPIO的配置方案
3.1 PWM调速的原理和参数选择
PWM调速的本质是改变电机两端电压的有效值。占空比越高,一个周期内高电平占的时间越长,电机获得的平均电压越接近电源电压,转速就越快。STM32的定时器输出PWM时,大致原理是:计数器不停从0计到ARR(自动重装载值),比较寄存器CCR里存一个阈值,计数器值小于CCR时输出高电平,到达CCR后输出低电平,计数器溢出后清零重新计数。所以改变CCR就改变了占空比,ARR决定了PWM频率。
对于TT马达这种小直流电机,PWM频率在1kHz左右是多数人验证过的甜点区。频率太低会听到电机“咔哒咔哒”的顿挫声,速度控制也不线性;频率太高比如20kHz以上,虽然听不见声音,但驱动芯片的开关损耗会上升,电机反而更容易发热。我一般习惯把定时器配成72MHz主频,预分频PSC设成71,自动重装载ARR设999,这样PWM频率就是72MHz/(71+1)/(999+1)=1kHz,非常干净。
3.2 CubeMX配置流程走一遍
用STM32CubeMX配定时器很方便,我以STM32F103C8T6为例说明关键步骤。首先在Pinout界面选好单片机型号,打开TIM2,勾选Channel1的PWM Generation CH1。然后在Parameter Settings里把Prescaler设成71,Counter Period设成999,Pulse初始设成0,这样PWM周期是1kHz,初始占空比0%。打开Clock Configuration确认APB1总线频率是72MHz,这样不会数错分频系数。
方向控制引脚我用PA6和PA7,配置成GPIO_Output,初始电平设成Low。这个初始状态很有讲究,最好在程序上电时先把方向引脚确定下来,再开PWM。否则在MCU复位到main函数执行这段极短的时间内,引脚是浮空状态,如果驱动芯片STBY被意外拉高,电机可能猛地抽动一下,虽说不至于烧毁,但吓人一跳是难免的。
3.3 驱动代码的结构设计思路
我习惯把电机驱动封装成一个独立模块,不把逻辑全堆在main.c里。在dc_motor.h里定义方向枚举和结构体,结构体成员包括PWM定时器句柄、通道号、方向引脚号和速度范围。这样做的好处是,以后从两路电机扩展成四路,复制粘贴结构体再改引脚就行,不需要每个main文件挖一次五脏六腑。
控制函数最好统一成一个接口,比如motor_set_speed(Motor_Handle_t *motor, int speed),speed范围从-1000到1000,正数正转,负数反转,0表示刹车。函数内部先判断speed的符号,再根据符号设置方向引脚电平,最后用__HAL_TIM_SET_COMPARE(motor->htim, motor->channel, abs(speed))更新占空比。这里必须注意一个顺序:先改方向引脚,再更新PWM,避免方向切换瞬间出现瞬间反转的毛刺。
4. 完整接线与代码实操记录
4.1 接线清单与共地原则
实际动手接线时,我建议把电源路径和逻辑路径分开理清楚。电源路径是电池正极到TB6612的VM,电机两端到驱动芯片的AO1、AO2;逻辑路径是STM32的3.3V到TB6612的VCC,PWM、方向引脚到对应输入。核心的一步是共地:电池负极、TB6612的GND、STM32的GND必须接到同一个参考点,否则控制信号没有回路,驱动芯片无法正确识别高低电平。
我常用的接线组合是这样(STM32F103C8T6开发板):
| TB6612FNG引脚 | 连接目标 |
|---|---|
| VM | 6V电池正极或5V电源正极 |
| VCC | STM32 3.3V |
| GND | 电池负极、STM32GND |
| PWMA | PA6(TIM2_CH1) |
| AIN1 | PB0 |
| AIN2 | PB1 |
| STBY | 3.3V |
| AO1 | TT马达正极 |
| AO2 | TT马达负极 |
在面包板上飞线时,尤其是电池线的杜邦线,我吃过不少亏:有些劣质杜邦线内芯细,满载时压降能到0.5V以上,电机转速明显偏低。后面我改用粗一点的杜邦线或者直接用电线焊接,问题就没了。如果手头有大电容,建议在VM和GND之间焊一个470uF电解电容,并联一个0.1uF瓷片电容,可以有效平抑堵转瞬间的电压跌落和反向电动势尖峰。
4.2 一个可以直接上手的工程代码
下面这套代码片段的思路是:上电后先让电机以50%占空比正转3秒,再反转3秒,然后刹车2秒,循环执行,方便你在没有示波器的情况下验证方向和PWM是否正常。我基于STM32 HAL库编写,CubeMX生成工程后,只需要补上这些逻辑。
/* dc_motor.h */ #ifndef __DC_MOTOR_H #define __DC_MOTOR_H #include "main.h" typedef enum { MOTOR_STOP = 0, MOTOR_FORWARD, MOTOR_BACKWARD } Motor_Dir_t; typedef struct { TIM_HandleTypeDef *htim; uint32_t channel; GPIO_TypeDef *in1_port; uint16_t in1_pin; GPIO_TypeDef *in2_port; uint16_t in2_pin; } Motor_Handle_t; void Motor_Init(Motor_Handle_t *motor, TIM_HandleTypeDef *htim, uint32_t channel, GPIO_TypeDef *in1_port, uint16_t in1_pin, GPIO_TypeDef *in2_port, uint16_t in2_pin); void Motor_SetSpeed(Motor_Handle_t *motor, int speed); #endif/* dc_motor.c */ #include "dc_motor.h" void Motor_Init(Motor_Handle_t *motor, TIM_HandleTypeDef *htim, uint32_t channel, GPIO_TypeDef *in1_port, uint16_t in1_pin, GPIO_TypeDef *in2_port, uint16_t in2_pin) { motor->htim = htim; motor->channel = channel; motor->in1_port = in1_port; motor->in1_pin = in1_pin; motor->in2_port = in2_port; motor->in2_pin = in2_pin; HAL_TIM_PWM_Start(htim, channel); __HAL_TIM_SET_COMPARE(htim, channel, 0); } void Motor_SetSpeed(Motor_Handle_t *motor, int speed) { if (speed > 1000) speed = 1000; if (speed < -1000) speed = -1000; if (speed > 0) { HAL_GPIO_WritePin(motor->in1_port, motor->in1_pin, GPIO_PIN_SET); HAL_GPIO_WritePin(motor->in2_port, motor->in2_pin, GPIO_PIN_RESET); } else if (speed < 0) { HAL_GPIO_WritePin(motor->in1_port, motor->in1_pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(motor->in2_port, motor->in2_pin, GPIO_PIN_SET); } else { /* 这里选择刹车,而不是滑行 */ HAL_GPIO_WritePin(motor->in1_port, motor->in1_pin, GPIO_PIN_SET); HAL_GPIO_WritePin(motor->in2_port, motor->in2_pin, GPIO_PIN_SET); } __HAL_TIM_SET_COMPARE(motor->htim, motor->channel, (uint32_t)(speed > 0 ? speed : -speed)); }/* main.c 中的调用示例 */ Motor_Handle_t motor1; void Motor_Test_Demo(void) { Motor_Init(&motor1, &htim2, TIM_CHANNEL_1, GPIOB, GPIO_PIN_0, GPIOB, GPIO_PIN_1); Motor_SetSpeed(&motor1, 500); /* 50%占空比正转 */ HAL_Delay(3000); Motor_SetSpeed(&motor1, -500); /* 50%占空比反转 */ HAL_Delay(3000); Motor_SetSpeed(&motor1, 0); /* 刹车 */ HAL_Delay(2000); }这个代码我已经在F103开发板上实际跑过,稳定性和可靠性没什么问题。使用时的注意点有三处:一是HAL_TIM_PWM_Start之后要立刻设占空比为0,防止上电瞬间输出高电平让电机全速转一下;二是方向引脚的电平改变必须赶在PWM更新之前;三是speed为0时建议用刹车而不是滑行,这样小车停止后不会因为惯性溜车。
4.3 调试过程的现场记录
第一次上电时,我踩过一个很蠢的坑:STBY引脚悬空了。TB6612的STBY内部没有上拉,悬空时电平不定,芯片有时候工作有时候不工作,表现出来就是电机偶发抖动一下然后完全没动静。当时我拿着万用表量了半天VM、VCC都正常,最后才怀疑到STBY头上。从那以后,我拿到模块的第一件事就是把STBY直接焊到3.3V,省心。
还有一次,电池电压在7V左右(四节镍氢),直接接VM,发现电机转速明显偏低,一量VM电压只有4.8V。原因是电池内阻加上杜邦线压降,启动瞬间掉压很严重。我在VM和GND之间加了一个470uF电容,问题立刻改善。不要小看这个电容,它对整个系统的稳定性帮助非常大,尤其是后面加上编码器测速后,你会发现示波器上PWM信号的毛刺少了很多。
5. 常见问题与排查技巧实录
5.1 电机完全不转,或者只会抖不会转
排查这类问题,我习惯按信号路径从后往前查:先看驱动芯片的供电,再查逻辑输入,最后看PWM和GPIO配置。
第一步用万用表量TB6612的VM和VCC,确认电压都正常,特别是共地必须接好。第二步检查STBY是否为高电平,很多人在这栽过。第三步查AIN1和AIN2的电平组合是否符合预期。如果方向引脚设置不对,比如两个都是低电平,电机就处于滑行状态,你用手去转它转得动,但程序里它不会自己跑。第四步用逻辑分析仪或示波器探PWM引脚,确认确实有波形输出。如果PWM输出正常但电机依然不动,就要怀疑驱动芯片本身是否损坏,换一块模块试试。
电机“嗡嗡”响但就是堵着转不起来,这种多半是PWM频率太低或者占空比太小。低于3%的占空比,很多TT马达的静摩擦力矩都克服不了,直接不转。解决办法是把PWM频率调到1kHz附近,同时让初始占空比从10%甚至20%起步。
5.2 电机能转,但转速明显无力
转速偏低,先说一个最容易被新手忽略的情况:电机电源是从STM32开发板的5V引脚取的。开发板的5V往往来自USB供电或者稳压芯片,最大输出电流本来就有限,电机一启动就把5V拉垮,轻则转速骤降,重则整板复位。正确做法是给电机准备独立的电池电源,比如两节18650或者4节AA电池,然后电池正极接TB6612的VM。
还有一种情况是杜邦线太细太长,电池到VM之间的线路压降过大。我实测过普通杜邦线在500mA电流下确实能掉0.3V左右,电机稍微带点负载转速下降非常明显。建议电源线用稍微粗一点的硅胶线,把长度控制在10厘米以内。最后别忘了检查电池本身是不是没电了,充满的电池和快没电的电池,驱动同一个电机那是完全两种表现。
5.3 驱动芯片发热,系统偶发复位
TB6612正常工作时会有一点温热,但如果烫手,就要考虑是不是接线有短路、电机堵转时间太长、PWM频率过高导致开关损耗增大。另一个常见问题是反电动势干扰。电机在换向和制动瞬间会产生很高的电压尖峰,这个尖峰如果顺着电源线窜回开发板,ESD和复位就都来了。
处理方法是在VM和GND之间加足够容量的储能电容,同时把电机电源线和信号线分开走,不要在面包板上让它们并排飞线。如果你的系统里同时有舵机或者编码器,也要注意电源拓扑,舵机启动瞬间的电流脉冲很容易影响相邻的逻辑电路。我在做四轮小车时就遇到过编码器读数偶发跳变的问题,后来在驱动电源端加了电容、把编码器线换成双绞线,问题才彻底解决。
5.4 ST-Link下载失败:no stm32 target found
这个问题在STM32调试中太经典了,报错提示大体是“Error: No STM32 target found! If your product embeds Debug Authentication, please...”之类。遇到这个报错时,第一反应不要慌,按照顺序排查。
先把ST-Link的四根线(SWDIO、SWCLK、GND、3.3V)确认接对,排除接触不良。然后检查STM32的BOOT0跳线,如果BOOT0接高电平,芯片会进入系统存储器引导模式,此时SWD口有时连接不稳定。更隐蔽的一个原因是:你之前在程序里把SWDIO和SWCLK两个引脚复用成了普通GPIO,导致调试器无法连接芯片。解决办法是按住开发板上的复位键,在IDE里点击下载,当IDE提示正在连接目标时瞬间松开复位键,多试几次一般能抢在程序运行前把新固件下载进去。如果还是不行,用串口ISP方式,把BOOT0拉高,通过串口烧录一个清空程序的固件,然后再恢复BOOT0为低电平,SWD口就回来了。
5.5 抖动、异响和后续的测速闭环
有些TT马达配的是编码器版本,输出轴后面带码盘和两个霍尔传感器,能输出A、B两路正交信号。这时候你用STM32的一个定时器配置成编码器模式,就能直接读出转速和方向。我在做两轮差速小车的时候,就用TIM4的编码器模式加TIM2的PWM输出,配合增量式PID调节左右电机速度,最终实现了直线行驶,走个四五米偏移量很小。
这里想提醒一句:闭环控制的前提是开环驱动先跑得稳。如果开环状态下电机转速就不稳定、抖动明显,直接上PID只会让参数调得怀疑人生。先解决PWM频率、供电和接线这些硬件问题,让开环驱动的电机转起来稳定、有力,再谈PID参数整定。这是我做了好几版小车之后最深的体会。方向确认、PWM占空比固定测试、编码器计数是否线性,这些步骤一步步来,问题会少很多。
写在最后的几点实验体会
TT马达看起来只是一个简单的小电机,但把它玩明白,其实把嵌入式里很重要的一整条链路都走了一遍:GPIO控制方向、定时器输出PWM、功率驱动芯片选型、电源完整性和地回路设计,再加后面的编码器测速和PID闭环。每一个环节拿出来,都是工程上躲不开的硬功夫。
我个人在实际操作中最受用的一个习惯是:调试电机驱动时,永远先把占空比固定成一个中间值,比如30%到50%,只去切换方向引脚,确认方向逻辑和刹车逻辑都正确了,再开始动态调速。这样排查问题的时候,变量少了很多,不会一会儿怀疑PWM、一会儿怀疑GPIO,乱成一锅粥。另外一点,接线布线时一定要在脑子里把“电源路径”和“信号路径”分开,两者只在共地点交汇,这样绝大多数干扰和掉压问题都能在设计阶段避免掉。
如果你正准备做自己的第一台小车,或者想把手里的TT马达用起来,建议就按这篇文章的步骤走一遍。别看步骤细碎,等你真正跑通“正转—反转—刹车”这三个动作,后面做编码器测速、里程计、差速转向,都会顺理成章。希望这篇笔记能帮你少踩几个我当年踩过的坑。