简介一份基于51单片机的两轮自平衡小车完整设计实例面向嵌入式初学者与电子爱好者重点展示如何结合PID控制、蓝牙遥控与姿态采集实现小车直立平衡。压缩包共140个文件主体为Keil工程文件含.c/.h源码、STARTUP.A51启动汇编、hex烧录文件及uvproj/uvopt工程配置另有obj/lst编译中间文件与bak备份整体仅477KB结构紧凑便于直接打开工程学习或二次开发。项目从硬件接线到软件控制均有覆盖围绕倾角检测、PID参数整定、蓝牙指令解析等关键环节展开是理解经典8051单片机应用与闭环控制的好素材。目前已有1361人浏览学习适合用来对照实物调试代码、梳理平衡车控制流程也可作为课程设计或电子竞赛的参考方案。1. 51单片机自平衡小车为什么“先把小车立起来”最容易翻车51单片机自平衡小车是入门级“倒立摆”控制里最典型的落地项目一块 STC89C52 级别的芯片只靠读姿态、算 PID、发 PWM就能让一辆两轮小车自己站稳。很多人从压缩包里拿到代码编译能过、接线也对小车却起不来或者疯狂原地打转——问题往往不在代码本身而在电机响应、传感器方向和结构重心这些“代码之外”的地方。这篇笔记把从拆包到站稳的完整路径拆给你看覆盖硬件选型、姿态解算、PID 参数整定和典型踩坑适合想真正把源码跑起来而不是只停留在“编译成功”阶段的入门者。2. 先想清楚原理再接线倒立摆模型与核心硬件选型自平衡两轮小车的物理本质是一个“倒立摆”重心在轮轴上方天然的失稳状态决定了系统必须靠持续动态调整才能保持直立。这不是上电就能静止的东西它的每个控制周期都在“跌倒”和“扶正”之间循环只是频率足够快看起来像是站稳了。理解这一点你才明白为什么电机响应速度、传感器采样率和控制周期三者是绑在一起的而不是“随便找个直流电机就能调出来”。2.1 两轮自平衡的核心原理角度偏差与角速度叠加控制的自变量是“倾斜角度”。小车往哪边倒轮子就往哪边加速让支撑点追着重心跑。经典位置式 PD 控制是最常用的起步方案直立环输出 Kp × 当前倾角 Kd × 角速度。这里有个容易被新手误解的点只用比例项的小车会来回摆动因为角度到了 0 但角速度还在往另一侧冲加上角速度项也就是微分项相当于给系统的运动加了“阻尼”让角度接近 0 时速度也归零。加速度计能测角度陀螺仪能测角速度两者结合才是完整输入所以姿态传感器是必须的不是可选项。2.2 硬件选型清单与匹配关系常见做这辆小车的最小硬件组合如下表我按“起步难度”和“后续扩展”分别标注方便你判断手里的材料能做什么部件常见型号作用选用理由与注意点主控STC89C52RC 或兼容 51跑控制算法、发 PWM价格低、资料多但算力有限不适合跑卡尔曼滤波姿态传感器MPU6050输出加速度与角速度自带 DMP 可解算四元数但 51 读 DMP 比较吃力多用原始数据配合互补滤波电机驱动TB6612FNG 优先L298N 备选把 PWM 信号转成电机电压TB6612 压降低、响应快适合高频启停L298N 压降大发热明显电机带霍尔编码器的 N20 减速电机输出动力并测转速编码器是为速度环准备的只做纯直立可以先用普通 N20但后续加速度环还得换电源7.4V 锂电池组供给电机与逻辑电路必须分开供电或共地处理否则电机启停会拉崩单片机电压车架两层亚克力板加电机支架固定所有部件重心越低越稳电池横放贴近底板别竖着绑这里有一个取舍要提前讲清楚51 的 PWM 频率不高通常用定时器模拟输出 500Hz 到 1kHz 的 PWM。N20 减速电机配 1:30 的减速比在小车上响应够用但如果你用的是 1:48 甚至更高减速比会发现同样的 PID 参数下车反应“肉”很多角度还没纠回来就已经倒了。2.3 接线原则先别焊死用杜邦线把系统跑通我第一次做这辆小车时直接焊死了所有线结果电机方向反了只能拿烙铁拆线折腾了一晚上。现在我的习惯是第一步全部用杜邦线连接跑通直立后再整理线路、热缩管固定。接线按功能分成三组每组独立测试供电组电池正极接驱动板 VM电池负极接驱动板 GND同时接单片机 GND共地单片机 VCC 用 5V可以取自驱动板的稳压输出或单独降压模块。控制组单片机 PWM 引脚接驱动板 AIN1、AIN2、BIN1、BIN2 和 PWMA、PWMB传感器 SCL、SDA 接单片机的 P2 口引脚自定但必须确认代码里的引脚定义。电机组左电机两根线接 AO1、AO2右电机接 BO1、BO2先不固定方便后面交换相序。接线完成后先做两个简单测试一是用极短的程序让电机分别正转、反转确认驱动逻辑二是把 MPU6050 的原始数据打印到串口倾斜板子看角度数据是否跟着变。这两个测试通过了才说明硬件链路完整再进入控制代码阶段。3. 从 MPU6050 到 PID让小车“自己站起来”的代码骨架代码部分我按“采集姿态 → 滤波 → 控制 → 输出”四段拆开。压缩包里通常已经有完整的工程文件但你能看懂骨架、会改参数比直接烧录重要得多。下面给的是从零搭的最小可运行逻辑不是某个特定工程的全量代码但核心结构和参数说明是通用的。3.1 模拟 I2C 读 MPU6050跳过 DMP直接拿原始数据51 没有硬件 I2C 外设通常用 GPIO 模拟时序。读取 MPU6050 时不需要开 DMP直接读加速度和陀螺仪寄存器即可。初始化 MPU6050 的常见片段void MPU6050_Init(void) { I2C_Start(); I2C_SendByte(0xD0); // MPU6050 地址写操作 I2C_SendByte(0x6B); // PWR_MGMT_1 寄存器 I2C_SendByte(0x00); // 设置为 0解除休眠 I2C_Stop(); I2C_Start(); I2C_SendByte(0xD0); I2C_SendByte(0x1C); // ACCEL_CONFIG 寄存器 I2C_SendByte(0x00); // 加速度量程 ±2g对应 16384 LSB/g I2C_Stop(); I2C_Start(); I2C_SendByte(0xD0); I2C_SendByte(0x1B); // GYRO_CONFIG 寄存器 I2C_SendByte(0x08); // 陀螺仪量程 ±500°/s对应 65.5 LSB/(°/s) I2C_Stop(); }逻辑说明三段写操作分别完成“唤醒模块”“设定加速度量程”“设定陀螺仪量程”。量程的选择影响后面换算系数——±2g 对应 16384±500°/s 对应 65.5这两个数会出现在把你的代码里如果改量程必须同步改换算。读取时注意加速度和陀螺仪数据都是 16 位有符号数高字节在前。连续读取时寄存器地址会自动递增所以只需把起始地址设为 0x3B 即可一次性读完 6 轴原始数据。我一般把读到的数据累计多次求平均再做换算比如每轮控制周期读 5 次取均值能在不大幅降低采样率的前提下减少噪声。3.2 互补滤波51 上比卡尔曼更现实的姿态解算选择卡尔曼滤波在这个平台上不是不能用而是 51 的算力跑完一次完整的 6 维矩阵运算后控制周期会被拖慢到十几毫秒以上反而让小车更难稳。互补滤波的思路很简单加速度计算出的角度在长时间尺度上可信但短期噪声大陀螺仪积分出的角度短期准确但会随时间漂移。两者互补代码量小且稳定。角度计算公式如下float angle_filtered; float gyro_y_rate; // 陀螺仪绕 Y 轴角速度单位 °/s #define ACCEL_SCALE 16384.0f #define GYRO_SCALE 65.5f #define T 0.010f // 控制周期 10ms angle_filtered 0.98f * (angle_filtered gyro_y_rate * T) 0.02f * angle_accel;参数说明0.98 和 0.02 是权重实际含义是“信任陀螺仪多一些用加速度计纠正漂移”。系数越大短期的角度变化越平滑但长期漂移修正越慢系数过小角度会带进较多加速度计噪声表现为小车高频抖动。10ms 的控制周期是 51 上比较常见的折中值如果你用 12T 模式跑 12MHz 晶振这个周期内还能剩下一些时间做串口输出和速度环。3.3 直立环 速度环串级 PID 的最小可运行骨架只靠角度 PD 的小车能站稳但会缓慢朝一个方向跑要让它原地镇定还得加一个速度环作为外环输出修正量叠加到直立环的目标值上。串级结构是“速度环输出 → 修改目标角度 → 直立环输出 PWM”。下面是我常用的代码骨架参数需要按你的机械结构调不要直接套#define KP 300 // 直立环比例系数 #define KD 10 // 直立环微分系数 #define PWM_MAX 7200 // 定时器装填上限对应占空比 int Balance(float angle, float gyro_y) { int pwm; static float angle_bias 0.0f; // 速度环累积修正 angle_bias speed_control_output; // 速度环输出累加到目标角 pwm (int)(KP * (angle angle_bias) KD * gyro_y); if (pwm PWM_MAX) pwm PWM_MAX; if (pwm -PWM_MAX) pwm -PWM_MAX; return pwm; }参数说明KP 的量纲是“PWM 装填值每度”KD 是“PWM 装填值每度每秒”。PWM 输出采用定时器装填值而不是百分比是因为定时器产生的 PWM 更容易做到 1kHz 左右的高频率。这里的 angle_bias 用静态变量累积速度环输出等于让小车在往一个方向加速时通过“主动倾斜一点”来反向减速。积分累积的风险必须提一句如果速度环持续输出angle_bias 会无限增大最后小车直接倒下。所以我通常会给 angle_bias 加一个 ±2° 的限幅这个值不大但对直立来说已经足够改变运动方向。3.4 PWM 输出与电机方向让“倾角 → 轮速”正确对应PID 算出来的 pwm 值有正有负但电机驱动芯片需要用方向引脚和占空比分开控制。常见的处理是把正负值拆成方向与速度void Motor_Output(int left_pwm, int right_pwm) { if (left_pwm 0) { AIN1 1; AIN2 0; PWMA left_pwm; // PWMA 是定时器装填值 } else { AIN1 0; AIN2 1; PWMA -left_pwm; } // 右电机同理BIN1/BIN2 PWMB }逻辑说明这一段看起来简单但方向对应关系错了会让小车“越倒越追不上”。判断标准是——把小车往前推两个轮子应该主动往前滚去追重心。如果推一下轮子反而后退把 AIN1/AIN2 或者 BIN1/BIN2 交换一下即可。我习惯先在代码里故意写死一个方向烧录后用手推小车验证错了就改引脚定义而不是改机械接线。4. 调试避坑与常见问题排查从“狂转”到“站稳”的几处关键门槛这一章直接给排查记录。每一条都是我在实际调试中遇到过的现象按“现象 → 原因 → 处理”写给你省得你重复踩。4.1 现象上电后轮子狂转小车像“发疯”一样抽动原因最常见的是角度换算错误。加速度计读到的是原始整数 LSB如果你忘记除以 16384角度算出来会是实际值的几十倍PID 输出瞬间饱和轮子自然疯狂转。另一个常见原因是控制周期没校准T 写 0.01 但实际循环耗时 20ms滤波和微分项整体失真。处理先把角度值直接通过串口发到电脑上把小车放在水平桌面确认显示的角度在 0 附近且随倾斜线性变化。如果数值是几千的整数优先检查 LSB 换算。确认角度没问题后再检查控制周期实际耗时用 GPIO 翻转一个引脚示波器量一下两次翻转的间隔没有示波器就在循环里累加计数器配合秒表估算。4.2 现象小车来回大幅度摆动幅度越来越大直到倒掉原因这是典型的“比例项太大、微分项不足”系统在临界点附近振荡发散。KP 太大让轮子响应过猛KD 太小起不到阻尼作用。处理先把 KD 清零KP 从小往大加加到小车开始持续振荡记住这个 KP 值。然后恢复 KD从小往大加加到振荡明显减弱、小车能撑住 1 秒左右。最后微调 KP增加 KP 让响应更硬但每次只能加原来值的 5% 左右加完观察 5 秒以上。这个流程是有效的跳过它直接套别人的参数往往越调越乱。4.3 现象小车能站稳但持续朝一个方向加速跑直到撞墙原因这是纯直立环没有速度环的典型症状。直立环只负责“不倒”不负责“不动”只要有一点角度偏置或重心偏移小车就会朝着一个方向加速。另一个隐藏原因是陀螺仪零偏——MPU6050 静止时陀螺仪输出不为 0积分出来的角速度误差会让角度持续累积。处理加速度环并做陀螺仪零偏校准。上电后保持在静止状态连续读取 100 次陀螺仪数据取平均作为零偏值存到全局变量后续每次读数都减掉它。这个校准只需要在初始化时做一次效果立竿见影。如果加了速度环仍然跑偏检查角度限幅是否起作用以及编码器方向是否装反。4.4 现象小车低频慢速晃动像“喝醉”一样偶尔抖动停顿原因多数是 PWM 频率过低或电池电压下降导致的驱动力不足。51 用定时器产生的 PWM 如果频率只有 100Hz 左右电机会有明显的声音和顿挫感控制力矩不连续。电池电压从 8.4V 掉到 7.2V 后电机响应速度下降PID 参数不再匹配。处理把定时器 PWM 频率提到 1kHz 左右装填值的范围相应调整。换用带 3.3V 逻辑的驱动芯片时注意单片机的 5V 逻辑电平能否直驱必要时加电平转换或确认驱动模块兼容 5V 输入。另外电池尽量用内阻小的锂电不要用干电池——干电池在大电流驱动下电压跌落明显小车会表现为“时好时坏”。5. 进阶技巧从“会站”到“能走”——串级 PID 整合与无线调参到这里小车已经能原地站稳了但距离“能走能转向”还差最后一步把速度环和转向环整合进同一套控制循环并解决调参效率问题。下面两个技巧是你真正把这辆车做成成品的关键。5.1 串级 PID 整合把速度环和转向环串进同一个 10ms 周期速度环不必每次控制循环都计算常见做法是每 10 次直立控制周期计算一次速度环避免编码器测速与直立抢占算力static int speed_integral 0; int speed_control_out 0; void Speed_Control(int target_speed, int current_speed) { float err target_speed - current_speed; speed_integral err; if (speed_integral 200) speed_integral 200; if (speed_integral -200) speed_integral -200; speed_control_out (int)(KP_S * err KI_S * speed_integral); }转向环更简单用遥控或串口给一个目标转角转向环输出直接叠加到左右轮 PWM 差值上。三环叠加顺序是左轮 PWM 直立环输出 速度环输出 转向环输出右轮 PWM 直立环输出 速度环输出 − 转向环输出。注意各环的输出限幅要分开放避免转向环饱和时把直立环的调节余量吃光。5.2 无线调参蓝牙串口救命的实战习惯PID 参数整定是需要反复试错的过程。每次改参数都要重新编译烧录再蹲下来把小车按住的整个循环至少浪费一分钟遇到一个棘手的振荡一天调不了几次。我后来把串口收发接到手机蓝牙模块上留了一组调试用变量列表的解析接口在手机蓝牙串口 App 里输入“KP300”就能在线改参数输入“READ”回读当前值。这组接口在压缩包里不一定有但值得你自己加算是投资回报率最高的几十行代码。最终的嵌入式调试习惯是先在代码里预留串口协议解析框架再调参等所有参数确定后把调试接口留在串口而不用删掉方便以后移植到别的车上。严格说这已经是工程习惯而不是高深算法但对 51 这种资源紧张的平台它才是决定你能不能在一天内把车调稳的胜负手。我自己从第一辆 demo 到现在最大的教训就是别迷信“拿别人的代码一烧就稳”稳压全靠亲手调出来的对车架的感知。希望这串经验帮你在同样的坑前停住脚。本文还有配套的精品资源点击获取