在这里插入图片描述
“Arduino BLDC 之贝塞尔 + 完整七段 S 曲线规划器(工业级机器人)”可以理解为:用贝塞尔曲线生成几何上连续、可跟踪的空间路径,再用完整七段 S 曲线对路径进行时间参数化,使位置、速度、加速度和加加速度都连续;最终由 BLDC 闭环执行,实现低冲击、低振动、可重复的工业级运动。贝塞尔曲线解决“走哪条路”,七段 S 曲线解决“按什么速度走这条路”。两者结合后,机器人不会像梯形加减速那样出现加速度阶跃,也不会像折线插补那样在拐角处产生速度突变。
一、主要特点

  1. 贝塞尔曲线提供高阶连续路径
    贝塞尔曲线通过控制点定义轨迹形状,天然具有凸包性、参数连续性和可微性。三次或五次贝塞尔曲线可以保证路径在连接点处位置、切向甚至曲率连续,适合机械臂末端、移动机器人底盘和差速 BLDC 平台。
  2. 七段 S 曲线实现 jerk 受限运动
    完整七段 S 曲线包括:加加速、匀加速、减加速、匀速、加减速、匀减速、减减速。
    其核心是限制加加速度,使加速度线性变化、速度呈 S 形过渡。这样启动和停止时不会出现加速度突变,可明显降低机械冲击、齿轮背隙激励和 BLDC 电流尖峰。
  3. 短行程会自动退化为五段或三段
    当位移不足以达到最大速度或最大加速度时,七段曲线会自动缩短或合并部分阶段,例如取消匀速段,甚至只保留加加速、减加速、减速段。这样既保持平滑,又避免规划出不可执行的轨迹。
  4. 路径与时间解耦,便于多轴同步
    贝塞尔曲线给出归一化路径 s(u),u 属于 [0,1];七段 S 曲线给出弧长或时间映射 s(t)。两者结合后,多轴只需按同一时间参数同步插补,就能保证末端轨迹不畸变。对差速底盘或机械臂关节来说,底层只需跟踪各自的时间同步目标位置。
  5. BLDC FOC 闭环保证轨迹可执行
    规划器输出的是期望位置、速度或加速度,真正落地需要 BLDC 的位置环、速度环和电流环跟踪。FOC 能降低低速转矩脉动,编码器闭环能修正负载、摩擦和打滑造成的偏差,使实际运动更接近规划曲线。
  6. 适合“上层规划 + 底层伺服”分层架构
    贝塞尔路径生成、S 曲线时间参数化、多轴同步和工况判断可放在 ESP32、STM32 或上位机;底层 MCU 专注高频 FOC、编码器采样、电流保护和急停。8 位 Arduino 可做轻量演示,但完整七段 S 曲线加多轴贝塞尔插补会明显吃算力和定时器资源。
    二、应用场景
  7. 桌面级/轻型工业机械臂
    用于拾放、涂胶、点焊、视觉分拣等任务。贝塞尔曲线保证末端轨迹光滑,七段 S 曲线抑制启停冲击,减少工件晃动和定位超调。
  8. 差速 BLDC 移动机器人
    在仓储、巡检或实验室环境中,用贝塞尔曲线平滑拐角,再用 S 曲线控制线速度和角速度,可避免直角转向导致的打滑、里程计跳变和货物倾倒。
  9. 高速往复机构
    例如搬运滑台、升降轴、旋转分度台、取料臂。短行程高频运动最容易出现加速度阶跃,七段 S 曲线能降低结构振动和电机热负荷。
  10. 云台、转台和检测平台
    摄像头、激光传感器、扫码器或检测探头需要平滑跟踪。S 曲线可降低云台抖动,贝塞尔曲线可平滑目标切换路径。
  11. 教育与科研平台
    适合演示路径连续性、jerk 限制、时间参数化、多轴插补、FOC 伺服跟踪和嵌入式实时调度。
    三、需要注意的事项
  12. 先做动力学可行性检查
    规划前必须确认最大速度、最大加速度、最大 jerk、电机转矩、负载惯量和机械限位。若规划曲线超出执行能力,再光滑也无法稳定跟踪。
  13. 七段 S 曲线需要处理多种工况
    包括:能达到最大速度、只能达到最大加速度、极短行程仅处于 jerk 控制区、加减速不对称、起止速度不为零等。实现时要避免除零、负时间和阶段重叠。
  14. 贝塞尔曲线不能只看几何光滑
    路径曲率必须与机器人最小转弯半径、最大角速度和最大向心加速度匹配。否则即使几何上连续,实际跟踪时仍会打滑或超调。
  15. 多轴同步必须基于同一时间基准
    机械臂或差速底盘的各轴如果按不同周期更新目标位置,末端轨迹会偏离预期。应使用统一定时器中断或时间戳,保证所有轴在同一时刻读取相同的时间参数。
  16. 8 位 Arduino 不适合完整实时计算
    若使用 ATmega328P,建议采用查表法、定点数、分段线性近似,或把规划任务交给上位机/ESP32/STM32。底层 Arduino 只负责接收目标位置并执行 FOC 闭环。
  17. BLDC 驱动器必须支持闭环位置/速度跟踪
    普通航模 ESC 只能做开环速度控制,无法精确跟随 S 曲线位置指令。应使用带编码器的 BLDC 模组、SimpleFOC、ODrive、VESC 或同类闭环方案。
  18. 规划器输出必须经过限幅和滤波保护
    即使 S 曲线本身连续,也可能因参数设置不当产生过高加速度或 jerk。应在规划层和执行层同时设置速度、加速度、电流和位置误差上限。
  19. 异常工况要有降级策略
    当编码器丢失、通信超时、电机堵转或路径跟踪误差过大时,应停止轨迹跟踪,进入减速停机或保持位置模式,而不是继续执行后续规划点。

在这里插入图片描述
1、三次贝塞尔路径 + 七段S曲线速度规划(基础双轨框架)
此案例建立最核心的双轨结构:贝塞尔曲线生成空间路径点,七段S曲线规划器为路径进度 t 生成平滑的时间映射,两者通过“进度参数”耦合。

#include <SimpleFOC.h>

// ==================== BLDC 差速底盘 ====================
BLDCMotor motorL(5), motorR(6);
BLDCDriver3PWM driverL(9, 10, 11, 8);
BLDCDriver3PWM driverR(12, 13, 14, 15);
Encoder encL(2, 3, 1024), encR(18, 19, 1024);

// ==================== 三次贝塞尔路径 ====================
struct BezierPath {
    float P0x, P0y;
    float P1x, P1y;
    float P2x, P2y;
    float P3x, P3y;
};

BezierPath path;

// 计算贝塞尔曲线上参数 t 处的坐标
void bezierCubic(float t, float& x, float& y) {
    float u = 1.0f - t;
    float b0 = u*u*u;
    float b1 = 3.0f*u*u*t;
    float b2 = 3.0f*u*t*t;
    float b3 = t*t*t;
    x = b0*path.P0x + b1*path.P1x + b2*path.P2x + b3*path.P3x;
    y = b0*path.P0y + b1*path.P1y + b2*path.P2y + b3*path.P3y;
}

// ==================== 七段S曲线规划器 ====================
struct SCurvePlanner {
    float pos, vel, acc;
    float maxVel, maxAcc, maxJerk;
    float targetPos;
    bool active;
    
    // 七段状态计时
    float t1, t2, t3, t4, t5, t6, t7;
    float totalTime;
    float startPos;
};

SCurvePlanner sPlanner;

void initSCurve(SCurvePlanner& p, float vMax, float aMax, float jMax) {
    p.maxVel = vMax;
    p.maxAcc = aMax;
    p.maxJerk = jMax;
    p.pos = 0; p.vel = 0; p.acc = 0;
    p.active = false;
}

// 规划七段S曲线的时间分段(对称情况简化)
void planSCurve(SCurvePlanner& p, float target) {
    p.startPos = p.pos;
    p.targetPos = target;
    float dist = target - p.pos;
    float sign = (dist >= 0) ? 1.0f : -1.0f;
    dist = fabs(dist);
    
    if (dist < 1e-4f) { p.active = false; return; }
    
    // 计算各阶段时间(对称七段)
    float tJerk = p.maxAcc / p.maxJerk;
    float distJerk = p.maxJerk * tJerk*tJerk*tJerk / 6.0f;
    
    // 简化:假设能达到最大速度
    float tConstAcc = (p.maxVel / p.maxAcc) - tJerk;
    if (tConstAcc < 0) { tConstAcc = 0; tJerk = sqrt(p.maxVel / p.maxJerk); }
    
    float distAcc = p.maxJerk * tJerk*tJerk*tJerk / 6.0f * 2 
                    + p.maxAcc * tJerk * tConstAcc + p.maxAcc * tConstAcc*tConstAcc / 2.0f;
    distAcc *= 2;  // 加速+减速
    
    float tConstVel = 0;
    if (dist > distAcc) {
        tConstVel = (dist - distAcc) / p.maxVel;
    } else {
        // 达不到最大速度,重新计算(简化:按比例缩减)
        float scale = sqrt(dist / distAcc);
        tJerk *= scale;
        tConstAcc *= scale;
    }
    
    p.t1 = tJerk; p.t2 = tConstAcc; p.t3 = tJerk;
    p.t4 = tConstVel;
    p.t5 = tJerk; p.t6 = tConstAcc; p.t7 = tJerk;
    p.totalTime = p.t1+p.t2+p.t3+p.t4+p.t5+p.t6+p.t7;
    
    p.acc = 0; p.vel = 0;
    p.active = true;
}

// 更新S曲线(时间驱动)
void updateSCurve(SCurvePlanner& p, float dt) {
    if (!p.active) return;
    
    static float elapsed = 0;
    elapsed += dt;
    
    float sign = (p.targetPos >= p.startPos) ? 1.0f : -1.0f;
    
    // 分段计算加速度(七段状态机)
    if (elapsed < p.t1) {
        p.acc = sign * p.maxJerk * elapsed;
    } else if (elapsed < p.t1 + p.t2) {
        p.acc = sign * p.maxAcc;
    } else if (elapsed < p.t1 + p.t2 + p.t3) {
        p.acc = sign * p.maxAcc - sign * p.maxJerk * (elapsed - p.t1 - p.t2);
    } else if (elapsed < p.t1 + p.t2 + p.t3 + p.t4) {
        p.acc = 0;
    } else if (elapsed < p.t1 + p.t2 + p.t3 + p.t4 + p.t5) {
        p.acc = -sign * p.maxJerk * (elapsed - p.t1 - p.t2 - p.t3 - p.t4);
    } else if (elapsed < p.t1+p.t2+p.t3+p.t4+p.t5+p.t6) {
        p.acc = -sign * p.maxAcc;
    } else {
        p.acc = -sign * p.maxAcc + sign * p.maxJerk * (elapsed - p.t1-p.t2-p.t3-p.t4-p.t5-p.t6);
    }
    
    p.vel += p.acc * dt;
    p.pos += p.vel * dt;
    
    if (elapsed >= p.totalTime) {
        p.active = false;
        p.vel = 0; p.acc = 0;
        p.pos = p.targetPos;
        elapsed = 0;
    }
}

// ==================== 主循环 ====================
float robotX = 0, robotY = 0;
unsigned long lastTime = 0;

void setup() {
    Serial.begin(115200);
    motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
    motorL.linkSensor(&encL);   motorR.linkSensor(&encR);
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    
    // 定义一条S形路径(起点 0,0 → 终点 3,2)
    path = {0, 0, 1.0f, 2.5f, 2.0f, 2.5f, 3.0f, 2.0f};
    
    // 初始化S曲线:最大速度 1.0(归一化进度/秒),加速度 2.0,加加速度 10.0
    initSCurve(sPlanner, 1.0f, 2.0f, 10.0f);
    planSCurve(sPlanner, 1.0f);  // 目标进度 1.0(走完整条路径)
    
    lastTime = millis();
}

void loop() {
    motorL.loopFOC(); motorR.loopFOC();
    
    unsigned long now = millis();
    float dt = (now - lastTime) / 1000.0f;
    if (dt < 0.001f) return;
    lastTime = now;
    
    // 更新S曲线进度
    updateSCurve(sPlanner, dt);
    
    // 用S曲线输出的进度 t 在贝塞尔曲线上取点
    float t = constrain(sPlanner.pos, 0.0f, 1.0f);
    float targetX, targetY;
    bezierCubic(t, targetX, targetY);
    
    // 计算朝向与速度
    float dx = targetX - robotX;
    float dy = targetY - robotY;
    float dist = sqrt(dx*dx + dy*dy);
    
    if (dist > 0.01f && sPlanner.active) {
        float heading = atan2(dy, dx);
        float speed = fabs(sPlanner.vel) * 2.0f;  // 速度缩放
        
        // 差速驱动
        float wheelBase = 0.25f;
        motorL.move(speed - heading * wheelBase / 2.0f);
        motorR.move(speed + heading * wheelBase / 2.0f);
        
        robotX = targetX;
        robotY = targetY;
    } else {
        motorL.move(0);
        motorR.move(0);
    }
    
    delay(10);
}

要点:sPlanner.pos 是归一化进度(0→1),不是物理距离。S曲线规划的是“走完整条路径的进度如何随时间变化”,贝塞尔曲线负责“进度为 t 时机器人在哪”。这种解耦让路径形状和速度剖面可以独立设计。

2、五次贝塞尔过渡 + 前视S曲线约束(多段连续路径)
此案例针对工业场景中多段路径连接的冲击问题:用五次贝塞尔曲线做转角过渡,S曲线约束过渡段的速度,避免路径切换时的加速度突变。

#include <SimpleFOC.h>

BLDCMotor motorL(5), motorR(6);
BLDCDriver3PWM driverL(9, 10, 11, 8);
BLDCDriver3PWM driverR(12, 13, 14, 15);

// ==================== 五次贝塞尔过渡 ====================
// 用于连接两条直线段,控制点为 P0~P5
struct QuinticTransition {
    float Px[6], Py[6];
    float length;  // 过渡段弧长(近似)
};

QuinticTransition transition;

// 五次贝塞尔计算
void bezierQuintic(float t, float& x, float& y) {
    float u = 1.0f - t;
    float b0 = u*u*u*u*u;
    float b1 = 5.0f*u*u*u*u*t;
    float b2 = 10.0f*u*u*u*t*t;
    float b3 = 10.0f*u*u*t*t*t;
    float b4 = 5.0f*u*t*t*t*t;
    float b5 = t*t*t*t*t;
    x = b0*transition.Px[0] + b1*transition.Px[1] + b2*transition.Px[2]
      + b3*transition.Px[3] + b4*transition.Px[4] + b5*transition.Px[5];
    y = b0*transition.Py[0] + b1*transition.Py[1] + b2*transition.Py[2]
      + b3*transition.Py[3] + b4*transition.Py[4] + b5*transition.Py[5];
}

// 生成直线-转角-直线的五次贝塞尔过渡
// 参考:P0=起点, P5=终点, P2=P3=转角点, P1=P0-P2中点, P4=P3-P5中点
void generateTransition(float x0, float y0, float cornerX, float cornerY,
                         float x5, float y5, float radius) {
    // 计算转角两侧的切入/切出点
    float d1x = cornerX - x0, d1y = cornerY - y0;
    float d2x = x5 - cornerX, d2y = y5 - cornerY;
    float len1 = sqrt(d1x*d1x + d1y*d1y);
    float len2 = sqrt(d2x*d2x + d2y*d2y);
    
    float r = min(radius, min(len1, len2) * 0.5f);
    
    transition.Px[0] = cornerX - (d1x/len1) * r;
    transition.Py[0] = cornerY - (d1y/len1) * r;
    transition.Px[5] = cornerX + (d2x/len2) * r;
    transition.Py[5] = cornerY + (d2y/len2) * r;
    
    transition.Px[2] = cornerX; transition.Py[2] = cornerY;
    transition.Px[3] = cornerX; transition.Py[3] = cornerY;
    
    transition.Px[1] = (transition.Px[0] + transition.Px[2]) / 2.0f;
    transition.Py[1] = (transition.Py[0] + transition.Py[2]) / 2.0f;
    transition.Px[4] = (transition.Px[3] + transition.Px[5]) / 2.0f;
    transition.Py[4] = (transition.Py[3] + transition.Py[5]) / 2.0f;
    
    // 近似弧长
    transition.length = 0;
    float prevX = transition.Px[0], prevY = transition.Py[0];
    for (int i = 1; i <= 10; i++) {
        float t = i / 10.0f;
        float x, y;
        bezierQuintic(t, x, y);
        transition.length += hypot(x - prevX, y - prevY);
        prevX = x; prevY = y;
    }
}

// ==================== 前视S曲线速度约束 ====================
// 根据过渡段长度和曲率,计算允许的最大通过速度
float computeLookAheadSpeed(float transitionLength, float maxJerk) {
    // 简化:过渡段速度受加加速度和长度约束
    // v_max ≈ (length * jerk)^(1/3)
    return pow(transitionLength * maxJerk, 1.0f/3.0f);
}

// ==================== 主循环 ====================
float robotX = 0, robotY = 0;
float progress = 0;
bool inTransition = false;
float transitionSpeed = 0;

void setup() {
    Serial.begin(115200);
    motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    
    // 生成过渡:从 (0,0) 经转角 (2,2) 到 (4,0)
    generateTransition(0, 0, 2, 2, 4, 0, 0.8f);
    
    float maxJerk = 10.0f;
    transitionSpeed = computeLookAheadSpeed(transition.length, maxJerk);
    Serial.print("过渡段长度: "); Serial.println(transition.length);
    Serial.print("允许通过速度: "); Serial.println(transitionSpeed);
}

void loop() {
    motorL.loopFOC(); motorR.loopFOC();
    
    if (inTransition) {
        progress += transitionSpeed * 0.01f / transition.length;
        if (progress >= 1.0f) {
            progress = 1.0f;
            inTransition = false;
        }
        
        float targetX, targetY;
        bezierQuintic(progress, targetX, targetY);
        
        float dx = targetX - robotX;
        float dy = targetY - robotY;
        float dist = sqrt(dx*dx + dy*dy);
        
        if (dist > 0.005f) {
            float heading = atan2(dy, dx);
            float speed = transitionSpeed;
            motorL.move(speed - heading * 0.125f);
            motorR.move(speed + heading * 0.125f);
            robotX = targetX;
            robotY = targetY;
        }
    } else {
        // 直线段(简化:直接前进)
        motorL.move(0.3f);
        motorR.move(0.3f);
    }
    
    delay(10);
}

要点:五次贝塞尔比三次贝塞尔多两个控制点,能同时保证位置、速度、加速度在过渡段两端的连续性。computeLookAheadSpeed 根据过渡段长度和允许的加加速度反推最大通过速度,避免“路径平滑了但速度太快导致离心冲击”。

3、在线S曲线重规划 + 贝塞尔路径动态修正
此案例针对工业场景中的动态障碍/目标变更:S曲线规划器支持在线重规划(不停止运动),贝塞尔控制点根据新目标动态调整。

#include <SimpleFOC.h>

BLDCMotor motorL(5), motorR(6);
BLDCDriver3PWM driverL(9, 10, 11, 8);
BLDCDriver3PWM driverR(12, 13, 14, 15);

// ==================== 贝塞尔路径(可动态修正) ====================
struct BezierPath {
    float Px[4], Py[4];
    bool modified;
};

BezierPath activePath;
BezierPath originalPath;

void bezierCubic(float t, float& x, float& y, BezierPath& p) {
    float u = 1.0f - t;
    x = u*u*u*p.Px[0] + 3*u*u*t*p.Px[1] + 3*u*t*t*p.Px[2] + t*t*t*p.Px[3];
    y = u*u*u*p.Py[0] + 3*u*u*t*p.Py[1] + 3*u*t*t*p.Py[2] + t*t*t*p.Py[3];
}

// 障碍物修正:偏移控制点
void adjustForObstacle(BezierPath& p, float obsX, float obsY, float safeDist) {
    for (int i = 1; i <= 2; i++) {  // 只调整中间控制点
        float dx = p.Px[i] - obsX;
        float dy = p.Py[i] - obsY;
        float dist = sqrt(dx*dx + dy*dy);
        if (dist < safeDist) {
            float push = (safeDist - dist) + 0.1f;
            p.Px[i] += (dx/dist) * push;
            p.Py[i] += (dy/dist) * push;
        }
    }
    p.modified = true;
}

// ==================== 在线S曲线(支持重规划) ====================
struct OnlineSCurve {
    float currentPos, currentVel, currentAcc;
    float maxVel, maxAcc, maxJerk;
    float goalPos;
    float plannedEndTime;
    float elapsed;
    bool active;
};

OnlineSCurve online;

void initOnlineSCurve(OnlineSCurve& p) {
    p.maxVel = 1.0f; p.maxAcc = 1.5f; p.maxJerk = 8.0f;
    p.currentPos = 0; p.currentVel = 0; p.currentAcc = 0;
    p.active = false;
}

// 在线重规划:从当前状态平滑过渡到新目标
void replanTo(OnlineSCurve& p, float newGoal) {
    p.goalPos = newGoal;
    // 保留当前速度,重新计算减速距离
    float remaining = newGoal - p.currentPos;
    float stopDist = (p.currentVel * p.currentVel) / (2.0f * p.maxAcc);
    
    // 如果剩余距离小于制动距离,进入紧急减速
    if (remaining < stopDist && p.currentVel > 0) {
        // 紧急减速模式
        p.active = true;
        Serial.println("紧急减速重规划");
    } else {
        p.active = true;
    }
}

// 简化在线更新:速度趋近控制 + 位置约束
void updateOnline(OnlineSCurve& p, float dt) {
    if (!p.active) return;
    
    float remaining = p.goalPos - p.currentPos;
    float desiredVel = constrain(remaining * 2.0f, -p.maxVel, p.maxVel);
    
    // 速度平滑趋近
    float velError = desiredVel - p.currentVel;
    float accCmd = constrain(velError * 3.0f, -p.maxAcc, p.maxAcc);
    
    // 加速度平滑(Jerk限制)
    p.currentAcc += constrain(accCmd - p.currentAcc, -p.maxJerk*dt, p.maxJerk*dt);
    p.currentVel += p.currentAcc * dt;
    p.currentPos += p.currentVel * dt;
    
    if (fabs(remaining) < 0.01f && fabs(p.currentVel) < 0.05f) {
        p.active = false;
        p.currentVel = 0;
        p.currentAcc = 0;
        p.currentPos = p.goalPos;
    }
}

// ==================== 主循环 ====================
float robotX = 0, robotY = 0;

void setup() {
    Serial.begin(115200);
    motorL.linkDriver(&driverL); motorR.linkDriver(&driverR);
    motorL.controller = MotionControlType::velocity;
    motorR.controller = MotionControlType::velocity;
    motorL.init(); motorR.init();
    motorL.initFOC(); motorR.initFOC();
    
    originalPath = {{0, 0, 1.5f, 2.5f, 2.5f, 2.5f, 4.0f, 1.5f}};
    activePath = originalPath;
    
    initOnlineSCurve(online);
    replanTo(online, 1.0f);
}

void loop() {
    motorL.loopFOC(); motorR.loopFOC();
    
    // 模拟检测到障碍物(在中途触发)
    static bool obstacleDetected = false;
    if (!obstacleDetected && online.currentPos > 0.3f) {
        obstacleDetected = true;
        adjustForObstacle(activePath, 2.0f, 2.0f, 1.0f);
        Serial.println("检测到障碍物,贝塞尔路径已修正");
    }
    
    // 在线S曲线更新
    updateOnline(online, 0.01f);
    
    // 用当前进度在(可能修正后的)路径上取点
    float t = constrain(online.currentPos, 0.0f, 1.0f);
    float targetX, targetY;
    bezierCubic(t, targetX, targetY, activePath);
    
    float dx = targetX - robotX;
    float dy = targetY - robotY;
    float dist = sqrt(dx*dx + dy*dy);
    
    if (dist > 0.005f) {
        float heading = atan2(dy, dx);
        float speed = fabs(online.currentVel) * 2.0f;
        motorL.move(speed - heading * 0.125f);
        motorR.move(speed + heading * 0.125f);
        robotX = targetX;
        robotY = targetY;
    } else {
        motorL.move(0); motorR.move(0);
    }
    
    delay(10);
}

要点:replanTo 不重置 currentVel,保留当前速度进行重规划。如果剩余距离小于制动距离,进入紧急减速。贝塞尔的 adjustForObstacle 只偏移中间控制点(P1、P2),起点和终点不变,保证路径修正不影响已确定的任务端点。

要点解读

  1. 贝塞尔解决“路径长什么样”,S曲线解决“什么时候走到哪”
    这是两个完全独立的数学工具。贝塞尔曲线的控制点决定路径的几何形状(弯多大、往哪弯),S曲线的 maxVel/maxAcc/maxJerk 决定沿这条路径的速度剖面。案例一用 sPlanner.pos(归一化进度)作为桥梁,让贝塞尔只关心“t=0.5 时在哪”,S曲线只关心“t 如何随时间变化”。

  2. 工业级场景必须用五次贝塞尔,三次不够
    三次贝塞尔只能保证位置和速度连续(C¹),加速度在连接点可能突变。五次贝塞尔有六个控制点,可以同时约束位置、速度、加速度在过渡段两端的连续性。搜索资源明确指出,五次贝塞尔过渡能有效抑制路径切换时的加速度不连续,避免刚性冲击和柔性振荡。

  3. 七段S曲线的“完整”意味着七段时间分段都参与计算
    简化五段式(省略匀加速/匀减速)在嵌入式平台常用,但工业级场景需要完整七段。案例一的 planSCurve 计算了 t1~t7 七段时间,包括加加速、匀加速、减加速、匀速、加减速、匀减速、减减速。每段的加速度表达式不同,updateSCurve 用状态机逐段计算。

  4. 在线重规划必须保留当前速度状态
    工业机器人不会“停下来再重新规划”。案例三的 replanTo 只改 goalPos,保留 currentVel 和 currentAcc 作为重规划的初始条件。如果新目标导致剩余距离小于当前速度的制动距离,立即进入紧急减速模式。这是“不停机重规划”与“离线规划”的根本区别。

  5. Arduino 的算力瓶颈决定了架构必须是“离线规划 + 在线插值”
    完整七段S曲线的参数计算(求解各段时间)涉及三次方程,在 Arduino 上做实时求解不现实。案例一的 planSCurve 在 setup() 中一次性计算好 t1~t7,loop() 只做查表式积分。贝塞尔曲线同理:控制点固定后,bezierCubic 每次只需几次乘加。规划层重计算,执行层轻插值,是嵌入式平台实现工业级平滑性的务实路径。

在这里插入图片描述
4、完整七段 S 曲线规划器(解析实现 + 自动退化)+ BLDC 位置环
适用硬件:ESP32 / Arduino Due + SimpleFOC BLDC + 编码器。
核心思路:规划器一次性解出全部时间参数(加加速/匀加速/减加速/匀速/加减速/匀减速/减减速共七段),运行时按 t 落入哪段代入解析式求位置;位移不足时自动降速退化(七段→六段→三角 S),保证 vmax/amax/jmax 三约束始终同时成立。

/*
 * 案例4:完整七段S曲线 + 位置环跟踪(v0=v1=0)
 */
#include <SimpleFOC.h>

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

// ---- 规划器约束(工业级默认三参数)----
const float VMAX = 20.0f;    // rad/s
const float AMAX = 60.0f;    // rad/s²
const float JMAX = 400.0f;   // rad/s³

struct SProfile {
  float Tj, Tc1, Ta;         // 加速段:加加速时长/匀加速时长/总时长
  float Tc2, Td;             // 减速段(对称)
  float Tv;                  // 匀速段时长
  float j, a, v;             // 实际使用的 jerk/加速度/速度(可能被降级)
  float Sa, Sd;              // 加速/减速位移
  float T, x0, x1;           // 总时长、起止位置
};
SProfile P;

// 位移约束下可用的最大速度(退化求解核心)
float solveVel(float S_half, float amax, float jmax) {
  float Tj = amax / jmax;
  float disc = 0.25f*Tj*Tj + 2.0f*S_half/amax;
  float v = (sqrt(disc) - 0.5f*Tj) * amax;     // 梯形S解
  if (v >= 0.5f*amax*Tj) return v;             // 可达:用全加速度
  return pow(1.5f*S_half*sqrt(jmax), 2.0f/3.0f); // 退化三角S解
}

void planS(float x0, float x1, float vmax, float amax, float jmax) {
  P.x0 = x0; P.x1 = x1;
  float s = fabs(x1 - x0);
  P.Tj = amax / jmax;  P.a = amax;  P.j = jmax;  P.v = vmax;

  // 1) 单侧位移预算 s/2(加速/减速对称),位移不足则降速
  float vNeed = solveVel(s / 2.0f, amax, jmax);
  if (vNeed < P.v) P.v = vNeed;
  float v = P.v;

  // 2) 判分支:能否达到峰值加速度
  if (v <= 0.5f*amax*P.Tj) {                    // 三角S:峰值加速度未达
    float Tj2 = sqrt(v / jmax);
    P.Tj = Tj2;  P.Ta = 2*Tj2;  P.Tc1 = 0;      // 只有加加速+减加速
    P.a = jmax*Tj2;
    P.Sa = (2.0f/3.0f)*v*Tj2;                   // j·Tj²³/3
  } else {                                      // 梯形S:标准七段
    float X = v / amax;                         // Tj+Tc1
    P.Tc1 = X - P.Tj;
    P.Ta = 2*P.Tj + P.Tc1;
    P.Sa = 0.5f*v*(X + P.Tj);                   // ½v(v/amax+Tj)
  }
  P.Sd = P.Sa;  P.Td = P.Ta;  P.Tc2 = P.Tc1;    // 对称减速

  // 3) 匀速段(可能为0 -> 六段)
  float S4 = s - P.Sa - P.Sd;
  P.Tv = (S4 > 1e-6f) ? S4 / v : 0.0f;
  P.T  = P.Ta + P.Tv + P.Td;
}

// 运行时求值:按 t 分段代入解析式
float evalPos(const SProfile &P, float t) {
  float s = 0; float j = P.j, a = P.a, v = P.v;
  float Tj = P.Tj, Tc1 = P.Tc1, Ta = P.Ta;
  if (t <= 1e-9f) return P.x0;

  if (t < Tj) {                                 // 段1 加加速
    s = j*t*t*t / 6.0f;
  } else if (t < Tj + Tc1) {                    // 段2 匀加速
    float u = t - Tj;
    s = j*Tj*Tj*Tj/6 + 0.5f*j*Tj*Tj*u + 0.5f*a*u*u;
  } else if (t < Ta) {                          // 段3 减加速(镜像到加速终点)
    float tau = Ta - t;
    s = P.Sa - j*tau*tau*tau/6.0f;
  } else if (t < Ta + P.Tv) {                   // 段4 匀速
    s = P.Sa + v*(t - Ta);
  } else if (t < Ta + P.Tv + Tj) {              // 段5 加减速
    float u = t - (Ta + P.Tv);
    s = P.Sa + v*P.Tv + j*u*u*u/6.0f;
  } else if (t < Ta + P.Tv + Tj + P.Tc2) {      // 段6 匀减速
    float u = t - (Ta + P.Tv + Tj);
    float v1 = v - 0.5f*j*Tj*Tj;
    s = P.Sa + v*P.Tv + j*Tj*Tj*Tj/6 + v1*u + 0.5f*a*u*u;
  } else {                                      // 段7 减减速(镜像到终点)
    float tau = P.T - t;
    s = P.Sa + v*P.Tv + P.Sd - j*tau*tau*tau/6.0f;
  }
  return P.x0 + (P.x1 >= P.x0 ? s : -s);        // 方向
}

void setup() {
  encoder.init(); encoder.enableInterrupts(onEncoder);
  motor.linkSensor(&encoder);
  motor.controller = MotionControlType::angle;  // 位置环
  motor.PID_angle.P = 30.0f; motor.PID_angle.I = 0; motor.PID_angle.D = 0.5f;
  motor.voltage_limit = 12.0f; motor.current_limit = 3.0f;
  motor.init();
  driver.voltage_power_supply = 12.0f; driver.init();
  motor.linkDriver(&driver);

  planS(0, 10.0f, VMAX, AMAX, JMAX);            // 一次规划
  Serial.print("T="); Serial.print(P.T);
  Serial.print(" v_used="); Serial.println(P.v);
}

unsigned long last = 0; float t = 0;
void loop() {
  float dt = (millis() - last) / 1000.0f; last = millis();
  t += dt; if (t > P.T) t = P.T;                // 时间钳位
  float q = evalPos(P, t);
  motor.loopFOC(); motor.move(q);               // 执行:纯位置跟踪
  if (t >= P.T) { Serial.println("MOTION DONE"); while(1); }
  delay(1);
}

工程提示:evalPos 是 O(1) 查段 + 解析式,1kHz 控制周期内开销可忽略;若目标点连续下发(多段运动),只需每段到来时重跑 planS 并清 t,无需停电机——见案例三的段间衔接。

5、FreeRTOS 规划/插值/执行三任务 + 贝塞尔路径 × S 曲线时间律
适用硬件:ESP32 双核 + 双轴 BLDC(X/Y 龙门式)+ 编码器。
核心思路:把"路径形状"与"运动时间"彻底解耦——三次贝塞尔控制点只定义空间曲线形状;S 曲线只定义"沿路径走了多长弧长 s(t)";两轴共享同一时间律 → 天然同步联动。弧长先离散成查找表,运行时 s(t) → 查表得参数 u → 贝塞尔公式出 (x,y)。

/*
 * 案例5:FreeRTOS 三任务 + 贝塞尔路径 + S曲线时间参数化
 *   Planner(低频): 新目标 -> 生成贝塞尔控制点 + S曲线profile
 *   Interp(100Hz): t -> s(t) -> u -> 贝塞尔(x,y) -> 目标位置
 *   Exec(1kHz):    双轴位置环跟踪
 */
#include <SimpleFOC.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

BLDCMotor mX(7), mY(7);
BLDCDriver3PWM dX(9,10,11,8), dY(22,23,24,25);
Encoder eX(2,3,2048), eY(32,33,2048);
void onX(){eX.handleA();} void onY(){eY.handleA();}
SemaphoreHandle_t mux;

// ---- 三次贝塞尔控制点(路径几何,仅形状)----
float P0[2] = {0,0}, P1[2] = {2,1}, P2[2] = {4,-1}, P3[2] = {6,0};
// 弧长查找表(规划时一次性生成)
#define LU 256
float arcLen[LU+1]; float totalLen;
void buildArcTable() {
  arcLen[0] = 0;
  float px = P0[0], py = P0[1];
  for (int i = 1; i <= LU; i++) {
    float u = (float)i / LU;
    float x = (1-u)*(1-u)*(1-u)*P0[0] + 3*(1-u)*(1-u)*u*P1[0]
            + 3*(1-u)*u*u*P2[0] + u*u*u*P3[0];
    float y = (1-u)*(1-u)*(1-u)*P0[1] + 3*(1-u)*(1-u)*u*P1[1]
            + 3*(1-u)*u*u*P2[1] + u*u*u*P3[1];
    arcLen[i] = arcLen[i-1] + hypot(x-px, y-py);
    px = x; py = y;
  }
  totalLen = arcLen[LU];
}
// 弧长 -> 参数 u(线性插值查表)
float uFromArc(float s) {
  if (s <= 0) return 0; if (s >= totalLen) return 1;
  for (int i = 1; i <= LU; i++) if (arcLen[i] >= s) {
    float w = (s - arcLen[i-1]) / (arcLen[i] - arcLen[i-1] + 1e-9f);
    return ((float)(i-1) + w) / LU;
  }
  return 1;
}
void bezierAt(float u, float &x, float &y) {
  float a = (1-u)*(1-u)*(1-u), b = 3*(1-u)*(1-u)*u, c = 3*(1-u)*u*u, d = u*u*u;
  x = a*P0[0] + b*P1[0] + c*P2[0] + d*P3[0];
  y = a*P0[1] + b*P1[1] + c*P2[1] + d*P3[1];
}

// ---- S曲线时间律(复用案例一的 planS/evalPos,此处用弧长域)----
// 简写:用案例一 SProfile 存"弧长s(t)",这里直接复用其函数(略重复定义)
SProfile SP;   // 规划 s(t):s(0)=0 -> s(T)=totalLen
// evalPos 改名 evalS,语义为"t 时刻已走弧长"

volatile float g_qx = 0, g_qy = 0;   // 插值任务写,执行任务读
volatile bool  newPlan = false;

// ---- 规划任务:事件驱动(收到新目标才跑,100ms 周期检查)----
void taskPlanner(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    if (serialHasNewTarget()) {               // 上位机新路径(示意)
      readBezierControlPoints();              // 更新 P0..P3
      buildArcTable();                        // 重算弧长表
      planS(0, totalLen, 15.0f, 40.0f, 300.0f); // 规划时间律 s(t)
      xSemaphoreTake(mux, portMAX_DELAY);
      newPlan = true; g_qx = 0; g_qy = 0;
      xSemaphoreGive(mux);
    }
    vTaskDelayUntil(&last, pdMS_TO_TICKS(100));
  }
}

// ---- 插值任务:100Hz,s(t)->u->贝塞尔点 ----
void taskInterp(void*) {
  TickType_t last = xTaskGetTickCount();
  float t = 0;
  for (;;) {
    bool np;
    xSemaphoreTake(mux, portMAX_DELAY); np = newPlan; xSemaphoreGive(mux);
    if (np) { t = 0; newPlan = false; }

    float s = evalPos(SP, t);                 // 时间律:已走弧长
    float u = uFromArc(s);                    // 弧长 -> 参数
    float x, y; bezierAt(u, x, y);            // 贝塞尔几何 -> 笛卡尔点
    xSemaphoreTake(mux, portMAX_DELAY);
    g_qx = x; g_qy = y;
    xSemaphoreGive(mux);
    t += 0.01f;                               // 100Hz 推进
    vTaskDelayUntil(&last, pdMS_TO_TICKS(10));
  }
}

// ---- 执行任务:1kHz 双轴位置环,纯跟踪 ----
void taskExec(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    float qx, qy;
    xSemaphoreTake(mux, portMAX_DELAY);
    qx = g_qx; qy = g_qy;
    xSemaphoreGive(mux);
    mX.loopFOC(); mY.loopFOC();
    mX.move(qx); mY.move(qy);
    vTaskDelayUntil(&last, pdMS_TO_TICKS(1));
  }
}

void setup() {
  // 双轴电机/编码器初始化同案例一(略)
  buildArcTable(); planS(0, totalLen, 15.0f, 40.0f, 300.0f);
  mux = xSemaphoreCreateMutex();
  xTaskCreatePinnedToCore(taskPlanner, "plan", 4096, NULL, 1, NULL, 0);  // 最低优先
  xTaskCreatePinnedToCore(taskInterp,  "interp", 4096, NULL, 2, NULL, 0);
  xTaskCreatePinnedToCore(taskExec,    "exec",  4096, NULL, 3, NULL, 1);
}
void loop() { vTaskDelay(portMAX_DELAY); }

工程提示:弧长表 LU=256 一次性生成,运行时只做线性查表;两轴共享同一 s(t),任何时刻两轴都在贝塞尔曲线上(联动误差为零);新目标到达时 newPlan 标志让插值任务原子重启时间轴,电机无需停止。

6、多段贝塞尔拼接 + 前瞻约束 + 非零初速 S 曲线衔接(工业连续轨迹)
适用硬件:ESP32 + SimpleFOC BLDC + 编码器(单轴示意,多轴同构扩展)。
核心思路:工业焊接/涂胶轨迹是多段连续运动,段间不能停。两个关键扩展——① 前瞻(look-ahead):扫描后续段的转角,约束本段末端速度 v1;② 带初速度的 S 曲线:段内从 v0 加速/减速到 v1,全轨迹速度连续、加速度连续(jerk 受限)。

/*
 * 案例三:多段拼接 + 前瞻约束 + 非零初速 S 曲线衔接
 * 每段 profile:从 v0 到 v1,jerk/加速度受限;段间 v 连续
 */
#include <SimpleFOC.h>

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

// ---- 段表:每段终点 + 段末目标速度(由前瞻算出,非0)----
#define MAX_SEG 8
struct Seg {
  float x1;        // 段终点位置
  float v1;        // 段末速度(前瞻约束后)
  float T;         // 本段时长
  float x0; float v0;
};
Seg segs[MAX_SEG]; int nSeg = 0;

// ---- 前瞻:扫描后续段转角,约束本段末速度 ----
// 简化:两段夹角越大,允许的衔接速度越低(v1 = f(转角))
float lookAheadV(int i) {
  if (i >= nSeg - 1) return 0;              // 末段末速度 0(精确停)
  float a0 = 0, a1 = 0;                     // 本段/下段方向角(示意已知)
  float ang = fabs(a1 - a0); if (ang > PI) ang = 2*PI - ang;
  float v = 15.0f * cos(ang / 2.0f);        // 转角越大,衔接越慢
  return max(v, 0.0f);
}

// ---- 带非零初速的 S 曲线(单段内 v0 -> v1,方向可变)----
struct S2 { float Tj, Tj2, Tv, T1, T2, T, j, amax; float S; };
S2 planSeg(float x0, float x1, float v0, float v1, float amax, float jmax) {
  S2 p; p.j = jmax; p.amax = amax;
  float s = x1 - x0;                        // 有符号位移
  float dir = (s >= 0) ? 1.0f : -1.0f;
  float S = fabs(s);
  float va = v0 * dir, vb = v1 * dir;       // 投影到正方向
  p.Tj = amax / jmax;
  float dv = vb - va;
  // 加速行程(jerk 受限的初等计算,示意核心结构)
  p.T1 = p.Tj;                              // 加加速+匀加速+减加速 组合时长
  p.Tj2 = p.Tj;
  // 单段时长由加减速对称结构解出(工程上有解析公式,此处给出结构)
  float vp = 0.5f * (va + vb);
  float Sa = 0.5f * (va + vb) * p.T1 * 0.5f; // 示意近似
  float Tv = (S - 2 * Sa) / max(vp, 1e-3f);
  if (Tv < 0) { Tv = 0; /* 位移不足则降低目标速度(退化),此处示意 */ }
  p.Tv = Tv;
  p.T = 2 * p.T1 + Tv;
  p.S = S;
  p.x0 = x0; p.v0 = v0; p.v1 = v1;
  return p;
}

// 运行时求值:段内位置(示意:匀速+三角加速包络,工程用完整七段)
float evalSeg(const S2 &p, float t) {
  if (t >= p.T) return p.x0 + p.S;
  float dir = (p.S >= 0) ? 1 : -1;
  float vavg = 0.5f * (p.v0 + p.v1);
  float s = vavg * t;                       // 简化积分(实际按段分段积分)
  return p.x0 + dir * s;
}

void setup() {
  // 电机初始化同案例一(略)
  // 示例:3 段连续轨迹,段末速度由前瞻决定
  segs[0] = {2.0f, 0, 0, 0, 0}; nSeg = 1;
  segs[1] = {4.0f, 0, 0, 0, 0}; nSeg = 2;
  segs[2] = {5.0f, 0, 0, 0, 0}; nSeg = 3;
  for (int i = 0; i < nSeg; i++) segs[i].v1 = lookAheadV(i);
  // 逐段生成 profile(每段 v0 = 上段 v1,速度连续)
  for (int i = 0; i < nSeg; i++) {
    float v0 = (i == 0) ? 0 : segs[i-1].v1;   // 段间速度连续
    float x0 = (i == 0) ? 0 : segs[i-1].x1;
    S2 p = planSeg(x0, segs[i].x1, v0, segs[i].v1, 60, 400);
    segs[i].v0 = v0; segs[i].T = p.T;
  }
  Serial.begin(115200);
}

unsigned long last = 0; float t = 0; int idx = 0;
void loop() {
  float dt = (millis() - last) / 1000.0f; last = millis();
  t += dt;
  if (idx < nSeg) {
    float q = evalSeg(planSeg(segs[idx].x0, segs[idx].x1, segs[idx].v0,
                   segs[idx].v1, 60, 400), t);
    motor.loopFOC(); motor.move(q);           // 执行:位置环
    if (t >= segs[idx].T) { idx++; t = 0; }   // 段间无缝切换(v 连续)
  }
  delay(1);
}

工程提示:案例三为示意结构,生产代码应把 planSeg 写成完整七段解析式(含 v0≠v1 的加/减速混合段)+ 弧长/位移前瞻表(扫描后 N 段取最小 v1);段间切换在 1kHz 中断里完成,主循环只管推进索引,保证不丢帧。

要点解读
一、S 曲线的价值是"限制 jerk",而不是"限制加速度"
工业机器人抖动、共振、机构磨损的根源是加加速度(jerk)突变——加速度图上的尖角就是冲击。完整七段 S 曲线把加速度分成"加加速-匀加速-减加速"三段斜坡(jerk 恒定为 Jmax),使加速度连续、无尖角(案例一七段的段界正是按加速度形状切的)。判定"是否真 S 曲线"只需看加速度图是否连续:梯形加速度图(加减速时加速度一步跳满)是伪 S,只有 jerk 受限的才是真 S。轨迹平滑度的工业级标准,本质上就是"jerk 是否可设计、可限制"。

二、规划器必须"自动退化",位移不足时优雅降级而不是报错
短距离小位移是最常见的工况,但七段参数按满速设计必然不可行。正确的规划器在解算前先做可行性检查(案例一 solveVel:用单侧位移预算反解可用峰值速度),自动完成七段→六段(匀速段归零)→三角 S(加速度峰值未达)的降级,且降级后 v/amax/jmax 三约束依然同时成立。工业级规划器的健壮性就体现在这里:任何输入(哪怕位移只有 1μm)都给出一个平滑、可达、时间最短的轨迹,而不是异常或急停。

三、贝塞尔负责"形状",S 曲线负责"节奏",两者必须解耦
贝塞尔曲线天然平滑但弧长无法解析,S 曲线天然无冲击但不定义形状。把它们耦合在一起算(直接对 x(t)、y(t) 各做一条 S 曲线)会破坏路径形状(两轴时间律不同步);正确做法是案例二的"路径-时间参数化":贝塞尔只定义空间曲线,S 曲线定义"沿曲线走了多少弧长 s(t)",再用弧长查找表(arcLen[] 线性插值)把 s 映射回贝塞尔参数 u。这样两轴共享同一时间律,任意时刻都在曲线上,联动误差为零——这是"工业级联动"与"两轴各自动"的分水岭。

四、多段连续轨迹的难点是"段间衔接",核心工具是前瞻与初速
焊接、涂胶、点胶最忌段间停一下——停点就是瑕疵点。解决靠两件事:前瞻(look-ahead)在运动前扫描后续段转角,把本段末端速度压到"过弯不抖"的水平(案例三 lookAheadV:转角越大 v1 越小);非零初速 S 曲线让每段从 v0 平滑过渡到 v1,而不是从零加速(案例三 v0 = 上段 v1 的速度连续性)。两者配合,整条轨迹速度曲线连续、加速度曲线连续、无停点——衔接质量直接决定焊接/涂胶的成品质量,是工业规划器最值钱的部分。

五、规划-插值-执行三层分离,插值层是"时间轴唯一所有者"
轨迹规划是低频的(新目标 1~10Hz),插值求值是中频的(100Hz 或 1kHz 随控制周期),电机执行是高频的(1kHz 位置环)。案例二用三个 Task 把它们物理分开,关键约束是:插值层独占时间轴(t 只在插值任务里推进),规划层只负责换 profile,执行层只消费目标位置。这样即使规划层被上位机通信阻塞,插值层仍能把当前轨迹走完;即使执行层被打断,规划参数也不受损。每一层的失败都被隔离,整个运动链才称得上"工业级"。

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

在这里插入图片描述

Logo

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

更多推荐