拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

发那科机器人C#二次开发:FOCAS2点位读写实战指南

发那科机器人C#二次开发:FOCAS2点位读写实战指南

简介:本资源是一套面向工业自动化工程师与机器人开发者的发那科(FANUC)机器人C#二次开发实践项目,聚焦于通过C#调用官方SDK实现机器人数据读取、写入及点位信息获取等核心功能,适用于汽车制造、电子装配等场景下的轨迹控制与系统集成需求。压缩包共129个文件,含99个DLL动态库(提供底层通信与API封装)、8个CS源码文件(含主窗体Form1.Designer.cs及SDK核心交互逻辑)、2个EXE可执行程序(用于快速验证连接与数据交互),以及配置文件、缓存与调试符号等配套资源,整体体积仅1.44MB,结构紧凑、开箱即用。已有513人学习下载,资源包含完整VS解决方案(.sln/.csproj),支持直接编译运行,附带详尽的配置缓存与设计时引用文件,便于理解SDK集成机制与异常处理流程,是入门发那科二次开发并掌握实时点位监控与指令下发的实用参考范例。

1. 发那科机器人二次开发 C#读取和写入数据:为什么点位信息必须用原生协议而不是“通用OPC”硬扛?

你手头有一台发那科 R-30iB 或 R-30iB Plus 控制柜,产线需要实时获取当前 TCP 坐标、关节角度、用户坐标系偏移量,甚至要动态修改 PR 程序点(Position Register)或 SR(System Register)里的数值——但你刚在 Visual Studio 里配好 OPC UA 客户端,连上发那科的 OPC UA Server,却发现:PR[1] 的 X/Y/Z 值始终是 0,Joint[1] 读出来全是 NaN,写入操作直接报错BadNotWritable。这不是你的代码问题,而是发那科对 OPC UA 的支持有明确边界:它只开放了极少数系统状态变量(如 Alarm Status、Servo On),所有运动控制层的核心数据——点位信息(PR/SR/JOINT/TOOL FRAME)、程序执行状态、I/O 映射表——全部被锁死在 FOCAS2 协议栈内。C# 二次开发绕不开 FOCAS2,而 FOCAS2 不是 REST API,不是 WebSocket,它是一套基于 TCP Socket 的二进制私有协议,带校验、分帧、会话管理、权限校验。本文不讲“怎么装驱动”,不讲“怎么开 OPC”,就聚焦一件事:用 C# 原生调用 FOCAS2 DLL,在 Windows 上稳定读写发那科机器人内部点位数据,包括 PR、JR、SR、UF、UT 等全部 7 类坐标系与寄存器类型,且能处理断线重连、数据缓存、多线程并发访问等真实产线场景。适合已拿到发那科授权 SDK(FOCAS2 for Windows)、熟悉 .NET Framework 4.7.2+ 的自动化工程师,也适合正在为 MES 系统对接发那科设备卡在“读不到实际点位”环节的上位机开发者。


2. FOCAS2 协议本质与 C# 封装选型:为什么不用 P/Invoke 手写结构体,而选官方 DLL + 自研 Wrapper?

2.1 FOCAS2 不是“协议文档”,而是一套带状态机的二进制通信引擎

发那科官方从不公开 FOCAS2 的完整协议帧格式(Header + Body + CRC),只提供cncall32.dll(32 位)和cncall64.dll(64 位)两个封装好的 Win32 DLL。它内部做了三件事:

  • 连接管理:TCP 连接建立后,自动发送0x00 0x00 0x00 0x00心跳包(默认 5 秒间隔),超时 3 次即断连;
  • 命令路由:所有请求通过cnc_rdcncdata()/cnc_wrcncdata()等函数提交,参数no(数据号)决定读写对象(如1000= PR[1] 的 X 值,2000= JR[1] 的 J1 角度);
  • 数据序列化:返回值不是 JSON 或 XML,而是short[]或int[]数组,需按发那科《FOCAS2 Function Reference》手册中定义的“数据长度 + 字节序 + 编码规则”手动解析(例如 PR 坐标是float类型,占 4 字节,小端序,需BitConverter.ToSingle()转换)。

提示:不要试图用 Wireshark 抓包逆向 FOCAS2——发那科在 TCP 层加了混淆(非标准 TCP Option),且关键字段加密,抓到的全是乱码。官方 DLL 是唯一合法入口。

2.2 为什么放弃纯 P/Invoke 手写结构体?血泪经验告诉你三个硬伤

曾有人用StructLayout(LayoutKind.Sequential)定义FocasData结构体,再Marshal.PtrToStructure()解析返回内存,结果在 R-30iB Plus 上频繁崩溃。原因有三:

  • 内存对齐陷阱:FOCAS2 返回的short[]实际是short*指针,Marshal.Copy()时若未指定length,会越界读取导致 Access Violation(C0000005);
  • 指针生命周期失控:cnc_rdcncdata()返回的是 DLL 内部缓冲区地址,C# GC 不知道该地址归属,可能在Marshal.PtrToStructure()前就回收了内存;
  • 多线程竞争:FOCAS2 DLL 本身不是线程安全的,多个线程同时调用cnc_rdcncdata()会导致返回数据错乱(比如读 PR[1] 却返回了 PR[2] 的值)。

我一般会直接使用官方 DLL 的导出函数,但绝不裸调——必须封装一层线程安全的 Wrapper 类,并强制单例模式管理连接句柄。

2.3 封装核心:CNCWrapper 类设计与关键字段说明

public class CNCWrapper : IDisposable { private const string DLL_PATH_32 = @"C:\Windows\System32\cncall32.dll"; private const string DLL_PATH_64 = @"C:\Windows\System32\cncall64.dll"; // FOCAS2 连接句柄,全局唯一,避免多实例冲突 private static IntPtr _handle = IntPtr.Zero; private static readonly object _lock = new object(); // 缓存最近一次读取的 PR 数据,避免高频轮询压垮控制器 private static readonly Dictionary<int, PositionRegister> _prCache = new Dictionary<int, PositionRegister>(); // 断线重连策略:指数退避,最大重试 5 次,间隔 1s→2s→4s→8s→16s private static int _retryCount = 0; /// <summary> /// 初始化连接:IP 地址、端口(默认 8193)、超时毫秒(默认 5000) /// </summary> public bool Connect(string ip, int port = 8193, int timeoutMs = 5000) { lock (_lock) { if (_handle != IntPtr.Zero) return true; // 已连接 var ret = cnc_allclibhndl32(ip, port, timeoutMs, out _handle); if (ret != 0) { // 错误码映射见 FOCAS2 手册 Table 3-1:-1=网络不可达,-2=端口拒绝,-3=超时 throw new Exception($"FOCAS2 连接失败,错误码 {ret}"); } _retryCount = 0; return true; } } }
  • cnc_allclibhndl32:FOCAS2 初始化函数,返回 0 表示成功,非 0 为错误码;
  • _handle:全局静态句柄,确保整个 AppDomain 只有一个连接,避免“一个线程连上、另一个线程又连”导致控制器资源耗尽;
  • _prCache:点位缓存字典,Key 为 PR 编号(1~999),Value 为自定义PositionRegister类(含 X/Y/Z/W/P/R 六轴 float 值 + 时间戳);
  • _retryCount:断线重连计数器,配合Connect()方法实现自动恢复,比“每次读前都 connect”更可靠。

3. 读取点位信息:从 PR 到 UF,C# 如何精准解析 FOCAS2 返回的二进制数组?

3.1 点位数据类型映射表:发那科内部编号 vs C# 解析逻辑

FOCAS2 用no参数区分数据类型,每个类型对应固定长度的short[]返回数组。下表列出最常用 5 类点位数据的no范围、数组长度、C# 解析方式:

数据类型FOCAS2no起始值长度(short 元素数)C# 解析逻辑典型用途
PR(位置寄存器)1000 + PR 编号6BitConverter.ToSingle(data, i * 4)循环 6 次,i=0~5 → X/Y/Z/W/P/R获取示教点坐标
JR(关节寄存器)2000 + JR 编号6同 PR,但 W/P/R 对应 J4/J5/J6 关节角度读取当前关节角
SR(系统寄存器)3000 + SR 编号1BitConverter.ToInt16(data, 0)读取用户坐标系编号(SR[1])
UF(用户坐标系)4000 + UF 编号6同 PR,但表示 UF 坐标系原点相对于 WORLD 的偏移获取 UF[1] 原点位置
UT(工具坐标系)5000 + UT 编号6同 PR,表示 TOOL 相对于 FLANGE 的偏移获取 TCP 工具中心点

注意:no不是数组索引!例如读 PR[5],no = 1000 + 5 = 1005,不是5;读 UF[2],no = 4000 + 2 = 4002。

3.2 读取 PR[1] 的完整 C# 示例:含异常捕获与缓存更新

/// <summary> /// 读取指定 PR 编号的六轴坐标(X/Y/Z/W/P/R) /// </summary> public PositionRegister ReadPR(int prNo) { if (prNo < 1 || prNo > 999) throw new ArgumentOutOfRangeException(nameof(prNo), "PR 编号必须在 1~999 之间"); // 先查缓存,100ms 内未更新则走网络 if (_prCache.TryGetValue(prNo, out var cached) && DateTime.Now.Subtract(cached.Timestamp).TotalMilliseconds < 100) return cached; lock (_lock) // 防止多线程并发读同一 PR { short[] data = new short[6]; int ret = cnc_rdcncdata(_handle, 1000 + prNo, 0, 6, data); if (ret != 0) { // 错误码 -11=数据不存在,-12=权限不足(需确认控制器是否启用 FOCAS2) throw new Exception($"读取 PR[{prNo}] 失败,FOCAS2 错误码 {ret}"); } // 将 short[] 转为 float[]:每个 float 占 4 字节,需跨 2 个 short float[] floats = new float[6]; for (int i = 0; i < 6; i++) { // FOCAS2 返回的是 little-endian float 的 byte 序列,short[] 每 2 个元素组成 1 个 float byte[] bytes = new byte[4]; bytes[0] = BitConverter.GetBytes(data[i * 2])[0]; bytes[1] = BitConverter.GetBytes(data[i * 2])[1]; bytes[2] = BitConverter.GetBytes(data[i * 2 + 1])[0]; bytes[3] = BitConverter.GetBytes(data[i * 2 + 1])[1]; floats[i] = BitConverter.ToSingle(bytes, 0); } var pr = new PositionRegister { X = floats[0], Y = floats[1], Z = floats[2], W = floats[3], // W/P/R 对应欧拉角或四元数,取决于控制器设置 P = floats[4], R = floats[5], Timestamp = DateTime.Now, PRNumber = prNo }; _prCache[prNo] = pr; // 更新缓存 return pr; } }
  • cnc_rdcncdata()第 3 参数0表示起始索引(对 PR 无效,固定读全部 6 个值);
  • data[i * 2]和data[i * 2 + 1]组合还原float:因为short是 16 位,float是 32 位,FOCAS2 把每个float拆成两个short存储;
  • bytes数组手动拼接是必须的——BitConverter.ToSingle()直接作用于short[]会错位,这是发那科二进制协议的硬性约定。

3.3 读取 UF 坐标系:为什么 UF[1] 的 W/P/R 值总是 0?真相在这里

很多工程师读 UF[1] 时发现W/P/R为 0,以为数据异常。其实这是发那科坐标系定义的特性:

  • UF 坐标系只存储原点偏移(X/Y/Z)和Z 轴旋转角(R),W/P 无意义,固定为 0;
  • 真正的 UF 姿态(绕 X/Y/Z 的旋转)由UF[1]的R值 + 控制器内部的UF_FRAME参数共同决定,FOCAS2 不暴露完整姿态矩阵;
  • 若需完整 4×4 变换矩阵,必须调用cnc_rdaxisdata()读取AXIS数据,再结合cnc_rdsyscfg()获取 UF 定义参数,自行计算——这已超出点位读取范畴,属高级运动学解算。

4. 写入点位信息:PR 修改、SR 设定与防误操作的三重保险机制

4.1 写入 PR[1] 的 X 值:为什么不能直接cnc_wrcncdata()?必须先解锁

发那科控制器默认锁定所有 PR 数据,防止上位机误改导致撞机。写入前必须:

  1. 调用cnc_exeprg()执行#UNLOCK系统指令(需控制器处于 EDIT 模式);
  2. 检查cnc_sysinfo()返回的sysinfo.mode是否为EDIT;
  3. 写入后立即调用cnc_exeprg()执行#LOCK恢复锁定。
/// <summary> /// 安全写入 PR 某轴坐标(仅 X/Y/Z,W/P/R 需单独处理) /// </summary> public bool WritePRAxis(int prNo, char axis, float value) { if (!_isEditMode) throw new InvalidOperationException("控制器未处于 EDIT 模式,无法写入 PR"); // 1. 解锁 int ret = cnc_exeprg(_handle, "#UNLOCK"); if (ret != 0) throw new Exception($"解锁失败,错误码 {ret}"); try { // 2. 构造写入数据:只改单轴,其余保持原值 var current = ReadPR(prNo); float[] newData = { current.X, current.Y, current.Z, current.W, current.P, current.R }; int idx = axis switch { 'X' => 0, 'Y' => 1, 'Z' => 2, _ => throw new ArgumentException("仅支持 X/Y/Z 轴") }; newData[idx] = value; // 3. 转 short[]:每个 float 拆为 2 个 short short[] data = new short[12]; // 6 个 float × 2 short for (int i = 0; i < 6; i++) { byte[] bytes = BitConverter.GetBytes(newData[i]); data[i * 2] = BitConverter.ToInt16(bytes, 0); data[i * 2 + 1] = BitConverter.ToInt16(bytes, 2); } // 4. 写入 ret = cnc_wrcncdata(_handle, 1000 + prNo, 0, 6, data); if (ret != 0) throw new Exception($"写入 PR[{prNo}] {axis} 轴失败,错误码 {ret}"); // 5. 立即锁定 cnc_exeprg(_handle, "#LOCK"); return true; } catch { // 异常时强制锁定,避免控制器长期处于解锁态 cnc_exeprg(_handle, "#LOCK"); throw; } }
  • #UNLOCK/#LOCK是发那科系统指令,不是 FOCAS2 函数,必须用cnc_exeprg()执行;
  • newData数组必须包含全部 6 轴值,FOCAS2 不支持“只写单个字段”,否则未指定轴会被清零;
  • try/catch中的#LOCK是最后防线,确保即使写入失败,控制器也不会停留在解锁状态。

4.2 写入 SR[1](当前用户坐标系编号):为什么写完立刻读还是旧值?

SR[1] 存储的是当前激活的 UF 编号(1~9),但写入后需触发坐标系切换才能生效。常见误区:

  • 只写 SR[1] 不调用cnc_exeprg("#UFSEL")→ 值已存,但控制器未应用;
  • #UFSEL执行后需等待cnc_sysinfo().mode从EDIT切回AUTO→ 否则新 UF 不生效;

正确流程:

// 1. 写 SR[1] = 2 short[] srData = { 2 }; cnc_wrcncdata(_handle, 3001, 0, 1, srData); // 2. 切换 UF cnc_exeprg(_handle, "#UFSEL"); // 3. 等待模式切换(最多 2 秒) for (int i = 0; i < 20; i++) { var info = cnc_sysinfo(_handle); if (info.mode == 0) break; // 0= AUTO, 1= EDIT, 2= JOG... Thread.Sleep(100); }

4.3 防误操作三重保险:IP 白名单、写保护开关、操作日志

真实产线必须加这三层防护,否则一个WritePRAxis(1,'X',9999)就可能撞毁夹具:

  • IP 白名单:在发那科控制器SYSTEM→FOCAS2设置页,只允许上位机 IP 访问,禁用0.0.0.0;
  • 写保护开关:在 C# Wrapper 中增加IsWriteEnabled属性,初始化为false,需人工调用EnableWrite("ADMIN_PASSWORD")才解锁;
  • 操作日志:每次WritePRAxis()前记录DateTime.Now, Thread.CurrentThread.ManagedThreadId, prNo, axis, oldValue, newValue, Environment.UserName到本地 SQLite 数据库,审计留痕。

5. 避坑指南:发那科 C# 二次开发的 4 个致命陷阱与现场排查法

5.1 现象:cnc_allclibhndl32()返回 -2,但 ping 通、telnet 8193 端口也通

原因:发那科控制器 FOCAS2 功能未启用。R-30iB 默认关闭 FOCAS2,需进入MENU→SYSTEM→FOCAS2→ENABLE设为ON,并重启控制器。
解决:用示教器确认FOCAS2 STATUS显示RUNNING,而非STOPPED。注意:部分老版本固件(如 V8.30 以下)需额外安装 FOCAS2 License Key。

5.2 现象:读 PR[1] 时cnc_rdcncdata()返回 0,但data数组全是 0

原因:no参数错误。新手常把no = prNo(如 PR[1] 传 1),实际应为1000 + prNo(即 1001)。FOCAS2 对非法no不报错,只返回默认 0 值。
解决:打印no值调试,对照《FOCAS2 Function Reference》Table 4-1 确认编号范围;用cnc_rdcncdata()读no=1(系统状态)验证连接是否真通。

5.3 现象:C# 程序运行 2 小时后cnc_rdcncdata()开始超时,重启程序即恢复

原因:FOCAS2 连接句柄泄漏。cnc_allclibhndl32()分配的_handle未被cnc_freelibhndl32()释放,Windows 句柄数耗尽(默认 10000 个)。
解决:在CNCWrapper.Dispose()中强制调用cnc_freelibhndl32(_handle);用 Process Explorer 查看进程句柄数,确认是否持续增长。

5.4 现象:多线程读 PR[1] 和 PR[2],偶尔 PR[2] 返回 PR[1] 的值

原因:FOCAS2 DLL 非线程安全,且_handle是全局静态变量,多个线程共用同一句柄导致命令错乱。
解决:删除static IntPtr _handle,改为每个CNCWrapper实例独占句柄;或加lock (_lock)包裹所有cnc_*调用(本文采用后者,牺牲性能保稳定)。


6. 进阶技巧:用点位缓存 + 变化率检测实现“无感”实时监控与异常预警

6.1 点位缓存升级:从单次快照到滑动窗口趋势分析

单纯缓存最新值只能防抖,无法预警。我在产线部署时,给每个 PR 维护一个ConcurrentQueue<float>滑动窗口(长度 100,采样间隔 100ms),实时计算:

  • 变化率:(current - previous) / 0.1f(单位:mm/s);
  • 加速度:(rateNow - ratePrev) / 0.1f(单位:mm/s²);
  • 突变检测:若|current - median(last50)| > 5.0f且rate > 1000,触发“疑似急停或碰撞”告警。
private readonly ConcurrentDictionary<int, ConcurrentQueue<float>> _prTrend = new ConcurrentDictionary<int, ConcurrentQueue<float>>(); public void UpdatePRTrend(int prNo, float xValue) { var queue = _prTrend.GetOrAdd(prNo, _ => new ConcurrentQueue<float>()); queue.Enqueue(xValue); if (queue.Count > 100) queue.TryDequeue(out _); // 保持窗口大小 // 计算变化率(取最后两个值) if (queue.Count >= 2) { var arr = queue.ToArray(); float rate = (arr[arr.Length-1] - arr[arr.Length-2]) / 0.1f; if (Math.Abs(rate) > 1000 && IsSuddenJump(arr)) TriggerAlarm($"PR[{prNo}] X 轴突变,速率 {rate:F0} mm/s"); } } private bool IsSuddenJump(float[] values) { var last50 = values.Skip(Math.Max(0, values.Length - 50)).ToArray(); var median = CalculateMedian(last50); return Math.Abs(values.Last() - median) > 5.0f; }

6.2 用 Windows 服务 + NamedPipe 实现“永不掉线”的上位机守护

C# 上位机若以桌面程序运行,Win10 锁屏后 FOCAS2 连接会因电源策略中断。我的方案:

  • 主服务:Windows Service(.NET 6),常驻后台,负责 FOCAS2 连接、点位读写、告警推送;
  • 前端 UI:WPF 程序,通过NamedPipeServerStream连接服务,只做展示与指令下发;
  • 断线自愈:Service 内置Timer每 5 秒Ping()控制器 IP,失败则Disconnect()+Connect(),无需重启服务。

这样即使操作员关机、蓝屏、远程桌面断开,点位监控依然在跑。

6.3 最后一条血泪经验:永远在finally里cnc_freelibhndl32()

我曾因忘记释放句柄,导致一台 R-30iB 控制器连续 3 天无法响应任何 FOCAS2 请求,重启后才恢复。发那科控制器对未释放句柄极其敏感——它不会主动回收,而是把句柄当“活跃连接”维护,直到超时(默认 30 分钟),期间拒绝新连接。所以:

  • 所有cnc_allclibhndl32()调用必须配对cnc_freelibhndl32();
  • CNCWrapper构造函数里Connect(),Dispose()里Disconnect();
  • try/catch/finally中的finally块必须包含cnc_freelibhndl32(),哪怕前面已报错。

这条规则我写了 7 年,贴在工位显示器边框上,每天开工第一眼就看到。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表