# C# 工控实战:Modbus RTU 大寄存器采集方案,油井示功图分片读取完整实现
Modbus RTU 协议单帧最大只能读取 125 个寄存器,但一张完整的油井示功图需要采集 1015 个寄存器(2030 字节)。本文深入解析一套"拆分采集 + 缓存组合"的实战方案,展示如何通过 13 个监控命令、5 张配置表,实现示功图的完整采集与存储。核心内容包括:架构设计、13 个子命令拆分策略、配置驱动采集流程、数据缓存与组合存储、示功图关键指标提取(最大/最小载荷、上/下行程电流分析)。适用于工业物联网开发、Modbus 协议开发、C# 工控系统开发、远程数据采集等场景。
一、什么是示功图?
示功图(Dynagraph Card)是抽油机在一个冲程周期内,载荷与位移的关系曲线。它是油井工况诊断的核心工具:
- 正常工况:示功图呈规则的平行四边形
- 含沙影响:出现不规则尖峰
- 充满度低:示功图呈刀把状
- 杆断:示功图呈窄条状
一张完整的示功图包含以下数据:
| 数据类型 | 寄存器数量 | 每寄存器字节 | 总字节 |
|---|---|---|---|
| 元数据 | 15 | 2 | 30 |
| 位移数据 | 250 (80+100+70) | 2 | 500 |
| 载荷数据 | 250 (81+101+68) | 2 | 500 |
| 电流数据 | 250 (82+90+78) | 2 | 500 |
| 功率数据 | 250 (85+93+72) | 2 | 500 |
总寄存器数 1015 个(2030 字节),而 Modbus RTU 功能码 03 单帧最多读取 125 个寄存器,无法一次性传输。
二、解决方案架构
2.1 核心设计思想

我们采用**“拆分采集 + 缓存组合”**的方案:
- 拆分采集:将示功图数据拆分为 13 个子命令分别采集
- 数据缓存:每个子命令返回后,数据先缓存到内存
- 就绪检查:所有子命令数据到齐后,触发数据组合
- 组合存储:按配置将数据拼装成完整示功图记录
2.2 13 个监控命令
示功图采集涉及 13 个独立的监控命令:
| 命令 ID | 命令名称 | 功能码 | 起始地址 | 读取数量 | 数据内容 |
|---|---|---|---|---|---|
| MCYJ307 | 功图采集数据 | 3 | 980 | 15 | 冲程、冲次、采集点数、采集时间等元数据 |
| MCYJ308 | 位移1 | 3 | 1000 | 80 | 位移数据包1 |
| MCYJ309 | 位移2 | 3 | 1080 | 100 | 位移数据包2 |
| MCYJ310 | 位移3 | 3 | 1180 | 70 | 位移数据包3 |
| MCYJ311 | 载荷1 | 3 | 1250 | 81 | 载荷数据包1 |
| MCYJ312 | 载荷2 | 3 | 1331 | 101 | 载荷数据包2 |
| MCYJ313 | 载荷3 | 3 | 1432 | 68 | 载荷数据包3 |
| MCYJ314 | 电流1 | 3 | 1500 | 82 | 电流数据包1 |
| MCYJ315 | 电流2 | 3 | 1582 | 90 | 电流数据包2 |
| MCYJ316 | 电流3 | 3 | 1672 | 78 | 电流数据包3 |
| MCYJ317 | 功率1 | 3 | 1750 | 85 | 功率数据包1 |
| MCYJ318 | 功率2 | 3 | 1835 | 93 | 功率数据包2 |
| MCYJ319 | 功率3 | 3 | 1928 | 72 | 功率数据包3 |
关键设计:每个命令独立下发、独立返回、独立解析。系统通过配置表知道这些命令属于同一张示功图。
三、配置驱动:5 张表如何定义示功图采集
3.1 Monitor:命令定义
MCYJ307 | 功图采集数据 | code=3 | dllid=DLL002 | timeout=15 | retrytimes=2
MCYJ308 | 位移1 | code=3 | dllid=DLL002 | timeout=15 | retrytimes=2
MCYJ309 | 位移2 | code=3 | dllid=DLL002 | timeout=15 | retrytimes=2
...
MCYJ319 | 功率3 | code=3 | dllid=DLL002 | timeout=15 | retrytimes=2
dllid=DLL002 将命令绑定到 Modbus 协议。timeout=15 表示 15 秒超时,retrytimes=2 表示超时后重试 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 | 小数
关键字段:
repeat=True:表示该字段在响应中重复出现多次(如载荷包的 81 个数据点)modifiedoutput=小数:表示需要将原始值除以 100 转换为实际值datalen=2:每个数据点占 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_cyyofm
period=3 表示数据等待时间窗口为 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 任务对象(Timeout=15, RetryTimes=2)
↓
commPort.AddNew(dev) 加入任务队列
4.2 数据返回与缓存
每个命令返回后,系统执行以下操作:
- 协议解析:Modbus 协议 DLL 解析响应帧,提取数据点
- 第一级缓存:数据写入
HaoPuServer.ResponseData字典 - 第二级缓存:数据传递给
Comm.Server.data字典 - 就绪检查:遍历 SMonitor 配置,检查所有子命令数据是否到齐
// Server.cs - CacheData()
lock (this.data)
{
// 写入当前命令数据
string cacheKey = cacheDATA.DeviceId + ";" + cacheDATA.MonitorId;
this.data[cacheKey] = cacheDATA;
// 检查存储命令的所有子命令是否就绪
foreach (StorageMonitor sm in enabledSM)
{
if (sm.Monitor.Contains(cacheKey))
{
bool allMonitorsReady = true;
foreach (string monitorKey in sm.Monitor)
{
if (!this.data.ContainsKey(monitorKey))
{
allMonitorsReady = false;
break;
}
// 检查时间窗口
if (this.DiffDate(this.data[monitorKey].ResponseTime,
cacheDATA.ResponseTime) > sm.Period)
{
allMonitorsReady = false;
break;
}
}
if (allMonitorsReady)
{
// 所有数据就绪,触发存储
this.UPDATA(sm);
}
}
}
}
4.3 数据组合与存储
当所有 13 个命令的数据都就绪后,系统调用 UPDATA 方法组合数据:
// Server.cs - UPDATA()
private Dictionary<string, string> UPDATA(StorageMonitor sm)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
// 遍历 SMonitorResponse 配置
foreach (SMonitorResponse smr in sm.SMonitorResponse)
{
string smpValues = "";
// 遍历 SMPValue 映射
foreach (SMPValue smp in smr.SMPValue)
{
string cacheKey = smp.DeviceId + ";" + smp.Monitorid;
if (this.data.ContainsKey(cacheKey))
{
// 获取对应字段的数据
string value = this.data[cacheKey].Data[smp.MonitorResponseID];
smpValues += value + ",";
}
}
// 去掉末尾逗号
smpValues = smpValues.TrimEnd(',');
// 应用 modifiedoutput 处理
if (!string.IsNullOrEmpty(smr.Modifiedoutput))
{
smpValues = this.OutputValue(smr.Modifiedoutput, smpValues);
}
dictionary[smr.StorageField] = smpValues;
}
return dictionary;
}
五、数据处理函数
系统内置了多种数据处理函数,用于从原始数据中提取有价值的指标:
5.1 位移包/载荷包处理
// 将多个数据包拼接为完整数据
// 以载荷包为例:ActualNumber + Z1(81) + Z1(101) + Z1(68) = 251 个值
private string OutputLoadPackage(string value)
{
// 原始数据来自 MCYJ307.ActualNumber + MCYJ311.Z1 + MCYJ312.Z1 + MCYJ313.Z1
// 第一个值是功图实际点数,后续是 250 个载荷数据点
return value; // 逗号分隔的字符串
}
5.2 最大/最小载荷
// 从载荷数据中提取最大值
private string OutputMaxLoad(string value)
{
string[] array = value.Split(',');
int dataCount = Convert.ToInt32(array[0]); // 第一个元素是实际点数
double maxValue = Convert.ToDouble(array[1]); // 从第二个元素开始是数据
for (int i = 2; i < dataCount + 1; i++)
{
if (Convert.ToDouble(array[i]) > maxValue)
{
maxValue = Convert.ToDouble(array[i]);
}
}
return maxValue.ToString();
}
5.3 上行程最大电流
这是示功图分析的关键指标。系统需要:
- 从载荷包中找到最大值和最小值的位置
- 确定上行程区间(从最小载荷到最大载荷)
- 在电流包中对应区间找到最大电流

private string OutputMaxUpI(string value)
{
// value 格式:点数,载荷1,载荷2,...,载荷N,电流1,电流2,...,电流N
string[] array = value.Split(',');
int dataCount = Convert.ToInt32(array[0]); // 第一个元素是采集点数
// 提取载荷数据(从第 2 个元素开始,共 dataCount 个)
string loadData = "";
for (int i = 1; i < dataCount + 1; i++)
{
loadData = loadData + array[i] + ",";
}
// 提取电流数据(从第 dataCount+2 个元素开始,共 dataCount 个)
string currentData = "";
for (int i = 0; i < dataCount; i++)
{
currentData = currentData + array[dataCount + 1 + i] + ",";
}
string[] loadArray = loadData.Split(',');
string[] currentArray = currentData.Split(',');
// 找到载荷最大值和最小值的索引
int maxIndex = 0, minIndex = 0;
double maxLoad = Convert.ToDouble(loadArray[0]);
double minLoad = Convert.ToDouble(loadArray[0]);
for (int i = 0; i < loadArray.Length; i++)
{
if (Convert.ToDouble(loadArray[i]) > maxLoad)
{
maxLoad = Convert.ToDouble(loadArray[i]);
maxIndex = i;
}
if (Convert.ToDouble(loadArray[i]) < minLoad)
{
minLoad = Convert.ToDouble(loadArray[i]);
minIndex = i;
}
}
// 上行程:从最小载荷到最大载荷
// 需要考虑循环(minIndex 可能在 maxIndex 之后)
double result = 0.0;
if (maxIndex > minIndex)
{
// 正常情况:minIndex → maxIndex
for (int i = minIndex; i <= maxIndex; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
}
else
{
// 跨周期情况:minIndex → 末尾 → 开头 → maxIndex
for (int i = minIndex; i < currentArray.Length; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
for (int i = 0; i < maxIndex; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
}
return result.ToString();
}
5.4 下行程最大电流
与上行程类似,但区间是从最大载荷到最小载荷:
private string OutputMaxDownI(string value)
{
// 提取载荷和电流数据...
// 下行程:从最大载荷到最小载荷
if (maxIndex < minIndex)
{
// 正常情况:maxIndex → minIndex
for (int i = maxIndex; i <= minIndex; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
}
else
{
// 跨周期情况:maxIndex → 末尾 → 开头 → minIndex
for (int i = maxIndex; i < currentArray.Length; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
for (int i = 0; i < minIndex; i++)
{
if (Convert.ToDouble(currentArray[i]) > result)
{
result = Convert.ToDouble(currentArray[i]);
}
}
}
return result.ToString();
}
六、最终存储结果
所有数据处理完成后,系统生成一条完整的示功图记录,存储到 cyj_Dynagraph 表中:
| 存储字段 | 数据来源 | 处理方式 |
|---|---|---|
dyn_TestCount |
MCYJ307.ActualNumber | 直接取值 |
dyn_Degree |
MCYJ307.AtTimes | 直接取值(冲次) |
dyn_TestTime |
MCYJ307.AcquisitionTime | 直接取值 |
dyn_Stroke |
MCYJ307.Stroke | 直接取值(冲程) |
dyn_MaxLoad |
SID002(载荷包) | OutputMaxLoad(最大值) |
dyn_MinLoad |
SID002(载荷包) | OutputMinLoad(最小值) |
dyn_Imax |
SID013(电流包) | OutputMaxCurrent(最大值) |
dyn_Imin |
SID013(电流包) | OutputMinCurrent(最小值) |
dyn_UpImax |
SID002 + SID013 | OutputMaxUpI(上行程最大电流) |
dyn_DnImax |
SID002 + SID013 | OutputMaxDownI(下行程最大电流) |
dyn_PkgDist |
SID001(位移包) | 拼接(点数 + 250 个位移点) |
dyn_PkgLoad |
SID002(载荷包) | 拼接(点数 + 250 个载荷点) |
dyn_PkgDL |
SID013(电流包) | 拼接(点数 + 250 个电流点) |
dyn_PkgGL |
SID020(功率包) | 拼接(点数 + 250 个功率点) |
最终生成的 SQL 插入语句:
INSERT INTO cyj_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. 添加设备
INSERT INTO Device (id, name, commid, netaddress)
VALUES ('CYJ0010', '新井-10号', 'COM001', '192.168.1.100');
-- 2. 绑定示功图命令(13 个)
INSERT INTO DeviceMonitor (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. 添加存储命令
INSERT INTO SMonitor (scyjid, sname, deviceid, device, monitorid, period, storagetable, storageid)
VALUES ('SCYJ010', '示功图', 'CYJ0010', '新井-10号', 'MCYJ002', 3, 'cyj_Dynagraph', 'ms_cyyofm');
-- 4. 添加 SMPValue 映射(复制已有井的配置,修改 deviceid 即可)
INSERT INTO SMPValue (smpid, sid, deviceid, monitorid, monitorresponseid)
SELECT smpid, 'SCYJ010', 'CYJ0010', monitorid, monitorresponseid
FROM SMPValue WHERE sid = 'SCYJ006';
4 条 SQL,完成一口新井的示功图采集配置。
如需完整源码或技术咨询,欢迎交流。
更多推荐


所有评论(0)