1. 项目开篇为什么选择做一辆自平衡小车大家好我是老张一个在嵌入式领域摸爬滚打了十多年的工程师。今天想和大家聊聊一个既经典又充满挑战的DIY项目——基于STM32和PID算法的两轮自平衡小车。你可能在视频网站上见过各种炫酷的平衡车它们能稳稳地立在地上甚至载着人跑来跑去。你有没有想过自己动手从零开始做一个会是一种什么样的体验我当初做这个项目纯粹是出于兴趣和“不服气”。市面上成熟的平衡车产品很多但黑盒子里的技术原理总让人心痒痒。于是我决定自己动手从选芯片、画电路、写代码到最后的调参测试完整地走一遍。这个过程里踩过的坑、获得的成就感是买一个成品完全无法比拟的。更重要的是这个项目几乎涵盖了嵌入式开发的所有核心技能微控制器编程、传感器数据融合、经典控制算法、电机驱动、实时系统调试……可以说做完一辆平衡小车你对嵌入式系统的理解会上一个大台阶。对于初学者来说可能会觉得自平衡小车涉及太多高深的理论什么倒立摆模型、PID控制、姿态解算听着就头大。别担心我的目标就是把这件事讲得明明白白。我会尽量避开复杂的数学推导用大白话和生活中的例子带你理解背后的原理。你不需要是控制理论专家只要有基本的C语言和单片机知识加上一点耐心和动手能力完全可以跟着做出来。咱们不搞纸上谈兵所有内容都基于我实际做出来的那辆小车代码、参数、遇到的问题都是实打实的经验。那么这辆小车最终能做到什么程度呢它不仅能像一根被手指立起的木棍一样稳稳站立还能听从指令前进、后退、转弯甚至在受到轻微推搡时迅速恢复平衡。接下来我就带你一步步拆解这个项目从核心器件的“选秀大会”开始。2. 硬件“选秀大会”核心器件怎么选做硬件项目第一步永远是“搭台子”——选择合适的元器件。这就像盖房子打地基选对了材料后面才省心。网上方案很多容易看花眼我结合自己的实战经验帮你分析一下关键部件的选择逻辑。2.1 大脑核心STM32F1还是F4控制器是小车的大脑负责处理所有传感器数据、运行控制算法、发出驱动指令。STM32是当仁不让的主流选择但系列繁多最常见的是F1和F4。STM32F103F1系列这是经典的“入门神U”。Cortex-M3内核主频通常在72MHz外设够用价格亲民资料和社区支持极其丰富。对于平衡小车它的性能其实是够用的。PID运算和基本的MPU6050数据处理对它来说不算重担。STM32F407F4系列我最终选择了它。原因很简单性能余量和浮点单元FPU。F4是Cortex-M4内核带硬件FPU主频能跑到168MHz。这意味着什么首先在进行姿态解算涉及大量三角函数、浮点运算时硬件FPU比软件模拟快几十倍计算更精准、更及时。其次更高的主频和更大的内存让我可以更从容地添加功能比如用OLED实时显示丰富的数据曲线或者运行更复杂的滤波算法而不用担心系统卡顿。虽然价格比F1贵一些但为了调试时更顺畅的体验和未来扩展的可能性我觉得这笔投资值得。我的建议如果你是绝对的初学者想以最低成本验证原理F1完全可行。但如果你希望项目更稳健、调试体验更好并且愿意为未来的学习铺路直接上F4是更省心的选择。我用的就是STM32F407VET6这款。2.2 感知核心为什么MPU6050是性价比之王感知姿态的传感器是平衡小车的“小脑”和“前庭”负责告诉主控“我现在歪了多少”、“正在以多快的速度倒下去”。方案无非分体式加速度计陀螺仪和集成式。分体式方案比如用ADXL345加速度计和ITG3205陀螺仪。这种组合性能可能极高但你需要自己用软件进行数据融合和校准电路设计和编程调试都更复杂对新手不友好。集成式方案MPU6050这是我强烈推荐的。它把三轴加速度计和三轴陀螺仪集成在一颗芯片里还内置了运动处理引擎DMP。DMP是个“外挂”它能直接在芯片内部完成传感器数据的融合通过I2C接口直接输出稳定、准确的姿态角俯仰角、横滚角极大减轻了主控MCU的负担也降低了我们编程的难度。你不需要从零开始写复杂的卡尔曼滤波或互补滤波算法就能获得不错的数据。对于自平衡小车我们主要关心俯仰角前后倾斜MPU6050的DMP输出完全满足要求。所以除非你有特殊的高精度需求否则MPU6050就是那个“闭眼入”的选项成本低、效果好、资料多。2.3 动力与执行电机、驱动与编码器这是小车的“腿”和“肌肉”决定了它能不能有力、听话地执行大脑的指令。电机选择常见的有步进电机和直流减速电机。步进电机控制精准可以精确控制旋转角度。但它的启动和停止特性在需要快速、连续响应速度变化的平衡控制中并不理想容易产生振荡。直流减速电机这是更主流的选择。减速箱提供了更大的扭矩劲大能让小车负载更重、响应更快。我们通过PWM脉冲宽度调制来控制它的速度虽然单从位置控制角度看不如步进电机精准但对于需要快速进行速度调整以维持平衡的系统来说其动态响应特性更合适。我选用的是N20型号的直流减速电机配有一定减速比扭矩足够。驱动芯片选择L298N vs TB6612。L298N老牌经典皮实耐造驱动电流大单桥2A价格便宜。但它有个缺点发热量大效率相对较低需要加装散热片。它的控制逻辑简单直接给高低电平控制方向给PWM控制速度。TB6612更现代的芯片效率高发热小外围电路简单。它同样可以驱动双路电机。从性能上讲TB6612更优。我为什么选了L298N因为在项目初期手边正好有现成的L298N模块而且它足够“抗造”在调试阶段频繁正反转、堵转也不会轻易烧毁。对于新手L298N模块集成度高接线清晰更容易上手。你可以根据手头资源选择两者在代码控制上逻辑相似。编码器这是实现“速度环”控制的关键。电机自带的编码器通常是霍尔编码器可以反馈电机实际转速。主控通过读取编码器脉冲数就能知道轮子转得多快、转了多远。没有速度反馈小车只能实现“直立平衡”无法实现“定速行驶”或“位置控制”。我建议务必选择带编码器的电机它为后续的功能扩展奠定了基础。2.4 其他关键模块电源推荐使用两节18650锂电池串联约7.4V-8.4V或一块2S锂聚合物电池7.4V。这个电压既能直接给电机驱动供电也可以通过降压模块如LM2596稳定到5V和3.3V给单片机和传感器供电。务必注意电源的持续放电能力C数要能满足两个电机瞬间启动的电流需求。蓝牙模块HC-05/06用于无线遥控和调试。你可以用手机APP发送指令控制小车运动更关键的是可以通过蓝牙串口将单片机内部的姿态角、PID误差、电机PWM值等数据实时发送到电脑的上位机软件进行可视化显示这对调试PID参数来说简直是“神器”。车架与结构你可以购买现成的亚克力或金属平衡小车套件车架也可以自己用洞洞板或3D打印制作。核心原则是重心要低结构要紧凑牢固。电池等重物尽量放在底部这样小车更稳定。3. 控制核心用“人话”讲明白PID算法硬件搭好了相当于有了身体。接下来要注入灵魂——控制算法。别被“算法”吓到PID可能是世界上最易懂、最实用的算法之一。咱们不用公式轰炸我用骑自行车和倒立扫把的例子来类比。3.1 平衡的本质倒立摆与负反馈想象一下你用手指竖直顶着一根长木棍让它不倒。你是怎么做的眼睛看着棍子顶端的偏移大脑判断偏移的方向和速度然后手指移动去“追”棍子的重心。这个过程就是一个典型的负反馈控制。自平衡小车就是一个“倒立摆”。它的自然状态是不稳定的就像倒立的棍子一松手就会倒下。我们要做的就是通过控制车轮的转动产生一个力去抵消车身的倾斜。核心思想是让车轮朝着车身倾斜的方向加速运动。车往前倾轮子就往前转用车轮的位移来“接住”要倒下的车身车往后仰轮子就往后转。3.2 PID控制器三位一体的“调节大师”PID控制器就是模仿我们大脑的这个调节过程。它根据“期望值”和“实际值”的偏差e来工作。在我们的平衡小车里期望值就是竖直状态角度为0实际值就是MPU6050测出的当前俯仰角。偏差e 期望角度 - 实际角度。PID就是对这个偏差进行三种运算然后加起来去控制电机。P比例控制 - “有多歪就多用劲”这是最直接的反应。小车倾斜角度越大偏差e越大P项输出就越大电机就会用更大的力气往倾斜的反方向转动试图把车身“拉”回来。P是维持平衡的主力。但只有P会出问题小车可能会在平衡点附近来回剧烈振荡就像你用力过猛地调整木棍总是调过头。D微分控制 - “倒得快就提前刹”D关注的是偏差变化的速度即角速度。小车倒下的速度越快D项输出就越大它起到一个“阻尼”或“预见”的作用。当小车快速倒下时D项会产生一个强大的反向控制力抑制其倒下趋势相当于提前刹车能有效减少P引起的振荡让恢复过程更平滑。P和D结合PD控制器是让小车稳定站立的黄金组合。I积分控制 - “老往一边偏就慢慢补”I关注的是偏差的累积。如果小车因为地面轻微不平、电机细微差异等原因存在一个持续的、微小的偏向力导致它总是微微偏向一侧光靠P和D无法完全回到零点这叫“静差”。I项会把这个微小的、持续的偏差不断累加起来时间越长累积值越大最终产生一个足够大的控制力去抵消那个持续的偏向力。在平衡小车中I项通常用于速度环让小车能稳定在某个设定速度上而不是越跑越快或越跑越慢。一个生动的比喻开车保持车道。P你发现车偏右了就向左打方向盘。偏得越多方向盘打得越多。D你不仅看车偏了多少还感觉车正在快速向右偏比如有侧风于是你更早、更果断地向左打方向盘来对抗这个趋势。I你发现车的方向盘本身有点向右跑偏即使你手扶正了它也会慢慢往右偏。于是你长期保持一个微小的向左修正力来抵消这个固有的偏差。3.3 双环PID结构直立与行走的分工在实际程序中我们通常采用双环PID控制结构这是平衡小车稳定且能受控运动的关键。内环直立环这是一个角度PD控制。输入是期望角度通常为0度和MPU6050解算出的实时角度。输出直接控制电机的PWM目标是让小车像不倒翁一样站稳。这个环要求响应速度极快所以控制周期要短比如1ms中断一次。外环速度环这是一个速度PI控制。输入是期望速度通过遥控器给定比如0表示静止和编码器测得的实际速度。输出不是一个直接的PWM而是作为内环直立环的期望角度的调整量。它是如何工作的假设我们想让小车以某个速度前进。速度环PI控制器发现实际速度小于期望速度就会计算出一个正值。这个正值会加到直立环的期望角度上。比如原本直立环期望是0度现在变成了一个微小的前倾角度如0.5度。直立环PD控制器一看“哎呀车身怎么前倾了” 它立刻命令电机向前转动来“扶正”车身。但因为这个前倾角度是速度环故意给的小车就会在“不断试图扶正这个假倾斜”的过程中持续地向前运动从而达到设定的速度。后退和转弯差速控制也是类似的原理。这种结构巧妙地将“站稳”和“走起来”两个任务解耦让控制逻辑非常清晰。4. 软件实战从数据到行动的代码之旅理论懂了我们来看代码怎么实现。我会挑最核心的片段用注释讲清楚。完整工程涉及初始化、中断、通信等这里聚焦控制逻辑。4.1 姿态获取MPU6050与DMP首先我们要从MPU6050获取可靠的俯仰角Pitch。使用DMP是最便捷的方式。// 伪代码展示流程 #include mpu6050.h #include inv_mpu.h float Pitch_Angle; // 全局变量存放俯仰角 void MPU6050_Init(void) { // 初始化I2C I2C_Configuration(); // 初始化MPU6050设置量程等 mpu_init(); // 初始化DMP加载固件 dmp_load_motion_driver_firmware(); dmp_set_orientation(); dmp_enable_feature(); // 使能所需功能如四元数输出 // 设置DMP输出速率例如200Hz mpu_set_dmp_rate(200); // 使能DMP中断 dmp_enable_interrupt(); } // 在DMP数据准备好中断或主循环轮询中调用 void Get_Angle_From_DMP(void) { short gyro[3], accel[3]; unsigned long sensor_timestamp; unsigned char more; long quat[4]; // 四元数 float q0, q1, q2, q3; if (dmp_read_fifo(gyro, accel, quat, sensor_timestamp, more) 0) { // 将DMP解算出的四元数转换为欧拉角俯仰角 q0 quat[0] / 16384.0f; // DMP输出的四元数是放大16384倍的 q1 quat[1] / 16384.0f; q2 quat[2] / 16384.0f; q3 quat[3] / 16384.0f; // 计算俯仰角Pitch弧度公式可能因坐标系定义而异 Pitch_Angle asin(2*(q0*q2 - q1*q3)) * 57.29578f; // 转换为度 } }通过DMP我们直接拿到了相对稳定的角度值省去了自己写滤波算法的麻烦。4.2 速度获取编码器读数我们需要知道每个轮子的实际转速。编码器输出两路相位差90度的方波A相、B相通过STM32的定时器编码器接口模式可以轻松计数。// 使用TIMx的编码器接口模式初始化后计数值会自动增减 int32_t Left_Wheel_Speed, Right_Wheel_Speed; int32_t Left_Encoder_Last, Right_Encoder_Last; // 在一个固定的周期如10ms中断里计算速度 void Speed_Calculate_Periodic(void) { int32_t left_cnt, right_cnt; // 读取当前编码器计数值 left_cnt (int32_t)TIM2-CNT; // 假设左电机编码器接TIM2 right_cnt (int32_t)TIM3-CNT; // 假设右电机编码器接TIM3 // 计算本次周期内的脉冲数速度 // 注意处理计数器溢出16位或32位自动重载 Left_Wheel_Speed left_cnt - Left_Encoder_Last; Right_Wheel_Speed right_cnt - Right_Encoder_Last; // 更新上一次的计数值 Left_Encoder_Last left_cnt; Right_Encoder_Last right_cnt; // 将脉冲数转换为实际速度单位cm/s 或 RPM // 需要根据车轮周长、编码器线数、减速比计算 // Real_Speed (Delta_Pulse / Pulse_Per_Revolution) * Wheel_Circumference / Period_Time; }4.3 核心控制双环PID的实现这是整个程序的心脏通常在一个高优先级定时器中断如1ms中执行。// 定义PID结构体 typedef struct { float Target; // 目标值 float Current; // 当前值 float Err; // 本次偏差 float Err_Last; // 上一次偏差 float Err_Sum; // 偏差积分积分限幅很重要 float Kp, Ki, Kd; // PID参数 float Output; // 输出值 float OutputMax; // 输出限幅 float OutputMin; float IntegralMax; // 积分限幅 } PID_TypeDef; PID_TypeDef Pitch_PID; // 直立环PD控制器 PID_TypeDef Speed_PID; // 速度环PI控制器 float Target_Speed 0.0f; // 目标速度由蓝牙遥控设定 float Balance_PWM; // 直立环计算出的基础PWM float Speed_PWM; // 速度环计算出的PWM调整量 float Final_PWM_Left, Final_PWM_Right; // 最终输出给左右电机的PWM // PID计算函数位置式 float PID_Calculate(PID_TypeDef *pid) { pid-Err pid-Target - pid-Current; // 比例项 float p_out pid-Kp * pid-Err; // 积分项带限幅防止积分饱和 pid-Err_Sum pid-Err; if (pid-Err_Sum pid-IntegralMax) pid-Err_Sum pid-IntegralMax; if (pid-Err_Sum -pid-IntegralMax) pid-Err_Sum -pid-IntegralMax; float i_out pid-Ki * pid-Err_Sum; // 微分项使用不完全微分或对偏差微分 float d_out pid-Kd * (pid-Err - pid-Err_Last); pid-Err_Last pid-Err; // 总和并限幅 pid-Output p_out i_out d_out; if (pid-Output pid-OutputMax) pid-Output pid-OutputMax; if (pid-Output pid-OutputMin) pid-Output pid-OutputMin; return pid-Output; } // 1ms中断服务函数中的核心控制逻辑 void Control_Loop_IRQHandler(void) { // 1. 读取当前姿态角来自DMP Get_Angle_From_DMP(); // 更新全局变量 Pitch_Angle // 2. 直立环PD控制内环 Pitch_PID.Target 0.0f; // 目标角度竖直为0度 Pitch_PID.Current Pitch_Angle; // 当前角度 Balance_PWM PID_Calculate(Pitch_PID); // 计算维持直立所需的PWM // 3. 速度环PI控制外环 - 控制周期可以比直立环长例如每10ms执行一次 static uint8_t speed_cnt 0; speed_cnt; if (speed_cnt 10) { // 10ms到了 speed_cnt 0; // 计算平均速度左右轮平均或分别控制 float current_speed (Left_Wheel_Speed Right_Wheel_Speed) / 2.0f; Speed_PID.Target Target_Speed; // 目标速度遥控设定 Speed_PID.Current current_speed; Speed_PWM PID_Calculate(Speed_PID); // 计算速度调整量 // 速度环的输出是作为直立环目标角度的微调量 // 这里简化处理直接叠加到最终的PWM输出上。更标准的做法是叠加到Pitch_PID.Target上。 } // 4. 合成最终PWM输出并加入转向控制 float turn_adjust Get_Turn_Adjust(); // 从遥控器获取转向调整量 Final_PWM_Left Balance_PWM Speed_PWM - turn_adjust; // 左电机 Final_PWM_Right Balance_PWM Speed_PWM turn_adjust; // 右电机 // 5. 设置电机PWM输出注意方向 Set_Motor_PWM(MOTOR_LEFT, Final_PWM_Left); Set_Motor_PWM(MOTOR_RIGHT, Final_PWM_Right); }这段代码清晰地展示了双环PID的控制流程。注意实际项目中需要对编码器速度进行低通滤波以消除噪声同时积分项和输出项的限幅是防止系统失控的关键。5. 调试心法让小车“站”起来的艺术硬件焊接无误代码也写好了但小车可能东倒西歪甚至“跳舞”。别灰心调试PID参数是必经之路也是有乐趣的过程。我分享一下我的“调参三部曲”。5.1 准备工作安全与可视化安全第一调试时先用东西架起小车让轮子悬空。防止参数不对时小车乱窜损坏或伤人。可视化工具务必搭建蓝牙无线调试通道。在电脑上用串口绘图工具如SerialPlot、匿名上位机、VOFA等实时查看角度、角速度、PID各分量输出、电机PWM等波形。“看得见”才能调得好。5.2 分步调试先直立后行走第一步只调试直立环PD参数先调P后调D速度环和转向环先关闭在代码中将Target_Speed和turn_adjust固定为0。初始化参数Kp0, Ki0, Kd0OutputMax设一个较小值比如对应PWM占空比30%。调P比例用手扶住小车让它稍微倾斜感受电机是否向正确的方向转动试图扶正。然后缓慢增大Kp。你会发现Kp太小小车无力一松手就倒Kp太大小车会剧烈振荡。目标是找到一个临界点在这个点附近小车松手后能在平衡点附近高频但幅度不太大地抖动。记住这个临界Kp值。调D微分在临界Kp的基础上逐渐增加Kd。D的作用是抑制振荡。你会看到小车的抖动幅度逐渐减小频率也可能变慢。一直加到小车能够基本稳住虽然可能还有轻微缓慢的漂移但不会剧烈振荡或倒下。D是阻尼能让你使用更大的P而保持稳定。适当回调一点Kp配合Kd让小车在悬空时能基本静止。第二步加入速度环PI参数让直立环工作良好后打开速度环。速度环的目标是消除静差和实现定速。先给一个很小的Ki积分Kp也从小开始。给小车一个目标速度比如让轮子慢慢正转。观察速度是否能跟上。如果响应太慢增大Kp如果出现超调或振荡适当增大Ki或调整Kp。速度环的响应应该比直立环慢如果速度环变化太快会干扰直立平衡。积分限幅IntegralMax非常重要必须设置一个合理的上限防止小车在启动、卡住等情况下积分项无限累积积分饱和导致恢复时产生巨大的“冲击”。第三步地面实测与微调将小车放在平整开阔的地面上进行测试。“点头”问题如果小车在原地前后高频点头可能是直立环D不足或P略大适当增加D或减小P。缓慢漂移小车能站住但会缓慢地向一个方向移动。这可能是机械重心不完全对称或电机细微差异造成的。这正好是速度环I项发挥作用的时候它会慢慢累积误差输出一个补偿量让小车停住。外力抗干扰轻轻推一下小车它应该能迅速反弹回来并在平衡点附近有1-2次阻尼振荡后恢复稳定。如果推了回不来可能是P不够如果推了来回振荡很多次可能是D不够。5.3 常见问题与“坑点”角度零点漂移MPU6050即使静止角度也可能缓慢变化。上电时确保小车在水平位置静止1-2秒进行软件校准记录此时的初始角度作为零点偏移。DMP输出相对稳定但也要注意。电机响应不一致两个电机的性能不可能完全一样。可以通过在代码中为左右电机设置不同的PWM补偿系数来微调。电源电压下降电池电量不足时电压下降电机力道会变弱可能导致平衡失败。可以考虑在代码中加入对电池电压的监测当电压低时适当等比例降低PID输出的最大限幅让控制量适应电机的实际能力。中断优先级与时序确保姿态读取如MPU6050 DMP中断、编码器计数、控制计算定时器中断的优先级合理且执行时间稳定。控制周期如1ms必须严格准时不能被打断太久。调试PID是个耐心活没有一成不变的最优参数。不同的车重、重心、轮胎摩擦力都需要微调。我的经验是参数的大致范围直立环Kp在几十到几百Kd在几到几十速度环Kp和Ki通常都比较小零点几的量级。关键是多观察波形理解每个参数改变对系统行为的物理影响。当你的小车终于稳稳立住听从指令前进后退时那种喜悦是无与伦比的。这个项目带给你的远不止一辆会动的小车更是一套解决实际工程问题的思维方法和动手能力。希望这篇长文能成为你“从零到一”路上的得力助手。如果在制作过程中遇到具体问题欢迎随时来交流讨论很多奇奇怪怪的问题往往都是一个电阻没焊好或者一句代码写错了慢慢找总能解决。