简介:面向C#/.NET开发者与自动化领域工程师的固高运动控制器WinForm源码工程,适合需要学习运动控制上位机开发、了解固高设备编程及WinForm工业软件设计的读者。压缩包共38个文件,体积约284KB,包含11个C#源码文件、VS解决方案与工程文件、配置文件、依赖DLL及可执行程序,可完整还原项目结构。目前已有485人学习浏览。源码覆盖设备初始化、控制指令收发、直线/圆弧插补运动规划、异常处理及UI交互等关键模块,并演示通过DllImport调用固高API、线程同步与异步通信等实用技巧;工程从程序入口到主窗体、控制卡封装类层次清晰,既有界面操作也有底层硬件交互,便于按模块研读,读者可据此掌握运动控制卡初始化流程、参数配置与状态反馈处理逻辑,可作为运动控制入门与二次开发的实践参考。
1. 从固高控制卡到 C# WinForm 上位机,这份源码包到底在讲什么
一个以 .rar 结尾的固高源码包,大概率是某台设备的完整上位机工程:C# 写 WinForm 界面,后端调用固高 GTS 系列运动控制卡的 gts.dll,在卡内完成规划、闭环和 IO 输出。这类项目真正难的不是让电机转,而是把 DLL 互操作、界面刷新、回零顺序和异常处理组织成一套不互相拖累的骨架。这篇内容按固高二次开发的推进顺序展开:先看懂 API 组织,再搭 WinForm 工程,接着调速度与回零参数,最后说产线调试里典型的数据类型和时序坑。代码片段不依赖某个具体源码包版本,适合刚接手固高卡、需要 C# 上位机参照的工程师。
2. 固高运动控制卡的开发模型:从 gts.dll 到 C# 互操作
2.1 固高 GTS 系列控制卡的函数库与调用约定
固高的 GTS 系列是插在 PCI/PCIe 槽上的运动控制卡,常见有四轴和八轴版本。卡上 DSP 负责插补、闭环和 IO 扫描,上位机只在需要时调用动态库下发命令。驱动装好后,C# 工程用 P/Invoke 加载 gts.dll。不同批次卡可能提供 C 接口或 C++ 接口,调用约定默认按 stdcall 处理,但最好以开发包里的 .h 头文件为准,不要只依赖网上流传的旧版声明。
下表是二次开发里一定会用到的函数,更完整的内容在固高开发手册附录里:
| 函数 | 作用 | 参数注意 |
|---|---|---|
| GT_Open | 建立上位机与卡会话 | 进程内唯一 |
| GT_Close | 释放资源 | 程序退出前必须调用 |
| GT_Reset | 复位卡内运动状态 | 复位后延时等待 |
| GT_ClrSts | 清除轴报警和限位状态 | 硬件限位持续触发时清不掉 |
| GT_AxisOn | 伺服使能 | 对应驱动器 Servo On |
| GT_PrfTrap | 进入点位运动模式 | 之后才能调用 Move 系列 |
| GT_SetTrapPrm | 下发梯形规划参数 | 单位是脉冲和毫秒 |
| GT_MoveAbs | 绝对位置运动 | 传入位置为 double |
| GT_GetSts | 读取轴状态字 | out 参数是 uint |
| GT_GetDiBit | 读取输入 IO | 视觉/PLC 握手用 |
函数命名有规律:前半段是对象类型,比如 Prf 是 profile(规划),Trap 是梯形;后半段是操作。理解这个命名,遇到陌生函数时不容易慌。调用流程上必须遵守“先开卡、再复位、清报警、使能轴、最后运动”的顺序。若初始化代码放在窗体构造函数里,窗体还没显示就抛异常,排查难度会成倍增加,因此我习惯把初始化放到单独的 Init() 方法,由启动画面调用。
2.2 在 C# 工程里用 DllImport 封装固高 API
我一般会单独建一个 GtsApi.cs,不把 DllImport 直接写在窗体代码里。这样既方便替换成仿真实现,也方便统一处理错误码。封装类做成静态类,所有对外方法返回 short 错误码,由上层业务决定如何提示。
// GtsApi.cs using System.Runtime.InteropServices; namespace MotionDemo.Device { public static class GtsApi { [DllImport("gts.dll")] public static extern short GT_Open(); [DllImport("gts.dll")] public static extern short GT_Close(); [DllImport("gts.dll")] public static extern short GT_Reset(); [DllImport("gts.dll")] public static extern short GT_ClrSts(short axis); [DllImport("gts.dll")] public static extern short GT_AxisOn(short axis); [DllImport("gts.dll")] public static extern short GT_AxisOff(short axis); [DllImport("gts.dll")] public static extern short GT_GetSts(short axis, out uint sts); } }把 API 孤立出来后,有条件时可以做一套 Mock 实现,在没有硬件的电脑上调试界面逻辑。固高手册里还有一个容易被忽略的要求:不能在运动过程中重复切换规划模式。例如轴已经在点位运动里,再次调 GT_PrfTrap 会返回错误,所以上层封装里应先检查轴状态再切模式,而不是让 UI 直接面对 DllImport。
错误检查统一收口成下面这样:
public static short Check(short rtn, string action) { if (rtn != 0) { Log.Error($"固高调用失败: {action}, 返回 {rtn}"); throw new InvalidOperationException($"固高指令失败 {action}: {rtn}"); } return rtn; }返回值里 0 是成功,非 0 是错误码。错误码表每个版本有差异,尤其遇到 0x8001 这类带位标志的返回值,不要想当然按旧版手册解释,要打开当前版本的头文件去查。
2.3 卡初始化、轴使能与“开卡失败”的排查顺序
初始化代码我按下面的顺序写,这段也是源码包里最该先看的函数:
public void Init() { Check(GtsApi.GT_Open(), "GT_Open"); Check(GtsApi.GT_Reset(), "GT_Reset"); Thread.Sleep(100); Check(GtsApi.GT_ClrSts(1), "GT_ClrSts(1)"); Check(GtsApi.GT_ClrSts(2), "GT_ClrSts(2)"); Check(GtsApi.GT_AxisOn(1), "GT_AxisOn(1)"); Check(GtsApi.GT_AxisOn(2), "GT_AxisOn(2)"); }GT_Reset 后必须留延时,让卡完成内部状态机切换。GT_ClrSts 是针对每个轴的,不是全局命令,所以轴数一多要用循环。GT_AxisOn 之后还要确认驱动器是否处于 Servo On 状态,很多驱动器在未收到使能时会报硬报警。
现场“开卡失败”的五个常见原因,按排查顺序列一下:
- 设备管理器里没有卡,检查插槽、驱动和 BIOS 里的 PCIe 设置。
- gts.dll 位数不对,64 位系统必须放 64 位版本,否则报 DllNotFoundException。
- 杀毒软件把驱动或 DLL 隔离了。
- 上一次程序崩溃没有正常 Close,卡资源被占用,需要重启或重插。
- 权限不足,以普通用户跑时无法打开内核驱动。
提示:初始化失败不要直接弹窗阻塞界面,把错误码和时间写日志,界面只显示“初始化失败,查看 log”。现场调试时日志比弹窗有用得多。
3. 用 WinForm 搭运动控制界面的项目结构与交互线程
3.1 上位机的基本模块划分:从源码包的目录反推功能
拿到一个 winform 项目案例压缩包,先看目录结构而不是先看 Form1 设计器。成熟的上位机至少分三层:界面层、运动逻辑层、DLL 封装层。这样分层的好处是界面按钮只调 MotionService 的方法,不直接碰 gts.dll,以后换卡型号时只动封装层。
| 文件/目录 | 职责 | 关键点 |
|---|---|---|
| Forms 目录 | 主窗体、参数窗体 | 只做界面显示 |
| Device/GtsApi.cs | 固高 API 封装 | DllImport 与错误码 |
| Device/MotionService.cs | 运动指令、轴状态 | 对外暴露业务方法 |
| Config/*.ini | 轴参数、IO 映射 | 现场可改 |
| Utils/LogHelper.cs | 日志 | 排障依赖 |
MotionService 是核心,它拿到界面传来的毫米坐标,转换成脉冲后调用固高 API。视觉对接和 PLC 对接也只认 MotionService,不认按钮。以后再接 MES 或扫码枪,改的是 UI 层,运动逻辑不动。
WinForm 界面美化则是另一层话题。运动控制主界面不需要花哨,但信息密度要大:当前位置、目标位置、速度、IO 状态、报警区最好一屏放完。常见做法是用 TableLayoutPanel 做栅格布局,限位和报警指示灯用自绘 UserControl,而不是堆一堆 Label。
3.2 用 Timer 或后台线程轮询轴状态,避免阻塞界面
轮询频率关系到界面响应,类似运动控制与追踪之间的平衡:频率太高浪费 CPU,Invoke 排队反而卡;太低界面滞后明显。WinForm 自带 Timer 的 Interval 设置在 50~100ms 足够。由于 Timer 运行在 UI 线程,不存在跨线程访问控件的问题,是简单又不容易出错的方案。
private void timerStatus_Tick(object sender, EventArgs e) { if (!_motion.IsOpen) return; uint sts1; if (GtsApi.GT_GetSts(1, out sts1) != 0) return; lblAxis1.Text = AxisState.ToText(sts1); lblAxis1.ForeColor = AxisState.IsReady(sts1) ? Color.Green : Color.Red; }AxisState.ToText 用位运算解析状态字:
public static string ToText(uint sts) { StringBuilder sb = new StringBuilder(); if ((sts & 0x0001) != 0) sb.Append("运动中 "); if ((sts & 0x0010) != 0) sb.Append("正限位 "); if ((sts & 0x0020) != 0) sb.Append("负限位 "); if ((sts & 0x0100) != 0) sb.Append("报警 "); return sb.Length == 0 ? "空闲" : sb.ToString(); }状态位 0x0010、0x0020 在固高手册里对应当前轴的硬件限位输入,具体位号必须和驱动器接线图核对,不要只看例程。如果换成后台线程轮询,就必须用 Control.BeginInvoke 把文本更新封送回 UI 线程,否则随机抛跨线程异常。
3.3 点位运动、连续运动与回零的 UI 操作封装
MotionService 向外提供 MoveAbs、MoveVel、Stop、Home 四个方法,界面按钮只做绑定。
public void MoveAbs(short axis, double posMm, bool waitDone = false) { long pulse = (long)Math.Round(MmToPulse(posMm)); GtsApi.Check(GtsApi.GT_PrfTrap(axis), "启动点位模式"); GtsApi.Check(GtsApi.GT_SetTrapPrm(axis, ref _trap), "下发参数"); GtsApi.Check(GtsApi.GT_MoveAbs(axis, pulse), "绝对运动"); if (waitDone) { uint sts = 0; do { GtsApi.GT_GetSts(axis, out sts); Thread.Sleep(5); } while ((sts & 0x0001) != 0); } }先调 GT_PrfTrap 让轴进入点位规划模式,再设置梯形参数,最后下发目标位置。waitDone 只在自动流程里用,手动按钮不应该阻塞 UI 线程,否则界面在运动期间失去响应。连续运动改速度模式:
public void MoveVel(short axis, double speedMmPerSec) { double velPulse = speedMmPerSec / _pulsePerMm * 1000.0; GtsApi.Check(GtsApi.GT_PrfVel(axis), "速度模式"); GtsApi.Check(GtsApi.GT_SetVel(axis, velPulse), "设定速度"); }速度模式下轴会一直动,UI 上必须有停止按钮,停止要调 GT_Stop(axis, 1) 而不是关使能。GT_Stop 的第二个参数 0 表示立即停,1 表示按减速停;立即停对机械冲击大,正常流程用 1。
4. 固高源码里的运动控制参数:速度、加速度与回零逻辑
4.1 脉冲当量与电子齿轮比在 C# 端的换算
固高卡的加减速和位置单位全部是脉冲,现场习惯用毫米或度,所以必须做单位换算。脉冲当量的计算由导程、编码器分辨率和驱动器电子齿轮比共同决定。
| 物理量 | 计算方式 | 例:导程 5mm、分辨率 10000、齿轮比 2 |
|---|---|---|
| 电机每转脉冲数 | 编码器分辨率 / 电子齿轮比 | 5000 pulse/rev |
| 每脉冲移动量 | 导程 / 电机每转脉冲数 | 0.001 mm/pulse |
| 每毫米脉冲数 | 1 / 每脉冲移动量 | 1000 pulse/mm |
这些值放到配置里,不要写死在 C# 代码中:
[Axis1] PulsePerMm=1000 VelMmPerSec=50 AccMmPerSec2=200 HomeVelMmPerSec=2C# 端加载后转成固高参数。注意固高 TrapPrm 里的 dVel 单位通常是 pulse/ms,每秒 50mm 换算成每秒 50000 脉冲,再除以 1000 得到 50.0。不少人在这里少除一次,结果电机跑得飞快。
4.2 梯形与 S 型速度规划的参数选取
点位运动用到 TAG_TTrapPrm,里面有 dAcc、dDec、dVel。第一次配置时我用一组保守值:
| 参数 | 值 | 说明 |
|---|---|---|
| dVel | 20.0 | 20000 pulse/s,适合中小负载 |
| dAcc | 100.0 | 100000 pulse/s²,约 0.2s 到目标速度 |
| dDec | 100.0 | 与加速一致 |
如果末端有撞击声,说明减速度太大;如果电机嗡嗡响但不动,可能是加速度太大造成驱动器过流。固高同时支持 S 型速度规划,入口是另一组 Prf 函数,效果是减小 jerk,也就是加加速度突变。垂直轴建议用 S 型,防止负载在加减速切换时晃动。
4.3 回零与限位信号的状态字判断
回零前必须明确原点定义:是用正限位、负限位还是 Z 相。固高回零函数 GT_Home 配合回零参数结构体使用。一个实用的流程是先以低速找限位,离开限位后找 Z 相:
TAG_THomePrm home = new TAG_THomePrm(); home.dVel = 2.0; // 搜索速度 2000 pulse/s home.dHomeVel = 0.5; // 找 Index 的速度 500 pulse/s home.dIndexAcc = 2.0; home.IndexCount = 1; // 第 1 个 Z 相 home.HomeMode = 1; // 模式 1:先找限位再找 Z 相 GtsApi.Check(GtsApi.GT_Home(axis, ref home), "启动回零");回零是否完成不能只看电机停没停,要看 GT_GetHomeResult 的返回值。很多源码里只在按钮事件里调用一次 GT_Home,然后立刻显示“回零完成”,这在限位未触发的场合会出错。正确做法是类似状态轮询的 while 循环,判断完成位后再更新界面。
5. 从源码到产线:固高运动控制程序的调试与验证技巧
5.1 用卡自带示波器与 I/O 观察运动输出时序
固高调试软件自带示波器,能同时看规划位置、反馈位置和跟随误差。C# 上位机里出现“定位不准”时,先看这三条曲线再判断:
| 现象 | 可能原因 | 优先处理 |
|---|---|---|
| 反馈位置跟不上规划 | 速度/加速度过大 | 降低 dVel |
| 跟随误差周期性摆动 | 增益或反向间隙 | 查机械和驱动器 |
| 两条曲线相差固定值 | 脉冲当量错 | 核对 PulsePerMm |
| 电机不动但 IO 有脉冲 | 使能或驱动器报警 | 看报警码 |
如果系统里还接了视觉,运动完成信号与视觉触发信号的时序要用 IO 抓到逻辑分析仪。固高卡上的通用输出点用 GT_SetDoBit 控制,触发宽度要大于 PLC 或视觉的最小保持时间,通常设在 50ms 以上。
5.2 C# 侧常见数据类型坑与指令超时处理
固高接口大量使用 short、uint、int,C# 里最容易出错的是 out 参数类型不匹配。GT_GetSts 的 out sts 必须声明为 uint;写成 int 后第 31 位会被当符号位,位掩码全部判断失败。
另一个坑是浮点位置的取整。GT_MoveAbs 参数是 double,但底层可能转成 32 位整数,超过 2 的 31 次方的脉冲位置会溢出。大行程设备要提前算最大脉冲数,界面值用毫米坐标记录,只在调 API 时转脉冲。
指令超时建议封装重试。现场曾出现“上电后第一次运动失败,第二次成功”的现象,原因是驱动器 Servo Ready 信号还没就绪,固高发了运动指令但输出被硬件禁止。可靠做法是先读驱动器 Ready 输入 IO,再发运动命令。
5.3 用 IO 中断代替轮询:运动控制与视觉握手的一种替代方案
产线里运动到位和视觉拍照是典型的握手时序:运动到位后触发相机,相机完成后发给固高一个输入信号。普遍做法是上位机用 GT_GetDiBit 轮询输入点,但轮询周期 10ms 时,2ms 的短脉冲可能被漏掉。更可靠的是用输入 IO 变化回调机制,C# 端注册回调:
GtsApi.GT_SetDiInt(ioIndex, isrCallback);回调里不能执行 UI 操作,只设置一个 volatile 标志位或塞入并发队列,UI 线程在 Timer 里取队列处理。信号处理做到“中断置位、UI 消费”,能避免界面卡死和漏信号两个极端。视觉软件那边配合输出 50ms 以上的握手脉宽,比单纯调高轮询频率更彻底。
本文还有配套的精品资源,点击获取