在这里插入图片描述
“Arduino BLDC 之基于尺寸差异的会车责任权重”可以理解为:在异构机器人集群中,把机器人的尺寸、制动距离、机动性和任务属性纳入会车优先级模型,让“更难避让、更难恢复、更难掉头”的一方承担更少避让责任,而“更灵活、更安全、更容易退让”的一方承担更多会车责任;最终通过 DWA/势场/优先级仲裁把责任权重转化为速度修正,并由 BLDC FOC 柔顺执行。它解决的核心问题是:如果所有机器人都用同一套避障规则,遇到窄道、门口、十字路口时容易出现“互相试探、同时让行、同时停滞”的对称死锁。 引入尺寸差异和责任权重后,通行权不再只由“谁先到”或“谁更近目标”决定,而是由“谁避让代价更高”来分配。
一、主要特点

  1. 尺寸差异被显式纳入会车决策
    异构机器人不仅长度、宽度不同,载货后尺寸还会变化。会车时不能只看质心距离,还要看包络宽度、转弯半径、最小会车通道和制动距离。
    例如,一台长轴距载货 AGV 和一台小型巡检车在窄道相遇,若让大车倒车或侧移,恢复成本高、风险高;让小车退到避让点通常更经济。
  2. 责任权重是多因素综合结果
    责任权重通常由以下因素共同决定:
    几何尺寸:宽度、长度、包络半径越大,避让空间需求越高;
    动力学特性:质量大、惯量大、制动距离长,不宜频繁急停;
    机动能力:差速、阿克曼、全向底盘的退让能力不同;
    负载状态:载货后最小转弯半径和制动距离会变化;
    任务紧急度:急救、关键搬运、主路径任务可获得更高通行权;
    路径属性:主干道、单行道、独享区域会影响通行顺序;
    等待时间:避免低优先级机器人长期饥饿。
  3. 责任权重转化为可执行的安全约束
    责任权重不是简单给机器人贴“高/低优先级”标签,而是会改变局部规划器的约束强度。高责任机器人应更早减速、更大侧向偏移、更长等待时间;低责任机器人可保持主路径,但也要受最小安全距离约束。
    在 DWA 中,这可以体现为障碍代价、邻居代价、偏航角代价和速度上限的动态调整;在势场法中,可体现为排斥势场强度随责任权重变化。
  4. 会车互斥区需要按尺寸动态膨胀
    不同尺寸机器人不能使用统一圆形安全半径。应使用椭圆包络、方向相关安全域或沿航向膨胀的安全走廊。对向会车时,互斥区长度还要包含双方制动距离和定位误差。
  5. BLDC FOC 让责任分配能平稳落地
    会车责任最终要变成减速、停车、倒车、侧移或原地转向。BLDC 配合 FOC 可以在低速下提供稳定力矩,减少普通 ESC 换相带来的顿挫和噪声,使让行动作更自然。 这对餐厅、医院、展厅等需要安静运行的场景尤其重要。
  6. 适合“上层责任仲裁 + 局部速度筛选 + 底层电机执行”的分层架构
    尺寸、任务、地图拓扑和邻居状态适合在上位机、ESP32-S3 或 STM32 上处理;DWA 局部筛选负责实时安全;Arduino 或同类 MCU 负责 BLDC 电流环、速度环、编码器采样和限幅保护。
    二、应用场景
  7. 仓储异构 AGV 会车
    载货 AGV 与空载 AGV、大型托盘车与小型料箱车在同一通道相遇。大车或载货车优先保持主路径,小车进入避让点或等待放行。
  8. 工厂混线物流
    不同尺寸机器人共享走廊、电梯口、装卸口。责任权重可避免大车频繁倒车,也防止小车长期被压制在路口。
  9. 医院/园区配送
    急救任务机器人、药品配送车和巡检小车共存。尺寸差异叠加任务紧急度后,可形成更合理的通行顺序。
  10. 餐厅/展厅服务机器人
    小型送餐车、导览屏车和清洁车在门口、拐角、餐桌间频繁交叉。尺寸差异不明显时,可更多依赖任务优先级和等待时间;尺寸差异明显时,大尺寸设备应减少急停急转。
  11. 科研与竞赛平台
    适合验证异构机器人优先级、动态责任权重、DWA 代价调制、窄道会车、死锁解除和 BLDC 柔顺退让。
    三、需要注意的事项
  12. 尺寸参数必须在线更新
    机器人空载、载货、挂载工具后包络尺寸会变化。若仍用固定半径,会车时要么过于保守,要么存在碰撞风险。应在任务开始或负载变化时更新长宽、质心偏移和最小转弯半径。
  13. 责任权重不能只由尺寸决定
    如果只按尺寸分配优先级,小车可能长期饥饿,大车也可能滥用通行权。应加入任务等级、等待时间、路径独占状态和制动能力,形成可解释的权重公式。
  14. 优先级必须全局一致
    异构机器人之间如果采用不同判定规则,例如 A 机按尺寸、B 机按任务、C 机按到达时间,仍会出现对称死锁。 应预先约定统一仲裁顺序,例如:安全硬约束 > 任务紧急度 > 尺寸/制动代价 > 等待时间 > ID 破 tie。
  15. 互斥区要随速度和方向变化
    静态圆形安全区不足以描述会车风险。对向高速接近时,纵向安全距离应随制动距离增加;侧向会车时,应按包络宽度和定位误差膨胀。
  16. DWA 代价函数需要动态调制
    高责任机器人在会车时应提高邻居障碍权重、降低速度上限;低责任机器人可提高目标导向权重,但仍需保留最小避让代价。权重切换应平滑,避免速度指令跳变。
  17. 退让动作必须限制距离和时间
    低责任机器人不能无限后退,否则可能退入其他冲突区或阻塞第三方。应设置最大退让距离、最大等待时间和重规划触发条件。
  18. 通信延迟会影响责任仲裁效果
    分布式系统中,邻居尺寸、速度、任务状态可能滞后。若基于过时信息判断通行权,可能误判。应加入时间戳、状态预测和通信丢失降级策略;通信不可靠时宁可保守停车。
  19. BLDC 执行层要限速、限流、限加速度
    会车退让常涉及低速倒车、原地转向和频繁微调。若不限制 q 轴电流和角速度,容易引起电压跌落、编码器噪声和底盘抖动。
  20. 8 位 Arduino 不适合承担完整责任仲裁
    ATmega328P 可用于 BLDC FOC 执行、编码器采样和基础速度跟踪;但异构尺寸建模、责任权重计算、DWA 多机代价评估和邻居通信更适合放在 ESP32、STM32 或上位机。
  21. 必须保留硬件级安全兜底
    无论责任权重如何分配,传感器失效、定位跳变、电机堵转或通信中断时,都应优先减速停机或进入保守等待。算法优先级不能高于硬件急停。
    整体来看,这个架构的关键是:尺寸差异决定“谁避让更贵”,责任权重决定“谁应该让”,DWA/势场把责任转化为速度约束,BLDC FOC 把让行和退让平稳执行,分层架构保证实时性和安全性。做原型时,建议先实现“单机尺寸包络 + 动态安全半径”,再加入“两机会车责任权重 → DWA 代价调制 → 退让点管理 → 多机饥饿保护”。

在这里插入图片描述
1、基于尺寸比的异构会车责任权重计算与差速执行
适用场景:小型配送机器人与大型运输机器人在狭窄通道相向而行。小型机器人机动性高、占道面积小,应承担更多避让责任;大型机器人尺寸大、转向笨拙,责任权重相应降低。核心逻辑是“尺寸因子决定谁多让”。

#include <SimpleFOC.h>

// 双轮 BLDC 差速底盘
BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9, 10, 11, 8), driverR(3, 5, 6, 7);
Encoder encL(2, 3, 2048), encR(18, 19, 2048);

// ===== 自身身份参数(小型配送机器人) =====
const float SELF_RADIUS = 0.20f;     // 自身半径 20cm
const float SELF_MOBILITY = 0.9f;    // 机动性评分 0~1,小型机灵活

// ===== 对向机器人参数(通过通信接收) =====
float oppRadius = 0.40f;             // 大型运输机器人 40cm
float oppMobility = 0.4f;            // 大型机机动性较低
float oppSpeed = 0.3f;
bool oppDataValid = false;

// ===== 责任权重 =====
float responsibilitySelf = 0.5f;     // 本机应承担的避让责任比例

void setup() {
  Serial.begin(115200);
  encL.init(); encR.init();
  motorL.linkSensor(&encL); motorR.linkSensor(&encR);
  driverL.voltage_power_supply = 24; driverR.voltage_power_supply = 24;
  driverL.init(); driverR.init();
  motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
  motorL.controller = MotionControlType::velocity;
  motorR.controller = MotionControlType::velocity;
  motorL.init(); motorL.initFOC();
  motorR.init(); motorR.initFOC();
  Serial.println("尺寸异构责任权重就绪");
}

// ===== 核心:基于尺寸与机动性的责任计算 =====
void computeSizeBasedResponsibility() {
  // 尺寸因子:对方越大,本方责任越大(小型机应多让)
  // 公式:oppRadius / (SELF_RADIUS + oppRadius)
  float sizeFactor = oppRadius / (SELF_RADIUS + oppRadius);
  
  // 机动性因子:自身越灵活,责任越大
  float mobilityFactor = SELF_MOBILITY / (SELF_MOBILITY + oppMobility);
  
  // 综合权重:尺寸占主导 60%,机动性辅助 40%
  responsibilitySelf = 0.6f * sizeFactor + 0.4f * mobilityFactor;
  
  // 约束在合理范围,避免极端值导致死锁或过度保守
  responsibilitySelf = constrain(responsibilitySelf, 0.3f, 0.8f);
}

// ===== 模拟通信接收(实际应用 ESP-NOW/NRF24L01) =====
void receiveOpponentData() {
  // 模拟:大型机广播自身参数
  oppRadius = 0.40f;
  oppMobility = 0.4f;
  oppDataValid = true;
}

void loop() {
  motorL.loopFOC(); motorR.loopFOC();
  
  receiveOpponentData();
  
  if (oppDataValid) {
    computeSizeBasedResponsibility();
  }
  
  // ===== 基于责任权重的速度调整 =====
  // 本机责任越大,减速越明显
  float baseSpeed = 2.0f;  // rad/s
  float speedScale = 1.0f - responsibilitySelf * 0.6f;  // 责任 0.7 → 速度降为 0.58
  float targetSpeed = baseSpeed * speedScale;
  
  // 差速执行(简化:直行减速)
  motorL.move(targetSpeed);
  motorR.move(targetSpeed);
  
  // 调试输出
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 500) {
    lastPrint = millis();
    Serial.print("责任权重: "); Serial.print(responsibilitySelf, 2);
    Serial.print(" | 目标速度: "); Serial.println(targetSpeed, 2);
  }
  
  delay(20);
}

解读:该案例直接采用了搜索结果中给出的责任计算公式:sizeFactor = oppRadius / (SELF_RADIUS + oppRadius),综合尺寸与机动性得到 responsibilitySelf,并约束在 [0.3, 0.8] 区间内。当小型机遇到大型机时,oppRadius 较大导致 sizeFactor 增大,本机责任上升,速度主动降低。执行层使用 FOC 速度环确保减速平滑,避免传统电机在会车减速时的顿挫感。

2、ARVO 自适应膨胀半径 + 责任权重的耦合避让
适用场景:会车双方距离较近时,仅靠责任权重减速不足以消除碰撞风险,需要将责任权重映射为 ARVO 的膨胀半径——责任越大的一方,其避让的“安全边界”扩张越明显,从而在速度空间中更早地剔除危险速度。

#include <SimpleFOC.h>

BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9, 10, 11, 8), driverR(3, 5, 6, 7);
Encoder encL(2, 3, 2048), encR(18, 19, 2048);

// 超声波
#define TRIG_C 22
#define ECHO_C 23

// 身份参数
const float SELF_RADIUS = 0.20f;
const float SELF_MOBILITY = 0.9f;
float oppRadius = 0.40f;
float oppMobility = 0.4f;

// 责任与膨胀半径
float responsibilitySelf = 0.5f;
float inflationRadius = 0.5f;

float readDistCM() {
  digitalWrite(TRIG_C, LOW); delayMicroseconds(2);
  digitalWrite(TRIG_C, HIGH); delayMicroseconds(10);
  digitalWrite(TRIG_C, LOW);
  long dur = pulseIn(ECHO_C, HIGH, 20000);
  return dur * 0.034f / 2.0f / 100.0f;  // 转米
}

void computeResponsibility() {
  float sizeFactor = oppRadius / (SELF_RADIUS + oppRadius);
  float mobilityFactor = SELF_MOBILITY / (SELF_MOBILITY + oppMobility);
  responsibilitySelf = constrain(0.6f * sizeFactor + 0.4f * mobilityFactor, 0.3f, 0.8f);
}

// ===== 自适应膨胀半径:责任越大,半径扩张越激进 =====
void computeAdaptiveInflation(float minDist) {
  // 基础半径 = 双方半径之和 + 安全余量
  float baseR = SELF_RADIUS + oppRadius + 0.15f;
  
  // 责任权重放大系数:责任 0.3→1.0x,责任 0.8→1.6x
  float respScale = 1.0f + (responsibilitySelf - 0.3f) * 1.2f;
  
  // 距离越近,额外膨胀越多
  float distBoost = 0.0f;
  if (minDist < 0.8f) {
    distBoost = (0.8f - minDist) * 0.5f;
  }
  
  inflationRadius = baseR * respScale + distBoost;
  inflationRadius = constrain(inflationRadius, 0.3f, 1.2f);
}

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_C, OUTPUT); pinMode(ECHO_C, INPUT);
  
  encL.init(); encR.init();
  motorL.linkSensor(&encL); motorR.linkSensor(&encR);
  driverL.voltage_power_supply = 24; driverR.voltage_power_supply = 24;
  driverL.init(); driverR.init();
  motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
  motorL.controller = MotionControlType::velocity;
  motorR.controller = MotionControlType::velocity;
  motorL.init(); motorL.initFOC();
  motorR.init(); motorR.initFOC();
}

void loop() {
  motorL.loopFOC(); motorR.loopFOC();
  
  computeResponsibility();
  
  float dist = readDistCM();
  computeAdaptiveInflation(dist);
  
  // ===== 基于膨胀半径的速度决策 =====
  // 膨胀半径越大,允许的最大速度越低
  float maxAllowedSpeed = 2.5f * (1.0f - inflationRadius * 0.4f);
  maxAllowedSpeed = constrain(maxAllowedSpeed, 0.3f, 2.5f);
  
  float targetSpeed = maxAllowedSpeed;
  
  // 若距离小于膨胀半径,进一步减速
  if (dist > 0 && dist < inflationRadius) {
    targetSpeed = maxAllowedSpeed * (dist / inflationRadius);
  }
  
  motorL.move(targetSpeed);
  motorR.move(targetSpeed);
  
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 300) {
    lastPrint = millis();
    Serial.print("责任:"); Serial.print(responsibilitySelf, 2);
    Serial.print(" 膨胀R:"); Serial.print(inflationRadius, 2);
    Serial.print(" 距离:"); Serial.print(dist, 2);
    Serial.print(" 速度:"); Serial.println(targetSpeed, 2);
  }
  
  delay(20);
}

解读:该案例将责任权重从“速度折扣”升级为“空间边界的动态扩张”。搜索结果中的原始代码给出了 computeAdaptiveRadius() 函数,其逻辑是:baseR = SELF_RADIUS + oppRadius + 0.15,距离小于 0.8m 时额外膨胀。本案例在此基础上引入了 respScale 因子,使责任权重直接作用于膨胀半径——小型机对大型机责任为 0.7 时,膨胀半径被放大 1.48 倍,ARVO 速度空间中的禁入区更早出现,从而在物理接触发生前就完成减速。

3、责任权重驱动的“优先通行”与“主动让行”状态机
适用场景:两台异构机器人在单车道走廊会车,需要明确谁先通过。尺寸差异决定了路权:大型机器人(尺寸大、机动性低)应获得优先通行权,小型机器人应主动退让。状态机根据责任权重阈值决定行为模式。

#include <SimpleFOC.h>

BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9, 10, 11, 8), driverR(3, 5, 6, 7);
Encoder encL(2, 3, 2048), encR(18, 19, 2048);

// 身份参数
const float SELF_RADIUS = 0.20f;
const float SELF_MOBILITY = 0.9f;
float oppRadius = 0.45f;     // 对方更大
float oppMobility = 0.3f;
float oppDist = 2.0f;        // 通过超声波/通信获取

// 责任权重
float responsibilitySelf = 0.5f;
const float YIELD_THRESHOLD = 0.55f;  // 责任超过此值,主动让行

// 状态机
enum ConvoyState { CRUISE, YIELDING, RESUMING };
ConvoyState state = CRUISE;

float baseSpeed = 1.8f;
float currentSpeed = 0;

void computeResponsibility() {
  float sizeFactor = oppRadius / (SELF_RADIUS + oppRadius);
  float mobilityFactor = SELF_MOBILITY / (SELF_MOBILITY + oppMobility);
  responsibilitySelf = constrain(0.6f * sizeFactor + 0.4f * mobilityFactor, 0.3f, 0.8f);
}

void setup() {
  Serial.begin(115200);
  encL.init(); encR.init();
  motorL.linkSensor(&encL); motorR.linkSensor(&encR);
  driverL.voltage_power_supply = 24; driverR.voltage_power_supply = 24;
  driverL.init(); driverR.init();
  motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
  motorL.controller = MotionControlType::velocity;
  motorR.controller = MotionControlType::velocity;
  motorL.init(); motorL.initFOC();
  motorR.init(); motorR.initFOC();
}

void loop() {
  motorL.loopFOC(); motorR.loopFOC();
  
  computeResponsibility();
  
  // 模拟:对方距离逐渐接近(实际由传感器获取)
  oppDist = max(0.3f, oppDist - 0.01f);
  if (oppDist < 0.5f) oppDist = 2.0f;  // 重置模拟
  
  // ===== 状态机决策 =====
  switch (state) {
    case CRUISE:
      currentSpeed = baseSpeed;
      // 会车条件:对方接近且本机责任超过阈值
      if (oppDist < 1.5f && responsibilitySelf > YIELD_THRESHOLD) {
        state = YIELDING;
        Serial.println("→ 进入让行状态");
      }
      break;
      
    case YIELDING:
      // 小型机主动减速,甚至停车让大型机先过
      if (oppDist < 0.8f) {
        currentSpeed = 0.0f;  // 近距离停车
      } else {
        currentSpeed = baseSpeed * 0.25f;  // 缓行
      }
      // 对方通过后恢复
      if (oppDist > 1.8f) {
        state = RESUMING;
        Serial.println("→ 对方已通过,恢复");
      }
      break;
      
    case RESUMING:
      // 平滑加速回正常速度
      currentSpeed = min(baseSpeed, currentSpeed + 0.1f);
      if (currentSpeed >= baseSpeed) {
        state = CRUISE;
      }
      break;
  }
  
  motorL.move(currentSpeed);
  motorR.move(currentSpeed);
  
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 400) {
    lastPrint = millis();
    Serial.print("责任:"); Serial.print(responsibilitySelf, 2);
    Serial.print(" 对方距离:"); Serial.print(oppDist, 1);
    Serial.print(" 状态:"); Serial.print(state);
    Serial.print(" 速度:"); Serial.println(currentSpeed, 2);
  }
  
  delay(20);
}

解读:该案例将责任权重从连续的速度调节,升级为离散的路权决策。当 responsibilitySelf > 0.55 时,小型机器人判定自己“应该让”,进入 YIELDING 状态,在近距离直接停车。这与搜索结果中关于 ARVO 责任分配的设计原则一致:责任权重的作用是打破对称性,避免双方同时减速、同时试探、同时加速的振荡。FOC 速度环的平滑加减速在这里至关重要——如果使用开环 ESC,从 baseSpeed * 0.25 恢复到 baseSpeed 的过程会出现明显顿挫,让对方机器人难以判断本机意图。

要点解读

  1. 尺寸因子的公式选择决定了“谁让谁”的直觉正确性
    搜索结果给出的 sizeFactor = oppRadius / (SELF_RADIUS + oppRadius) 是一个经过验证的简洁形式。它的含义是:当对方半径远大于自身时,sizeFactor 趋近于 1,本机责任趋近于上限;当双方尺寸相当时,sizeFactor = 0.5,责任回归中性。这个公式的物理直觉是“占道面积的比例”,小型机器人对大型机器人时天然承担更多避让义务。需要注意的是,SELF_RADIUS 和 oppRadius 必须是包含安全余量的有效半径,而非裸机械尺寸。

  2. 机动性因子是尺寸因子的必要补充,防止“小但笨”的机器人被错误分配高责任
    仅靠尺寸差异决定责任是不够的。一台小型但老旧、转向迟缓的机器人,如果被分配高避让责任,反而可能因为执行能力不足而无法完成避让。mobilityFactor = SELF_MOBILITY / (SELF_MOBILITY + oppMobility) 的作用是:机动性高的机器人承担更多责任,因为它的避让动作更可靠、对通行效率的损失更小。尺寸 60% + 机动性 40% 的权重分配是一个合理的起点,但实际部署时应根据场景调参——在狭窄通道中尺寸权重应提高,在开阔场地中机动性权重可以提高。

  3. 责任权重必须约束在有限区间内,否则会引发死锁或激进通行
    搜索结果的代码中明确使用了 constrain(responsibilitySelf, 0.3, 0.8)。下限 0.3 防止大型机器人产生“我完全不用让”的错觉——即使尺寸优势明显,它仍需承担最低限度的协同责任,否则会迫使小型机器人做出超出其运动能力的极限避让。上限 0.8 防止小型机器人过度保守——如果责任接近 1.0,它可能在远处就停车等待,在交通流量大的场景中造成不必要的通行效率损失。ARVO 框架的“互惠”本质要求双方都保留一定的避让义务。

  4. 责任权重到执行层的映射方式是架构设计的核心决策点
    三个案例展示了三种不同的映射路径:速度缩放(案例一)、膨胀半径调制(案例二)、状态机路权(案例三)。选择哪一种取决于场景的紧迫程度和机器人的感知能力。速度缩放最简单,但响应滞后;膨胀半径调制在 ARVO 速度空间层面生效,前瞻性更好,但需要准确的对方尺寸和距离信息;状态机路权适合结构化走廊,决策明确,但需要通信保证双方对“谁优先”的认知一致。实际系统可以组合使用:远距离用膨胀半径渐进压缩速度空间,近距离用状态机做明确让行/通行决策。

  5. 异构责任分配对通信的依赖远高于同构系统
    同构机器人之间可以假定“对方和我一样”,责任默认各半。异构系统中,oppRadius 和 oppMobility 必须通过通信获取,且这些参数一旦错误,责任权重会系统性偏移。如果一台小型机器人误将对方识别为大型机(oppRadius 偏大),它会过度避让,频繁停车;如果误将大型机识别为小型机,它可能做出对方无法完成的避让预期,导致碰撞。因此,通信中应携带尺寸和机动性的显式字段,而非仅广播位置和速度,并且接收端应对参数做合理性校验(如 oppRadius 不应小于 0.1m 或大于 2m)。搜索结果中 PPO-ARVO 的研究也强调了“基于尺寸的责任分配”需要可靠的尺寸信息输入。

在这里插入图片描述
4、三因子责任权重表 + 侧向让行量按份额分配
适用硬件:ESP32 + SimpleFOC 差速底盘(2×BLDC)+ 编码器 + 左右侧向超声(测通道净宽)+ ESP-NOW。
核心思路:责任权重用“让行代价”算,不用“尺寸”直接当权重。尺寸大、惯性大、转弯半径大 → 让行代价高 → 责任份额小(少让);份额双方加起来等于 1,天然对称。

/*
 * 案例4:尺寸/质量/机动性 -> 让行代价 -> 责任份额 -> 侧移量分配
 */
#include <SimpleFOC.h>

BLDCMotor wheelL(7), wheelR(7);
BLDCDriver3PWM drvL(9,10,11,8), drvR(22,23,24,25);
Encoder encL(2,3,2048), encR(32,33,2048);
void onL(){ encL.handleA(); } void onR(){ encR.handleA(); }

const float WHEEL_R = 0.065f, TRACK = 0.32f;

// ---- 异构车辆规格(同一份结构体,两台车各自填自己)----
struct Spec {
  uint8_t id;
  float width;    // 车体包络宽度 m
  float mass;     // kg
  float vmax;     // m/s
  float turnR;    // 最小转弯半径 m(机动性)
};
const Spec ME   = {1, 0.32f,  8.0f, 1.20f, 0.25f};   // 小型配送车
const Spec PEER = {2, 0.60f, 40.0f, 0.80f, 0.90f};   // 大型搬运车

const float MARGIN = 0.10f;   // 会车安全余量(每侧)

// ---- 让行代价:三项归一化加权 ----
float yieldCost(const Spec &s) {
  const float W_REF = 0.60f, M_REF = 40.0f, R_REF = 0.90f;
  float sz = s.width / W_REF;        // 尺寸因子
  float ms = s.mass  / M_REF;        // 惯性因子(刹车/让行更费力)
  float mb = s.turnR / R_REF;        // 机动性因子(转弯半径越大越僵)
  return 0.5f * sz + 0.3f * ms + 0.2f * mb;   // 权重可标定
}

// ---- 责任份额:本车承担的比例(双方和为 1)----
float yieldShare(const Spec &me, const Spec &peer) {
  float c1 = yieldCost(me), c2 = yieldCost(peer);
  return c2 / (c1 + c2);     // 本车代价低 -> 份额大 -> 多让
}

// ---- 侧向感知:通道净宽 + 当前横向偏移 ----
const int TRIG_L = 6, ECHO_L = 7, TRIG_R = 8, ECHO_R = 9;
float readUS(int t, int e) {
  digitalWrite(t, LOW); delayMicroseconds(2);
  digitalWrite(t, HIGH); delayMicroseconds(10); digitalWrite(t, LOW);
  long d = pulseIn(e, HIGH);
  return (d > 0) ? d * 0.034f / 2.0f / 100.0f : 3.0f;   // m
}

float corridorW, offCenter;      // 通道宽 / 本车偏离通道中心
void senseCorridor() {
  float dL = readUS(TRIG_L, ECHO_L), dR = readUS(TRIG_R, ECHO_R);
  corridorW = dL + dR + ME.width;
  offCenter = (dL - dR) * 0.5f;              // 正=偏右
}

float px = 0, py = 0, pth = 0, vCur = 0, wCur = 0;
float lateralCmd = 0;                        // 本次侧移目标(m,正=向右)

// ---- 会车决策:先判物理可行,再按份额分配 ----
enum Pass { GO, YIELD_MOVE, STOP_WAIT };
Pass decide(float &myOffset, bool &peerMustStop) {
  float need = ME.width + PEER.width + 2 * MARGIN;   // 并排通过所需宽度
  if (corridorW < need) {                            // 无法并排
    float sh = yieldShare(ME, PEER);
    if (sh > 0.5f) { myOffset = 0; peerMustStop = false; return STOP_WAIT; }
    else           { myOffset = 0; peerMustStop = true;  return GO; }  // 我先行
  }
  // 可并排:所需横向分离量
  float sepNeed = (ME.width + PEER.width) * 0.5f + MARGIN;
  float sepNow  = fabs(offCenter) * 2.0f;    // 粗略:双方各在中心线两侧
  float delta = max(0.0f, sepNeed - sepNow);
  myOffset = yieldShare(ME, PEER) * delta * 2.0f;   // 双方各移一半距离
  myOffset = constrain(myOffset, 0, 0.35f);          // 执行限幅
  peerMustStop = false;
  return (delta > 0.02f) ? YIELD_MOVE : GO;
}

void setup() {
  encL.init(); encL.enableInterrupts(onL);
  encR.init(); encR.enableInterrupts(onR);
  wheelL.linkSensor(&encL); wheelR.linkSensor(&encR);
  wheelL.controller = wheelR.controller = MotionControlType::velocity;
  wheelL.PID_velocity.P = 0.3f; wheelL.PID_velocity.I = 3.0f;
  wheelR.PID_velocity.P = 0.3f; wheelR.PID_velocity.I = 3.0f;
  wheelL.voltage_limit = wheelR.voltage_limit = 12.0f;
  wheelL.init(); wheelR.init();
  drvL.voltage_power_supply = drvR.voltage_power_supply = 12.0f;
  drvL.init(); drvR.init();
  wheelL.linkDriver(&drvL); wheelR.linkDriver(&drvR);
  pinMode(TRIG_L, OUTPUT); pinMode(ECHO_L, INPUT);
  pinMode(TRIG_R, OUTPUT); pinMode(ECHO_R, INPUT);
  Serial.begin(115200);
}

void loop() {
  senseCorridor();

  float myOffset; bool peerStop;
  Pass p = decide(myOffset, peerStop);

  float v = 0.8f, w = 0;
  switch (p) {
    case GO:        v = 0.6f; break;                       // 减速通过
    case YIELD_MOVE: v = 0.35f;                            // 边让边走
      w = constrain((myOffset - lateralCmd) * 1.5f, -0.8f, 0.8f);
      lateralCmd += constrain(myOffset - lateralCmd, -0.02f, 0.02f); break;
    case STOP_WAIT: v = 0; break;                          // 靠边等待
  }

  // 差速执行
  float vL = v - w * TRACK * 0.5f, vR = v + w * TRACK * 0.5f;
  wheelL.loopFOC(); wheelR.loopFOC();
  wheelL.move(vL / WHEEL_R); wheelR.move(vR / WHEEL_R);

  Serial.print("W="); Serial.print(corridorW, 2);
  Serial.print(" share="); Serial.print(yieldShare(ME, PEER), 2);
  Serial.print(" off="); Serial.print(myOffset, 2);
  Serial.print(" pass="); Serial.println(p);
  delay(50);
}

工程提示:yieldShare 在双方各自计算必须得到互补结果(A 得 0.72,B 得 0.28),所以规格参数必须广播后使用对方的真值,而非各自估计;MARGIN 与执行限幅 0.35m 是硬边界,权重大小不能突破物理可达范围。

5、动态责任权重 —— 尺寸基座 × 载荷/故障降级因子(FreeRTOS)
适用硬件:ESP32 双核 + 差速 BLDC + ESP-NOW + 载荷传感器(电流/称重)。
核心思路:静态尺寸权重只是“基座”,真实责任还要乘上动态因子——满载、传感器降级、电量低、牵引受限都应抬高自己的让行代价(即降低让行责任),让对方多让。权重随状态广播,双方用同一公式算,带迟滞防抖。

/*
 * 案例5:动态责任权重 = 尺寸基座 × (1 + 载荷 + 故障 + 低电)
 * 三任务:感知(20Hz) / 会车仲裁(10Hz) / 执行(1kHz)
 */
#include <SimpleFOC.h>
#include <esp_now.h>
#include <WiFi.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

BLDCMotor wheelL(7), wheelR(7);
// 驱动/编码器初始化同案例一(略)

SemaphoreHandle_t mux;

// ---- 广播的车辆档案 ----
struct PeerMsg {
  uint8_t id;
  float width, mass, turnR;    // 静态规格
  float loadRatio;             // 载荷比 0..1(满载=1)
  uint8_t fault;               // 0正常 1传感器降级 2牵引受限
  float battery;               // 0..1
  float x, y, th, v, w;
};
PeerMsg gPeer; bool gHavePeer = false;
uint8_t MY_ID = 1;

volatile float s_load = 0, s_batt = 1.0f;
volatile uint8_t s_fault = 0;

// ---- 动态让行代价 ----
float dynCost(float width, float mass, float turnR,
              float load, uint8_t fault, float batt) {
  const float W_REF = 0.60f, M_REF = 40.0f, R_REF = 0.90f;
  float base = 0.5f*(width/W_REF) + 0.3f*(mass/M_REF) + 0.2f*(turnR/R_REF);
  float k = 1.0f;
  k += 0.25f * constrain(load, 0, 1);          // 满载:让行风险高
  if (fault == 1) k += 0.20f;                  // 感知降级
  if (fault == 2) k += 0.40f;                  // 牵引受限
  if (batt < 0.20f) k += 0.15f;                // 低电:避免大机动
  return base * k;
}

float myShare() {
  float c1 = dynCost(ME_W, ME_M, ME_T, s_load, s_fault, s_batt);
  float c2 = dynCost(gPeer.width, gPeer.mass, gPeer.turnR,
                     gPeer.loadRatio, gPeer.fault, gPeer.battery);
  return c2 / (c1 + c2);
}

// ---- 仲裁输出 ----
volatile int  c_mode = 0;        // 0=正常 1=侧移让行 2=停车等待 3=先行
volatile float c_v = 0, c_w = 0;

// ---- 会车仲裁(10Hz,带迟滞)----
int modePrev = 0; unsigned long modeT0 = 0;
void taskArbiter(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    if (gHavePeer) {
      float sh = myShare();
      int want;
      if (sh > 0.62f)      want = 2;   // 我责任大 -> 停车让
      else if (sh > 0.52f) want = 1;   // 相当 -> 侧移让行
      else if (sh < 0.38f) want = 3;   // 我责任小 -> 先行
      else                 want = 1;

      // 迟滞:模式变化需持续 300ms,防止权重抖动导致来回切换
      if (want != modePrev) {
        if (modeT0 == 0) modeT0 = millis();
        else if (millis() - modeT0 > 300) { modePrev = want; modeT0 = 0; }
      } else modeT0 = 0;

      xSemaphoreTake(mux, portMAX_DELAY);
      c_mode = modePrev;
      c_v = (modePrev == 2) ? 0.0f : (modePrev == 3 ? 0.7f : 0.35f);
      xSemaphoreGive(mux);
    }
    vTaskDelayUntil(&last, pdMS_TO_TICKS(100));
  }
}

// ---- 感知(20Hz)----
void taskSense(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    xSemaphoreTake(mux, portMAX_DELAY);
    s_load = constrain(loadCellRead() / 30.0f, 0, 1);   // 30kg 满载
    s_batt = batteryRatio();
    s_fault = (imuHealthy() ? 0 : 1);
    xSemaphoreGive(mux);
    vTaskDelayUntil(&last, pdMS_TO_TICKS(50));
  }
}

// ---- 执行(1kHz)----
void taskExec(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    float v, w;
    xSemaphoreTake(mux, portMAX_DELAY);
    v = c_v; w = c_w;
    xSemaphoreGive(mux);
    float vL = v - w*0.16f, vR = v + w*0.16f;
    wheelL.loopFOC(); wheelR.loopFOC();
    wheelL.move(vL/0.065f); wheelR.move(vR/0.065f);
    vTaskDelayUntil(&last, pdMS_TO_TICKS(1));
  }
}

工程提示:动态因子必须只增不让行代价(乘 ≥1 的系数),否则“电量低反而更有路权”会出现反直觉行为。所有因子都应有上限(如 k ≤ 2.0),避免极端叠加后责任份额趋近 0/1,退化成“永远一方全让”。

6、通道可通行性预判 + 单列模式 + 按份额分侧移(连续闭环)
适用硬件:ESP32 + 差速 BLDC + 侧向超声阵列(3 路)+ ESP-NOW。
核心思路:先算“通行几何”再谈责任——通道宽 ≥ 双方宽 + 余量才允许并排会车;只能容一车时进“单列模式”,由责任份额小(灵活)的一方退到避让湾或贴边停,责任大的一方低速通过;并排时侧移量仍按份额连续分配。

/*
 * 案例6:可通行性三态判定(NORMAL / SQUEEZE / SINGLE_FILE)
 *        + 份额分侧移 + 避让湾退让
 */
#include <SimpleFOC.h>

BLDCMotor wheelL(7), wheelR(7);
// 驱动/编码器初始化同案例一(略)

struct Spec { uint8_t id; float width, mass, turnR; };
const Spec ME   = {1, 0.32f,  8.0f, 0.25f};
const Spec PEER = {2, 0.60f, 40.0f, 0.90f};
const float MARGIN = 0.10f;

float cost(const Spec &s) {
  const float WR=0.60f, MR=40.0f, RR=0.90f;
  return 0.5f*(s.width/WR) + 0.3f*(s.mass/MR) + 0.2f*(s.turnR/RR);
}
float share() { return cost(PEER) / (cost(ME) + cost(PEER)); }

// ---- 三路侧向测距(左/中/右,用于估通道宽与找空档)----
const int T[3] = {6, 8, 10}, E[3] = {7, 9, 11};
float dL, dC, dR;
void scan() {
  for (int i = 0; i < 3; i++) {
    digitalWrite(T[i], LOW); delayMicroseconds(2);
    digitalWrite(T[i], HIGH); delayMicroseconds(10); digitalWrite(T[i], LOW);
    long us = pulseIn(E[i], HIGH);
    float d = (us > 0) ? us * 0.034f / 2.0f / 100.0f : 3.0f;
    if (i == 0) dL = d; else if (i == 1) dC = d; else dR = d;
  }
}

enum Gate { NORMAL, SQUEEZE, SINGLE_FILE };
Gate gateOf(float wCorr) {
  float sideBySide = ME.width + PEER.width + 2*MARGIN;
  float single     = max(ME.width, PEER.width) + 2*MARGIN;
  if (wCorr >= sideBySide) return NORMAL;         // 可并排
  if (wCorr >= single)     return SQUEEZE;        // 勉强并排(需侧移)
  return SINGLE_FILE;                             // 只能单列
}

// ---- 连续侧移分配(含速率限幅与死区)----
float latCmd = 0;
float allocLateral(float delta, float sh) {
  float tgt = sh * delta * 2.0f;                  // 份额决定我移多少
  tgt = constrain(tgt, 0, 0.30f);                  // 机构可达上限
  if (fabs(tgt - latCmd) < 0.01f) return latCmd;   // 死区防抖
  latCmd += constrain(tgt - latCmd, -0.02f, 0.02f); // 20ms 限速 1m/s
  return latCmd;
}

// ---- 避让湾表:沿通道的加宽点(里程位置 m, 额外宽度 m)----
struct Bay { float s; float extraW; };
const Bay BAYS[] = {{3.0f, 0.55f}, {9.5f, 0.70f}, {16.0f, 0.60f}};
const int N_BAY = 3;
float alongS = 0;          // 沿通道里程(编码器积分)

bool bayAhead(float &dist) {
  for (int i = 0; i < N_BAY; i++) {
    float d = BAYS[i].s - alongS;
    if (d > 0.2f && d < 6.0f) { dist = d; return true; }
  }
  return false;
}

void setup() {
  // 电机初始化同案例一(略)
  for (int i = 0; i < 3; i++) { pinMode(T[i], OUTPUT); pinMode(E[i], INPUT); }
  Serial.begin(115200);
}

void loop() {
  scan();
  float wCorr = dL + dR + ME.width;        // 通道估算净宽
  Gate g = gateOf(wCorr);
  float sh = share();

  float v = 0.7f, w = 0;
  switch (g) {
    case NORMAL: {
      float sepNeed = (ME.width + PEER.width)*0.5f + MARGIN;
      float sepNow  = fabs(dL - dR);                    // 已有横向间隙
      float delta = max(0.0f, sepNeed - sepNow);
      float off = allocLateral(delta, sh);
      w = constrain((off - latCmd*0.5f) * 1.2f, -0.7f, 0.7f);
      v = 0.6f;
      break; }

    case SQUEEZE: {                                     // 缓慢贴边通过
      float off = allocLateral(0.12f, sh);              // 固定小幅侧移
      w = constrain((off - latCmd*0.5f) * 1.0f, -0.5f, 0.5f);
      v = 0.30f;
      break; }

    case SINGLE_FILE: {                                 // 单列:弱者退让
      float dist; bool hasBay = bayAhead(dist);
      if (sh > 0.5f) {                                  // 我责任大 -> 找湾退让
        if (hasBay && dist < 1.5f) { v = 0; w = 0; }     // 到湾停靠,等对方过
        else v = 0.25f;                                  // 慢速前往避让湾
      } else {
        v = 0.35f;                                       // 我责任小 -> 低速通过
      }
      break; }
  }

  // 里程与执行
  float vl = wheelL.shaft_velocity * 0.065f;
  float vr = wheelR.shaft_velocity * 0.065f;
  alongS += 0.5f * (vl + vr) * 0.05f;

  float vL = v - w*0.16f, vR = v + w*0.16f;
  wheelL.loopFOC(); wheelR.loopFOC();
  wheelL.move(vL/0.065f); wheelR.move(vR/0.065f);

  Serial.print("W="); Serial.print(wCorr, 2);
  Serial.print(" gate="); Serial.print(g);
  Serial.print(" share="); Serial.print(sh, 2);
  Serial.print(" v="); Serial.println(v, 2);
  delay(50);
}

工程提示:SINGLE_FILE 的“退让方找湾”必须配有超时升级——若 15s 内找不到避让湾,责任小的一方也停车、改用倒退让行(或上报调度),否则通道两端会无限对顶。避让湾表应按实际场地测绘,extraW 要含开门/转向的包络余量。

要点解读
一、责任权重必须由“让行代价”导出,不能直接用尺寸
尺寸只是代价的一个代理量。真正决定“谁该让”的是让开这一步对谁更贵:车体宽度(要占用多少通道)、质量(让行动作的风险与制动距离)、转弯半径(能否在不刮蹭的前提下侧移)。案例一把三者归一化后加权成单一代价,再用 share_i = c_j/(c_i+c_j) 转成份额——这个式子有个必需性质:双方算出来必须互补为 1。用尺寸直接当权重(w_A/(w_A+w_B))也能算出数,但一辆又宽又轻又灵活的车和一辆窄而重、转弯半径大的车会得到错误排序,现场表现就是“该让的不让”。

二、权重公式必须可广播、可复算、可标定,绝不能靠协商
异构车队最容易掉进的坑是“谈判式路权”:A 说我急,B 说我重,来回几轮谁都不动。正确做法是同一份公式 + 双方真值广播 + 各自本地计算(案例二把 width/mass/turnR/load/fault/battery 全打包广播)。任何一方算出的份额都一致,无需协商、无死锁、可离线复算。参数(0.5/0.3/0.2 这类系数)要写进配置并可标定,因为它直接定义了车队的“伦理”——系数一改,谁总让谁就变了。

三、权重只分配“让行量”,不分配“路权归属”
把份额直接映射成“让/不让”的二元判决,是振荡的根源:份额在 0.5 附近抖动就会反复切换。案例一、三的做法是让份额连续地决定侧移多少(Δ_i = share_i · Δ_total),份额大就让得多一点、小就让得少一点——没有切换、没有对峙。只有在份额极端(>0.62 或 <0.38)时才升级为“停车等待/先行”的离散模式,并且必须带 300ms 迟滞(案例二)。连续分配为主、离散判决为辅,是异构会车稳定的关键结构。

四、动态因子只能“抬高自己的代价”,且必须有上限
载荷、传感器降级、牵引受限、低电量都应让自己更不愿意让(代价乘 >1 的系数),因为让行动作对一台重载/降级的车风险更高。两个约束不能破:一是系数只增不减(否则会出现“电量越低路权越大”的荒谬行为),二是总系数设上限(如 k ≤ 2.0),否则极端叠加会让份额趋近 0 或 1,退化成“一方永远全让”,另一方的通行效率被无限牺牲。案例二的 k 结构就是这两条的直接实现。

五、物理可行性判定必须排在权重之前——权重不能创造空间
最容易犯的错是先算份额、再发现通道根本过不去。案例三先做几何三态判定:走廊宽 ≥ w_A + w_B + 2·margin 才允许并排;只够一车时进单列模式;再不够就必须有一方退到避让湾或倒退。权重的作用是在“可并排”的前提下分配侧移,而不是决定“能不能并排”。同时单列模式必须有超时升级与倒退兜底,否则两端责任都“不算大”的车会在窄道里无限对顶——这是异构车队现场最常见的死锁,且用权重永远解不开。

请注意:以上案例仅作为思路拓展的参考示例,不保证完全正确、适配所有场景或可直接编译运行。由于硬件平台、实际使用场景、Arduino 版本的差异,均可能影响代码的适配性与使用方法的选择。在实际编程开发时,请务必根据自身硬件配置、使用场景及具体功能需求进行针对性调整,并通过多次实测验证效果;同时需确保硬件接线正确,充分了解所用传感器、执行器等设备的技术规范与核心特性。对于涉及硬件操作的代码,使用前务必核对引脚定义、电平参数等关键信息的准确性与安全性,避免因参数错误导致硬件损坏或运行异常。

在这里插入图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐