1. 从零到一为什么选择开环控制作为单片机小车的第一步如果你刚拿到一块STM32开发板和一套小车底盘看着一堆电机、轮子和杜邦线脑子里第一个冒出来的想法多半是“怎么让它动起来”。很多教程会直接上PID闭环控制讲得天花乱坠但对于一个初学者来说这就像还没学会走路就想跑马拉松。我的建议是别急我们先从“开环控制”开始。这不仅是让小车动起来的起点更是你理解整个嵌入式控制系统底层逻辑的基石。开环控制说白了就是“我说什么你就做什么至于你做没做到位我不关心”。比如我让电机以50%的占空比转3秒它就转3秒至于这3秒里小车是不是因为地面不平、电池电压下降而跑偏了控制器不管。听起来很“傻”对吧但正是这种“傻”让它成为了入门的最佳选择。它剥离了复杂的反馈和纠错环节让你能集中精力解决几个核心问题如何用STM32的定时器产生PWM波驱动电机如何通过程序逻辑控制小车的“前进”、“后退”、“左转”、“右转”如何搭建一个最基础的硬件系统这个过程你会亲手打通从代码到物理运动的整个链路。当你第一次通过几行代码让轮子按照你的意愿转动时那种“掌控感”是无与伦比的。这比一上来就面对误差、积分、微分这些抽象概念要直观得多。开环控制是“骨架”先把骨架搭起来后续的PID闭环控制、传感器融合才是往上添加的“肌肉”和“神经”。跳过骨架直接堆肌肉系统是立不起来的。所以咱们今天就踏踏实实从给STM32小车搭建这个最基础的“开环骨架”开始。2. 硬件清单与核心电路连接别在第一步就“翻车”在写代码之前硬件连接是重中之重。很多新手在这里栽跟头不是电机不转就是单片机冒烟。我结合自己焊板子、调车的经验给你捋一个清晰且避坑的连接清单。2.1 核心部件清单主控STM32F103C8T6俗称“蓝桥杯”或“最小系统板”。它性价比高资源足够社区资料海量是学习的不二之选。电机驱动模块L298N或TB6612FNG。强烈推荐TB6612FNG原因有三效率高、发热小、外围电路简单。L298N是老将但需要外接散热片逻辑电压和驱动电压要处理好对新手不友好。TB6612是MOSFET桥更现代。直流减速电机带编码器的那种。虽然开环用不到编码器反馈但买带编码器的电机是为未来升级闭环预留了硬件接口避免重复投资。通常工作电压在3-6V注意与驱动模块匹配。电源这是最大的坑点。务必准备两路独立电源一路给STM32开发板供电5V Micro USB或3.3V引脚供电。一路给电机驱动模块供电根据电机额定电压选择比如4节5号电池约6V或2节18650锂电池约7.4V。严禁只用USB给整个系统供电USB的500mA电流带不动两个电机瞬间就会导致电压被拉低单片机不断复位现象就是程序跑飞、电机抽搐。小车底盘四轮或两轮差分驱动底盘。四轮的后两轮通常是万向轮靠前两轮差分驱动决定方向和速度。杜邦线公对公、母对母、公对母都需要用于连接。2.2 电路连接详解与避坑指南以TB6612FNG驱动模块为例连接STM32F103C8T6。一个TB6612可以驱动两个电机我们假设控制小车的左轮电机MOTOR A和右轮电机MOTOR B。注意接线前确保所有电源处于断开状态养成好习惯。电源连接关键VM引脚接电机驱动电源正极如电池盒的6V。这是电机的“力气”来源。VCC引脚接逻辑电源正极接STM32的3.3V或5V输出引脚。这是给驱动芯片内部逻辑电路供电的必须接。GND引脚共地共地共地将电池的负极、STM32的GND、TB6612的GND三者用杜邦线全部连接在一起。这是保证信号基准一致的基础否则控制信号会乱。电机输出A01和A02引脚分别接左轮电机的两根线。B01和B02引脚分别接右轮电机的两根线。如果电机转向与预期相反交换这两根线即可。STM32控制信号连接核心 TB6612每个电机需要3个控制信号AIN1/AIN2方向、PWMA速度PWM。我们将其连接到STM32的通用定时器如TIM2的通道上以实现硬件PWM输出。左轮电机AIN1- STM32PA0(TIM2_CH1)AIN2- STM32PA1(TIM2_CH2)PWMA- STM32PA2(TIM2_CH3)右轮电机BIN1- STM32PA6(TIM3_CH1)BIN2- STM32PA7(TIM3_CH2)PWMB- STM32PB0(TIM3_CH3)STBY待机引脚直接接VCC逻辑电源让驱动模块始终处于工作状态。这里为什么选择TIM2和TIM3因为STM32F103C8T6的通用定时器TIM2、TIM3、TIM4都支持4路PWM输出且引脚分布方便布线。你也可以根据自己板子的引脚定义灵活选择但原则是一个电机的方向和PWM信号尽量来自同一个定时器方便程序管理。2.3 上电前最后检查电压确认用万用表量一下VM和GND之间的电压是否在电机额定电压附近比如6V左右VCC和GND之间是否是3.3V或5V短路检查快速目视检查所有杜邦线头有没有金属部分裸露接触导致短路的风险。连接牢固度轻轻拉扯一下杜邦线确保都插紧了。接触不良是调试中最诡异的问题来源。硬件连接就像盖房子的地基这里扎实了后面的软件调试才能顺风顺水。3. STM32定时器与PWM配置让电机“听令行事”的核心硬件通了现在要让STM32发出正确的控制信号。开环控制的核心就是通过PWM脉冲宽度调制波来控制电机的平均电压从而控制转速。PWM的频率和占空比是两个关键参数。3.1 PWM频率的选择不是越高越好对于普通的直流减速电机PWM频率通常在1kHz到20kHz之间。频率太低如几十Hz你会听到电机发出刺耳的啸叫声这是脉冲间隔太长电机线圈在不断地“启动-停止”造成的对电机寿命和控制平滑度都不好。频率太高如上百kHz虽然电机声音会变得很安静人耳听不到但会对电机驱动模块的开关损耗提出更高要求有些低端驱动芯片可能响应不过来导致实际效果变差。经验值我通常选择5kHz到10kHz。这个区间对于TB6612和L298N都工作良好电机运行平稳噪音在可接受范围内。我们以10kHz为例进行配置。3.2 使用CubeMX进行图形化配置HAL库对于初学者我强烈推荐使用STM32CubeMX工具配合HAL库。它通过图形界面配置时钟和引脚自动生成初始化代码能避免大量底层寄存器操作的错误。时钟树配置确保系统时钟SYSCLK被正确设置为72MHzSTM32F103的最高主频。这是计算定时器计数值的基准。定时器配置以TIM2为例在Pinout视图找到PA0、PA1、PA2分别设置为TIM2_CH1、TIM2_CH2、TIM2_CH3。在左侧配置栏找到TIM2。Clock Source选择Internal Clock。Parameter Settings选项卡Prescaler预分频器PSC设置为71。定时器时钟 72MHz / (711) 1MHz。Counter ModeUp向上计数。Counter Period自动重装载值ARR设置为99。PWM频率 定时器时钟 / ((PSC1) * (ARR1)) 1MHz / (100) 10kHz。公式一定要理解Fpwm Fclk / ((PSC1) * (ARR1))。auto-reload preloadEnable使能自动重装载。切换到PWM Generation CHx对应通道子选项卡ModePWM mode 1。Pulse初始脉冲值先设为0。这个值就是后续控制占空比的关键范围是0到ARR即99。占空比 Pulse / (ARR1)。Fast ModeDisable。CH PolarityHigh。这意味着当计数器小于Pulse值时输出高电平。这个极性要和你的驱动模块逻辑匹配。TB6612一般是高电平有效。生成代码在Project Manager里设置好工程名、路径、IDE如MDK-ARM V5然后生成代码。3.3 在代码中操控PWM与方向CubeMX生成的代码已经完成了定时器和GPIO的初始化。我们只需要在main.c的用户代码区调用HAL库函数即可。// 在main函数初始化部分之后启动PWM输出 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 启动TIM2 CH1 (PA0, AIN1的PWM? 不这里注意) HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); // 启动TIM2 CH2 (PA1, AIN2的PWM? 不这里注意) HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); // 启动TIM2 CH3 (PA2, PWMA) // 同理启动TIM3的各个通道 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_3);这里有一个非常重要的细节我们连接时AIN1/AIN2、BIN1/BIN2接的是普通GPIO口虽然我们用了定时器通道引脚但可以配置为纯输出模式而PWMA/PWMB接的才是真正的PWM输出引脚。在CubeMX里PA0和PA1应该配置为GPIO_Output而不是TIM2_CH1/2。只有PA2PWMA需要配置为TIM2_CH3的PWM模式。所以正确的做法是PA0(AIN1),PA1(AIN2) 配置为GPIO Output。PA2(PWMA) 配置为TIM2_CH3的PWM Generation。在代码中通过HAL_GPIO_WritePin来控制PA0和PA1的高低电平组合决定电机方向通过__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, pulse)来改变PA2的PWM占空比控制电机速度。方向控制逻辑以左轮为例AIN1 (PA0)AIN2 (PA1)电机状态10正转01反转00刹车停止11刹车停止我们可以封装一个函数来控制一个电机// 控制左轮电机 void Left_Motor_Control(int16_t speed) // speed范围-100 ~ 100 { if(speed 0) { HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_SET); // 正转 HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, speed); // 假设ARR99 speed就是占空比 } else if(speed 0) { HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET); // 反转 HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, -speed); } else { HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET); // 刹车 HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, 0); } }对于右轮电机同理封装Right_Motor_Control函数操作BIN1、BIN2和TIM3_CH3。4. 基础运动函数封装与调试让小车“走”起来有了单个电机的控制函数我们就可以组合出小车的基本动作了。封装这些动作函数能让主程序逻辑变得非常清晰。4.1 运动函数封装// 小车运动函数封装 void Car_Stop(void) { Left_Motor_Control(0); Right_Motor_Control(0); } void Car_Forward(uint8_t speed) { Left_Motor_Control(speed); Right_Motor_Control(speed); } void Car_Backward(uint8_t speed) { Left_Motor_Control(-speed); Right_Motor_Control(-speed); } void Car_TurnLeft(uint8_t speed) // 原地左转左轮后退右轮前进 { Left_Motor_Control(-speed); Right_Motor_Control(speed); } void Car_TurnRight(uint8_t speed) // 原地右转 { Left_Motor_Control(speed); Right_Motor_Control(-speed); } void Car_SpinLeft(uint8_t speed) // 左轮不动或慢速右轮快速前进实现弧线左转 { Left_Motor_Control(speed / 3); // 比例可调 Right_Motor_Control(speed); } void Car_SpinRight(uint8_t speed) // 弧线右转 { Left_Motor_Control(speed); Right_Motor_Control(speed / 3); }4.2 上电调试与常见问题排查将代码编译下载到STM32后先别急着让小车跑。按步骤调试静态测试车轮悬空在main函数的while(1)循环里写一个简单的测试序列。while (1) { Car_Forward(50); // 50%速度前进 HAL_Delay(2000); Car_Stop(); HAL_Delay(500); Car_TurnLeft(60); // 原地左转 HAL_Delay(1000); Car_Stop(); HAL_Delay(2000); // ... 其他动作测试 }观察车轮是否按预期转动。如果不动按以下顺序排查电源指示灯STM32板和驱动板上的电源LED亮了吗信号测量用万用表电压档或示波器如果有测量PWMA引脚对地电压。在电机转动时你应该能看到一个平均电压比如3.3V * 50% ≈ 1.65V。用万用表频率档可以粗略看PWM频率。方向信号测量AIN1和AIN2的电压是否根据代码变化0V或3.3V。电机驱动输出测量AO1和AO2之间的电压。当电机转动时这里应该有接近VM电源电压的PWM输出。注意安全避免短路。代码检查确认Left_Motor_Control函数里的引脚名和CubeMX配置的一致。确认__HAL_TIM_SET_COMPARE函数的第一个参数定时器句柄正确。动态测试小车落地静态测试通过后将小车放在空旷平整的地面上远离桌子边缘。再次运行测试程序。观察小车运动轨迹。典型问题小车不走直线而是偏向一边。这是100%会出现的现象也是开环控制的固有缺陷。原因有两个电机差异性即使是同一批次的电机其空载转速、负载特性也有微小差异。车轮安装与摩擦车轮安装的同心度、与地面的摩擦力不可能完全一致。开环阶段的“土办法”校准我们无法感知误差但可以“预补偿”。通过实验找到一个偏差值。比如你发现小车总是向右偏那么可以让左轮的速度略高于右轮。void Car_Forward_Adjusted(uint8_t speed) { uint8_t left_adj speed 5; // 左轮增加5个“单位”的补偿 uint8_t right_adj speed; if(left_adj 100) left_adj 100; // 防止溢出 Left_Motor_Control(left_adj); Right_Motor_Control(right_adj); }这个补偿值需要你在实际场地上反复测试获得。这就是开环控制基于事先的“经验”或“校准”来发指令无法应对运行中的新变化。5. 构建开环运动序列设计你的第一条“巡线”开环控制并非一无是处。在已知的、结构化的环境中我们可以通过精心设计的时序控制让小车完成一些固定任务。最经典的例子就是让小车走一个正方形轨迹。5.1 “走正方形”程序设计与陷阱思路很简单前进一段距离 - 右转90度 - 前进 - 右转... 循环4次。但这里的关键是“一段距离”和“90度”如何用时间和速度来定义void Car_Run_Square(void) { uint8_t side_speed 60; uint16_t forward_time 1000; // 前进时间单位ms uint16_t turn_time 500; // 旋转90度时间单位ms for(int i 0; i 4; i) { Car_Forward(side_speed); HAL_Delay(forward_time); // 前进 Car_Stop(); HAL_Delay(100); // 停顿一下让动作更清晰 Car_TurnRight(side_speed); HAL_Delay(turn_time); // 右转 Car_Stop(); HAL_Delay(100); } Car_Stop(); }你会发现这个小车画出来的绝对不是一个标准的正方形可能是一个歪歪扭扭的平行四边形甚至是个螺旋形。原因如下速度-时间与距离的非线性Car_Forward(side_speed)指令发出后小车从静止加速到稳定速度需要时间停止时也有减速过程。HAL_Delay(forward_time)延迟的这1秒钟里包含了加速和减速段真正以稳定速度前进的距离小于速度 * 时间。地面摩擦影响不同地面的摩擦系数不同加速和减速时间也不同。转弯角度的不确定性Car_TurnRight是差速转弯其角速度受电机性能、电池电压、地面摩擦等多重影响。HAL_Delay(turn_time)延迟0.5秒转过的角度不一定是90度。5.2 从“开环”到“开环”的改进思路纯粹的延时开环控制鲁棒性极差。为了让它更可靠一点我们可以引入一些非常简单的“准反馈”或更精细的控制模型这可以看作是从纯开环到闭环的一个过渡练习。思路一分段速度控制。模仿工业上的“S曲线加减速”让启动和停止更平滑减少打滑提高时间-距离关系的可重复性。void Car_Forward_Smooth(uint8_t target_speed, uint16_t distance_time) { // 加速段 for(int s 30; s target_speed; s5) { Car_Forward(s); HAL_Delay(20); // 每20ms增加一点速度 } // 匀速段 Car_Forward(target_speed); HAL_Delay(distance_time); // 减速段 for(int s target_speed; s 30; s-5) { Car_Forward(s); HAL_Delay(20); } Car_Stop(); }通过匀加速/减速电机负载变化更平缓电池电压波动更小两次执行的一致性会稍好一些。思路二利用简单的传感器进行事件触发。比如在小车底盘前端安装一个微动开关作为“碰撞传感器”。我们可以写一个程序“一直前进直到碰到障碍物开关被压下然后后退一点右转90度继续前进”。这就构成了一个非常初级的“随机避障”行为。虽然它没有测量距离只是感知了“碰撞”这个事件但已经引入了外部反馈系统的智能性上了一个台阶。这需要你学习STM32的外部中断EXTI编程。5.3 开环控制的价值总结通过以上实践你应该深刻体会到开环控制的局限性它无法应对扰动精度差。但它的价值同样巨大验证了硬件系统电机、驱动、电源、单片机连接全部正确。掌握了最底层的控制API你知道了如何让轮子正转、反转、变速。建立了系统性的调试思维从电源、信号、到软件逻辑的逐层排查。直观理解了“为什么需要闭环”你亲眼看到了小车跑不直、转弯不准这为你后续学习编码器测速、PID控制提供了最强烈的学习动机——你知道了你要解决的问题到底是什么。开环控制是起点而不是终点。它就像学骑自行车时的辅助轮帮你建立起平衡感和操控感但最终你要拆掉它去面对更真实、更复杂的路况。当你成功实现小车的开环控制后下一步自然就是为电机装上编码器开始探索“闭环控制”的世界那时你的小车才能真正做到“指哪打哪”抵抗各种外部干扰。