
1. 项目缘起从“能动”到“可控”的必经之路很多朋友在入门嵌入式开发特别是想动手做一辆智能小车时第一个拦路虎往往不是复杂的传感器也不是玄妙的算法而是最基础的“让轮子转起来”。你可能已经用STM32点亮了LED也学会了按键控制但当你兴冲冲地把电机驱动模块接上写一个简单的GPIO_SetBits让引脚输出高电平时却发现电机要么纹丝不动要么就“嗡”的一声全速狂转完全不听使唤。这感觉就像你拿到了汽车的钥匙却只会拧到启动和熄火两个档位中间的油门、刹车一概失灵车子自然没法平稳上路。这就是我们今天要解决的核心问题如何让STM32输出的信号从简单的“开/关”数字量变成可以平滑调节的“油门”信号。而PWM脉冲宽度调制技术正是实现这一转变的钥匙。对于循迹小车而言精准的电机控制是底盘运动的基础。无论是直线行驶的稳定性还是转弯时的灵活性都依赖于两个电机转速的精确、独立调节。如果电机只能全速或停止那小车的运动将非常生硬循迹算法再精妙也无从施展。所以这个系列教程的第一篇我们就从最根本的“动力控制”开始。我将带你一步步理解PWM是什么它在STM32上如何产生又如何通过一个经典的电机驱动模块比如TB6612或L298N去实际控制一个直流电机的转速和方向。我会假设你已经有了一些STM32的基础比如会用Keil或STM32CubeIDE建工程知道如何配置GPIO但如果你对PWM还感到陌生完全不用担心我们会从原理讲起直到你能亲手让轮子按照你想要的节奏转动起来。2. PWM控制电机的核心原理不只是“通断”而是“平均”在深入代码之前我们必须先搞清楚PWM到底是如何控制电机转速的。这是一个非常经典且巧妙的思想理解了它你就能举一反三应用到LED调光、舵机控制、甚至开关电源等众多领域。2.1 什么是PWM信号PWM全称脉冲宽度调制。你可以把它想象成一个周期性的方波信号。这个信号只有两种状态高电平比如3.3V和低电平0V。描述一个PWM波最关键的是两个参数频率和占空比。频率指这个方波在一秒钟内重复的次数单位是赫兹Hz。比如1kHz的频率意味着每秒钟有1000个完整的方波周期。占空比指在一个周期内高电平持续时间所占整个周期的百分比。这是PWM的灵魂。占空比50%意味着高电平和低电平各占一半时间占空比80%意味着高电平时间占大头。那么一个平均电压为1.65V的直流信号和一个频率足够高、占空比为50%的3.3V PWM信号对于电机来说效果为什么是近似的呢这里就引出了“惯性”和“平均”的概念。2.2 电机为何“感受”到的是平均电压直流电机本质上是一个把电能转化为机械能的装置。它的转速忽略负载等因素的理想情况下大致与施加在其两端的平均电压成正比。当你给电机两端加上一个稳定的3.3V电压它就会以一个固定的转速旋转。现在我们换一种方式我们以极高的频率比如20kHz远高于电机机械响应的频率快速地接通和断开这个3.3V的电源。在接通高电平时电机获得能量开始加速在断开低电平时电机依靠惯性继续旋转同时因内部反电动势和摩擦而减速。由于开关频率非常高电机转子的机械惯性使得它根本来不及跟随每一次电压的突变做出明显的转速变化。它的转速响应会“滞后”于电信号最终表现出来的就是一个相对平滑的转速而这个转速对应的“等效电压”正好是电源电压乘以占空比。举个例子我们用3.3V的电源产生一个占空比为30%的PWM波。对于电机而言它“感受”到的就是一个持续存在的、大小为3.3V * 30% 0.99V的等效电压。因此它的转速大约会等于直接给它加0.99V直流电时的转速。注意这里说“平均”或“等效”是为了便于理解。在实际的电机驱动电路中我们通常不会直接把MCU的PWM引脚接到电机上因为MCU引脚的驱动电流太小通常只有几十mA而电机启动和堵转时电流可能高达数百mA甚至数安培会直接烧毁MCU引脚。所以必须通过电机驱动模块如TB6612这个“中间人”来放大电流。此时MCU的PWM信号是作为驱动模块的“控制信号”驱动模块内部的功率MOS管会按照这个PWM的节奏来导通或关断供给电机的大电流电源比如7.2V的电池。所以最终决定电机两端电压的是电池电压和PWM占空比的乘积。2.3 关键参数选择频率与占空比分辨率理解了原理我们就要为实际应用选择合适的参数。PWM频率的选择不能太低如果频率太低比如几十Hz你会明显听到电机发出“滋滋”的啸叫声这是线圈在通断电流时产生的振动。更重要的是转速波动会很大控制起来不平稳。不能太高频率太高会对驱动模块的开关管MOSFET提出更高要求开关损耗会增加。对于STM32的通用定时器频率也受系统时钟和分频系数限制。常用范围对于小型直流减速电机频率在1kHz 到 20kHz之间是一个很好的选择。我个人的经验是5kHz到10kHz是一个非常折中的点既能保证控制平稳无噪声又对硬件要求不高。本教程后续我们将以10kHz为例进行配置。占空比分辨率的选择 占空比不是连续可调的。STM32的定时器通过一个计数器和一个比较寄存器CCR来产生PWM。计数器从0累加到某个值ARR自动重装载值然后清零重新开始。当计数器的值小于CCR时输出高电平大于等于CCR时输出低电平。因此占空比 CCR / (ARR 1)。ARR的值决定了分辨率。如果ARR设置为999那么CCR可以从0取到999总共1000个不同的值占空比分辨率就是1/1000 0.1%。这意味着你可以将转速精细地分成1000个档位。分辨率与频率的权衡在系统时钟如72MHz固定的情况下PWM频率 系统时钟 / ((PSC1) * (ARR1))。为了提高分辨率增大ARR在时钟和预分频PSC不变的情况下频率就会降低。所以我们需要在“足够高的频率”和“足够细的分辨率”之间取得平衡。对于小车电机控制分辨率不需要特别高。因为电机本身有死区、齿轮间隙且受电池电压波动、负载变化影响很大过于精细的占空比调节意义不大。通常8位分辨率256级或10位分辨率1024级已经完全够用。我们将采用ARR999实现1000级约10位的分辨率配合10kHz的频率。3. 硬件连接与驱动模块选型给信号装上“功率放大器”前面提到MCU引脚不能直接驱动电机。因此一个可靠的电机驱动模块是必不可少的。这里我们以目前最主流的TB6612FNG和经典的L298N为例进行对比和讲解。3.1 TB6612FNG vs L298N如何选择特性TB6612FNGL298N驱动原理MOSFET H桥双H桥集成电路晶体管效率高MOSFET导通电阻小发热少较低压降和发热较大最大电流每通道1.2A连续3.2A峰值每通道2A需加散热片控制逻辑简单两个控制引脚一个PWM引脚稍复杂使能端两个逻辑输入待机功能有STBY引脚无需外部控制供电尺寸与集成度小SOP24贴片封装需自己设计或购买模块大多引脚直插封装模块集成度高价格相对较低相对较高选择建议对于新手和大多数循迹小车项目强烈推荐TB6612FNG模块。它效率高、发热小、接线简单PWMA, AIN1, AIN2 即可控制一个电机外围电路通常已经集成在模块上使用起来非常方便。L298N更“古老”和经典但功耗和发热是硬伤除非手头已有否则不建议新项目选用。3.2 TB6612FNG模块接线详解一个典型的TB6612模块可以独立控制两个直流电机。我们以控制电机A为例说明其与STM32和电机的连接方式。请务必对照你的模块原理图或引脚说明。电源部分VM电机电源接你的动力电源比如7.4V锂电池组的正极。这是电机的“燃料”。VCC逻辑电源接3.3V或5V。这个电压需要与STM32的IO电平匹配。如果模块支持宽电压如2.5V-5.5V接3.3V最安全。它为芯片内部的逻辑电路供电。GND将STM32的GND、电机电源的负极、TB6612模块的GND全部连接在一起共地是保证信号正常通信的基础很多奇怪的问题都源于此。控制信号部分连接STM32PWMA接STM32的一个具有PWM输出功能的引脚如TIM2_CH1 (PA0)。这个引脚将输出我们配置好的PWM波形控制电机的“油门”大小。AIN1和AIN2接STM32的两个普通GPIO输出引脚如PA1, PA2。这两个引脚的电平组合决定电机的旋转方向。AIN11, AIN20- 电机正转AIN10, AIN21- 电机反转AIN10, AIN20或AIN11, AIN21- 电机刹车停止STBY待机接STM32的一个GPIO输出引脚或直接接高电平VCC。当STBY为高电平时芯片正常工作为低电平时所有输出关闭电机自由停止。为了方便我们可以直接将其接到VCC3.3V上让芯片一直处于工作状态。如果想实现低功耗待机再用GPIO控制。电机输出部分A01 和 A02接直流电机的两根线。注意这里不分正负电机的转向由AIN1/AIN2控制。如果发现转向与预期相反交换这两根线或者交换AIN1/AIN2的逻辑即可。接线检查清单[ ] STM32、TB6612逻辑电源VCC、电机电源GND共地。[ ] PWMA连接到STM32的PWM引脚。[ ] AIN1/AIN2连接到STM32的GPIO输出引脚。[ ] STBY接高电平VCC。[ ] VM接电池正极注意电压不要超过模块最大值。[ ] 电机接在A01和A02上。4. STM32定时器配置与PWM生成让芯片“心跳”起来硬件连接好后我们进入软件部分。在STM32中PWM功能是由通用定时器TIM2, TIM3, TIM4等或高级定时器TIM1, TIM8的“输出比较”模式产生的。这里我们以最常用的通用定时器TIM2的通道1PA0为例使用STM32CubeMX进行图形化配置。如果你习惯直接操作寄存器理解了CubeMX的配置项写寄存器代码也会非常清晰。4.1 使用STM32CubeMX进行配置选择定时器打开CubeMX在Pinout Configuration视图的左侧找到Timers展开并点击TIM2。选择时钟源在Clock Source中选择Internal Clock内部时钟。配置参数切换到Parameter Settings选项卡。Prescaler (PSC - 预分频器)这个值决定了定时器计数器的时钟频率。我们的系统时钟SYSCLK是72MHz。为了得到10kHz的PWM频率我们需要计算。公式定时器时钟 SYSCLK / (PSC 1)。我们先设定一个目标让定时器计数器时钟为720kHz。那么PSC (72,000,000 / 720,000) - 1 99。填入99。Counter Mode (计数模式)选择Up向上计数。Counter Period (ARR - 自动重装载值)这就是之前提到的决定分辨率的参数。计数器从0数到这个值后溢出并产生更新事件。PWM频率 定时器时钟 / (ARR 1)。我们想要10kHz定时器时钟是720kHz所以ARR (720,000 / 10,000) - 1 71。等等这个分辨率太低了只有72级。还记得我们的权衡吗为了更高的分辨率我们愿意适当降低频率。我们改用ARR 719试试。此时频率 720,000 / (7191) 1kHz。这个频率对于电机有点偏低。我们再调整一下让PSC71则定时器时钟 72M / (711) 1MHz。再令ARR99则频率 1M / (991) 10kHz分辨率是100级。这个分辨率对于初步测试可以接受。但为了更好的效果我们采用一个更优的方案PSC71ARR999。此时定时器时钟1MHz频率1M / 1000 1kHz不对频率应该是 1M / (9991) 1kHz。看来我算错了。我们的目标是10kHz。重新设定目标频率Fpwm 10kHz 分辨率Res 1000。根据公式Fpwm Ftim_ck / ((PSC1)*(ARR1)) 且ARR1 Res。所以(PSC1) Ftim_ck / (Fpwm * Res) 72,000,000 / (10,000 * 1000) 7.2。这显然不是一个整数。因此我们需要在频率和分辨率之间妥协。一个非常常见的配置是PSC71ARR899。这样定时器时钟1MHz频率1M / 900 ≈ 1111Hz ≈ 1.11kHz分辨率900级。这个频率对于小型电机勉强可用但可能听到一点噪音。另一个更常见的配置是PSC719ARR999。此时定时器时钟72M / 720 100kHz频率100k / 1000 100Hz。这个频率太低了电机抖动会非常严重。经过权衡对于电机控制一个经典的配置是PSC71ARR999。此时PWM频率 72,000,000 / ((711)(9991)) 72,000,000 / (721000) 1000Hz (1kHz)。分辨率是1000级。1kHz的频率对于很多小型直流减速电机来说是完全可以接受的噪音和波动都在可控范围内同时获得了很高的控制精度。我们就采用这个配置。填入PSC71,ARR999。auto-reload preload使能Enable这样可以在运行时平滑地更新ARR值。配置PWM输出通道在TIM2配置页的下方找到Channel1选择PWM Generation CH1。这时左侧的引脚图上的PA0会被自动标记为TIM2_CH1。在下方出现的PWM Generation Channel 1配置中Mode选择PWM mode 1计数器小于CCR时输出有效电平大于等于时输出无效电平。Pulse即初始的CCR值先设为0。Output Compare preload选择Enable这样可以在运行时平滑地更新CCR值即占空比。CH Polarity通道极性选择High表示有效电平为高电平。这个设置需要与你的驱动模块逻辑匹配。对于TB6612PWMA输入高电平有效所以这里选High是正确的。生成代码配置好系统时钟、工程名、IDE等选项后点击GENERATE CODE。4.2 理解生成的代码与关键函数CubeMX会生成完整的初始化代码。在tim.c文件中你会找到MX_TIM2_Init()函数里面完成了我们所有的配置。更重要的是它调用了HAL_TIM_PWM_Init()和HAL_TIM_PWM_ConfigChannel()。要让PWM信号实际从引脚输出还需要在main.c中启动PWM输出/* 在初始化部分启动TIM2的通道1输出PWM */ HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);现在PA0引脚就会输出一个占空比为0%因为初始Pulse0的PWM波。要改变占空比就是改变CCR寄存器的值。HAL库提供了非常方便的宏/* 设置占空比为50% (ARR999, 所以50%对应500) */ __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500); /* 或者使用HAL库函数效果相同 */ TIM2-CCR1 750; // 设置占空比为75%一个重要的细节__HAL_TIM_SET_COMPARE这个宏实际上就是直接给TIMx-CCRy寄存器赋值。在PWM Mode 1且PolarityHigh的情况下CCR的值就是高电平的计数周期数。占空比 CCR / (ARR1)。5. 整合控制方向、速度与刹车有了PWM控制“油门”我们还需要GPIO来控制“方向”。结合TB6612的真值表我们可以编写一个完整的电机控制函数。首先定义好连接AIN1和AIN2的GPIO引脚。假设我们使用PA1和PA2。在main.c的私有变量定义区或头文件中我们可以定义一个结构体来抽象一个电机typedef struct { TIM_HandleTypeDef *htim; // 使用的定时器句柄 uint32_t channel; // PWM通道 GPIO_TypeDef *in1_port; // AIN1端口 uint16_t in1_pin; // AIN1引脚 GPIO_TypeDef *in2_port; // AIN2端口 uint16_t in2_pin; // AIN2引脚 } Motor_t;然后初始化这个结构体Motor_t motor_left { .htim htim2, .channel TIM_CHANNEL_1, .in1_port GPIOA, .in1_pin GPIO_PIN_1, .in2_port GPIOA, .in2_pin GPIO_PIN_2, };接下来编写核心的控制函数/** * brief 设置电机速度和方向 * param motor: 电机结构体指针 * param speed: 速度值范围[-1000, 1000]。负值代表反转。 * 内部会映射到PWM的CCR值CCR范围[0, ARR]。 * 我们假设ARR999所以满幅是1000。 * retval None */ void Motor_SetSpeed(Motor_t *motor, int16_t speed) { uint16_t pwm_val 0; // 限制速度范围 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); pwm_val (uint16_t)speed; // 速度值直接作为CCR } 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); pwm_val (uint16_t)(-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_RESET); pwm_val 0; // 也可以采用“滑行停止”IN1和IN2都置0但PWM输出高阻或低。 // TB6612在AIN1AIN20时为刹车模式停止较快。 // 另一种是“滑行”AIN1AIN21此时电机两端短路惯性滑行停止。 // 这里采用刹车模式。 } // 更新PWM占空比 __HAL_TIM_SET_COMPARE(motor-htim, motor-channel, pwm_val); }这个函数将速度控制统一了起来。传入正数电机正转传入负数电机反转数值的绝对值大小代表速度的快慢。这样在高级控制如PID中只需要计算出一个速度值调用此函数即可。刹车与滑行的区别刹车Brake将电机的两个输入端短接通过驱动芯片内部的下管此时电机线圈形成回路转动时产生的反电动势会在回路中产生电流从而产生一个制动力矩使电机快速停止。对应TB6612的AIN10, AIN20。滑行Coast将电机与驱动电路完全断开高阻态电机依靠惯性自由旋转直到停止。对应TB6612的AIN11, AIN21实际上对于TB6612这个状态输出也是高阻但有些资料也称之为刹车具体看芯片手册。在代码中你可以增加一个Motor_Coast函数将两个输入引脚都置高并将PWM占空比设为0。6. 调试与实测让轮子听话地转起来代码写好了但实际效果如何还需要上电调试。这是一个从理论到实践的关键步骤也会遇到一些典型问题。6.1 基础功能测试在main函数的初始化部分while(1)之前确保已经启动了PWMHAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);然后可以在while(1)循环中或者通过按键、串口指令来测试电机。简单的测试程序// 测试1电机以50%速度正转5秒停止2秒再以30%速度反转5秒 Motor_SetSpeed(motor_left, 500); // 50%速度正转 HAL_Delay(5000); Motor_SetSpeed(motor_left, 0); // 刹车停止 HAL_Delay(2000); Motor_SetSpeed(motor_left, -300); // 30%速度反转 HAL_Delay(5000); Motor_SetSpeed(motor_left, 0); // 刹车停止 while(1); // 停在这里观察现象电机是否按照指令正转、反转、停止转速是否平滑在低速比如speed100时电机是平稳转动还是抖动仔细听电机是否有明显的“滋滋”声这是PWM频率过低的表现。如果噪音很大可以尝试回到CubeMX在频率和分辨率之间重新权衡适当提高频率比如增大PSC减小ARR牺牲一些分辨率。6.2 常见问题排查踩坑实录即使按照教程操作你也可能会遇到一些问题。下面是我在多次项目中总结的排查清单问题1电机完全不转也没有声音。检查电源用万用表测量TB6612的VM和GND之间是否有电压电池有电吗。测量VCC引脚是否有3.3V或5V。检查STBY引脚是否接到了高电平VCC如果悬空芯片处于待机状态。检查控制信号用逻辑分析仪或示波器查看STM32的PWMA引脚是否有PWM波形输出如果没有检查代码中是否调用了HAL_TIM_PWM_Start。用万用表或调试器查看AIN1和AIN2引脚的电平是否按照你的代码设置变化了正转时应为1和0。检查接线电机线是否牢固接在A01和A02上尝试交换电机两根线。问题2电机抖动严重或只在某个方向轻微震动。PWM频率可能过低1kHz对于某些电机可能偏低尝试提高到5kHz或10kHz。修改CubeMX中TIM的PSC和ARR值。记住公式Fpwm F_ck / ((PSC1)*(ARR1))。电源功率不足电机启动电流很大如果电池电量不足或电源线太细会导致电压被拉低驱动芯片工作不正常。尝试单独给驱动模块VM接一个动力电源如稳压电源与给单片机供电的电源共地。占空比设置过低有些电机有启动电压阈值占空比太小比如低于10%可能无法克服静摩擦力启动表现为抖动。尝试给一个较大的占空比如30%看能否正常启动。问题3一个电机转另一个不转或两个转速明显不一致。这是正常现象即使是同一型号的电机其内部的磁钢、绕组、齿轮间隙也存在微小差异导致在相同PWM占空比下转速不同。这就是为什么后续需要引入编码器和PID闭环控制来精确控制转速而不是依赖开环PWM。在开环阶段如果差异不大可以暂时忽略或者为每个电机单独设置一个速度校准系数。检查接线和配置确认两个电机对应的控制引脚PWMA/B, AIN1/AIN2, BIN1/BIN2没有接错或配置错。问题4电机驱动模块发热严重。检查是否短路电机线或电源线是否有短路负载是否过大小车是否被卡住电机是否在堵转堵转时电流极大会迅速发热。确保电机能自由旋转。散热TB6612效率较高但长时间大电流工作也需要散热。可以加装一个小散热片。6.3 进阶调试使用示波器观察波形如果你有示波器调试会直观很多。测量PWM引脚将探头接到STM32的PWMA引脚如PA0地线夹子接GND。你应该能看到一个标准的、频率和占空比可调的方波。调整代码中的CCR值观察占空比是否线性变化。测量电机两端电压将探头接到电机的A01和A02之间注意这是差分测量需要两个探头或差分探头或者分别测量对GND的电压再相减。你应该能看到一个幅值等于电池电压VM的PWM波。这直接验证了驱动模块是否正确地将控制信号放大。7. 从开环到闭环的思考PID控制的引子到目前为止我们实现的是开环控制。我们设定一个PWM占空比电机就以一个大致对应的转速旋转。但这个转速会随着电池电压下降、负载变化如上坡、电机自身特性差异而波动。对于要求精确的循迹小车特别是需要两个电机完全同步走直线的情况开环控制是远远不够的。这就引出了下一个核心主题闭环控制。我们需要一个传感器来实时测量电机的实际转速然后将这个测量值与我们的目标值进行比较根据偏差来动态调整PWM占空比使得实际转速紧紧跟随目标转速。这个传感器就是霍尔编码器或光电编码器而那个动态调整的算法最经典的就是PID控制器。在下一篇教程中我们将为电机加装编码器并深入浅出地讲解PID算法的原理和在STM32上的实现。届时你将能真正实现“指哪打哪”的精准电机控制为小车的精准运动打下坚实基础。现在你已经掌握了让STM32小车“动起来”的核心技能。不妨多尝试不同的PWM频率和占空比感受电机响应的差异。也可以尝试用两个定时器分别控制左右电机为后续的双电机差速转向做准备。实践出真知动手调试过程中遇到的问题和解决过程才是最有价值的经验。