资讯动态

二轮平衡车源码解析:姿态解算、PID调参与避坑实践

发布时间:2026/10/15 11:33:42 来源:尧图企业网站定制
简介面向正在DIY二轮平衡车或入门嵌入式姿态控制的开发者这里集中了一份本科阶段通过调试的完整方案覆盖STM32硬件设计、电路连接与三类常见滤波算法源码可直接作为个人小车上手的参考资料。软件部分均编译通过包含互补滤波版、卡尔曼滤波版和DMP版可对照学习不同姿态解算方法在真实底盘上的差异也方便直接烧录后观察数据波形、调整PID参数。硬件资料同步给出电路原理图、接线方式和元器件说明从主控、电机驱动到传感器均有对应内容能减少从原理图到实物装配的踩坑环节。压缩包整体约148.18MB主要文件为工程源码、原理图及连线说明文档目录按硬件与算法分模块整理查找高效。目前已有3128人学习下载适合想在平衡车上做课程设计、竞赛调试或毕业设计参考的读者直接基于这套资料可快速搭建属于自己的平衡小车。1. 二轮平衡车全套源码先想清楚你拿到的是什么东西如果你搜到这份「二轮平衡车全套源码超全」大概率不是想买一台成品小车而是想自己从头搭一台能直立、能跑、能转向的平衡车。我见过太多人下载源码后第一件事就是打开主函数找main()然后对着上千行代码发呆。我建议你换个顺序先花半小时确认这套源码的硬件依赖、控制链路和文件组织再决定是直接烧录还是按自己的板子改。因为二轮平衡车看起来是个玩具但它背后的姿态解算、电机驱动、PID 串联这三段链路和工业级运动控制没本质区别坑也几乎一样。这套源码能帮你省掉从零开始写滤波和解算的时间但前提是你得知道每一段代码对应哪个硬件、哪个参数、哪个故障。我用一线调试的真实视角把这个源码拆成五块讲清楚硬件框架怎么对号入座、姿态解算代码怎么读、三段 PID 怎么调、踩过哪些坑、以及最后怎么验证你的车真的稳定。2. 硬件框架与源码目录约定先看接口再谈移植2.1 这套源码默认的硬件配置MCU、IMU、电机编码器绝大多数二轮平衡车源码基于 ARM Cortex-M 系列 MCU常见的是某款主流 M4 内核芯片主频在 168MHz 上下。IMU 几乎都是六轴三轴陀螺仪加三轴加速度计常见的型号是 MPU6050 或 ICM20602用 I2C 接口挂在 MCU 上。电机驱动用直流减速电机加霍尔编码器编码器信号接在定时器的编码器模式引脚上PWM 输出接在另一组定时器通道上。电池一般是 11.1V 三节锂电经过降压给 MCU 供电直接给电机驱动模块供电。你在看源码前先把硬件手册翻出来对一下引脚定义。源码里的bsp.c或board.h这类文件会定义引脚映射比如编码器 A 相接哪两个引脚、PWM 输出接哪两个引脚、I2C 接哪两个引脚。如果你的板子和源码默认的不一样第一步不是改代码逻辑而是先把这些宏定义改对否则后面一切现象都是玄学。我一般会先画一张表格把自己的板子引脚和源码默认引脚逐一对照功能源码默认引脚我的板子实际引脚备注I2C SCLPB6PB10注意上拉电阻I2C SDAPB7PB11注意上拉电阻左电机 PWMPA8PA0TIM1_CH1右电机 PWMPA9PA1TIM1_CH2左编码器 A/BPB3/PB4PB3/PB4定时器编码器模式右编码器 A/BPB5/PB6PB5/PB6定时器编码器模式串口打印PA2/PA3PA2/PA3调参时用这张表看起来简单但能避免一半的翻车。编码器引脚接错是最隐蔽的因为车能走但不稳你很难第一时间想到是引脚映射错了。2.2 源码目录结构与关键文件不要盲目打开main.c拿到全套源码后先看目录结构。常见做法是分成BSP板级支持包、App应用层、Control控制算法、Fusion姿态融合、Driver外设驱动。有的源码会混在一起但控制算法一般放在单独目录。你优先打开下面这几个文件其他可以先不看. ├── BSP │ ├── bsp_motor.c # 电机 PWM 输出与编码器读取 │ ├── bsp_mpu6050.c # IMU 驱动I2C 读写、寄存器配置 │ └── bsp_uart.c # 串口打印与调试指令解析 ├── App │ ├── app_balance.c # 主循环状态机调度解算和控制 │ └── app_control.c # 三段 PID直立、速度、转向 ├── Fusion │ └── mahony.c # 姿态解算互补滤波 / 四元数 ├── Control │ ├── pid.c # 位置式和增量式 PID │ └── pid.h # PID 结构体定义 └── main.c # 入口初始化硬件并启动调度先读pid.h和mahony.c因为这两个文件决定了控制链路的基本形态。main.c反而是最后才看的东西——它只是把初始化函数按顺序调一遍真正干活的是app_control.c里每 5ms 或 10ms 执行一次的控制任务。2.3 移植到自己的板子三步改引脚别改算法我自己的习惯是先把bsp_motor.c、bsp_mpu6050.c里的引脚宏全部改掉然后编译看报错再逐一调试外设。下面这段是bsp_motor.c里最常见的一段引脚配置代码void BSP_Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能定时器1和对应GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // 配置 PA8、PA9 为复用功能推挽输出(PWM输出引脚) GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStructure); // 将 PA8、PA9 连接到 TIM1 的 CH1、CH2 复用功能 GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_TIM1); // 定时器1配置预分频和周期决定PWM频率 TIM_TimeBaseStructure.TIM_Prescaler 84 - 1; // 84MHz/84 1MHz TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 1MHz/1000 1kHz PWM TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // PWM通道配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须使能主输出 }这段代码的关键在于两个参数TIM_Prescaler 84 - 1和TIM_Period 1000 - 1它把 PWM 频率定在 1kHz。平衡车电机 PWM 频率一般建议 1kHz10kHz低于 1kHz 电机会有啸叫高于 10kHz 驱动管的开关损耗会增加。用 IGBT 或大功率 MOSFET 驱动时尤其注意频率别太高。参数说明TIM_Prescaler是预分频值84 - 1表示把 84MHz 的时钟除以 84得到 1MHzTIM_Period是计数周期1000 - 1表示计数到 1000 才溢出于是最终频率是 1MHz / 1000 1kHz。你把这两个值同时乘以 10就能得到 10kHz 的 PWM但前提是驱动模块支持这么高的开关频率。2.4 编码器读取与方向判断为什么车轮反转时你会怀疑人生编码器代码是另一个移植重点。霍尔编码器输出两路正交方波定时器工作在编码器模式时硬件会自动根据 A、B 相先后顺序累计计数。源码里读取编码器值的函数通常是int16_t BSP_Encoder_Get_Left(void) { return (int16_t)TIM3-CNT; // 读取计数寄存器 } void BSP_Encoder_Clear_Left(void) { TIM3-CNT 0; // 清零供速度环周期累加使用 }注意两个细节一是TIM3-CNT是有符号的正负方向由 A、B 相先后决定不要把它当无符号数处理二是每次读取完要清零因为速度环需要的是“一个周期内转了多少圈”而不是累计总量。我见过有人不清零导致速度环积分越积越离谱车原地抖动排查了一下午。如果两个轮子的编码器读数方向不一致会出现什么现象车能直立但你推它一下它不会前进而是原地打转。原因是左右速度环反馈方向相反控制器以为一个轮子超速、一个轮子欠速于是持续输出相反的修正力矩。这个现象极其容易误判为 PID 参数问题其实是硬件方向问题。最简单的修正方式是在初始化时判断方向或者干脆在读取函数里把其中一个取反int16_t BSP_Encoder_Get_Right(void) { return -(int16_t)TIM4-CNT; // 如果右轮方向反了在这里取反 }3. 姿态解算源码走读互补滤波那条主链路的参数都在哪3.1 为什么平衡车不用卡尔曼滤波而用互补滤波姿态解算是平衡车的灵魂。小车直立的前提是知道自己倾斜了多少度而倾斜角不能直接测量只能通过陀螺仪积分或者加速度计反算。陀螺仪短时间准但会漂移加速度计长时间稳但怕震动互补滤波就是把两者按频率互补地融合在一起低频信加速度计高频信陀螺仪。卡尔曼滤波在原理上更优但它在嵌入式上有个实际问题需要构造状态转移矩阵和测量矩阵维数多了计算量大而且几个噪声协方差矩阵的初值调起来非常玄学。平衡车这个场景动态范围不大、模型相对固定互补滤波的精度已经足够而且代码短、容易调参。所以这套源码几乎可以肯定用的是互补滤波或它的四元数版本。3.2 读懂 Mahony 互补滤波核心代码就三行下面这段是 Mahony 互补滤波的核心它用加速度计修正陀螺仪积分带来的漂移void Mahony_Update(float gx, float gy, float gz, float ax, float ay, float az) { float norm; float vx, vy, vz; float ex, ey, ez; float halfT 0.005f; // 采样周期的一半按 5ms 控制周期算 // 1. 把加速度计测量值归一化得到重力方向单位向量 norm sqrt(ax*ax ay*ay az*az); ax / norm; ay / norm; az / norm; // 2. 用当前四元数推算重力方向向量 vx 2.0f * (q1*q3 - q0*q2); vy 2.0f * (q0*q1 q2*q3); vz q0*q0 - q1*q1 - q2*q2 q3*q3; // 3. 叉积求误差陀螺仪积分导致的重力方向偏差 ex ay*vz - az*vy; ey az*vx - ax*vz; ez ax*vy - ay*vx; // 4. 误差反馈到陀螺仪积分上Kp 决定修正速度 gx Kp * ex; gy Kp * ey; gz Kp * ez; // 5. 四元数一阶积分更新 q0 halfT * (-q1*gx - q2*gy - q3*gz); q1 halfT * ( q0*gx q2*gz - q3*gy); q2 halfT * ( q0*gy - q1*gz q3*gx); q3 halfT * ( q0*gz q1*gy - q2*gx); }这段代码里Kp是滤波器的核心参数。Kp越大加速度计对陀螺仪的修正越强姿态角跟随越快但对震动越敏感Kp越小姿态角越平滑但陀螺仪漂移会导致角度慢慢飘走。一般经验值在5.0f ~ 20.0f之间具体要看你的采样周期和 IMU 噪声水平。要注意的是这里的Kp和控制 PID 的 Kp 是两回事新手最容易混淆。halfT 0.005f表示采样周期的一半如果你把控制周期改成 2ms这里要改成0.002f否则四元数积分会不准确表现出来是静止时角度缓慢漂移。这个值改错的现象很隐蔽因为车刚开机时是稳的过几十秒才慢慢歪。3.3 从四元数到欧拉角翻滚角、俯仰角哪个是你的控制输入平衡车只需要一个方向的倾斜角通常叫俯仰角Pitch。四元数转欧拉角的代码基本长这样float q0, q1, q2, q3; // 全局四元数 float Get_Pitch(void) { // 从四元数计算俯仰角结果单位是弧度 float pitch asinf(-2.0f * (q1*q3 - q0*q2)) * 57.29578f; return pitch; }注意这个asinf的范围问题当俯仰角接近 ±90° 时asinf的输入接近 ±1此时角度分辨率变低而且可能产生跳变。平衡车正常的倾斜范围在 ±30° 以内不需要担心这个问题。但如果你把小车拿起来竖着转角度显示可能会突然跳一个很大的值这是正常的数学特性不是代码 bug。这里有一个非常常见的坑有些源码会用atan2算俯仰角有些用asin两者在小角度下结果接近但符号方向可能相反。如果你发现把车往前倾角度输出是负的而 PID 期望的是正的整个控制就是正反馈一上电就飞车。遇到这种情况不要急着调 PID先看看角度符号方向是否和你的电机正方向匹配。4. 三段串联 PID直立、速度、转向的控制链路与参数设定4.1 为什么是“串级”而不是“并联”直立环是内环速度环是外环二轮平衡车有至少三个控制目标保持直立、达到目标速度、按目标转向。这三个目标不是并列关系而是嵌套关系。直立环是内环它直接输出电机 PWM速度环是外环它输出的是角度目标值。也就是说速度环不是直接操纵 PWM而是告诉直立环“你要稍微往前倾一点”直立环再去执行这个倾斜指令。这个结构叫串级 PID内环快、外环慢。转向环相对独立它输出的是左右轮 PWM 的差值叠加在直立环的输出上。用公式表达就是PWM_Left Output_Pitch Output_Speed Output_Turn PWM_Right Output_Pitch Output_Speed - Output_TurnOutput_Pitch来自直立环Output_Speed是速度环输出叠加在角度目标上的量Output_Turn是转向环输出。这个结构决定了一个问题转向环不能单独工作它必须依赖直立环先立住。很多人在调完直立后直接调转向发现转向没反应其实是因为速度环还没调好角度目标一直在变转向量被淹没了。4.2 直立环 PID先调 P再调 DI 最好为零直立环是整台车第一个要调的环。它的输入是当前俯仰角输出是 PWM 修正量。直立环的控制目标是让角度回到 0°所以它本质上是一个角度 PD 控制器I 项通常不需要。int16_t Pitch_PID(float angle, float gyro_rate) { static float last_angle 0.0f; static float integral 0.0f; // 1. 比例项角度偏差直接决定修正力度 float P Pitch_Kp * angle; // 2. 微分项用陀螺仪角速度替代角度差分信号更干净 float D Pitch_Kd * gyro_rate; // 3. 积分项平衡车直立不需要积分抗静差靠角度本身就够 // 除非机械结构存在严重不对称才需要加一点点 I float output P D; // 输出限幅防止飞车 if (output PWM_MAX) output PWM_MAX; if (output -PWM_MAX) output -PWM_MAX; return (int16_t)output; }这里最关键的细节是微分项用的是陀螺仪角速度而不是角度差分。因为角度是从滤波输出的本身有延迟如果再对它差分相当于二次放大噪声。陀螺仪输出的是角速度它和角度微分是同一个物理量但噪声特性更好。这个细节决定了你的 D 项能不能调大而不产生震荡。调参顺序先给Pitch_Kp一个很小的值比如 5观察车能不能“软绵绵”地往平衡点靠。如果车完全没反应加大 Kp如果车开始高频抖动说明 Kp 太大或者缺少 D 项。当车能勉强立住但来回晃动时开始加Pitch_Kd每次加 0.5直到晃动明显减弱。这个过程大概需要半小时。4.3 速度环 PID把“目标角度”变成“目标速度”的反馈速度环的输入是当前速度与目标速度的差输出是角度目标值的修正量。它不需要直接输出 PWM而是修改直立环的平衡点。这是整台车最难调的部分因为速度环和直立环之间存在耦合你加一点速度环输出直立环就以为车要往前倒于是加大修正速度环又觉得速度不够继续加——这是极限环震荡的经典来源。int16_t Speed_PID(int16_t current_speed, int16_t target_speed) { static float integral 0.0f; float error target_speed - current_speed; // 1. 积分项累积速度偏差克服静差 integral Speed_Ki * error; // 2. 限幅积分不能无限大否则恢复太慢甚至过冲 if (integral 100.0f) integral 100.0f; if (integral -100.0f) integral -100.0f; // 3. 输出的是角度修正量单位是度 float output Speed_Kp * error integral; // 4. 限幅防止给的角度目标太离谱 if (output 15.0f) output 15.0f; if (output -15.0f) output -15.0f; return (int16_t)output; }速度环的output直接加到角度目标值上所以它的单位是“度”而不是 PWM 值。这就是为什么速度环限制了±15度——如果目标倾斜角超过 15 度车基本已经处于失控边缘了。速度环的正确调法是先让车在原地直立稳定然后手动把目标速度设为一个固定值比如 0.2m/s然后从小到大调Speed_Kp观察车是否开始缓慢加速。如果车前后震荡加速说明 Kp 太大如果车不动说明 Kp 太小。速度环积分项上限100不是拍脑袋定的。我测试过积分上限过大会导致车从静止启动时猛地往前冲因为积分项已经积累到一个很大的值即使速度已经达标它还要先“泄洪”。所以积分限幅要和小车的响应速度匹配底盘轻、响应快的车积分限幅要小一些。4.4 转向环 PID这是三个环里最容易忽略的转向环的输入是目标转向角速度与当前转向角速度的差输出是左右轮差值。这里不需要用陀螺仪的 Z 轴角速度直接用编码器左右轮速度差就能推算转向角速度int16_t Turn_PID(int16_t left_speed, int16_t right_speed, int16_t target_turn_rate) { int16_t current_turn_rate (right_speed - left_speed) / 2; float error target_turn_rate - current_turn_rate; float output Turn_Kp * error; // 转向环一般只用 P 项加 D 项容易引入噪声 if (output 100.0f) output 100.0f; if (output -100.0f) output -100.0f; return (int16_t)output; }有人问转向环的 D 项去哪里了其实转向 D 项就是左轮速度减去右轮速度再取反本质上和当前转向速率是同一个量用了反而容易振荡。转向环的核心是 Kp 和死区如果你发现车向左转一点然后回正来回摆动说明 Kp 偏大如果转向响应迟钝可以加一点点Turn_Kd但一般建议不需要。还有一个极容易被忽视的点转向环的限幅不能和直立环一样大否则在高速转向时左右轮差会把直立环的输出淹没导致侧翻。4.5 参数速查表从零调通一套车的参考区间给出一个参考参数表基于常见 12V 减速电机、轮径 65mm、车重 1.5kg 左右的小车。这张表的作用是让你有一个安全起点而不是直接照抄参数参考起点调整方向现象Pitch_Kp8.0加大则更硬过大会抖低频晃动加大 Kp高频抖动减 KpPitch_Kd1.2加大能抑制晃动过大会像齿轮一样卡顿晃动→加 Kd声音发闷→减 KdSpeed_Kp0.5控制速度响应快慢车慢加速→加大前后震荡→减小Speed_Ki0.05消除速度静差速度偏差持续存在→加大Turn_Kp3.0控制转向灵敏度转向迟钝→加大来回摆头→减小PWM_MAX800限幅飞车时调小动力不足时调大注意这套参数不是万能模板但它是一个能让你在二十分钟内跑起来的起点。调参的黄金法则是“每次只改一个参数改完记录现象”。否则两个参数互相掩盖你永远不知道当前车的问题到底是哪个环造成的。5. 全套源码落地避坑清单五条来自产线的血泪记录5.1 上电飞车根本不是 PID 问题而是电机方向反了现象一上电车轮猛转车像弹射一样冲出去速度极快根本不给调参机会。很多人第一反应是 Kp 太大赶紧把 Kp 往下调但即使调到 0车依然在冲。原因直立环输出正负和电机转动方向不匹配。控制器检测到车往前倾输出正 PWM 让轮子往前滚去“接住”车结果电机机械方向是反的轮子往后滚相当于车越往前倾、控制器越把它推倒形成正反馈。解决先做开环测试。把直立环输出旁路直接手动给电机一个固定 PWM确认左右轮前进方向一致然后把车放在地面扶正用手向前推一下同时看编码器数值变化和电机输出的符号是否“试图”阻止你推。如果不一致在代码里改电机方向宏或在编码器读取函数里取反。这是最优先要检查的一步没有之一。5.2 角度静止漂移不是滤波问题而是陀螺仪零偏没消除现象车静止放在桌面上用上位机看角度发现角度以每分钟几度的速度缓慢爬升或下降完全不动也在变。调滤波的Kp没用调到很大反而角度开始抖动。原因陀螺仪内部存在零偏bias即静止时输出不为零而是有一个小的固定偏移。四元数积分会把这个偏移累积起来表现为角度漂移。滤波器的Kp只能修正动态误差无法完全消除静态零偏。解决在初始化时做陀螺仪零偏校准。常见做法是上电后让车静止 1 秒读 100 次陀螺仪 Z 轴和 XY 轴数据取平均把这个平均值存下来之后每次读取陀螺仪时都减去这个零偏值。代码长这样float gyro_offset_x 0.0f, gyro_offset_y 0.0f; void IMU_Calibrate_Offset(void) { float sum_x 0.0f, sum_y 0.0f; int i; for (i 0; i 200; i) { sum_x Read_Gyro_X(); // 读取陀螺仪X轴原始值 sum_y Read_Gyro_Y(); // 读取陀螺仪Y轴原始值 delay_ms(5); } gyro_offset_x sum_x / 200.0f; gyro_offset_y sum_y / 200.0f; } float Read_Gyro_Calibrated_X(void) { return Read_Gyro_X() - gyro_offset_x; // 校准后陀螺仪数据 }参数说明校准样本数200和采样间隔5ms加一起是 1 秒时间太长用户体验差太短零点稳定性差。如果你校准后还是漂移检查是不是校准完成后车又被移动过或者 IMU 芯片离电机太近、受到发热影响。注意零偏是温度的函数车刚开机和跑十分钟后的零偏会不一样严谨的做法是在工作温度下校准但 DIY 场景用开机校准就够。5.3 编码器读数跳变导致速度环抽搐定时器初始化顺序有讲究现象速度环调好后车静止时电机有“哒哒哒”的轻微抽搐声看编码器数值静止时也在 ±3 以内跳变而不是稳定为 0。这个跳变像噪声一样被速度环积分后变成小幅震荡。原因编码器用的是定时器编码器模式但定时器初始化时计数器初始值不是 0或者编码器模式配置和引脚复用没设好导致在电机没动的情况下计数器自己跳。还有一个隐蔽原因是编码器引脚没有配置上拉输入导致悬空噪声被当成有效电平。解决首先把编码器引脚的模式从GPIO_Mode_AF改成GPIO_Mode_IN并带上拉GPIO_PuPd_UP因为编码器模式并不需要复用功能输出但必须有上拉输入才能保证静止电平稳定。其次在配置完定时器后手动将TIMx-CNT 0清零一次。第三如果跳变仍然存在检查编码器线缆是否和电机 POWER 线并行走线若是将编码器线单独走线远离电机。void BSP_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 编码器引脚用输入模式带上拉 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOB, GPIO_InitStructure); // 复用配置为定时器功能 GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_TIM3); GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_TIM3); // 清除计数寄存器保证初始状态为0 TIM_SetCounter(TIM3, 0); }我把这段代码放在这里是想提醒你编码器读取跳变在平衡车项目中不是疑难杂症但它特别影响调参效率。你要先花十分钟把信号质量搞定再去调速度环否则每一次调参都会被“哒哒哒”的抽搐声干扰判断。我自己的习惯是任何 PID 调试之前先把所有外设的原始数据打印出来确认干净再做控制。5.4 电池电压不同手感完全不一样这是参考电压的锅现象刚充满电时平衡车有力、容易控制玩半小时后明显变软甚至无法直立。或者同一套代码换一块电池测试参数全乱之前调好的 Kp 现在全错。原因一部分原因是电池电压下降导致电机最大转速下降PWM 满占空比的输出扭矩变小另一部分原因是 PWM 的参考电压变了如果电机驱动的逻辑电源是从电池直接取的那么同样的 PWM 占空比在 12.6V 和 11.1V 下实际电压差了 12%电机响应差了接近 20%。解决这是物理限制改代码没法完全解决。但你能做两件事。第一把电机的 PWM 输出改成“电流模式”或者“电压补偿模式”即在代码里读取电池电压算一个补偿系数乘到输出上。第二更实用的做法是把直立环的输出限幅设置为随电压动态变化低电压时减小限幅防止电机因为扭矩不足而被 PID 输出推死。常见做法是加一层电池电压采集代码长这样uint8_t Calc_Battery_Compensation(void) { float vbat Read_Battery_Voltage(); // ADC采集电池电压 float comp 1.0f; if (vbat 11.0f) { comp 0.85f; // 低压时降低输出限幅 } else if (vbat 10.5f) { comp 0.70f; // 更低压时限制更明显 } return (uint8_t)(PWM_MAX * comp); // 返回动态限幅值 }如果你的项目要求“插上电就能玩”不考虑电池电压那就要把调试时的电池状态固定在一个区间比如电量在 40% 到 60% 之间调参这样至少保证大部分时间的表现居中可用。我自己的习惯是把电池电压挂在串口调试画面上调参前先看一眼电压电压太低不调因为那是浪费时间。5.5 滤波太“柔和”导致车永远在晃动Kp 与控制周期不匹配现象车的角度波形看起来非常平滑但车就是一直小幅度前后晃动怎么调直立环 Kp 和 Kd 都无法消除。看代码发现滤波器的Kp是 2.0而控制周期是 10ms。原因互补滤波的Kp决定了加速度计修正的速度。当控制周期快滤波Kp太小角度信息里包含了过多的陀螺仪积分成分导致角度响应跟不上实际物理倾斜。表现出来就是角度波形平滑但实际车已经来回晃了好几轮滤波后才慢慢跟上。解决控制周期和滤波Kp需要匹配。控制周期 5ms 时Kp用 810 起步控制周期 10ms 时Kp要相应增加因为每次修正的间隔变长了需要更大的修正量。一个快速判断方法是把车拿在手里做快速俯仰运动观察串口打印的角度是否跟手。如果角度有明显的滞后说明Kp太小。如果你发现无论怎么调Kp角度总有稳定的一拍延迟那么问题可能在四元数更新的halfT上——它必须和控制周期的实际值一致。6. 把源码跑透的最后一步用数据回放习惯验证你的整车状态调完这三段 PID你的车应该能直立、能前进后退、能转向了。但这时候你只完成了 80%剩下的 20% 是验证整车状态是否“健康”。我强烈建议你养成的第一个习惯是每次调完参数把俯仰角、角速度、左右编码器速度、目标速度这五路数据通过串口打出来并录制下来。你不需要一帧一帧看数据只需要关注三段波形第一段是俯仰角在 ±2° 以内是否稳定第二段是左右轮速度是否在目标速度附近且没有持续振荡第三段是转向输出是否频繁打满限幅。这三段波形能告诉你车的“体质”如何。参数都调好了但俯仰角波形有间歇性尖峰那可能是编码器线缆偶尔受到电机干扰速度波形有稳定的正弦波那可能是速度环带宽太低和直立环产生了耦合。接下来做四个验证动作。第一个是推车恢复测试车立住后用手向前推一下看它能否在 2 秒内恢复直立且不前后甩动。第二个是斜坡驻车测试找一块 5 度左右的斜坡让车停上去看它会不会自己溜走。第三个是满载测试在车顶放一个 500g 的负载看它会不会变得迟钝或开始震荡这能检验速度环的鲁棒性。第四个是电池低电压测试把电池玩到接近没电确认低压补偿逻辑正常工作。我自己的习惯是准备一个独立的调试记录本每改动一组参数就记录参数值、室温、电池电压、现象描述。这个习惯救过我很多次。有时候你调了一小时参数发现车越来越差回到上一个“能直立”的版本时如果没有记录你只能凭记忆往回瞎摸。有了记录你三分钟就能恢复到最优参数。另外每次只调一个环、只改一个参数、改完立刻验证是效率最高的路虽然听起来慢实际上半小时的成果比各种参数乱飞的三个小时还大。最后说一个玄学以外的建议所有参数在源码里都要有注释说明修改意图。比如#define Pitch_Kd 1.2f // 底盘偏重已调大过轻车需要减半。半年后你翻回来看这些代码会发现这些注释比代码本身更值钱。希望这些从源码里榨出来的经验和教训能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑