# C# 工控实战:Modbus RTU 大寄存器采集方案,油井示功图分片读取完整实现
Modbus RTU 协议单帧最大只能读取 125 个寄存器但一张完整的油井示功图需要采集 1015 个寄存器2030 字节。本文深入解析一套拆分采集 缓存组合的实战方案展示如何通过 13 个监控命令、5 张配置表实现示功图的完整采集与存储。核心内容包括架构设计、13 个子命令拆分策略、配置驱动采集流程、数据缓存与组合存储、示功图关键指标提取最大/最小载荷、上/下行程电流分析。适用于工业物联网开发、Modbus 协议开发、C# 工控系统开发、远程数据采集等场景。一、什么是示功图示功图Dynagraph Card是抽油机在一个冲程周期内载荷与位移的关系曲线。它是油井工况诊断的核心工具正常工况示功图呈规则的平行四边形含沙影响出现不规则尖峰充满度低示功图呈刀把状杆断示功图呈窄条状一张完整的示功图包含以下数据数据类型寄存器数量每寄存器字节总字节元数据15230位移数据250 (8010070)2500载荷数据250 (8110168)2500电流数据250 (829078)2500功率数据250 (859372)2500总寄存器数 1015 个2030 字节而 Modbus RTU 功能码 03 单帧最多读取 125 个寄存器无法一次性传输。二、解决方案架构2.1 核心设计思想我们采用**“拆分采集 缓存组合”**的方案拆分采集将示功图数据拆分为 13 个子命令分别采集数据缓存每个子命令返回后数据先缓存到内存就绪检查所有子命令数据到齐后触发数据组合组合存储按配置将数据拼装成完整示功图记录2.2 13 个监控命令示功图采集涉及 13 个独立的监控命令命令 ID命令名称功能码起始地址读取数量数据内容MCYJ307功图采集数据398015冲程、冲次、采集点数、采集时间等元数据MCYJ308位移13100080位移数据包1MCYJ309位移231080100位移数据包2MCYJ310位移33118070位移数据包3MCYJ311载荷13125081载荷数据包1MCYJ312载荷231331101载荷数据包2MCYJ313载荷33143268载荷数据包3MCYJ314电流13150082电流数据包1MCYJ315电流23158290电流数据包2MCYJ316电流33167278电流数据包3MCYJ317功率13175085功率数据包1MCYJ318功率23183593功率数据包2MCYJ319功率33192872功率数据包3关键设计每个命令独立下发、独立返回、独立解析。系统通过配置表知道这些命令属于同一张示功图。三、配置驱动5 张表如何定义示功图采集3.1 Monitor命令定义MCYJ307 | 功图采集数据 | code3 | dllidDLL002 | timeout15 | retrytimes2 MCYJ308 | 位移1 | code3 | dllidDLL002 | timeout15 | retrytimes2 MCYJ309 | 位移2 | code3 | dllidDLL002 | timeout15 | retrytimes2 ... MCYJ319 | 功率3 | code3 | dllidDLL002 | timeout15 | retrytimes2dllidDLL002将命令绑定到 Modbus 协议。timeout15表示 15 秒超时retrytimes2表示超时后重试 2 次。3.2 MonitorRequest请求参数bh | id | mid | name | datatype | defaultvalue 1922 | StartBit | MCYJ307 | 起始地址 | UInt16 | 980 1923 | EndBit | MCYJ307 | 结束地址 | UInt16 | 15 1902 | StartBit | MCYJ308 | 起始地址 | UInt16 | 1000 1903 | EndBit | MCYJ308 | 结束地址 | UInt16 | 80 1380 | StartBit | MCYJ317 | 起始地址 | UInt16 | 1750 1381 | EndBit | MCYJ317 | 结束地址 | UInt16 | 85以MCYJ307为例请求参数告诉 Modbus 协议从寄存器地址 980 开始读取 15 个寄存器。3.3 MonitorResponse响应解析规则bh | id | mid | name | datatype | datalen | repeat | modifiedoutput 17403 | ActualNumber| MCYJ307 | 功图实际点数 | UInt32 | 2 | False | 17404 | AcquisitionTime| MCYJ307 | 功图采集时间 | DateTime | 12 | False | 17405 | AtTimes | MCYJ307 | 冲次 | Float | 4 | False | 17406 | Stroke | MCYJ307 | 冲程 | Float | 4 | False | 16960 | W1 | MCYJ308 | 位移包 | UInt32 | 2 | True | 小数 17391 | W1 | MCYJ309 | 位移包 | UInt32 | 2 | True | 小数 16980 | W1 | MCYJ310 | 位移包 | UInt32 | 2 | True | 小数 17480 | Z1 | MCYJ311 | 载荷包 | UInt32 | 2 | True | 小数 17420 | Z1 | MCYJ312 | 载荷包 | UInt32 | 2 | True | 小数 17481 | Z1 | MCYJ313 | 载荷包 | UInt32 | 2 | True | 小数 14174 | DL | MCYJ314 | 电流包 | UInt32 | 2 | True | 小数 17377 | DL | MCYJ315 | 电流包 | Int32 | 2 | True | 小数 17138 | DL | MCYJ316 | 电流包 | Int32 | 2 | True | 小数 14547 | GL | MCYJ317 | 功率包 | Int16 | 2 | True | 小数 15603 | GL | MCYJ318 | 功率包 | Int16 | 2 | True | 小数 14836 | GL | MCYJ319 | 功率包 | Int16 | 2 | True | 小数关键字段repeatTrue表示该字段在响应中重复出现多次如载荷包的 81 个数据点modifiedoutput小数表示需要将原始值除以 100 转换为实际值datalen2每个数据点占 2 字节3.4 SMonitor存储命令scyjid | sname | deviceid | monitorid | period | storagetable | storageid SCYJ001 | 示功图 | CYJ0001 | MCYJ002 | 3 | cyj_Dynagraph | ms_cyyofm SCYJ002 | 示功图 | CYJ0003 | MCYJ002 | 3 | cyj_Dynagraph | ms_cyyofm SCYJ004 | 示功图 | CYJ0008 | MCYJ002 | 3 | cyj_Dynagraph | ms_cyyofm SCYJ006 | 示功图 | CYJ0005 | MCYJ002 | 3 | cyj_Dynagraph | ms_cyyofmperiod3表示数据等待时间窗口为 3 分钟——所有子命令的数据必须在这个时间窗口内到齐否则视为无效。3.5 SMonitorResponse存储字段定义id | sid | name | storagefield | modifiedoutput SID005 | SCYJ004 | 功图点数 | dyn_TestCount | SID003 | SCYJ004 | 冲次 | dyn_Degree | SID004 | SCYJ004 | 功图时间 | dyn_TestTime | SID000 | SCYJ004 | 冲程 | dyn_Stroke | SID007 | SCYJ004 | 最大载荷 | dyn_MaxLoad | 最大载荷 SID008 | SCYJ004 | 最小载荷 | dyn_MinLoad | 最小载荷 SID009 | SCYJ004 | 最大电流 | dyn_Imax | 最大电流 SID010 | SCYJ004 | 最小电流 | dyn_Imin | 最小电流 SID015 | SCYJ004 | 上行程最大电流 | dyn_UpImax | 上行程最大电流 SID016 | SCYJ004 | 下行程最大电流 | dyn_DnImax | 下行程最大电流 SID001 | SCYJ004 | 位移 | dyn_PkgDist | 位移包 SID002 | SCYJ004 | 载荷 | dyn_PkgLoad | 载荷包 SID013 | SCYJ004 | 电流包 | dyn_PkgDL | 电流包 SID020 | SCYJ004 | 功率包 | dyn_PkgGL | 有功功率包modifiedoutput字段定义了数据处理函数如最大载荷、位移包等系统会在存储前调用对应的处理函数。3.6 SMPValue数据来源映射这是存储层最核心的表定义了存储字段的数据来源smpid | sid | deviceid | monitorid | monitorresponseid SID001 | SCYJ004 | CYJ0008 | MCYJ307 | ActualNumber → 位移包第1个元素点数 SID001 | SCYJ004 | CYJ0008 | MCYJ308 | W1 → 位移包第2-81个元素 SID001 | SCYJ004 | CYJ0008 | MCYJ309 | W1 → 位移包第82-181个元素 SID001 | SCYJ004 | CYJ0008 | MCYJ310 | W1 → 位移包第182-251个元素 SID002 | SCYJ004 | CYJ0008 | MCYJ307 | ActualNumber → 载荷包第1个元素点数 SID002 | SCYJ004 | CYJ0008 | MCYJ311 | Z1 → 载荷包第2-82个元素 SID002 | SCYJ004 | CYJ0008 | MCYJ312 | Z1 → 载荷包第83-183个元素 SID002 | SCYJ004 | CYJ0008 | MCYJ313 | Z1 → 载荷包第184-251个元素 SID013 | SCYJ004 | CYJ0008 | MCYJ307 | ActualNumber → 电流包第1个元素点数 SID013 | SCYJ004 | CYJ0008 | MCYJ314 | DL → 电流包第2-83个元素 SID013 | SCYJ004 | CYJ0008 | MCYJ315 | DL → 电流包第84-173个元素 SID013 | SCYJ004 | CYJ0008 | MCYJ316 | DL → 电流包第174-251个元素 SID020 | SCYJ004 | CYJ0008 | MCYJ307 | ActualNumber → 功率包第1个元素点数 SID020 | SCYJ004 | CYJ0008 | MCYJ317 | GL → 功率包第2-86个元素 SID020 | SCYJ004 | CYJ0008 | MCYJ318 | GL → 功率包第87-179个元素 SID020 | SCYJ004 | CYJ0008 | MCYJ319 | GL → 功率包第180-251个元素关键设计同一个smpid如SID001可以映射到多个monitorresponseid系统会自动将这些数据拼接起来每个数据包位移、载荷、电流、功率都以 ActualNumber功图实际点数作为第一个元素这是为了后续数据处理函数能知道实际采集了多少个点四、数据采集流程4.1 命令下发系统每 10 分钟触发一次示功图采集TimerHandler 定时触发 ↓ 遍历 DeviceMonitor 配置CYJ0005 绑定了 13 个示功图命令 ↓ sendMonitor(定时监控, device, monitor, null) ↓ 创建 New 任务对象Timeout15, RetryTimes2 ↓ commPort.AddNew(dev) 加入任务队列4.2 数据返回与缓存每个命令返回后系统执行以下操作协议解析Modbus 协议 DLL 解析响应帧提取数据点第一级缓存数据写入HaoPuServer.ResponseData字典第二级缓存数据传递给Comm.Server.data字典就绪检查遍历 SMonitor 配置检查所有子命令数据是否到齐// Server.cs - CacheData()lock(this.data){// 写入当前命令数据stringcacheKeycacheDATA.DeviceId;cacheDATA.MonitorId;this.data[cacheKey]cacheDATA;// 检查存储命令的所有子命令是否就绪foreach(StorageMonitorsminenabledSM){if(sm.Monitor.Contains(cacheKey)){boolallMonitorsReadytrue;foreach(stringmonitorKeyinsm.Monitor){if(!this.data.ContainsKey(monitorKey)){allMonitorsReadyfalse;break;}// 检查时间窗口if(this.DiffDate(this.data[monitorKey].ResponseTime,cacheDATA.ResponseTime)sm.Period){allMonitorsReadyfalse;break;}}if(allMonitorsReady){// 所有数据就绪触发存储this.UPDATA(sm);}}}}4.3 数据组合与存储当所有 13 个命令的数据都就绪后系统调用UPDATA方法组合数据// Server.cs - UPDATA()privateDictionarystring,stringUPDATA(StorageMonitorsm){Dictionarystring,stringdictionarynewDictionarystring,string();// 遍历 SMonitorResponse 配置foreach(SMonitorResponsesmrinsm.SMonitorResponse){stringsmpValues;// 遍历 SMPValue 映射foreach(SMPValuesmpinsmr.SMPValue){stringcacheKeysmp.DeviceId;smp.Monitorid;if(this.data.ContainsKey(cacheKey)){// 获取对应字段的数据stringvaluethis.data[cacheKey].Data[smp.MonitorResponseID];smpValuesvalue,;}}// 去掉末尾逗号smpValuessmpValues.TrimEnd(,);// 应用 modifiedoutput 处理if(!string.IsNullOrEmpty(smr.Modifiedoutput)){smpValuesthis.OutputValue(smr.Modifiedoutput,smpValues);}dictionary[smr.StorageField]smpValues;}returndictionary;}五、数据处理函数系统内置了多种数据处理函数用于从原始数据中提取有价值的指标5.1 位移包/载荷包处理// 将多个数据包拼接为完整数据// 以载荷包为例ActualNumber Z1(81) Z1(101) Z1(68) 251 个值privatestringOutputLoadPackage(stringvalue){// 原始数据来自 MCYJ307.ActualNumber MCYJ311.Z1 MCYJ312.Z1 MCYJ313.Z1// 第一个值是功图实际点数后续是 250 个载荷数据点returnvalue;// 逗号分隔的字符串}5.2 最大/最小载荷// 从载荷数据中提取最大值privatestringOutputMaxLoad(stringvalue){string[]arrayvalue.Split(,);intdataCountConvert.ToInt32(array[0]);// 第一个元素是实际点数doublemaxValueConvert.ToDouble(array[1]);// 从第二个元素开始是数据for(inti2;idataCount1;i){if(Convert.ToDouble(array[i])maxValue){maxValueConvert.ToDouble(array[i]);}}returnmaxValue.ToString();}5.3 上行程最大电流这是示功图分析的关键指标。系统需要从载荷包中找到最大值和最小值的位置确定上行程区间从最小载荷到最大载荷在电流包中对应区间找到最大电流privatestringOutputMaxUpI(stringvalue){// value 格式点数,载荷1,载荷2,...,载荷N,电流1,电流2,...,电流Nstring[]arrayvalue.Split(,);intdataCountConvert.ToInt32(array[0]);// 第一个元素是采集点数// 提取载荷数据从第 2 个元素开始共 dataCount 个stringloadData;for(inti1;idataCount1;i){loadDataloadDataarray[i],;}// 提取电流数据从第 dataCount2 个元素开始共 dataCount 个stringcurrentData;for(inti0;idataCount;i){currentDatacurrentDataarray[dataCount1i],;}string[]loadArrayloadData.Split(,);string[]currentArraycurrentData.Split(,);// 找到载荷最大值和最小值的索引intmaxIndex0,minIndex0;doublemaxLoadConvert.ToDouble(loadArray[0]);doubleminLoadConvert.ToDouble(loadArray[0]);for(inti0;iloadArray.Length;i){if(Convert.ToDouble(loadArray[i])maxLoad){maxLoadConvert.ToDouble(loadArray[i]);maxIndexi;}if(Convert.ToDouble(loadArray[i])minLoad){minLoadConvert.ToDouble(loadArray[i]);minIndexi;}}// 上行程从最小载荷到最大载荷// 需要考虑循环minIndex 可能在 maxIndex 之后doubleresult0.0;if(maxIndexminIndex){// 正常情况minIndex → maxIndexfor(intiminIndex;imaxIndex;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}}else{// 跨周期情况minIndex → 末尾 → 开头 → maxIndexfor(intiminIndex;icurrentArray.Length;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}for(inti0;imaxIndex;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}}returnresult.ToString();}5.4 下行程最大电流与上行程类似但区间是从最大载荷到最小载荷privatestringOutputMaxDownI(stringvalue){// 提取载荷和电流数据...// 下行程从最大载荷到最小载荷if(maxIndexminIndex){// 正常情况maxIndex → minIndexfor(intimaxIndex;iminIndex;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}}else{// 跨周期情况maxIndex → 末尾 → 开头 → minIndexfor(intimaxIndex;icurrentArray.Length;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}for(inti0;iminIndex;i){if(Convert.ToDouble(currentArray[i])result){resultConvert.ToDouble(currentArray[i]);}}}returnresult.ToString();}六、最终存储结果所有数据处理完成后系统生成一条完整的示功图记录存储到cyj_Dynagraph表中存储字段数据来源处理方式dyn_TestCountMCYJ307.ActualNumber直接取值dyn_DegreeMCYJ307.AtTimes直接取值冲次dyn_TestTimeMCYJ307.AcquisitionTime直接取值dyn_StrokeMCYJ307.Stroke直接取值冲程dyn_MaxLoadSID002载荷包OutputMaxLoad最大值dyn_MinLoadSID002载荷包OutputMinLoad最小值dyn_ImaxSID013电流包OutputMaxCurrent最大值dyn_IminSID013电流包OutputMinCurrent最小值dyn_UpImaxSID002 SID013OutputMaxUpI上行程最大电流dyn_DnImaxSID002 SID013OutputMaxDownI下行程最大电流dyn_PkgDistSID001位移包拼接点数 250 个位移点dyn_PkgLoadSID002载荷包拼接点数 250 个载荷点dyn_PkgDLSID013电流包拼接点数 250 个电流点dyn_PkgGLSID020功率包拼接点数 250 个功率点最终生成的 SQL 插入语句INSERTINTOcyj_Dynagraph(dyn_WellName,dyn_TestCount,dyn_Degree,dyn_TestTime,dyn_Stroke,dyn_MaxLoad,dyn_MinLoad,dyn_Imax,dyn_Imin,dyn_UpImax,dyn_DnImax,dyn_PkgDist,dyn_PkgLoad,dyn_PkgDL,dyn_PkgGL,dyn_ResponseTime)VALUES(西柳10-17井,250,5.2,2024-01-15 10:30:00,3.5,85.6,12.3,45.2,8.1,38.5,42.1,250,1.2,1.5,1.8,...,250,85.6,82.1,78.3,...,250,45.2,43.1,41.5,...,250,12.3,11.8,11.2,...,2024-01-15 10:30:15)七、方案优势优势说明协议兼容无需修改 Modbus 协议完全兼容标准设备配置驱动通过数据库配置即可调整拆分策略无需修改代码数据完整时间窗口机制确保所有子命令数据在合理时间内返回灵活扩展新增监控命令只需配置不影响现有逻辑高并发ThreadPool 任务队列支持多设备并发采集容错机制超时重试 CRC 校验保证数据可靠性数据价值内置多种数据处理函数直接提取关键指标八、适用场景本方案适用于以下场景油井示功图远程采集通过 GPRS/4G 远程采集抽油机示功图数据工业设备大数据包采集任何超过单帧限制的数据采集场景Modbus 协议扩展应用在不修改协议的前提下实现大数据量传输配置驱动的工业物联网系统通过配置表驱动整个采集流程九、配置示例新增一口井的示功图采集只需在数据库中添加配置无需修改代码-- 1. 添加设备INSERTINTODevice(id,name,commid,netaddress)VALUES(CYJ0010,新井-10号,COM001,192.168.1.100);-- 2. 绑定示功图命令13 个INSERTINTODeviceMonitor(deviceid,monitorid,timinginterval)VALUES(CYJ0010,MCYJ307,10),(CYJ0010,MCYJ308,10),(CYJ0010,MCYJ309,10),(CYJ0010,MCYJ310,10),(CYJ0010,MCYJ311,10),(CYJ0010,MCYJ312,10),(CYJ0010,MCYJ313,10),(CYJ0010,MCYJ314,10),(CYJ0010,MCYJ315,10),(CYJ0010,MCYJ316,10),(CYJ0010,MCYJ317,10),(CYJ0010,MCYJ318,10),(CYJ0010,MCYJ319,10);-- 3. 添加存储命令INSERTINTOSMonitor(scyjid,sname,deviceid,device,monitorid,period,storagetable,storageid)VALUES(SCYJ010,示功图,CYJ0010,新井-10号,MCYJ002,3,cyj_Dynagraph,ms_cyyofm);-- 4. 添加 SMPValue 映射复制已有井的配置修改 deviceid 即可INSERTINTOSMPValue(smpid,sid,deviceid,monitorid,monitorresponseid)SELECTsmpid,SCYJ010,CYJ0010,monitorid,monitorresponseidFROMSMPValueWHEREsidSCYJ006;4 条 SQL完成一口新井的示功图采集配置。如需完整源码或技术咨询欢迎交流。