一、简介:为什么运动控制库是PLC的"心脏"?

  • 工业场景痛点

    • 机械臂焊接抖动 → 焊缝偏差0.1mm,良品率从99%跌至85%。

    • 数控机床加减速不平滑 → 刀具磨损加速3倍,维护成本飙升。

    • 多轴联动不同步 → 立体雕刻错位,整批物料报废。

  • 运动控制库价值

    • 将梯形/S曲线加减速、直线/圆弧插补等复杂算法封装为标准化API。

    • 通过实时Linux的确定性调度,实现微秒级周期控制,抖动<50μs。

    • 支持EtherCAT、CANopen等工业总线,无缝接入现有PLC生态。

  • 掌握收益:能独立设计从单轴点动到6轴联动的完整运动系统,具备工业产品化能力。


二、核心概念:6个关键词搞懂运动控制

关键词一句话说明本文出现场景
梯形加减速速度曲线呈梯形,加速度恒定,实现简单但冲击大快速定位、低成本场景
S曲线加减速加速度连续变化, jerk(加加速度)受限,冲击小高精度、低振动场景
直线插补两轴或多轴按直线轨迹联动,各轴速度成比例平面切割、3D打印
圆弧插补按圆弧轨迹联动,需实时计算圆心、半径、角度圆形倒角、弧形焊接
前瞻规划提前计算多段轨迹衔接,避免急停急启高速连续加工
电子齿轮/凸轮从轴跟随主轴,速度比可调或按凸轮曲线飞剪、追剪、包装机械

三、环境准备:15分钟搭好开发环境

3.1 硬件环境

组件规格说明
工控机/嵌入式板x86_64或ARM64,≥2核推荐研华UNO-2372G或瑞芯微RK3588
运动控制卡EtherCAT主站卡或GPIO+编码器接口可选IgH EtherCAT Master或软件模拟
伺服驱动器支持CiA402协议汇川、松下、安川均可
电机+编码器伺服电机,17位以上绝对值编码器精度直接决定控制效果

3.2 软件环境

组件版本安装命令
实时Linux内核5.15-rt或5.10-rt见下文一键脚本
GCC≥9.0sudo apt install build-essential
CMake≥3.16sudo apt install cmake
IgH EtherCAT Master1.5.2源码编译安装
数学库GSL或自研sudo apt install libgsl-dev

3.3 一键安装实时内核(可复制)

#!/bin/bash
# install_rt_kernel.sh
set -e
VERSION="5.15.71"
RT_PATCH="rt53"
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-${VERSION}.tar.xz
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/projects/rt/${VERSION}/older/patch-${VERSION}-${RT_PATCH}.patch.xz
tar -xf linux-${VERSION}.tar.xz
cd linux-${VERSION}
xzcat ../patch-${VERSION}-${RT_PATCH}.patch.xz | patch -p1
make menuconfig  # 启用CONFIG_PREEMPT_RT_FULL
make -j$(nproc) deb-pkg
sudo dpkg -i ../linux-image*.deb ../linux-headers*.deb
sudo update-grub
echo "重启后选择RT内核"

3.4 创建项目目录

mkdir -p ~/motion-ctrl-lib/{src,include,test,docs}
cd ~/motion-ctrl-lib
git init  # 建议版本控制

四、应用场景:数控机床多轴联动加工

场景描述:某汽车零部件厂需改造3台数控加工中心,要求:

  • 加工铝合金发动机缸盖,表面粗糙度Ra≤1.6μm。

  • 3轴联动(X/Y直线+Z圆弧),最大进给速度10m/min。

  • 换刀时间<2s,刀位精度±0.005mm。

  • 24小时连续运行,MTBF>2000小时。

技术方案

  • 实时Linux+自研运动控制库替代传统封闭数控系统。

  • X/Y轴采用S曲线加减速,Z轴圆弧插补配合前瞻规划。

  • EtherCAT总线周期1ms,伺服周期125μs。

  • 通过电子齿轮功能实现主轴与进给轴同步。

效果:单件加工时间从4.2分钟降至3.1分钟,刀具寿命延长40%,改造成本仅为进口系统的1/5。


五、实际案例与步骤:从零构建运动控制库

5.1 项目架构设计

motion-ctrl-lib/
├── include/
│   ├── mc_core.h          # 核心数据结构与宏
│   ├── mc_profile.h       # 加减速规划
│   ├── mc_interpolation.h # 插补算法
│   ├── mc_axis.h          # 单轴控制
│   └── mc_group.h         # 多轴联动
├── src/
│   ├── mc_profile.c       # 梯形/S曲线实现
│   ├── mc_interpolation.c # 直线/圆弧插补
│   ├── mc_axis.c          # 轴状态机
│   └── mc_group.c         # 轴组管理
├── test/
│   ├── test_profile.c     # 加减速单元测试
│   └── test_interpolation.c
└── examples/
    └── demo_3axis_gcode.c # 3轴G代码解析示例

5.2 核心数据结构(可复制)

/* include/mc_core.h */
#ifndef MC_CORE_H
#define MC_CORE_H

#include <stdint.h>
#include <stdbool.h>
#include <math.h>

/* 轴状态枚举 */
typedef enum {
    MC_AXIS_IDLE = 0,       /* 空闲 */
    MC_AXIS_HOMING,         /* 回零中 */
    MC_AXIS_JOGGING,        /* 点动 */
    MC_AXIS_MOVING,         /* 运动中 */
    MC_AXIS_ERROR           /* 故障 */
} mc_axis_state_t;

/* 加减速类型 */
typedef enum {
    MC_PROFILE_TRAP = 0,    /* 梯形 */
    MC_PROFILE_S_CURVE        /* S曲线 */
} mc_profile_type_t;

/* 轴配置参数 */
typedef struct {
    double max_vel;         /* 最大速度 (mm/s) */
    double max_acc;         /* 最大加速度 (mm/s²) */
    double max_jerk;        /* 最大加加速度 (mm/s³),S曲线用 */
    double enc_resolution;  /* 编码器分辨率 (脉冲/mm) */
    double gear_ratio;        /* 减速比 */
} mc_axis_config_t;

/* 轴实时数据 */
typedef struct {
    int32_t id;             /* 轴号 */
    mc_axis_state_t state;
    double cmd_pos;         /* 指令位置 (mm) */
    double act_pos;         /* 实际位置 (mm) */
    double cmd_vel;         /* 指令速度 */
    double act_vel;         /* 实际速度 */
    int64_t timestamp_us;   /* 时间戳 */
} mc_axis_data_t;

/* 轨迹段 */
typedef struct {
    double start_pos[4];    /* 起点,支持4轴 */
    double end_pos[4];       /* 终点 */
    double target_vel;       /* 目标速度 */
    mc_profile_type_t profile;
    double acc_time;         /* 加速时间(前瞻规划用) */
    double dec_time;         /* 减速时间 */
} mc_segment_t;

/* 错误码 */
#define MC_OK               0
#define MC_ERR_PARAM       -1
#define MC_ERR_LIMIT       -2
#define MC_ERR_INTERPOLATE -3

#endif /* MC_CORE_H */

5.3 S曲线加减速算法实现(核心代码)

/* src/mc_profile.c */
#include "mc_profile.h"
#include <string.h>

/* S曲线规划:7段式(加加速-匀加速-减加速-匀速-加减速-匀减速-减减速) */
int mc_s_curve_plan(double dist, double v0, double vt, 
                    double vmax, double amax, double jmax,
                    mc_s_profile_t *out)
{
    memset(out, 0, sizeof(*out));
    
    /* 参数检查 */
    if (dist < 0 || vmax <= 0 || amax <= 0 || jmax <= 0)
        return MC_ERR_PARAM;
    
    /* 计算关键时间点 */
    double t1 = amax / jmax;           /* 加加速段时间 */
    double t2 = (vmax - v0) / amax - t1; /* 匀加速段时间(可能为0) */
    if (t2 < 0) { t2 = 0; t1 = sqrt((vmax - v0) / jmax); }
    
    double t3 = t1;                     /* 减加速段时间 */
    double t4;                          /* 匀速段时间(待求) */
    double t5 = t1, t6 = t2, t7 = t3;   /* 减速段对称(简化模型) */
    
    /* 计算各段位移 */
    double s1 = v0*t1 + jmax*t1*t1*t1/6;           /* 加加速段 */
    double s2 = (v0 + jmax*t1*t1)*t2 + amax*t2*t2/2; /* 匀加速段 */
    double s3 = s1 + (vmax - v0)*t1/2;              /* 减加速段(对称) */
    double s_acc = s1 + s2 + s3;                    /* 总加速段位移 */
    double s_dec = s_acc;                           /* 对称减速段 */
    
    /* 判断是否能达到vmax */
    if (s_acc + s_dec > dist) {
        /* 三角形速度曲线,无匀速段 */
        out->vmax_reach = false;
        /* 重新计算峰值速度(数值求解,此处用近似) */
        out->v_peak = sqrt(dist * amax + (v0*v0 + vt*vt)/2);
        t4 = 0;
    } else {
        out->vmax_reach = true;
        out->v_peak = vmax;
        t4 = (dist - s_acc - s_dec) / vmax;
    }
    
    /* 填充输出结构 */
    out->t[0] = 0;
    out->t[1] = t1;
    out->t[2] = t1 + t2;
    out->t[3] = t1 + t2 + t3;
    out->t[4] = out->t[3] + t4;
    out->t[5] = out->t[4] + t5;
    out->t[6] = out->t[5] + t6;
    out->t[7] = out->t[6] + t7;
    
    out->v0 = v0;
    out->vt = vt;
    out->vmax = vmax;
    out->amax = amax;
    out->jmax = jmax;
    out->total_dist = dist;
    
    return MC_OK;
}

/* 根据时间t计算当前速度、位置、加速度 */
int mc_s_curve_eval(const mc_s_profile_t *prof, double t,
                    double *pos, double *vel, double *acc)
{
    if (t < 0 || t > prof->t[7]) return MC_ERR_PARAM;
    
    int i;
    for (i = 0; i < 7; i++) {
        if (t >= prof->t[i] && t < prof->t[i+1]) break;
    }
    
    double dt = t - prof->t[i];
    double v_start, a_start, j;
    
    switch (i) {
        case 0: /* 加加速段 */
            v_start = prof->v0;
            a_start = 0;
            j = prof->jmax;
            *acc = a_start + j * dt;
            *vel = v_start + a_start*dt + 0.5*j*dt*dt;
            *pos = prof->v0*prof->t[i] + 0.5*a_start*prof->t[i]*prof->t[i] 
                   + j*prof->t[i]*prof->t[i]*prof->t[i]/6;
            break;
        /* 其余段类似,略... */
        default:
            return MC_ERR_INTERPOLATE;
    }
    return MC_OK;
}

5.4 直线插补算法

/* src/mc_interpolation.c */
#include "mc_interpolation.h"

/* 两轴直线插补 */
int mc_line_interpolate_2d(const mc_segment_t *seg, double ratio,
                           double *pos_x, double *pos_y)
{
    if (ratio < 0 || ratio > 1) return MC_ERR_PARAM;
    
    double dx = seg->end_pos[0] - seg->start_pos[0];
    double dy = seg->end_pos[1] - seg->start_pos[1];
    double len = sqrt(dx*dx + dy*dy);
    
    if (len < 1e-6) return MC_ERR_PARAM; /* 零距离 */
    
    /* 当前位移 = 总位移 * 比例 */
    double curr_len = len * ratio;
    
    *pos_x = seg->start_pos[0] + dx * ratio;
    *pos_y = seg->start_pos[1] + dy * ratio;
    
    return MC_OK;
}

/* 带速度规划的直线插补:输出每个周期的指令位置 */
int mc_line_interp_with_profile(const mc_segment_t *seg,
                                double cycle_time_ms,
                                int max_points,
                                mc_interp_point_t *points,
                                int *out_count)
{
    mc_s_profile_t profile;
    double dist = sqrt(pow(seg->end_pos[0]-seg->start_pos[0], 2) +
                       pow(seg->end_pos[1]-seg->start_pos[1], 2));
    
    /* S曲线规划 */
    mc_s_curve_plan(dist, 0, 0, seg->target_vel, 
                    seg->target_vel/0.1, seg->target_vel/0.01, &profile);
    
    double t = 0;
    int i = 0;
    while (t <= profile.t[7] && i < max_points) {
        double pos, vel, acc;
        mc_s_curve_eval(&profile, t, &pos, &vel, &acc);
        
        double ratio = pos / dist;
        mc_line_interpolate_2d(seg, ratio, 
                               &points[i].x, &points[i].y);
        points[i].vel = vel;
        points[i].timestamp_ms = t * 1000;
        
        t += cycle_time_ms / 1000.0;
        i++;
    }
    *out_count = i;
    return MC_OK;
}

5.5 标准化API设计

/* include/mc_api.h */
#ifndef MC_API_H
#define MC_API_H

#include "mc_core.h"

#ifdef __cplusplus
extern "C" {
#endif

/* 库初始化 */
int mc_init(void);
int mc_deinit(void);

/* 轴管理 */
int mc_axis_create(int axis_id, const mc_axis_config_t *cfg);
int mc_axis_destroy(int axis_id);
int mc_axis_enable(int axis_id);
int mc_axis_disable(int axis_id);
int mc_axis_home(int axis_id);           /* 回零 */
int mc_axis_jog(int axis_id, double vel); /* 点动 */

/* 单轴定位 */
int mc_axis_move_abs(int axis_id, double target_pos, double vel);
int mc_axis_move_rel(int axis_id, double delta_pos, double vel);

/* 多轴联动 */
int mc_group_create(int group_id, int *axis_ids, int num_axes);
int mc_group_line(int group_id, double *target_pos, double vel, 
                  mc_profile_type_t profile);
int mc_group_arc(int group_id, double *center, double radius, 
                 double angle, double vel);

/* 前瞻规划(高级) */
int mc_group_set_lookahead(int group_id, int segments, double tolerance);

/* 周期服务函数:必须在实时线程中1ms周期调用 */
int mc_periodic_service(int64_t timestamp_us);

/* 状态查询 */
int mc_axis_get_state(int axis_id, mc_axis_data_t *data);
int mc_group_get_state(int group_id, mc_axis_data_t *data_array);

#ifdef __cplusplus
}
#endif

#endif /* MC_API_H */

5.6 实时线程示例(1ms周期)

/* examples/rt_thread_demo.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#include <time.h>
#include "mc_api.h"

#define CYCLE_US 1000  /* 1ms周期 */

void *rt_motion_thread(void *arg)
{
    struct timespec next;
    clock_gettime(CLOCK_MONOTONIC, &next);
    
    while (1) {
        /* 绝对时间唤醒,保证周期确定性 */
        next.tv_nsec += CYCLE_US * 1000;
        if (next.tv_nsec >= 1000000000) {
            next.tv_sec++;
            next.tv_nsec -= 1000000000;
        }
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
        
        /* 运动控制周期服务 */
        int64_t now_us = next.tv_sec * 1000000LL + next.tv_nsec / 1000;
        mc_periodic_service(now_us);
        
        /* 此处可插入EtherCAT PDO发送 */
    }
    return NULL;
}

int main()
{
    /* 设置实时优先级 */
    pthread_t rt_thread;
    pthread_attr_t attr;
    struct sched_param param = { .sched_priority = 80 };
    
    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    pthread_attr_setschedparam(&attr, &param);
    
    /* 初始化库 */
    mc_init();
    
    /* 配置X轴 */
    mc_axis_config_t x_cfg = {
        .max_vel = 500,      /* 500mm/s */
        .max_acc = 5000,     /* 5000mm/s² */
        .max_jerk = 50000,   /* 50000mm/s³ */
        .enc_resolution = 10000, /* 10000脉冲/mm */
        .gear_ratio = 1.0
    };
    mc_axis_create(0, &x_cfg);
    mc_axis_enable(0);
    
    /* 配置Y轴 */
    mc_axis_create(1, &x_cfg);
    mc_axis_enable(1);
    
    /* 创建轴组 */
    int axes[] = {0, 1};
    mc_group_create(0, axes, 2);
    
    /* 启动实时线程 */
    pthread_create(&rt_thread, &attr, rt_motion_thread, NULL);
    
    /* 主线程:接收上位机指令 */
    printf("输入目标位置 X Y (如: 100 200): ");
    double tx, ty;
    while (scanf("%lf %lf", &tx, &ty) == 2) {
        double target[4] = {tx, ty, 0, 0};
        mc_group_line(0, target, 300, MC_PROFILE_S_CURVE);
        printf("运动启动,目标: %.2f, %.2f\n", tx, ty);
    }
    
    mc_deinit();
    return 0;
}

编译运行

gcc -O2 -o rt_demo rt_thread_demo.c -I../include -L../lib -lmotionctrl -pthread -lrt
sudo ./rt_demo  # 需root设置SCHED_FIFO

六、常见问题与解答(FAQ)

问题现象解决
S曲线计算耗时>500μs实时周期抖动查表法预计算,或降频到2ms周期
多轴联动不同步轨迹有棱角检查各轴max_vel/max_acc是否匹配,启用前瞻规划
电机啸叫高频振动降低jmax,或改用梯形加减速
EtherCAT周期超时PDO丢失隔离网卡中断,或改用IgH的ecrt工具优化
回零位置漂移重复精度差改用绝对值编码器,或增加回零减速点
圆弧插补有折线逼近误差大减小插补周期,或启用样条过渡

七、实践建议与最佳实践

  1. 算法优化

    • S曲线查表:离线计算典型速度曲线,运行时查表+线性插值,计算时间<10μs。

    • 定点数运算:嵌入式场景用Q15/Q31定点数,避免浮点单元延迟。

  2. 实时保障

    • CPU隔离:isolcpus=2,3 将运动控制绑定到隔离核。

    • 禁用超线程:BIOS关闭HT,消除同核竞争。

    • 锁粒度:轴级锁而非全局锁,减少阻塞。

  3. 调试技巧

    • 双缓冲轨迹:前台执行+后台预计算下一段,无缝衔接。

    • 虚拟轴模式:无硬件时模拟电机响应,提前验证算法。

  4. 安全设计

    • 软限位+硬限位双重保护。

    • 紧急停止(E-Stop)走独立硬件回路,不依赖软件。

  5. 版本管理

    • 算法参数JSON化,不同机型加载不同配置。

    • Git管理,tag对应发布版本,可追溯。


八、总结与应用场景

通过本文,我们完成了从S曲线加减速、直线/圆弧插补到标准化API的完整运动控制库设计。核心要点:

运动控制库架构
├─ 算法层:梯形/S曲线、直线/圆弧/样条插补
├─ 规划层:前瞻速度规划、电子齿轮/凸轮
├─ 轴控层:状态机、PID、前馈补偿
├─ 总线层:EtherCAT/CANopen PDO映射
└─ API层:mc_axis_* / mc_group_* 标准接口

典型应用场景

  • 3C行业:高速贴片机,XY直线插补+Z轴快速定位,节拍0.3s/件。

  • 汽车零部件:发动机缸盖加工,3轴联动+S曲线,表面粗糙度Ra1.2μm。

  • 新能源:锂电池叠片机,电子凸轮控制叠片张力,精度±0.05mm。

  • 机器人:SCARA机械臂,圆弧插补完成圆形密封胶涂布。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐