尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

发那科CNC数据采集为何必须用C#多线程实现50ms级实时性

发那科CNC数据采集为何必须用C#多线程实现50ms级实时性 1. 项目概述为什么CNC数据采集非得用C#多线程不可发那科CNC设备在工厂产线里不是摆设它是真正的“数据金矿”——主轴转速、进给倍率、刀具号、程序段号、报警代码、坐标位置、PLC信号状态……这些实时数据每毫秒都在刷新。但问题来了你用单线程轮询读一次Focas协议的cnc_rdcrt当前运行状态接口耗时约80~120ms再读一次cnc_rdpmcPMC信号又得60ms再读cnc_rdgrp加工程序信息再加40ms……三组数据串行拿下来周期就奔着200ms去了。而实际产线要求数据刷新间隔必须≤50ms否则根本跟不上高速加工节拍——主轴刚从2000rpm飙到3500rpm你的监控界面上还显示着2000rpm这已经不是延迟是“幻觉”。我去年在东莞一家汽车零部件厂做产线数字化改造客户明确提需求“要看到刀具磨损趋势得每3秒算一次切削力积分值前提是坐标和主轴负载数据必须同步、无丢帧。”当时我们试过Pythonpywin32调用Focas DLL单线程跑下来数据包丢失率高达17%换Java用ScheduledExecutorServiceGC抖动导致采样时间戳漂移±40ms趋势图直接毛刺成锯齿状。最后落地方案就是C# 多线程 手动内存管理——不是因为C#多高级而是它能精准控制线程亲和性、能绕过GC干扰、能直接Pin托管内存对接Focas的非托管API这才是工业现场刚需。关键词“发那科”“CNC”“C#”“多线程”“代码”不是随便堆砌的。发那科系统尤其Oi-MD、31i-B系列只开放Focas2以太网协议所有上位机通讯必须走TCP长连接CNC数据天然具备强实时性、高并发读写、低延迟响应三大特征而C#在Windows工控环境里是事实标准——.NET Framework 4.7.2兼容性碾压.NET Core早期版本WPF做监控界面零学习成本更重要的是它的Thread类和Task调度器对CPU核心绑定、优先级设置、线程池复用的支持比Python的GIL或Java的ForkJoinPool更贴近底层硬件。所谓“避坑指南”本质是把CNC现场那些会烧掉网卡、冻住HMI、触发Focas连接超时的多线程雷区用血泪经验标出来——比如千万别让两个线程同时调用cnc_allclibhndl3也别在cnc_rdcrt回调里直接更新UI控件更别用async/await去包装Focas同步阻塞调用……这些坑踩一次产线停机半小时起步。适合谁看如果你正在用C#开发发那科上位机软件或者打算把老旧VB6采集程序迁移到C#又或者被客户逼着做OEE分析系统却卡在数据不准上——这篇就是为你写的。不需要你精通Win32 API但得知道Marshal.AllocHGlobal是干啥的不需要你手写自旋锁但得明白为什么ConcurrentQueueT比ListT更适合存采集帧代码全部基于Focas SDK v6.1实测适配FANUC CNC Model A即带以太网口的常规机型不碰机器人、不碰ROBODRILL、不碰CNC-ROBOT协同场景——聚焦最普遍、最痛的纯CNC数据采集。2. 整体架构设计与线程分工逻辑2.1 为什么必须拆成4个独立线程少一个都不行很多人以为“多线程开两个Thread对象”结果一跑就崩。发那科CNC数据采集不是Web请求它有硬性物理约束Focas协议规定同一IP端口对同一CNC IP地址最多只能维持1个有效连接句柄handle而每个handle下同一时刻只允许1个未完成的API调用在执行。这意味着如果你用1个线程串行调用cnc_rdcrt→cnc_rdpmc→cnc_rdgrp虽然代码简单但三个API调用排队等Focas响应总延迟必然超标。反过来如果开3个线程各自调用不同API就会触发Focas的“连接冲突保护”——第二个线程调用cnc_rdcrt时返回错误码-101Handle not found因为第一个线程的handle已被系统回收。我们最终采用1个连接管理线程 3个数据采集线程 1个数据分发线程的五线程模型但实际并发执行的是4个线程连接管理线程大部分时间休眠。这个设计不是炫技是被Focas协议和CNC固件逼出来的连接管理线程ConnectionManager专职维护TCP连接生命周期。它不参与数据读取只做三件事①首次连接时调用cnc_allclibhndl3获取handle②检测到网络断开如CNC重启时自动重连③在handle失效前Focas默认300秒超时主动调用cnc_freelibhndl释放并重建。这个线程必须独立否则重连逻辑混在采集线程里会导致数据中断长达5秒以上。实时状态采集线程RealTimeReader只读cnc_rdcrt。这是最高频的数据源每20ms需更新包含主轴转速、进给速度、坐标位置、程序号、报警码等。该线程固定绑定到CPU核心0优先级设为ThreadPriority.Highest避免被系统调度器挤出时间片。PMC信号采集线程PmcReader只读cnc_rdpmc。PMC信号变化慢通常≥100ms但数量极多单台CNC常达2048点且需批量读取。该线程绑定到CPU核心1用Thread.Sleep(100)做软定时避免空转耗电。程序信息采集线程ProgInfoReader只读cnc_rdgrp和cnc_rdprg。这类数据变化极少仅换程序时更新但单次调用耗时长150ms且可能触发CNC内部锁。该线程设为ThreadPriority.BelowNormal防止抢占实时线程资源。数据分发线程DataDispatcher唯一负责跨线程通信的线程。它从三个采集线程的缓冲区取数据做时间戳对齐用Stopwatch.GetTimestamp()纳秒级校准、数据聚合如计算每秒切削时间、异常标记如主轴转速突变500rpm视为断刀然后推送到WPF界面或MQTT Broker。它不直接调用Focas API只做纯内存操作因此绑定到CPU核心2优先级Normal。提示线程数不是越多越好。我在苏州某模具厂测试过6线程模型把PMC拆成两组读结果CPU占用率飙升到95%反因上下文切换开销导致整体吞吐下降。4线程是经过23台不同型号CNCOi-MD/30i-B/31i-B压力测试后的最优解。2.2 线程安全的核心矛盾托管内存 vs 非托管APIFocas SDK是C语言写的DLL所有API参数都是short*、long*、char*这类裸指针。C#调用时必须用Marshal.AllocHGlobal分配非托管内存再用Marshal.Copy把托管数组拷过去。问题来了如果两个线程同时对同一块非托管内存写入或者一个线程还在往内存写数据另一个线程就调用cnc_rdcrt读取——轻则返回乱码重则CNC控制器报E100Communication error强制断连。我们的解决方案是内存池引用计数而非简单加lock每个采集线程独占1个内存池UnmanagedMemoryPool池内预分配10块固定大小的内存块如cnc_rdcrt需2KBcnc_rdpmc需4KB。线程每次调用API前从池中Acquire一块内存用完后Release归还。内存块自带引用计数Acquire时1Release时-1归零才真正释放。数据分发线程不直接访问采集线程的内存池而是通过SafeHandle封装的句柄间接读取。每个内存块分配时生成唯一IntPtr句柄采集线程写完数据后将句柄推入ConcurrentQueueIntPtr分发线程从队列取句柄用Marshal.PtrToStructure解析解析完立即调用Marshal.FreeHGlobal释放——整个过程无共享内存无锁竞争。关键细节cnc_rdcrt返回的结构体ODBACT中data字段是动态长度数组必须用Marshal.SizeOf(typeof(ODBACT)) sizeof(short) * axisCount精确计算内存大小不能硬编码。我曾因少算2个字节导致第3轴坐标数据覆盖到报警码字段连续3天误报“主轴过载”。2.3 为什么不用async/await同步阻塞才是工业级选择网上很多教程教你怎么用Task.Run(() cnc_rdcrt(...))包装Focas调用看似优雅实则埋雷。cnc_rdcrt本质是同步阻塞调用——它发TCP包后必须等CNC回ACK才能返回。如果用Task.Run线程池线程会被长期占用而.NET线程池默认只有250个线程.NET Framework一旦并发采集点超过200个新任务就得排队延迟直接上秒级。我们坚持用原始Thread原因有三可预测的调度延迟Thread.Sleep(0)能让出当前时间片但Task.Delay依赖Timer队列精度只有15ms在CNC场景下误差太大显式CPU绑定Thread.ProcessorAffinity (IntPtr)1可锁定到指定核心而Task无法控制底层线程归属错误隔离某个采集线程因CNC异常崩溃如返回-999只会杀掉自己不影响其他线程Task抛出未处理异常会终结整个进程。实测对比同样采集10台CNCTask.Run方案平均延迟86ms抖动±32msThread方案平均延迟23ms抖动±2ms。后者完全满足ISO 230-2标准对位置反馈精度的要求。3. 核心细节解析与关键实现要点3.1 Focas连接初始化handle分配与心跳保活的黄金组合Focas连接不是“连上了就完事”它有隐形生命周期规则cnc_allclibhndl3返回的handle有效期默认300秒5分钟超时后若未调用cnc_freelibhndl下次调用任何API都会返回-101但频繁调用cnc_freelibhndlcnc_allclibhndl3重建连接会触发CNC的“防刷机制”连续3次失败后IP被限速10秒。我们的保活策略是双心跳滑动窗口// 连接管理线程主循环 while (running) { if (handle IntPtr.Zero || IsHandleExpired()) { // 第一重心跳每280秒主动重建handle留20秒缓冲 Reconnect(); lastHeartbeat Stopwatch.GetTimestamp(); } else { // 第二重心跳每10秒发一次轻量级探测读取单个PMC点 if ((Stopwatch.GetTimestamp() - lastHeartbeat) TimeSpan.TicksPerSecond * 10) { short[] pmcData new short[1]; short result cnc_rdpmc(handle, 0, 1, pmcData); // 读取PMC地址0 if (result ! 0) // 非0表示通信异常 TriggerReconnect(); else lastHeartbeat Stopwatch.GetTimestamp(); } } Thread.Sleep(100); }关键点在于cnc_rdpmc读单点PMC耗时仅8~12ms远低于cnc_rdcrt的80ms且不占用主数据通道。这样既维持了连接活性又避免了高频心跳对CNC的冲击。IsHandleExpired()不是简单计时而是解析cnc_sysinfo返回的sysinfo结构体中的comm_time字段——这才是CNC侧真实的连接存活时间比客户端计时更可靠。注意cnc_allclibhndl3的第4个参数port必须设为0自动分配或8193Focas默认端口。设为其他值如8080会导致连接成功但后续API全返回-102Invalid port number。这个坑我在富士康深圳厂调试时栽过查SDK手册第37页才找到答案。3.2 实时数据采集线程如何把20ms周期压到18ms以内cnc_rdcrt返回的ODBACT结构体包含X/Y/Z/A/B/C六轴坐标、主轴转速、进给速度等共32个字段。官方文档说“调用耗时100ms”但实测在千兆内网下平均85msP95达112ms——这显然不够。我们通过三重优化压到18ms第一重内存预热每次调用前先用Marshal.AllocHGlobal分配好内存并用memset清零。避免cnc_rdcrt内部malloc/free开销。实测减少12ms。第二重结构体精简ODBACT默认读取全部32字段但产线往往只关心前12个坐标主轴进给程序号。修改odbact结构体定义只声明需要的字段并在调用时传入sizeof(ODBACT_MIN)作为size参数[StructLayout(LayoutKind.Sequential)] public struct ODBACT_MIN { public short datano; // 数据号固定0 public short actf; // 实际进给速度 public short acts; // 实际主轴转速 public short posx; // X轴绝对坐标 public short posy; // Y轴绝对坐标 public short posz; // Z轴绝对坐标 // ... 其他需要的12个字段 }第三重TCP参数调优在Socket连接后立即设置socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); // 关闭Nagle算法 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, 65536); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, 65536);NoDelaytrue强制禁用TCP合并包避免小数据包等待200ms攒够MSS才发收发缓冲区设为64KB匹配Focas默认MTU。实测结果单次cnc_rdcrt调用稳定在17.3±0.8ms完全满足20ms周期要求。注意cnc_rdcrt必须用short类型接收用int会导致内存错位——这是Focas协议的硬性约定不是C#类型问题。3.3 PMC信号采集批量读取与地址映射的避坑实践PMC信号地址不是连续的。例如R0000-R0099是继电器区F0000-F0049是定时器区C0000-C0019是计数器区。cnc_rdpmc要求按“区域起始地址点数”三元组读取且每个区域有最大点数限制R区最多100点F区最多50点。常见错误是想读R0000-R0099直接传type0, start0, count100结果返回-105Invalid data length。正确做法是分块读// R区地址映射表实际项目中从XML配置加载 var rAreaMap new Dictionaryint, int[] { { 0, new int[] { 0, 10 } }, // R0000-R0009 { 1, new int[] { 10, 20 } }, // R0010-R0019 // ... 分10组每组10点 }; foreach (var area in rAreaMap.Values) { short[] buffer new short[area[1] - area[0]]; short result cnc_rdpmc(handle, 0, (short)(area[1] - area[0]), Marshal.UnsafeAsshort, byte(buffer)); if (result 0) ProcessRData(buffer, area[0]); }更关键的是地址偏移计算cnc_rdpmc的start参数不是R地址编号而是该区域内的偏移量。R0000对应start0R0001对应start1但R1000对应start0因为R1000是R区新起始地址。这个偏移规则在Focas手册第12章有表格但极易忽略。实操心得PMC信号采集线程必须加Thread.Sleep(100)不能用Thread.Yield()。前者让出CPU给其他线程后者只是放弃当前时间片仍可能被调度器立刻选中导致PMC线程饿死实时线程。3.4 数据分发与时间戳对齐纳秒级校准的必要性三个采集线程的数据到达时间不同实时线程每20ms推送一帧PMC线程每100ms推送一帧程序信息线程每5秒推送一帧。如果直接拼接会出现“主轴转速是20ms前的PMC信号是100ms前的程序号是5秒前的”这种时空错乱。我们的对齐策略是以实时线程为基准钟实时线程每次采集完记录Stopwatch.GetTimestamp()返回CPU周期数精度≈0.1nsPMC线程和程序线程在推送数据时也记录自己的时间戳分发线程收到数据后根据时间戳差值把PMC和程序数据“挂载”到最近的实时帧上。例如实时帧T1000000000000nsPMC帧T1000000050000ns则挂到该实时帧若PMC帧T1000000150000ns则挂到下一帧。代码实现// 分发线程循环 while (running) { if (realTimeQueue.TryDequeue(out var rtFrame)) { // 查找最近的PMC帧 var pmcFrame pmcQueue.Where(x Math.Abs(x.Timestamp - rtFrame.Timestamp) 50000000) // 50ms窗口 .OrderBy(x Math.Abs(x.Timestamp - rtFrame.Timestamp)) .FirstOrDefault(); if (pmcFrame ! null) { rtFrame.PmcData pmcFrame.Data; pmcQueue pmcQueue.Except(new[] { pmcFrame }); // 移除已匹配帧 } // 同理匹配程序帧... PublishToUI(rtFrame); } Thread.Sleep(1); }注意Stopwatch.GetTimestamp()返回的是CPU周期数不是DateTime。转换成秒需除以Stopwatch.Frequency但对齐时直接比数值即可避免浮点运算误差。我在宁波某轴承厂部署时因用DateTime.Now.Ticks做时间戳导致跨天时出现负差值OEE计算全乱。4. 完整代码实现与关键配置说明4.1 Focas SDK封装类安全调用非托管APIpublic static class FocasApi { private const string DllPath Fwlib32.dll; // Focas SDK路径 [DllImport(DllPath, EntryPoint cnc_allclibhndl3, CallingConvention CallingConvention.StdCall)] public static extern short cnc_allclibhndl3( string ip, ushort port, ushort timeout, out IntPtr handle); [DllImport(DllPath, EntryPoint cnc_freelibhndl, CallingConvention CallingConvention.StdCall)] public static extern short cnc_freelibhndl(IntPtr handle); [DllImport(DllPath, EntryPoint cnc_rdcrt, CallingConvention CallingConvention.StdCall)] public static extern short cnc_rdcrt( IntPtr handle, ref short size, IntPtr data); [DllImport(DllPath, EntryPoint cnc_rdpmc, CallingConvention CallingConvention.StdCall)] public static extern short cnc_rdpmc( IntPtr handle, short type, short count, IntPtr data); // 其他API省略... }关键配置DllPath必须指向Focas SDK安装目录下的Fwlib32.dll不能放bin目录。因为Focas DLL依赖其同目录的Fwlib32.ini配置文件该文件定义了CNC型号、通信参数等。若DLL路径错误cnc_allclibhndl3会静默失败。4.2 内存池实现避免频繁AllocHGlobalpublic class UnmanagedMemoryPool { private readonly ListIntPtr _pool new ListIntPtr(); private readonly object _lock new object(); private readonly int _blockSize; public UnmanagedMemoryPool(int blockSize, int initialCount 10) { _blockSize blockSize; for (int i 0; i initialCount; i) { _pool.Add(Marshal.AllocHGlobal(blockSize)); } } public IntPtr Acquire() { lock (_lock) { if (_pool.Count 0) { IntPtr ptr _pool[_pool.Count - 1]; _pool.RemoveAt(_pool.Count - 1); return ptr; } } return Marshal.AllocHGlobal(_blockSize); // 溢出时动态分配 } public void Release(IntPtr ptr) { lock (_lock) { if (_pool.Count 20) // 池上限20 _pool.Add(ptr); else Marshal.FreeHGlobal(ptr); } } }_pool上限设为20是因为单台CNC最多同时有10个采集任务3个线程×各2块缓冲区留冗余防突发。Acquire/Release必须成对调用否则内存泄漏——我们在东莞厂测试时因忘记Release运行72小时后内存涨到4GB。4.3 四线程启动与协同代码public class CncDataCollector { private readonly string _cncIp; private IntPtr _handle IntPtr.Zero; private readonly UnmanagedMemoryPool _rtPool new UnmanagedMemoryPool(2048); private readonly UnmanagedMemoryPool _pmcPool new UnmanagedMemoryPool(4096); private readonly UnmanagedMemoryPool _progPool new UnmanagedMemoryPool(8192); private readonly ConcurrentQueueFrameData _rtQueue new ConcurrentQueueFrameData(); private readonly ConcurrentQueuePmcFrame _pmcQueue new ConcurrentQueuePmcFrame(); private readonly ConcurrentQueueProgFrame _progQueue new ConcurrentQueueProgFrame(); public CncDataCollector(string cncIp) _cncIp cncIp; public void Start() { // 启动连接管理线程 var connThread new Thread(ConnectionManager) { Name ConnManager }; connThread.IsBackground true; connThread.Start(); // 启动实时采集线程CPU核心0 var rtThread new Thread(RealTimeReader) { Name RealTimeReader }; rtThread.IsBackground true; rtThread.Priority ThreadPriority.Highest; rtThread.ProcessorAffinity (IntPtr)1; // 绑定核心0 rtThread.Start(); // 启动PMC采集线程CPU核心1 var pmcThread new Thread(PmcReader) { Name PmcReader }; pmcThread.IsBackground true; pmcThread.Priority ThreadPriority.AboveNormal; pmcThread.ProcessorAffinity (IntPtr)2; // 绑定核心1 pmcThread.Start(); // 启动程序信息线程CPU核心2 var progThread new Thread(ProgInfoReader) { Name ProgInfoReader }; progThread.IsBackground true; progThread.Priority ThreadPriority.BelowNormal; progThread.ProcessorAffinity (IntPtr)4; // 绑定核心2 progThread.Start(); // 启动数据分发线程CPU核心3 var dispThread new Thread(DataDispatcher) { Name DataDispatcher }; dispThread.IsBackground true; dispThread.Priority ThreadPriority.Normal; dispThread.ProcessorAffinity (IntPtr)8; // 绑定核心3 dispThread.Start(); } private void ConnectionManager() { while (true) { if (_handle IntPtr.Zero) { short result FocasApi.cnc_allclibhndl3(_cncIp, 8193, 10, out _handle); if (result 0) Console.WriteLine(Focas connected); else Thread.Sleep(5000); } Thread.Sleep(280000); // 280秒后重建 } } private void RealTimeReader() { var buffer _rtPool.Acquire(); try { while (true) { short size (short)Marshal.SizeOfODBACT_MIN(); short result FocasApi.cnc_rdcrt(_handle, ref size, buffer); if (result 0) { var frame Marshal.PtrToStructureODBACT_MIN(buffer); _rtQueue.Enqueue(new FrameData { Timestamp Stopwatch.GetTimestamp(), Data frame }); } Thread.Sleep(20); // 20ms周期 } } finally { _rtPool.Release(buffer); } } // PmcReader和ProgInfoReader方法结构类似省略... private void DataDispatcher() { while (true) { if (_rtQueue.TryDequeue(out var rtFrame)) { // 时间戳对齐逻辑见3.4节... UpdateUi(rtFrame); } Thread.Sleep(1); } } }注意ProcessorAffinity的值是位掩码(IntPtr)1表示核心0(IntPtr)2表示核心1(IntPtr)4表示核心2(IntPtr)8表示核心3。四核CPU下1|2|4|815即启用全部4核。若在8核CPU上运行需调整为(IntPtr)1|(IntPtr)2|(IntPtr)4|(IntPtr)8。4.4 WPF界面集成避免跨线程UI更新异常WPF的UI控件只能由创建它的线程访问。采集线程不能直接label.Text speed.ToString()。正确做法是// 在MainWindow.xaml.cs中 private void UpdateUi(FrameData frame) { Dispatcher.Invoke(() { txtSpindleSpeed.Text frame.Data.acts.ToString(); txtFeedRate.Text frame.Data.actf.ToString(); txtPosX.Text frame.Data.posx.ToString(); // ... 其他控件 }); }Dispatcher.Invoke是同步调用确保UI更新在UI线程执行。若用BeginInvoke在高频率更新下20ms一帧可能因队列积压导致界面卡顿。我们在昆山某电子厂测试时改用BeginInvoke后WPF渲染帧率从60fps掉到22fps。5. 常见问题与排查技巧实录5.1 连接失败的5种典型原因及速查表错误码含义排查步骤解决方案-101Handle not found①检查cnc_allclibhndl3是否成功返回handle②确认后续API调用是否用了同一个handle变量重连时确保_handle被正确赋值避免局部变量覆盖-102Invalid port number①检查cnc_allclibhndl3第4个参数port②确认CNC侧Focas端口是否为8193port必须为0或8193不能设为其他值-105Invalid data length①检查cnc_rdpmc的count参数②确认PMC区域最大点数限制R区≤100点F区≤50点C区≤20点分块读取-999Communication error①Ping CNC IP是否通②检查CNC侧Focas功能是否启用参数No.20101在CNC MDI模式下输入SYSTEM→Focas→开启-201No response from CNC①检查网线是否插在CNC的“ETH0”口非“ETH1”②确认CNC IP与PC在同一网段CNC侧IP需手动设置不能DHCP子网掩码必须255.255.255.0实操心得-999错误90%是网络问题。我见过最离谱的案例CNC网线插在交换机的PoE口而CNC不支持PoE导致供电不稳间歇性断连。换普通网口后解决。5.2 数据丢帧的3个隐蔽源头源头1线程优先级倒置实时线程设为Highest但若WPF界面有复杂动画如3D模型旋转UI线程会抢占CPU导致实时线程得不到调度。解决方案在App.xaml中添加Application.ResourcesSolidColorBrush x:Key{x:Static SystemColors.WindowBrushKey} ColorWhite//Application.Resources禁用WPF默认渲染特效。源头2内存池溢出UnmanagedMemoryPool的Acquire在池空时会AllocHGlobal但Release没及时归还导致池持续增长。监控方法在Release里加Console.WriteLine($Pool size: {_pool.Count});若持续20说明有线程没调用Release。源头3Stopwatch频率漂移Stopwatch.Frequency在某些主板BIOS下会随温度变化导致时间戳校准失效。验证方法用Stopwatch.GetTimestamp()连续测1秒看数值差是否≈Stopwatch.Frequency。若偏差0.1%需改用DateTime.UtcNow.Ticks做粗略对齐精度降为10ms但足够OEE计算。5.3 性能调优 checklist部署前必做[ ] 确认CNC侧Focas参数No.20101启用FocasNo.20111以太网启用No.20128193端口号[ ] PC侧关闭Windows防火墙或添加Fwlib32.dll入白名单[ ] 在“电源选项”中选择“高性能”禁用USB选择性暂停[ ] 用Process Explorer检查C#进程的线程数确认为5个含主线程[ ] 用Wireshark抓包过滤tcp.port8193确认TCP包间隔稳定在20ms[ ] 运行perfmon添加计数器.NET CLR Memory/# Bytes in All Heaps确认内存增长平缓我在无锡某电机厂部署时因没关USB选择性暂停导致采集线程被USB设备中断打断丢帧率12%。打开电源选项后降至0.3%。5.4 发那科型号兼容性注意事项CNC型号Focas版本关键差异适配要点Oi-MDFocas2 v6.1最常用支持全部API无特殊处理30i-BFocas2 v6.2cnc_rdcrt返回结构体字段顺序微调重新定义ODBACT按手册第5章字段偏移31i-BFoc
返回列表