1. 从零开始:搭建你的平衡小车硬件与基础感知
玩平衡小车,第一步永远是把硬件搭起来,把“眼睛”和“耳朵”装好。很多朋友一上来就想调算法,结果发现车都站不稳,代码也不知道该读什么数据,这就本末倒置了。我自己做第一台平衡车的时候,光是把MPU6050的数据读稳就折腾了一个星期。所以,咱们先别急,把地基打牢。
你需要准备的核心部件其实不多:一个Arduino主控(比如ESP32,性能足够且自带蓝牙Wi-Fi方便调试)、两个带编码器的直流减速电机、一个电机驱动板(比如TB6612FNG)、一个MPU6050六轴传感器,再加上车架、电池和一些连接线。硬件连接是第一步,这里有个坑我踩过:电机的编码器线和驱动线一定要远离,最好用双绞线或者屏蔽线,否则电机一转动产生的电磁干扰会让编码器计数乱跳,你的速度环永远调不准。我的经验是,电源走一边,信号线走另一边,地线要接好。
接下来是代码的“基础设施”。原始文章里给出了编码器中断计数的经典代码,这确实是核心。但我得补充几个实战细节。首先,volatile关键字绝对不能少,它告诉编译器这个变量可能在中断里被修改,要直接从内存读取,避免优化出问题。其次,那个portMUX_TYPE和portENTER_CRITICAL_ISR是ESP32等多核芯片在FreeRTOS环境下防止计数冲突的利器,如果你用的是Arduino Uno这种单核AVR芯片,直接用cli()和sei()关开全局中断也行,但ESP32上必须用这套“互斥锁”机制。
注意:中断服务函数(ISR)里要快进快出。像原文里
delayMicroseconds(10)这样的消抖,对于高质量编码器可能不是必须的,但如果你的电机比较廉价,机械抖动大,这个延时能救命。不过记住,在ISR里延时是“阻塞”的,会影响到其他中断的响应,所以时间要尽可能短。
把脉冲数转换成实际的角度或速度,是控制的前提。原文提到了CountperCircuit(每圈脉冲数)。这个参数必须实测!不要相信电机手册上的标称值,不同负载、不同电压下可能会有细微差异。我的方法是:把车轮悬空,让电机匀速转10圈,同时用代码记录中断计数总数,然后除以10,得到每圈的平均脉冲数。多测几次取平均,这个值越准,后面位置环和速度环的计算就越靠谱。
2. 经典之王:深入理解PID控制与三轮调参实战
PID,比例-积分-微分,大概是每个搞自动控制的人第一个学会的算法。它简单,但想调好,让它在小车上乖乖工作,里面全是经验。很多人调PID就是瞎试,Kp、Ki、Kd三个参数胡乱给,调半天车要么抽搐要么倒,最后说PID不行。其实不是算法不行,是方法不对。
PID到底在干什么?你可以把它想象成骑自行车。当你发现车开始往左倒(这就是误差),你会立刻把车把往右转一点(比例P的作用,误差越大,纠正动作越大)。但光转一下车把可能不够,车子还在慢慢倒,所以你持续地施加一个向右的力(积分I的作用,消除静态误差)。同时,你感觉到车子倒的速度越来越快(这是误差的变化率),你会提前加大向右转的力度来“刹住”这个倒的趋势(微分D的作用,预测未来,抑制振荡)。平衡小车的直立环,就是这么一个“骑自行车”的过程。
对于平衡小车,我们通常需要两个PID环:内环是速度环,外环是直立环(角度环)。直立环是保证小车不倒的核心,它直接读取MPU6050解算出的车身倾角,与目标角度(通常是0度)比较,通过PID计算输出电机的基本控制量。速度环则是为了让小车能静止在一点,或者按指令运动。它读取编码器计算出的车轮速度,与目标速度(比如0)比较,其输出会叠加到直立环的输出上。调试顺序必须是先调直立环,再调速度环。直立环没调好,车根本站不住,调速度环毫无意义。
下面是我总结的三轮调参法,亲测非常有效:
第一轮:粗调比例(Kp),找到临界振荡点。
- 将
Ki和Kd设为0。 - 从小到大地增加
Kp值(比如从1开始,每次翻倍)。 - 用手扶住小车,让它保持直立,然后上电,感受电机输出的力度。当
Kp增大到某个值时,你会发现小车开始高频剧烈抖动(不是倾倒,是抖动),这就是临界振荡。记下这个Kp值,称为Kp_max。 - 将最终使用的
Kp设为Kp_max的50%-70%。比如Kp_max是20,那么Kp可以先设为10到14之间。这时小车应该能基本站住,但可能会缓慢地向一个方向漂移。
第二轮:加入积分(Ki),消除静差。
- 保持
Kp为上一轮的值,Kd仍为0。 - 从小到大地增加
Ki(比如从0.01开始)。 - 观察小车是否还漂移。合适的
Ki能消除漂移,让小车稳定在一点。但Ki太大会引入积分饱和,导致小车在受到扰动后恢复缓慢甚至反向加速。通常,Ki的值会远小于Kp。
第三轮:加入微分(Kd),抑制超调,让响应更干脆。
Kp和Ki保持不动。- 从小到大地增加
Kd。 - 观察效果。合适的
Kd能让小车在快要倒下时“猛地”拉回来,减少摇晃次数,响应更敏捷。但Kd对噪声极其敏感(MPU6050的角速度是有噪声的!),太大会放大噪声,导致电机高频振动。强烈建议对陀螺仪角速度数据进行低通滤波后,再用于D项计算。
这里给一个Arduino上直立环PID的简化代码框架,包含了角度融合和滤波:
// PID参数 float Kp = 12.0, Ki = 0.5, Kd = 0.8; float error, lastError, integral, derivative, output; float targetAngle = 0.0; // 目标角度:直立 // 低通滤波系数 (0<alpha<1, 越小滤波越强) float alpha = 0.8; float filteredGyroY = 0.0; // 假设Y轴是俯仰角速度 void balancePIDControl(float currentAngle, float gyroY) { // 1. 低通滤波角速度,用于D项 filteredGyroY = alpha * filteredGyroY + (1 - alpha) * gyroY; // 2. 计算误差 error = targetAngle - currentAngle; // 3. 计算积分项(注意抗饱和) integral += error * dt; // dt是循环时间间隔 // 积分限幅,防止windup if (integral > 100) integral = 100; if (integral < -100) integral = -100; // 4. 计算微分项(使用滤波后的角速度,近似为误差变化率) derivative = -filteredGyroY; // 注意负号,角速度与角度变化趋势相反 // 5. 计算PID输出 output = Kp * error + Ki * integral + Kd * derivative; // 6. 输出限幅并驱动电机 if (output > 255) output = 255; if (output < -255) output = -255; setMotorOutput(output); // 7. 更新上次误差 lastError = error; }调PID是个耐心活,没有一蹴而就的参数。不同的小车重量、重心高度、电机力度,参数都不一样。我的习惯是,每调一个参数,就观察小车30秒到1分钟,看它的整体表现,而不是只看它一瞬间是否站稳。
3. 状态反馈:LQR控制算法的原理与Arduino实现
当你玩透了PID,可能会遇到一些瓶颈:PID参数之间互相影响,调起来费劲;或者小车性能要求高了,比如要它扛住更大的干扰,PID就显得有点力不从心。这时候,可以试试LQR(线性二次型调节器)。听名字很高大上,其实它的核心思想很美:用一个数学上最优的方式,自动计算出状态反馈的系数。
PID只关注“误差”这一个量,而LQR关注的是系统的全部状态。对于平衡小车,状态通常包括:倾角、倾角速度(角速度)、车轮位置、车轮速度。LQR的终极目标,是设计一个控制器u = -K * x。这里的x是状态向量(包含上面说的四个状态),K是一个行向量(也叫反馈增益矩阵),u就是我们的控制输出(电机电压)。LQR算法能帮你算出这个最优的K,使得某个性能指标(通常是状态偏差和控制能量消耗的加权和)最小化。
那么,怎么在Arduino上搞LQR呢?分四步走:
第一步:建立状态空间模型。这是最理论的一步,但也是基础。我们需要小车的线性化动力学方程。原文中给出了一个简化模型:θ'' = (g/L) * θ + (1/(m*L)) * u。其中θ是倾角,u是控制力。把这个二阶微分方程改写成状态空间形式。定义状态变量x1 = θ(倾角),x2 = θ'(角速度)。那么状态方程就是:
x1' = x2 x2' = (g/L) * x1 + (1/(m*L)) * u写成矩阵形式x' = A * x + B * u,这里的A和B矩阵就确定了。
第二步:离散化模型。我们的控制器是在单片机里离散运行的,每隔dt时间(比如5毫秒)计算一次。所以需要把连续的A, B矩阵转换成离散的Ad, Bd矩阵。对于采样时间短的情况,有个简单近似:Ad ≈ I + A*dt,Bd ≈ B*dt。更精确的可以用矩阵指数计算,但在Arduino上我们追求实用,近似通常够用。
第三步:求解LQR增益K。这是核心计算。需要解一个叫Riccati方程的玩意儿。在电脑上(用MATLAB或Python的control库)做这件事非常容易。你需要设定两个权重矩阵Q和R。Q惩罚状态偏差,R惩罚控制量大小。比如,你觉得倾角比角速度更重要,就把Q矩阵中对应倾角的权重设大;你觉得电机输出不要太猛,就把R设大一点。在电脑上算出增益K后,它就是一串数字,直接复制到Arduino代码里用就行。
第四步:在Arduino上实现状态反馈。这是最简单的一步。在每次控制循环中:
- 获取状态
x:从MPU6050得到倾角和角速度(x1, x2)。车轮位置和速度(x3, x4)从编码器得到。 - 计算控制量:
u = - (K1*x1 + K2*x2 + K3*x3 + K4*x4)。 - 把
u限幅后输出给电机。
// 在电脑上计算好的LQR增益,例如: float K1 = -12.5, K2 = -1.8, K3 = -0.05, K4 = -0.2; void lqrControl(float angle, float gyro, float wheelPos, float wheelSpeed) { // 计算控制量 u = -K*x float u = -(K1 * angle + K2 * gyro + K3 * wheelPos + K4 * wheelSpeed); // 限幅 if (u > 255) u = 255; if (u < -255) u = -255; // 驱动电机 setMotorOutput(u); }LQR的优势在于,一旦模型和权重确定,增益是全局最优的,性能通常比手动调的PID更均衡、更鲁棒。但它的缺点是严重依赖模型的准确性,如果小车模型和实际差别太大(比如重心变了),或者有未建模的动态(比如电机死区),性能会下降。这时候就需要更高级的算法了。
4. 预测未来:MPC控制算法的概念与轻量化实现探索
如果说LQR是“最优的现在”,那么**MPC(模型预测控制)**就是“最优的未来”。它是目前工业界和学术界都非常火的高级控制算法。MPC的核心思想可以概括为:在每一个控制时刻,基于当前状态和系统模型,预测未来一段时间内系统的行为,并通过优化计算出一系列最优的未来控制量,但只实施第一个控制量;到下一个时刻,重复这个过程。
这就像开车过弯,老司机(MPC)不会只看眼前(当前误差),他会预测未来几秒车的轨迹,提前规划好方向盘怎么打、油门怎么踩。对于平衡小车,MPC可以考虑电机力矩的极限(约束),提前规划控制动作,避免饱和,性能潜力巨大。
但是,MPC最大的挑战就是计算量大。它需要在每个控制周期内在线求解一个优化问题,这对于计算资源有限的Arduino来说是巨大的负担。不过,我们依然可以探索一些轻量化的实现思路,在小车上跑起来。
第一步:建立预测模型。和LQR一样,我们需要小车的离散状态空间模型:x(k+1) = Ad * x(k) + Bd * u(k)。这个模型将用于预测未来N步的状态。
第二步:定义优化问题。我们预测未来N个时刻(预测时域),希望在这段时间内,状态尽量接近目标(比如0),同时控制量不要太大。优化目标通常是一个二次型代价函数:
J = Σ [x(i)^T * Q * x(i)] + Σ [u(i)^T * R * u(i)]求和从当前时刻到未来N步。我们还要加上约束,比如电机输出u的上下限:-255 <= u <= 255。
第三步:简化求解。在Arduino上解带约束的优化问题不现实。一个实用的简化方法是:去掉约束,或者用近似方法处理约束。这样,无约束的MPC优化问题可以转化为一个最小二乘问题,甚至有一个解析解(如果预测模型是线性的且代价函数是二次型)。这个解析解可以预先在电脑上算好,变成一个固定的增益矩阵,在Arduino上就只剩下矩阵和向量的乘法运算,速度飞快。这其实就是一种特殊的“显式MPC”。
另一种更工程化的思路是使用线性MPC,并利用其最终形式可以转化为一个二次规划(QP)问题的特性。虽然Arduino上跑通用的QP求解器不现实,但对于像平衡小车这样的小规模问题,我们可以使用非常高效的专用求解算法,例如基于梯度投影的快速QP求解器,或者甚至用定点数运算来替代浮点数,进一步提速。我做过一个实验,将预测时域N设为5,在ESP32上使用简单的梯度下降法在线求解,控制周期可以做到10ms以内,小车能够稳定站立。
这里给出一个极度简化的MPC概念性代码框架,它忽略了约束求解,展示了核心流程:
// 假设预测时域 N=3 // Ad, Bd, Q, R 矩阵已在电脑上定义好 // 这里仅为示意,实际需要大量预计算 void simplifiedMPC(float currentState[4]) { float u_optimal[3]; // 未来三步的最优控制量 float cost = 1e9; float best_u0 = 0; // 我们最终只取第一个控制量 // 暴力搜索法(仅用于示意,实际不可行!) for (int u0 = -255; u0 <= 255; u0 += 10) { for (int u1 = -255; u1 <= 255; u1 += 10) { for (int u2 = -255; u2 <= 255; u2 += 10) { // 用模型预测未来三步状态 float x[4] = {currentState[0], currentState[1], currentState[2], currentState[3]}; float totalCost = 0; float u_seq[3] = {u0, u1, u2}; for (int i = 0; i < 3; i++) { // 计算代价: x^T*Q*x + u^T*R*u totalCost += computeStateCost(x, Q) + computeControlCost(u_seq[i], R); // 状态更新: x = Ad*x + Bd*u updateState(x, Ad, Bd, u_seq[i]); } // 寻找最小代价对应的第一个控制量 if (totalCost < cost) { cost = totalCost; best_u0 = u0; } } } } setMotorOutput(best_u0); }上面的暴力搜索只是用于理解MPC的“滚动优化”思想,实际中计算量爆炸。真正的实现需要用到更数学的方法(如二次规划求解)。对于想在小车上实践MPC的朋友,我建议先从现成的轻量级MPC库开始探索,或者采用“离线计算、在线查表”的显式MPC思路。MPC打开了最优控制的一扇大门,即使在小车上实现一个简化版,对理解先进控制理论也大有裨益。
5. 算法对决:PID、LQR、MPC的横向对比与选型指南
好了,现在我们手上有三把“武器”:经典的PID,基于状态反馈的LQR,以及能预测未来的MPC。到底该用哪个?这不是一个谁取代谁的问题,而是在不同的应用场景和资源约束下,选择最合适的工具。我根据自己多年的折腾经验,做了一个详细的对比表格,你可以一目了然地看到它们的区别。
| 特性维度 | PID控制 | LQR控制 | MPC控制 |
|---|---|---|---|
| 核心原理 | 基于当前及过去的误差进行反馈调节 | 基于系统全部状态的线性最优反馈 | 基于模型预测未来,并滚动优化未来控制序列 |
| 调参对象 | Kp,Ki,Kd三个参数,物理意义直观 | 权重矩阵Q和R,调整性能权衡 | 预测时域N、权重Q/R、约束条件 |
| 模型依赖 | 不依赖精确数学模型,鲁棒性强 | 高度依赖线性模型准确性 | 极度依赖模型精度,模型失配影响大 |
| 计算复杂度 | 极低,乘加运算,适合所有MCU | 低,仅需状态反馈乘法,适合嵌入式 | 很高,需在线求解优化问题,对算力要求高 |
| 处理约束能力 | 无法显式处理,靠输出限幅间接实现 | 无法显式处理,靠输出限幅间接实现 | 核心优势,可显式处理输入/输出/状态约束 |
| 性能潜力 | 简单系统可调至很好,复杂系统有瓶颈 | 在模型准确时,性能优于PID,更优 | 理论上限最高,能处理多变量、强耦合、带约束问题 |
| 实现难度 | 最容易,资料多,上手快 | 中等,需建模和离线求解Riccati方程 | 最难,涉及建模、离散化、优化求解 |
给新手的选型建议:
如果你是第一次做平衡小车,或者项目时间紧、资源有限(用Arduino Uno):毫不犹豫选PID。你的目标是让小车站起来、走起来,PID完全足够。把时间花在硬件调试和PID参数整定上,收获会更大。这是最稳妥、成功率最高的路径。
如果你已经玩转PID,想追求更优、更稳定的性能,且主控有一定算力(如ESP32):强烈推荐尝试LQR。它帮你摆脱手动调三个参数的纠结,通过调整Q和R的物理意义更明确(哪个状态更重要,控制量要不要省着用)。在模型不太离谱的情况下,LQR给出的性能往往更“干脆利落”,抗干扰能力也更强。
如果你是控制理论爱好者,想挑战前沿,主控性能较强(如STM32F4,甚至树莓派),并且小车有更复杂的任务(如轨迹跟踪、避障):可以深入研究MPC。MPC是解决带约束优化问题的利器。比如你的小车电机有明确的电压极限,或者你希望小车在保持平衡的同时,轮子位置不能超出某个范围(防止跑出桌面),MPC可以完美地将这些约束考虑到控制律中。虽然在小车上实现真正的MPC很有挑战,但这个过程本身对能力的提升是巨大的。
一个折中的高级玩法:串级PID + LQR/状态反馈。这是我个人很喜欢的一种结构。内环用LQR来保证小车的平衡(状态反馈响应快),外环用PID来做位置控制或者速度控制。这样结合了LQR的稳定性和PID的易调性。或者,直接用PID作为底层电机速度环,上层用MPC来给出速度指令。控制算法的世界不是非此即彼,灵活组合才是工程实践的精髓。
最后记住一点,没有“最好”的算法,只有“最合适”的算法。一个精心调试的PID,其性能完全可以秒杀一个模型不准、匆匆实现的MPC。算法的选择,最终要落到你的项目需求、硬件平台和时间成本这个铁三角上来权衡。