资讯中心

STM32小车开环控制实战:从PWM原理到电机驱动调试

📅 2026/8/28 14:44:11
STM32小车开环控制实战:从PWM原理到电机驱动调试
1. 项目概述从零到一让小车动起来搞嵌入式开发的朋友尤其是刚接触STM32的新手总想找个能动手、有反馈的项目来练手。一个能满地跑的小车无疑是那个最吸引人的“Hello World”。它不像点个LED灯那么简单也不像复杂的物联网项目那样让人望而生畏。它综合了GPIO控制、定时器、PWM、电机驱动等多个基础且核心的知识点是检验你单片机学习成果的绝佳试金石。今天要聊的就是这个经典项目的入门基石——小车的开环控制。所谓“开环”你可以把它想象成给小车下达一个“盲指令”比如“左轮以50%速度正转右轮以50%速度正转持续3秒”。在这3秒里程序只管执行命令不会去管小车是不是真的在直行、有没有因为地面不平而跑偏、速度是不是稳定。这是一种最简单、最直接的控制方式也是理解更高级的“闭环控制”比如PID调速、循迹之前必须迈出的第一步。这个项目适合谁呢如果你已经掌握了STM32的基本开发环境搭建比如用Keil或STM32CubeIDE会使用库函数或HAL库点灯、操作按键那么你就可以开始尝试了。通过这个项目你将亲手实现用STM32的PWM信号来控制直流电机的转速和方向并组装出一个能听你命令行事的简易小车底盘。整个过程充满了硬件连接的乐趣和代码调试的挑战最终看到小车动起来的那一刻成就感是实实在在的。2. 核心硬件选型与电路设计思路要让一个小车动起来我们需要一个“大脑”、一个“心脏”、还有“四肢”。在这个项目中STM32单片机是大脑负责发出指令电机驱动模块是心脏负责提供动力直流电机和轮子就是四肢负责执行运动。2.1 主控芯片STM32家族的选择STM32系列型号繁多对于小车项目我们不需要性能过剩的旗舰型号一个资源适中、性价比高的型号就足够了。STM32F103C8T6常说的“蓝色药丸”或“最小系统板”是经久不衰的选择。它拥有足够的GPIO、多个定时器用于产生PWM、USART、SPI、I2C等外设价格亲民资料和社区支持极其丰富。如果你的小车未来打算扩展更多传感器如超声波避障、红外循迹它的资源也完全够用。注意在采购开发板时确认板载的晶振、复位电路、调试接口通常是SWD是否完整。一块好的最小系统板能省去很多硬件上的麻烦。2.2 动力核心电机与驱动模块详解直流电机是玩具小车、模型车中最常见的动力源。我们通常需要两个电机分别驱动左右轮这样可以实现前进、后退、转向。单独用单片机的IO口是无法直接驱动电机的因为电机启动和运行需要较大的电流几百mA到几A而单片机IO口的驱动能力通常只有几十mA。因此我们必须使用电机驱动模块。最常用的是基于L298N或TB6612FNG芯片的模块。L298N驱动模块经典、皮实、功率大。它可以驱动两个直流电机支持正反转和PWM调速。但它有个明显的缺点发热量较大效率相对较低需要加装散热片。TB6612FNG驱动模块更现代的选择。效率高、发热小、外围电路简单同样支持双路直流电机控制和PWM调速。对于小型小车项目我个人更推荐TB6612它的体积更小巧控制逻辑也更清晰。这里以TB6612FNG为例说明其与STM32的连接逻辑电源部分模块需要两个电源输入。VM接电机电源例如使用两节18650锂电池串联约7.4V-8.4V。VCC接逻辑电源通常接STM32的3.3V或5V为芯片内部逻辑供电。控制信号部分每个电机需要3个控制信号AIN1, AIN2, PWMA 对应电机ABIN1, BIN2, PWMB 对应电机B。AIN1/AIN2控制电机的转向。设置为 (1,0) 正转(0,1) 反转(0,0)或(1,1) 刹车。PWMA/PWMB接收来自STM32的PWM信号用于控制电机的速度。PWM占空比越高电机平均电压越高转速越快。输出部分A01/A02,B01/B02分别连接到两个直流电机的两根线上。2.3 车体与电源方案车体可以选择现成的亚克力板小车底盘套件通常包含底盘、两个带减速箱的直流电机、轮子、万向轮或从动轮、螺丝包等。减速电机扭矩更大更适合小车起步和爬坡。电源是整个系统的稳定基石。常见的方案有独立双电源最稳妥的方案。用一块锂电池组如7.4V单独给电机驱动模块VM供电。再用另一块电池如3.7V锂电池或稳压模块如AMS1117-3.3降压后给STM32和驱动模块的逻辑部分VCC供电。这样可以有效避免电机启停时的大电流波动对单片机造成干扰导致复位或死机。单电源稳压隔离使用一块电池如7.4V通过一个DC-DC降压模块如LM2596得到稳定的5V或3.3V供给单片机和驱动模块逻辑部分。电机驱动仍直接接电池。这种方式需要确保降压模块的电流余量足够且输入输出端最好加上滤波电容。在电路设计上一个必须养成的习惯是在电机电源输入端并联一个大容值的电解电容如470uF-1000uF/16V。这个电容相当于一个“小水池”可以在电机突然启动需要大电流时快速补充平滑电源电压的波动对保护电池和稳定系统至关重要。3. 软件架构与PWM驱动原理硬件连接好比搭好了骨架软件则是赋予小车灵魂的关键。开环控制的软件核心非常简单初始化外设然后在主循环或定时器中断里根据我们预设的动作序列设置电机控制引脚的电平和PWM占空比。3.1 开发环境与工程配置无论你使用标准外设库Standard Peripheral Library、HAL库Hardware Abstraction Layer还是LL库Low-Layer原理都是相通的。我以目前ST主推的STM32CubeIDE基于HAL库为例因为它集成了图形化配置工具CubeMX能极大简化外设初始化过程。创建工程在CubeIDE中新建一个STM32项目选择你的具体芯片型号如STM32F103C8Tx。时钟配置在Clock Configuration标签页配置系统时钟源通常用外部晶振HSE并将系统时钟SYSCLK设置到芯片允许的最高频率对于F103C8T6通常是72MHz。更高的主频意味着可以产生更高精度的PWM。GPIO配置找到并启用控制电机方向的引脚例如PA0, PA1作为AIN1, AIN2PA2, PA3作为BIN1, BIN2。将它们设置为GPIO_Output模式初始输出电平设为低。定时器配置PWM核心这是最关键的一步。我们需要使用一个通用定时器如TIM2, TIM3, TIM4的PWM输出功能。在Pinout Configuration视图的Timers下选择一个TIMx。将某个通道如TIM2的Channel1设置为PWM Generation CHx模式。在Parameter Settings标签页配置定时器Prescaler预分频器PSC决定定时器时钟的频率。定时器时钟 系统时钟 / (PSC 1)。例如系统时钟72MHzPSC设为71则定时器时钟为1MHz。Counter Period自动重装载值ARR这个值决定了PWM的频率。PWM频率 定时器时钟 / (ARR 1)。例如定时器时钟1MHzARR设为999则PWM频率为1MHz / 1000 1kHz。对于直流电机1kHz到10kHz都是常用范围频率太低电机会有啸叫太高则驱动模块可能响应不了。配置好后对应的引脚如PA0会自动被配置为复用推挽输出。生成代码后CubeIDE会自动生成这些外设的初始化代码MX_GPIO_Init,MX_TIM2_Init等。3.2 PWM生成与速度控制代码实现PWM脉冲宽度调制是控制模拟量的数字手段。对于电机我们通过调节一个周期固定、但高电平时间脉宽可变的方波信号来等效地输出一个可变的平均电压从而控制转速。在HAL库中控制PWM占空比非常方便。占空比 (脉冲宽度 / 周期) * 100%。在代码中脉冲宽度对应捕获/比较寄存器CCR的值周期对应自动重装载寄存器ARR的值。// 在main.c的合适位置如初始化后 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 启动TIM2的通道1输出PWM // 设置PWM占空比为50% // 假设ARR设置为999那么CCR设置为500则高电平时间为500个时钟周期占空比500/100050% __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500); // 改变占空比实现调速 // 设置占空比为30% __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 300);对于电机方向控制就是简单的GPIO操作// 假设AIN1接PA4 AIN2接PA5 // 电机正转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 电机反转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 电机刹车停止 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 或者同时置低取决于驱动芯片逻辑3.3 运动函数封装与基础动作库为了让代码更清晰、可复用我们应该将基本的运动操作封装成函数。这是从“能跑”到“代码优雅”的重要一步。// motor_control.h #ifndef __MOTOR_CONTROL_H #define __MOTOR_CONTROL_H #include main.h // 定义电机编号和方向 typedef enum { MOTOR_LEFT 0, MOTOR_RIGHT } Motor_ID; typedef enum { DIR_FORWARD 0, DIR_BACKWARD, DIR_BRAKE } Motor_Dir; // 函数声明 void Motor_Init(void); void Motor_Set_Speed(Motor_ID id, uint16_t speed); // speed: 0-1000 (对应CCR值) void Motor_Set_Direction(Motor_ID id, Motor_Dir dir); void Car_Stop(void); void Car_Run(int16_t left_speed, int16_t right_speed); // 速度可正可负正为前进 void Car_Spin(int16_t speed); // 速度为正则右旋为负则左旋 #endif// motor_control.c #include motor_control.h // 假设PWM和GPIO定义 extern TIM_HandleTypeDef htim2; // 左轮PWM通道1 extern TIM_HandleTypeDef htim3; // 右轮PWM通道1 #define LEFT_PWM_CHANNEL TIM_CHANNEL_1 #define RIGHT_PWM_CHANNEL TIM_CHANNEL_1 #define LEFT_DIR_PIN1 GPIO_PIN_4 #define LEFT_DIR_PIN2 GPIO_PIN_5 #define RIGHT_DIR_PIN1 GPIO_PIN_6 #define RIGHT_DIR_PIN2 GPIO_PIN_7 #define MOTOR_GPIO_PORT GPIOA void Motor_Init(void) { HAL_TIM_PWM_Start(htim2, LEFT_PWM_CHANNEL); HAL_TIM_PWM_Start(htim3, RIGHT_PWM_CHANNEL); Car_Stop(); // 初始化时停止所有电机 } void Motor_Set_Speed(Motor_ID id, uint16_t speed) { // 限制速度值在ARR范围内假设ARR999 if(speed 999) speed 999; if(id MOTOR_LEFT) { __HAL_TIM_SET_COMPARE(htim2, LEFT_PWM_CHANNEL, speed); } else { __HAL_TIM_SET_COMPARE(htim3, RIGHT_PWM_CHANNEL, speed); } } void Motor_Set_Direction(Motor_ID id, Motor_Dir dir) { GPIO_PinState pin1_state, pin2_state; uint16_t left_pin1 LEFT_DIR_PIN1, left_pin2 LEFT_DIR_PIN2; uint16_t right_pin1 RIGHT_DIR_PIN1, right_pin2 RIGHT_DIR_PIN2; switch(dir) { case DIR_FORWARD: pin1_state GPIO_PIN_SET; pin2_state GPIO_PIN_RESET; break; case DIR_BACKWARD: pin1_state GPIO_PIN_RESET; pin2_state GPIO_PIN_SET; break; case DIR_BRAKE: pin1_state GPIO_PIN_SET; // 或都置低根据驱动芯片手册 pin2_state GPIO_PIN_SET; break; default: return; } if(id MOTOR_LEFT) { HAL_GPIO_WritePin(MOTOR_GPIO_PORT, left_pin1, pin1_state); HAL_GPIO_WritePin(MOTOR_GPIO_PORT, left_pin2, pin2_state); } else { HAL_GPIO_WritePin(MOTOR_GPIO_PORT, right_pin1, pin1_state); HAL_GPIO_WritePin(MOTOR_GPIO_PORT, right_pin2, pin2_state); } } void Car_Stop(void) { Motor_Set_Speed(MOTOR_LEFT, 0); Motor_Set_Speed(MOTOR_RIGHT, 0); Motor_Set_Direction(MOTOR_LEFT, DIR_BRAKE); Motor_Set_Direction(MOTOR_RIGHT, DIR_BRAKE); } void Car_Run(int16_t left_speed, int16_t right_speed) { // 处理速度符号决定方向 Motor_Set_Direction(MOTOR_LEFT, (left_speed 0) ? DIR_FORWARD : DIR_BACKWARD); Motor_Set_Direction(MOTOR_RIGHT, (right_speed 0) ? DIR_FORWARD : DIR_BACKWARD); // 取绝对值作为PWM值 Motor_Set_Speed(MOTOR_LEFT, (uint16_t)abs(left_speed)); Motor_Set_Speed(MOTOR_RIGHT, (uint16_t)abs(right_speed)); } void Car_Spin(int16_t speed) { // 原地旋转左右轮速度大小相等方向相反 Car_Run(speed, -speed); }有了这个基础库你在主函数中控制小车就变得非常直观int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_TIM3_Init(); Motor_Init(); while (1) { // 小车前进速度70% Car_Run(700, 700); HAL_Delay(2000); // 小车右转差速转弯左轮快右轮慢 Car_Run(600, 300); HAL_Delay(1000); // 小车原地左旋 Car_Spin(500); HAL_Delay(500); // 小车停止 Car_Stop(); HAL_Delay(1000); } }4. 开环控制实践动作编排与调试技巧开环控制就像编排一段舞蹈你需要预先设计好每个动作的细节速度、方向、持续时间然后让小车严格按剧本执行。它的效果完全依赖于你的预设参数和硬件的一致性。4.1 基础动作序列设计与实现一个完整的开环演示程序可以设计成让小车走一个简单的几何图形比如正方形。走正方形的关键在于精确的“前进一段”和“旋转90度”的组合。void Car_Run_Square(uint16_t side_speed, uint16_t side_time, uint16_t turn_speed, uint16_t turn_time) { for(int i 0; i 4; i) { // 1. 直行一条边 Car_Run(side_speed, side_speed); HAL_Delay(side_time); // 直行时间决定边长 // 2. 停止一下避免惯性影响可选 Car_Stop(); HAL_Delay(100); // 3. 原地旋转90度右转 Car_Spin(turn_speed); // 正数为右旋 HAL_Delay(turn_time); // 旋转时间需要实验校准 // 4. 停止一下 Car_Stop(); HAL_Delay(100); } }在主循环中调用Car_Run_Square(600, 1500, 400, 650);。这里的参数side_speed600,turn_speed400,side_time1500ms是直行时间turn_time650ms是旋转90度所需的时间。这些参数没有理论值必须通过实际调试获得。4.2 参数调试让小车走“直线”和“准直角”开环控制最大的挑战就是不确定性。两个电机即使型号相同其特性、减速箱齿轮间隙、轮子摩擦等都有微小差异这会导致“明明给了相同的PWM值小车却跑不直”。调试“走直线”将小车放在长直线上标记起点。让小车以中等速度如PWM500运行一段较长时间如5秒。观察终点偏离直线的方向。如果向右偏说明左轮实际速度比右轮快。微调代码给两个轮子设置一个微小的速度补偿。例如将Car_Run(500, 500)改为Car_Run(490, 510)即让偏快的那侧轮子速度略降偏慢的略升。重复测试直到小车能基本沿直线运动。这个补偿值需要针对你的小车单独测定。调试“转直角”让小车执行原地旋转命令Car_Spin(400)。用手机秒表记录小车旋转360度所需的时间T360。计算旋转90度所需的理论时间T90 T360 / 4。在实际代码中使用这个T90作为turn_time。由于启动和停止的惯性实际角度可能不准。可能需要略微调整turn_time或尝试在旋转开始和结束时加入短暂的加速/减速过程例如先给一个短时间的更高PWM值启动再降到稳定值。实操心得调试时最好将调试参数速度、时间定义为宏或变量放在代码开头。这样修改起来非常方便不用在函数里到处找数字。同时在关键动作前后通过串口打印日志如“Start moving forward”, “Stop”能极大帮助理解程序执行流程和问题定位。4.3 进阶实现匀加速与匀减速直接给电机施加一个固定的PWM小车会猛地启动和停止动作很生硬也容易打滑。我们可以通过软件实现简单的速度斜坡Ramp让启动和停止更平滑。void Car_Speed_Ramp(Motor_ID id, uint16_t start_speed, uint16_t end_speed, uint16_t duration_ms) { int16_t step (end_speed - start_speed) / (duration_ms / 10); // 每10ms变化一次 uint16_t current_speed start_speed; for(uint16_t t 0; t duration_ms; t 10) { Motor_Set_Speed(id, current_speed); current_speed step; HAL_Delay(10); // 阻塞延时实际项目建议用定时器非阻塞方式 } Motor_Set_Speed(id, end_speed); // 确保达到目标速度 } // 平滑启动前进 void Car_Smooth_Start(uint16_t target_speed, uint16_t ramp_time) { Car_Speed_Ramp(MOTOR_LEFT, 0, target_speed, ramp_time); Car_Speed_Ramp(MOTOR_RIGHT, 0, target_speed, ramp_time); }这个函数让速度在duration_ms时间内从start_speed线性变化到end_speed。虽然用了简单的阻塞延时HAL_Delay在要求不高的场合够用。在更复杂的系统中你应该使用定时器中断来管理这些渐变过程避免阻塞主循环。5. 常见问题排查与硬件优化实录做项目就是不断遇到问题和解决问题的过程。下面是我在多次小车项目中踩过的坑和总结的排查思路希望能帮你快速定位问题。5.1 上电无反应或电机不转这是最让人头疼的起步问题。请按照以下清单像查电路图一样逐一排查电源是首要怀疑对象测量电压用万用表测量STM32的3.3V/5V引脚、电机驱动模块的VM和VCC引脚电压是否正常。电池电量是否充足观察指示灯开发板、驱动模块上的电源指示灯是否亮起单独供电测试尝试断开电机驱动与单片机的连接单独给驱动模块供电用手短接驱动模块的输入信号如将AIN1接VCCAIN2接GND看电机是否转动。这能快速区分是驱动/电机问题还是单片机控制问题。信号通路排查检查接线这是最容易被忽视的确保杜邦线连接牢固没有虚焊、错位。特别是电机的两根线接反了只会影响转向但PWM线或方向线接错了电机可能完全不动。测量控制信号用万用表电压档或示波器如果有测量STM32输出给驱动模块的PWM和方向控制引脚。在程序运行时方向引脚的电平应该随你的代码变化PWM引脚应该有电压跳动平均电压与占空比相关。如果信号没有输出回头检查代码初始化是否正确引脚配置是否冲突。软件逻辑检查初始化顺序确保先完成所有外设GPIO, TIM的初始化再启动PWM输出 (HAL_TIM_PWM_Start)。代码是否执行在控制电机的地方加一个LED翻转或串口打印语句确认程序确实运行到了那里。参数是否超限检查你设置的PWM占空比值CCR是否超过了定时器的周期值ARR。5.2 电机抖动、啸叫或转速不稳定这种现象通常与PWM信号有关。PWM频率不合适频率太低如几十Hz电机会发出明显的“咯咯”声或脉冲声转动不连续。这是因为电枢绕组在频繁地通断。频率太高超过驱动芯片或电机响应能力可能导致驱动芯片发热严重电机效率下降甚至不转。解决方案调整定时器的预分频器PSC和重装载值ARR将PWM频率设置在1kHz到5kHz之间是大多数直流电机和L298N/TB6612的舒适区。用示波器测量一下输出波形频率是否与计算值相符。电源功率不足电机启动瞬间电流很大如果电池容量小或内阻大会导致电压瞬间被拉低造成单片机复位或驱动芯片工作异常表现为电机抽搐。解决方案在电机电源输入端并联一个大电容如470uF以上作为能量缓冲。使用动力型锂电池如18650而非普通的碱性电池或容量小的锂电。软件干扰如果主循环中有很多其他任务或长时间的阻塞延时可能会影响PWM信号的稳定输出。解决方案确保电机控制相关的操作如改变速度尽量高效。复杂的动作序列或速度规划最好放在定时器中断服务函数中处理。5.3 小车跑不直或动作重复性差这是开环系统的固有缺陷但可以通过以下方法改善电机配对与筛选在购买电机时如果条件允许可以简单测试一下空载转速。给两个电机相同的电压用手机测速软件通过拍摄转轴标记大致对比一下转速挑选两个接近的使用。机械对称性确保两个轮子安装牢固没有松动。车体重心尽量居中避免左右重量不均。软件校准与补偿如前文所述这是最有效的手段。通过实验测定一组属于你自己小车的“校准参数”左/右轮速度微调系数旋转时间补偿值并保存在代码中。供电电压监测电池电压会随着放电而下降。同样的PWM占空比在电池满电和亏电时对应的电机端电压不同速度也就不同。对于要求稍高的开环应用可以增加一个ADC通道监测电池电压根据电压动态微调PWM占空比进行简单的电压补偿。5.4 硬件优化建议电源隔离与滤波强烈建议为单片机和数字电路部分驱动模块逻辑电源使用独立的线性稳压电源如LM7805或低压差稳压器LDO与电机电源分离。在单片机的电源入口处增加一个10uF的钽电容和一个0.1uF的陶瓷电容并联用于滤除高频噪声。电机驱动模块的VM和GND之间除了大容量电解电容再并联一个0.1uF的陶瓷电容滤除高频干扰。信号保护在STM32的电机控制信号输出线上可以串联一个100欧姆左右的电阻限制瞬间电流对GPIO口有一定保护作用。如果导线较长考虑使用双绞线或屏蔽线连接电机减少电磁干扰。添加基础状态指示在开发板上多接几个LED用于指示电源状态、程序运行状态、当前运动模式等调试时会非常方便。一定要把串口调试功能用起来。通过printf重定向到串口实时打印速度、方向、传感器数据等信息是发现逻辑错误最强大的工具。开环控制是小车项目的第一步它让你掌握了最基础的驱动力法。它的所有不确定性也正是引入闭环控制如编码器测速PID、陀螺仪平衡的动机。当你调通了开环小车并为其不听话的“个性”而烦恼时恭喜你你已经具备了向更智能、更自主的机器人世界进发的坚实基础。接下来你可以尝试给它装上编码器实现精确的转速闭环或者装上红外传感器让它学会循迹避障。每一步问题的解决都是对你嵌入式开发能力最实在的锤炼。