简介:基于ARM Cortex-M3内核STM32F103与MPU6050的自平衡小车源码工程,覆盖PID控制、姿态传感与电机驱动等完整闭环。源码面向嵌入式初学者及机器人爱好者,尤其适合希望上手ARM/STM32开发、理解平衡算法与PID参数整定的人群。工程共56个文件,压缩包2.34MB,以C源文件、H头文件、Keil工程文件uvprojx为核心,其中.c/.h为源码,.uvprojx为工程配置,.hex/.axf为可直接烧录固件,.map/.lst供内存与汇编分析,同时包含o/crf中间文件,可清晰查看从源码到编译产品的完整流程。已有303人学习下载。通过研读和烧录,可掌握MPU6050的I2C读取、姿态解算、PID调节与PWM电机控制;借助User、Object、Listing等目录和完整编译输出,能快速定位代码、分析ST库函数调用与链接配置,便于扩展蓝牙遥控、避障功能,或用于课程设计、毕业设计参考。 说起来,做自平衡小车这事儿,我前前后后折腾了快三周才把源码真正跑顺。中间炸过单片机、烧过驱动、摔过车架,最后还是老老实实回到源码层面,一行一行抠才找到问题。所以这篇东西我不打算只贴一堆代码让你抄——网上开源的平衡车源码一抓一把,但真正值钱的,是搞清楚这段源码为什么这么写、硬件该配什么、PID参数该怎么调,以及那些让小车一上电就抽风的隐藏坑到底在哪。
这篇内容适合谁看?如果你手里已经有一套自平衡小车套件(或者打算自己买散件拼),想彻底读透、改透、调透一套能跑的源码;如果你之前照着别人的教程抄完代码,结果小车要么原地打转、要么点头如捣蒜、要么直接躺平,那你来对地方了。我会从源码的整体框架,到核心的姿态解算和PID控制逐行拆解,再到实际操作中的调试参数和避坑经验,把我踩过的坑全部摊开讲。
1. 自平衡小车源码的整体框架:先看清手里这套代码是什么
拿到源码第一件事,别急着编译烧录,先打开工程目录,把里面的文件结构捋一遍。绝大多数开源自平衡小车源码(不管是基于STM32还是Arduino)的文件组织方式都差不多,我拿最常见的STM32版本来举例,你对照自己的代码稍微调整预期就行。
1.1 源码里通常会有哪些模块
一套能稳定站起来的自平衡小车源码,至少要包含下面这几块核心逻辑:
- 传感器驱动层:负责读取MPU6050(最常见的六轴惯性传感器)的原始数据,包括三轴加速度计和三轴陀螺仪。这个层面做的事情是I2C通信、寄存器配置、数据校准。
- 姿态解算层:把加速度和角速度的原始数据融合成当前车身的真实倾角。这一层是整个源码的灵魂——融合算法算出来的角度准不准,直接决定小车能不能站起来。
- 控制算法层:通过PID(比例-积分-微分)控制器,根据当前倾角计算出电机应该输出多大的力来把车身“拉回来”。
- 电机驱动层:把PID算出来的控制量转换成实际的PWM波形,通过H桥驱动芯片控制直流减速电机的转速和方向。
- 数据处理与接口层:包括串口打印调试信息、蓝牙遥控指令解析、电池电压检测、各种滤波和限幅保护逻辑。
我见过很多新手拿到源码直接看main函数,这是最大的误区。main函数里就是个while(1)死循环,你盯着看半天也看不出门道。正确的打开方式是先看文件列表,找到上面这几个模块对应的文件,然后按“传感器读取 → 姿态解算 → PID计算 → 电机输出”这条数据流向去读。这条数据流就是自平衡小车的“神经反射弧”,每秒钟要跑几百上千次,任何一环卡顿或者数据不对,小车立刻躺给你看。
1.2 控制周期与主循环:为什么小车“反应”这么快
自平衡小车的核心秘密在于控制频率。人能够站稳是靠内耳前庭感知倾斜,大脑以极低频率发指令修正——但机器人不行,它必须在几十毫秒甚至几毫秒内完成一次“感知-决策-执行”闭环。
看源码时重点关注主循环里的延时时间,或者定时器中断的配置周期。大多数自平衡小车的控制周期在5到10毫秒之间,也就是说每秒钟要执行100到200次完整的“读传感器→算角度→算PID→给PWM”。这里面有一个非常关键的技巧:姿态解算的MPU6050读取频率,最好和控制周期保持同步或者整数倍关系。如果传感器读到的是旧的姿态数据,PID控制器算出来的输出就是滞后的,小车会表现出“慢半拍”的抖动。
我用的这套源码里,控制周期是通过定时器中断实现的,中断周期5ms,主循环只做低优先级的串口输出和遥控指令处理。这个设计是对的——PID控制绝不能放在主循环里跑,因为主循环里任何一句阻塞代码(比如串口打印用阻塞式发送)都会让控制周期抖到无法预测,小车就会莫名其妙地突然抽一下然后趴下。
2. 核心源码拆解:姿态解算到底在算什么
自平衡小车最大的认知门槛不在PID——PID那三步谁都能背出来——而在姿态解算。很多新手栽在这里,觉得代码晦涩看不懂,其实本质上就一句话:把加速度计和陀螺仪的原始数据,换算成一个干净、稳定、可以参考的倾角。
2.1 加速度计与陀螺仪的分工与合并逻辑
MPU6050能输出两类数据:三轴加速度(单位g)和三轴角速度(单位度/秒)。它们各自有致命的短板和独特的优势:
- 加速度计测角度,理论上没问题:重力方向始终指向地心,通过测量重力加速度在X轴和Z轴的分量,用反正切函数就能算出倾角。但加速度计对运动加速度极其敏感——小车一加速、一震动,测出来的“倾角”就全是噪声,根本没法直接用。
- 陀螺仪测角速度,积分可以得到角度:陀螺仪短时间内的数据非常稳定平滑,不受运动加速度干扰。但它有零点漂移和积分累积误差——你静止不动,积分出来的角度也会慢慢飘走。
姿态解算的全部意义,就是把这两个“各有毛病”的传感器数据融合起来:短时间相信陀螺仪(因为平滑),长时间相信加速度计(因为没有漂移),最终得到一个既不抖又不飘的倾角。
最经典也最简单的融合算法叫互补滤波,源码里通常会长这样:
// 互补滤波 float angle = 0.98 * (last_angle + gyro_rate * dt) + 0.02 * accel_angle;这个公式你可以理解成一个“信任度分配”:98%的信任给陀螺仪积分出来的角度,2%的信任给加速度计实测的角度。0.98和0.02这两个系数决定了对陀螺仪的依赖程度,系数越大,角度越平滑,但跟踪速度越慢;系数越小,角度越灵敏,但越容易被震动噪声污染。
我调这个系数的时候踩过一次坑:把互补系数设成0.99,觉得越平滑越好。结果小车站起来以后像喝醉了酒一样,左右晃得越来越厉害——因为角度融合反应太慢了,等PID控制器发现车身已经倾斜,实际的倾斜早就超出了能纠正的范围。后来把系数强行拉到0.90,车身反而稳了不少。所以这个参数不是越极端越好,大多数源码默认的0.95到0.98之间的值,通常是最稳妥的起点。
2.2 卡尔曼滤波与四元数:源码里更高级的算法
比互补滤波高级一点的,是卡尔曼滤波。有些开源源码里嵌了一段看起来很唬人的五六个方程的矩阵运算,那就是卡尔曼滤波。它的思路比互补滤波复杂:建立一个系统状态模型,预测下一个时刻的姿态,然后用加速度计的观测值去修正预测值,每次修正还会带上不确定性估计(协方差矩阵)。
卡尔曼滤波的实际体验是:角度曲线更平滑、抗震动能力更强、相位延迟更小。但代价是代码复杂度高、需要调协方差矩阵参数、在低性能单片机上跑起来CPU占用更高。而四元数方法本质上是另一种姿态表达方式,常配合Mahony或Madgwick算法使用。这种算法计算的不仅是单一倾角,而是完整的3D姿态,适合做四轴无人机或者需要全姿态输出的场景。用在自平衡小车上,属于杀鸡用牛刀。
我的建议很直接:做自平衡小车,互补滤波完全够用。你上手就把互补滤波调通、理解透,远比抄一段卡尔曼滤波代码然后完全不会调参强。
2.3 零点漂移校准的源码逻辑:为什么小车静止时角度不是0
几乎所有源码里都有一段开机校准的代码,核心逻辑是上电后采集几百次陀螺仪的静态输出求平均,得到零漂值(gyro_zero),之后每次读取数据都减去这个零漂值。这段代码通常在初始化函数里,看起来不起眼,但非常关键——如果MPU6050的陀螺仪零漂没校准,角度会在静止状态下以每秒几度的速度漂移,PID控制器会把这种漂移当成“车身正在倾倒”,然后疯狂给电机下发修正指令,小车就会站在地上自己抽搐。
我以前图省事,直接把网上抄来的gyro_zero=0固定值写死进去,结果小车在平整桌面上放得好好的,突然就猛冲一下——这就是没校准零漂的典型症状。源码里那段校准代码,建议完全保留,不要动。
3. PID控制器源码逐行看:三段论法则的核心实现
PID控制是自平衡小车的“大脑决策层”。看懂PID源码的每一行,等于看懂了小车“保持平衡”这个行为本身。市面上自平衡小车源码大多用PD控制器(去掉积分项)就够了,因为平衡控制的目标是稳定在零点附近,比例项P提供回复力,微分项D提供阻尼,积分项I用在有恒定外力干扰的场景,实际效果往往弊大于利。
3.1 P项、I项、D项在平衡控制中各管什么
- P项(比例项):相当于弹簧——车身倾斜越大,回正力矩越大。P太小,小车软弱无力,站不直;P太大,小车硬如钢板,一有扰动就剧烈震荡。
- I项(积分项):用来消除稳态误差——如果小车始终向一边微微倾斜,积分项会逐渐累积额外的出力把车身推回水平。但积分项在自平衡场景下很容易引发“低频摆动”:小车好不容易站稳了,积分项却还在慢慢累积,导致车身缓慢左右摇摆。这就是为什么很多自平衡源码直接不用I项。
- D项(微分项):相当于阻尼——抑制角速度变化,防止小车在平衡点附近来回过冲。如果说P项是“拉的力度”,D项就是“刹车的力度”。D太小,小车会像钟摆一样越摆越大最终摔倒;D太大会引入高频噪声,让电机发出刺耳的嗡嗡声。
源码里PID计算的典型代码长这样:
// 自平衡车的PD控制 float balance_pwm = Kp * angle + Kd * gyro_rate;注意这里的写法:只用了比例系数乘以角度(P项)加上微分系数乘以角速度(D项)。这里的角速度直接用了陀螺仪的输出值,而不是把角度做微分——这是一个非常重要的实现细节,直接使用角速度比计算角度的导数更平滑、更实时。看懂这一行,你就明白了为什么源码里PD控制的D项效果这么好。
3.2 串级PID与直立环+速度环的配合逻辑
把小车放平了加一个速度控制后,源码通常会变成串级PID结构。
- 直立环(内环):控制倾角为0,是最核心的平衡环。
- 速度环(外环):控制速度为0,防止小车往一个方向越走越快。因为直立环只能控制姿态,如果车身受到一个轻微的持续推力,小车会一直朝那个方向加速走——速度环就是要把这种“漂移”拉回来。
实际源码里,直立环的输出(balance_pwm)会作为速度环PID的输入项之一,或者两个PID的输出做加权叠加。具体做法因源码而异,但你只要记住:平衡是基础,速度稳定是锦上添花。先把纯直立环调通,再加速度环。
速度环千万别调太猛,否则小车会像一个“执意往前走”的倔驴,你越拉它越走,来回拉扯中直接摔倒。我把速度环比例系数从0.1调到0.5,结果小车原地打转加疯狂冲刺,花了半小时才调回正常。从0.05开始慢慢加上去,是最稳妥的路径。
3.3 PWM输出限幅与电机死区补偿
源码里PID的输出值一般不是直接给到电机PWM引脚的,中间至少要做两件事:
- 限幅保护:把PID输出限制在PWM允许的范围内(比如-1000到+1000)。如果PID输出溢出,不让它直接截断到上限,而是做一些平滑处理,防止小车突然满速猛冲。
- 死区补偿:电机在低占空比下是不转的(因为有静摩擦和齿槽转矩),所以源码里通常会在PWM输出超过一定阈值后再加一个基础电压偏移,让电机“更容易启动”。
这两段代码一般在“电机输出”函数里,容易被忽略。我曾经遇到过一个问题:小车刚站起来就剧烈抖动,排查很多天,最后发现是我把PWM限制设得太宽,PID输出的微小抖动被人为放大了。限制设小一点,加上死区补偿,立竿见影。
4. 编码器数据与测速源码:为什么速度环离不开编码器
当你的自平衡小车加了速度环之后,代码里一定会多出一个重要的数据来源:电机编码器。编码器(一般H桥电机带霍尔编码器)用来测量电机实际转速,速度环PID靠这个转速反馈值来调节电机的输出。
编码器信号的读取在源码里通常是外部中断或编码器定时器模式实现的。STM32的定时器有专门的编码器接口模式(Encoder Mode),它把A相和B相信号接在定时器的两个通道上,硬件自动根据相位差判断正反转并计数。这个功能非常方便,代码里只需要初始化定时器、清零计数器、周期读取寄存器值,就能得到电机转速。
// 读取编码器计数值(STM32定时器编码器模式) int16_t encoder_count = TIM2->CNT; TIM2->CNT = 0; // 清零,方便计算下一周期的增量这段代码的关键在于周期读取:每隔一定时间(比如10ms)读取一次计数器的增量值,就是电机的转速。需要注意的是:计数器的读取清零要放在定时器中断里,与速度环的计算保持同步,否则读到的数据是乱序的,速度环就会表现得不稳定。我最初把编码器读取函数直接丢在main函数的while循环里,完全读不到稳定的速度数据,后来才意识到必须跟控制中断对齐。
编码器的数据质量决定了速度环的“视野”——如果编码器读数有噪声或者丢步,速度环就会误以为电机转速在波动,从而输出错误修正。硬件上要确认编码器的电源和信号线走线远离电机驱动的大电流线,否则电机的PWM电流会在编码器线上感应出巨大的噪声,导致计数乱跳。我在调试时发现左右轮编码器读数不对称,排查到最后才发现有一侧的信号线跟电机电源线绑在一起了,分开之后数据立刻干净了。
5. 调参与实测:从“原地躺平”到“稳稳站立”的完整过程
源码读完了,参数也了解了一部分,真正决定小车能不能站起来的是调参。这一节我直接讲实操,把你可能遇到的情况一步一步拆开。
5.1 调参前的硬件自检
为了不出现那种“程序看着没问题,小车就是死活站不起来”的困境,硬件自检是必须的。
- 确认MPU6050数据能正常读出。用串口或者OLED把角度实时打印出来,手持小车绕X轴前后倾斜,观察角度数值是否跟实际倾角一致、是否平滑、是否有跳变。
- 确认左右电机方向一致。把左右电机PWM引脚手动给定一个固定的PWM值,看看两个轮子是否同向同速转动。方向反了,小车永远不可能平衡。
- 确认电池电压。如果电池电压跌到电机最低工作电压以下,即使PID输出再大,电机也没劲把车身拉回来。我用的方案是两节18650串联供电,控制板和电机驱动分开取电,避免电机突然的大电流拉低控制板的电压导致单片机复位。
5.2 P、D参数的调节顺序和方法
一般来说,调直立环参数分四步走:
第一步,只给P,不给D。把D设成0,P从0开始慢慢往上加,观察小车反应。你会发现P很小的时候,小车完全站不起来,像一个软塌塌的布偶;随着P增大,小车能短暂“挣扎”起来,但会明显震荡。记录下这个临界P值——在这个值附近,小车能靠“蛮力”勉强回正。
第二步,在临界P值基础上加D。D从0开始逐步加,你会看到震荡逐渐衰减,车身从“疯狂摆动”变成“小幅抖动”,再变成“几乎站稳但轻微颤抖”。继续加D,颤抖会消失,小车能稳定站住一两秒。这时候就找到了一个能站稳的PD组合。
**第三步,回到P,在你觉得稳定的D值基础上微调P。**加P让反应更灵敏,减D让响应更柔和。我个人的经验是:P比临界值略大10%~20%,D尽量大但不引起高频噪声。D太大时会听到电机发出尖锐的“滋滋”声,那是D项在放大陀螺仪的高频噪声,就该往回减了。
**第四步,加入速度环。**速度环先从极小的P值(0.03~0.05)加起,观察小车是否缓慢朝某个方向漂移。增大速度环P值,漂移会逐渐被抑制,但调太大就会出现“来回逛”现象——小车来回走几步、停一下、再走几步,这种“逛游”就是速度环P过大的典型症状。这时应该适当减小速度环P,或者调整速度环的I项来解决。
5.3 常见异常现象与根源排查
我用表格把最常见的几种异常表现和对应的排查方向列出来,方便你对照自己的小车进行检查。
| 异常现象 | 可能原因 | 排查方向 |
|---|---|---|
| 上电后电机狂转,完全不受控制 | 角度方向反了;PID符号反了 | 串口打印角度,确认倾斜方向和角度符号是否对应 |
| 小车能站起来但剧烈左右震荡 | P过大;D过小 | 降低P,或适当增加D |
| 小车站起来后缓慢向一边倒 | 陀螺仪零漂未校准;速度环P太小 | 重启校准程序;加大速度环P |
| 小车点头(前后反复俯仰) | 控制周期不固定;传感器数据延迟 | 确认PID是否在定时器中断中执行;检查角度滤波是否过于滞后 |
| 轮子嗡嗡响,车身高频细抖 | D过大导致高频噪声放大 | 降低D,检查PWM驱动是否受到电源噪声干扰 |
| 小车往一个方向加速跑,最终摔倒 | 速度环未生效;方向反了 | 检查速度环PID符号和编码器方向 |
5.4 供电与机械结构对源码的隐性影响
这一条可能很多人没说过,但我必须强调:自平衡小车能不能调稳,7分靠代码,3分靠机械和供电。
- 机械结构上,重心越高越难调。如果车架设计成电池高高的立在重心上方,P和D会非常难调到稳定区间——因为类似于一个倒立摆,重心越高,恢复难度越大。
- 电机减速比也很关键。1:30以上的减速比(转速低、扭矩大)比1:10的更容易调平衡。螺母、联轴器的间隙也是致命的——齿隙过大,电机换向时会有空程,PID输出会“打滑”,导致控制无效。
- 电源方面,电机启动瞬间的电流冲击非常大,如果控制板和电机驱动共用一个电源,瞬间电压跌落会导致单片机复位——小车会表现为“站起来一瞬间就断电重启”。给控制板做好滤波(大电容),是很多老玩家常做的事。
6. 从“能站起来”到“能走起来”:源码的进阶改造思路
当你手里的自平衡小车已经能稳稳站住了,恭喜你完成了最难的部分。接下来如果你想让小车能遥控行走、能自动保持位置不漂移,就需要对源码做一些进阶改造。
6.1 增加上位机调参:串口解析PID参数
每次调参都要重新编译烧录,时间成本太高。我建议花一点时间,在源码的串口处理函数里加一个简单的参数解析协议:
// 简单串口协议示例:$P 110.5 2.5 0.05 0.001 // 分别设置 直立P 直立D 速度P 速度I if (strcmp(cmd, "P") == 0) { balance_kp = parse_float(); balance_kd = parse_float(); speed_kp = parse_float(); speed_kp = parse_float(); }这样你在电脑上打开串口助手,输入一行指令回车,小车参数就实时更新了,不用频繁插拔烧录器。调整到满意后,再把参数固化在源码里烧录进去。这个方法能省下好几个小时,属于源码调参的“必装外挂”。
6.2 蓝牙遥控与自定义控制模式
在源码里加蓝牙遥控,核心是接收字符串指令并解析。你可以把控制模式分成几个档位:平衡模式、速度环锁定模式(车体保持原地,不移动)、遥控模式(根据遥控器摇杆值叠加一个速度目标值)。每个模式本质上是切换控制环路的输入信号,而不是改变核心控制算法。
我在实际使用时发现,最快的遥控调试方法,是先用手机蓝牙串口发送一个“锁定速度=0”的指令,把小车的速度环目标值直接设成0,确认它能在原地保持不漂移;然后再慢慢叠加前进后退的目标速度。如果直接跳到遥控模式,速度环还没调好,小车就会乱跑,根本分不清是遥控问题还是控制问题。
6.3 数据记录与性能分析
进阶玩法是让小车通过无线串口把运行数据(角度、角速度、目标输出、实际转速)实时传回电脑,用串口绘图软件或Python脚本画出来。这能让你看到PID输出的实际波形,定位“为什么小车偶尔抽一下”这种偶发问题的根源。
印象很深的是,我发现自己小车在桌面上偶尔会突然往前窜一下,用数据回放才发现是MPU6050的I2C读取偶尔返回了错误数据,导致角度突变,PID瞬间输出了一个巨大的修正值。在那之后,我在源码的传感器读取函数里加了数据校验和重试逻辑,问题彻底消失。这种问题靠眼睛盯着小车根本看不出来,只有数据回放能暴露。
7. 写在最后的干货:我调参踩坑后形成的几个习惯
自平衡小车这个东西,源码只是起点,真正漫长的过程是“读懂—修改—调试—再修改”的循环。如果你现在正卡在某个环节,以下几个小习惯是我吃了不少亏之后才养成的,分享给你参考。
第一个习惯,每次只改一个参数。很多新手调参时又加P又加D又加速度环,小车状态一变就不知道是谁的功劳谁的过。我只改一个值,记录下现象,有需要再改下一个,宁可多烧几次程序也不跳步。
第二个习惯,保留一份“已知能稳定运行”的源码备份。每次尝试新参数前,先把当前能跑的版本打包存好。折腾坏了随时回退,调试心态会完全不一样——真的,炸到心态崩的时候,一个能回退的备份比什么都救命。
第三个习惯,调参之前先把MPU6050的数据用串口打出来看一眼。很多“小车一上电就发疯”的案例,根源只是传感器数据异常。先确认角度可信,再谈PID调参,能省掉大半天真。
自平衡小车源码的真正价值,不在于它能让你照着跑起来一台车,而在于它把“传感器读取—数据融合—闭环控制—执行输出”这整条嵌入式控制链路完完整整地展现在你眼前。读透一套源码,比盲目翻十套别人的代码都有用。老样子,有问题欢迎在评论区直接贴你的现象描述和参数截图,看到我都会回。
本文还有配套的精品资源,点击获取