做小车、做机械臂、做自动浇花装置,只要是“带得动”的活儿,绕不开的第一个执行元件基本都是直流电机,而TT马达又是直流电机里最接地气的那一类。物美价廉、自带减速箱、装上轮子就能跑,所以从课程设计到各种竞赛小车,几乎到处都有它的身影。这篇内容我打算把STM32、TT马达、直流电机这一整条驱动链路完整拆开讲一遍,从H桥驱动原理、硬件接线、HAL库代码到PID调速和常见问题排查,一次说透。适合刚接触电机控制的同学,也适合想快速把电机驱动模块化抄走的开发者。
1. TT马达与控制方案的底层逻辑
1.1 TT马达到底是什么,选型时别看错减速比
TT马达这个名字其实是业内叫法,全称是TT Motor,因为圆柱形外壳加一颗塑料齿轮箱,大家干脆都叫它TT马达。它本质上不是一个单独的电机,而是“直流电机本体 + 减速齿轮箱 + 输出轴”的组合体。你拿到手的那颗东西,拆开里面就是一颗普通的空心杯或者永磁直流电机,输出轴经过齿轮减速后,转速降下来、扭矩提上去,这样才带得动轮子或者机械臂。
选型时最容易看走眼的参数是减速比。市面上的TT马达减速比常见有1:20、1:48、1:90甚至1:120,数字越大输出转速越慢,扭矩越大。做小车底盘常用1:48,在6V供电下空载转速差不多在150到200转每分钟,配合直径65mm的轮子,跑起来速度适中,既不会慢到让人着急,也不至于快到失控。减速比选小了,扭矩不够,小车起步会抖甚至原地不动;选大了,极速太低,跑起来像蜗牛。所以拿到模块第一件事,看减速比,别只问“这是不是TT马达”。
此外还有一种带编码器的TT马达,尾部多了一块小PCB,上面有两个霍尔传感器,输出A、B两路方波信号,用于测速和判断旋转方向。普通版就只能靠PWM开环控制,带编码器版才能做闭环调速。做两轮差速小车或者需要精确控速的项目,建议一步到位买带编码器的版本,差价不大,后面能省很多事。
1.2 为什么控制TT马达要用PWM加H桥
直流电机要调速,本质是调节它两端的平均电压;要反转,本质是改变电流方向。这两件事落到STM32的控制方案上,就对应两个概念:PWM和H桥。
PWM调速的原理一句话就能讲清楚:通过高频开关电压,让电机在一个周期内部分时间通电、部分时间断电,由于电机转子和绕组有惯性,它感受到的平均电压等于“占空比 × 供电电压”。占空比是50%时,等效电压大概就是一半。这个逻辑和生活里的电风扇很像,你拧低档开关,风扇不会一会儿转一会儿停,而是整体转速变慢,只不过电风扇是靠绕组抽头改变电压,PWM是靠高速开关模拟出不同的平均电压。
TT马达本身是一个电感负载,不像灯珠那样直接吃平均电压就能稳定工作,所以PWM频率也不能太低。频率太低,电流纹波大,电机会一冲一冲,还会发出滋滋的啸叫。后面我会专门讲频率选多少合适。
反转为什么必须要H桥?因为电机只有两根线,你给A高B低,它正转;想要反转,就得让A低B高,也就是在电机两端切换电源极性。H桥由四个开关管组成,对角导通控制电流方向,正好完成这个切换。你用分立元件搭H桥当然也可以,但还得考虑续流、死区、过流保护这些问题,对新手太不友好。所以实际项目里,基本都是买一颗集成电机驱动芯片,里面的H桥、逻辑电路和保护电路都已经做好,你只需要告诉它方向和占空比就行。
1.3 L298N、TB6612FNG、DRV8833驱动芯片怎么选
TT马达常用的驱动方案有三类,我先把对比摆出来:
| 驱动方式 | 类型 | 管压降 | 逻辑电压 | 适用场景 | 主要短板 |
|---|---|---|---|---|---|
| L298N | 双H桥,三极管方案 | 约2V | 5V | 教学演示、大电流电机 | 发热严重,低压场景效率低 |
| TB6612FNG | 双H桥,MOSFET方案 | 约0.5V | 2.7V~5.5V | 小型直流电机、TT马达 | 单路峰值电流有限(约1.2A) |
| DRV8833 | 双H桥,MOSFET方案 | 低 | 2.7V~5.5V(逻辑),VM可到10.8V | 小型电机、电池供电设备 | 小封装,接线需要转接板 |
个人首选TB6612FNG,原因很简单:STM32的IO电平是3.3V,TB6612的逻辑电源和支持的电平范围都在3.3V附近,不需要额外做电平转换;管压降只有零点几伏,6V电池进去,电机端还能有接近5.5V,扭矩不容易打折。L298N虽然便宜又经典,但它是老一代三极管方案,管压降能吃掉将近2V,TT马达这种低压电机本来就电压不高,再被吃掉一段,转速和扭矩都会明显缩水,而且芯片烫得很。DRV8833性能也不错,就是封装小,手工焊接或者用转接板都稍微麻烦一些,新手用TB6612最省心。
如果你手里只有L298N,也不是不能用,但心里要有数:6V供电下电机实际分到的电压可能只有4V左右,在实际项目中如果你发现小车动力明显弱,优先怀疑压降问题,而不是PWM配置错了。
2. 硬件接线与驱动电路,焊错一个电容都可能翻车
2.1 TB6612FNG引脚功能与STM32接线规划
TB6612FNG模块上的引脚看起来有点多,其实功能非常清晰。它内部有两路独立H桥,分别叫A路和B路,可以同时驱动两个电机,正好对应两轮差速小车的左右轮。
每个电机需要三个控制信号:两路方向输入(AIN1/AIN2)和一路PWM输入(PWMA),电机输出是AO1/AO2两个端子。另外模块上有VM、VCC、GND三个电源引脚,这里最容易犯错:VM是电机电源,接电池正极或者可调电源的电机端;VCC是逻辑电源,接STM32的3.3V;GND一定要和STM32的GND连在一起。再有一个STBY引脚,这是芯片的使能脚,必须拉高电平,芯片的PWM输出才会真正生效,悬空的话电机死活不转,这是新手翻车率最高的原因之一。
我实际项目里常用的引脚分配如下:
| 功能 | 引脚 | 说明 |
|---|---|---|
| 左电机PWM | PA6 | TIM3_CH1 |
| 右电机PWM | PA7 | TIM3_CH2 |
| 左电机方向1 | PB0 | AIN1 |
| 左电机方向2 | PB1 | AIN2 |
| 右电机方向1 | PB10 | BIN1 |
| 右电机方向2 | PB11 | BIN2 |
| 左电机编码器A/B | PB6/PB7 | TIM4_CH1/CH2 |
| 右电机编码器A/B | PA0/PA1 | TIM2_CH1/CH2 |
这里特别说明一下,如果你只是单电机测试,不用把左右轮都接上,PWM占空比和方向组合的逻辑是一样的。如果你准备做两轮差速闭环,那左右电机编码器都需要独立定时器,避免和PWM定时器冲突。
2.2 电源共地与滤波,电机一启动MCU就复位的根源
TT马达工作时电流动态变化非常大。空载电流可能只有几十毫安,但启动瞬间或者堵转时,电流能飙升到几百毫安甚至接近1A。如果你把STM32的USB供电和电机供电混在同一个电源上,电机一启动,电压被拉低,STM32瞬间掉电复位,表现就是小车一动,单片机就重启,程序永远跑不完。
解决办法就是分开供电:STM32用USB或者单独的5V供电,电机电源用独立的电池组或者开关电源,比如两节18650串联(约7.4V)或者4节5号电池(约6V)。两套电源之间只需要共地,也就是把STM32的GND和驱动板的GND接在一起,给控制信号提供一个共同的参考电位。这个共地不是可选项,是必选项,不共地的话,PWM信号的电平参考点不对,电机可能会出现各种诡异的小抖动或者不转。
经验上,我会在电机电源两端并联一颗100uF到470uF的电解电容,再并一颗0.1uF瓷片电容,尽量靠近驱动芯片的VM和GND引脚。因为电机换向的瞬间,线路电感会产生尖峰毛刺,电容能把这些毛刺吸收掉一部分。如果发现驱动芯片旁边的电容摸起来烫手,多半是电容耐压不够或者极性焊反了,换一颗立刻就好。
2.3 方向控制引脚与PWM引脚的组合逻辑
TB6612的输入逻辑控制电机工作状态,这一点用表格记最方便:
| AIN1 | AIN2 | PWMA | 电机状态 |
|---|---|---|---|
| 1 | 0 | 正占空比 | 正转 |
| 0 | 1 | 正占空比 | 反转 |
| 0 | 0 | 任意 | 滑行(惯性停止) |
| 1 | 1 | 任意 | 刹车(强停) |
看到没有,如果两个方向引脚同时为高,不管PWM占空比多大,电机都是处于刹车状态,转速上不去。很多同学“PWM加到100%电机还是没力”,先查一下IN1/IN2是不是写成了同为1。
还有一点,电机的两根输出线AO1/AO2怎么接都行,正反只影响旋转方向。如果程序里设定正转,但电机转反了,不用改代码,把两根输出线对调一下就行,或者把方向引脚的电平逻辑对调。但要注意,一旦确定了对线方式,后面编码器的方向判断也要跟着一致,不然闭环调试时会出现“目标正转,反馈却是负数”的乌龙。
3. 基于HAL库的驱动代码实现,从CubeMX到函数封装
3.1 CubeMX里的定时器PWM配置
现在主流做法是用STM32CubeMX生成工程,这里以STM32F103C8T6加HAL库为例。打开CubeMX后,把系统时钟配置到72MHz,然后找到TIM3这个定时器。
在TIM3的配置页面里做两件事:第一,把Clock Source设置为Internal Clock;第二,把Channel1和Channel2的模式都选成PWM Generation CHn。这样TIM3_CH1和TIM3_CH2就会分别映射到PA6和PA7。引脚映射可以在芯片视图上确认,鼠标点一下引脚就能看到复用功能。
接下来要设置两个关键参数:Prescaler和Counter Period,它们决定PWM频率和占空比分辨率。假设定时器输入时钟是72MHz,要让PWM频率为10kHz,计算方法如下:
PWM频率 = 定时器时钟 / ((Prescaler + 1) × (Counter Period + 1))
我常用的组合是Prescaler = 71,Counter Period = 99。算一下:72MHz / ((71 + 1) × (99 + 1)) = 72MHz / 7200 = 10kHz,刚好。占空比分辨率就是Counter Period的档位,从0到99一共100档,对应0%到100%的占空比,对TT马达来说足够细腻了。
如果你想让占空比更精细,可以保持10kHz不变,把Prescaler设为71,Counter Period设为999,这样占空比有1000档,不过对这个小电机来说有点浪费,100档完全够用。CubeMX配置完成后直接生成代码,PWM初始化函数已经帮你生成好了,不需要手写定时器初始化。
3.2 电机控制函数封装,正反转和调速一套搞定
CubeMX生成的代码只是把PWM通道初始化了,实际控制还需要自己封装函数。我习惯用一个简单的结构体把电机相关的资源绑在一起,这样左右电机可以复用一套代码。
typedef struct { TIM_HandleTypeDef *htim; // PWM定时器 uint32_t channel; // PWM通道 GPIO_TypeDef *in1_port; // 方向引脚1 uint16_t in1_pin; GPIO_TypeDef *in2_port; // 方向引脚2 uint16_t in2_pin; } Motor_t;初始化时把引脚资源填进去,然后写核心的调速函数:
void Motor_SetSpeed(Motor_t *motor, int speed) { // speed范围:-100 ~ +100 if (speed > 100) speed = 100; if (speed < -100) speed = -100; 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); __HAL_TIM_SET_COMPARE(motor->htim, motor->channel, speed); } else { // 反转,占空比取绝对值 HAL_GPIO_WritePin(motor->in1_port, motor->in1_pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(motor->in2_port, motor->in2_pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(motor->htim, motor->channel, -speed); } }这里有个小细节,Counter Period配置的是99,所以占空比数值范围就是0到99,直接把speed当成CCR寄存器值就行,不用再做比例换算。有人习惯把Period设成999,那speed就要乘上10再写入,写代码前先看下自己的配置,不然会出现“写入100%实际只有10%”的错误。
主程序里初始化完定时器,记得调用HAL_TIM_PWM_Start启动PWM输出,然后就可以:
Motor_SetSpeed(&motor_left, 60); // 左轮正转60% Motor_SetSpeed(&motor_right, -40); // 右轮反转40%这样左右轮就按你要的方向和速度转起来了。
3.3 定时器编码器模式测速,转速和方向都能读出来
带编码器的TT马达尾部有两路霍尔信号,称为A相和B相。电机转动时,A、B两路输出相位差90度的方波,正转时A相超前B相,反转时B相超前A相。STM32的定时器编码器模式可以同时捕获这两路信号,自动完成方向判断和计数累加,不需要你在中断里手动处理,非常方便。
CubeMX里配置编码器模式以TIM4为例:把TIM4的Channel1和Channel2都设为Encoder Mode,然后在参数里把Encoder Mode选为TI1 and TI2,也就是同时使用两路信号,这种方式叫4倍频,计数精度最高。PB6和PB7会自动被复用为TIM4的编码器输入引脚。
代码里只需要初始化并启动编码器模式:
TIM_Encoder_InitTypeDef encoder_cfg = {0}; encoder_cfg.EncoderMode = TIM_ENCODERMODE_TI12; encoder_cfg.IC1Polarity = TIM_ICPOLARITY_RISING; encoder_cfg.IC1Selection = TIM_ICSELECTION_DIRECTTI; encoder_cfg.IC1Prescaler = TIM_ICPSC_DIV1; encoder_cfg.IC1Filter = 0; encoder_cfg.IC2Polarity = TIM_ICPOLARITY_RISING; encoder_cfg.IC2Selection = TIM_ICSELECTION_DIRECTTI; encoder_cfg.IC2Prescaler = TIM_ICPSC_DIV1; encoder_cfg.IC2Filter = 0; HAL_TIM_Encoder_Init(&htim4, &encoder_cfg); HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);然后在定时器中断里或者主循环的定时间隔内读取计数器值:
int16_t count = (int16_t)__HAL_TIM_GET_COUNTER(&htim4); __HAL_TIM_SET_COUNTER(&htim4, 0);这里用int16_t类型特别重要,因为计数器是16位有符号数,正转时递增,反转时递减,符号正好表示方向。如果用uint16_t读,反转时会读出一个很大的正数,后面算速度就会乱套。
速度换算公式也不复杂。一般TT马达编码器电机端每转一圈输出11个脉冲,经过1:48减速后,输出轴转一圈对应 11 × 48 = 528 个原始脉冲,4倍频后就是2112个计数。如果每100ms读一次计数器值,那一秒钟的计数就是 count × 10,转每秒就是 count × 10 / 2112,再乘以60就得到每分钟转速RPM。实际编码器参数请以你买的模块标成为准,有些是13线,公式里的数要对应改。
3.4 PWM频率到底选多少,1kHz和10kHz差别在哪
很多教程用1kHz驱动电机也能转,但我建议你直接上10kHz,理由有三点。第一,1kHz时PWM周期是1ms,电机绕组的感性滤波不够平滑,电流纹波明显,转速波动会比较大,尤其低速时能感觉到一顿一顿的。第二,1kHz落在人耳听觉范围内,电机和齿轮箱会发出刺耳的滋滋声,在教室里演示特别明显。10kHz已经接近听觉上限,听起来安静很多。第三,TB6612这类MOSFET驱动芯片的开关损耗在10kHz级别非常小,完全扛得住,不需要担心发热问题。
如果你用的是L298N这种三极管方案,超过20kHz开关损耗会明显增大,所以也不建议盲目把频率调高。综合下来,TT马达配TB6612用10kHz是最省心的选择。
4. 从转动到闭环:串口监控、PID调速与差速控制
4.1 串口打印转速,先看得见才能调得准
电机能转了只是第一步,你还需要知道它到底转多快。最直接的办法是串口把转速数据打出来。STM32的USART1接USB转串口模块,通过printf把目标速度、当前实际转速、PWM输出值这些关键信息实时发到电脑上看。
使用HAL库时,printf需要重定向,简单做法是在usart.c里加一个fputc函数:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10); return ch; }然后在主循环里定时打印:
printf("target=%d, rpm=%d, count=%d\r\n", target_speed, rpm, encoder_count);串口数据能让你直观看到两个现象:一是电机实际的转速和目标转速差多少,二是转速的波动有多大。如果rpm在目标值附近来回晃,说明电机摩擦力不均匀,或者PWM频率太低。有了这个数据基础,再去调PID就有依据了。
串口还可以顺便做一个命令解析功能,比如通过串口发送“S120”设置目标转速120RPM,“P1.0 0.1 0”临时设置一组PID参数。这样调参不用反复编译下载程序,效率高很多。
4.2 一个能用的PID速度环,代码和调参顺序
PID速度反馈的流程不复杂:编码器测出实际转速,和目标转速求误差,误差经过PID运算得到一个PWM占空比输出。我用一个结构体把PID状态保存起来,方便左右轮各维护一套参数。
typedef struct { float kp, ki, kd; int target; // 目标转速 int error; // 当前误差 int last_error; // 上次误差 float integral; // 积分累计 int output; // 输出限幅后的PWM } Pid_t; void Pid_Calc(Pid_t *pid, int feedback) { pid->error = pid->target - feedback; pid->integral += pid->error; // 积分限幅,防止积分饱和 if (pid->integral > 5000) pid->integral = 5000; if (pid->integral < -5000) pid->integral = -5000; int derivative = pid->error - pid->last_error; pid->output = (int)(pid->kp * pid->error + pid->ki * pid->integral + pid->kd * derivative); // 输出限幅到占空比范围 if (pid->output > 100) pid->output = 100; if (pid->output < -100) pid->output = -100; pid->last_error = pid->error; }调参顺序是有讲究的,别上来就三个参数一起试。先把Ki和Kd设成0,只留Kp。从小往大加,比如从0.1开始,观察转速响应:如果实际转速一直在目标值附近振荡,说明Kp太大;如果响应很慢、差值始终消不掉,说明Kp太小。TT马达经过减速箱后齿隙和静摩擦力比较明显,Kp太小的时候电机根本拉不动,转速会一直差一截,这是正常现象,继续加大Kp到轻微振荡再回调一点就行。
Kp调到一个能让转速基本接近目标、但有静差的值后,再加Ki消除静差。Ki从很小的值开始,比如0.02,大了转速会来回冲过头,甚至形成等幅振荡。最后加Kd,能抑制超调,但如果编码器计数本身波动大,Kd反而会把噪声放大,实测下来Kd很多时候设成0就够用了。
4.3 两轮差速小车怎么走直线和转向
两轮差速小车的底盘,本质是左右轮独立驱动。直线行驶不是让左右轮PWM相同,而是让左右轮转速闭环后相同。开环给同样占空比,看起来公平,但两个电机的机械特性、摩擦、齿轮磨损都不一样,实际速度会有偏差,小车会慢慢偏到一边。
做法是左右各维护一套PID,分别读取左右编码器转速,然后给同一个目标转速,让两个闭环各自去追。走直线的质量,很大程度取决于左右编码器计数的准确性和两个PID的调参一致性。
转弯的原理也不复杂,就是给左右轮不同的目标转速。设基础速度为base,偏量为delta,那么左转弯时left_target = base - delta,right_target = base + delta;原地旋转时left_target = base,right_target = -base。代码里只需要在目标分配时修改target值,底下的PID每个周期正常跑就行。
我踩过的坑是,转弯偏移量给太大,小车转弯特别猛,导致编码器计数跟不上,PID错误地把PWM拉满,然后电机堵转、电流飙升。后来我把delta做成和当前速度相关的比例值,低速时小转弯、高速时也只能温和转向,整体就稳很多。做差速小车的人,建议先用串口把左右轮实际转速打出来,确认两个轮子在同目标下的偏差,再上整车的运动控制。
5. 常见问题与排查技巧实录
5.1 电机不转,先查这三个地方
电机完全不转时,别急着怀疑代码,按这个顺序排查下来,九成问题能解决。
第一步,查STBY。TB6612模块的STBY引脚如果没有拉高,芯片内部所有输出都是高阻态,电机端量不到电压。有些模块已经加上拉电阻,有些没有,稳妥做法是直接接一个GPIO拉高,或者在硬件上跳线到3.3V。
第二步,查电源。用万用表量VM和VCC引脚,VM应该有电池电压,VCC应该有3.3V或者5V。如果VM有电但电机不转,再量AO1和AO2两端电压,正转且PWM 100%时,这两端电压应该接近VM电压。
第三步,查方向逻辑。方向引脚的组合决定了电机是转、滑行还是刹车。如果AIN1和AIN2同为高,电机处于刹车状态,就会“有电但转不动”。另外PWM引脚要确认是否有波形,最简单的方式是用示波器,没有示波器的话可以临时把PWM引脚接一个LED,亮度随占空比变化就说明PWM确实输出了,只是后续接线有问题。
5.2 芯片发烫、电机一启动就复位,多半是电源问题
芯片发烫通常有几个原因:电源接反、VM和VCC电压过高、PWM频率过高导致开关损耗大,或者电机长时间堵转。TB6612正常工作温度是温热,如果摸上去烫到不敢碰,立刻断开电源,先检查VM和VCC是否接反,再看电机是否被机械卡死。
电机启动瞬间MCU复位的现象,基本可以确定是电源功率不足或者没有把电机电源和逻辑电源分开。解决办法前面已经提过,分开供电并共地,同时在电机电源两端加滤波电容。如果使用的电池内阻太大,也会在启动瞬间被拉低电压,这时候换一颗高倍率电池或者把电池容量做大一点会有明显改善。
还有一个容易忽略的点,就是杜邦线接触不良。电机电流大时,劣质杜邦线压降很严重,甚至发热,导致VM实际到芯片的电压远低于电池端电压。如果你发现电机空载和堵转的力度都不对,把杜邦线换成短粗的线或者直接焊接,问题立竿见影。
5.3 编码器读数乱跳或者方向反了怎么处理
编码器计数乱跳,先检查共地。编码器信号和MCU的GND如果不共地,信号电平参考点不一致,读数肯定乱。然后检查接线长度,TT马达的编码器信号线如果和电机电源线并行长距离走线,电机换向的干扰会耦合进编码器信号,造成误计数。处理办法是让编码器信号线尽量远离电机电源线,或者给每一路编码器输出加一个10nF左右的滤波电容到GND。
计数器数值一直增大、但电机实际方向和程序判断的方向相反,这是AB相接反了。解决办法可以交换编码器的A、B两根线,或者在代码里把读到的计数值取负再使用,哪个方便用哪个。但要记得,左右电机都要检查一遍,不要只调一边,否则后面左右轮方向会不一致,小车直接原地打转。
TIM编码器模式里的ICFilter参数也可以适当加大,比如设成0x0F,这是硬件层面的输入滤波,能过滤掉部分毛刺,比软件滤波效率更高。不过滤波加到太大,会损失高频信号的响应,对TT马达这种转速不高的场景影响不大。
5.4 下载程序失败,报“no stm32 target found”意味着什么
烧录程序遇到过“error: no stm32 target found”的人应该不少,这个问题是ST-Link或J-Link调试器没有和目标芯片建立连接,而不是程序本身写错了。
排查方向先看硬件连接:SWDIO、SWCLK、GND三根线是否都接到位,调试器和目标板是否共地,杜邦线是否过长。连接线超过20cm时信号质量会明显下降,解决办法是把线剪短,或者在MDK的Debug设置里把SW时钟频率降低,比如从4MHz降到1MHz。
另一个常见原因是程序把SWD引脚复用成了普通GPIO。比如你在代码里把PA13和PA14配置成普通IO输出,程序下载进去后,调试接口就被占用了,第二次再下载就找不到芯片。这种情况下,按住目标板复位键不放,点击下载按钮,在开始下载的瞬间松开复位,成功率会高一些;再不行就用串口ISP方式擦除芯片。所以做项目时,SWD引脚如果还有其他用途,一定要在读代码前想清楚是否冲突。
5.5 转速上不去、PWM满格但电机无力
转速上不去,先检查占空比和CCR寄存器的关系。Counter Period设成99时,占空比范围是0到99,写入100会超出范围,相当于溢出,行为不可控。如果你在代码里看到__HAL_TIM_SET_COMPARE的第二个参数写的是100,而Period配置是99,这就是问题所在,改成99或者把Period改成999再匹配写入。
还有一个被我忽略过的原因,是PWM和方向引脚的配合。有些代码里在每次调速时把IN1/IN2都设置了一遍,但如果GPIO配置成开漏且没有外部上拉,输出高电平实际是悬空状态,方向引脚拉不起来,逻辑电压不明确,电机自然无力。GPIO模式记得用推挽输出,尤其是STM32驱动外部芯片的方向脚,开漏模式不是不能用,但必须加正确电平的上拉电阻,没事别给自己挖这个坑。
电机电源电压不足也会导致无力,特别是电池电量快耗尽时,转速明显下降。TT马达额定电压6V,用两节锂电池变成7.4V虽然转速会高一点,但长时间超压会加速齿轮箱磨损和电刷损耗,不建议长期用超过额定电压太多的电源。
最后再分享一点实操心得
TT马达虽然便宜,但它把电机控制里的核心知识串起来了:PWM调速、H桥换向、编码器测速、PID闭环、差速控制,每一步都能迁移到更复杂的电机系统。我个人的建议是,拿到模块后先做最小化验证,一个电机、一块驱动板、一颗STM32最小系统板,把正反转和调速跑通,再往车架上装。直接裸奔调试比装到车架后带着一堆线去查问题容易得多。
调试节奏上,一次只改一个变量。先固定PWM频率,再调占空比;先开环测转速,再闭环调PID;先单轮跑直,再双轮差速。每次改动后用串口记录数据,有了数据才能判断到底是谁的问题。踩坑多了,你对电流、电压、PWM频率这些抽象概念会慢慢建立直觉,之后再做各种小车就会顺很多。