STM32标准库PWM开环控制直流电机:从定时器配置到TB6612驱动实战

📅 2026/7/30 5:55:53
STM32标准库PWM开环控制直流电机:从定时器配置到TB6612驱动实战
1. 项目概述与核心思路最近在做一个智能小车底盘核心需求就是让两个直流电机能按照指定的速度转动。对于这种基础的速度控制开环PWM方案是性价比最高、实现最快的选择。所谓开环就是“我发我的指令电机转不转、转多快我不去实时测量和纠正”完全依赖于PWM信号的占空比来控制平均电压进而近似控制转速。这个项目基于STM32F103C8T6也就是常说的“蓝桥杯”或“最小系统板”使用标准外设库Standard Peripheral Library来驱动定时器产生PWM波再通过一个简单的电机驱动模块比如TB6612或L298N来驱动电机。整个过程不涉及编码器反馈纯粹是“发令”模式。为什么选这个方案首先对于很多对转速精度要求不高的场景比如玩具车、简单的传送带、风扇调速开环PWM完全够用成本低程序简单。其次STM32的定时器功能非常强大配置成PWM模式后硬件自动输出波形CPU几乎零开销。最后标准库虽然现在官方主推HAL/LL库但对于从51或AVR转过来的开发者或者学校教学、老项目维护标准库的寄存器级操作感更清晰理解起来更直接。这个项目就是带你走通从定时器配置、GPIO初始化到PWM输出、电机驱动的完整链路把理论变成能让轮子转起来的代码。2. 硬件平台与核心原理拆解2.1 硬件选型与连接硬件是整个项目的地基选错了或者接错了代码写得再漂亮也没用。主控芯片STM32F103C8T6。这是STM32家族里的“国民级”芯片72MHz主频64K Flash20K RAM完全足够。它拥有多个高级/通用定时器都支持PWM输出。电机驱动模块这里以TB6612FNG为例。相比经典的L298NTB6612效率更高、发热更小是更现代的选择。你需要理解它的几个关键引脚VM电机电源正极建议7-12V根据你的电机额定电压来。VCC逻辑电源正极接3.3V或5V与STM32共地。GND电源地电机电源和逻辑电源地必须接在一起。AIN1/AIN2, BIN1/BIN2两路电机的方向控制引脚。接STM32的普通GPIO。PWMA/PWMB两路电机的PWM速度控制引脚。接STM32定时器的PWM输出通道GPIO。AO1/AO2, BO1/BO2电机输出端接电机两极。STBY待机控制高电平有效使能通常直接接VCC。连接示意图以控制一个电机为例STM32的某个GPIO如PA0 - TB6612的AIN1STM32的某个GPIO如PA1 - TB6612的AIN2STM32的某个定时器通道引脚如PA2 TIM2_CH3 - TB6612的PWMATB6612的AO1和AO2- 直流电机的两个电极确保所有GNDSTM32、TB6612逻辑侧、电机电源连接在一起。注意电机是大电流负载务必为电机电源接VM单独供电并使用足够粗的导线。切勿试图从STM32的开发板取电给电机否则极易烧毁芯片或USB口。2.2 PWM控制电机速度的原理PWMPulse Width Modulation脉冲宽度调制的本质是用数字信号模拟模拟量。对于一个直流电机其转速近似与两端电压成正比。我们无法用单片机直接输出一个可变的直流电压但可以输出一个固定频率、可变占空比的方波。频率指PWM波每秒周期性变化的次数单位Hz。频率太高电机电感可能无法响应频率太低电机会抖动、噪音大。对于普通直流有刷电机通常选择5kHz到20kHz之间。本项目我们选用10kHz。占空比指一个周期内高电平时间占总周期的比例。占空比高电平时间/周期时间。占空比越大平均电压越高电机转速越快占空比为0%电机停止占空比为100%电机全速。开环控制的局限性电机负载变化、电源电压波动、电机个体差异都会导致“同样的占空比不同的实际转速”。比如小车爬坡时负载变大同样的PWM占空比下实际转速会下降。开环系统对此无能为力。这就是为什么对速度稳定性要求高的场合如无人机、平衡车必须使用带编码器反馈的闭环如PID控制。但开环胜在简单、可靠对于很多应用足够了。2.3 STM32定时器与PWM生成STM32的通用定时器如TIM2, TIM3, TIM4和高级定时器TIM1, TIM8都可以轻松生成PWM。其核心是利用了定时器的比较/捕获功能。工作流程简述定时器配置一个自动重装载值ARR决定PWM的周期频率。定时器配置一个通道的比较值CCRx对于通道x决定PWM的脉冲宽度占空比。定时器时钟启动计数器CNT从0开始向上计数。在计数过程中硬件不断比较CNT和CCRx的值。当CNTCCRx时PWM输出引脚被置为有效电平可配置高或低当CNTCCRx时输出无效电平。当CNT计数到ARR时产生溢出事件CNT清零开始下一个周期同时输出电平根据新的CCRx值进行切换。通过改变CCRx的值就能动态调整占空比而ARR一旦设定周期就固定了。所有这些都是由硬件自动完成的无需软件干预效率极高。3. 软件设计与标准库配置详解3.1 工程建立与库文件准备首先你需要一个完整的STM32标准库开发环境。通常包括MDK-ARM (Keil)或STM32CubeIDE后者需手动添加标准库文件。STM32F10x标准外设库从ST官网下载包含core_cm3.h,stm32f10x.h,system_stm32f10x.c以及所有外设的.c/.h文件。在工程中正确添加这些文件路径并在魔术棒Options for Target的C/C选项卡的Define中定义USE_STDPERIPH_DRIVER和STM32F10X_MD对于中等容量F103C8T6。3.2 定时器PWM模式配置步骤我们计划使用TIM3的通道2对应引脚PA7来产生PWM。以下是基于标准库的配置代码和详细解释。// tim_pwm.c #include stm32f10x.h #include tim_pwm.h /** * brief 初始化TIM3的通道2PA7为PWM输出模式 * param arr: 自动重装载值决定PWM周期 * param psc: 预分频系数用于调整定时器时钟 * retval None */ void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; /* 1. 开启时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3挂载在APB1总线开启时钟 /* 2. 配置GPIOA Pin7 为复用推挽输出 */ // PA7是TIM3_CH2的复用功能输出引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 3. 配置定时器时基单元 */ // 计算目标PWM频率PWM_Freq SystemCoreClock / ((psc1) * (arr1)) // 假设系统时钟72MHz欲得10kHz PWM可设psc71, arr99 // 则PWM_Freq 72,000,000 / (72 * 100) 10,000 Hz TIM_TimeBaseStructure.TIM_Period arr; // 自动重装载值ARR TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频系数PSC TIM_TimeBaseStructure.TIM_ClockDivision 0; // 时钟分频与输入捕获有关此处设为0 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); /* 4. 配置TIM3通道2为PWM模式 */ TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // 选择PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高电平有效 // TIM_OCPolarity_Low则为低电平有效根据你的驱动模块逻辑调整 TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比对应的比较值CCR2设为0电机不转 TIM_OC2Init(TIM3, TIM_OCInitStructure); // 初始化通道2 TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能通道2的预装载寄存器 /* 5. 使能ARR预装载和启动定时器 */ TIM_ARRPreloadConfig(TIM3, ENABLE); // 使能自动重装载预装载保证波形稳定 TIM_Cmd(TIM3, ENABLE); // 启动定时器TIM3 }关键参数解析TIM_Period (ARR)和TIM_Prescaler (PSC)共同决定PWM频率。定时器时钟 系统时钟 / (PSC1)。PWM频率 定时器时钟 / (ARR1)。ARR也决定了PWM分辨率的理论最大值例如ARR999则占空比可调级数为1000级。TIM_OCModePWM1和PWM2模式区别在于输出电平极性。在PWM1模式下当CNTCCRx时输出有效电平我们上面配置为高有效在PWM2模式下则相反。需要根据驱动模块的输入逻辑选择。TIM_OCPolarity输出极性。即使同一种PWM模式也可以再次通过此参数翻转有效电平。通常与PWM1/2模式配合达到最终想要的波形。TIM_Pulse这个值就是写入通道比较寄存器CCRx的初始值直接决定了初始占空比。实操心得配置GPIO时务必设置为GPIO_Mode_AF_PP复用推挽输出。如果错设为普通推挽输出虽然引脚也能高低变化但定时器硬件无法控制其输出PWM波形。这是新手常犯的错误。3.3 电机方向控制与速度设置函数有了PWM还需要控制方向。方向控制就是给TB6612的AIN1/AIN2高低电平。// motor.c #include stm32f10x.h #include motor.h // 假设AIN1PA0, AIN2PA1, PWMA来自TIM3_CH2 (PA7) #define MOTOR_AIN1_PIN GPIO_Pin_0 #define MOTOR_AIN1_PORT GPIOA #define MOTOR_AIN2_PIN GPIO_Pin_1 #define MOTOR_AIN2_PORT GPIOA /** * brief 电机GPIO初始化 */ void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin MOTOR_AIN1_PIN | MOTOR_AIN2_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出用于方向控制 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 初始化状态刹车或停止根据驱动模块逻辑定 GPIO_ResetBits(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN); } /** * brief 设置电机转动方向和速度 * param dir: 方向0停止1正转2反转 * param speed: 速度值范围0-ARR。例如ARR999则speed500代表约50%占空比。 */ void Motor_Set(int8_t dir, uint16_t speed) { // 1. 设置方向 switch(dir) { case 0: // 停止/刹车具体看驱动模块有的低电平刹车有的要特定组合 GPIO_ResetBits(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN); // 也可以将PWM占空比设为0来实现软停止 break; case 1: // 正转 GPIO_SetBits(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN); break; case 2: // 反转 GPIO_ResetBits(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN); GPIO_SetBits(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN); break; default: // 错误处理例如停止电机 GPIO_ResetBits(MOTOR_AIN1_PORT, MOTOR_AIN1_PIN); GPIO_ResetBits(MOTOR_AIN2_PORT, MOTOR_AIN2_PIN); speed 0; break; } // 2. 设置速度PWM占空比 // 确保速度值不超过ARR防止设置错误导致异常 if(speed TIM3-ARR) { speed TIM3-ARR; } TIM_SetCompare2(TIM3, speed); // 修改TIM3通道2的比较值CCR2 }代码逻辑解析Motor_GPIO_Init函数初始化控制方向的两个GPIO为普通输出模式。Motor_Set函数是核心控制接口。dir参数控制AIN1/AIN2的电平组合对应电机的正转、反转和停止注意TB6612的停止模式有“短路刹车”和“待机”之分具体看数据手册上述代码示例是一种常见接法。speed参数直接赋值给CCR2寄存器。TIM_SetCompare2是标准库提供的便捷函数用于设置指定定时器通道的比较值。注意事项在改变电机转向前一个好的实践是先将PWM占空比speed设置为0或一个很小的值短暂延时后再改变方向引脚最后再恢复速度。这可以避免电机在高速运行时突然反转产生的巨大反向电流冲击驱动芯片和电源俗称“换向打嗝”。3.4 主程序逻辑与调试主函数里我们完成初始化然后就可以自由控制电机了。// main.c #include stm32f10x.h #include tim_pwm.h #include motor.h #include delay.h // 需要一个简单的延时函数可以用SysTick实现 int main(void) { Delay_Init(); // 初始化延时函数72MHz下约1ms TIM3_PWM_Init(999, 71); // 初始化PWM频率约10kHz (72M/(711)/(9991)) Motor_GPIO_Init(); // 初始化电机控制GPIO while(1) { // 示例1电机正转加速过程 for(uint16_t i0; i1000; i10) { Motor_Set(1, i); // 正转速度从0加到1000ARR999所以最大到999 Delay_ms(20); // 每20ms增加一次速度 } Motor_Set(0, 0); // 停止 Delay_ms(1000); // 示例2电机反转匀速 Motor_Set(2, 700); // 反转70%占空比 Delay_ms(3000); Motor_Set(0, 0); // 停止 Delay_ms(1000); // 示例3点动控制 Motor_Set(1, 300); Delay_ms(100); Motor_Set(0, 0); Delay_ms(900); } }调试技巧先测波形再接电机在将PWM引脚连接到驱动模块前先用示波器或者一个LED串联电阻接到PWM输出引脚上。运行程序观察是否有频率和占空比符合预期的方波输出。这是排查硬件连接和软件配置问题的第一步。逻辑分析仪是神器如果没有示波器一个几十块的USB逻辑分析仪也可以很好地显示PWM波形和GPIO电平变化帮助你确认方向控制信号和PWM信号是否同步正确。分步测试先让电机单向低速转动确认没问题后再测试转向切换。逐步增加速度观察电机响应。4. 进阶话题与深度优化4.1 多路电机与定时器资源分配一个小车通常需要控制两个或四个电机。STM32F103C8T6有多个定时器每个定时器通常有4个通道。高效利用定时器资源是关键。方案一单定时器多通道。例如使用TIM2的四个通道CH1~CH4对应PA0, PA1, PA2, PA3分别控制两个电机的方向和PWM不对方向控制用普通GPIOPWM用定时器通道。所以更合理的做法是一个定时器的两个通道控制两个电机的PWM。比如TIM2_CH1PA0控制电机1的PWMTIM2_CH2PA1控制电机2的PWM。它们的ARR和PSC是共用的因此频率必然相同但CCRx可以独立设置占空比独立。方向控制则用另外的4个普通GPIO。方案二多定时器。如果两个电机需要完全独立、不同频率的PWM虽然不常见或者定时器通道不够用可以使用两个定时器如TIM2和TIM3。配置多通道PWM时只需在初始化函数中对需要的通道如TIM_OC1Init,TIM_OC2Init分别调用初始化函数并分别使能它们的预装载即可。4.2 PWM频率与分辨率权衡前面提到PWM频率 定时器时钟 / (ARR1)而PWM分辨率级数 ARR 1。在定时器时钟固定的情况下频率和分辨率是矛盾的。提高频率减小ARR分辨率就会下降提高分辨率增大ARR频率就会降低。对于直流有刷电机频率太低如几十Hz会听到明显的“滋滋”声电机可能抖动频率太高如几十kHz以上虽然听不到声音但驱动模块的开关损耗会增加且可能由于电机电感特性导致电流响应跟不上。5kHz-20kHz是一个常用的舒适区间。对于舵机舵机控制是特殊的PWM频率通常是50Hz周期20ms高电平脉宽在0.5ms到2.5ms之间变化。这需要极低频率和相对较高的分辨率需要单独计算ARR和PSC。计算示例系统时钟72MHz目标PWM频率10kHz则定时器时钟 / 频率 72M / 10k 7200。这个7200就是(PSC1)*(ARR1)的乘积。我们需要分配PSC和ARR。为了得到较高的分辨率我们让ARR尽量大。取PSC71则PSC172那么ARR1 7200/72 100所以ARR99。此时分辨率为100级。如果想提高分辨率到1000级在频率不变的情况下需要PSC1 7200/1000 7.2不是整数所以只能近似。取PSC6PSC17则ARR17200/7≈1028ARR1027实际频率约为72M/(7*1028)≈9.99kHz分辨率1028级可以接受。4.3 从开环到闭环的思想延伸虽然本项目是开环控制但理解其局限性是为了更好地迈向闭环。闭环控制的核心是反馈调节。反馈最常见的速度反馈元件是霍尔编码器或光电编码器安装在电机轴上。STM32的定时器通常带有编码器接口模式可以硬件读取编码器脉冲从而计算出电机实际转速。调节最经典的算法是PID比例-积分-微分。控制器比较“目标转速”和“实际转速”得到误差然后根据PID公式计算出一个新的PWM占空比输出给电机从而让实际转速紧紧跟随目标转速抵抗负载变化等干扰。开环项目是闭环控制的基础。当你成功让电机随PWM变化而转动后下一步就可以尝试接入一个编码器读取转速然后编写一个简单的P控制器PID中最基础的P项体验一下闭环控制的魔力。你会发现即使负载加重电机的转速也能基本保持稳定。5. 常见问题排查与实战心得在实际焊接、接线、写代码的过程中你会遇到各种各样的问题。下面是一些典型问题的排查清单。现象可能原因排查步骤与解决方案电机完全不转1. 电源问题2. 驱动模块未使能3. 信号逻辑错误1. 用万用表测量电机驱动模块VM和GND之间电压是否正常。2. 检查STBY引脚是否已接高电平使能。3. 用逻辑分析仪或示波器检查AIN1/AIN2和PWMA引脚是否有预期信号。确认PWM是否有输出方向引脚电平组合是否正确参考TB6612真值表。电机抖动、振动或噪音大1. PWM频率过低2. 电源功率不足3. 机械结构问题1. 尝试提高PWM频率到10kHz以上。避开音频范围20Hz-20kHz。2. 检查电源是否能提供电机堵转时所需的瞬时大电流。尝试单独给电机供电。3. 检查电机安装是否牢固联轴器是否同心。电机只朝一个方向转方向控制信号错误检查Motor_Set函数中正转和反转case下的GPIO电平设置是否正确。用逻辑分析仪确认切换方向时两个方向引脚的电平是否按预期翻转。改变PWM占空比速度变化不明显1. PWM频率不合适2. 电机本身特性3. 驱动模块问题1. 频率过高可能导致电机电感滤波效果过强平均电压变化不明显。尝试降低频率到1-5kHz试试。2. 有些电机有启动电压阈值低于该阈值不转。确保你的占空比变化范围覆盖了电机的有效工作区间。3. 更换一个电机或驱动模块测试。代码编译通过但下载后没反应1. 系统时钟未正确配置2. 初始化顺序问题3. 硬件复位或Boot引脚问题1. 标准库默认使用内部8MHz RC时钟HSI。如果你的计算基于72MHz需要在系统初始化SystemInit()后手动配置时钟树到72MHzPLL。或者你的所有计算都基于8MHz重新算一遍。2. 确保先开启外设时钟RCC_APBxPeriphClockCmd再配置该外设。3. 检查BOOT0和BOOT1引脚是否都接地从主Flash启动。下载程序后按一下复位键。使用高级定时器TIM1/TIM8无输出高级定时器需要额外使能主输出高级定时器TIM1, TIM8用于PWM输出时在启动定时器TIM_Cmd之后需要额外加一行代码TIM_CtrlPWMOutputs(TIM1, ENABLE);来使能PWM主输出。这是与通用定时器的主要区别。我的几点实战心得供电隔离是王道电机驱动电路和单片机控制电路尽量使用独立的电源或者至少用一个大功率的DCDC隔离模块。电机启停、堵转时产生的电压毛刺和电流冲击是单片机复位、程序跑飞甚至芯片损坏的罪魁祸首。在两个电源的GND之间用0欧电阻或磁珠单点连接。善用飞线测试在最终焊接或接插件之前用杜邦线连接所有线路并确保电机轴可以自由旋转悬空。在测试阶段避免让电机带重负载或堵转很容易烧东西。软件刹车与硬件刹车让电机快速停止除了设置PWM占空比为0相当于断开电源靠摩擦力自然停止更好的方法是使用驱动芯片的“短路刹车”功能。以TB6612为例将AIN1和AIN2同时置为高电平电机两端被短接会产生一个制动力矩实现快速刹车。这在需要急停的场景下非常有用。理解“死区时间”虽然本项目用不到但在控制H桥电路像TB6612内部就是两个半桥时如果控制同一桥臂上下两个开关管的PWM信号有瞬间同时导通的可能就会造成电源短路烧毁芯片。高级定时器TIM1/TIM8支持硬件插入“死区时间”确保一个开关管完全关闭后另一个才开启。这是驱动更复杂电机如无刷直流的基础。