ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32驱动TT马达从零到实战:PWM调速与H桥原理及HAL库实现

2026/9/10 6:29:28 拓冰建站 浏览量
STM32驱动TT马达从零到实战:PWM调速与H桥原理及HAL库实现 很多刚接触STM32的朋友第一件事就是想让电机转起来。我见过太多人卡在这一步代码抄了一堆电机要么纹丝不动要么一通电就乱转转速完全不受控制。其实问题往往不在代码而在你根本没搞清楚TT马达是什么、驱动板怎么选、PWM到底在调什么。这篇东西就是把这些事一次讲透从硬件选型到HAL库代码一步步来目标就一个让你手里的TT马达听话说转就转、说停就停、说多快就多快。1. TT马达家族盘点先搞清楚你手里是哪一种TT马达这个名字在中文社区里流传极广但很多人其实不知道它到底是什么。简单说TT马达指的不是某个具体型号而是一类带减速齿轮箱的直流电机最典型的就是玩具车、智能小车底盘上那种圆柱形黑色电机。因为名字里有TT两个字新手容易误以为它是某个厂商的标准件实际上它更像一种约定俗成的规格。1.1 空转马达和减速马达的区别市面上常见的TT马达分两大类。空转马达也叫普通直流电机轴上不带齿轮箱。它的特点是转速极高、扭矩极小。三五伏电压一上去转速轻松上万转但用手一捏轴就停——根本没有带负载的能力。这种电机直接接轮子小车基本是打滑空转的命根本跑不动。减速马达也就是TT马达的主流形态在电机输出轴和外部轴之间加了一组行星减速齿轮。减速比常见的有1:48、1:90、1:120等。减速的本质是用转速换扭矩。以最常见的1:48减速比为例电机内部转子转48圈输出轴才转1圈但输出轴上的扭矩大约是转子轴的几十倍。装上轮子之后小车不仅跑得动爬坡都有劲。所以如果你做智能小车不要图便宜买空转电机一定要选带减速箱的。最简单的判断方法就是看电机尾部或者侧面有没有凸起的齿轮箱结构再就是用手拧一下输出轴能明显感觉到有阻力、有齿轮咬合的顿挫感那就是减速电机。1.2 带编码器和不带编码器的版本TT马达还有带编码器和无编码器两个版本。不带编码器的版本就是纯电机两根线正负极通电转断电停。便宜也够用但转速控制全靠开环PWM负载一变化转速就飘。带编码器的版本在电机尾部多了一块小PCB板通常是霍尔编码器配合电机内部转轴上的一圈磁钢来感知转速。常见的是11线制6根信号线加5根电源地或者20线制TT马达常见的是霍尔编码器加AB相输出。编码器能告诉你电机实际转速是多少是后续做PID闭环调速的基础做小车的必备配置。我的建议很直接新手第一次做用不带编码器的版本先把开环调速跑通搞明白PWM是怎么回事。第二次做小车直接换带编码器的感受一下闭环和开环的差距有多大。1.3 关键参数怎么看买TT马达的时候标称参数通常包括空载转速、额定电压、空载电流、堵转电流和扭矩。给你一组典型值作为参考参数典型值说明额定电压6V3-6V可工作超过6V电机发热和磨损明显增加空载转速200-300转/分钟1:48减速后实际带载后会更低空载电流100-300mA很小的电流堵转电流1A以上甚至2-3A这是选驱动必须看的数字输出扭矩约0.5-1.5 kg·cm1:48足够驱动小型小车对新手来说最关键的参数就两个堵转电流和你打算用的供电电压。堵转电流决定驱动芯片的最小电流能力供电电压决定你要配什么电源。2. 驱动板选型为什么STM32不能直接驱动TT马达新手常有个天真想法STM32有那么多引脚直接接电机不就行了我当初也这么干过结果MOS管或者芯片直接冒烟。STM32的GPIO为什么推不动TT马达物理上其实就两个原因。2.1 电流和电压都不满足STM32的GPIO在输出高电平时最大的灌电流/拉电流一般在20mA左右F103系列数据手册里典型值而这还是极限值长期工作建议在10mA以内。TT马达空载就要200mA堵转可能2A差了整整两个数量级。电压方面STM32逻辑电平是3.3VTT马达额定6V就算勉强能转转速和扭矩也远远不够。如果说得好听点GPIO是信号脚不是功率脚。它负责传递决定不负责出力气。2.2 主流驱动方案对比目前驱动TT马达的方案主要有L298N、TB6612FNG、DRV8833和分立MOS管H桥这几种我直接给一张实测感受对比驱动方案价格压降最大电流特点适合场景L298N便宜模块约10元大2V左右2A实际建议1A以下模块大、发热大、老设计大电机大电流实验用TB6612FNG中模块约15元小0.5V左右1.2A/每通道体积小、效率高、带过流保护STM32小车的绝配DRV8833中高小1.5A峰值2A内部集成H桥、逻辑简单有硬件PWM和电流限制需求的场景分立MOS管H桥低零件几块钱可做极低看MOS管选型需要自己画板、调试难度大电调、进阶练手如果你做的是普通STM32小车我首推TB6612FNG。原因不复杂L298N是上世纪8、90年代的老设计内部是双极型三极管压降大、发热大效率差模块又重又占地方DRV8833也很优秀模块上引脚相对密一些对新手焊板不太友好TB6612FNG是MOSFET H桥结构压降只有零点几伏体积小引脚间距正常逻辑电压支持2.7V到5.5VSTM32的3.3V逻辑电平可以直连供电范围也宽大部分情况下不需要额外电平转换。2.3 以TB6612FNG为例的接线规划TB6612FNG模块的引脚不多核心逻辑是两路独立的H桥。每个通道有3个控制脚IN1、IN2、PWM。另外有STBY待机、VM电机电源、VCC逻辑电源、GND。对新手推荐这样接线模块引脚接STM32说明VM电池6V正极或5V升压电机电源VCCSTM32的3.3V逻辑电源GNDSTM32的GND必须共地STBY3.3V或STM32引脚拉高低电平进入待机PWMA定时器PWM引脚比如PA0TIM2_CH1控制A通道速度AIN1/AIN2普通GPIO比如PA1/PA2控制A通道方向PWMB定时器PWM引脚比如PA3TIM2_CH4控制B通道速度BIN1/BIN2普通GPIO比如PA4/PA5控制B通道方向这样接好之后STM32只负责发逻辑信号和PWM功率全由外部电源提供。3. 控制原理PWM调速和H桥换向一张逻辑表搞定很多人拿到代码第一时间就往上烧烧完发现不对再回头翻原理效率很低。我建议先花十分钟搞懂控制原理后面调起错来会快很多。3.1 H桥的本质两个闸刀决定电流方向电机的转向取决于电流从正极流向负极还是反过来。H桥的四个开关实际是MOS管按对角线开合就能切换电流方向。你可以把H桥想象成一座连接电机两端的桥左边两个开关管一组、右边两个开关管一组。左上和右下导通电流从左往右流过电机电机正转。右上和左下导通电流从右往左流过电机电机反转。全部断开电机处于滑行状态靠惯性转。全部导通两个接线端短路电机制动立刻停下但电流很大实际是同一个桥臂上下管不能同时导通否则直通短路。3.2 TB6612FNG的逻辑真值表TB6612FNG把H桥的控制简化成两个逻辑输入和一个PWM使能输入。下面这张表一定要记住这是后面写代码的根基STBYPWMIN1IN2电机状态LXXX待机/停止HHHL正转全速HHLH反转全速HLHH短接制动HLLL短接制动HPWMHL正转PWM调速HPWMLH反转PWM调速注意后面两行这才是PWM调速的正确用法让IN1和IN2确定方向PWM引脚输出脉冲序列控制平均电压。3.3 PWM调速的实质PWM脉宽调制的本质是通过快速切换通断来改变平均电压。假设你给电机一个6V方波占空比50%那就相当于平均电压约3V。电机因为机械惯性和绕组电感的作用对这种高速通断有平滑效果转速大致和占空比成正比。但大致两个字很重要。TT马达在占空比低于一定值时比如5%-10%以下因为摩擦和齿槽效应实际是转不动的。这就产生了死区。做调速系统时建议把最低有效占空比设在这个阈值之上。根据我实际测试1:48减速比TT马达PWM频率10kHz到20kHz占空比低于8%左右基本不动低于15%左右转速极不稳定、明显一顿一顿的。3.4 PWM频率怎么选有个问题很多人忽略PWM的频率该设成多少。设太低比如几十Hz电机会一抖一抖地动噪音巨大设太高比如几十kHz则可能超出驱动芯片和电机的响应能力导致实际有效电压反而下降。对于TT马达和TB6612FNG我实测下来10kHz到20kHz是最舒服的区间电机运行平稳、噪音小、响应也快。在STM32上这个频率很容易配出来比如给定时器一个72MHz的时钟预分频71重装载值100就得到72MHz/(711)/(1001)10kHz的PWM。4. 基于HAL库的STM32代码实现从初始化到调速前面把原理讲清楚了现在来写代码。下面这套是基于STM32F103C8T6和HAL库CubeMX生成工程的示例目标是让一个TT马达跑起来并通过按键或串口指令调节转速。注意代码的重点不光是能跑而是讲清楚每一行在干什么。4.1 CubeMX工程配置要点新建CubeMX工程选定芯片STM32F103C8T6。时钟配置参照典型外部8MHz晶振倍频到72MHz主频。在Pinout标签页里做如下配置把PA0配为TIM2_CH1PWM输出通道PA1和PA2配为GPIO_Output。如果还要驱动第二个电机把PA3配为TIM2_CH4或者另开一个定时器PA4和PA5配为GPIO_Output。配置定时器TIM2时钟源选Internal ClockChannel1勾选PWM Generation CH1预分频PSC71计数模式Up自动重装载ARR99。这样PWM频率是72MHz/72/10010kHz分辩率100级0-99对应0%-99%占空比。这样生成工程代码后再在main.c中加入电机控制逻辑。4.2 电机初始化函数CubeMX生成代码已经做好了GPIO和定时器的初始化但PWM默认是没有开启输出的。需要启动PWM通道// main.c 的 main 函数内 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4); // 初始状态电机停止 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_4, 0);这里有个小细节HAL_GPIO_WritePin设置的是电机方向引脚IN1/IN2必须在开始输出PWM之前就把方向设置好。如果先输出PWM再突然切换方向相当于给电机一个瞬间的换向冲击电流会非常大。4.3 电机控制核心函数封装为了让你写主逻辑时不用天天操作寄存器我把电机控制封装成几个简单函数// 电机A正转speed范围0-100 void MotorA_Forward(uint8_t speed) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // AIN1 1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); // AIN2 0 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, speed); } // 电机A反转speed范围0-100 void MotorA_Backward(uint8_t speed) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // AIN1 0 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); // AIN2 1 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, speed); } // 电机A自由滑行停止输出但不断电逻辑 void MotorA_Stop(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0); } // 电机A急停短接制动 void MotorA_Brake(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0); }注意__HAL_TIM_SET_COMPARE这个宏它做一件事更新定时器通道的捕获比较寄存器值也就是改变占空比。它不需要类似HAL_TIM_OC_StartChannel这样的启动操作只要PWM已经开始输出随时改这个值都会实时生效。同样的方式对第二个电机写一套MotorB_Forward / MotorB_Backward用TIM2_CH4和PA4/PA5。4.4 主循环跑一个调速演示下面给一个简单的演示逻辑上电后电机A正转每次PA8引脚接一个触摸按键或普通按键被按下时转速增加10%超过90%就反转并且从10%开始。while (1) { if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) GPIO_PIN_SET) { HAL_Delay(20); // 消抖 speed 10; if (speed 90) { speed 10; direction !direction; } if (direction) { MotorA_Forward(speed); } else { MotorA_Backward(speed); } // 把当前速度打印到串口 printf(speed: %d%%\r\n, speed); while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) GPIO_PIN_SET); // 等待按键释放 } HAL_Delay(10); }逻辑很简单但有几个实战细节PWM占空比范围是0-99我建议把上限设为90而不是99。占空比99%意味着几乎全速一旦负载突变电流可能是最大的驱动芯片发热非常快。留点余量对系统和寿命都好。每次改速前先改方向引脚再改PWM这个顺序别倒过来。消抖延时20ms足够别加太长否则按键体验很糟糕。4.5 挂载到串口做调速如果你的板子上有串口USART1可以做一个非常方便的调试接口直接发数字改变转速比按键感受直观得多。串口初始化CubeMX直接生成然后在中断回调里收到整数就设置对应速度uint8_t rx_data; uint8_t recv_buf[4]; uint8_t recv_len 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_data 0 rx_data 9) { recv_buf[recv_len] rx_data; if (recv_len 3) { recv_len 0; uint8_t spd atoi((char *)recv_buf); spd spd 100 ? 100 : spd; MotorA_Forward(spd); } } HAL_UART_Receive_IT(huart1, rx_data, 1); } }串口调试这个方法强烈推荐特别是当你后面要调PID的时候靠串口在线改参数比重新烧录代码效率高十倍。5. 实测踩坑总结发烫、怪鸣叫、不转的排查清单下面这部分是真正的价值所在。我把自己实测过程中翻过的车、排查过的坑列出来按现象-原因-解决的结构组织你遇到问题时可以直接对着查。5.1 电机通电后不转但驱动板发烫这个现象最常见而且原因通常不在代码。原因排查顺序先摸一下驱动芯片。如果芯片烫得厉害C级烫手基本就是输出短路或过流。对TT马达和TB6612FNG最常见的是一上电电机就处于制动状态——IN1和IN2都是高两个下管全部导通电机两端被短路电流全灌在芯片里。这时候如果代码初始化里先把这个状态设好再开PWM可能就正常但如果芯片过热损坏了就得换芯片。再测一下电机本身。用镊子直接短接电机两端用手拧电机轴如果明显很费力说明电机内部绕组正常如果不费力或者完全拧不动可能是减速齿轮卡死或者电机绕组开路。5.2 电机疯狂发热且转速上不去这种情况通常是PWM频率设置过低比如你用的是1kHz或者几百Hz。低频PWM下电机线圈每次都要从零建立电流瞬间电流尖峰大且失控导致噪音大、发热严重。也有可能是占空比太高。占空比90%以上电机长期处于接近堵转的大电流状态发热自然会快。解决方法是把PWM频率提升到10kHz以上同时预留一段上限不跑满。5.3 电机发出尖锐的鸣叫声这基本上是PWM频率落在了人耳可听的范围内20Hz到20kHz。如果你用的是10kHz其实人耳还是能听到轻微的嘶嘶声正常。但如果声音非常尖锐刺耳先检查是不是PWM频率在几百到几kHz之间。把PWM频率往上调到15kHz到20kHz人耳能察觉到的音量会明显下降。如果还是吵检查一下是不是占空比在某个值附近出现了电机振荡——这种情况占空比30%-50%区域比较容易出现可以试着提高PWM频率或者给电机线圈并联一个0.1uF的瓷片电容。5.4 驱动芯片和STM32必须共地这个坑我当年踩得痛不欲生。驱动板的VM接电池正极STM32的电源来自USB供电两个系统各自有自己的地没接在一起。结果就是STM32的3.3V逻辑信号到了驱动板这里参考地电位不一致完全无法产生有效的逻辑电平。现象就是电机时转时不转、乱转串口打印正常换了好几个代码实验都一样。最后的解决办法很简单——把STM32的GND和驱动板的GND用一根线连起来。只要共地一切问题迎刃而解。注意共地是信号完整性里最基础也最容易被忽略的一点。任何时候信号收发两边必须共用一个参考地否则逻辑电平全是在猜。5.5 电机转一下停一下转速稳定不下来这个症状最常见的原因是供电跟不上。USB口供电能力一般只有500mA而TT马达空载启动瞬间电流可能就超过500mA稍微带点负载更是直接拉垮电压。电压一掉STM32直接复位电机就停电压恢复又启动形成一个循环。解决办法是给系统一个独立的电源两节18650电池约7.4V或者单节18650加升压模块到6V让VM独立供电STM32用USB或者LDO单独供电。值得注意的是所有电源的地最终还是要在某一点汇合共地但电源本体要分开。5.6 占空比设了很大但速度就是上不去如果你用的是带编码器的TT马达占空比很高但转速还是低先检查一下供电电压是不是已经被压到很低了。拿万用表量一下VM引脚对GND的电压空载和带载各测一次。如果带载时电压下降超过1V说明电源内阻太大换电源。另一个容易被忽视的原因是你占空比设置的数值和ARR重装载值不匹配。比如ARR199但你在代码里写的是__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 99)那占空比只有50%不是你以为是99%。5.7 所有接线正确但电机还是不动最后终极checklist按顺序过一遍驱动板VM有没有供电用万用表量VM-GND应该是4V以上。STM32的GND和驱动板的GND有没有连接STBY引脚是不是拉高了STBY低电平整个模块待机怎么控制都没反应。IN1/IN2逻辑是否在任意一时刻只有一边是高如果同高就是制动PWM通道是否已经在HAL_TIM_PWM_Start启动之后并且用__HAL_TIM_SET_COMPARE设置了非零占空比这一套下来99%的问题都能自动排查掉。剩下的如果是硬件损坏那也只能换模块了。6. 进阶玩法编码器测速与两轮差速小车如果你把前面内容全部搞定了恭喜你已经掌握了开环控制的全部要点。但如果只是这样你的小车在平地跑还没问题一遇到坡度、地毯速度就会明显下降。这时候就需要闭环。6.1 编码器TT马达接线与定时器编码器模式带编码器的TT马达在电机尾部有一个小PCB上面通常有4个引脚也有的做成6个VCC3.3V或5V、GND、A相、B相。A、B是两路相位差90度的脉冲输出。关于转速的推算编码器输出脉冲数 电机内部转轴每圈的脉冲数。以常见的霍尔编码器如测速码盘20格为例内部轴每转一圈输出20个脉冲因为减速比1:48输出轴每转一圈内部轴转48圈总脉冲 20*48 960个脉冲。在STM32上你可以把A相接定时器的CH1B相接CH2然后把定时器配置为Encoder Mode编码器模式。定时器会自动对A、B两路脉冲进行正交解码计数方向自动跟随电机转向。用定时器的计数器值变化就能算出转速。CubeMX里配置Encoder Mode步骤选择定时器比如TIM3Combined Channels选Encoder Mode配置好计数频率和自动重装载值比如ARR设为960刚好对应输出轴一圈。6.2 速度闭环的一个极简PID实现有了实际转速反馈就能做PID。代码可以极简到这个程度// 一个极简的增量式PID单位时间调用一次 int PID_Update(int target_speed, int current_speed) { static int integral 0; static int last_error 0; int error target_speed - current_speed; integral error; if (integral 100) integral 100; if (integral -100) integral -100; int derivative error - last_error; last_error error; // Kp、Ki、Kd需要根据你的小车实测调整 return (int)(Kp * error Ki * integral Kd * derivative); }PID三个参数怎么调我个人经验是先给Ki和Kd设0只调Kp从1开始慢慢往上加直到轮子产生震荡前后来回抖再回退一点然后加一点Ki消除稳态误差最后加一点Kd抑制超调。这个流程对于TT马达小车来说好用得很。6.3 两轮差速的控制思路差速转向本质上就是让左右轮转速不同。右轮比左轮快车身向左转左快右慢向右转。这个在代码里很自然——给左右电机分别一个PID回路各自跟踪各自的目标速度。前进时左右目标速度相同原地旋转时一个正转一个反转转弯时一侧减速另一侧保持。至于目标速度怎么协调是上层路径规划的事但底层每个电机的速度执行就是靠你在前面学的PWM加PID一点点建起来的。最后再分享一点我这几年做电机控制的感受电机控制最大的误区不是不会写代码而是没建立起信号-功率之间的分层思维。STM32负责逻辑驱动芯片负责功率电源负责能量三者各司其职、缺一不可。当你掌握了用GPIO发方向命令、用PWM调节平均电压、用驱动芯片放大驱动能力这个思路之后换成无刷电机、步进电机甚至伺服电机底层逻辑都是相通的。TT马达只是你入门的第一个台阶但把这个台阶踩实了后面路就好走得多。