🎛️ 第06课:参数(Parameter)

ROS2基础 ✅ Docker验证通过

📋 课程目标

🧠 什么是参数?

参数(Parameter)是节点的运行时配置值,允许在不修改代码的情况下调整节点行为。与硬编码常量不同,参数可以在启动时设置,也可以在运行时动态修改。

🔑 参数特性

📐 参数架构图

启动时配置 运行时修改 ┌──────────────────────┐ ┌──────────────────────┐ │ YAML配置文件 │ │ ros2 param set │ │ config.yaml │ │ ros2 service call │ └──────────┬───────────┘ └──────────┬───────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────────────────────────┐ │ ROS2 Node │ │ │ │ declare_parameter('name', default_value, descriptor) │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────┐ │ │ │ Parameter Store │ │ │ │ ┌────────┬────────┬────────┬────────┐ │ │ │ │ │ int │ double │ string │ bool │ │ │ │ │ │ rate:10│ kp:1.5 │frame:map│en:true│ │ │ │ │ └────────┴────────┴────────┴────────┘ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ ▼ on_parameter_event callback │ │ ┌──────────────────────────────────────────┐ │ │ │ 参数变化回调 → 动态调整行为 │ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘

📊 参数类型

类型PythonC++示例
整数intint64_t42
浮点floatdouble3.14
字符串strstd::string"hello"
布尔boolbooltrue
字节数组bytesstd::vector<uint8_t>[0x01, 0x02]
整数数组list[int]std::vector<int64_t>[1, 2, 3]
浮点数组list[float]std::vector<double>[1.0, 2.5]
字符串数组list[str]std::vector<std::string>["a","b"]
布尔数组list[bool]std::vector<bool>[true, false]

🐍 Python:参数完整示例

#!/usr/bin/env python3
"""PID控制器节点 - 演示ROS2参数系统完整用法"""

import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, FloatingRange, IntegerRange
from std_msgs.msg import Float64


class PidControllerNode(Node):
    """PID控制器节点,所有参数可动态调整"""

    def __init__(self):
        super().__init__('pid_controller')

        # ====== 声明参数(带描述和约束)======

        # KP - 比例增益
        self.declare_parameter(
            'kp', 1.0,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_DOUBLE,
                description='比例增益(Kp),控制响应速度',
                floating_range=[FloatingRange(from_value=0.0, to_value=100.0)],
                read_only=False
            )
        )

        # KI - 积分增益
        self.declare_parameter(
            'ki', 0.1,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_DOUBLE,
                description='积分增益(Ki),消除稳态误差',
                floating_range=[FloatingRange(from_value=0.0, to_value=10.0)],
            )
        )

        # KD - 微分增益
        self.declare_parameter(
            'kd', 0.05,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_DOUBLE,
                description='微分增益(Kd),抑制超调',
                floating_range=[FloatingRange(from_value=0.0, to_value=10.0)],
            )
        )

        # 目标值
        self.declare_parameter(
            'setpoint', 0.0,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_DOUBLE,
                description='目标设定值',
            )
        )

        # 采样频率
        self.declare_parameter(
            'sample_rate', 100,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_INTEGER,
                description='采样频率(Hz)',
                integer_range=[IntegerRange(from_value=1, to_value=1000, step=1)],
            )
        )

        # 输出限幅
        self.declare_parameter(
            'output_limit', 10.0,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_DOUBLE,
                description='输出限幅值',
                floating_range=[FloatingRange(from_value=0.0, to_value=100.0)],
            )
        )

        # 启用标志
        self.declare_parameter(
            'enabled', True,
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_BOOL,
                description='是否启用控制器',
            )
        )

        # 话题名称
        self.declare_parameter(
            'topic_prefix', 'pid',
            ParameterDescriptor(
                type=rcl_interfaces.msg.ParameterType.PARAMETER_STRING,
                description='话题前缀',
            )
        )

        # 读取初始参数
        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.output_limit = self.get_parameter('output_limit').value
        self.enabled = self.get_parameter('enabled').value

        # PID状态
        self.integral = 0.0
        self.prev_error = 0.0

        # 发布者
        self.output_pub = self.create_publisher(Float64, 'control_output', 10)

        # 定时器
        rate = self.get_parameter('sample_rate').value
        self.timer = self.create_timer(1.0 / rate, self.control_loop)

        # 添加参数变化回调
        self.add_on_set_parameters_callback(self.parameter_callback)

        self.get_logger().info(
            f'PID控制器启动 - Kp={self.kp}, Ki={self.ki}, Kd={self.kd}, '
            f'Setpoint={self.setpoint}'
        )

    def control_loop(self):
        """PID控制循环"""
        if not self.enabled:
            return

        # 模拟:使用正弦波作为测量值
        import math, time
        measurement = math.sin(time.time()) * 5.0

        # PID计算
        error = self.setpoint - measurement
        self.integral += error * (1.0 / self.get_parameter('sample_rate').value)

        # 积分限幅(防积分饱和)
        integral_limit = self.output_limit / max(self.ki, 0.001)
        self.integral = max(-integral_limit, min(integral_limit, self.integral))

        derivative = (error - self.prev_error) * self.get_parameter('sample_rate').value
        output = self.kp * error + self.ki * self.integral + self.kd * derivative

        # 输出限幅
        output = max(-self.output_limit, min(self.output_limit, output))

        self.prev_error = error

        msg = Float64()
        msg.data = output
        self.output_pub.publish(msg)

    def parameter_callback(self, params):
        """参数变化回调"""
        for param in params:
            if param.name == 'kp':
                self.kp = param.value
                self.get_logger().info(f'Kp 更新为: {self.kp}')
            elif param.name == 'ki':
                self.ki = param.value
                self.get_logger().info(f'Ki 更新为: {self.ki}')
            elif param.name == 'kd':
                self.kd = param.value
                self.get_logger().info(f'Kd 更新为: {self.kd}')
            elif param.name == 'setpoint':
                self.setpoint = param.value
                self.integral = 0.0  # 重置积分
                self.prev_error = 0.0
                self.get_logger().info(f'设定值更新为: {self.setpoint}')
            elif param.name == 'enabled':
                self.enabled = param.value
                if not self.enabled:
                    self.integral = 0.0
                self.get_logger().info(f'控制器{"启用" if self.enabled else "禁用"}')
            elif param.name == 'output_limit':
                self.output_limit = param.value
                self.get_logger().info(f'输出限幅更新为: {self.output_limit}')

        return rclpy.parameter.SetParametersResult(successful=True)


def main(args=None):
    rclpy.init(args=args)
    node = PidControllerNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

📄 YAML配置文件

参数可以通过YAML文件在启动时批量加载:

# config/pid_controller.yaml
pid_controller:
  ros__parameters:
    kp: 2.5
    ki: 0.3
    kd: 0.1
    setpoint: 10.0
    sample_rate: 200
    output_limit: 15.0
    enabled: true
    topic_prefix: "left_motor_pid"
# 启动时加载YAML
ros2 run my_pkg pid_controller --ros-args \
  --params-file config/pid_controller.yaml

# 或在launch文件中加载
import launch
import launch_ros.actions

node = launch_ros.actions.Node(
    package='my_pkg',
    executable='pid_controller',
    parameters=['config/pid_controller.yaml']
)

🔧 C++参数使用

// pid_controller_cpp.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float64.hpp"
#include "rcl_interfaces/msg/set_parameters_result.hpp"

#include <functional>
#include <string>

class PidControllerCpp : public rclcpp::Node {
public:
    PidControllerCpp() : Node("pid_controller_cpp"),
        integral_(0.0), prev_error_(0.0) {

        // 声明参数
        this->declare_parameter("kp", 1.0);
        this->declare_parameter("ki", 0.1);
        this->declare_parameter("kd", 0.05);
        this->declare_parameter("setpoint", 0.0);
        this->declare_parameter("output_limit", 10.0);
        this->declare_parameter("enabled", true);

        // 读取参数
        kp_ = this->get_parameter("kp").as_double();
        ki_ = this->get_parameter("ki").as_double();
        kd_ = this->get_parameter("kd").as_double();

        // 参数回调
        param_callback_handle_ = this->add_on_set_parameters_callback(
            std::bind(&PidControllerCpp::on_parameter_change, this,
                      std::placeholders::_1));

        // 发布者
        output_pub_ = this->create_publisher<std_msgs::msg::Float64>(
            "control_output_cpp", 10);

        // 定时器
        timer_ = this->create_wall_timer(
            std::chrono::milliseconds(10),
            std::bind(&PidControllerCpp::control_loop, this));

        RCLCPP_INFO(this->get_logger(),
            "C++ PID控制器启动 - Kp=%.2f, Ki=%.2f, Kd=%.2f",
            kp_, ki_, kd_);
    }

private:
    void control_loop() {
        if (!this->get_parameter("enabled").as_bool()) return;

        double setpoint = this->get_parameter("setpoint").as_double();
        double measurement = 0.0; // 简化
        double error = setpoint - measurement;
        double limit = this->get_parameter("output_limit").as_double();

        integral_ += error * 0.01;
        double derivative = (error - prev_error_) / 0.01;
        double output = kp_ * error + ki_ * integral_ + kd_ * derivative;
        output = std::max(-limit, std::min(limit, output));

        prev_error_ = error;

        auto msg = std_msgs::msg::Float64();
        msg.data = output;
        output_pub_->publish(msg);
    }

    rcl_interfaces::msg::SetParametersResult on_parameter_change(
        const std::vector<rclcpp::Parameter> &params) {

        for (const auto& p : params) {
            if (p.get_name() == "kp") {
                kp_ = p.as_double();
                RCLCPP_INFO(this->get_logger(), "Kp -> %.2f", kp_);
            } else if (p.get_name() == "ki") {
                ki_ = p.as_double();
                RCLCPP_INFO(this->get_logger(), "Ki -> %.2f", ki_);
            } else if (p.get_name() == "kd") {
                kd_ = p.as_double();
                RCLCPP_INFO(this->get_logger(), "Kd -> %.2f", kd_);
            }
        }

        rcl_interfaces::msg::SetParametersResult result;
        result.successful = true;
        return result;
    }

    rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr output_pub_;
    rclcpp::TimerBase::SharedPtr timer_;
    OnSetParametersCallbackHandle::SharedPtr param_callback_handle_;
    double kp_, ki_, kd_, integral_, prev_error_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<PidControllerCpp>());
    rclcpp::shutdown();
    return 0;
}

🖥️ 参数命令行工具

# 列出节点所有参数
ros2 param list /pid_controller

# 获取参数值
ros2 param get /pid_controller kp

# 设置参数值(运行时动态修改)
ros2 param set /pid_controller kp 2.5

# 查看参数类型和描述
ros2 param describe /pid_controller kp

# 导出所有参数为YAML
ros2 param dump /pid_controller

# 从YAML文件加载参数
ros2 param load /pid_controller config/pid_controller.yaml

🎯 练习题

📝 练习1:动态参数调优

运行PID控制器节点,使用 ros2 param set 动态调整Kp/Ki/Kd,观察控制输出的变化。

📝 练习2:参数持久化

使用 ros2 param dump 导出参数,修改后用 ros2 param load 重新加载。

📝 练习3:参数验证

在参数回调中添加验证逻辑:如果Kp设为负数,拒绝修改并返回错误。

🏆 成就解锁

🏅 参数配置专家

经验值:+150 XP