欢迎光临
我们一直在努力

【花雕学编程】Arduino BLDC 之送餐机器人 - 滚动窗口拥挤分级 + 动态重规划

在这里插入图片描述
“Arduino BLDC 之送餐机器人:滚动窗口拥挤分级 + 动态重规划”可以理解为:送餐机器人在餐厅、酒店走廊等拥挤环境中,不依赖全局地图一次性决定整条路径,而是以机器人当前位置为中心维护一个局部感知窗口,把障碍物按距离、密度、运动趋势和通行代价进行拥挤分级,再据此动态调整速度、绕行方向、等待策略和局部目标点,最后由 BLDC 差速底盘平滑执行。它的典型链路是:传感器扫描 → 局部代价地图/滚动窗口更新 → 拥挤度评估与分级 → 行为/轨迹重规划 → BLDC 速度/力矩闭环 → 安全验证与恢复全局路径。

一、主要特点

  • 滚动窗口降低实时计算压力
    全局 SLAM 地图适合判断“从厨房到包间怎么走”,但在行人频繁穿行的走廊里,全局地图更新慢、计算重。滚动窗口只维护机器人前方、侧方和后方一定范围内的局部环境信息,例如激光雷达扇区、ToF 阵列或多路超声/红外距离,计算量小,更适合嵌入式平台高频更新。
  • 拥挤分级不是简单“有/无障碍”
    更合理的做法是把局部空间划分为多个方向扇区,对每个扇区计算拥挤度:近距离障碍权重高,中距离次之,远距离更低;同时考虑障碍密度、通行宽度、是否持续逼近、是否处于目标方向上。这样机器人能区分“旁边有人经过”和“正前方通道被堵死”,避免把轻微干扰误判为必须停车。
  • 动态重规划分为行为级和轨迹级
    行为级重规划决定“继续前进、减速观察、左/右绕行、原地等待、退回安全点”;轨迹级重规划则在局部窗口内生成可执行的弧线或折线轨迹,例如 DWA 采样线速度和角速度,选择最安全、最贴近全局路径且最平滑的速度指令。行为级负责策略,轨迹级负责执行细节。
  • BLDC 底盘提供柔顺加减速和低速稳定性
    送餐机器人需要频繁启停、绕行和低速转弯,若电机响应差,容易出现顿挫、洒汤、托盘晃动。BLDC 配合编码器闭环、FOC 电流环和 S 曲线加减速,可以让速度指令变化更平滑,减少急刹和急转带来的冲击。
  • 需要与全局路径保持协同
    滚动窗口重规划只在局部有效,不能长期偏离全局目标。系统应在绕行后持续评估是否回到全局参考路径上;若局部拥堵持续存在,例如走廊长时间被人群占据,应触发更高层策略,如等待、更换通道或重新请求全局路径。
  • 适合分层部署
    完整实现通常由上位机或高性能 MCU 负责 SLAM、全局路径、拥挤度评估和局部规划,底层 MCU 负责 BLDC 速度闭环、急停保护和传感器采样。8 位 Arduino 更适合做轻量原型或执行层,不建议同时承担激光 SLAM、DWA 采样和多路传感器滤波。
  • 二、应用场景

  • 餐厅堂食送餐
    走廊窄、桌椅多、服务员和顾客频繁穿行。机器人需要在门口、过道交汇处和餐桌间隙判断拥挤程度,选择减速绕行或短暂等待,避免碰撞托盘和顾客腿部。
  • 酒店/公寓楼层配送
    电梯口、房门前和转角处容易出现短时拥堵。滚动窗口可判断电梯口是否有人排队,动态选择等待或绕行;进入客房走廊后可降低速度、增大安全裕度。
  • 医院药品/餐食配送
    对安全性和安静性要求更高。BLDC 低噪声特性适合病房环境,拥挤分级可避免在病床旁或推车通道强行穿行。
  • 校园/办公楼送餐
    高峰期走廊、楼梯口和茶水间附近拥堵明显。系统可根据局部拥挤度调整通行策略,例如在开阔区保持巡航速度,在狭窄区自动降速。
  • 多机协同配送
    多台送餐机器人同时运行时,可通过局部拥挤度和对向距离判断是否需要让行,避免在窄通道中对向僵持。更高阶方案可加入优先级协商,例如载热餐机器人优先通行。
  • 三、需要注意的事项

  • 传感器布局要覆盖前向和侧向盲区
    仅靠前方传感器无法判断侧向行人切入。建议前向使用激光雷达、深度相机或多路 ToF,侧向补充超声/红外,底盘下方可增加防撞条或跌落传感器。传感器安装高度应能同时覆盖行人腿部和托盘边缘。
  • 拥挤分级阈值必须现场标定
    不同餐厅通道宽度、桌椅密度和人流速度差异很大。近/中/远阈值、扇区权重、等待时间和绕行角度都不能照搬示例值,需要在真实环境中根据碰撞风险、通行效率和用户观感逐步调整。
  • 重规划频率要与执行带宽匹配
    局部规划更新过快会导致速度指令频繁跳变,过慢则避障滞后。建议先保证底层 BLDC 速度环稳定,再逐步提高滚动窗口更新频率。控制回路应使用定时器中断或非阻塞调度,避免 delay 阻塞电机闭环。
  • 绕行策略要防止“贴边刮蹭”和“反复横跳”
    如果只按当前最近障碍决定左右绕行,机器人在狭窄通道中可能在两侧障碍之间来回切换方向。应加入方向保持、迟滞判断和最短绕行代价评估,使绕行方向在短时间内保持稳定。
  • 动态重规划不能丢失全局目标
    局部避障成功后应尽快回归全局参考路径;若连续多次重规划仍无法接近目标,应判定为“局部被困”,触发等待、后退或请求全局重规划,而不是无限小幅度绕行。
  • BLDC 执行层必须限速、限加速度、限电流
    送餐机器人载有液体和餐具,急加速和急转向会明显增加洒漏风险。应设置最大线速度、最大角速度、加速度上限和电流保护;在拥挤分级较高时自动降低速度上限和转弯角速度。
  • 通信和上层算法故障时底层仍能安全停机
    若上位机卡死、SLAM 丢失或串口通信中断,底层应进入安全模式:停止输出速度、保持刹车或低速原地等待,并上报故障状态。安全停机优先级高于路径跟踪。
  • Arduino 平台要控制算法复杂度
    轻量方案可以用多路距离传感器做扇区拥挤评估和简单绕行决策;如果引入 DWA、局部代价地图或 SLAM,建议把规划放在 ESP32-S3、STM32 或上位机,Arduino/底层 MCU 只接收目标线速度和角速度并执行闭环。
    整体来看,这个方案的关键不是“避开每一个障碍”,而是让机器人具备局部拥挤感知能力、分级响应能力和平滑执行能力:轻拥堵时减速观察,中拥堵时择向绕行,重拥堵时等待或重规划,异常时安全降级。做原型时,建议先实现“多扇区距离扫描 → 拥挤度加权 → 四档行为输出 → BLDC 速度限幅执行”的最小闭环,再加入 DWA 轨迹采样和全局路径回归。
  • 在这里插入图片描述
    1、8方向滚动窗口拥挤分级 + 离散动作决策
    此案例建立最核心的分级框架:8 方向传感器以 150ms 周期扫描,计算前方三方向拥堵比,超过阈值则触发转向或等待。

    #include <SimpleFOC.h>

    BLDCMotor motorL(5);
    BLDCMotor motorR(6);

    // 8方向传感器(模拟超声波/红外阵列)
    const int SENSOR_PINS[8] = {A0, A1, A2, A3, A4, A5, A6, A7};
    const int NUM_DIR = 8;

    // 拥挤分级阈值:三级距离带
    const uint16_t NEAR_DIST = 30; // 近距 0-30cm,权重3
    const uint16_t MID_DIST = 80; // 中距 30-80cm,权重2
    const uint16_t FAR_DIST = 150; // 远距 80-150cm,权重1
    const float BLOCK_RATIO = 0.6; // 前方拥堵比阈值(60%)

    struct DirState {
    uint16_t dist;
    int weight;
    };
    DirState dirs[8];

    enum PathAction { GO_FORWARD, TURN_LEFT, TURN_RIGHT, WAIT };
    PathAction action = GO_FORWARD;

    unsigned long lastScan = 0;
    const unsigned long SCAN_INTERVAL = 150; // 150ms 滚动周期

    void setup() {
    Serial.begin(115200);
    for (int i = 0; i < 8; i++) pinMode(SENSOR_PINS[i], INPUT);
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;
    Serial.println("滚动窗口拥挤分级就绪");
    }

    void loop() {
    motorL.loopFOC();
    motorR.loopFOC();

    if (millis() – lastScan > SCAN_INTERVAL) {
    scanWindow();
    computeCrowdLevel();
    executeAction();
    lastScan = millis();
    }
    delay(10);
    }

    uint16_t readDistance(int idx) {
    return analogRead(SENSOR_PINS[idx]) / 4; // 简化映射
    }

    void scanWindow() {
    for (int i = 0; i < NUM_DIR; i++) {
    dirs[i].dist = readDistance(i);
    if (dirs[i].dist < NEAR_DIST) dirs[i].weight = 3;
    else if (dirs[i].dist < MID_DIST) dirs[i].weight = 2;
    else if (dirs[i].dist < FAR_DIST) dirs[i].weight = 1;
    else dirs[i].weight = 0;
    }
    }

    // 前方三方向(索引 0,1,7 对应 0°/45°/315°)拥堵比计算
    void computeCrowdLevel() {
    int frontWeight = dirs[0].weight + dirs[1].weight + dirs[7].weight;
    float frontRatio = (float)frontWeight / 9.0f; // 最大 3×3=9

    if (frontRatio > BLOCK_RATIO) {
    // 前方拥堵,检查左右侧净空
    int leftWeight = dirs[5].weight + dirs[6].weight;
    int rightWeight = dirs[2].weight + dirs[3].weight;

    if (leftWeight < rightWeight && leftWeight <= 2) action = TURN_LEFT;
    else if (rightWeight < leftWeight && rightWeight <= 2) action = TURN_RIGHT;
    else action = WAIT; // 两侧均拥堵,等待
    } else {
    action = GO_FORWARD;
    }
    }

    void executeAction() {
    const float CRUISE = 1.5f;
    const float TURN_SPEED = 0.8f;

    switch (action) {
    case GO_FORWARD:
    motorL.move(CRUISE); motorR.move(CRUISE);
    break;
    case TURN_LEFT:
    motorL.move(CRUISE * 0.3f); motorR.move(CRUISE);
    break;
    case TURN_RIGHT:
    motorL.move(CRUISE); motorR.move(CRUISE * 0.3f);
    break;
    case WAIT:
    motorL.move(0); motorR.move(0);
    break;
    }
    }

    要点:分级不是简单的“近中远”标签,而是加权求和——近距权重 3,中距 2,远距 1。前方拥堵比超过 0.6 时触发转向,而非一检测到障碍就停。这种“滚动窗口 + 分级决策”避免了连续避障的振荡问题。

    2、拥挤分级 + 等待/绕行/直行的三态调度
    此案例在案例一基础上引入时间维度的等待策略:当两侧均拥堵时,不立即永久等待,而是计时后重新评估;若持续拥堵超过阈值,则触发更激进的绕行。

    #include <SimpleFOC.h>

    BLDCMotor motorL(5), motorR(6);

    // 传感器(前、左、右三方向简化)
    const int PIN_FRONT = A0, PIN_LEFT = A1, PIN_RIGHT = A2;

    // 分级阈值
    const uint16_t NEAR = 30, MID = 80, FAR = 150;

    // 状态机
    enum TrafficState { CLEAR, MODERATE, CONGESTED, WAITING, DETOUR };
    TrafficState state = CLEAR;

    // 等待计时
    unsigned long waitStart = 0;
    const unsigned long MAX_WAIT = 3000; // 最长等待 3 秒
    const unsigned long RE_EVAL_MS = 500; // 等待期间每 500ms 重评估

    unsigned long lastEval = 0;

    void setup() {
    Serial.begin(115200);
    pinMode(PIN_FRONT, INPUT); pinMode(PIN_LEFT, INPUT); pinMode(PIN_RIGHT, INPUT);
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;
    }

    void loop() {
    motorL.loopFOC(); motorR.loopFOC();

    if (millis() – lastEval > 150) {
    evaluateTraffic();
    lastEval = millis();
    }
    executeTraffic();
    delay(10);
    }

    void evaluateTraffic() {
    uint16_t dF = analogRead(PIN_FRONT) / 4;
    uint16_t dL = analogRead(PIN_LEFT) / 4;
    uint16_t dR = analogRead(PIN_RIGHT) / 4;

    // 前方分级
    int frontLevel = (dF < NEAR) ? 3 : (dF < MID) ? 2 : (dF < FAR) ? 1 : 0;
    int leftLevel = (dL < NEAR) ? 3 : (dL < MID) ? 2 : (dL < FAR) ? 1 : 0;
    int rightLevel = (dR < NEAR) ? 3 : (dR < MID) ? 2 : (dR < FAR) ? 1 : 0;

    if (frontLevel == 0) {
    state = CLEAR;
    waitStart = 0;
    } else if (frontLevel == 1) {
    state = MODERATE; // 减速通过
    } else {
    // 前方中距或近距有障碍
    if (leftLevel <= 1 && leftLevel < rightLevel) {
    state = DETOUR; // 左侧更空,绕行
    } else if (rightLevel <= 1 && rightLevel < leftLevel) {
    state = DETOUR; // 右侧更空,绕行(需区分方向,此处简化)
    } else {
    // 两侧均拥堵,进入等待
    if (state != WAITING) {
    state = WAITING;
    waitStart = millis();
    } else if (millis() – waitStart > MAX_WAIT) {
    // 等待超时,尝试强制绕行
    state = DETOUR;
    }
    }
    }
    }

    void executeTraffic() {
    const float CRUISE = 1.5f;
    switch (state) {
    case CLEAR:
    motorL.move(CRUISE); motorR.move(CRUISE);
    break;
    case MODERATE:
    motorL.move(CRUISE * 0.6f); motorR.move(CRUISE * 0.6f);
    break;
    case DETOUR:
    // 简化:向左绕行
    motorL.move(CRUISE * 0.3f); motorR.move(CRUISE);
    break;
    case WAITING:
    motorL.move(0); motorR.move(0);
    break;
    }
    }

    要点:等待不是无限期的。MAX_WAIT 超时后强制绕行,避免机器人在拥堵点“卡死”。等待期间每 500ms 重新评估窗口,一旦前方清空立即恢复直行。这是时间维度上的动态重规划——决策不仅取决于当前空间状态,还取决于拥堵持续时间。

    3、滚动窗口 + 全局目标引导的混合决策
    此案例将滚动窗口的局部决策与全局目标方向融合:当局部窗口内无拥堵时,优先朝全局目标方向前进;当拥堵时,绕行方向的选择也参考全局目标,避免“绕反方向”。

    #include <SimpleFOC.h>

    BLDCMotor motorL(5), motorR(6);

    // 8方向传感器
    const int SENSOR_PINS[8] = {A0, A1, A2, A3, A4, A5, A6, A7};

    // 全局目标方向(来自上位机或全局路径)
    float globalTargetAngle = 0.0f; // 弧度,0 = 正前方
    float currentHeading = 0.0f; // 当前航向(来自IMU或里程计)

    // 分级参数
    const uint16_t NEAR = 30, MID = 80, FAR = 150;
    const float BLOCK_RATIO = 0.6;

    struct DirInfo {
    uint16_t dist;
    int weight;
    float angle; // 该方向相对机器人的角度
    };

    DirInfo dirs[8];

    // 动作
    enum Action { FWD, LEFT, RIGHT, WAIT };
    Action action = FWD;

    void setup() {
    Serial.begin(115200);
    for (int i = 0; i < 8; i++) pinMode(SENSOR_PINS[i], INPUT);
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;

    // 预计算方向角度(0°在前,顺时针)
    for (int i = 0; i < 8; i++) {
    dirs[i].angle = i * 45.0f * PI / 180.0f;
    }
    }

    void loop() {
    motorL.loopFOC(); motorR.loopFOC();

    static unsigned long lastScan = 0;
    if (millis() – lastScan > 150) {
    scanAndDecide();
    lastScan = millis();
    }
    delay(10);
    }

    void scanAndDecide() {
    // 1. 扫描分级
    for (int i = 0; i < 8; i++) {
    uint16_t d = analogRead(SENSOR_PINS[i]) / 4;
    dirs[i].dist = d;
    if (d < NEAR) dirs[i].weight = 3;
    else if (d < MID) dirs[i].weight = 2;
    else if (d < FAR) dirs[i].weight = 1;
    else dirs[i].weight = 0;
    }

    // 2. 前方拥堵比
    int frontW = dirs[0].weight + dirs[1].weight + dirs[7].weight;
    float frontRatio = frontW / 9.0f;

    if (frontRatio <= BLOCK_RATIO) {
    // 前方通畅:检查是否朝向全局目标
    float targetDiff = globalTargetAngle – currentHeading;
    while (targetDiff > PI) targetDiff -= 2*PI;
    while (targetDiff < –PI) targetDiff += 2*PI;

    if (fabs(targetDiff) < PI / 6) { // 偏差 < 30°,直行
    action = FWD;
    } else if (targetDiff > 0) {
    action = RIGHT; // 目标在右,右转
    } else {
    action = LEFT;
    }
    } else {
    // 前方拥堵:选择绕行方向,但需考虑全局目标
    int leftW = dirs[5].weight + dirs[6].weight;
    int rightW = dirs[2].weight + dirs[3].weight;

    // 全局目标在左且左侧可行 → 左绕
    float targetDir = globalTargetAngle – currentHeading;
    bool targetLeft = (targetDir > 0 && targetDir < PI) || (targetDir < –PI);

    if (leftW <= 2 && (leftW < rightW || targetLeft)) {
    action = LEFT;
    } else if (rightW <= 2) {
    action = RIGHT;
    } else {
    action = WAIT;
    }
    }

    execute();
    }

    void execute() {
    const float CRUISE = 1.5f;
    switch (action) {
    case FWD:
    motorL.move(CRUISE); motorR.move(CRUISE);
    break;
    case LEFT:
    motorL.move(CRUISE * 0.3f); motorR.move(CRUISE);
    break;
    case RIGHT:
    motorL.move(CRUISE); motorR.move(CRUISE * 0.3f);
    break;
    case WAIT:
    motorL.move(0); motorR.move(0);
    break;
    }
    }

    要点:滚动窗口的局部决策必须受全局目标约束。否则机器人可能在拥堵中“绕反方向”,越绕越远。targetLeft 判断确保绕行方向与全局目标方向一致,避免局部避障破坏全局效率。这是“局部响应 + 全局引导”分层架构的核心。

    要点解读

  • 拥挤分级是“加权求和”,不是“取最危险值”
    简单的“检测到近距障碍就停”会导致机器人频繁急停,餐品泼洒。分级加权(近距 3、中距 2、远距 1)后计算拥堵比,允许机器人在部分方向有障碍但整体仍可通过时继续前进。前方拥堵比 0.6 的阈值是经验值,需根据餐厅过道宽度和机器人尺寸标定。

  • 滚动窗口的周期决定响应速度与计算负载的平衡
    150ms 周期意味着每秒约 6~7 次决策更新。太短(如 50ms)会增加 CPU 负载且引入传感器噪声抖动;太长(如 500ms)会导致对行人移动响应滞后。150~200ms 是餐厅场景的合理区间。决策周期与 FOC 控制周期(1kHz)分离,是架构设计的必要前提。

  • 等待策略必须有超时机制,否则机器人会“卡死”
    纯反应式避障在两侧均拥堵时只能永久等待。案例二的 MAX_WAIT 超时后强制绕行,打破僵局。实际餐厅中,等待超过 3 秒意味着该通道可能被长时间占用,绕行或请求人工介入比继续等待更合理。

  • 局部绕行方向必须与全局目标对齐
    案例三的 targetLeft 判断是防止“绕反”的关键。如果全局目标在左侧,即使右侧局部净空更大,也应优先左绕或等待。局部避障不能以牺牲全局效率为代价,这是分层导航架构的核心原则。

  • BLDC 的 FOC 柔顺执行是“分级决策不洒汤”的物理保障
    分级决策输出的是阶跃式的速度切换(直行→转向→等待),如果执行层用开环 ESC,速度突变会导致餐品惯性晃动。SimpleFOC 的电流环配合适当的加速度限制(案例中通过 CRUISE * 0.3f 的转向速度差实现),能让速度过渡“软着陆”。决策可以离散,执行必须连续。

  • 在这里插入图片描述
    4、滚动窗口拥挤分级 + 分级调速与侧向避让
    适用硬件:ESP32 / Arduino + SimpleFOC BLDC(或 ESC)+ 前方超声波/红外阵列(左、前、右三路)。
    核心思路:用"滑动窗口内近距采样比例"而非单次读数来定拥挤等级,抗瞬时抖动;等级映射到速度与避让策略,拥堵时按窗口内左右空档动态重选侧向偏移。

    /*
    * 案例4:滚动窗口拥挤分级 + 分级调速 / 侧向动态避让
    * 窗口:10 个采样(每 50ms 一个,共 500ms)
    */

    #include <SimpleFOC.h>

    BLDCMotor motor = BLDCMotor(7);
    BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8);
    Encoder encoder = Encoder(2, 3, 2048);
    void onEncoder() { encoder.handleA(); }

    // 三路超声波:(左, 前, 右) 的 trig/echo
    const int usPin[3][2] = {{6,7},{8,9},{10,11}};

    #define WIN 10
    #define THRESH_NEAR 35.0f // 认为"有人挡路"的距离(cm)
    float window[3][WIN]; // 每路一个滚动窗口
    int wi = 0; // 当前写入位置

    float readUS(int idx) {
    digitalWrite(usPin[idx][0], LOW); delayMicroseconds(2);
    digitalWrite(usPin[idx][0], HIGH); delayMicroseconds(10);
    digitalWrite(usPin[idx][0], LOW);
    long d = pulseIn(usPin[idx][1], HIGH);
    return (d > 0) ? d * 0.034f / 2.0f : 200.0f;
    }

    // 拥挤分级:窗口内"近距采样占比" -> 0~3 级
    int gradeLevel(int idx) {
    int near = 0;
    for (int i = 0; i < WIN; i++) if (window[idx][i] < THRESH_NEAR) near++;
    float ratio = (float)near / WIN;
    if (ratio > 0.7f) return 3; // 极高:持续拥堵
    if (ratio > 0.4f) return 2; // 高
    if (ratio > 0.15f) return 1; // 中
    return 0; // 畅通
    }

    void setup() {
    encoder.init(); encoder.enableInterrupts(onEncoder);
    motor.linkSensor(&encoder);
    motor.controller = MotionControlType::velocity;
    motor.PID_velocity.P = 0.2f; motor.PID_velocity.I = 2.0f;
    motor.voltage_limit = 12.0f;
    motor.init();
    driver.voltage_power_supply = 12.0f; driver.init();
    motor.linkDriver(&driver);
    for (int i = 0; i < 3; i++) pinMode(usPin[i][0], OUTPUT);
    for (int i = 0; i < 3; i++) for (int j = 0; j < WIN; j++) window[i][j] = 200;
    Serial.begin(115200);
    }

    void loop() {
    // —- 感知:三路入窗口,只更新一个槽位 —-
    for (int i = 0; i < 3; i++) window[i][wi] = readUS(i);
    wi = (wi + 1) % WIN;

    // —- 分级:左/前/右各取窗口等级 —-
    int gF = gradeLevel(0), gL = gradeLevel(1), gR = gradeLevel(2);

    // —- 决策1:拥挤等级 -> 目标速度(分级调速)—-
    float speed = 3.0f; // 畅通全速
    if (gF == 1) speed = 1.8f; // 中:明显减速
    else if (gF == 2) speed = 0.8f; // 高:龟速试探
    else if (gF == 3) speed = 0; // 极高:原地等待
    // 左右也拥堵则直接停(走廊两侧都有人)
    if (gL >= 2 && gR >= 2 && speed > 0) speed = 0;

    // —- 决策2:动态重规划(侧向),用窗口均值比单次读数稳定 —-
    float dL = 0, dR = 0;
    for (int i = 0; i < WIN; i++) { dL += window[1][i]; dR += window[2][i]; }
    dL /= WIN; dR /= WIN;

    float steer = 0;
    if (gF >= 2) { // 前向拥堵:往更空的一侧让
    steer = (dL > dR) ? 0.6f : –0.6f;
    if (fabs(dL – dR) < 10) steer = 0; // 两侧差不多:不偏,原地降速
    }

    // —- 执行:速度环,方向盘式映射到期望速度 —-
    motor.move(speed); // 简化示例:真实差速车用左右轮差分
    Serial.print("grade F/L/R="); Serial.print(gF); Serial.print("/");
    Serial.print(gL); Serial.print("/"); Serial.print(gR);
    Serial.print(" steer="); Serial.println(steer);
    delay(50); // 窗口槽位周期 50ms
    }

    工程提示:pulseIn 最长可阻塞 30ms 以上,量产应将电流环挂 1kHz 定时器中断;本例为可读性保留主循环写法。

    5、FreeRTOS 三层分离 + 窗口分级驱动的动态重规划(备选路线)
    适用硬件:ESP32(双核)+ SimpleFOC BLDC 差速。
    核心思路:感知 Task 维护滚动窗口;规划 Task 按"拥挤等级 + 等级持续时间"从路线表里动态切换(原路线→降速→备选路线→原地等待);执行 Task 只跟目标速度。重规划是"分级 × 计时"驱动的,而不是每帧都重算——省算力且稳定。

    /*
    * 案例5:FreeRTOS 感知-规划-执行 + 滚动窗口分级 + 路线表动态重规划
    */

    #include <SimpleFOC.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"

    BLDCMotor motorL = BLDCMotor(7), motorR = BLDCMotor(7);
    BLDCDriver3PWM drvL(9,10,11,8), drvR(22,23,24,25);
    Encoder encL(2,3,2048), encR(32,33,2048);
    void onEncL() { encL.handleA(); } void onEncR() { encR.handleA(); }

    SemaphoreHandle_t mux;
    volatile int s_grade = 0; // 感知输出:0~3
    volatile int s_gradeHold = 0; // 等级持续采样数(重规划判据)
    volatile float c_speed = 0; // 规划输出:目标线速度

    // 全局路线表:3 条送餐路线,各自 5 个路点(方向°, 时长ms) —— 示意
    const int ROUTES[3][5][2] = {
    {{0,3000},{45,2000},{0,4000},{–45,2000},{0,3000}}, // 路线0
    {{0,2000},{90,1500},{0,3000},{–90,1500},{0,2500}}, // 路线1(备选)
    {{0,1500},{0,6000},{–90,2000},{0,3000},{0,2000}}, // 路线2(备选)
    };
    int route = 0; // 当前路线索引

    // —- 感知:滚动窗口分级(单任务内维护)—-
    void taskSense(void*) {
    TickType_t last = xTaskGetTickCount();
    static float win[10]; static int i = 0;
    for (;;) {
    float d = readFrontUS(); // 前方人群距离
    win[i] = d; i = (i + 1) % 10;
    int near = 0;
    for (int k = 0; k < 10; k++) if (win[k] < 35.0f) near++;
    int g = (near > 7) ? 3 : (near > 4) ? 2 : (near > 1) ? 1 : 0;
    xSemaphoreTake(mux, portMAX_DELAY);
    s_grade = g;
    s_gradeHold = (s_gradeHold == g) ? s_gradeHold + 1 : 1; // 等级持续时间
    xSemaphoreGive(mux);
    vTaskDelayUntil(&last, pdMS_TO_TICKS(100)); // 100ms/采样
    }
    }

    // —- 规划:按等级 + 持续时间做动态重规划 —-
    void taskPlanner(void*) {
    TickType_t last = xTaskGetTickCount();
    int prevRoute = 0;
    for (;;) {
    int g, hold;
    xSemaphoreTake(mux, portMAX_DELAY);
    g = s_grade; hold = s_gradeHold;
    xSemaphoreGive(mux);

    float sp = 3.0f;
    if (g == 1) sp = 1.8f;
    else if (g == 2) sp = 0.8f;
    else if (g == 3) sp = 0;

    // 动态重规划:高等级持续 >30 次采样(3s) -> 切备选路线
    if (g >= 2 && hold > 30) {
    route = (route + 1) % 3; // 绕行到备选路线
    s_gradeHold = 0; // 防反复横跳(重置计时)
    Serial.print("REPLAN -> route "); Serial.println(route);
    }

    xSemaphoreTake(mux, portMAX_DELAY);
    c_speed = sp;
    xSemaphoreGive(mux);
    vTaskDelayUntil(&last, pdMS_TO_TICKS(50));
    }
    }

    // —- 执行:BLDC 速度环(双轮差速)—-
    void taskExec(void*) {
    TickType_t last = xTaskGetTickCount();
    for (;;) {
    float sp;
    xSemaphoreTake(mux, portMAX_DELAY);
    sp = c_speed;
    xSemaphoreGive(mux);
    motorL.loopFOC(); motorR.loopFOC();
    motorL.move(sp); motorR.move(sp); // 简化:转向由路点表另行处理
    vTaskDelayUntil(&last, pdMS_TO_TICKS(1));
    }
    }

    void setup() {
    encL.init(); encL.enableInterrupts(onEncL);
    encR.init(); encR.enableInterrupts(onEncR);
    motorL.linkSensor(&encL); motorR.linkSensor(&encR);
    motorL.controller = motorR.controller = MotionControlType::velocity;
    motorL.PID_velocity.P = 0.2f; motorL.PID_velocity.I = 2.0f;
    motorR.PID_velocity.P = 0.2f; motorR.PID_velocity.I = 2.0f;
    motorL.voltage_limit = motorR.voltage_limit = 12.0f;
    motorL.init(); motorR.init();
    drvL.voltage_power_supply = drvR.voltage_power_supply = 12.0f;
    drvL.init(); drvR.init();
    motorL.linkDriver(&drvL); motorR.linkDriver(&drvR);

    mux = xSemaphoreCreateMutex();
    xTaskCreatePinnedToCore(taskSense, "sense", 4096, NULL, 3, NULL, 0);
    xTaskCreatePinnedToCore(taskPlanner, "planner", 4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(taskExec, "exec", 4096, NULL, 3, NULL, 1);
    }
    void loop() { vTaskDelay(portMAX_DELAY); }

    6、滚动窗口栅格代价地图 + 局部 A* 动态重规划
    适用硬件:ESP32 + SimpleFOC BLDC 差速 + 前向激光/超声阵列(如 5 路)或轮式里程计。
    核心思路:滚动窗口不只在时间轴上平滑,更把"每个方位格子的占用频次"累计成局部代价栅格(占用频次/窗口内采样数 = 拥挤概率,直接当通行代价);以车体前方 3×5 栅格为地图,代价超过阈值即触发局部 A* 重规划,选出代价最小的下一步方向。

    /*
    * 案例6:滚动窗口拥挤概率栅格 + 局部A*动态重规划
    * 栅格:3列 x 5行,车前方;每格存"窗口内被占用比例"作为通行代价
    */

    #include <SimpleFOC.h>
    #include <vector>

    BLDCMotor motorL = BLDCMotor(7), motorR = BLDCMotor(7);
    BLDCDriver3PWM drvL(9,10,11,8), drvR(22,23,24,25);
    Encoder encL(2,3,2048), encR(32,33,2048);
    void onEncL() { encL.handleA(); } void onEncR() { encR.handleA(); }

    #define COLS 3
    #define ROWS 5
    #define WIN 12
    float occ[COLS][ROWS]; // 拥挤概率(0~1):占用采样数/窗口
    int count[COLS][ROWS]; // 窗口内累计占用次数
    int winIdx = 0;

    // 5 路测距数组:模拟把前方扇区分成 3 列(左/中/右各取多路平均)
    float readSector(int col) {
    static const int pin[3][2] = {{6,7},{8,9},{10,11}}; // 左中右
    digitalWrite(pin[col][0], LOW); delayMicroseconds(2);
    digitalWrite(pin[col][0], HIGH); delayMicroseconds(10);
    digitalWrite(pin[col][0], LOW);
    long d = pulseIn(pin[col][1], HIGH);
    return (d > 0) ? d * 0.034f / 2.0f : 200.0f;
    }

    // 每 100ms 滚动一个槽位:距离近(<40cm)则该格占用计数+1
    void updateWindow() {
    float front[3];
    for (int c = 0; c < COLS; c++) front[c] = readSector(c);
    // 新槽位写入第0行(最近处),旧数据向远处推一格后滑出窗口
    for (int r = ROWS – 1; r > 0; r—)
    for (int c = 0; c < COLS; c++) occ[c][r] = occ[c][r–1];
    for (int c = 0; c < COLS; c++) {
    float d = front[c];
    // 按距离把占用落到不同行(越近行越靠前)
    int r = (d < 20) ? 0 : (d < 35) ? 1 : (d < 60) ? 2 : 3;
    occ[c][0] = (d < 60) ? 0.9f : 0.0f; // 简化:近距=高占用
    }
    winIdx = (winIdx + 1) % WIN;
    }

    // —- 局部A*:3×5 栅格,代价 = 拥挤概率 + 转向代价 + 距离 —-
    struct Node { int x, y; float g, f; };
    float astarNextDirection() {
    // 起点(1,4)车头,终点(1,0)正前方远处;4方向移动
    const float INF = 1e9;
    float dist[COLS][ROWS]; bool closed[COLS][ROWS];
    Node prev[COLS][ROWS];
    for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) {
    dist[c][r] = INF; closed[c][r] = false;
    }
    auto h = [](int x, int y) { return (float)(abs(x – 1) + y); }; // 曼哈顿启发
    std::vector<Node> open = {{1, ROWS–1, 0, h(1, ROWS–1)}};
    dist[1][ROWS–1] = 0;
    while (!open.empty()) {
    // 取 f 最小
    int bi = 0;
    for (int i = 1; i < (int)open.size(); i++) if (open[i].f < open[bi].f) bi = i;
    Node n = open[bi]; open.erase(open.begin() + bi);
    if (closed[n.x][n.y]) continue;
    closed[n.x][n.y] = true;
    if (n.x == 1 && n.y == 0) break; // 到达目标
    int dx[4] = {0,0,–1,1
    ```cpp
    int dx[4] = {0,0,–1,1}, dy[4] = {–1,1,0,0};
    for (int k = 0; k < 4; k++) {
    int nx = n.x + dx[k], ny = n.y + dy[k];
    if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) continue;
    if (closed[nx][ny]) continue;
    // 代价 = 拥挤概率 + 转向惩罚(换列+0.3)+ 单位距离
    float cost = occ[nx][ny] * 2.0f
    + (nx != n.x ? 0.3f : 0.0f)
    + 1.0f;
    float nd = n.g + cost;
    if (nd < dist[nx][ny]) {
    dist[nx][ny] = nd;
    prev[nx][ny] = n;
    open.push_back({nx, ny, nd, nd + h(nx, ny)});
    }
    }
    }
    // 回溯第一步方向:从目标往前找起点后一格
    Node cur{1, 0, 0, 0};
    if (dist[1][0] == INF) return 0; // 无路可走:原地停
    while (!(prev[cur.x][cur.y].x == 1 && prev[cur.x][cur.y].y == ROWS–1)) {
    Node p = prev[cur.x][cur.y];
    if (p.x == 0 && p.y == 0) break; // 防御
    cur = p;
    }
    return (cur.x < 1) ? –0.6f : (cur.x > 1) ? 0.6f : 0.0f; // 转向方向
    }

    void setup() {
    encL.init(); encL.enableInterrupts(onEncL);
    encR.init(); encR.enableInterrupts(onEncR);
    motorL.linkSensor(&encL); motorR.linkSensor(&encR);
    motorL.controller = motorR.controller = MotionControlType::velocity;
    motorL.PID_velocity.P = 0.2f; motorL.PID_velocity.I = 2.0f;
    motorR.PID_velocity.P = 0.2f; motorR.PID_velocity.I = 2.0f;
    motorL.voltage_limit = motorR.voltage_limit = 12.0f;
    motorL.init(); motorR.init();
    drvL.voltage_power_supply = drvR.voltage_power_supply = 12.0f;
    drvL.init(); drvR.init();
    motorL.linkDriver(&drvL); motorR.linkDriver(&drvR);
    for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) occ[c][r] = 0;
    Serial.begin(115200);
    }

    void loop() {
    updateWindow(); // 滚动窗口:每 100ms 一槽
    float steer = astarNextDirection();// 动态重规划:A* 找代价最小方向
    float speed = 2.5f;
    // 前方三列平均占用高 -> 减速;中列近距 -> 接近停
    float occFront = (occ[0][0]+occ[1][0]+occ[2][0]) / 3.0f;
    if (occFront > 0.6f) speed = 0.6f;
    if (occ[1][0] > 0.8f && occ[0][0] > 0.5f && occ[2][0] > 0.5f) speed = 0;

    motorL.move(speed + steer); // 差速转向:执行层只消费目标
    motorR.move(speed – steer);
    Serial.print("occF="); Serial.print(occFront, 2);
    Serial.print(" steer="); Serial.println(steer);
    delay(100);
    }

    要点解读
    一、滚动窗口的本质是把"瞬时读数"变成"时间维度上的置信度"
    单次超声波读到 30cm 可能是行人一闪而过,也可能是传感器噪声。滚动窗口(案例一 10 槽、案例二 10 槽、案例三 12 槽)统计的是"近距占比",本质是一个低通滤波 + 占用频率统计:只有持续出现的障碍才升级拥挤等级。这直接消灭了抖动导致的频繁变速和原地抽搐,是送餐车在人群中"稳"的关键。

    二、拥挤分级必须"非对称":升级快、降级慢
    人流是突发性的,拥堵出现往往一瞬间,散去却要好几秒。所以等级上升(1→2→3)应该用较短窗口/较低阈值快速响应,而等级下降要引入保持时间(案例二的 s_gradeHold 计数就是典型做法),防止"刚减速又立刻加速"的振荡。真正的工程里还会给降级加一个延时器(如持续畅通 2s 才升速),让乘客体感平顺。

    三、动态重规划要"分级 × 计时"触发,而不是每帧重算
    MCU 算力有限,A* 在 3×5 栅格上虽便宜,但每 100ms 重算一次仍属浪费,且可能来回横跳。更稳的触发条件是"等级 ≥2 且持续超过 N 秒"才切换路线(案例二)或"栅格代价超阈值"才跑 A*(案例三)。重规划输出的是"下一步方向"而非全局路径,这正是"局部滚动"的意义——全局路径由调度层维护(送哪桌、走哪条主路线),局部重规划只负责当前 1~2 秒的微调,两层都不越权。

    四、执行层只消费"一个目标",重规划的复杂逻辑绝不下沉
    无论分级调速还是 A* 转向,最终落到电机上的只有一个量:目标速度/差速(案例一 speed、案例三 speed±steer)。实时控制环(电流环 1kHz、速度环数百 Hz)里不允许出现窗口统计、栅格计算、路线切换这些逻辑。这样规划层无论多复杂、甚至跑飞,执行层都能维持一个确定、可证的安全行为(限速、限力矩、急停)。这也是本主题与"实时控制分离"一脉相承的核心理念。

    五、失效降级与"无路可走"要有明确出口
    人群送餐场景必须回答两个问题:传感器全堵了怎么办?四面八方都拥堵怎么办?对应策略:单路超声超时 → 按窗口内其他槽位/其他方向补值(案例一 readUS 超时返回 200);A* 找不到通路(dist==INF)→ 原地停车并上报调度层(案例三),而不是硬闯或反复横跳。送餐机器人可以迟到,但不能撞人——"安全停车"永远是重规划的最后兜底选项,优先级高于任何任务完成度。

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

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【花雕学编程】Arduino BLDC 之送餐机器人 - 滚动窗口拥挤分级 + 动态重规划
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址