【花雕学编程】Arduino BLDC 之园区巡逻跟随机器人UWB+视觉多模态抗遮挡

“Arduino BLDC 之园区巡逻跟随机器人 UWB+视觉多模态抗遮挡”可以理解为:机器人以 UWB 提供稳定的目标相对定位,以视觉提供身份识别、语义判断和近距离精校正,再以 IMU、里程计和卡尔曼滤波进行短时遮挡下的航位推算;当目标被树木、车辆、行人短暂遮挡时,系统不会立即丢失,而是依靠融合估计维持跟随;最终由 BLDC FOC 把距离、角度和避障约束平滑落地为左右轮速度指令。
一、主要特点
- UWB 解决“目标在哪里”的底层定位问题
UWB 通过 TOF/TWR 或 PDoA/AoA 获得目标距离和方位角,厘米级精度、低延迟,且不依赖光照。 在园区夜间、逆光、树荫、地库等视觉不稳定场景下,UWB 能保持较稳定的相对定位。 - 视觉解决“目标是谁、当前姿态如何”的问题
视觉可用于人脸/人体检测、Re-ID、AprilTag 识别、深度估计和近距离精校正。它的优势是信息量大,能判断目标身份、朝向、是否跌倒、是否被误跟;缺点是受遮挡、逆光、粉尘、夜间低照度影响较大。 - 多模态抗遮挡的核心是“互补 + 切换 + 推算”
典型融合策略是:
UWB 与视觉一致时,按置信度加权融合;
视觉丢失但 UWB 仍有效时,以 UWB 为主;
UWB 跳变但视觉稳定时,以视觉为主;
两者短时异常时,用 IMU/里程计做航位推算;
超时仍未恢复,则原地搜索或返回安全点。 - 遮挡判定不能只看单传感器是否“有输出”
更稳健的做法是结合多指标:UWB 与视觉位置偏差、UWB 测距质量、视觉置信度、目标检测框大小、IMU 运动一致性、历史轨迹连续性。单一阈值容易把噪声误判为遮挡,或把真实遮挡漏判。 - 跟随控制通常采用“距离 + 航向 + 安全避障”三层约束
距离环保证机器人与目标保持期望间距;航向环保证机器人朝向目标方向;避障作为硬优先级,当前方有障碍、行人或台阶时,必须中断跟随。 园区环境还需要加入速度限幅、转弯限幅和跟停缓冲,避免急追急刹。 - BLDC FOC 让低速跟随更柔顺
园区巡逻跟随常在 0.2–1.0 m/s 低速区间运行,且需要频繁微调、原地转向和绕障。BLDC 配合 FOC 电流环、速度环和编码器闭环,可降低低速转矩脉动,使跟随轨迹更平滑,也更适合夜间安静场景。 - 分层架构更适合落地
感知层负责 UWB/视觉/IMU/编码器数据采集;融合层负责目标位置估计、遮挡判定和航位推算;决策层负责跟随距离、搜索策略和安全降级;执行层负责 BLDC 速度/电流闭环。 ATmega328P 适合做电机执行和传感器采样,但完整多模态融合、视觉推理和卡尔曼滤波更适合放在 ESP32-S3、STM32 或上位机。
二、应用场景
- 园区保安人机协同巡逻
保安佩戴 UWB 标签,机器人保持侧后方跟随,夜间利用视觉/红外扫描环境,发现异常时定点记录并上报。 - 厂区物料随行
工人搬运物料时,机器人跟随其移动,避免频繁往返取料。UWB 保证在货架、设备间稳定定位,视觉辅助确认目标身份。 - 园区巡检与设备点检
机器人跟随巡检人员到达配电箱、消防栓、管廊口等点位,自动拍照、记录位置,并在人员短暂离开时保持等待。 - 园区导览与接待跟随
访客佩戴标签或通过视觉绑定身份,机器人跟随讲解。视觉可判断访客是否停留、转向或离开,UWB 保证在人群交叉时不易跟丢。 - 校园/社区安防巡逻
夜间光线不足、树荫遮挡、车辆临时停放较多,单一视觉容易失效。UWB+视觉融合可提升连续跟随能力,机器人也可承担照明、告警和远程视频回传任务。 - 科研与竞赛平台
适合验证 UWB PDoA/TWR、YOLO/AprilTag 识别、EKF 融合、遮挡恢复、跟随状态机和 BLDC 差速闭环。
三、需要注意的事项
- UWB 并非完全不怕遮挡
UWB 对轻遮挡、非视距环境比视觉更鲁棒,但金属墙体、密集货架、混凝土柱、地面反射仍会造成多径和 NLOS 误差。 部署时应避免大面积金属反射,基站/标签安装高度和角度也需要实测。 - 视觉需要处理身份绑定与 ID 切换
多人交叉、相似服装、目标转身、短暂出画都会导致视觉 ID 漂移。应结合 UWB 标签 ID、历史轨迹、运动一致性和置信度阈值,防止跟错人。 - 遮挡判定要有迟滞和超时机制
不能因为一帧检测失败就判定丢失,也不能长时间维持不可信估计。应设置遮挡进入阈值、恢复阈值、最大推算时间和最大推算距离,超时后进入原地搜索或安全停机。 - 融合算法要考虑时间同步和坐标系对齐
UWB、视觉、IMU、编码器采样率不同,时间戳不一致会导致滤波发散;各传感器坐标系也必须通过外参标定对齐。 - 跟随距离应随环境动态调整
开阔区域可适当增大跟随距离,提高通行效率;人流密集、拐角、坡道、地库入口应缩短距离并降低速度;夜间或低照度场景应提高避障权重。 - 避障优先级必须高于跟随
无论目标位置多么明确,前方出现行人、车辆、台阶、玻璃门或狭窄通道时,都应先保证安全。避障触发时应挂起跟随,恢复后再重新锁定目标。 - BLDC 执行层需限速、限流、限加速度
跟随过程中目标可能突然加速、急停或转向。若不限制线速度、角速度、加速度和 q 轴电流,容易造成电压跌落、底盘抖动、目标丢失甚至碰撞。 - 8 位 Arduino 不宜承担完整融合与视觉推理
ATmega328P 可用于 FOC 执行、编码器采样、IMU 读取和简单跟随控制;但 UWB 滤波、视觉检测、EKF 融合、状态机管理和通信调度更适合放在 ESP32-S3、STM32 或上位机。 底层 MCU 应保证电机控制周期稳定,不被视觉推理或串口通信阻塞。 - 必须保留失联、低电、急停等安全兜底
当 UWB 长时间失联、视觉持续丢失、IMU 异常、编码器故障、电池欠压或通信中断时,应减速停机或进入保守等待。算法层面的抗遮挡不能替代硬件级安全保护。
整体来看,这个架构的关键是:UWB 提供稳定相对定位,视觉提供身份与语义校正,融合层处理遮挡与推算,决策层管理跟随距离和安全优先级,BLDC FOC 把指令柔顺执行。做原型时,建议先跑通“UWB 单目标跟随 + BLDC 速度闭环”,再加入“视觉身份绑定 → 多模态融合 → 遮挡判定 → 航位推算 → 避障硬优先级 → 失联安全降级”。

1、UWB/视觉置信度加权融合 + 遮挡降级跟随
适用场景:巡逻人员佩戴 UWB 标签,机器人前方摄像头同时检测人员视觉特征。当视觉因人群遮挡或光照突变失效时,系统自动降低视觉权重、提升 UWB 权重,保持跟随连续性而非急停。
#include <SimpleFOC.h>
BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9,10,11,8), driverR(3,5,6,7);
Encoder encL(18,19,2048), encR(20,21,2048);
// ===== 多模态感知输入(由外部模块通过串口/ROS 更新) =====
float uwbDist = 2.0, uwbAngle = 0.0; // UWB 测距与测角
bool uwbValid = false;
float visionDist = 2.0, visionAngle = 0.0; // 视觉估算距离与角度
bool visionValid = false;
// 融合输出
float fusedDist = 2.0, fusedAngle = 0.0, fusedConfidence = 0.0;
// 跟随参数
const float TARGET_DIST = 1.5; // 目标跟随距离 (m)
const float MAX_SPEED = 0.8; // m/s
// ===== 多模态融合 =====
void fuseSensors() {
float uwbWeight = 0.0, visionWeight = 0.0;
// UWB 置信度:有效且距离在合理范围
if (uwbValid && uwbDist > 0.3 && uwbDist < 10.0) {
uwbWeight = 0.6; // UWB 基础权重
// 距离越远,UWB 相对视觉更可靠(视觉远距离精度下降)
if (uwbDist > 3.0) uwbWeight = 0.8;
}
// 视觉置信度:有效且目标在视野中央区域(边缘精度低)
if (visionValid && abs(visionAngle) < 0.6) {
visionWeight = 0.8; // 视觉在近距离、正前方时精度高
if (visionDist > 3.0) visionWeight = 0.4; // 远距离视觉降权
}
// 归一化融合
float totalWeight = uwbWeight + visionWeight;
if (totalWeight > 0.01) {
fusedDist = (uwbDist * uwbWeight + visionDist * visionWeight) / totalWeight;
fusedAngle = (uwbAngle * uwbWeight + visionAngle * visionWeight) / totalWeight;
fusedConfidence = totalWeight / 1.4; // 归一化到 0~1
} else {
fusedConfidence = 0.0;
}
}
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();
// 模拟传感器更新(实际从串口/ROS 读取)
// uwbValid = ...; visionValid = ...;
fuseSensors();
// ===== 跟随控制 =====
float linearCmd = 0, angularCmd = 0;
if (fusedConfidence > 0.15) {
// 距离误差 → 线速度(PID 简化)
float distError = fusedDist - TARGET_DIST;
linearCmd = constrain(distError * 0.8, -MAX_SPEED, MAX_SPEED);
// 角度误差 → 角速度
angularCmd = constrain(fusedAngle * 1.2, -1.5, 1.5);
// 死区
if (abs(linearCmd) < 0.05) linearCmd = 0;
if (abs(angularCmd) < 0.1) angularCmd = 0;
} else {
// 双模态均失效:安全停车
linearCmd = 0;
angularCmd = 0;
}
// 差速解算
float wheelBase = 0.3;
float vL = linearCmd - angularCmd * wheelBase / 2;
float vR = linearCmd + angularCmd * wheelBase / 2;
motorL.move(vL / 0.05);
motorR.move(vR / 0.05);
delay(50);
}
解读:该案例的核心是置信度动态加权而非硬切换。搜索结果中的原始代码采用“偏差超过 1m 判定遮挡 → 切换惯性推算”的硬逻辑,但硬切换在临界点附近容易产生振荡。本案例改为连续加权:视觉在近距离正前方权重高,UWB 在远距离和视觉失效时权重自然上升。当两者都失效时,fusedConfidence 降至阈值以下,机器人安全停车而非盲目惯性推算——园区场景中,盲目推算比停车更危险。IEEE 的研究同样指出,UWB 在视觉失效时接管是提升鲁棒性的关键。
2、UWB 主导 + 视觉侧向约束的纵横向解耦跟随
适用场景:园区道路有明确边界(路沿、绿化带),UWB 负责纵向距离保持,视觉负责横向纠偏。两者解耦处理,避免融合后的控制量相互干扰。
#include <SimpleFOC.h>
BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9,10,11,8), driverR(3,5,6,7);
Encoder encL(18,19,2048), encR(20,21,2048);
// UWB 纵向
float uwbDist = 2.0;
bool uwbValid = false;
// 视觉横向(车道线/路沿检测,输出横向偏差)
float visionLateralOffset = 0.0; // 正=偏右,负=偏左 (m)
bool visionValid = false;
// 控制参数
const float TARGET_DIST = 1.8;
float Kp_long = 0.6; // 纵向 P
float Kp_lat = 1.5; // 横向 P
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();
// ===== 纵向:UWB 主导 =====
float linearCmd = 0;
if (uwbValid && uwbDist > 0.3 && uwbDist < 8.0) {
float distError = uwbDist - TARGET_DIST;
linearCmd = constrain(distError * Kp_long, -0.6, 0.6);
} else {
linearCmd = 0; // UWB 失效,停止纵向运动
}
// ===== 横向:视觉主导 =====
float angularCmd = 0;
if (visionValid && abs(visionLateralOffset) < 1.5) {
// 横向偏差 → 角速度修正(负反馈)
angularCmd = constrain(-visionLateralOffset * Kp_lat, -1.2, 1.2);
} else {
// 视觉失效:保持当前航向,不额外转向
angularCmd = 0;
}
// 前进时横向修正才有意义;原地转向会放大噪声
if (abs(linearCmd) < 0.1) {
angularCmd *= 0.3;
}
// 差速
float wheelBase = 0.3;
float vL = linearCmd - angularCmd * wheelBase / 2;
float vR = linearCmd + angularCmd * wheelBase / 2;
motorL.move(vL / 0.05);
motorR.move(vR / 0.05);
delay(50);
}
解读:该案例借鉴了学术研究中“UWB 纵向 + 视觉横向解耦”的思路。搜索结果中的融合方案将距离和角度加权平均后统一控制,但在园区结构化道路场景中,纵向跟随距离和横向道路约束的物理意义不同,加权融合会引入耦合误差。解耦后,UWB 失效时纵向停车但横向保持,视觉失效时横向保持当前航向但纵向继续——两者的降级策略各自独立,更符合实际失效模式。需要注意:横向视觉检测需要稳定的道路边界特征,若园区路面无标线,可替换为视觉检测巡逻人员相对机器人的横向像素偏差。
3、抗遮挡状态机 + S 型加减速平滑执行
适用场景:巡逻人员频繁穿梭于人群或设备之间,UWB 和视觉交替失效。状态机管理“正常跟随 / 单模态降级 / 惯性维持 / 安全停车”四种模式,BLDC 执行层用 S 型曲线柔化频繁启停的冲击。
#include <SimpleFOC.h>
BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9,10,11,8), driverR(3,5,6,7);
Encoder encL(18,19,2048), encR(20,21,2048);
// 传感器
float uwbDist = 2.0, uwbAngle = 0.0;
bool uwbValid = false;
float visionDist = 2.0, visionAngle = 0.0;
bool visionValid = false;
// 状态机
enum FollowState { NORMAL, UWB_ONLY, VISION_ONLY, DEAD_RECKON, SAFE_STOP };
FollowState state = NORMAL;
unsigned long stateTimer = 0;
const unsigned long DEAD_RECKON_TIMEOUT = 800; // 惯性推算最长 800ms
// 惯性推算
float lastValidDist = 2.0, lastValidAngle = 0.0;
float imuYawRate = 0.0;
// S 型加减速
float currentSpeed = 0;
float targetSpeed = 0;
float sCurveRamp(float current, float target, float dt) {
float maxAccel = 1.5; // m/s²
float diff = target - current;
if (abs(diff) < 0.02) return target;
float step = maxAccel * dt;
if (diff > 0) return min(current + step, target);
else return max(current - step, target);
}
void updateState() {
bool uwbOk = uwbValid && uwbDist > 0.3 && uwbDist < 8.0;
bool visionOk = visionValid && abs(visionAngle) < 0.8;
if (uwbOk && visionOk) {
state = NORMAL;
lastValidDist = (uwbDist + visionDist) / 2;
lastValidAngle = (uwbAngle + visionAngle) / 2;
} else if (uwbOk && !visionOk) {
state = UWB_ONLY;
lastValidDist = uwbDist;
lastValidAngle = uwbAngle;
} else if (!uwbOk && visionOk) {
state = VISION_ONLY;
lastValidDist = visionDist;
lastValidAngle = visionAngle;
} else {
// 双模态失效
if (state != DEAD_RECKON && state != SAFE_STOP) {
state = DEAD_RECKON;
stateTimer = millis();
}
if (state == DEAD_RECKON && millis() - stateTimer > DEAD_RECKON_TIMEOUT) {
state = SAFE_STOP;
}
}
}
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();
updateState();
float dist, angle;
float targetLinear = 0, targetAngular = 0;
switch (state) {
case NORMAL:
dist = (uwbDist + visionDist) / 2;
angle = (uwbAngle + visionAngle) / 2;
targetLinear = constrain((dist - 1.5) * 0.7, -0.7, 0.7);
targetAngular = constrain(angle * 1.0, -1.2, 1.2);
break;
case UWB_ONLY:
targetLinear = constrain((uwbDist - 1.5) * 0.7, -0.7, 0.7);
targetAngular = constrain(uwbAngle * 0.8, -0.8, 0.8); // UWB 角度精度有限,降增益
break;
case VISION_ONLY:
targetLinear = constrain((visionDist - 1.5) * 0.7, -0.7, 0.7);
targetAngular = constrain(visionAngle * 1.2, -1.2, 1.2);
break;
case DEAD_RECKON:
// 基于最后有效角度和 IMU 航向变化推算
targetLinear = 0.3; // 低速维持
targetAngular = lastValidAngle * 0.5 + imuYawRate * 0.1;
break;
case SAFE_STOP:
targetLinear = 0;
targetAngular = 0;
break;
}
// S 型平滑
currentSpeed = sCurveRamp(currentSpeed, targetLinear, 0.05);
float wheelBase = 0.3;
float vL = currentSpeed - targetAngular * wheelBase / 2;
float vR = currentSpeed + targetAngular * wheelBase / 2;
motorL.move(vL / 0.05);
motorR.move(vR / 0.05);
delay(50);
}
解读:该案例将搜索结果中的“遮挡检测 → 惯性推算”硬逻辑扩展为带超时的状态机。关键设计是 DEAD_RECKON_TIMEOUT = 800ms:惯性推算超过此时间仍未恢复任一传感器,强制进入 SAFE_STOP。园区巡逻场景中,巡逻人员可能进入建筑物或转角,UWB 和视觉同时丢失超过 1 秒是正常的,此时继续惯性推算是危险的——机器人可能撞上巡逻人员刚刚绕过的障碍物。S 型加减速函数 sCurveRamp 解决了频繁启停的顿挫问题,这是 BLDC + FOC 相对有刷电机的核心优势,搜索结果中也强调了 S 型算法在园区跟随场景中的必要性。
要点解读
-
抗遮挡的核心不是“预测目标在哪”,而是“知道何时该放弃预测”
搜索结果中原始代码的逻辑是“偏差超过 1m 判定遮挡,切换惯性推算”。但惯性推算的误差随时间累积,800ms 后推算位置可能偏离实际目标数米。案例三设置超时强制停车,原则是:短时遮挡(<800ms)用推算维持平滑,长时丢失(>800ms)停车等待是更安全的选择。园区环境中巡逻人员可能进入设备间或转角,机器人不应盲目追入未知区域。IEEE 研究也指出,UWB 接管的目的是“维持跟随直到视觉恢复”,而非无限期替代视觉。 -
UWB 与视觉的权重应随距离和角度动态调整,而非固定比例
UWB 在远距离(>3m)的测距精度相对稳定,但测角精度有限;视觉在近距离正前方精度高,但远距离和边缘视野误差增大。案例一采用 uwbDist > 3.0 时 UWB 权重升至 0.8、abs(visionAngle) < 0.6 时视觉权重升的规则,使融合输出在两种传感器的“各自优势区间”内偏向更可靠的一方。固定 50:50 加权在远距离会拉低 UWB 精度,在近距离会拉低视觉精度。 -
园区巡逻场景的跟随是“纵横向解耦”问题,不是单一距离控制
案例二借鉴了农业机器人研究中的解耦思路:纵向(前后距离)由 UWB 负责,横向(左右偏移)由视觉负责。这种分工的合理性在于:UWB 测距的物理基础是飞行时间,对纵向距离敏感;视觉的横向像素偏差对左右位置敏感。将两者加权融合为一个“距离+角度”的复合值会引入耦合误差——UWB 的角度噪声会污染纵向距离控制。解耦后,任一传感器失效时只需降级其对应的控制通道,另一通道不受影响。 -
BLDC 的 FOC 平滑执行是“人走车随”体感的关键,而非单纯动力选择
园区巡逻跟随中,巡逻人员的步速频繁变化,机器人需要频繁加减速。有刷电机配合简单 PWM 在低速段存在死区和齿槽效应,加减速时会产生顿挫,让被跟随者感到“车在拽我”或“车跟不上”。FOC 速度环的毫秒级响应配合 S 型加减速曲线(案例三的 sCurveRamp),使速度指令的变化被柔化执行。搜索结果中明确将“BLDC + S 型加减速”列为园区跟随场景的核心配置,原因正在于此。 -
Arduino 主控的算力分配决定了视觉处理必须外置
案例中的视觉输入(visionDist、visionAngle)是作为外部变量存在的,实际系统中这些值来自树莓派、K210 或 ESP32-S3 等协处理器。Arduino/ESP32 主控负责的是传感器融合、状态机决策和 FOC 执行。若试图在 Arduino 上直接运行视觉算法(如 AprilTag 检测或 YOLO 推理),FOC 环的定时抖动会严重恶化。合理的分工是:视觉协处理器以 10-30Hz 输出目标位置,主控以 50-100Hz 运行融合与状态机,FOC 环以 10kHz+ 运行。搜索结果中关于 UWB 精度的讨论也隐含了这一架构:UWB 模块本身通常通过串口输出测距数据,主控只需解析而非处理原始射频信号。

4、UWB + 视觉置信度门控切换(抗遮挡最小闭环)
适用硬件:ESP32 + SimpleFOC 差速底盘(2×BLDC)+ 编码器 + DW1000/DWM1000 UWB(目标佩戴标签)+ HuskyLens/ESP32-CAM 视觉模块(串口输出目标框)。
核心思路:两个传感器不做像素级融合,而是按信任域切换——视觉可信时用视觉方位 + UWB 距离;视觉丢失时用“最后方位 + 指数外推”滑行,并立即限速;超过 2s 不再信任方位,转原地搜索。
/*
* 案例4:UWB(距离/身份) + 视觉(方位/确认) 门控切换跟随
* 信任域:视觉有效 -> 视觉方位;视觉丢失 -> 方位外推(≤2s) -> 停止转向
*/
#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 R_WHEEL = 0.065f, TRACK = 0.34f;
const float D_FOLLOW = 1.20f; // 期望跟随距离
const float D_STOP = 0.80f; // 最小安全距离
const float V_MAX = 0.90f;
const uint8_t TARGET_ID = 7; // 目标 UWB 标签 / 视觉 ID
// ---------- 视觉输入(串口帧: id,cx,cy,w,h)----------
struct Vision { bool valid=false; uint8_t id=0; float cx=0; float wRel=0; };
Vision vis;
unsigned long lastVisMs = 0;
int visGood = 0, visBad = 0;
bool visTrusted = false;
void readVision() {
if (Serial2.available()) {
String s = Serial2.readStringUntil('\n');
int id, cx, cy, w, h;
if (sscanf(s.c_str(), "%d,%d,%d,%d,%d", &id, &cx, &cy, &w, &h) == 5) {
vis.id = id;
vis.cx = (cx - 160) / 160.0f; // 归一化 -1..1(320 宽视野)
vis.wRel = w / 320.0f; // 相对宽度(距离代理)
vis.valid = true;
lastVisMs = millis();
}
}
if (millis() - lastVisMs > 250) vis.valid = false; // 250ms 无帧=丢
}
// 迟滞:连续 3 帧有效才可信,连续 4 帧丢失才失信(防抖)
void updateTrust() {
bool ok = vis.valid && (vis.id == TARGET_ID) && (vis.wRel > 0.04f);
if (ok) { visGood++; visBad = 0; } else { visBad++; visGood = 0; }
if (visGood >= 3) visTrusted = true;
if (visBad >= 4) visTrusted = false;
}
// ---------- UWB ----------
struct Uwb { float d=0; uint8_t id=0; float fpRatio=0; bool valid=false; };
Uwb uwb;
float readUWB(); // 库封装:返回距离(m)
uint8_t readUWBid();
float readUWBquality(); // 首径功率/总功率比,低=NLOS 多径
// ---------- 方位状态 ----------
float lastBearing = 0, bearingCmd = 0;
unsigned long lastGoodMs = 0;
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.35f; wheelL.PID_velocity.I = 3.5f;
wheelR.PID_velocity.P = 0.35f; wheelR.PID_velocity.I = 3.5f;
wheelL.voltage_limit = wheelR.voltage_limit = 12.0f;
wheelL.current_limit = wheelR.current_limit = 4.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);
Serial2.begin(115200);
Serial.begin(115200);
}
void loop() {
// 1) 感知
uwb.d = readUWB(); uwb.id = readUWBid(); uwb.fpRatio = readUWBquality();
uwb.valid = (uwb.d > 0.05f) && (uwb.d < 30.0f) && (uwb.id == TARGET_ID);
readVision();
updateTrust();
// 2) 方位来源仲裁(抗遮挡核心)
bool bearingTrusted;
if (visTrusted) {
bearingCmd = vis.cx * 0.62f; // 视觉方位(按 FOV 标定系数)
lastBearing = bearingCmd; lastGoodMs = millis();
bearingTrusted = true;
} else {
float t = (millis() - lastGoodMs) / 1000.0f;
bearingCmd = lastBearing * expf(-t / 1.5f); // 指数外推,逐渐归零
bearingTrusted = (t < 2.0f);
}
// 3) 距离环(UWB 主导)
float vCmd = 0;
if (uwb.valid) {
float e = uwb.d - D_FOLLOW;
vCmd = constrain(1.6f * e, -0.25f, V_MAX);
if (uwb.d < D_STOP) vCmd = -0.15f; // 过近:主动后退
if (uwb.fpRatio < 0.60f) vCmd = min(vCmd, 0.45f); // NLOS:限速
} else {
vCmd = 0; // 无距离:不盲目前进
}
// 4) 方位环 -> 角速度,遮挡时禁止大转向
float wCmd = bearingTrusted ? constrain(1.8f * bearingCmd, -0.9f, 0.9f) : 0.0f;
if (!visTrusted) vCmd = min(vCmd, 0.45f); // 盲跟限速
// 5) 执行
float vL = vCmd - wCmd * TRACK * 0.5f;
float vR = vCmd + wCmd * TRACK * 0.5f;
wheelL.move(vL / R_WHEEL); wheelR.move(vR / R_WHEEL);
Serial.print("uwb="); Serial.print(uwb.d, 2);
Serial.print(" fp="); Serial.print(uwb.fpRatio, 2);
Serial.print(" vis="); Serial.print(visTrusted);
Serial.print(" v="); Serial.println(vCmd, 2);
delay(20);
}
镜头遮挡、强逆光、目标背对镜头都表现为“视觉连续丢帧”,这套迟滞+外推能在 4 帧(约 100ms)内做出降级反应,而不是用 0 值参与运算(那是“把遮挡当成了目标消失”)。
5、FreeRTOS 四任务 + 遮挡状态机(跟踪/滑行/搜索/丢失)+ α-β 预测
适用硬件:ESP32 双核 + 差速 BLDC + UWB + 视觉 + 前向 TOF。
核心思路:抗遮挡的本质是不确定性管理。用 α-β 滤波器维护目标相对位置与速度估计,遮挡时按预测继续滑行(速度限幅、时间限时),并主动执行“原地慢转扫描”搜索;重捕获需连续帧确认,不允许单帧跳变。
/*
* 案例5:UWB+视觉四任务 + α-β 预测 + 遮挡状态机
* 状态:TRACK(双源可信) / COAST(视觉丢,按预测滑行) / SEARCH(限时后搜索) / LOST(停车上报)
*/
#include <SimpleFOC.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
BLDCMotor wheelL(7), wheelR(7);
// 驱动/编码器初始化同案例一(略)
SemaphoreHandle_t mux;
const float R_WHEEL = 0.065f, TRACK = 0.34f;
enum Occl { ST_TRACK, ST_COAST, ST_SEARCH, ST_LOST };
volatile Occl g_st = ST_TRACK;
// ---- α-β 滤波:跟踪目标相对位置/速度(机体坐标系)----
struct AB { float x=0, y=0, vx=0, vy=0; };
AB est;
const float ALPHA = 0.55f, BETA = 0.18f;
void abPredict(float dt) { est.x += est.vx*dt; est.y += est.vy*dt; }
void abUpdate(float zx, float zy, float dt) {
float ex = zx - est.x, ey = zy - est.y;
est.x += ALPHA*ex; est.y += ALPHA*ey;
est.vx += BETA*ex/dt; est.vy += BETA*ey/dt;
}
// ---- 共享量 ----
volatile float s_uwbD=9, s_uwbQ=0, s_visCx=0;
volatile bool s_uwbOK=false, s_visOK=false;
volatile float s_ex=0, s_ey=0; // 预测相对位置
volatile float c_v=0, c_w=0;
volatile bool c_estop=false;
volatile unsigned long s_lastVisMs=0, s_visOkMs=0;
volatile int s_visCnt=0;
// ---- 任务1:视觉(100Hz),重捕获连续帧计数 ----
void taskVision(void*) {
TickType_t last = xTaskGetTickCount();
for (;;) {
bool got = visionParse(); // 解析一帧
xSemaphoreTake(mux, portMAX_DELAY);
if (got) {
s_visCx = visionCx(); s_visOK = true;
s_lastVisMs = millis(); s_visCnt++;
} else {
s_visOK = false;
if (millis() - s_lastVisMs > 300) s_visCnt = 0;
}
xSemaphoreGive(mux);
vTaskDelayUntil(&last, pdMS_TO_TICKS(10));
}
}
// ---- 任务2:UWB(20Hz)----
void taskUwb(void*) {
TickType_t last = xTaskGetTickCount();
for (;;) {
float d = readUWB(), q = readUWBquality();
xSemaphoreTake(mux, portMAX_DELAY);
s_uwbD = d; s_uwbQ = q;
s_uwbOK = (d > 0.05f && d < 30.0f) && (readUWBid() == 7);
xSemaphoreGive(mux);
vTaskDelayUntil(&last, pdMS_TO_TICKS(50));
}
}
// ---- 任务3:融合与状态机(50Hz)----
void taskFusion(void*) {
TickType_t last = xTaskGetTickCount();
float dt = 0.02f;
unsigned long coastT0 = 0;
for (;;) {
float d, q, cx; bool uok, vok; unsigned long lastVis; int vcnt;
xSemaphoreTake(mux, portMAX_DELAY);
d=s_uwbD; q=s_uwbQ; cx=s_visCx; uok=s_uwbOK; vok=s_visOK;
lastVis=s_lastVisMs; vcnt=s_visCnt;
xSemaphoreGive(mux);
// ---- 观测构造:UWB 给距离,视觉给方位 ----
bool measOK = uok && (q > 0.5f);
float zx=0, zy=0;
if (measOK) {
float bear = vok ? (cx * 0.62f) : est.y / (fabsf(est.x)+0.2f); // 视觉方位或估计方位
zx = d * cosf(bear); zy = d * sinf(bear);
}
// ---- 预测 ----
abPredict(dt);
if (measOK && (vok || g_st != ST_TRACK)) abUpdate(zx, zy, dt);
// ---- 状态机:遮挡时长驱动分级降级 ----
unsigned long gone = millis() - lastVis;
switch (g_st) {
case ST_TRACK:
if (!vok) { g_st = ST_COAST; coastT0 = millis(); }
break;
case ST_COAST:
// 滑行:按预测走,限速限时
if (vok && vcnt >= 3) { g_st = ST_TRACK; }
else if (millis() - coastT0 > 2000) { g_st = ST_SEARCH; coastT0 = millis(); }
break;
case ST_SEARCH:
// 搜索:原地慢转扫描(不前进),扩大视野
if (vok && vcnt >= 3) { g_st = ST_TRACK; }
else if (millis() - coastT0 > 8000) { g_st = ST_LOST; }
break;
case ST_LOST:
if (vok && vcnt >= 3) { g_st = ST_TRACK; coastT0 = millis(); }
break;
}
// ---- 输出 ----
float v = 0, w = 0;
float dist = sqrtf(est.x*est.x + est.y*est.y);
float bear = atan2f(est.y, est.x);
switch (g_st) {
case ST_TRACK:
v = constrain(1.6f*(dist - 1.2f), -0.2f, 0.9f);
w = constrain(1.8f*bear, -0.9f, 0.9f);
if (dist < 0.8f) v = -0.12f;
if (q < 0.6f) v = min(v, 0.45f); // NLOS 限速
break;
case ST_COAST:
v = constrain(1.2f*(dist - 1.2f), -0.15f, 0.45f); // 限速滑行
w = constrain(1.4f*bear, -0.6f, 0.6f);
break;
case ST_SEARCH:
v = 0; // 原地不前进
w = 0.55f; // 慢速扫描(一个方向)
break;
case ST_LOST:
v = 0; w = 0;
break;
}
xSemaphoreTake(mux, portMAX_DELAY);
s_ex = est.x; s_ey = est.y;
c_v = v; c_w = w;
c_estop = (g_st == ST_LOST);
xSemaphoreGive(mux);
vTaskDelayUntil(&last, pdMS_TO_TICKS(20));
}
}
// ---- 任务4:执行(1kHz,纯跟踪 + 斜率限幅)----
void taskExec(void*) {
TickType_t last = xTaskGetTickCount();
float vC = 0, wC = 0;
for (;;) {
float v, w; bool es;
xSemaphoreTake(mux, portMAX_DELAY);
v = c_v; w = c_w; es = c_estop;
xSemaphoreGive(mux);
if (es) { v = 0; w = 0; }
vC += constrain(v - vC, -0.8f*0.001f, 0.8f*0.001f);
wC += constrain(w - wC, -3.0f*0.001f, 3.0f*0.001f);
float vL = vC - wC*TRACK*0.5f, vR = vC + wC*TRACK*0.5f;
wheelL.loopFOC(); wheelR.loopFOC();
wheelL.move(vL/R_WHEEL); wheelR.move(vR/R_WHEEL);
vTaskDelayUntil(&last, pdMS_TO_TICKS(1));
}
}
void setup() {
// 电机初始化同案例一(略)
mux = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(taskVision, "vis", 4096, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(taskUwb, "uwb", 4096, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(taskFusion, "fus", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(taskExec, "exec", 4096, NULL, 3, NULL, 1);
}
void loop() { vTaskDelay(portMAX_DELAY); }
关键设计:ST_COAST 限时 2s、限速 0.45 m/s,超时后转 ST_SEARCH(原地慢扫,不前进);ST_LOST 停车并上报。三个阈值(300ms 判丢、2s 滑行、8s 搜索)就是抗遮挡的“时间预算”,绝不能无限期盲追。
案例6、:多假设跟踪 + 主动避遮挡(peek maneuvers)+ 衣着重识别
适用硬件:ESP32 + 差速 BLDC + UWB + 云台视觉(或用舵机偏转摄像头)。
核心思路:遮挡后目标可能“直走 / 左绕 / 右绕”,用三个假设并行预测,靠 UWB 距离序列给每个假设打分(最一致的胜出);若假设仍不确定,机器人主动横向移动到能重新看到目标的位置(脱离自身遮挡),并用衣着颜色直方图做重识别,防止跟错人。
/*
* 案例6:多假设 + 主动 peek + 颜色重识别
*/
#include <SimpleFOC.h>
BLDCMotor wheelL(7), wheelR(7);
// 驱动/编码器初始化同案例一(略)
const float R_WHEEL=0.065f, TRACK=0.34f;
// ---- 假设:位置 + 航向 + 权重 ----
struct Hyp { float x, y, th; float w; };
Hyp H[3]; // 0=直行 1=左偏 2=右偏
float dUwb = 9; bool uwbOK = false;
float px=0, py=0, pth=0; // 自身位姿(里程计)
// ---- 颜色直方图(重识别)----
#define BINS 8
float histRef[BINS]; // 首次确认时建立
unsigned long lastVisMs = 0;
bool visOK = false, reidOK = false;
void buildHistogram(const uint8_t* rgb, int n, float* h) {
for (int i=0;i<BINS;i++) h[i]=0;
for (int i=0;i<n;i++) { h[rgb[i*3] * BINS / 256] += 1; }
float s=0; for(int i=0;i<BINS;i++) s+=h[i];
if (s>0) for(int i=0;i<BINS;i++) h[i]/=s;
}
float histDist(const float* a, const float* b) { // Bhattacharyya 距离
float bc=0; for(int i=0;i<BINS;i++) bc += sqrtf(a[i]*b[i]);
return 1.0f - bc;
}
// ---- 初始化假设 ----
void initHyps(float x0, float y0, float th0, float v) {
H[0] = {x0 + v*cosf(th0)*0.5f, y0 + v*sinf(th0)*0.5f, th0, 1.0f/3};
H[1] = {x0 + v*cosf(th0+0.6f)*0.5f, y0 + v*sinf(th0+0.6f)*0.5f, th0+0.6f, 1.0f/3};
H[2] = {x0 + v*cosf(th0-0.6f)*0.5f, y0 + v*sinf(th0-0.6f)*0.5f, th0-0.6f, 1.0f/3};
}
void predictHyps(float dt, float v) {
for (int i=0;i<3;i++) {
H[i].x += v*cosf(H[i].th)*dt;
H[i].y += v*sinf(H[i].th)*dt;
}
}
// ---- UWB 距离给假设打分 ----
void scoreHyps() {
if (!uwbOK) return;
float sum = 0;
for (int i=0;i<3;i++) {
float dPred = hypotf(H[i].x - px, H[i].y - py);
float e = fabsf(dPred - dUwb);
H[i].w *= expf(-e*e/(2*0.25f*0.25f)); // 高斯似然,σ=0.25m
H[i].w += 1e-4f; // 防归零
sum += H[i].w;
}
for (int i=0;i<3;i++) H[i].w /= sum; // 归一化
}
int bestHyp() {
int b=0; for(int i=1;i<3;i++) if (H[i].w>H[b].w) b=i;
return b;
}
// ---- 主动 peek:横向移动以获得视线 ----
enum Act { A_FOLLOW, A_PEEK, A_SEARCH };
Act act = A_FOLLOW;
unsigned long peekT0 = 0;
float peekDir = 1;
float usLeft(), usRight(); // 侧向测距,用于判断哪侧更空
void setup() {
// 电机初始化同案例一(略)
Serial.begin(115200);
}
void loop() {
dUwb = readUWB(); uwbOK = (dUwb > 0.05f && dUwb < 30.0f) && (readUWBid()==7);
bool visNow = visionParse();
if (visNow) { lastVisMs = millis(); visOK = true; }
else if (millis() - lastVisMs > 300) visOK = false;
// 视觉新鲜时更新目标绝对位置并重设假设
if (visOK) {
float bear = visionCx() * 0.62f;
float tx = px + dUwb*cosf(pth + bear);
float ty = py + dUwb*sinf(pth + bear);
// 首次建立衣着模板
if (!reidOK) { buildHistogram(visionPersonRgb(), visionPersonPixels(), histRef); reidOK = true; }
else if (histDist(histRef, visionCurrentHist()) > 0.25f) {
// 跟错人:丢弃视觉,仅用 UWB,并降低置信
visOK = false;
}
initHyps(tx, ty, pth + bear, 0.6f);
}
predictHyps(0.02f, 0.6f);
scoreHyps();
// ---- 行为选择 ----
float v=0, w=0;
if (visOK && reidOK) {
act = A_FOLLOW;
int b = bestHyp();
float dx = H[b].x - px, dy = H[b].y - py;
float dist = hypotf(dx, dy), bear = atan2f(dy, dx) - pth;
while (bear > PI) bear -= 2*PI; while (bear < -PI) bear += 2*PI;
v = constrain(1.5f*(dist - 1.2f), -0.2f, 0.9f);
w = constrain(1.8f*bear, -0.9f, 0.9f);
} else {
// 遮挡:先尝试主动 peek(横向挪一步,换视角)
if (act == A_FOLLOW) {
act = A_PEEK; peekT0 = millis();
peekDir = (usLeft() > usRight()) ? 1.0f : -1.0f; // 往更空一侧让
}
if (act == A_PEEK) {
v = 0.10f; w = 0.45f * peekDir; // 小角度斜移
if (millis() - peekT0 > 2500) act = A_SEARCH;
} else {
v = 0; w = 0.55f; // 原地扫描
if (visOK) act = A_FOLLOW;
if (millis() - lastVisMs > 8000) { v = 0; w = 0; } // 彻底丢失:停车
}
if (visOK) act = A_FOLLOW;
}
// 执行
float vL = v - w*TRACK*0.5f, vR = v + w*TRACK*0.5f;
wheelL.loopFOC(); wheelR.loopFOC();
wheelL.move(vL/R_WHEEL); wheelR.move(vR/R_WHEEL);
Serial.print("hyp="); Serial.print(bestHyp());
Serial.print(" w0="); Serial.print(H[0].w,2);
Serial.print(" w1="); Serial.print(H[1].w,2);
Serial.print(" w2="); Serial.print(H[2].w,2);
Serial.print(" act="); Serial.println(act);
delay(20);
}
peek 的价值在于:很多遮挡不是目标消失了,而是机器人自己站的位置看不到(比如柱子正好挡住视线)。横向挪 20~40cm 往往立刻恢复视线,比原地转圈搜索快得多。重识别阈值 0.25 要按服装样式标定,纯色衣服易混淆时,需加入身高/步态或 UWB 标签 ID 双重约束。
要点解读
一、抗遮挡不是加权平均,而是“信任域切换 + 不确定性显式管理”
把 UWB 距离和视觉方位丢进一个加权公式,在遮挡时会得到“半个目标”,机器人也就跟着“半个目标”走;这种做法比只用 UWB 更危险,因为视觉失效后置信度并没有真正降下来。正确结构是案例一的信任域开关 + 迟滞:视觉可信就用视觉方位,不可信就切换源,并把“当前不确定性”作为一个显式状态(visTrusted、coast 时长、假设权重)传递给控制层,让速度随之降下来。不确定性必须是一个可被决策的数,而不是一个被平均掉的误差。
二、UWB 与视觉的分工要按能力互补划,不是“谁更准用谁”
UWB 给的是距离 + 身份锚定(标签 ID 唯一,不怕认错人),缺点是方位精度差、NLOS 下测距会跳;视觉给的是方位 + 身份确认(衣着/人脸),缺点是怕遮挡和光照。所以正确的融合是“UWB 定距离与身份、视觉定方位与确认”(案例一),而不是互相取长补短地平均。这也解释了为什么遮挡期必须靠 UWB 兜住距离:方位可以暂时不知道,但距离绝不能失——距离是安全边界,方位只是航向修正。
三、遮挡必须被“检测”出来,且要多判据、带迟滞
“读不到”有三种完全不同的原因:真遮挡、检测器漏检、目标离开视野;处理方式各不相同。判据要叠三层:① 帧连续性与 bbox 面积(案例一 visGood/visBad 计数 + wRel > 0.04);② UWB 首径功率比 fpRatio(NLOS 多径会让测距突然偏大,案例一用它限速);③ 距离序列的突变检测(一跳半米以上大概率是 NLOS 而非目标移动)。每一条都要带迟滞(连续 N 帧才切换),否则单帧噪声就会触发降级,行为变得神经质。
四、遮挡期必须“预测—滑行—搜索”三态限时,重捕获要连续帧确认
遮挡后的正确行为不是原地等,也不是加速追,而是分三级:COAST(按预测滑行,限速、限时 2s)→ SEARCH(原地慢扫,不前进,限时 8s)→ LOST(停车上报)。案例二的三个时间预算就是这套逻辑。滑行期必须限速,因为此时位置估计的方差在单调增长;重捕获必须连续 3 帧 + ID 一致才恢复 TRACK,否则目标从柱子后一闪而过就会造成“追一下—停一下”的抽动。允许短时不知道目标在哪,但不允许在不知道的情况下高速移动。
五、主动感知与身份绑定,是园区场景区别于“跟着走”的关键
园区里有柱子、灌木、其他人,被动抗遮挡迟早会跟丢或跟错。两个主动手段:① peek 机动(案例三)——遮挡常是自身视角问题,横向挪一步比原地转圈更有效;② 身份绑定——UWB 标签 ID 做硬绑定,视觉衣着直方图做软确认(案例三 histDist > 0.25 就丢弃视觉),两者任一不符都不跟随。执行层依旧是 1kHz 纯跟踪、只消费一个 (v, ω),失效即停。跟对人比跟得紧重要得多——园区巡逻跟错人的后果,远比跟丢严重。
请注意:以上案例仅作为思路拓展的参考示例,不保证完全正确、适配所有场景或可直接编译运行。由于硬件平台、实际使用场景、Arduino 版本的差异,均可能影响代码的适配性与使用方法的选择。在实际编程开发时,请务必根据自身硬件配置、使用场景及具体功能需求进行针对性调整,并通过多次实测验证效果;同时需确保硬件接线正确,充分了解所用传感器、执行器等设备的技术规范与核心特性。对于涉及硬件操作的代码,使用前务必核对引脚定义、电平参数等关键信息的准确性与安全性,避免因参数错误导致硬件损坏或运行异常。

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



所有评论(0)