
简介面向多机器人系统研究初学者与开发者这份压缩包围绕机器人编队、避障与一致性控制提供13个MATLAB脚本覆盖虚拟结构法、领导者跟随者法、一致性协议等经典算法实现可用于队形保持、路径规划与协同避障仿真验证。包内文件以.m为主共13个脚本包含基于非完整约束模型的编队仿真、有限时间一致性控制、噪声与误差分析以及多组对比绘图程序压缩包仅31KB整体结构小巧便于快速运行与二次开发。完整代码涵盖路径规划、避障算法、通信协议、一致性控制等模块读者可结合描述中的Laplacian矩阵、Lyapunov函数等理论实际观察多智能体协同行为提升参加机器人竞赛或课程项目时的调试与创新能力。已有285人学习该资源程序注释与可视化输出有助于理解编队控制的动态过程和参数调优思路。1. 机器人编队、编队避障和编队一致性的关系下载标着“编队控制.rar”的压缩包很容易难的是让里面的仿真模型变成你自己机器人上稳定的队形。机器人编队、避障和一致性是同一件事的三个面编队是目标几何关系避障是外部约束一致性是内部收敛。很多单机做得好的人一上多机就发现单机路径规划根本没考虑邻居状态结果队形散掉根源就在这里。所以这个标题真正要回答的是三个问题怎么保持队形、遇到障碍怎么散开再汇合、以及用什么指标判断编队完成了。下面把一致性协议和人工势场避障放在同一个控制律里用 Python 跑一个最小可闭环的仿真并给出参数边界和排查方法。适合正在做多机器人协同、无人机集群或者从单机路径规划转编队控制的工程师如果你有 STM32 避障小车或 ROS 底盘基础这里的障碍物输入只是换成传感器坐标而已。2. 编队一致性与避障的数学模型从拉普拉斯矩阵到势场选框架是第一步。工程里常用的编队框架有 Leader-Follower、虚拟结构、行为法和基于一致性Consensus的方法。标题里明确出现“编队一致性”所以下面以一致性为骨架避障作为外部扰动写入控制律这样能保证队形误差有明确的收敛边界。2.1 为什么一致性协议是编队控制的主干一致性思想非常简单每个机器人只和通信邻居交换状态并根据邻居状态和自己状态的差调整速度最终所有状态收敛到公共值。它不依赖全局编号节点掉线后剩余拓扑只要仍连通系统还能继续收敛这非常适合无线通信不稳的现场。编队和纯粹一致性只差一项在状态差中减去期望编队偏移 $\Delta_{ij}$。定义 $p_i [x_i, y_i]^T$ 为第 $i$ 个机器人的位置$N_i$ 是其邻居集合。一致性控制律为$$u_i(t) -k_c \sum_{j \in N_i} a_{ij} \left( p_i - p_j - \Delta_{ij} \right)$$这里 $a_{ij}$ 是邻接矩阵元素连上为 1断连为 0$k_c$ 是编队一致项增益。$\Delta_{ij}$ 表示编队中 $i$ 相对 $j$ 的期望位移例如期望正三角形三个机器人之间的 $\Delta$ 就是固定的三对向量。这个式子让相对位置误差指数衰减所以“编队一致性”本质上就是把这个减法写对。写对之后还要检查拓扑。通信拓扑可用有向图或无向图描述。无向图连通时系统能收敛有向图需要存在有向生成树。这也是后面排查队形收不拢时最先要看的地方。2.2 用拉普拉斯矩阵写出编队误差状态方程把控制律堆叠成矩阵形式。令 $p [p_1^T, \dots, p_N^T]^T$$d [d_1^T, \dots, d_N^T]^T$系统变成$$\dot{p} -k_c (L \otimes I_2)(p - d)$$其中 $\otimes$ 是 Kronecker 积$L D - A$ 是拉普拉斯矩阵$D$ 是度矩阵$A$ 是邻接矩阵。$(L \otimes I_2)$ 只是把二维位置的 x 和 y 分量分别应用同一个 $L$。$L$ 的性质决定了收敛速度若图连通$L$ 有一个零特征值其余特征值为正。最小非零特征值 $\lambda_2(L)$ 出现在收敛速度的指数项里$\lambda_2$ 越大队形误差衰减越快但过大又会导致系统对初始扰动很敏感。实际调试时我一般先把这个矩阵打印出来看特征值而不去空猜。import numpy as np # 3 个机器人通信图为环形0-1-2-0 A np.array([ [0, 1, 1], [1, 0, 1], [1, 1, 0]], dtypefloat) D np.diag(A.sum(axis1)) L D - A eig np.linalg.eigvalsh(L) print(拉普拉斯矩阵:\n, L) print(特征值:, np.round(eig, 4))这段代码构建无向完全图的拉普拉斯矩阵用eigvalsh算特征值。连通图里最小特征值为 0第二个特征值应当大于 0比如这里 3 个节点的环图 $\lambda_21$。如果你把邻接矩阵改成 0-1 断链第二个特征值会变 0收敛性就没了。这也是“程序跑起来队形收不拢”的常见元凶。2.3 避障势场如何写进控制律避障我常用人工势场法它不是编队控制的一部分而是叠加在一致性输出上的外部项。每个障碍物周围定义斥力势场$$U_{\text{rep}}(q) \frac{1}{2} k_{\text{rep}} \left( \frac{1}{\rho} - \frac{1}{\rho_0} \right)^2, \quad \rho \le \rho_0$$$\rho$ 是机器人到障碍物的距离$\rho_0$ 是斥力影响半径。势场的梯度取负就是斥力$$f_{\text{rep}} k_{\text{rep}} \left( \frac{1}{\rho} - \frac{1}{\rho_0} \right) \frac{1}{\rho^2} \hat{n}$$$\hat{n}$ 是从障碍物指向机器人的单位向量。这个式子保证障碍物越近斥力增长越快并且 $\rho \rho_0$ 处为 0不会在远处干扰队形。实际项目里障碍物通常不止一个我习惯把每个障碍物的斥力向量相加再做一个饱和限制防止某个离得太近的点产生脉冲力。静态势场处理不了快速移动的障碍。常见的落地做法是在动态避障小车路径规划里引入速度障碍法但那需要预测位置多机时计算量增长很快。编队中我的折中方案是加一项阻尼$f_{\text{dyn}} -b_{\text{rep}} \cdot v_{io}$其中 $v_{io}$ 是机器人相对障碍物的速度。这相当于把障碍逼近的速度也当成斥力来源既保留势场的实时性又让机器人提前减速。2.4 稳定性边界与编队刚度参数带避障后的完整控制率是$$u_i -k_c \sum a_{ij}(p_i - p_j - \Delta_{ij}) f_{\text{rep},i} k_g (G - p_i)$$最后一项 $k_g(G - p_i)$ 是虚拟中心引导项用来把整个队形拉到目标点 $G$。$k_g$ 过大会让前方机器人拖跑后面的机器人过小队伍又慢吞吞需要和 $k_c$ 一起调。稳定性上一致性项决定了系统的无扰动收敛性。只要 $\lambda_2(L) 0$没有障碍时误差指数收敛到 0有界斥力作为扰动输入会把编队误差限制在一个与 $k_{\text{rep}}$、$\rho_0$ 相关的边界内。这也是这种分层设计的工程优势只要一致性项比扰动能提供的恢复速度快避障散开的队形会自动收回来。下表是我在调参时的起点具体含义在第 4 章展开参数含义常见起点影响$k_c$一致项增益1.2加速收敛过大震荡$k_{\text{rep}}$斥力增益0.8避障强度过大压过队形$\rho_0$斥力作用半径1.2m小于队形尺寸时避障来不及$k_g$虚拟中心引导0.5整体目标跟踪速度$dt$仿真步长0.02s积分稳定性受实时性限制3. 用 Python 搭一套机器人编队避障的最小可跑程序下面给出一个不依赖 ROS 的 Python 仿真脚本模拟 3 个机器人在二维平面形成正三角形编队并向目标点运动途中避开一个静态圆形障碍。完整脚本可以直接保存为formation_avoidance.py运行。3.1 环境准备为什么先跑纯 Python 再上 ROS工程里常见做法是先在线下验证算法再移植到 ROS 或自己的底盘。我这里只用三样东西NumPy 做矩阵运算Matplotlib 画轨迹Python 3.8 以上版本。安装命令pip install numpy matplotlib这样你可以在一分钟内拿到一个能跑的编队闭环避开 ROS 编译和 TF 坐标的时间成本。真实机器人上的传感器坐标无论是octomap的点云还是海思双目避障输出的障碍框最终都要转换到机器人坐标系下替换代码里obstacles列表就行。3.2 一致性控制项与期望队形实现先定义机器人数量、期望队形和障碍物。队形用相对位置矩阵表示每一行是一个机器人在编队中心坐标系下的位置。import numpy as np import matplotlib.pyplot as plt N 3 k_consensus 1.2 # 一致项增益 k_goal 0.5 # 目标引导增益 k_rep 0.8 # 斥力增益 rho0 1.2 # 斥力作用半径 b_rep 0.6 # 动态避障阻尼 dt 0.02 # 控制周期 steps 800 goal np.array([5.0, 0.0]) # 期望正三角形中心在 (0,0) formation np.array([ [0.0, 0.0], [1.0, 0.0], [0.5, np.sqrt(3)/2] ]) # 静态障碍物列表每个元素是 (x, y, r) obstacles [(2.5, 0.0, 0.4)] # 初始位置故意偏离验证一致性收敛 pos np.array([ [0.2, 0.1], [1.2, -0.5], [0.8, 1.5] ])编队控制项按公式计算邻居关系默认是全连接即每个机器人都能看到其他两个。这里的delta[i, j]直接用formation[i] - formation[j]表示期望相对位移。def formation_force(pos, formation): F np.zeros_like(pos) for i in range(N): for j in range(N): if i j: continue err (pos[i] - pos[j]) - (formation[i] - formation[j]) F[i] -k_consensus * err return Ferr表示编队误差当它归零时机器人之间保持预定的相对位置。这个函数只负责队形不掺避障。实际项目中若通信有延迟k_consensus不能设太大否则误差收敛速度远快于通信周期会出现抖动。下表给出这个脚本里几个关键数据结构的含义变量维度含义pos(N,2)当前机器人位置formation(N,2)期望编队相对坐标obstacles列表障碍物位置与半径F_total(N,2)最终控制输入加速度3.3 避障势场和障碍物叠加以下函数遍历所有障碍物把每个障碍的斥力加总。公式和第二章一致但加上了一个限幅防止机器人紧贴障碍时斥力无穷大。def repulsion(pos, obstacles, k_rep, rho0): F np.zeros_like(pos) for i, p in enumerate(pos): for (ox, oy, r) in obstacles: d np.linalg.norm(p - np.array([ox, oy])) if d rho0 r and d 1e-6: rho d - r # 表面距离 n (p - np.array([ox, oy])) / d mag k_rep * (1.0/rho - 1.0/rho0) / (rho**2) F[i] mag * n # 限幅避免单步位移突变 norm np.linalg.norm(F[i]) if norm 5.0: F[i] * 5.0 / norm return F注意这里rho用的是机器人到障碍表面的距离而不是到障碍中心。对于有半径的圆形障碍这样才符合常理墙边贴得太近时斥力才显著上升。norm限幅是必须的如果不限幅一个 0.1 米近距离的障碍物会给出一个很大的力积分步长下直接导致位置飞出。主循环里把三项力相加用欧拉积分更新位置pos_history [pos.copy()] for step in range(steps): F_form formation_force(pos, formation) F_rep repulsion(pos, obstacles, k_rep, rho0) F_goal k_goal * (goal - pos) F_total F_form F_rep F_goal vel F_total * dt pos pos vel pos_history.append(pos.copy()) pos_history np.array(pos_history) print(终点位置:\n, pos)这里dt是控制周期实际系统里就是 50Hz。欧拉积分是显式的稳定条件大致要求 $k_{\text{consensus}} \cdot \lambda_{\max}(L) \cdot dt^2$ 不能太大实际中dt0.02s、k_c1.2通常没有稳定性问题。如果你把dt放大到 0.1队形大概率发散。3.4 用动画快速看编队避障的动态效果仿真代码跑完后最好把轨迹画出来。Matplotlib 的animation模块能直接看到队形如何散开、避障、再恢复import matplotlib.animation as animation fig, ax plt.subplots(figsize(6, 5)) ax.set_xlim(-1, 6) ax.set_ylim(-2, 3) ax.set_aspect(equal) lines [ax.plot([], [], o-, labelfbot {i})[0] for i in range(N)] def update(frame): for i in range(N): xs pos_history[:frame, i, 0] ys pos_history[:frame, i, 1] lines[i].set_data(xs, ys) return lines ani animation.FuncAnimation(fig, update, framessteps, interval20) plt.show()动画是一个比数据更直接的验证工具如果看到队形在目标点附近来回转圈通常是k_consensus太小如果看到某台机器人直接穿障碍说明rho0小于障碍物尺寸或者k_rep被限幅卡住了。这里我用的是全连接通信假设机器人之间没有丢包真机上丢包时会变为时变拓扑需要在一致性项里加入通信超时保护。4. 编队程序里的 5 个必调参数与典型故障4.1 编队控制器的参数一览与初始推荐编队程序跑不跑得稳一半在参数。以下五个参数是按影响优先级排的和第二章的表格对应但这里给出调整策略和故障表象。参数推荐初始值典型故障调整策略k_consensus1.0~1.5队形松散、收敛慢每档加 0.2超过 3.0 容易震荡k_goal0.4~0.8整体到不了目标提高但过大队形会被拉伸成直线k_rep0.6~1.0穿透障碍增大过大约队形变形大rho01.2~2.0 倍机器人尺寸避障太晚调大但要防止避障影响正常队形dt0.02s发散或位置跳变只能缩小不能放大我用过的最少一个工程里只调前三个就完成了动态避障后两个保持默认。关键点是k_rep和k_consensus的比例关系避障时斥力应该成为主导离开障碍后一致性项立即接管。如果k_rep / k_consensus太大机器人会远离障碍几米才停下队形恢复时间被拉长如果太小避障形同虚设。4.2 队形震荡把一致项增益降下来还是加阻尼震荡的典型现象是机器人在目标队形附近来回振荡轨迹呈锯齿形。初学者第一反应是降低k_consensus但很多时候问题出在dt和k_consensus的乘积也就是积分时间常数。我的排查方法是把速度轨迹画出来看速度符号是否在几个周期内频繁切换velocities np.diff(pos_history, axis0) / dt # 统计每个时刻第 0 个机器人的 x 速度符号切换次数 sign_change np.sum(np.diff(np.sign(velocities[:, 0, 0])) ! 0) print(符号切换次数:, sign_change)如果切换次数超过 100 次/800 步说明阻尼不足。此时不要一味降k_consensus应该先检查dt是否为 0.02再把k_consensus按 20% 步长下调。还有一种隐蔽来源虚拟中心引导项k_goal * (goal - pos)会在目标点附近形成持续振荡尤其是k_goal大于k_consensus的一半时。所以震荡不一定是编队项的问题可能只是目标跟踪过冲。4.3 队形收不拢拉普拉斯矩阵连通性与初始条件队形收不拢指的是机器人各自运动但迟迟形成不了预设队形。头号原因在通信拓扑断开。全连接图下任务步内收敛是必然的一旦有机器人收不到邻居消息a_{ij}变 0该节点就像断了线的风筝。要定位这个问题可以在每个控制周期打印一次邻接矩阵的特征值def check_connectivity(A): D np.diag(A.sum(axis1)) L D - A eig np.linalg.eigvalsh(L) return eig[1] 1e-5 # 第二个特征值大于0才算连通 A np.array([ [0, 1, 0], [1, 0, 0], [0, 0, 0]], dtypefloat) print(check_connectivity(A))这段代码把 0-1 和 1-2 的链路断开第二个特征值会变成 0输出False。真机上通信质量差时A每个周期都在变我建议把a_{ij}做一次低通滤波只有连续若干周期收到邻居数据才把置 1否则保留 0.5 的中间权重。这比直接硬切换稳定得多。另一个收不拢原因更容易被忽略formation矩阵本身没有平移不变性如果把期望编队中心设为目标点队形会整体被拉向目标导致形变。正确做法是只定义相对偏移中心单独由k_goal控制。4.4 避障死锁墙面障碍避障与动态避障的常见原因死锁是指机器人停在障碍前不断抖动既不绕行也不后退。传统势场对单个凸障碍没有问题但面对墙面或两个相距很近的障碍时多个斥力向量互相抵消合力接近零。常见处理方式是给控制律加切向扰动把斥力方向旋转一个小角度让机器人沿墙面滑动。另一种原因是k_rep过大而一致性项很小机器人在势场局部极小值点被“困住”。墙面障碍避障的真实路况里这种局部极小几乎无解所以我通常加一个随机扰动项if np.linalg.norm(F_total) 0.05: F_total np.random.normal(0, 0.2, sizeF_total.shape)这个扰动只在合力很小时才生效不影响正常队形却能打破势场对称性。对于快速移动的障碍还要注意动态避障阻尼b_rep的作用。如果机器人到了障碍附近才猛打方向多半是b_rep太小速度方向没有提前变的余地。调试动态避障时我会故意把障碍速度加 20% 再观察路径确保留出响应余量。5. 验证编队控制效果的一个具体技巧误差曲线与收敛判据5.1 记录编队误差定义收敛判据仿真跑完后不要只截图要在主循环里记录编队误差 $\epsilon(t)\sum_{i}\sum_{j} |p_i-p_j-\Delta_{ij}|^2$。下面这段代码给一个完整的判断逻辑error_history [] for step in range(steps): # 计算控制并更新位置 err 0.0 for i in range(N): for j in range(N): if i j: continue err np.linalg.norm((pos[i]-pos[j]) - (formation[i]-formation[j]))**2 error_history.append(err) # 连续 200 毫秒10步误差低于 0.05 认为收敛 tail np.array(error_history[-50:]) converged np.all(tail 0.05) print(是否收敛:, converged)这个判据比“看动画里队形像不像”要可靠。我把阈值取 0.05对应 1m 编队半径下相对误差约 5%。如果你做的是高精度仓库机器人那一类可以收紧到 0.01但要注意传感器噪声的影响。5.2 把避障次数和编队恢复时间也作为指标除了误差还要记录避障过程中队形被拉伸最大多少、恢复花了多久。具体做法每次repulsion函数输出了非零力就把当前时间计为避障起始之后第一次恢复误差阈值就是恢复时间。这个指标在动态避障小车路径规划里非常常用可以用来比较不同增益的优劣。5.3 从仿真到真机的三个衔接点真机替换时把obstacles列表换成octomap的占用栅格点或海思双目避障的矩形障碍格式需要做一次坐标旋转固定翼无人机编队还要加上高度层的一致性把 $p_i$ 从二维扩展为三维控制律写法完全不变。仿真里的dt0.02s对应 50Hz 控制周期真机如果只能跑到 20Hz必须重新检查稳定性边界不能直接照搬。把误差曲线保存下来和上一版参数跑出的曲线叠加对比你就能看到每个参数改动到底优化了什么。本文还有配套的精品资源点击获取