项目概览

MotionControl 是一套面向工业设备软件的 .NET 8 运动控制库。它把汇川 30G、60G EtherCAT 控制器的 Native API 封装在独立驱动中,对上提供稳定的控制器、轴、IO 与高级轨迹接口;同时提供 Core、Configuration 和 Mock 三层通用能力,让上位机不必在业务代码里到处判断控制卡型号。

当前分支已经在多个真实产线项目中使用。30G 与 60G 的控制器生命周期、单轴运动、回原点、伺服、报警和 IO 等通用链路经过项目级实机验证;60G 的 PVT、多轴插补、XYZ 坐标系轨迹、分段变速与轨迹缓冲 IO 也用于实际项目。这里的“验证”表示功能进入过真实设备项目,不等同于公开的性能基准或长期可靠性统计。

本文只讨论可以从源码和实际使用状态确认的工程设计。客户名称、设备拓扑、真实配置路径、工艺参数、轴数、节拍、产量和故障率均不公开,也不会把厂商 PDF、头文件或 Native DLL 复制到网站中。

为什么要单独做一层运动控制库

直接在 WPF、工艺流程或设备状态机中调用厂商 API,短期看起来最快,项目增加后却会反复遇到相同问题:

  • 同一种“移动到位置”,在不同控制器上有不同句柄、状态码和单位约定;
  • 业务层同时承担 EtherCAT 初始化、轴使能、错误码判断和界面状态刷新;
  • 机械参数改变后,脉冲、毫米、角度和电子齿轮比容易在多处重复换算;
  • 没有控制卡时,上位机流程、HMI 和配置功能无法独立开发;
  • 点位、轴组和工艺序列写死在代码中,设备换型需要重新编译;
  • PTP 批量发令与控制卡硬件插补概念混在一起,容易产生错误的同步假设。

这个项目的目标不是隐藏所有硬件差异,而是把差异放在正确的边界内:通用能力通过统一接口使用,厂商或型号独有的能力通过可选接口显式暴露,上层只有在确实需要高级轨迹时才依赖对应能力。

设计目标与边界

项目遵循几条明确原则:

  1. 依赖倒置:Core 只依赖公共契约,不引用任何汇川 Native 类型;
  2. 能力显式:基础控制与 PVT、插补、坐标系轨迹使用不同接口表达;
  3. 工程单位优先:应用层使用毫米、度和单位每秒,驱动层负责转换为脉冲;
  4. 失败可追踪:保留厂商错误码,同时提供统一的 MotionResult 与日志入口;
  5. 无硬件可开发:Mock 与真实驱动共享接口,并模拟运动、IO 和高级请求;
  6. 配置只负责装配:JSON 可以描述轴组、点位和序列,但真正执行仍属于 Core;
  7. 安全退出:关闭和释放时先停止运动、下使能,再清理轴与控制卡句柄。

它不是 PLC 安全程序,也不替代硬接线急停、安全继电器和驱动器自身保护。软件急停、软限位与状态检查只能作为控制系统的一层防护。

技术栈与运行约束

领域 选择 作用
运行时 .NET 8 / C# Nullable 与统一异步模型,承载公共库和驱动实现
总线 EtherCAT 控制器扫描、OP 状态、轴运动与远程 IO
Native 互操作 P/Invoke 调用汇川 30G、60G 厂商库
配置 System.Text.Json 轴组、示教点和运动序列的加载与保存
日志 IMotionLogger / NLog 适配器 将驱动日志接入宿主应用,同时允许空日志实现
离线调试 Mock Driver 在无控制卡环境模拟轴运动、IO 与高级轨迹请求

公共契约、Core、Configuration 与 Mock 目标框架为 net8.0;两个真实驱动为 net8.0-windows8.0。默认构建会把 x64 厂商 DLL 复制到驱动输出目录,x86 文件不进入默认输出。因此真实控制器宿主以 Windows x64、正确安装的厂商驱动和匹配实际从站拓扑的设备配置为前提。

解决方案架构

解决方案由六个职责独立的项目组成:

MotionControl.sln
├── MotionControl.Abstractions
│   ├── IMotionController / IAxis / IO interfaces
│   ├── optional trajectory capability interfaces
│   ├── request, state and configuration models
│   └── MotionResult / IMotionLogger
├── MotionControl.Core
│   ├── MotionManager / AxisGroup
│   ├── MotionSequencer / AxisMonitor
│   ├── TeachPointManager / SoftLimitManager
│   └── IoMonitor / Cylinder / extensions
├── MotionControl.Configuration
│   ├── JSON loaders
│   ├── validators
│   └── AxisGroup / TeachPoint / Sequence builders
├── MotionControl.Driver.Inovance
│   └── 30G controller, axis, P/Invoke and NLog adapter
├── MotionControl.Driver.Inovance60G
│   └── 60G controller, axis, P/Invoke and advanced trajectories
└── MotionControl.Driver.Mock
    └── in-memory controller, axes, IO and trajectory simulation

依赖保持单向:

Application
    ├──→ Configuration ──→ Core ──→ Abstractions
    ├──→ Driver.Inovance ─────────→ Abstractions
    ├──→ Driver.Inovance60G ──────→ Abstractions
    └──→ Driver.Mock ─────────────→ Abstractions

Core 不知道配置文件是否存在,也不知道控制器来自 30G、60G 还是 Mock。Configuration 只把文本配置装配为 Core 对象;驱动则只实现契约和硬件调用。这种依赖方向使宿主应用可以按项目组合需要的层,而不是引入一个包含全部状态的“万能设备类”。

公共接口与能力矩阵

IMotionController 定义控制卡生命周期、资源发现、轴管理、数字/模拟 IO、错误和状态事件;IAxis 定义位置与速度读取、伺服、报警、PTP、JOG、停止、回原点、状态刷新和配置应用。

高级轨迹没有被强塞进基础接口,而是拆成三个可选能力:

  • IMultiAxisPvtPathController:执行多轴 PVT 路径;
  • IMultiAxisInterpolationController:执行多轴直线插补;
  • ICoordinateSystemInterpolationController:准备并执行 XYZ 坐标系轨迹。

当前源码中的能力归属如下:

能力 汇川 30G 汇川 60G Mock
控制器生命周期、资源与 IO 实现并实机使用 实现并实机使用 内存模拟
PTP、JOG、回零、伺服与报警 实现并实机使用 实现并实机使用 运动过程模拟
轴组批量发令与序列编排 通过 Core 使用 通过 Core 使用 通过 Core 使用
多轴 PVT 当前未实现该能力接口 实现并实机使用 模拟并保留请求
多轴直线插补 当前未实现该能力接口 实现并实机使用 模拟并保留请求
XYZ 插补、分段速度与轨迹 IO 当前未实现该能力接口 实现并实机使用 模拟调度与请求留存

这里的“当前未实现”只描述本仓库公开类型的能力边界,不推断控制卡硬件本身是否具备相应功能。调用方可以通过接口检查能力,而不是根据类名或卡号猜测:

if (controller is ICoordinateSystemInterpolationController trajectoryController)
{
    var result = await trajectoryController.ExecuteCoordinateSystemInterpolationAsync(request, ct);
    if (!result.Success)
    {
        // 记录 ErrorCode 与 ErrorMessage,并进入设备定义的恢复流程
    }
}

控制器初始化与生命周期

30G 与 60G 使用各自的强类型初始化选项,但遵循相同生命周期。以 60G 为例:

using MotionControl.Abstractions.Interfaces;
using MotionControl.Driver.Inovance60G;

var factory = new Inovance60GMotionControllerFactory();
using IMotionController controller = factory.Create(0);

var openResult = controller.Open(new Inovance60GInitOptions
{
    DeviceConfigPath = "device-config.xml",
    SystemConfigPath = "system-config.xml",
    BlockingScan = true,
    ScanTimeoutMs = 30_000,
    InvertEmergencyStopLevel = false,
});

if (!openResult.Success)
    throw new InvalidOperationException(openResult.ErrorMessage);

当前 Open 链路按以下顺序执行:

校验初始化选项类型


检测控制卡数量并校验 cardNumber


获取控制卡句柄

        ├── 可选:下载设备配置

扫描 EtherCAT 从站并等待 OP


读取轴、DI、DO、AI、AO 资源并创建轴对象

        ├── 可选:配置急停触发电平
        └── 可选:下载系统配置

错误分支会写入日志、更新控制器状态并关闭已经取得的句柄。关闭控制器时则反向执行:急停全部轴、全部下使能、释放轴对象、关闭句柄并清空资源。初始化、关闭和关键清理路径由同步锁保护,避免同一控制器被并发打开或释放。

设备配置文件必须与真实 EtherCAT 拓扑一致。示例文件名只是接口演示,不代表任何生产环境路径,也不能直接复制到另一台设备使用。

从机械结构到工程单位

设备业务关心的是毫米、角度和单位每秒,控制器底层经常使用脉冲。把换算散落在每个动作中,容易出现“位置换算了、速度没换算”或插补阶段量纲不一致的问题。

项目使用 AxisMechanicalParameters 描述编码器、电子齿轮和机械传动链。直线轴的每工程单位脉冲数为:

PulsesPerUnit = PulsesPerRevolution × ReductionRatio
                ─────────────────────────────────────
                    GearRatio × LeadScrewPitch

旋转轴则把丝杠螺距替换为一整圈对应的工程单位数:

PulsesPerUnit = PulsesPerRevolution × ReductionRatio
                ─────────────────────────────────────
                    GearRatio × FullRevolutionUnits

AxisConfig.MechanicalParameters 是可选字段。设置后可以自动计算并覆盖 PulsesPerUnit;旧项目不设置时仍保留手工填写值,避免单位模型升级破坏已有配置。驱动内部统一使用:

ToPulse(engineeringValue) = engineeringValue × PulsesPerUnit
ToUnit(pulseValue)        = pulseValue ÷ PulsesPerUnit

60G EtherCAT 轴还结合用户指令单位和编码器分辨率配置电子齿轮参数。坐标系插补开始前会按参考轴当量临时统一量纲,轨迹结束或异常退出后在 finally 中恢复各轴原始单位配置,避免一次插补影响后续单轴运动。

单轴、轴组与流程编排

基础轴接口覆盖设备调试和自动流程最常用的动作:

  • 绝对/相对 PTP、运动中更新目标位置和速度;
  • JOG 启动、在线调速、平滑停止和急停;
  • 多种回原点参数、状态查询和超时等待;
  • 伺服上下使能、报警清除、跟随误差与限位状态;
  • 默认速度、加减速度、软限位、到位窗口和急停减速度配置。

Core 在单轴接口之上提供更接近设备业务的对象:

  • MotionManager 管理多控制器、命名轴、统一开关卡、使能、停止和批量等待;
  • AxisGroup 把若干轴组成逻辑组,支持批量移动、回零和完成等待;
  • TeachPointManager 从当前反馈位置示教点位,并执行多轴点位移动;
  • SoftLimitManager 校验或裁剪命名轴目标位置;
  • MotionSequencer 编排移动、相对移动、多轴移动、延时、等待 DI、设置 DO、自定义动作和回零;
  • AxisMonitorIoMonitor 把轮询结果转换为状态变化事件;
  • Cylinder 把输出与到位输入组合成带超时的执行器语义。

MotionManagerAxisGroup 的“多轴移动”是软件层依次发出多个单轴命令,再统一等待完成,不应被解释为严格同步的轮廓控制。需要确定轨迹关系时,应使用 60G 驱动提供的控制器级 PVT 或插补能力。

60G 高级轨迹与过程 IO

60G 驱动把高级运动建模为不可变请求数据,再由控制器级接口执行。当前包含三条主要链路。

多轴 PVT

PVT 请求包含轴号、各路径点位置、累计时间、超时和路径名。位置仍以工程单位输入,驱动在下发前按各轴当量转换为控制器单位,并逐轴等待运动完成或取消。

多轴直线插补

直线插补请求包含插补组号、轴列表、终点序列、轨迹速度、加减速度、过渡类型和过渡比例。执行阶段会校验组号、轴号、点维度和运动参数;异常或超时时先停止仍在运行的插补组,确认停止后才删除临时组,避免清理动作与硬件运动竞争。

XYZ 坐标系轨迹

坐标系请求固定使用 XYZ 三轴点位,并支持每段单独速度。过程 IO 与轨迹点绑定,可以表达:

  • 第一段开始前立即输出;
  • 到达某个点时立即输出;
  • 到点后按规划时间延时输出;
  • 相对点位按负距离提前输出。

这类 IO 由控制卡缓冲区和轨迹一起执行,避免 Windows 线程定时器承担精确工艺同步。执行结束后还可以等待末点延时输出完成。无论成功、失败、取消还是超时,清理路径都会处理坐标系停止、删除和轴单位恢复,并在无法安全清理时保留诊断信息而不是盲目删除运行资源。

JSON 配置装配

Configuration 层把设备数据从代码中分离出来,覆盖轴组、示教点和运动序列:

JSON files


JsonConfigurationLoader


ConfigurationValidator ──→ errors / warnings
    │ valid

AxisGroupBuilder / TeachPointBuilder / SequenceBuilder


AxisGroup / TeachPointManager / MotionSequencer

验证器可以检查重复名称、空轴组、未知命名轴、点位引用、序列引用、缺失参数和不合理的运动参数。它是显式组件:ConfigurationManager.LoadAsync 负责加载和装配,但不会自动替代调用方的加载前验证。推荐流程是先反序列化、使用带 MotionManager 的验证器检查,再把合法配置交给管理器。

var loader = new JsonConfigurationLoader();
var config = await loader.LoadAsync("motion-config.json", ct);

var validator = new ConfigurationValidator(motionManager);
var validation = validator.Validate(config);
if (!validation.IsValid)
    throw new InvalidOperationException(validation.GetSummary());

var configurationManager = new ConfigurationManager(motionManager, loader);
configurationManager.CreateTeachPointManager();
var loadResult = await configurationManager.LoadAsync("motion-config.json", ct);

同一层也支持把运行中的轴组、点位和序列重新导出为 JSON。配置只保存可迁移的工艺数据,不保存控制卡句柄、实时状态或 Native 指针。

无硬件快速开始

Mock 驱动适合先验证上位机流程、页面交互和配置装配。下面的代码不需要控制卡:

using MotionControl.Abstractions.Interfaces;
using MotionControl.Abstractions.Models;
using MotionControl.Driver.Mock;

using IMotionController controller = new MockMotionController(
    cardNumber: 0,
    axisCount: 4,
    digitalInputCount: 16,
    digitalOutputCount: 16,
    analogInputCount: 2,
    analogOutputCount: 2);

var openResult = controller.Open();
if (!openResult.Success)
    throw new InvalidOperationException(openResult.ErrorMessage);

var xAxis = controller.GetAxis(0);
xAxis.Name = "X";
xAxis.SetMotionParameters(new MotionParameters
{
    Velocity = 100,
    Acceleration = 500,
    Deceleration = 500,
});

if (!xAxis.ServoOn().Success || !xAxis.MoveAbsolute(100).Success)
    throw new InvalidOperationException("X axis command failed");

if (!await xAxis.WaitForMotionCompleteAsync(5_000))
    xAxis.EmergencyStop();

Mock 会模拟加减速、位置变化和 IO 状态;高级方法还会保存最近一次 PVT、直线插补、坐标系准备和坐标系轨迹请求,便于宿主验证参数是否按预期生成。它能够减少“必须等控制柜通电才能开发”的耦合,但当前解决方案没有自动测试项目,因此不能把“存在 Mock”写成“已经有完整测试覆盖”。

错误、安全与可观测性

大多数写操作返回 MotionResult

Success + ErrorCode + ErrorMessage

厂商返回码为零时映射为成功,其他值保留原始十六进制错误码;没有厂商错误码的逻辑失败使用统一失败值。读取位置或 IO 的部分接口在 Native 调用失败时抛出异常,因此上层轮询器和设备宿主必须同时处理结果对象与读取异常,不能只检查布尔返回值。

驱动通过 IMotionLogger 接收日志实现,默认使用不输出的 NullMotionLogger;需要诊断时可注入 NLog 适配器或宿主自己的结构化日志。控制器初始化、配置应用、运动模式切换、插补建组、超时、停止与清理都会记录上下文。

安全与恢复措施主要包括:

  • 控制器生命周期和部分模式切换使用锁,避免并发修改关键状态;
  • 等待接口提供超时与 CancellationToken
  • 多轴启动中途失败时停止已经启动的轴;
  • 插补与坐标系通过 finally 清理临时资源并恢复单位配置;
  • Dispose 先急停和下使能,再释放轴与控制器句柄;
  • 软限位、硬限位、报警、跟随误差与急停状态通过统一轴状态暴露。

这些软件措施不能代替硬件安全回路。生产应用还必须根据具体设备定义急停复位、回零前置条件、门禁、气压和执行器互锁。

真实项目使用结果

当前分支不是为了博客构造的示例库,而是从多个设备项目的重复需求中逐步抽取出来,并继续服务于产线软件:

  • 30G 与 60G 的通用接口让 HMI、状态机和工艺层复用同一套轴与 IO 调用方式;
  • 命名轴、轴组、示教点和序列减少了业务层对卡号与轴号的直接依赖;
  • 工程单位模型把机械换算集中在配置与驱动边界,避免业务动作混用脉冲和毫米;
  • Mock 让部分上位机开发和流程联调可以早于控制柜与现场硬件;
  • 60G 高级轨迹把分段速度和工艺 IO 下沉到控制卡缓冲区,服务于需要轨迹过程同步的设备动作;
  • 强类型初始化和详细日志使扫描、OP 状态、轴配置与清理问题更容易定位到具体阶段。

这里不提供未经统一采集的节拍、延迟、产量或在线时长数据。项目价值以可复用边界和实际设备使用为依据,而不是用无法复核的数字包装成果。

构建验证与已知边界

当前分支的 Release 解决方案构建已经实际执行:全部项目为 0 个编译错误、0 个编译警告。这个结果证明当前源码和依赖能够完成构建,但不能替代自动化测试或实机回归矩阵。

已知边界 当前影响 改进方向
没有单元测试或集成测试项目 回归主要依赖项目联调与人工验证 用 Mock 建立契约测试、单位换算和配置装配测试
轴与 IO 监控的部分轮询异常被吞掉 单个采样失败不会打断循环,但可能降低故障可见性 增加错误事件、节流日志和连续失败状态
配置校验需要调用方显式执行 跳过验证仍可能进入部分装配流程 提供 validate-then-load 原子入口
30G 与 60G 能力不完全对称 上层不能假设所有驱动都有高级轨迹 保留能力接口检查,并维护清晰能力矩阵
真实驱动依赖 Windows x64 与厂商运行库 不能像纯托管库一样跨平台复制 增加启动时依赖诊断和部署检查清单
依赖与发布尚未形成统一包版本 多项目引用时升级边界不够清晰 统一依赖版本,建立语义化版本和包发布流程

后续路线

下一阶段的重点不是继续扩充 API 数量,而是把现有产线经验转成可重复验证:

  1. MotionResult、机械单位换算、配置验证和 Builder 增加确定性单元测试;
  2. 以 Mock 对三个高级能力接口建立共享契约测试;
  3. 建立 30G/60G 硬件在环用例,覆盖开卡、回零、限位、急停、取消、超时和异常清理;
  4. 为监控器增加可观察的采样错误与连续失败策略,不再静默忽略全部异常;
  5. 将配置验证和装配组合成不产生半成品状态的入口;
  6. 统一日志、Native 依赖检查、版本号和发布产物。

项目复盘

运动控制抽象最难的部分不是把方法名翻译成 C#,而是决定哪些差异应该被隐藏、哪些能力必须显式保留。把所有控制器压成一个巨大接口,会让不支持的功能以运行时异常暴露;完全不抽象,又会让每个上位机重复处理句柄、错误码、单位和状态。

这个项目最终采用“稳定基础契约 + 可选高级能力 + 独立驱动 + 通用编排”的结构。它既允许业务层复用,又不掩盖 30G 与 60G 的真实能力差异。多个产线项目的使用证明了这条边界具备实际价值,而当前缺少自动化测试和硬件回归体系,也说明下一步应当把现场验证沉淀为持续可执行的工程证据。