简介平衡小车自主直立控制项目完整资料包面向嵌入式开发与自动控制学习者以实际可运行的工程帮助弄懂平衡车的闭环控制原理。资源共1420个文件以C语言源程序.c/.h为主另有启动汇编、链接脚本、Keil工程配置、txt说明文档及PDF原理讲解等压缩包大小约36.17MB。代码覆盖陀螺仪和加速度计数据读取、卡尔曼滤波姿态融合、PID控制器设计以及电机PWM驱动等关键环节工程目录按模块划分便于快速定位和学习同时包含编译调试所需的hex、axf、map文件可对照分析程序执行过程还能学习PID参数整定与滤波器协方差调整等调试细节。已有1746人学习下载适合在此基础上进行二次开发或用作课程设计、毕业设计参考既能理解控制参数对系统稳定性的影响也能积累嵌入式系统调试经验是理论与实践结合的优质资源。1. 拿到一台「两根轮子立起来」的小车先看它解决了什么问题第一次看到这版平衡小车源码时我脑子里冒出的是大学实验室里那些整天倒在地上、电池还没耗尽就疯狂抽搐的失败作品。这版《平衡小车car》不是玩具级 Demo它是一台能真正原地站稳、抗推、能遥控前进后退转弯的两轮自平衡小车。整车以一颗常见的主控 MCU 为核心搭配六轴姿态传感器和双路带编码器直流减速电机靠串级 PID 把「随时要倒」的物理系统硬生生摁在平衡点上。适合谁正在做课程设计、电子竞赛或者刚入坑机器人控制但不想从零调参的人。它把「姿态解算 → 直立控制 → 速度环 → 转向环」这条完整链路都写好了你拿到手不是看热闹而是能直接编译、烧录、上电看它站起来然后一步步把参数调成你自己的车。下面我按拆解顺序把硬件架构、核心代码、调试方法和二次开发空间一次讲透。2. 系统架构与技术选型为什么这版源码能直接落地2.1 硬件电路从传感器到电机驱动的信号链路先看硬件选型。这版小车的主控用的是 STM32F103 系列芯片具体到板子就是常见的「最小系统板 电机驱动扩展板」二合一结构。姿态传感器是 MPU6050六轴三轴加速度 三轴陀螺仪通过 I2C 总线挂在主控的 PB8/PB9 上。电机是带霍尔编码器的直流减速电机左右各一个编码器输出接在主控的定时器输入捕获通道上用来测轮子转速。驱动芯片是 TB6612FNG比老式 L298N 效率高发热小两路 H 桥刚好驱动两个电机。电源方案是两节 18650 锂电池串联7.4V 经过稳压模块分别给主控5V/3.3V和电机驱动供电。这里有个细节很多人第一次容易搞错编码器和电机共地。如果编码器的 GND 没和主控 GND 连在一起读出来的转速数据会像抽风一样乱跳这个问题我在后面避坑章节还会提到。信号链路是这样的MPU6050 把加速度和角速度原始值通过 I2C 发给主控 → 主控跑姿态解算得到当前倾角 → 串级 PID 算出一路 PWM 占空比 → TB6612 放大驱动电机 → 编码器把实际转速反馈回来形成闭环。整个链路的核心不是某个硬件多贵而是数据在每个节点怎么被处理、怎么组合成控制量。2.2 软件架构任务分层而不是一把梭打开源码包你会发现工程结构不是单一 main.c 堆到底而是按功能拆成了模块。目录大致是这样BalancedCar/ ├── Hardware/ │ ├── mpu6050.c/.h │ ├── motor.c/.h │ ├── encoder.c/.h │ └── bluetooth.c/.h ├── Control/ │ ├── pid.c/.h │ ├── filter.c/.h │ └── attitude.c/.h ├── App/ │ ├── main.c │ └── control_task.c └── System/ ├── stm32f10x_it.c └── delay.cHardware 层管底层驱动Control 层做算法App 层把两者串起来。这个分层的价值在于你想换一个姿态传感器只需要改 Hardware/mpu6050.c上层 Control 完全不用动你想把直立 PID 改成 LQR 或者模糊控制也只需要在 Control/pid.c 里做替换。这就是「可二次开发」和「能跑就行」的分水岭。主循环里跑的是 1ms 定时中断——所有控制逻辑都塞在定时器中断里而不是用 while(1) 轮询。为什么平衡车是典型的不稳定系统控制周期必须固定且足够短。1ms 的控制周期对应 1kHz 控制频率是两轮自平衡车比较稳妥的起点。主循环只负责处理蓝牙指令、OLED 显示和低电量检测这类非实时任务。这种「中断里做控制主循环做外设」的做法是嵌入式实时系统的基本功也是这版源码值得学的地方。3. 核心算法拆解从传感器原始数据到电机 PWM 的完整链路3.1 姿态解算MPU6050 原始数据怎么变成可信的倾角MPU6050 直接读出来的是加速度计的 XYZ 三轴值和陀螺仪的三轴角速度。这两个数据单独用都有问题加速度计在静止时很准但小车一震动高频噪声会被放大陀螺仪短时间内积分出来的角度很平滑但积分漂移会随时间累积。这版源码用的是一阶互补滤波思路很朴素但非常有效// filter.c - 互补滤波核心代码 float complementary_filter(float acc_angle, float gyro_rate, float dt) { float filtered_angle; // alpha 取值 0.95表示信任陀螺仪 95%信任加速度计 5% filtered_angle 0.95f * (last_angle gyro_rate * dt) 0.05f * acc_angle; last_angle filtered_angle; return filtered_angle; }这里的acc_angle是从加速度计算出来的角度计算方式是用atan2(acc_y, acc_z)把重力加速度分量投影到与地面垂直的轴上。gyro_rate是陀螺仪绕车轴方向的角速度单位是 rad/s。dt是两次采样间隔在这个工程里是 0.001 秒。为什么一阶互补滤波够用因为平衡车的控制周期是 1ms在这个时间尺度上陀螺仪的积分漂移还来不及累积到不可接受的程度而加速度计的噪声也能被低权重压住。滤出来的角度虽然有轻微滞后但 PID 控制器对相位滞后有一定的容忍度。如果你想上卡尔曼滤波效果会更好但代价是代码复杂度翻倍、运算时间变长对 STM32F103 这种主频 72MHz 的芯片来说1ms 周期内跑卡尔曼是有点紧的所以我建议先用互补滤波把车调稳再考虑换算法。3.2 PID 串级控制为什么单个 PID 拉不稳一辆车如果只用一个 PID——直接用倾角误差算 PWM——会出现什么情况车轮会疯狂往倾倒方向加速然后过头然后往反方向加速来回震荡最后摔在地上。原因很简单倾角变化是有惯性的只用倾角误差做反馈相当于一个只有位置环没有速度环的系统阻尼不够。这版源码采用的方案是直立环 速度环 转向环三环串级// control_task.c - 串级 PID 控制任务 // 1. 读取姿态角 float angle get_attitude_angle(); float gyro_rate get_gyro_rate(); // 2. 直立环 PD 控制 float balance_pwm Kp_balance * (target_angle - angle) - Kd_balance * gyro_rate; // 3. 速度环 PI 控制输出作为直立环的目标角度修正 float speed_error target_speed - current_speed; float speed_pwm Kp_speed * speed_error Ki_speed * speed_error_integral; float tilt_adjust speed_pwm; // 速度环输出修正目标倾角 // 4. 转向环 P 控制输出叠加到左右轮差速 float turn_pwm Kp_turn * (target_turn - turn_rate); // 5. 合成电机输出 int16_t motor_left balance_pwm tilt_adjust - turn_pwm; int16_t motor_right balance_pwm tilt_adjust turn_pwm;直立环用 PD 控制P 项负责把车拉回垂直位置D 项用陀螺仪角速度来抑制震荡——相当于给系统加阻尼。速度环是 PI 控制它的输出不是直接叠加到 PWM 上而是调整直立环的目标角度。这个设计是精髓车要往前走就让它前倾一点点重力自然把它往前拉而不是靠电机硬推。转向环就简单了P 控制直接做差速。注意看代码里的符号直立环的 D 项是减号速度和转向的符号要分别配合左右轮的转向定义。我第一次移植的时候就是没搞清符号方向导致小车一上电就往一个方向猛冲后来用串口把每个环节的计算值打出来逐个比对符号才发现问题。调参顺序是先调直立环的 Kp 和 Kd 让车能站住再加速度环让车不往一个方向溜最后加转向环。千万不要三个环同时调不然你根本不知道是哪一环在捣乱。3.3 避坑与排查调平衡车时我踩过的五个真实大坑坑一MPU6050 数据跳变倾角一下从 0 变到 180 度现象串口打印的 angle 值偶发出现完全错误的数据比如小车明明倾了 20 度输出却是 140 度。原因I2C 读取 MPU6050 时没有做数据校验。MPU6050 的寄存器读取是分两次的如果两次读取之间发生了中断抢占高字节和低字节可能来自不同时刻的采样组合出来的原始值完全错误。解决给 I2C 读取函数加临界区保护在读取期间关闭中断或者在读出数据后加一个合理性判断——相邻两次角度差超过某个阈值比如 30 度就丢弃本次数据用上一次的值。源码里用的是后者在 attitude.c 里有一个if(fabs(angle - last_angle) 0.5f) continue;这样的判断。坑二电机发出高频啸叫车轮不转或轻微抖动现象上电后电机嗡嗡响用手摸能感觉到震动但车轮就是不肯转起来。原因PWM 频率太低落在了人耳听觉范围内。电机驱动对 PWM 频率不敏感但电机的电感会发出声音。更关键的是频率太低会导致电机在每一个 PWM 周期内电流波动大低速时扭矩不稳定。解决把定时器的 PWM 频率设置到 10kHz 以上。在 motor.c 里改时基预分频参数把 72MHz 时钟分频到 10kHz 或 20kHz具体做法是TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_Period 100 - 1;72MHz / 72 / 100 10kHz。改完之后啸叫消失低速控制也顺滑很多。坑三左轮和右轮转速不一致小车走着走着画弧现象给同样的 PWM 占空比左轮明显比右轮转得快车往一边偏。原因两个电机的制造公差不同绕组电阻、减速箱摩擦力、磁铁强度都有差异。这是直流电机的通病不是代码问题。解决源码里在 encoder.c 处做了一个简单校准——用堵转或者空载测试测出两个电机在相同 PWM 下的实际转速比然后在电机输出处乘以一个修正系数。常见做法是给慢的那一侧加一个固定增益补偿比如motor_left_pwm pwm * 1.05f。更工程化的方案是在速度环里给左右轮分别设不同的速度目标以右轮编码器值为基准左轮做跟随。坑四上电瞬间小车猛冲一下然后倒地现象每次按复位键小车先往一个方向猛冲然后失去平衡倒地。原因启动瞬间 MPU6050 还没完成初始化第一次读到的姿态角是随机值可能偏了很大的角度导致直立环输出了满占空比的 PWM。解决加一个启动延时和零点校准。代码里在 main 函数初始化完 MPU6050 后先让小车保持水平静止采集 50 组加速度数据取平均写入 offset 寄存器再延时 500ms 让系统稳定。这个操作在 attitude.c 的mpu6050_calibrate()里实现跑一次大概耗时一两秒。从那以后我每次调新硬件都会强制走一遍这个校准流程。坑五蓝牙遥控时灵时不灵偶尔断连现象手机发指令小车有时响应有时不理人隔半米就断。原因蓝牙模块的串口波特率配置和主控不匹配或者主控串口中断优先级太低被控制中断频繁打断导致丢数据。解决先把两边波特率统一到 9600 或 115200然后在 stm32f10x_it.c 里把串口接收中断优先级提到最高抢占优先级为 0保证蓝牙指令过来时能立刻被处理。另外在蓝牙解析代码里加一个简单的帧格式——每包数据以0xAA 0x55开头加校验字节避免错位解析。这版源码的遥控指令格式是0xAA 0x55 CMD CRCCMD 用 0x01 前进、0x02 后退、0x03 左转、0x04 右转、0x05 停止。4. 编译、烧录与调试让小车在你桌面上站起来4.1 工程导入与环境搭建的关键步骤源码工程基于 Keil MDK 开发环境目标芯片是 STM32F103C8T6。你先安装 Keil MDK 5 并装好对应器件包然后直接打开工程文件.uvprojx第一次编译时会自动把 CMSIS 库包含进来。编译前需要检查三处设置第一在 Options for Target 里确认芯片型号选对如果是 C8T6RAM 只有 20KB某些优化选项要看情况开。第二把 Use MicroLIB 打上勾这个选项会让 C 库精简很多减少 flash 占用。第三烧录器如果是 ST-Link在 Debug 选项卡里选 ST-Link Debugger如果用的是串口 ISP 下载那要先把 boot0 拉高、boot1 拉低进系统存储器模式。编译通过后烧录。烧录成功后别急着把 boot0 跳线拆掉先打开串口调试助手波特率设成 115200看能不能收到 MPU6050 的初始化日志。这版源码在mpu6050_init()里会打印MPU6050 init success和检测到的偏移量。如果看到的是乱码先查波特率是否匹配如果什么输出都没有查串口 TX/RX 是否接反——这是最常见的翻车场景。4.2 参数整定从「倒地」到「站稳」的操作路线图拿到工程后我建议你按下面的顺序调参每步之间不要跳步第一步只调直立环。把速度环和转向环的系数先设成 0源码里Kp_speed 0, Ki_speed 0, Kp_turn 0这样小车只受直立环控制。然后把车拿在手里让它稍微偏离垂直观察电机的反应如果电机往偏离方向加速说明 Kp 的符号反了把Kp_balance取反。如果电机往恢复方向推但很弱就逐步加大Kp_balance直到小车能短暂站立——哪怕只有一两秒也算成功。接着从 0 开始加Kd_balance每加一点就用手推动小车看抗扰动能力。你会发现 Kd 加到位后小车的「梆硬感」出来了推一下它立刻顶回来。第二步加速度环。直立环能稳住之后小车会往一个方向溜。把这个现象解决的方法是设一个target_speed 0让速度环以「当前实际速度」为误差来修正目标倾角。Ki_speed的作用是消除稳态误差——如果车总是在往右溜积分项会逐渐积累一个反向修正。注意Ki_speed不宜过大不然小车会周期性地前后摇摆像在跳舞。第三步加转向环。转向环最简单Kp_turn决定了转弯灵敏度。调试的时候在蓝牙指令里发左转指令观察车是否在原地转弯、转弯半径是否过大。如果车完全不动检查左右轮 PWM 合成时转向项的符号有没有加反。第四步整体细调。这一步比较玄学——因为直立环和速度环之间存在耦合调完速度环后直立环的稳定裕度可能被吃掉。常见做法是回调几次每次修正幅度不超过 10%直到小车能稳定站立超过 5 秒且用手推一下后能在 1 秒内恢复直立。另有一招非常实用把控制周期从 1ms 改成 2ms然后重新调参你会发现直立环的 Kp 可以大幅加大——控制周期长了系统的相位裕度会下降但这个过程中你能感受到 PID 参数和控制周期之间的内在关系这对理解平衡控制的本质很有帮助。4.3 用串口和 OLED 做实况观测别靠猜靠数据调试平衡车最大的痛点是「看不到系统内部状态」。这版源码把调试彩蛋藏在串口输出里在control_task.c里有一段被条件编译包裹的调试打印代码把DEBUG_ENABLE这个宏改成 1它会以 100Hz 的频率输出angle, target_angle, pwm_left, pwm_right四个变量。把它们用串口助手存成 CSV再用 Excel 或者 Python 画出来你能直观看到倾角曲线和 PWM 输出曲线的关系。比如当你发现 PWM 输出已经饱和接近满幅但倾角还在增大说明 Kp 跟不上得加大 Kp当你发现倾角在平衡点附近高频震荡说明 Kd 不够或者控制周期太长当你发现倾角有一个缓慢的直流偏移说明速度环的 Ki 没有收敛。这套「看曲线调参」的方法比闭着眼睛试参数高效十倍。我一般会在调试时同时开着 OLED 显示实时倾角和当前控制模式这样手不离车就能确认状态。5. 二次开发与改造从「会站」到「随便玩」如果说前面所有内容都是「把车调稳」那这一章聊的是「让车按你的想法动」。这版源码的模块化结构让它的扩展成本很低以下三个方向我认为性价比最高。方向一加蓝牙遥控逻辑。源码里bluetooth.c已经实现了基本的指令收发但你完全可以扩展协议——比如加一个速度挡位切换低速/中速/高速用0x06到0x08三个指令每个挡位对应不同的target_speed上限和Kp_speed缩放比例。常见做法是加一个全局的speed_scale系数在速度环输出时乘上去避免每个挡位单独维护一套 PID 参数。// 追加在蓝牙解析后的指令处理处 case 0x06: // 低速挡 speed_scale 0.5f; target_speed_max 100; break; case 0x07: // 中速挡 speed_scale 1.0f; target_speed_max 200; break; case 0x08: // 高速挡 speed_scale 1.5f; target_speed_max 350; break;方向二把数据录下来回放。在调试模式里加一个「记录 10 秒姿态数据到 flash」的功能玩的时候可以记录一段颠簸路段的姿态变化拿回电脑上离线分析。这比现场看串口更能发现问题因为现场你根本来不及看数据。方向三换成自己的硬件。如果步进电机换成无刷电机或者传感器换成 ICM20602只需要改 Hardware 层对应驱动文件的接口函数把返回值对齐到现有上层接口上。比如get_attitude_angle()返回浮点角度get_gyro_rate()返回浮点角速度只要这两个接口不变Control 层完全不用动。这就是设计时留好抽象层的红利。调车的最后阶段我的一个习惯是把整份参数记录成表格写上「这组 PID 是在什么电池电压、什么地面摩擦条件下调出来的」。原因是电池电压从 8.4V 掉到 6.0V同样的 PID 参数表现会明显不同——我见过一台车满电时立得很稳快没电时疯狂点头就是因为 PWM 占空比对应的电压变低了等效增益变了。从那以后我每次调新硬件都用标签纸把关键参数贴在电池仓里条件变了就重调一组存档。做机器人控制参数永远是用来实验的不是用来供奉的。希望这篇拆解能让你手上的平衡车少摔几次早日站起来。本文还有配套的精品资源点击获取