基于STM32的双轮自平衡小车——从零搭建到稳定运行的实战指南

📅 发布时间:2026/7/4 20:24:17 👁️ 浏览次数:
基于STM32的双轮自平衡小车——从零搭建到稳定运行的实战指南
1. 项目开篇从零开始理解你的“不倒翁”小车嘿朋友们今天咱们来玩点硬核又有趣的——用STM32做一个属于自己的双轮自平衡小车。这玩意儿就像一个电子版的“不倒翁”或者更形象点像杂技演员脚下的独轮车需要不断调整轮子的转速来对抗倾倒的趋势保持直立。听起来很酷对吧但别被“自平衡”、“控制算法”这些词吓到我当年也是从一堆散件和满屏的报错信息里摸爬滚打过来的。这篇文章就是把我踩过的坑、试过的路掰开揉碎了讲给你听目标就一个让你也能亲手做出一个能稳稳站住甚至能听你指挥前后跑动的小车。这个项目非常适合刚接触STM32和嵌入式系统的朋友。它麻雀虽小五脏俱全涵盖了单片机开发、传感器数据采集、电机驱动、控制算法和系统调试这嵌入式开发的几大核心技能。你不需要有高深的数学和自动控制理论背景咱们用最直观的方式去理解。手头你需要准备一些基础硬件一块STM32核心板比如经典的“蓝桥杯”板子STM32F103C8T6或者资源更丰富的F103RCT6都行、两个带减速箱的直流电机最好带编码器、一个MPU6050姿态传感器、一个电机驱动模块L298N或TB6612、一块锂电池以及一些杜邦线和结构件。软件上准备好Keil或者STM32CubeIDE咱们的代码之旅就从这里开始。整个项目的构建过程我会把它拆解成清晰的几步硬件连接与测试、基础驱动代码编写、核心“大脑”姿态解算与PID控制的实现以及最磨人也最有成就感的系统联调与参数整定。每一步我都会告诉你为什么要这么做常见的坑在哪里以及怎么排查问题。相信我当你看到自己组装的小车从东倒西歪到颤颤巍巍最终稳稳立住的那一刻那种成就感是无与伦比的。好了废话不多说咱们卷起袖子开始干2. 硬件搭建给你的小车一副好“身板”硬件是项目的骨架搭建得是否牢靠直接决定了后续软件调试是事半功倍还是事倍功半。咱们的目标不是追求极致的性能而是在有限的预算内搭建一个稳定、可靠、便于调试的系统。2.1 核心控制器与姿态感知主控芯片我强烈推荐从STM32F103C8T6入手也就是大家常说的“最小系统板”或“BluePill”。它价格便宜资料丰富社区支持强大性能对于平衡小车来说完全够用。它的定时器资源丰富正好可以用来产生驱动电机的PWM波和读取编码器脉冲。姿态传感器方面MPU6050是绝对的首选它把三轴陀螺仪和三轴加速度计集成在一个芯片里通过I2C接口和单片机通信接线简单。陀螺仪测量角速度转得快慢加速度计在静止时能感知重力方向从而算出倾角。但要注意它俩各有缺点陀螺仪数据积分会漂移时间长了角度就歪了加速度计则非常怕震动小车一动数据就乱跳。所以后期我们需要用算法把它们的数据融合起来取长补短。接线是关键一步一定要仔细VCC接3.3V千万别接5V会烧GND接GNDSCL接单片机的一个I2C时钟引脚如PB6SDA接单片机的一个I2C数据引脚如PB7接好后第一件事不是写复杂的算法而是写一个简单的测试程序读取MPU6050的原始数据并打印到串口。用STM32CubeMX配置一下I2C和USART会非常方便。看看数据是否正常变化用手转动传感器观察陀螺仪数值倾斜传感器观察加速度计数值。这一步能排除80%的硬件连接和驱动问题。2.2 动力与驱动系统电机建议选择额定电压6V或12V的直流减速电机减速比大一些这样扭矩足低速控制更平稳。一定要选带编码器的型号编码器是电机的“眼睛”它能反馈轮子实际转了多少、转得多快是实现速度闭环控制不可或缺的部件。编码器常见有AB相输出需要接到STM32定时器的编码器接口模式如TIM2、TIM3、TIM4这样单片机硬件会自动计数非常省心。驱动模块TB6612比经典的L298N更推荐。它体积小、发热低、效率高而且内置保护电路。L298N需要外接散热片逻辑电压和电机驱动电压要分开对新手稍微麻烦点。接线时务必分清控制线和动力线单片机的两个PWM引脚如TIM1的CH1, CH2接驱动模块的PWMA和PWMB控制电机速度。单片机的两个GPIO引脚接驱动模块的AIN1/AIN2和BIN1/BIN2控制电机正反转。驱动模块的VM电机电源接锂电池如12VVCC逻辑电源接单片机的5V或直接接电池经降压后的5V。电机的两条线接到驱动模块的输出端。电源管理是稳定的基石。建议使用一块2S或3S的锂电池7.4V或11.1V通过一个DC-DC降压模块得到稳定的5V给单片机和驱动模块逻辑部分供电再用一个LDO如AMS1117-3.3从5V降到3.3V给STM32和MPU6050供电。强烈建议用STM32的ADC通道监测电池电压当电压过低时让小车进入保护状态防止电池过放。3. 软件基石让硬件“动”起来的驱动代码硬件连接无误后我们就要通过代码让各个部件听令行事了。这一步我们专注于编写最底层的驱动程序确保每一个硬件都能被我们精确控制。3.1 电机PWM与方向控制电机驱动本质就是通过调节PWM脉冲宽度调制信号的占空比来控制电机两端的平均电压从而控制转速。我们用STM32的通用定时器如TIM1来产生两路PWM。使用STM32CubeMX配置非常直观选择定时器设置预分频器和自动重装载值ARR来决定PWM频率一般10kHz-20kHz即可然后使能对应通道的PWM输出模式。在代码中我们通过一个设置函数来控制电机。例如我们可以这样封装// 设置单个电机的速度和方向 // motor: 0-左电机 1-右电机 // speed: -1000 ~ 1000 负值代表反转 void Motor_Set(int motor, int speed) { int dir (speed 0) ? 1 : 0; int abs_speed abs(speed); if(abs_speed 1000) abs_speed 1000; // 限幅 if(motor 0) { // 左电机 HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, dir); HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, !dir); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, abs_speed); } else { // 右电机 HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, dir); HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, !dir); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, abs_speed); } }写完后单独写个测试程序让左右电机分别正转、反转、加减速观察轮子转动是否顺畅、响应是否及时。这是检验驱动电路和代码是否正确的最直接方法。3.2 编码器速度测量编码器会随着电机转动输出两路相位差90度的方波A相和B相。STM32的定时器编码器模式可以自动根据这两路信号的边沿和顺序进行加减计数。我们只需要在CubeMX里配置定时器如TIM2、TIM3为“Encoder Mode”并指定编码器接口模式通常是TI1和TI2。之后我们定期比如每10毫秒读取一次定时器的计数器值CNT并清零。这个差值就是这段时间内编码器产生的脉冲数。根据电机减速比和编码器线数比如电机转一圈输出多少个脉冲就能换算成轮子的实际转速。公式是速度 (脉冲数 / 时间) / (减速比 * 编码器线数)。得到的速度单位可以是“转/秒”或“脉冲/毫秒”只要前后一致就行。这个速度值将是我们实现速度闭环控制的反馈量。3.3 MPU6050数据读取与校准MPU6050通过I2C通信我们可以使用HAL库的HAL_I2C_Mem_Read函数来读取其寄存器中的数据。首先需要初始化MPU6050设置它的量程例如陀螺仪±2000°/s加速度计±4g和采样率。上电校准是必须的传感器存在零偏即静止时输出的角速度和加速度不为零。校准方法很简单将小车水平静止放置一段时间几秒钟连续读取几百个陀螺仪和加速度计的原始数据分别求平均值这些平均值就是零偏值。在后续的数据处理中每次读取的原始值都要减去对应的零偏值。这一步没做好角度解算的起点就是歪的小车永远也站不稳。4. 核心算法上读懂小车的“感觉”——姿态解算现在硬件能动了数据也能读了接下来就是最核心的部分算法。我们要让单片机理解小车当前“歪了多少”、“正在以多快的速度倒下去”。这就是姿态解算。4.1 传感器数据的初步处理从MPU6050读出的陀螺仪原始数据是角速度单位通常是“度/秒”或“弧度/秒”取决于量程设置和换算。加速度计数据是三个轴的加速度值。在静止状态下加速度计测得的合力就是重力加速度。因此我们可以通过计算加速度计数据在X-Z平面或Y-Z平面取决于你的安装方式的夹角来估算出车身相对于竖直方向的倾斜角。公式是角度_acc atan2(-Accel_X, Accel_Z) * 180 / PI。这个角度对静态或缓慢变化很准但小车一动起来额外的加速度就会严重干扰它。陀螺仪数据积分可以得到角度变化角度_gyro 上次角度 角速度 * 时间间隔(dt)。这个角度动态响应好但积分误差会随时间累积哪怕零偏只有一点点几秒钟后角度就漂到不知道哪去了。4.2 互补滤波简单高效的融合方案既然两者各有优劣我们就用一个巧妙的办法把它们结合起来这就是互补滤波。它的思想直白又好用对于角度信号我们信任陀螺仪的高频部分变化快响应及时信任加速度计的低频部分长期稳定不漂移。用一个系数K0到1之间来分配信任度。代码实现起来非常简洁float g_fAngle; // 全局变量最终融合后的角度 float g_fGyroZero 0.0; // 陀螺仪零偏已校准 float COMPLEMENTARY_FACTOR 0.98f; // 互补滤波系数 void Update_Angle(float gyro_z, float accel_angle, float dt) { // 1. 用陀螺仪积分得到角度预测 float angle_gyro g_fAngle (gyro_z - g_fGyroZero) * dt; // 2. 用互补滤波融合加速度计角度和陀螺仪积分角度 g_fAngle COMPLEMENTARY_FACTOR * angle_gyro (1.0f - COMPLEMENTARY_FACTOR) * accel_angle; }这里的dt是两次调用此函数的时间间隔必须尽可能精确和稳定。最好的做法是在一个定时器中断服务函数里固定周期比如5毫秒执行这个更新函数。系数K一般取0.95到0.99K越大信任陀螺仪越多响应快但可能漂移K越小信任加速度计越多抗漂移好但对震动敏感。我建议从0.98开始调。4.3 进阶选择卡尔曼滤波如果你的数学功底不错或者对性能有更高要求可以尝试卡尔曼滤波。它不再是一个固定的加权平均而是一套动态最优估计算法。它会根据陀螺仪和加速度计各自的“可信度”在卡尔曼滤波里叫协方差矩阵实时调整对两者的信任权重。当小车剧烈运动、加速度计数据不可信时它会自动更依赖陀螺仪当小车静止或匀速时则用加速度计来修正陀螺仪的漂移。实现一个一维的卡尔曼滤波来估计角度并不算太复杂网上也有很多开源的代码。但对于初学者我仍然建议先从互补滤波开始把它调通调稳彻底理解传感器融合的概念后再考虑升级到卡尔曼滤波。毕竟能让小车稳定站立的互补滤波远比一个调不好的卡尔曼滤波要强。5. 核心算法下给小车装上“大脑”——PID控制知道了小车“歪了多少”接下来就要指挥轮子“怎么转”来把它扶正。这就是控制算法的任务而PID控制器是其中经久不衰、简单有效的法宝。5.1 PID控制器的直观理解别被公式吓到咱们用人话来说。假设小车向前倾了角度为正我们需要让轮子也向前转用车身的移动来“追”上重心从而把它拉回来。P比例 歪得越狠误差越大就让轮子转得越快。这是最直接的反应。但光有P小车会在平衡点附近来回振荡就像你用手去立一根筷子总是用力过猛。D微分 观察小车倒下的“速度”角速度。如果它倒得很快我们就需要更大的力去“刹住”这个趋势。D项就是干这个的它能预测未来的误差趋势提供阻尼有效减少振荡。对于平衡小车角度环的D项至关重要没有它基本立不住。I积分 累计长时间的误差。比如小车因为地面轻微倾斜或电机细微差异导致有一个持续的微小误差P和D都无法完全消除这时I项会慢慢增加输出最终消除这个静态误差。但I项太强容易导致系统反应迟钝甚至失控初期可以设为0。5.2 角度环直立环的实现这是最内环也是最重要的环。它的任务就是让角度归零。输入是目标角度0度和当前融合后的角度输出直接控制电机的PWM。typedef struct { float Kp, Ki, Kd; float integral; float last_error; } PID_t; PID_t g_AnglePID {25.0f, 0.0f, 0.8f, 0, 0}; // 初始参数需要调试 float PID_Calculate(PID_t *pid, float target, float measure) { float error target - measure; pid-integral error; // 积分限幅防止积分饱和 if(pid-integral 1000) pid-integral 1000; if(pid-integral -1000) pid-integral -1000; float derivative error - pid-last_error; pid-last_error error; float output pid-Kp * error pid-Ki * pid-integral pid-Kd * derivative; return output; } // 在控制周期如5ms定时中断中调用 void Balance_Control(void) { float current_angle g_fAngle; // 从姿态解算获取 float pwm_output PID_Calculate(g_AnglePID, 0.0f, current_angle); // 将输出同时作用到两个电机但方向相反为了平衡 Motor_Set(0, -pwm_output); // 左电机 Motor_Set(1, pwm_output); // 右电机 }注意这里左右电机的输出是相反的因为当小车前倾需要向前运动时两个轮子都需要向前转。5.3 速度环速度控制环的引入只有角度环的小车就像一个站在滑板上的不倒翁虽然能站住但轻轻一推就会无限地滑下去。我们需要增加速度环来让小车在平衡的同时也能控制它的移动和静止。速度环是外环角度环是内环。速度环的输入是目标速度通常由遥控器给定或者为0和编码器测得的实际速度它的输出不是直接给电机而是作为角度环目标值的偏移量。比如我们想让小车以一定速度前进速度环就会计算出一个负的角度目标偏移让小车稍微“前倾”角度环为了维持这个“前倾”就会驱动轮子向前转从而让小车前进。当实际速度接近目标速度时这个角度偏移就趋于0小车保持匀速。PID_t g_SpeedPID {1.5f, 0.05f, 0.0f, 0, 0}; // 速度环参数 float g_TargetSpeed 0.0f; // 目标速度通常由遥控设定 void Speed_Control(void) { float current_speed (Get_LeftSpeed() Get_RightSpeed()) / 2.0f; // 获取平均速度 float angle_adjust PID_Calculate(g_SpeedPID, g_TargetSpeed, current_speed); // 将角度调整量叠加到角度环的目标值上 float balance_output PID_Calculate(g_AnglePID, angle_adjust, g_fAngle); // 注意这里目标值变了 Motor_Set(0, -balance_output); Motor_Set(1, balance_output); }速度环的KP通常比角度环小很多KI也很小主要起缓慢调节的作用。它的存在使得小车在受到外力干扰后能自己回到原位而不是越跑越远。6. 调试实战让小车“站”起来的艺术所有代码写好之后最激动人心也最考验耐心的调试阶段就来了。这个过程就像教一个婴儿走路需要极大的耐心和细致的观察。6.1 分步调试层层验证第一步验证姿态解算。先不要装轮子或者把小车架起来让轮子悬空。上电后通过串口把解算出的角度g_fAngle实时打印出来可以用匿名上位机、SerialPlot等工具绘图观察。用手缓慢倾斜小车主板看角度值是否线性变化响应是否及时。静止时角度应该基本稳定在零点附近波动范围最好在1度以内。如果跳动很大检查MPU6050的安装是否牢固软件滤波参数是否需要调整。第二步单独测试电机和编码器。写一个测试程序用手给定PWM值看轮子正反转是否正常力度是否均匀。同时打印编码器计数值转动轮子看计数值增减是否正确、连续。确保左右轮子的转向和编码器计数方向在软件定义上是协调的。第三步只开启角度环进行“静态平衡”。这是最关键的一步。把小车放在平整、有一定摩擦力的地面上比如地毯用手扶住让它直立。先给一组很小的PID参数比如Kp10, Ki0, Kd0。然后松手观察现象如果小车毫无反应缓慢倒下说明Kp太小增大它。如果小车猛地向一边加速冲出去说明电机输出方向反了立刻检查Motor_Set函数中左右电机输出与角度误差的符号关系。这是最常见的错误之一。调整Kp到小车能快速响应倾斜但又不会冲过头。然后加入Kd。Kd的作用是抑制振荡。当你增加Kd时小车的抖动应该会明显减小变得“沉稳”。Kd太小会振荡太大会反应迟钝。反复微调Kp和Kd直到小车能在你松手后在原地高频小幅度抖动地保持直立此时可能还会缓慢漂移因为还没加速度环。6.2 参数整定的经验与技巧调参没有绝对的公式但有一些原则先P后D再I角度环的Ki初期永远设为0。速度环的Kp和Ki也从非常小的值开始。胆大心细每次调整参数改变幅度可以大一些比如翻倍或减半快速观察系统响应趋势找到大致范围后再精细微调。关注现象小车是发散振荡振幅越来越大还是收敛振荡振幅越来越小是响应太慢还是过于敏感这些现象直接对应了哪个参数过大或过小。利用上位机如果条件允许将角度、角速度、PID输出等关键变量通过蓝牙或无线串口发送到电脑上位机如VOFA、匿名科创地面站进行绘图观察这比盲目猜测高效无数倍。安全第一调试时最好用绳子或围栏限制小车的活动范围防止它失控撞坏。同时做好急停开关随时切断电机电源。当你调出一组参数小车能够稳稳立住并且轻轻推它一下它能抵抗扰动并回到平衡位置时恭喜你最艰难的部分已经过去了接下来加入速度环小车就能实现定速、刹车等更智能的行为了。再进一步你可以增加蓝牙遥控、OLED显示实时参数、甚至尝试更复杂的控制算法。这个从零到一的过程所获得的不仅仅是一个小车更是对嵌入式系统、传感器、控制理论一次深刻而有趣的实践认知。