ESC
输入关键词搜索文章标题和内容

ROS2 AGV导航从零到一与改进A星路径规划实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

ROS2 AGV导航从零到一与改进A星路径规划实战

导读:矿山爆破场景下的AGV,用ROS2构建控制系统,Navigation2实现自主导航,改进A+APF融合算法做路径规划。本文围绕阿克曼运动学、ros2_control控制器框架、改进A算法、势场法融合、Navigation2集成四大模块展开,附完整YAML配置和Python代码。


一、ROS2导航框架演进与架构特征

1.1 为什么要迁移到Navigation2

ROS1的move_base(2010年设计)存在若干问题:

问题 ROS1 move_base ROS2 Navigation2
架构 中心化,单点故障 生命周期管理,可热插拔
规划器 插件化但接口不一致 统一插件接口,行为树调度
计算图 ROS Master中心化 去中心化,DDS分布式
实时性 无确定性保证 支持QoS配置

Navigation2把导航看做行为树执行——根据当前状态(充电/导航/等待)切换不同的规划/控制策略,而不是一个简单的"给目标点就导航"的黑盒。

1.2 系统整体架构

AGV系统架构分为感知层、控制层、规划层、执行层四层:

flowchart TB subgraph 感知层["感知层"] direction TB L["激光雷达<br/>LDS-16"] IMU["IMU<br/>MPU6050"] end subgraph 控制层["控制层"] direction TB HW["HardwareInterface<br/>上位机"] SW["下位机<br/>STM32"] MOTOR["步进电机"] SERVO["舵机"] end subgraph 规划层["规划层"] direction TB ASTAR["改进A星<br/>全局规划"] APF["改进APF<br/>局部避障"] BT["行为树<br/>Navigator"] end subgraph 执行层["执行层"] direction TB CMD["cmd_vel<br/>速度命令"] POSE["PoseStamped<br/>目标位姿"] end L --> HW IMU --> HW HW --> SW SW --> MOTOR SW --> SERVO ASTAR --> BT APF --> BT BT --> CMD POSE --> BT CMD --> HW style L fill:#E3F2FD,stroke:#1976D2 style IMU fill:#E3F2FD,stroke:#1976D2 style HW fill:#FFF8E1,stroke:#F57C00 style SW fill:#FFF8E1,stroke:#F57C00 style MOTOR fill:#FFF8E1,stroke:#F57C00 style SERVO fill:#FFF8E1,stroke:#F57C00 style ASTAR fill:#F3E5F5,stroke:#7B1FA2 style APF fill:#F3E5F5,stroke:#7B1FA2 style BT fill:#F3E5F5,stroke:#7B1FA2 style CMD fill:#E8F5E9,stroke:#388E3C style POSE fill:#E8F5E9,stroke:#388E3C

二、ros2_control控制器框架与硬件抽象

2.1 为什么用ros2_control

ros2_control是ROS2的硬件抽象层,提供统一的接口让上层算法控制各种执行器。相比直接写串口驱动,选择ros2_control的原因:

对比 直接串口驱动 ros2_control
代码复用 每台机器人重写 通用控制器+硬件接口
生命周期 无 Managed lifecycle
插件化 无 标准化接口
调试工具 自研 rqt_controller_manager

2.2 阿克曼运动学

阿克曼转向的特点是内轮转向角比外轮转向角更大,这是前后轴中心位置不同导致的。

来自 linuxros.cn · linuxROS

汽车转弯时,前轮需要指向不同的角度,内侧轮比外侧轮转得更多,这样四个轮子才能绕同一个瞬时转向中心转动。

符号 含义
内轮转向角 转向时靠弯内侧的轮子转角
外轮转向角 转向时靠弯外侧的轮子转角
轴距 前后轴之间的距离
轮距 左右轮之间的距离

运动学模型(bicycle model)核心实现:

import numpy as np

class AckermannKinematics:
    """阿克曼运动学模型 (bicycle model)"""

    def __init__(self, wheelbase: float, max_steer: float = 0.5):
        self.L = wheelbase
        self.max_steer = max_steer

    def inverse_kinematics(
        self,
        v: float,
        omega: float,
        dt: float = 0.01
    ) -> dict:
        """逆运动学: 期望速度 → 电机/舵机指令"""
        if abs(omega) < 1e-6:
            return {"steer_angle": 0.0, "linear_vel": v}

        kappa = omega / v
        steer_angle = np.arctan(kappa * self.L)
        steer_angle = np.clip(steer_angle, -self.max_steer, self.max_steer)

        return {
            "steer_angle": float(steer_angle),
            "linear_vel": float(v),
            "curvature": float(kappa),
        }

    def forward_kinematics(self, v: float, steer_angle: float) -> dict:
        """正运动学: 电机/舵机指令 → 机器人速度"""
        kappa = np.tan(steer_angle) / self.L
        omega = kappa * v
        return {"vx": v, "omega": omega}

    def compute_wheel_angles(self, steer_angle: float, W: float) -> tuple:
        """计算左右轮独立转向角 (cot(ai) - cot(ao) = L/W)"""
        tan_s = np.tan(steer_angle)
        tan_ai = (W + self.L * tan_s) / (self.L - W * tan_s)
        return float(np.arctan(tan_s)), float(np.arctan(tan_ai))

# 使用示例
kin = AckermannKinematics(wheelbase=0.5, max_steer=0.6)
cmd = kin.inverse_kinematics(v=0.5, omega=0.3)
print(f"舵机角度: {np.degrees(cmd['steer_angle']):.1f}°")

2.3 ros2_control配置

controller_manager:
  ros__parameters:
    update_rate: 100  # Hz
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster
    ackermann_controller:
      type: diff_drive_controller/DiffDriveController

ackermann_controller:
  ros__parameters:
    wheelbase: 0.5           # 轴距 m
    wheel_radius: 0.1        # 轮子半径 m
    max_steering_angle: 0.6  # 最大转向角 rad
    linear:
      max: 2.0
      min: -0.5
    angular:
      max: 2.0
    odom_frame: odom
    base_frame_id: base_link
    enable_odom_tf: true
    velocity_rolling_window_size: 10

三、改进A星算法与全局路径规划

3.1 标准A星的缺陷分析

缺陷 标准A* 在矿山环境的危害
启发式函数 简单距离估算 未考虑坡度/障碍密度
搜索方式 4邻域或8邻域 矿山巷道狭窄,容易卡死
势场震荡 无局部极小处理 APF在狭窄通道震荡

针对上述缺陷,对启发函数、搜索邻域、势场震荡进行改进。

3.2 改进A星实现

import numpy as np
import heapq
from typing import Optional

class ImprovedAStar:
    """改进A*算法:密度加权启发+斜向惩罚+路径平滑"""

    DIRS_5 = [
        (0, 1, 1.0),    # 上
        (0, -1, 1.0),   # 下
        (1, 0, 1.0),    # 右
        (-1, 0, 1.0),   # 左
        (1, 1, 1.414),  # 右上
        (1, -1, 1.414), # 右下
        (-1, 1, 1.414), # 左上
        (-1, -1, 1.414) # 左下
    ]

    def __init__(self, grid: np.ndarray, resolution: float = 0.1):
        self.grid = grid
        self.resolution = resolution
        self.rows, self.cols = grid.shape
        self.obstacle_density = self._compute_density()

    def _compute_density(self) -> np.ndarray:
        """计算障碍物密度图 (3x3窗口平滑)"""
        from scipy.ndimage import uniform_filter
        return uniform_filter(
            (self.grid > 0.5).astype(float),
            size=3,
            mode='constant'
        )

    def heuristic(self, node: tuple, goal: tuple, alpha: float = 0.3) -> float:
        """改进启发函数: 欧氏距离 × (1 + α × 障碍密度)"""
        dist = np.sqrt((node[0] - goal[0])**2 + (node[1] - goal[1])**2)
        density = self.obstacle_density[
            min(node[0], self.rows-1), min(node[1], self.cols-1)
        ]
        return dist * (1.0 + alpha * density)

    def search(self, start: tuple, goal: tuple, alpha: float = 0.3) -> Optional[list]:
        """A*搜索,返回路径点列表"""
        open_set = []
        heapq.heappush(open_set, (0.0, start))
        came_from = {start: None}
        g_score = {start: 0.0}
        visited = set()

        while open_set:
            _, current = heapq.heappop(open_set)
            if current in visited:
                continue
            visited.add(current)
            if current == goal:
                return self._reconstruct_path(came_from, current)

            for dr, dc, cost in self.DIRS_5:
                nr, nc = current[0] + dr, current[1] + dc
                if not (0 <= nr < self.rows and 0 <= nc < self.cols):
                    continue
                if self.grid[nr, nc] > 0.5:
                    continue
                if (nr, nc) in visited:
                    continue

                # 斜向移动遇相邻障碍则加大cost
                move_cost = cost
                if abs(dr) == 1 and abs(dc) == 1:
                    if self.grid[current[0]+dr, current[1]] > 0.5 or \
                       self.grid[current[0], current[1]+dc] > 0.5:
                        move_cost *= 2.5

                tentative_g = g_score[current] + move_cost
                if (nr, nc) not in g_score or tentative_g < g_score[(nr, nc)]:
                    came_from[(nr, nc)] = current
                    g_score[(nr, nc)] = tentative_g
                    f = tentative_g + self.heuristic((nr, nc), goal, alpha)
                    heapq.heappush(open_set, (f, (nr, nc)))
        return None

    def _reconstruct_path(self, came_from: dict, current: tuple) -> list:
        path = [current]
        while current in came_from and came_from[current] is not None:
            current = came_from[current]
            path.append(current)
        path.reverse()
        return path

    def smooth_path(self, path: list) -> list:
        """路径平滑: 移除共线点"""
        if len(path) <= 2:
            return path
        smoothed = [path[0]]
        for i in range(1, len(path) - 1):
            d1 = (path[i][0] - path[i-1][0], path[i][1] - path[i-1][1])
            d2 = (path[i+1][0] - path[i][0], path[i+1][1] - path[i][1])
            if d1 != d2:
                smoothed.append(path[i])
        smoothed.append(path[-1])
        return smoothed

四、改进APF势场法与局部避障

4.1 传统APF的缺陷

缺陷 表现 矿山场景危险
局部极小 在U形障碍间振荡 巷道尽头无法逃脱
目标不可达 障碍物附近斥力>引力 爆破点无法到达
震荡 靠近障碍物时路径抖动 碰撞风险

4.2 改进斥力函数

改进的核心是距离阈值分段势场——斥力只在一定距离范围内生效,超过就归零,避免在远距离产生过大的排斥力导致震荡。

import numpy as np

class ImprovedAPF:
    """改进人工势场法:分段斥力+目标可达修复+速度平滑"""

    def __init__(
        self,
        k_att: float = 5.0,
        k_rep_max: float = 15.0,
        d0: float = 0.5,
        d_threshold: float = 2.0
    ):
        self.k_att = k_att
        self.k_rep_max = k_rep_max
        self.d0 = d0
        self.d_threshold = d_threshold
        self.prev_force = None

    def attractive_force(self, pos: np.ndarray, goal: np.ndarray) -> np.ndarray:
        """引力: F_att = k_att × (goal - pos)"""
        return self.k_att * (goal - pos)

    def repulsive_force(self, pos: np.ndarray, obs: np.ndarray) -> np.ndarray:
        """分段改进斥力:d>d_threshold时为0,d0<d<=d_threshold平滑衰减"""
        diff = pos - obs
        d = np.linalg.norm(diff) + 1e-8
        if d > self.d_threshold:
            return np.zeros(2)
        if d <= self.d0:
            factor = self.k_rep_max * (1.0/d - 1.0/self.d0)**2
        else:
            d_ratio = (self.d_threshold - d) / (self.d_threshold - self.d0)
            factor = self.k_rep_max * d_ratio**2 * (1.0/d - 1.0/self.d0)
        return factor * diff / d

    def compute_force(
        self,
        pos: np.ndarray,
        goal: np.ndarray,
        obstacles: list,
        prev_force: np.ndarray = None,
        smooth_alpha: float = 0.3
    ) -> tuple:
        """合力计算(含速度平滑)"""
        F_att = self.attractive_force(pos, goal)
        F_rep = np.zeros(2)
        for obs in obstacles:
            F_rep += self.repulsive_force(pos, np.array(obs))
        F_total = F_att + F_rep
        if prev_force is not None and smooth_alpha > 0:
            F_total = (1 - smooth_alpha) * F_total + smooth_alpha * prev_force
        return F_total, np.linalg.norm(F_total)

    def navigation_step(
        self,
        pos: np.ndarray,
        goal: np.ndarray,
        obstacles: list,
        dt: float = 0.1,
        max_speed: float = 0.5
    ) -> np.ndarray:
        """导航一步,返回更新后的位置"""
        F, mag = self.compute_force(pos, goal, obstacles, self.prev_force)
        if mag > 1e-6:
            direction = F / mag
            velocity = min(max_speed, mag * 0.1)
            new_pos = pos + direction * velocity * dt
        else:
            new_pos = pos.copy()
        self.prev_force = F
        return new_pos

5.1 Navigation2核心服务器架构

Navigation2通过行为树调度规划、控制、平滑、恢复四类服务器,配合代价地图与生命周期管理:

flowchart TB A(["🔴 接收NavigateToPose"]) --> B["BTNavigator<br/>行为树导航器"] B --> C["PlannerServer<br/>改进A星"] B --> D["ControllerServer<br/>DWB局部规划"] B --> E["SmootherServer<br/>路径平滑"] B --> F["BehaviorServer<br/>恢复行为"] C --> G["GlobalCostmap<br/>全局代价地图"] D --> H["LocalCostmap<br/>局部代价地图"] G --> I(["✅ LifecycleManager<br/>生命周期管理"]) H --> I F --> I style A fill:#FFEBEE,stroke:#D32F2F style B fill:#FFF8E1,stroke:#F57C00 style C fill:#E3F2FD,stroke:#1976D2 style D fill:#E3F2FD,stroke:#1976D2 style E fill:#F3E5F5,stroke:#7B1FA2 style F fill:#FFEBEE,stroke:#D32F2F style G fill:#E8F5E9,stroke:#388E3C style H fill:#E8F5E9,stroke:#388E3C style I fill:#E8F5E9,stroke:#388E3C

5.2 nav2_params.yaml 关键配置

amcl:
  ros__parameters:
    use_sim_time: False
    alpha1: 0.2
    alpha2: 0.2
    alpha3: 0.2
    alpha4: 0.2
    alpha5: 0.2
    base_frame_id: base_link
    beam_skip_distance: 0.5

bt_navigator:
  ros__parameters:
    use_sim_time: True
    global_frame_id: map
    robot_base_frame: base_link
    default_nav_to_pose_bt_xml: $(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_to_pose_via_points.xml
    plugin_lib_names:
      - nav2_compute_path_to_pose_action_bt_node
      - nav2_follow_path_action_bt_node
      - nav2_rate_controller_bt_node
      - nav2_recovery_node_bt_node
      - nav2_back_up_action_bt_node
      - nav2_spin_action_bt_node
      - nav2_wait_action_bt_node

controller_server:
  ros__parameters:
    use_sim_time: True
    controller_frequency: 20.0
    FollowPath:
      plugin: dwb_plugins::FollowPedestrianPath
    min_vel_x: -0.5
    max_vel_x: 0.5
    max_vel_theta: 1.0
    cost_weight: 1.0
    heading_weight: 0.5

planner_server:
  ros__parameters:
    use_sim_time: True
    planner_plugin_types: ["nav2_navfn_planner/NavfnPlanner"]
    planner_plugin_names: ["grid_based"]
    use_astar: true
    allow_unknown: true
    tolerance: 0.25

六、Gazebo仿真验证

实验平台架构(Gazebo矿山巷道仿真 → ROS2 Humble → Navigation2 → ros2_control阿克曼控制器 → Arduino下位机通讯 → 物理样机验证):

flowchart TB A["Gazebo<br/>矿山巷道仿真"] --> B["ROS2 Humble"] B --> C["Navigation2<br/>导航"] C --> D["ros2_control<br/>阿克曼控制器"] D --> E["Arduino<br/>下位机通讯"] E --> F(["✅ 物理样机验证"]) style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#F3E5F5,stroke:#7B1FA2 style D fill:#F3E5F5,stroke:#7B1FA2 style E fill:#FFF8E1,stroke:#F57C00 style F fill:#E8F5E9,stroke:#388E3C

七、开源仓库参考

仓库 内容
navigation2 ROS2官方导航框架,Lifecycle管理,行为树调度
nav2_smac_planner Hybrid-A*改进版,多分辨率搜索
ros2_controllers ROS2控制器生态
nav2_minimal 最小化Navigation2配置示例
turtlebot3 ROS2 TurtleBot3仿真
ackermann_msgs 阿克曼车辆标准消息类型

八、总结

模块 要点
ros2_control 生命周期管理,控制器插件化,HardwareInterface抽象
阿克曼运动学 Bicycle model,逆运动学求转向角
改进A* 障碍密度启发函数+斜向搜索惩罚+路径平滑
改进APF 分段斥力+目标可达修复+速度平滑
Navigation2 Lifecycle管理+行为树调度+代价地图+规划/控制分离

口诀:导航框架看Nav2,行为树调度四服务;A星加密度权重,APF分段斥力稳;阿克曼逆解转向角,ros2_control插件化。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页