三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

串级PID控制:从“画龙”到“丝滑”,机器人运动控制的进阶之路

串级PID控制:从“画龙”到“丝滑”,机器人运动控制的进阶之路

最近在调一个移动机器人的运动控制,硬件组的同事过来看了一眼,说你这转弯轨迹怎么跟画龙似的,一顿一顿的,看着都难受。他指着屏幕上的速度曲线说,你这单环PID,位置给得再准,电机速度跟不上,过弯道的时候内轮和外轮的速度差控制不住,能不抖吗?得用串级。

这句话点醒了我。我们很多时候调PID,总想着一个环搞定所有问题,位置不准就调位置环,速度不稳就调速度环。但像机器人转弯、无人机姿态调整、甚至高级一点的云台跟踪这种场景,被控对象本身就有很强的惯性,而且内外环的响应速度差了好几个数量级。这时候,单环PID就像一个人既要看路(外环,位置/角度),又要控制手脚肌肉的微操(内环,速度/角速度),脑子根本转不过来,结果就是要么反应迟钝,要么剧烈振荡。

串级PID(Cascade PID)解决的,就是把“看路”和“微操”这两个任务拆开,交给两个专一的“大脑”去处理。外环(主环)负责制定宏观目标,比如“走到A点”或者“保持水平”;内环(副环)则像一个高效的执行者,只专注于快速、精准地达到外环给它下达的瞬时速度或力矩指令。这种架构,特别适合对付那些大惯性、有延迟,并且内外动态特性差异明显的系统。

1. 为什么单环PID在过弯道时会“画龙”?

要理解串级PID的价值,得先看清单环PID在这种场景下的无力感。

假设我们用一个位置环PID来控制机器人的左右轮差速,从而实现转向。控制器输入是期望的转弯半径(或角度),输出是直接给电机的PWM占空比。这个环路的思考逻辑是:当前位置离期望轨迹还有偏差?好,我算一个PID输出,直接让电机转快点或慢点来纠正。

问题就出在这个“直接”上。电机和轮子是一个有惯性的物理系统。当你突然增大PWM想让电机加速时,电机转速不会瞬间跳变,它会有一个上升时间。同样,当你减小PWM时,由于惯性,它也不会立刻停下来。这个惯性带来的延迟,对于高速响应要求的位置环来说,是致命的。

在直线行驶时,这个矛盾还不突出,因为误差是单向的,PID输出变化相对平缓。但一到弯道,情况就复杂了:

  • 内、外轮速差需要实时精确控制:过弯时,内侧轮子需要减速,外侧轮子需要加速,这个速度差需要根据转弯半径和当前车速动态计算。单环PID的位置误差信号,很难直接、平滑地映射出这个动态变化的速度差指令。
  • 系统响应跟不上指令变化:弯道轨迹是连续变化的,这意味着位置误差和PID输出指令也在快速变化。但电机的机械响应速度(内环动态)远远跟不上这个指令的变化速度(外环动态)。外环指令变得太快,内环执行太慢,结果就是外环看到“还没跟上”,拼命加大输出;等内环好不容易加速起来,外环又发现“超调了”,赶紧刹车。如此反复,就形成了我们在速度曲线上看到的剧烈振荡,反映到机器人身上,就是一顿一顿的“画龙”现象。

本质上,单环PID试图用一个控制器去同时应对两种不同时间尺度的动态过程:慢速的轨迹规划(外环)和快速的电机响应(内环)。这就像用一把锤子去干螺丝刀和锤子两样活,肯定别扭。

2. 串级PID:给控制系统装上“大脑”和“小脑”

串级PID的思路很清晰:既然一个大脑忙不过来,那就分工。我们引入两个控制器,组成一个内外嵌套的闭环。

  • 外环(主环,Position Loop / Outer Loop):这就是“大脑”。它只关心高级目标,比如“机器人应该在哪里”、“云台应该指向哪个角度”。它的输入是期望值(Setpoint)和实际值(Feedback,通常来自编码器、视觉或IMU)的偏差,输出不再直接给执行机构,而是作为一个速度指令(或力矩指令)送给内环。你可以把它理解成一个“指挥官”,它根据战局(位置偏差)决定“步兵连,以每秒X米的速度向3点钟方向推进”。
  • 内环(副环,Velocity Loop / Inner Loop):这就是“小脑”或“执行官”。它只接受一个任务:以最快的速度、最稳的状态,达到外环下达的速度指令。它的输入是外环给的速度指令和电机编码器反馈回来的实际转速,输出才是直接驱动电机的PWM或电流。它专注于处理电机本身的惯性、阻力等快速动态过程。

这个过程形成了一个级联:期望位置->外环PID->期望速度->内环PID->电机驱动信号->电机/负载->实际速度->实际位置

2.1 串级PID是如何让过弯“丝滑”的?

回到过弯道的例子。当我们采用位置-速度串级PID后:

  1. 外环(位置环)计算速度差:机器人开始过弯,外环位置控制器根据当前位置与期望圆弧轨迹的偏差,计算出一个纠正这个偏差所需要的合成速度矢量。这个矢量分解到左右轮上,就是两个不断变化的期望轮速(V_left_desired,V_right_desired)。注意,外环此时不关心电机能不能瞬间达到这个速度,它只负责根据几何关系算出“理论上此刻应该有的速度”。
  2. 内环(速度环)全力执行:左右轮各自的期望速度被分别送入两个独立的速度环PID控制器。每个速度环的任务变得极其单纯:让我控制的这个电机的实际转速,死死地跟上外环给我的指令。电机惯性大?速度环的微分项(D)可以预测变化趋势,提前加大控制量。负载突变?速度环的积分项(I)可以累积误差,消除静差。由于速度环的响应频率可以做得非常高(通常控制周期在1ms以下),它能非常敏捷地对抗电机的惯性,让实际轮速紧密跟踪期望轮速。
  3. 结果:由于内环速度环的快速响应,外环位置环下达的速度指令被忠实且快速地执行了。这意味着机器人左右轮的实际速度差,能够实时、精准地匹配过弯所需的理论速度差。反映在运动效果上,就是转弯轨迹变得连续、平滑,速度曲线平稳,没有了单环控制下的那种挣扎和振荡,也就是硬件队友所说的“丝滑”。

2.2 参数整定:先内后外,由快至慢

串级PID带来了性能提升,也带来了双倍的调参工作量。但它的调参有非常清晰的哲学和步骤,概括起来就是:先调内环,再调外环;内环要快,外环要稳。

  1. 断开外环,独立整定内环(速度环)

    • 目标:让电机速度能快速、无超调、无静差地跟踪一个阶跃速度指令。这是整个系统的基础。
    • 方法:给定一个固定速度指令(比如100 RPM),只运行速度环PID。调整P、I、D参数,观察电机实际速度的响应曲线。
    • 经验:速度环通常需要较强的微分项(D)来抑制惯性引起的超调,也需要足够的积分项(I)来消除稳态误差(确保达到指令速度)。比例项(P)提供基础响应速度。内环的带宽(响应速度)应尽可能做高,但要以不引起高频振荡或执行机构饱和为限。
  2. 闭合外环,整定外环(位置环)

    • 前提:确保内环已经调好,且性能优异。可以把内环近似看作一个快速、准确的“速度伺服单元”。
    • 目标:让位置能够平滑、准确地跟踪期望轨迹。由于内环已经很快,外环的参数可以相对“温和”。
    • 方法:输入一个阶跃位置指令或一个低速平滑的移动指令(如缓慢的S形曲线)。调整外环的P、I、D。
    • 经验:外环的比例项(P)决定了系统对位置偏差的反应强度。太大容易导致系统振荡(因为内环很快,会放大外环的指令波动);太小则响应迟钝。外环的微分项(D)有时可以用来预测趋势,让系统提前减速,但对于已经有一个快速内环的系统,外环D常常可以设为0或很小。外环的积分项(I)用于消除最终的位置静差。

一个重要的比喻:调好后的串级系统,就像一个经验丰富的司机(外环)指挥着一辆性能极佳、刹车油门反应灵敏的跑车(内环)。司机只需要给出合理的速度指令,跑车自己就能极其精准地实现,因此过弯时自然流畅。而单环系统,则像司机直接控制着发动机喷油和刹车盘,手忙脚乱,车子动态响应又慢,过弯必然颠簸。

3. 从理论到实践:搭建一个串级PID控制器

理解了原理,我们来看如何具体实现一个位置-速度串级PID控制器。这里以常见的微控制器(如STM32)环境为例,给出一个概念性的代码框架和关键注意点。

首先,我们需要两个独立的PID控制器结构体。虽然本质相同,但参数和用途不同。

typedef struct { float Kp, Ki, Kd; // PID参数 float integral; // 积分项累计值 float prev_error; // 上一次误差,用于计算微分 float output_limit; // 输出限幅 float integral_limit; // 积分限幅(抗饱和) } PID_Controller; PID_Controller pid_velocity_left; // 左轮速度环PID PID_Controller pid_velocity_right; // 右轮速度环PID PID_Controller pid_position; // 位置环PID(输出为期望速度差或直接是期望轮速)

控制周期通常由定时器中断触发。假设我们有一个1ms的定时中断服务程序(ISR):

void TIM_Control_ISR(void) { // 1ms中断 // 1. 读取传感器反馈 float actual_position = get_robot_position(); // 从编码器或融合算法得到 float actual_velocity_left = get_left_wheel_speed(); // 左轮编码器速度 float actual_velocity_right = get_right_wheel_speed(); // 右轮编码器速度 // 2. **外环(位置环)计算** float desired_position = get_desired_position_from_trajectory(); // 从规划器获取 float position_error = desired_position - actual_position; // 位置环PID计算,输出为期望的线速度或角速度,这里以期望转向角速度为例 float desired_turn_rate = PID_Calculate(&pid_position, position_error); // 根据运动学模型,将期望转向角速度和期望线速度,分解为左右轮期望速度 float desired_linear_speed = get_desired_linear_speed(); // 规划器给出 float wheel_distance = 0.2f; // 轮距,单位米 float desired_velocity_left = desired_linear_speed - desired_turn_rate * wheel_distance / 2.0f; float desired_velocity_right = desired_linear_speed + desired_turn_rate * wheel_distance / 2.0f; // 3. **内环(速度环)计算** float velocity_error_left = desired_velocity_left - actual_velocity_left; float velocity_error_right = desired_velocity_right - actual_velocity_right; float pwm_left = PID_Calculate(&pid_velocity_left, velocity_error_left); float pwm_right = PID_Calculate(&pid_velocity_right, velocity_error_right); // 4. 输出到执行机构 set_motor_pwm(MOTOR_LEFT, pwm_left); set_motor_pwm(MOTOR_RIGHT, pwm_right); }

关键实现细节与避坑指南:

  1. 控制周期不同步:速度环需要快速响应,控制周期应尽可能短(如1ms)。位置环的周期可以和速度环一致,也可以稍慢一些(如5-10ms),尤其是当位置反馈来自更新频率较低的传感器(如视觉)时。确保你的中断和传感器读取时序是确定的。
  2. 反馈信号质量:速度反馈通常由编码器脉冲计数除以时间得到。短时间内的脉冲数少,速度计算会存在量化噪声。需要使用低通滤波(如一阶惯性滤波)对计算出的速度进行平滑,但要注意滤波会引入相位滞后,可能影响内环稳定性。
  3. 积分抗饱和(Anti-windup):这是PID实践中的重中之重。当电机输出已到极限(PWM满占空比)但误差仍存在时,积分项会持续累积到一个巨大的值(积分饱和)。一旦误差反向,需要很长时间才能“消化”掉这个巨大的积分值,导致控制失灵。必须在PID_Calculate函数中实现积分限幅或遇限削弱积分逻辑。
  4. 输出限幅:PID的输出必须限制在硬件执行机构(如电机驱动器)的允许范围内。
  5. 单位一致性:确保位置、速度、PID参数的单位在整个系统中一致(如米、米/秒、弧度、弧度/秒)。混乱的单位是调试噩梦的源头。

4. 调试与排查:当串级PID不“丝滑”时怎么办?

即使搭好了框架,调试过程也可能充满挑战。以下是一个系统性的排查链路,当你的串级PID效果不佳时,可以按顺序检查:

4.1 现象:系统根本不稳定,剧烈振荡

  • 排查内环(速度环)
    • 检查反馈信号:用示波器或高速数据采集查看编码器原始脉冲和计算出的速度波形。是否有丢脉冲、噪声极大、计算延迟大的问题?先确保反馈是干净、及时的。
    • 参数是否过于激进:大幅降低速度环的P和D值。内环是基础,必须优先保证稳定。从很小的P(比如0.1)开始,I和D设为0,给一个阶跃速度指令,看响应是否平滑上升。逐步增加P直到出现轻微超调,然后加入一点D来抑制超调。
    • 检查控制周期:1ms的中断是否被其他高优先级任务打断?确保速度环的计算和输出是严格周期性的。

4.2 现象:速度能跟上,但位置跟踪慢,过弯仍有偏差

  • 排查外环(位置环)
    • 内环是否真的够快:给速度环一个方波指令,看实际速度的跟随延迟和超调量。如果内环响应本身就有100ms的延迟,外环性能肯定上不去。先优化内环。
    • 外环参数过于保守:在保证内环稳定、外环不引发振荡的前提下,适当增加外环的P值。可以尝试给外环加入一点微分(D),它相当于一个“预见性”,能让系统提前开始减速或纠正,对改善过弯跟踪性能有时很有效。
    • 检查轨迹规划:你给位置环的desired_position指令本身是否平滑?如果规划器给出的就是一条有尖角的折线,再好的控制器也跟不光滑。确保期望轨迹是连续且一阶可导(速度连续)的。

4.3 现象:直线还行,一到弯道特定位置就抖

  • 排查非线性与耦合
    • 参数是否适应全工况:你整定的PID参数是在静态或匀速下做的吗?机器人在加减速、弯道时,负载惯量、地面摩擦力都在变化,固定参数可能无法覆盖所有工况。考虑是否需要参数自适应增益调度(例如,根据速度大小切换不同的PID参数组)。
    • 运动学模型误差:将期望转向角速度分解为左右轮速的公式是否正确?轮距测量是否准确?轮胎是否存在打滑?这些模型误差会被外环当作位置偏差,进而产生错误的纠偏指令。
    • 左右轮动力学不一致:两个电机的内环PID参数是否分别独立整定过?即使同一型号的电机,特性也可能有细微差别。分别调参有助于实现对称的性能。

4.4 进阶排查:数据记录与可视化

  • 搭建调试接口:将每个控制周期的关键变量(期望位置、实际位置、期望速度、实际速度、PID输出)通过串口或无线模块发送到上位机(如Python + Matplotlib)。
  • 绘制波形:在同一时间轴上对比观察。你能清晰地看到外环指令如何变化,内环如何跟踪,以及最终的位置误差。这是发现相位滞后、振荡频率、饱和现象的最直观方式。

串级PID不是一个“一劳永逸”的银弹,而是一个需要精心调试的精密架构。它的优势在于提供了清晰的问题分解思路:将复杂的控制问题,拆解为可以逐级击破的子问题。当你被硬件队友“压力”时,不妨坐下来,用示波器和数据曲线,和他一起从内环到外环,逐层验证。那个从“画龙”到“丝滑”的过程,不仅是参数的调整,更是对系统动力学理解的一次深化。最终,让机器按照你预想的轨迹平稳行进时,那种掌控感,才是工程实践中最实在的回报。

← 返回列表