1. 从点灯到转轮:为什么STM32小车的入门任务都选TT马达
我到现在还记得第一次用STM32把TT马达转起来的那个晚上,和之前点灯的体验完全不一样。点LED只是GPIO拉个电平,本质还在“数字世界”里打转;当TT马达接入电路、轮子真正开始旋转之后,你开始面对电流、功率、机械扭矩、电源噪声这些和现实物理强相关的问题。STM32 控制 TT 马达(直流电机)之所以被看作嵌入式入门“第二课”,不是因为它简单,而是因为它刚好踩在逻辑与物理的交界线上,把你学的定时器、PWM、GPIO、中断全部激活。这篇文章我会从电机本体、驱动电路、接线规范、CubeMX配置、HAL库代码、经典故障排查一路讲到差速小车扩展,中间穿插大量实测中踩过的坑。适合已经跑通开发环境、准备做小车底盘的同学,也适合正在被“电机一转单片机就复位”折磨的玩家对照排查。
1.1 TT马达到底是个什么东西:减速箱、额定参数与堵转电流
TT马达这个名称并不是严格意义上的工业型号,而是国内电子市场上的一种通用叫法。它通常由三部分构成:一个空心杯有刷直流电机、一组塑料齿轮减速箱、一根3mm输出轴。因为减速比不同,市面常见规格有1:48、1:90、1:120等,数值越大输出转速越低、扭矩越大。我做项目时最常用的是1:48版本,空载转速大约200到300rpm之间,搭配直径65mm轮子,做桌面小车正合适。
参数表里最需要盯住的是电流数据,很多新手恰恰栽在这上面:
| 参数 | 典型值 |
|---|---|
| 额定电压 | 3V ~ 6V |
| 空载转速 | 90rpm ~ 320rpm(由减速比决定) |
| 空载电流 | 100mA ~ 250mA |
| 堵转电流 | 0.8A ~ 2A |
| 输出轴 | 3mm,D形或圆轴 |
一片STM32的GPIO引脚,输出能力通常只有几到十几毫安。而电机空载时就要100mA以上,堵转时电流能瞬间冲到1A甚至更高。这已经不是一个“够不够”的问题,而是“GPIO根不是用来做功率输出”的问题。所以我一直强调:控制TT马达的实质不是让单片机去推电机,而是让单片机去控制一路“比它大得多的功率通路”,这个通路就是后面的驱动电路。
1.2 有刷直流电机的控制本质:通断、方向与占空比
先把有刷直流电机的控制模型建立起来。它本质上就是一个直流负载:给它正向电压就正转,反向电压就反转,不给电压就靠惯性滑行。所谓控制,拆解下来只有三件事:要不要通电、电流方向往哪走、一个周期里通电时间占多少。方向由H桥电路负责,转速由PWM波的占空比负责。
很多初学者把“调速”想复杂了,以为要输出一个“可变的直流电压”。实际上STM32输出的PWM波,是用高低电平的占空比去等效平均电压。占空比50%,平均电压就是电源电压的一半,转速大约也是空载转速的一半左右。这个理解一旦建立,后面写代码时你操作的每个寄存器就都有了明确意义:设置比较值就是设置占空比,拉高或拉低两个方向引脚就是改变电流方向,两根引脚同时拉高或同时拉低则是刹车或滑行。记住这个映射关系,你会少走很多弯路。
2. 驱动电路选型:H桥与驱动芯片,谁才是TT马达的正确搭档
直接给结论:TT马达必须经过驱动电路,不能直连STM32。市面上常见的驱动方案有三类:L298N模块、TB6612模块、DRV8833模块,另外还有自制分立H桥。很多人第一反应是“L298N最经典就选它”,我也确实走过这条路,但实测下来,它并不是TT马达的最好选择。下面把每条路都拆开聊透。
2.1 GPIFO不能直接驱动电机的根本原因
除了输出电流能力不足之外,另一个被忽略的问题是电机感性负载带来的反电动势。TT马达内部有线圈,电流突变时会产生反向电动势,如果不做续流处理,这个尖峰电压可以冲到几十伏,直接击穿单片机的引脚保护二极管。驱动芯片内部集成了续流二极管或MOSFET体二极管,能在PWM关断期间给感性电流提供泄放回路,这是“驱动电路能保护单片机”的第二个关键能力。
如果你选用的是没有集成续流桥的L298N裸板或自制H桥,那么外部必须额外并接续流二极管,通常采用快恢复二极管或肖特基二极管。很多模块板上已经画好了二极管,但廉价模块有时会为了省成本省掉,买回来先对着原理图验证一下,这点很值得养成习惯。
2.2 TB6612、L298N、DRV8833三款模块实测对比
我也在同一个TT马达上分别接过了这三款模块,差别真的能明显感觉到。L298N用的是双极性晶体管,导通时内部压降大约1.5V到2V。TT马达额定电压本来就只有3V到6V,如果给5V电源,L298N自己先吃掉2V,电机实际分到的电压可能只有3V多,转速会明显偏低,而且模块发烫严重。L298N更适合6V到12V电压区间的中型直流电机,对TT马达来说有些“杀鸡用牛刀”,性能却不占优。
TB6612FNG是我现在最推荐给纯新手的一款。它内部是MOSFET H桥,导通压降小得多,电机电压范围2.7V到13.5V,最大持续电流1.2A,峰值3.2A,够TT马达在堵转边缘使用。模块引脚也简单:VM、VCC、GND、PWMA、AIN1、AIN2、STBY,接线清爽,逻辑电压支持3.3V,能直接和STM32 GPIO对接。
DRV8833则更适合追求低电压和低压降的场景。它工作电压2.7V到10.8V,每通道1.5A,导通电阻非常低,单节锂电池3.7V供电时表现尤其是它好。它还带有输出电流限制功能,对防止小学生级别的“以为转不动就用手去捏”这类短路场景有奇效。如果你计划做一个手持设备或迷你小车,DRV8833的板子尺寸比TB6612还要小一圈。
三款模块的选型结论其实很简单:默认选TB6612,如果你只有3.7V单节电池且在意外形尺寸,选DRV8833,L298N留给你以后做中型机器人时再用。我不建议新手一上来就囤L298N,不是因为它坏,而是它的压降和体积会让调试体验下降不少。
2.3 自制H桥 vs 模块:什么阶段才值得自己搭
很多教程会教人用两个三极管或四个MOSFET搭H桥,这种精神值得鼓励,但不适合作为“第一个TB6612都没玩熟”阶段的方案。自制H桥涉及P沟道和N沟道MOSFET的搭配、栅极驱动电压是否够高、死区时间控制、续流二极管布局等复杂问题。如果只是想让TT马达转起来,模块的集成度和稳定性远比自制方案高。我自己的路线是先用TB6612把电机控制整个流程跑通,之后再回去补H桥原理和PCB设计,那样基础会更扎实,也不至于一开始就受挫。
3. 接线与供电:电机和单片机要“分居”,但地线必须连在一起
接线部分看似简单,其实是翻车率最高的一关。很多同学一接上程序,点击下载,电机没转,电脑却弹出“no stm32 target found”,第一反应以为是调试器坏了、驱动没装好,真正的原因往往是供电系统出了问题。这部分我把接线原则和细节一次讲清楚。
3.1 电机电源、逻辑电源要分离,但GND必须共地
先说核心原则:电机电源和STM32逻辑电源最好分开供电,但两块电源的GND必须连在一起。为什么?电机启动和堵转时会让电源电压剧烈波动,如果单片机的供电线直接和电机电源走同一条线路,瞬间的压降可能导致STM32断电复位。分开供电后,单片机和电机各自有稳定电源,噪声就不容易串过去。
但共地同样不能省。PWM控制信号属于逻辑信号,它的“0”和“1”参考的是单片机的GND,而驱动芯片内部判断逻辑电平参考的是模块的GND。如果两套地不连通,GPIO输出的3.3V高电平在驱动芯片看来可能是悬空的,逻辑完全乱掉。所以接线顺序应该是:
- 电池/电源正极接到驱动模块的VM电机电源端;
- 单片机开发板的5V或3.3V单独接到驱动模块的VCC逻辑电源端;
- 电池负极和开发板的GND一起接到驱动模块的GND;
- STBY引脚接3.3V或5V,让它脱离待机。
3.2 一套完整的TB6612接线表,直接抄作业
我举个例子。假设你手里是一块STM32F103C8T6核心板加上一块TB6612FNG模块,按照下面的引脚对应关系接线:
| TB6612引脚 | 接到STM32或电源 |
|---|---|
| VM | 电机电源正极(建议3V ~ 6V) |
| VCC | 3.3V(也可接5V,逻辑电平有区别) |
| GND | 电源负极与STM32的GND |
| PWMA | PA0(TIM2_CH1) |
| AIN1 | PA1 |
| AIN2 | PA2 |
| STBY | 3.3V或STM32 GPIO拉高 |
| AO1/AO2 | 电机一的两根线 |
保险起见,电机电源并联一个100uF到470uF的电解电容,位置越靠近VM引脚越好。这个电容的作用是吸收堵转瞬间的电流冲击,防止电压塌陷。我实测过很多次:加了电容后,系统稳定性提升非常明显,尤其是使用普通电池盒时,这个电容几乎是必需品。也有些封装模块已经带了比较大的稳压电容,你看一下板子背面,如果没有自己补焊一个最稳妥。
3.3 电源选型:别让TT马达超压或憋屈
TT马达额定电压在3V到6V之间,超过6V不是不能转,但长时间低速堵转时发热严重,很容易烧坏线圈。常见供电方案有三种:两节AA电池共3V,转速偏平稳,适合桌面小车;一节18650电池3.7V,理论上可以,但需要确认电池能输出足够的瞬时电流;两节18650串联7.4V则明显超压,不能直接接到电机的VM端。我在调试时发现一个项项目常用的做法是:动力系统用电池直接供电给驱动模块,STM32则通过独立的AMS1117稳压板从同一电池降压到3.3V。这样做既保持了电机电源波动不影响单片机,又能用一个电池给整机供电,市面上的智能小车底盘大多是这个架构。
4. STM32软件实现:CubeMX定时器PWM与方向GPIO的全套配置
软件部分我以STM32F103C8T6加HAL库为例,这套流程在F407、F722上几乎一样,只是引脚编号和定时器通道会有变化。标准库用户也别觉得被落下,原理完全互通,只需要把末尾我给的函数名对应替换成标准库写法即可。
4.1 定时器PWM频率怎么算:PSC、ARR和死区的关系
PWM频率由定时器时钟、预分频器PSC和自动重装载值ARR共同决定。公式是:
PWM频率 = 定时器时钟 ÷ ((PSC + 1) × (ARR + 1))
定时器时钟按72MHz计算。如果PSC=3,ARR=999,那么频率 = 72MHz ÷ ((3+1)×(999+1)) = 18kHz,占空比分辨率是1000级。这个配置我实测下来比较推荐:频率超过人耳敏感区的上限,听不到高频啸叫,1000级分辨率也足够平滑。
如果把PSC改成71、ARR保持999,频率就会降到约1kHz,占空比仍然有1000级,但这时候电机运转时你会听到非常明显的“吱吱”声,那是PWM的开关频率落在音频范围内,线圈铁芯在磁场变化下发出机械振动。频率并不是越高越好:频率越高,MOSFET和电机的开关损耗都会增加;频率太低,电流纹波变大,低速扭矩会明显不稳。18kHz对TT马达来说是一个甜点区间,大家可以直接抄这个配置。
CubeMX里的操作流程是:选择对应定时器,比如TIM2,把Channel1设置为PWM Generation CH1,然后在参数配置里把Prescaler设为3、Counter Period设为999,Pulse初始值可以留一个占空比如500。注意检查时钟树是否已经是72MHz,很多工程的HSE配置不同,实际频率对不上会让计算失真。
4.2 可复用的驱动函数封装:正转、反转、刹车、滑行
PWM配置完成之后,接下去把方向控制函数封装起来。PCB上我把PWMA接到了PA0对应的TIM2_CH1,AIN1接PA1,AIN2接PA2。基于此,完整的驱动函数代码如下:
// 启动PWM输出,占空比由CCR寄存器控制 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 设置速度,speed取值范围0~1000 void Motor_SetSpeed(uint16_t speed) { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed); } // 正转 void Motor_Forward(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); } // 反转 void Motor_Reverse(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); } // 刹车,快速停转 void Motor_Brake(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_SET); } // 滑行,断电依靠惯性 void Motor_Coast(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET); }方向逻辑需要结合TB6612FNG的真值表来理解:
| AIN1 | AIN2 | 电机状态 |
|---|---|---|
| 0 | 1 | 正转 |
| 1 | 0 | 反转 |
| 1 | 1 | 刹车(短接制动) |
| 0 | 0 | 滑行(不驱动) |
使用时先调用Motor_Forward()设置方向,再调用Motor_SetSpeed()给一个速度值,电机就会转动起来。切换方向时务必先调用Motor_Brake()或Motor_Coast()停一下,而不是直接从正转切到反转,原因放在下一节讲。
如果你用的是标准库,函数对应关系也就是TIM_SetCompare1(TIM2, speed)以及GPIO_WriteBit之类的调用,逻辑完全一致,可以参考着替换。
4.3 换向时为什么必须先刹车再反接
直接从一个方向切到另一个方向,最直观的后果是电源电流瞬间冲击很大。因为电机有惯性,转子的反电动势方向还在维持原转向,此时强行反向通电,相当于把一台正在全速转动的电机瞬间变成发电机并短路,电流会极致地冲到堵转值以上。对TT马达来说,这会加剧减速箱齿轮的冲击,对驱动模块来说则是一次电流高峰,有可能触发模块过流保护,还可能把电源电压打出一个深坑导致MCU复位。
我习惯在换向函数里强制加一个5到20ms的刹车窗口。比如要做反转,先拉高两个IN引脚让电机短接制动,等转速真正降下来,再设置反转方向的GPIO并恢复PWM。这个操作在纸面上只是多几行代码,但实测下来,模块的温度和成功切换的果断程度都有明显改善。不要让电机“硬拧”着反转,这是写驱动函数时最容易忽略的细节。
5. 实测高频故障排查:电机一转MCU复位、下载失败这些坑的根因
软件和硬件都接通之后,接下去的调试阶段才是真正的试炼场。我把这几年踩过以及帮朋友排查过的高频问题总结成四个方向,每个都给出排查链路和最终处理办法。
5.1 电机一转STM32立刻复位,下载报“no stm32 target found”
这个故障的典型表现是:程序里只要启动电机,开发板上的LED闪一下,随后板子掉电、程序从新开始跑,或者你在下载程序时根本识别不到芯片,调试器报“no stm32 target found”。
很多人第一反应是驱动板或者电机短路,但我遇见的绝大多数情况是电源问题。电机启动瞬间的电流抽走了电源线上的电压,STM32的VDD跌到复位阈值以下,芯片瞬间重启,SWD调试链路也随之断开。排查链路是这样的:先断开电机电源,单独给单片机供电并下载程序,确认下载器没问题;再用独立电源给电机供电,并把两块电源的地连好;最后在驱动模块VM端并联一个大电解电容,同时在STM32的3.3V引脚靠近处并联100nF陶瓷电容。做完这三步,绝大多数复位问题会消失。
如果仍然出现“no stm32 target found”,还有一种容易忽略的情况:你把电机的两根线接在了STM32的3.3V和GND上测试,导致芯片瞬间过流或引脚损坏。这不是驱动问题,是硬件连接问题,所以我再次强调驱动电路的必要性。
5.2 电机吱吱响,PWM频率选低了
电机运转时发出高频刺耳声响,大概率是PWM频率落在人耳可听范围。那个“吱吱”声并不是电机坏掉,而是定子铁芯在交变磁场作用下产生的磁致伸缩振动,频率等于PWM开关频率或其谐波。解决方式很直接:提高PWM频率到18kHz以上,或者低于可听范围也可以但通常不好用,因为低于几百赫兹后扭矩波动会非常厉害。
但是提高频率会带来一个副作用:频率越高,相同定时器时钟下ARR就越小,占空比分辨率就降低。74kHz下100步分辨率和18kHz下1000级分辨率给人的手感完全不一样。所以更合理的做法是保持PWM频率在17kHz到20kHz之间,通过调小PSC来尽量提高分辨率。我在前面给出的PSC=3、ARR=999就是一个兼顾两头的配置。
5.3 低速时转速忽快忽慢,“占空比死区”现象
转速调低以后电机出现断断续续或者完全停转的现象,这是有刷电机的死区效应。当占空比很低时,平均电压不足以克服电刷接触电阻和齿槽转矩,电机表现为不转或抖动。在这个区域里,占空比微调1%可能完全没反应,再调大一点又突然加速,给人感觉“非线性非常强”。
处理这个问题的常用手段是软件死区处理:把0到某个下限值(我常用10%)全部映射为停止状态,从10%开始才允许PWM实际输出,同时在10%附近用线性插值把曲线拉缓。这样处理之后,小车低速行驶时手感会平顺很多。另外有刷电机的碳刷磨损也会让低速品质下降,TT马达的电机寿命相对有限,不要长时间堵转虐待它。
5.4 调速还是不平滑,查一下定时器的时钟配置
有些同学按教程把PSC和ARR一改,结果转速依然一卡一卡。排查思路回到时钟树:CubeMX中如果APB1预分频不是1,那么挂在APB1上的定时器时钟会是APB1时钟的两倍,很多教程默认72MHz,但如果你改了外部晶振或HSE配置,实际定时器时钟可能不是72MHz。你用一个错误的时钟频率去反推PSC和ARR,自然不准确。
遇到这种情况,我建议打开CubeMX的Clock Configuration页面,直接看TIM2的时钟数值。也可以用调试器在线读RCC时钟寄存器的值,或者暴力一点:输出一个已知PWM,用示波器或逻辑分析仪实测频率,反推实际时钟。没有示波器的读者,用一根杜邦线把PWM引脚接到另一个定时器的输入捕获通道,用捕获模式直接测出频率,这也是一招很有用的“穷办法”。
6. 从驱动一个TT马达到驱动一台两轮小车:编码器、差速与闭环的进阶路线
聊完了单电机的控制细节,最后把它放到真实场景里,也就是两轮差速小车。这也是STM32模块教程里最常见的最终目标。当你把第一颗电机调顺之后,第二颗电机的加入会带来一个崭新的问题:两个电机的转速先天就存在差异,开环跑不直。你想让它走直线,它却总往一边偏,这几乎是每个小车爱好者都经历过的“幽灵跑偏”。
6.1 两路PWM加方向GPIO如何拼出差速转向
驱动两个电机只需要在驱动模块上再复用一组通道:TB6612的另一路PWMB、BIN1、BIN2正好对应第二个电机。初始化时再开一个定时器通道,比如TIM2_CH2,然后对左右轮分别设置速度值。差速转向的逻辑很简单:左轮速度大于右轮,车体右转;右轮速度大于左轮,车体左转;一正一反就能原地旋转。差速大小决定了转弯半径,两者速度相同才是直行。
但这个“直行”只有在理论中成立。现实中因为两个电机的绕组差异、轮胎半径差、地面阻力不一致,即使两个PWM占空比完全相同,转速也会存在几个百分点的偏差。想让它自动跑成直线,光靠开环PWM是不够的,这才有了闭环控制的必要。
6.2 带霍尔编码器的TT马达:测速原理与PID闭环的下一步
我建议下一步直接换成带霍尔编码器的TT马达,也就是市面上常见的“黄皮编码器减速电机”。它内部在电机转轴上装了霍尔传感器和磁性码盘,能够输出A、B两相脉冲,通过STM32的外部中断或定时器编码器模式,就能测出实际转速。A、B两相之间有90度相位差,不仅能测速,还能通过谁先跳变判断转向,这就是正交编码器的优势。
测速代码的思路非常简单:对每个上升沿触发一次外部中断,在一个固定时间窗口内统计脉冲数,再结合减速比和每圈脉冲数换算成rpm。有了实时转速之后,就可以用增量式PID对左右轮做闭环控制。具体来说,设定一个目标转速,PID根据实际转速和目标的误差调整PWM占空比,让两个轮子始终跟踪各自的目标。到了这一步,车跑直就再也不是“调参数碰运气”,而是变成了一套可控的反馈系统。
我在实际做小车时的习惯是先在桌面把单个电机的测速和PID调好,再把左右轮合起来做差速闭环。一次只增加一个变量,出问题时也更容易定位。比如先验证编码器计数是否准确,再用手动给定占空比观察转速是否稳定,最后才进入PID整定,避免比例、积分、微分三个参数和硬件问题搅在一起。这里也常出现新的坑,比如编码器模块上还有VCC、GND、A、B之外的引脚,注意它们是否和电机端子共地,不接共地的话A相脉冲会完全读不到。
最后再分享一个我调试小车时的习惯:永远在代码里保留一个“测试模式”,上电后先用固定占空比转动电机,再在串口打印实时的编码器脉冲数。这套自检流程在每次改装完车架、换电池之后都能帮你快速判断是硬件原因还是代码原因,省下来的时间非常可观。TT马达本身不复杂,但把它真正调顺这个过程,值得你多花心思慢慢磨。