1. 从硬件到灵魂:直线型一阶倒立摆的软件架构总览
搞过倒立摆的朋友都清楚,机械结构和电机驱动只是搭了个骨架,真正让它“活”起来、能稳稳立住的那口气,全在软件里。直线型一阶倒立摆这个平台,硬件上无非就是导轨、滑块、连杆、编码器和电机,但软件设计的好坏,直接决定了它是像醉汉一样东倒西歪,还是像钉子一样钉在目标位置纹丝不动。我见过太多人硬件焊得漂漂亮亮,一上电就疯狂振荡,最后发现是控制周期没对齐、滤波没做对、或者状态机写得一塌糊涂。
这篇内容就是围绕“直线型一阶倒立摆”的软件设计展开,把从底层驱动到上层控制算法的完整链路拆开揉碎讲清楚。不管你是刚接触控制算法的学生,还是想快速复现一个稳定倒立摆的工程师,这里面的思路和代码框架都能直接拿去用。核心关键词就几个:实时控制周期、编码器数据处理、状态观测、PID与LQR的取舍、状态机设计。我会按实际工程落地的顺序,从架构设计讲到代码实现,再讲到调试时踩过的坑,尽量把每个“为什么这么做”都说明白。
先给个整体印象:直线型一阶倒立摆的软件,本质上是一个硬实时闭环系统。它要在毫秒级的时间窗口里完成“读传感器→算状态→算控制量→写执行器”这一整套动作,而且不能有一次迟到。迟到一次,摆杆可能就倒了。所以软件设计的第一原则不是功能多花哨,而是确定性——每个周期该干的事必须按时干完,不能有不可预测的延迟。这也是为什么很多人在电脑上仿真跑得好好的,一上实物就崩,因为电脑上的操作系统调度、内存管理、中断响应都充满了不确定性。
下面我会从架构分层开始,一层层往下挖,把每个模块的设计考量和实现细节都摊开来讲。
2. 软件架构分层与实时性设计
2.1 为什么必须做分层架构
倒立摆的软件如果全堆在一个while循环里,初期可能跑得通,但一旦要加功能——比如加个上位机波形显示、加个参数在线整定、加个不同的控制模式——代码就会变成一团乱麻。我自己的习惯是分成四层:硬件抽象层、驱动层、控制层、应用层。硬件抽象层把GPIO、定时器、ADC、编码器接口这些寄存器操作封装成函数,换芯片时只改这一层;驱动层负责把原始传感器数据转换成物理量,比如编码器脉冲数转成角度和位移;控制层是核心,跑控制算法和状态估计;应用层处理人机交互、模式切换、数据记录。
这么分的好处是,调试的时候可以逐层验证。比如先确认编码器读数对不对,再确认角度计算准不准,最后才去调PID参数。如果混在一起,出了问题你根本不知道是传感器错了还是算法错了。另一个好处是实时性分析更清晰——控制层必须放在定时器中断里,应用层可以放在主循环里慢慢跑,互不干扰。
2.2 控制周期的选择与计算
控制周期是倒立摆软件设计里第一个要拍板的参数。选大了,系统响应慢,摆杆倒了控制器还没反应过来;选小了,CPU负担重,而且编码器差分计算噪声会放大。那到底选多少合适?
这里有个经验公式:控制周期至少要比系统最快动态的时间常数小一个数量级。一阶倒立摆的不稳定极点大概在 (\sqrt{g/l}) 附近,摆杆半长取0.2米的话,(\sqrt{9.8/0.2} \approx 7) rad/s,对应时间常数约0.14秒。按十分之一算,控制周期应该在14毫秒以下。但实际工程中,考虑到编码器测速需要足够的脉冲数、以及留出安全裕度,我一般选5毫秒或者2毫秒。5毫秒对应200Hz,对大多数MCU来说绰绰有余;2毫秒对应500Hz,适合性能好一点的芯片,控制效果会更顺滑。
选好周期后,必须用硬件定时器来严格保证。不能用软件延时或者系统滴答,因为那些都会被其他中断打断。我通常用一个高级定时器,配置成向上计数模式,自动重装载值根据主频算好,在更新中断里执行控制逻辑。中断优先级要设成最高的那一档,确保它不会被其他中断抢占。
注意:控制周期一旦确定,整个软件里所有跟时间相关的计算都必须基于这个周期,不能一会儿用实际测量时间一会儿用标称值,否则积分项和微分项会乱掉。
2.3 中断服务程序的设计原则
中断服务程序(ISR)里能做的事很有限,原则就是快进快出。读编码器、算控制量、写PWM这些必须做的动作要放在ISR里,但像串口打印、Flash存储、复杂的状态判断这些耗时操作绝对不能放进去。我见过有人在ISR里调用printf,结果控制周期直接从5毫秒抖到20毫秒,摆杆疯狂振荡。
具体来说,我的ISR里通常只做四件事:第一,读取编码器计数并计算增量;第二,调用状态估计函数更新角度和角速度;第三,调用控制算法计算输出;第四,更新PWM占空比。整个流程控制在几百微秒以内。如果某个环节特别耗时,比如浮点运算多,可以考虑用定点数或者查表法优化,或者把部分计算挪到主循环里做预处理。
另外,ISR里访问的全局变量要加volatile修饰,防止编译器优化出问题。如果主循环也要读写这些变量,还得考虑临界区保护,最简单的办法是在读写时关中断,但关中断时间要尽可能短。
3. 传感器数据处理与状态估计
3.1 编码器信号的采集与方向判别
直线型倒立摆一般用两个编码器:一个测滑块位移,一个测摆杆角度。增量式编码器输出A、B两相正交信号,通过定时器的编码器模式可以直接硬件解码,省去软件判向的麻烦。以常见的STM32为例,把定时器配置成编码器模式,A相接CH1,B相接CH2,计数器就会根据旋转方向自动加减。读的时候直接取CNT寄存器的值,两次读数相减就是增量。
这里有个细节:计数器是16位的,正转到65535后会溢出回0,反转则从0下溢到65535。所以做差分的时候必须处理溢出,否则会出现巨大的跳变。我的做法是用一个有符号32位变量累积,每次差分时判断差值是否超过32767,超过就说明发生了溢出,手动修正。代码大概长这样:
int32_t delta = (int16_t)(current_cnt - last_cnt); total_count += delta; last_cnt = current_cnt;把差值强制转成int16_t,利用补码特性自动处理正负和溢出,非常简洁。这个技巧在编码器处理里屡试不爽。
3.2 从脉冲数到物理量的换算
编码器读出来的是脉冲数,要转换成角度和位移才能给控制算法用。换算系数取决于编码器线数、减速比(如果有)、传动轮直径或连杆长度。以摆杆角度为例,假设编码器直接安装在摆杆转轴上,线数是1000线,四倍频后是4000脉冲每转,那么每个脉冲对应的角度就是 (360/4000 = 0.09) 度,换成弧度是 (2\pi/4000 \approx 0.00157) rad。
位移的换算类似,如果编码器通过同步带连接滑块,带轮周长是P,编码器每转对应滑块移动P,那么每个脉冲对应位移就是 (P/4000)。这些系数最好在代码里用宏定义或者常量表示,方便后期调整。我习惯在初始化时把这些系数算好存起来,运行时直接乘,避免每次重复计算。
实操心得:换算系数一定要用实际测量来校准,不能只信理论值。比如带轮周长,理论算出来和实际跑一圈测出来的可能有几个毫米的偏差,累积起来对控制精度影响很大。校准方法很简单:手动把滑块从一端推到另一端,记录编码器总脉冲数和实际距离,一除就得到真实系数。
3.3 角速度的获取:差分还是观测器
角度有了,角速度怎么来?最直接的办法是对角度做差分:(\omega = (\theta_k - \theta_{k-1}) / T)。但编码器信号有量化噪声,差分会把噪声放大,尤其是周期小的时候。假设角度分辨率是0.00157 rad,周期5毫秒,那速度噪声就是0.314 rad/s,相当可观。直接拿这个速度去做微分控制,电机会抖得厉害。
解决办法有两个:一是加低通滤波,二是用状态观测器。低通滤波简单,一阶滤波公式是 (\omega_f = \alpha \omega + (1-\alpha) \omega_f),(\alpha) 越小滤波越强但延迟越大。我一般取截止频率在20到50Hz之间,根据实际噪声情况调。观测器方法更优雅,比如用卡尔曼滤波或者龙伯格观测器,把角度和角速度一起估计出来,噪声抑制效果好而且相位延迟小。但观测器需要系统模型,调参也麻烦一些。
对于一阶倒立摆,我的建议是:如果只是做课程设计或者快速验证,一阶低通滤波足够了;如果追求更好的控制性能,可以上卡尔曼滤波。卡尔曼的Q和R矩阵可以根据编码器噪声方差和过程噪声来整定,网上有现成的公式,这里不展开。
3.4 状态估计中的常见陷阱
状态估计这块有几个坑我踩过不止一次。第一个是角度零点漂移。编码器安装时不可能绝对精确,摆杆垂直时读数不一定为零。所以每次启动前要做一次零点校准,把当前读数记为偏移量,后面所有角度都减去这个偏移。但要注意,校准的时候摆杆必须真的垂直,最好用水平仪辅助。
第二个是速度估计的初始值。如果上电时摆杆在晃动,初始速度估计不为零,控制器一上来就会输出很大的控制量。我的做法是启动后先等几百毫秒,让滤波器收敛,同时把速度估计强制置零,等系统稳定后再切入闭环。
第三个是多传感器融合的时序问题。如果用了额外的陀螺仪或者加速度计,不同传感器的采样时刻可能不同步,融合时要做时间对齐。简单的做法是在同一个控制周期里依次读取所有传感器,认为它们是同一时刻的,对于慢速系统来说误差可以接受。
4. 控制算法的软件实现
4.1 PID控制在倒立摆上的局限与改进
很多人第一个想到的就是PID:角度环用PD,位置环用PID,串级控制。这确实能让倒立摆立起来,但效果往往差强人意。原因在于倒立摆是个欠驱动、非线性、不稳定的系统,PID的线性假设在偏离平衡点较远时不成立。而且角度环和位置环之间存在耦合,参数整定非常麻烦。
如果非要用PID,我的经验是:角度环只用PD,不要加积分,因为积分会引入相位滞后,容易导致振荡;位置环用PI就够了,微分项作用不大。角度环的P决定回复力大小,P越大立得越稳但抖动也越大;D决定阻尼,D太小会振荡,D太大会引入高频噪声。整定顺序是先调角度环PD让摆杆能立住几秒钟,再加位置环PI让滑块慢慢回到中心。
但说实话,PID调好了也只是“能立”,离“立得稳、抗干扰”还有距离。我实测下来,同样的硬件,LQR的控制效果明显优于PID,尤其是受到扰动后的恢复速度和超调量。
4.2 LQR控制器的设计与参数整定
LQR(线性二次型调节器)是倒立摆控制的主流方案。它的核心思想是找一个状态反馈矩阵K,使得代价函数 (J = \int (x^T Q x + u^T R u) dt) 最小。Q矩阵惩罚状态偏差,R矩阵惩罚控制量。Q越大,系统越倾向于快速消除偏差;R越大,控制量越保守。
对于直线型一阶倒立摆,状态向量一般取四个:滑块位置、滑块速度、摆杆角度、摆杆角速度。Q矩阵是对角阵,四个对角元素分别对应这四个状态的权重。我的整定经验是:角度和角速度的权重给大一些,位置和速度的权重给小一些,因为首要目标是保持摆杆直立。比如Q = diag(1, 1, 100, 10),R = 1,这样算出来的K矩阵通常能让系统稳定。
K矩阵的计算可以在MATLAB里用lqr函数,也可以自己写Riccati方程求解。算出来之后直接硬编码到MCU里,运行时只做矩阵乘法。四个状态乘四个系数再求和,计算量很小,5毫秒周期完全够用。
注意:LQR是基于线性化模型设计的,只在平衡点附近有效。如果摆杆初始角度偏得太多(比如超过30度),线性模型误差太大,控制器可能拉不回来。所以实际使用时要加一个“起摆”逻辑,先用能量法把摆杆甩到平衡点附近,再切入LQR。
4.3 状态反馈的代码实现细节
LQR算出来的控制量是 (u = -Kx),其中x是四个状态的向量。代码实现就是一次点乘:
float u = -(K[0]*pos + K[1]*vel + K[2]*angle + K[3]*ang_vel);看起来简单,但有几个细节要注意。第一,状态量的单位和量纲要统一,比如角度用弧度、位移用米,否则K的数值会差好几个数量级,整定起来很别扭。第二,控制量要限幅,防止电机过载或者机械结构受损。限幅值根据电机和导轨的实际能力来定,我一般设成额定电压的80%左右。第三,如果用了积分项来消除稳态误差,积分限幅也要做,防止积分饱和。
另外,K矩阵的数值最好用浮点数存储,虽然MCU上浮点运算比定点慢,但现代Cortex-M4以上的芯片都有硬件FPU,几个乘加运算也就几十个时钟周期,完全不影响5毫秒的控制周期。
4.4 起摆策略的软件实现
倒立摆从下垂位置开始,需要先“起摆”到平衡点附近才能切入稳定控制。常见的起摆方法有能量法和摇起法。能量法的思路是给系统注入能量,让摆杆的机械能逐渐增加到直立位置对应的势能。具体实现时,控制滑块做往复运动,每次摆杆经过最低点时加速,经过最高点时减速,能量就会逐渐累积。
软件上,起摆阶段用一个独立的状态机来管理。检测摆杆角度,当角度进入平衡点±10度范围内且角速度较小时,切换到LQR控制。切换瞬间要做状态初始化,把积分项清零、滤波器状态重置,避免切换冲击。我试过不做初始化直接切,结果摆杆会猛地抖一下,有时候直接抖倒了。
起摆的参数需要根据实际机械特性调,主要是往复运动的幅度和频率。幅度太小能量注入慢,幅度太大滑块可能撞到限位。我的经验是先从小幅慢频开始试,观察摆杆摆幅是否逐渐增大,如果不是就调整相位关系。
5. 软件调试与问题排查实录
5.1 调试工具链的搭建
倒立摆调试离不开实时数据可视化。我的标配是:MCU通过串口或者无线模块把关键数据发到上位机,上位机用Python或者LabVIEW画实时曲线。数据包括角度、角速度、位移、速度、控制量、控制周期实际值。这些曲线能直观反映系统状态,比盯着摆杆看强多了。
串口波特率至少115200,数据格式用二进制比ASCII效率高。每帧数据打包成固定长度,帧头帧尾加校验,防止错位。上位机收到后解析并绘图,同时存盘方便回看。我一般还会加一个“触发录制”功能,检测到异常时自动把前后几秒的数据存下来,方便事后分析。
控制周期的实际值一定要监测。在ISR入口翻转一个GPIO,用示波器看高电平持续时间,就知道ISR执行了多久。如果接近控制周期,说明代码需要优化了。
5.2 典型问题与排查思路
下面这张表是我这些年遇到过的典型问题,以及对应的排查方向,基本覆盖了90%的故障场景。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后摆杆剧烈振荡 | 控制周期不稳定、微分噪声大 | 示波器看ISR时长,检查滤波参数 |
| 摆杆能立但缓慢漂移 | 位置环积分不足、零点漂移 | 检查角度零点校准,增大位置环积分 |
| 起摆失败,摆杆摆幅不增 | 起摆相位反了、能量注入不足 | 调整起摆方向,增大往复幅度 |
| 切换LQR瞬间倒下 | 状态未初始化、K参数错误 | 检查切换逻辑,核对K矩阵数值 |
| 电机发热严重 | 控制量限幅过大、高频抖动 | 降低限幅值,加强速度滤波 |
| 编码器读数跳变 | 溢出未处理、接线干扰 | 检查差分溢出逻辑,加屏蔽线 |
排查的基本原则是先分后总:先确认传感器数据对不对,再确认控制量算得对不对,最后看执行器响应对不对。很多时候问题出在最底层,比如编码器线松了、电源纹波大,但表现出来像是算法问题。
5.3 参数整定的实操技巧
整定LQR的Q和R矩阵,我的方法是从保守到激进。先给R一个较大的值,让控制量很小,系统响应很慢但不会振荡。然后逐渐减小R,观察响应速度和超调。找到临界点后,再把R调回去一点留裕度。Q矩阵的调整类似,先调角度权重,让摆杆立稳,再调位置权重,让滑块回中。
整定过程中一定要有量化指标,不能凭感觉。我通常记录三个指标:稳定时间(从扰动到恢复的时间)、超调量(最大偏差)、稳态误差(最终偏差)。每次调参后测一遍,画成表格对比,很快就能找到最优区域。
还有一个技巧是分步验证。先把位置环断开,只让角度环工作,这时候滑块会乱跑但摆杆能立住。确认角度环没问题后,再接入位置环。这样出问题时能快速定位是哪一环的问题。
5.4 代码优化与实时性保障
最后聊聊代码优化。倒立摆的代码量不大,但实时性要求高。几个关键点:第一,ISR里避免浮点除法,除法很慢,能改成乘法就改;第二,查表代替实时计算,比如三角函数可以预先算好表;第三,用DMA搬运串口数据,不占用CPU;第四,编译器优化等级开到-O2,但要注意volatile变量不会被优化掉。
如果控制周期实在压不下来,可以考虑把状态估计的一部分放到主循环里做,ISR里只做最紧急的控制计算。但这样会引入延迟,需要重新评估稳定性。我的原则是宁可换更快的芯片,也不要在ISR里做妥协,因为倒立摆对延迟太敏感了。
我个人在实际操作中的体会是,倒立摆的软件设计,三分靠算法,七分靠工程细节。算法书上都有,但编码器溢出处理、控制周期抖动、状态切换冲击这些细节,只有亲手调过才知道有多坑。把每个细节都做到位,系统自然就稳了。后续如果想进一步提升,可以尝试自适应控制或者模型预测控制,但那是另一个话题了。