运动控制卡开发
大约 8 分钟约 2280 字
运动控制卡开发
简介
运动控制卡是工业自动化的核心组件,用于精确控制电机运动。C# 通过厂商提供的 SDK 或通用运动控制库实现点位运动、直线插补、圆弧插补等功能。广泛应用于 CNC 数控、机器人控制、自动贴片机等场景。
特点
基本概念
运动控制术语
| 术语 | 说明 |
|---|---|
| 脉冲当量 | 一个脉冲对应的移动距离 |
| 伺服周期 | 控制卡发出指令的时间间隔 |
| 插补 | 多轴协调运动计算 |
| 原点回归 | 机械设备寻找参考点的过程 |
| 软限位 | 软件设置的移动范围限制 |
| 硬限位 | 物理开关的移动范围限制 |
通用运动控制接口
/// <summary>
/// 通用运动控制接口 — 抽象各厂商 SDK
/// </summary>
public interface IMotionController : IDisposable
{
// 连接/断开
bool Connect(int cardId = 0);
void Disconnect();
bool IsConnected { get; }
// 轴控制
void EnableAxis(int axis);
void DisableAxis(int axis);
bool IsAxisEnabled(int axis);
// 位置控制
void SetPosition(int axis, double position);
double GetPosition(int axis);
void ResetPosition(int axis);
// 速度设置
void SetSpeed(int axis, double speed, double acceleration, double deceleration);
// 运动命令
void MoveAbsolute(int axis, double position); // 绝对运动
void MoveRelative(int axis, double distance); // 相对运动
void MoveVelocity(int axis, double speed); // 连续运动
void StopAxis(int axis); // 停止
void EmergencyStop(); // 急停
// 状态查询
bool IsMoving(int axis);
bool IsInPosition(int axis);
bool IsLimitPositive(int axis);
bool IsLimitNegative(int axis);
// 原点回归
void HomeAxis(int axis, HomeMode mode);
bool IsHomed(int axis);
// 多轴插补
void MoveLinear(double[] positions, double speed); // 直线插补
void MoveArc(double[] center, double[] end, double speed); // 圆弧插补
// IO 操作
bool ReadInput(int port);
void WriteOutput(int port, bool value);
// 事件
event Action<int, MotionStatus>? StatusChanged;
}
public enum HomeMode
{
LimitSwitch, // 限位开关回原点
ZeroSignal, // 零点信号回原点
EncoderZ, // 编码器 Z 相回原点
Current // 当前位置设为原点
}
public enum MotionStatus
{
Idle,
Moving,
InPosition,
Error,
LimitTriggered
}虚拟运动控制(模拟)
开发调试用模拟器
/// <summary>
/// 虚拟运动控制器 — 开发调试用
/// </summary>
public class SimulatedMotionController : IMotionController
{
private readonly Dictionary<int, AxisState> _axes = new();
private readonly Dictionary<int, bool> _inputs = new();
private readonly Dictionary<int, bool> _outputs = new();
private readonly List<Task> _motionTasks = new();
private CancellationTokenSource _cts = new();
public bool IsConnected { get; private set; }
public event Action<int, MotionStatus>? StatusChanged;
public SimulatedMotionController()
{
// 初始化 4 个轴
for (int i = 0; i < 4; i++)
{
_axes[i] = new AxisState
{
Enabled = false,
Position = 0,
TargetPosition = 0,
Speed = 100,
Acceleration = 1000,
Deceleration = 1000,
IsMoving = false,
IsHomed = false
};
}
}
public bool Connect(int cardId = 0)
{
IsConnected = true;
return true;
}
public void Disconnect()
{
EmergencyStop();
IsConnected = false;
}
public void EnableAxis(int axis)
{
if (_axes.TryGetValue(axis, out var state))
state.Enabled = true;
}
public void DisableAxis(int axis)
{
if (_axes.TryGetValue(axis, out var state))
state.Enabled = false;
}
public bool IsAxisEnabled(int axis) => _axes.GetValueOrDefault(axis)?.Enabled ?? false;
public void SetPosition(int axis, double position)
{
if (_axes.TryGetValue(axis, out var state))
state.Position = position;
}
public double GetPosition(int axis) => _axes.GetValueOrDefault(axis)?.Position ?? 0;
public void ResetPosition(int axis) => SetPosition(axis, 0);
public void SetSpeed(int axis, double speed, double acceleration, double deceleration)
{
if (_axes.TryGetValue(axis, out var state))
{
state.Speed = speed;
state.Acceleration = acceleration;
state.Deceleration = deceleration;
}
}
public void MoveAbsolute(int axis, double position)
{
if (!_axes.TryGetValue(axis, out var state) || !state.Enabled) return;
state.TargetPosition = position;
SimulateMotion(axis);
}
public void MoveRelative(int axis, double distance)
{
if (!_axes.TryGetValue(axis, out var state)) return;
MoveAbsolute(axis, state.Position + distance);
}
public void MoveVelocity(int axis, double speed)
{
// 连续运动直到停止
}
public void StopAxis(int axis)
{
if (_axes.TryGetValue(axis, out var state))
state.IsMoving = false;
}
public void EmergencyStop()
{
_cts.Cancel();
foreach (var axis in _axes.Values)
axis.IsMoving = false;
}
public bool IsMoving(int axis) => _axes.GetValueOrDefault(axis)?.IsMoving ?? false;
public bool IsInPosition(int axis) => !IsMoving(axis);
public bool IsLimitPositive(int axis) => false;
public bool IsLimitNegative(int axis) => false;
public void HomeAxis(int axis, HomeMode mode)
{
if (_axes.TryGetValue(axis, out var state))
{
state.Position = 0;
state.IsHomed = true;
state.IsMoving = false;
}
}
public bool IsHomed(int axis) => _axes.GetValueOrDefault(axis)?.IsHomed ?? false;
public void MoveLinear(double[] positions, double speed)
{
// 模拟多轴直线插补
for (int i = 0; i < positions.Length && i < _axes.Count; i++)
{
MoveAbsolute(i, positions[i]);
}
}
public void MoveArc(double[] center, double[] end, double speed)
{
// 模拟圆弧插补
}
public bool ReadInput(int port) => _inputs.GetValueOrDefault(port);
public void WriteOutput(int port, bool value) => _outputs[port] = value;
private void SimulateMotion(int axis)
{
var state = _axes[axis];
state.IsMoving = true;
StatusChanged?.Invoke(axis, MotionStatus.Moving);
_ = Task.Run(async () =>
{
var start = state.Position;
var target = state.TargetPosition;
var steps = (int)(Math.Abs(target - start) / state.Speed * 100);
for (int i = 0; i <= steps; i++)
{
if (!state.IsMoving) break;
state.Position = start + (target - start) * i / steps;
await Task.Delay(10);
}
state.Position = target;
state.IsMoving = false;
StatusChanged?.Invoke(axis, MotionStatus.InPosition);
}, _cts.Token);
}
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
private class AxisState
{
public bool Enabled { get; set; }
public double Position { get; set; }
public double TargetPosition { get; set; }
public double Speed { get; set; }
public double Acceleration { get; set; }
public double Deceleration { get; set; }
public bool IsMoving { get; set; }
public bool IsHomed { get; set; }
}
}WPF 运动控制界面
运动控制 ViewModel
/// <summary>
/// 运动控制 ViewModel
/// </summary>
public class MotionControlViewModel : ObservableObject, IDisposable
{
private readonly IMotionController _controller;
// X 轴位置
private double _xPosition;
public double XPosition
{
get => _xPosition;
set => SetProperty(ref _xPosition, value);
}
// Y 轴位置
private double _yPosition
{
get => _yPosition;
set => SetProperty(ref _yPosition, value);
}
private double _yPosition;
// 目标位置
private double _targetX;
public double TargetX { get => _targetX; set => SetProperty(ref _targetX, value); }
private double _targetY;
public double TargetY { get => _targetY; set => SetProperty(ref _targetY, value); }
// 运动速度
private double _speed = 100;
public double Speed { get => _speed; set => SetProperty(ref _speed, value); }
// 连接状态
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
// 运动状态
private bool _isMoving;
public bool IsMoving
{
get => _isMoving;
set => SetProperty(ref _isMoving, value);
}
public ICommand ConnectCommand { get; }
public ICommand DisconnectCommand { get; }
public ICommand HomeAllCommand { get; }
public ICommand MoveAbsoluteCommand { get; }
public ICommand StopCommand { get; }
public ICommand EmergencyStopCommand { get; }
private DispatcherTimer _positionTimer;
public MotionControlViewModel(IMotionController controller)
{
_controller = controller;
ConnectCommand = new RelayCommand(Connect);
DisconnectCommand = new RelayCommand(Disconnect);
HomeAllCommand = new RelayCommand(HomeAll);
MoveAbsoluteCommand = new RelayCommand(() => MoveTo(TargetX, TargetY));
StopCommand = new RelayCommand(Stop);
EmergencyStopCommand = new RelayCommand(EmergencyStop);
// 定时刷新位置
_positionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
_positionTimer.Tick += UpdatePositions;
}
private void Connect()
{
IsConnected = _controller.Connect();
if (IsConnected)
{
_controller.EnableAxis(0); // X
_controller.EnableAxis(1); // Y
_controller.SetSpeed(0, Speed, 1000, 1000);
_controller.SetSpeed(1, Speed, 1000, 1000);
_positionTimer.Start();
}
}
private void Disconnect()
{
_positionTimer.Stop();
_controller.Disconnect();
IsConnected = false;
}
private void HomeAll()
{
_controller.HomeAxis(0, HomeMode.Current);
_controller.HomeAxis(1, HomeMode.Current);
}
private void MoveTo(double x, double y)
{
_controller.MoveLinear(new[] { x, y }, Speed);
}
private void Stop()
{
_controller.StopAxis(0);
_controller.StopAxis(1);
}
private void EmergencyStop()
{
_controller.EmergencyStop();
}
private void UpdatePositions(object? sender, EventArgs e)
{
XPosition = _controller.GetPosition(0);
YPosition = _controller.GetPosition(1);
IsMoving = _controller.IsMoving(0) || _controller.IsMoving(1);
}
public void Dispose()
{
_positionTimer.Stop();
_controller.Dispose();
}
}常见运动控制卡
| 品牌 | SDK | 特点 |
|---|---|---|
| 雷赛 | DMC3000 | 国产,性价比高 |
| 固高 | GTS | 国产,多轴联动 |
| 正运动 | ZMotion | 国产,EtherCAT |
| 凌华 | MotionNet | 台系,分布式 |
| Beckhoff | TwinCAT | 德系,EtherCAT |
优点
缺点
总结
运动控制卡开发的核心是抽象统一接口,隔离厂商差异。开发阶段用模拟器调试逻辑,生产环境切换实际 SDK。务必实现急停、限位保护和原点回归。核心掌握单轴运动、直线插补和状态监控。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
适用场景
- 当你准备把《运动控制卡开发》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《运动控制卡开发》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《运动控制卡开发》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《运动控制卡开发》最大的收益和代价分别是什么?
