项目概览
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 批量发令与控制卡硬件插补概念混在一起,容易产生错误的同步假设。
这个项目的目标不是隐藏所有硬件差异,而是把差异放在正确的边界内:通用能力通过统一接口使用,厂商或型号独有的能力通过可选接口显式暴露,上层只有在确实需要高级轨迹时才依赖对应能力。
设计目标与边界
项目遵循几条明确原则:
- 依赖倒置:Core 只依赖公共契约,不引用任何汇川 Native 类型;
- 能力显式:基础控制与 PVT、插补、坐标系轨迹使用不同接口表达;
- 工程单位优先:应用层使用毫米、度和单位每秒,驱动层负责转换为脉冲;
- 失败可追踪:保留厂商错误码,同时提供统一的
MotionResult与日志入口; - 无硬件可开发:Mock 与真实驱动共享接口,并模拟运动、IO 和高级请求;
- 配置只负责装配:JSON 可以描述轴组、点位和序列,但真正执行仍属于 Core;
- 安全退出:关闭和释放时先停止运动、下使能,再清理轴与控制卡句柄。
它不是 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、自定义动作和回零;AxisMonitor与IoMonitor把轮询结果转换为状态变化事件;Cylinder把输出与到位输入组合成带超时的执行器语义。
MotionManager 或 AxisGroup 的“多轴移动”是软件层依次发出多个单轴命令,再统一等待完成,不应被解释为严格同步的轮廓控制。需要确定轨迹关系时,应使用 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 数量,而是把现有产线经验转成可重复验证:
- 为
MotionResult、机械单位换算、配置验证和 Builder 增加确定性单元测试; - 以 Mock 对三个高级能力接口建立共享契约测试;
- 建立 30G/60G 硬件在环用例,覆盖开卡、回零、限位、急停、取消、超时和异常清理;
- 为监控器增加可观察的采样错误与连续失败策略,不再静默忽略全部异常;
- 将配置验证和装配组合成不产生半成品状态的入口;
- 统一日志、Native 依赖检查、版本号和发布产物。
项目复盘
运动控制抽象最难的部分不是把方法名翻译成 C#,而是决定哪些差异应该被隐藏、哪些能力必须显式保留。把所有控制器压成一个巨大接口,会让不支持的功能以运行时异常暴露;完全不抽象,又会让每个上位机重复处理句柄、错误码、单位和状态。
这个项目最终采用“稳定基础契约 + 可选高级能力 + 独立驱动 + 通用编排”的结构。它既允许业务层复用,又不掩盖 30G 与 60G 的真实能力差异。多个产线项目的使用证明了这条边界具备实际价值,而当前缺少自动化测试和硬件回归体系,也说明下一步应当把现场验证沉淀为持续可执行的工程证据。