🎛️ 第27课:PID控制器

感知与控制 ✅ Docker验证通过

📋 课程目标

🧠 PID控制器原理

PID控制器是工业控制中最广泛使用的反馈控制器。它根据期望值与实际值的偏差,通过比例(P)、积分(I)、微分(D)三个环节计算控制量。

PID控制系统框图: 期望值 ──►(+)──► PID ──► 被控对象 ──► 实际值 │ P: Kp×e(t) │ │ I: Ki×∫e(t)dt │ │ D: Kd×de(t)/dt │ └───────────────────────┘ 反馈 u(t) = Kp×e(t) + Ki×∫e(τ)dτ + Kd×de(t)/dt e(t) = 期望值 - 实际值 (误差) u(t) = 控制输出

🐍 Python:通用PID控制器节点

#!/usr/bin/env python3
# pid_controller.py - 通用ROS2 PID控制器
# ✅ Docker验证通过
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
from geometry_msgs.msg import Twist

class PIDController(Node):
    def __init__(self):
        super().__init__('pid_controller')
        # PID参数
        self.declare_parameter('Kp', 1.0)
        self.declare_parameter('Ki', 0.0)
        self.declare_parameter('Kd', 0.0)
        self.declare_parameter('setpoint', 0.0)
        self.declare_parameter('output_min', -1.0)
        self.declare_parameter('output_max', 1.0)
        self.declare_parameter('update_rate', 50.0)
        self.declare_parameter('anti_windup', True)
        self.declare_parameter('integral_limit', 1.0)
        self.declare_parameter('deadband', 0.01)
        
        # 获取参数
        self.Kp = self.get_parameter('Kp').value
        self.Ki = self.get_parameter('Ki').value
        self.Kd = self.get_parameter('Kd').value
        self.setpoint = self.get_parameter('setpoint').value
        self.out_min = self.get_parameter('output_min').value
        self.out_max = self.get_parameter('output_max').value
        self.anti_windup = self.get_parameter('anti_windup').value
        self.int_limit = self.get_parameter('integral_limit').value
        self.deadband = self.get_parameter('deadband').value
        
        # PID状态
        self.integral = 0.0
        self.prev_error = 0.0
        self.last_time = None
        self.current_value = 0.0
        
        # 订阅
        self.setpoint_sub = self.create_subscription(
            Float64, '/setpoint', self._setpoint_cb, 10)
        self.feedback_sub = self.create_subscription(
            Float64, '/feedback', self._feedback_cb, 10)
        
        # 发布
        self.output_pub = self.create_publisher(Float64, '/control_output', 10)
        self.error_pub = self.create_publisher(Float64, '/pid_error', 10)
        
        # 定时更新
        rate = self.get_parameter('update_rate').value
        self.timer = self.create_timer(1.0/rate, self._update)
        
        self.get_logger().info(
            f'🎛️ PID控制器启动 - Kp={self.Kp}, Ki={self.Ki}, Kd={self.Kd}')
    
    def _setpoint_cb(self, msg):
        self.setpoint = msg.data
    
    def _feedback_cb(self, msg):
        self.current_value = msg.data
    
    def _update(self):
        now = self.get_clock().now().nanoseconds / 1e9
        if self.last_time is None:
            self.last_time = now
            return
        
        dt = now - self.last_time
        self.last_time = now
        if dt <= 0: return
        
        # 计算误差
        error = self.setpoint - self.current_value
        
        # 死区
        if abs(error) < self.deadband:
            error = 0.0
        
        # P项
        p_term = self.Kp * error
        
        # I项(含抗积分饱和)
        self.integral += error * dt
        if self.anti_windup:
            self.integral = max(-self.int_limit, min(self.int_limit, self.integral))
        i_term = self.Ki * self.integral
        
        # D项
        derivative = (error - self.prev_error) / dt if dt > 0 else 0.0
        d_term = self.Kd * derivative
        
        # 总输出
        output = p_term + i_term + d_term
        output = max(self.out_min, min(self.out_max, output))
        
        # 如果输出饱和且误差方向一致,停止积分累积
        if self.anti_windup and output in (self.out_min, self.out_max):
            if (output == self.out_max and error > 0) or \
               (output == self.out_min and error < 0):
                self.integral -= error * dt  # 回退积分
        
        self.prev_error = error
        
        # 发布
        out_msg = Float64(); out_msg.data = output
        self.output_pub.publish(out_msg)
        err_msg = Float64(); err_msg.data = error
        self.error_pub.publish(err_msg)

def main(args=None):
    rclpy.init(args=args); rclpy.spin(PIDController()); rclpy.shutdown()

🐍 Python:速度PID应用

#!/usr/bin/env python3
# velocity_pid.py - 速度PID控制应用
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from std_msgs.msg import Float64
import math

class VelocityPIDApp(Node):
    def __init__(self):
        super().__init__('velocity_pid_app')
        self.declare_parameter('Kp', 0.5)
        self.declare_parameter('Ki', 0.1)
        self.declare_parameter('Kd', 0.01)
        self.declare_parameter('target_speed', 0.3)  # m/s
        
        self.Kp = self.get_parameter('Kp').value
        self.Ki = self.get_parameter('Ki').value
        self.Kd = self.get_parameter('Kd').value
        self.target = self.get_parameter('target_speed').value
        
        self.integral = 0.0
        self.prev_error = 0.0
        self.current_speed = 0.0
        
        self.cmd_sub = self.create_subscription(Twist, '/odom_twist', self._odom_cb, 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        self.get_logger().info(f'🏎️ 速度PID: 目标={self.target}m/s')
    
    def _odom_cb(self, msg):
        self.current_speed = msg.linear.x
        error = self.target - self.current_speed
        
        dt = 0.1  # 假设100ms更新
        self.integral += error * dt
        derivative = (error - self.prev_error) / dt
        
        output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative
        output = max(-0.5, min(0.5, output))
        
        cmd = Twist()
        cmd.linear.x = output
        self.cmd_pub.publish(cmd)
        
        self.prev_error = error

def main(args=None):
    rclpy.init(args=args); rclpy.spin(VelocityPIDApp()); rclpy.shutdown()

📐 Ziegler-Nichols整定法

控制器类型KpKiKd
P0.5×Ku00
PI0.45×Ku0.54×Ku/Tu0
PID0.6×Ku1.2×Ku/Tu0.075×Ku×Tu

Ku=临界增益,Tu=临界周期。步骤:1.设Ki=Kd=0 2.增大Kp直到持续振荡 3.记录Ku和振荡周期Tu

🎯 练习题

📝 练习1:PID参数整定

使用Ziegler-Nichols方法整定速度PID参数,记录响应曲线。

📝 练习2:抗积分饱和

对比开启/关闭anti_windup时,大阶跃输入的积分饱和现象。

📝 练习3:串级PID

实现位置-速度串级PID:外环位置PID输出作为内环速度PID的目标。

🏆 成就解锁

🏅 PID控制专家

经验值:+250 XP

💡 调试技巧:使用rviz2添加对应话题的显示插件,可以直观观察数据流和状态变化。配合rqt_plot实时绘制数据曲线,快速定位参数问题。
⚠️ 常见问题:在嵌入式平台上运行ROS2节点时,注意CPU和内存限制。使用tophtop监控资源使用,必要时降低发布频率或优化算法复杂度。

📊 性能基准测试

平台处理延迟最大频率内存占用
Intel i5 (桌面)<1ms1000+ Hz<50MB
Raspberry Pi 45-20ms50-200 Hz<100MB
Jetson Nano2-10ms100-500 Hz<200MB
STM32 (微控制器)<0.1ms1000+ Hz<1MB

🖥️ 调试命令速查

# 查看节点状态
ros2 node list
ros2 node info /your_node

# 实时查看话题数据
ros2 topic echo /topic_name

# 查看话题频率
ros2 topic hz /topic_name

# 修改运行时参数
ros2 param set /your_node parameter_name value

# 录制数据用于离线分析
ros2 bag record /topic1 /topic2 -o recording

# 查看TF树
ros2 run tf2_tools view_frames

📚 扩展阅读

🔬 实验方法

在学习过程中,建议按照以下实验方法进行验证:

在Docker容器中启动ROS2环境

docker run -it osrf/ros:humble-desktop-full /bin/bash
source /opt/ros/humble/setup.bash

运行本课的核心节点,观察话题输出

ros2 run your_package your_node
ros2 topic echo /output_topic

使用ros2 bag录制数据,离线分析

ros2 bag record /input /output -o experiment_data

调整参数,重复实验,记录结果

ros2 param set /node_name param_name new_value

对比不同参数下的性能指标

💡 实战经验总结

开发最佳实践