最近在工业自动化领域遇到一个典型需求:某工厂需要实时监控分布在7个车间的70台中电品牌电量表的用电数据。这些电表都支持MODBUS-RTU协议,通过RS485总线连接。我的任务是开发一个C#系统,实现每10分钟自动采集一次所有电表的电度数,并持久化存储到SQL Server数据库。
这种场景在能耗监测系统中非常常见。传统的人工抄表方式效率低下且容易出错,而自动化采集系统可以:
70台电表采用菊花链式拓扑连接,使用屏蔽双绞线作为传输介质。关键参数配置:
注意:实际施工时要确保A/B线不接反,所有设备波特率设置一致。我们曾因一个电表的波特率被误设为19200导致整个系统无法通信。
系统采用三层架构:
mermaid复制graph TD
A[定时触发器] --> B[MODBUS通信模块]
B --> C[数据解析模块]
C --> D[数据库存储模块]
D --> E[异常处理模块]
中电电量表使用的MODBUS-RTU协议,关键功能码:
电度数通常存储在4xxxx系列的保持寄存器中。以某型号为例:
使用NModbus库简化开发:
csharp复制// 初始化MODBUS主站
using Modbus.Device;
var port = new SerialPort("COM3", 9600, Parity.Even, 8, StopBits.One);
port.Open();
var master = ModbusSerialMaster.CreateRtu(port);
// 读取电度数据
ushort startAddress = 0; // 对应40001
ushort numRegisters = 2; // 32位浮点占2个寄存器
byte slaveId = 1; // 从站地址
float ReadElectricity(byte deviceId)
{
var registers = master.ReadHoldingRegisters(deviceId, startAddress, numRegisters);
byte[] bytes = {
(byte)(registers[0] >> 8),
(byte)(registers[0] & 0xFF),
(byte)(registers[1] >> 8),
(byte)(registers[1] & 0xFF)
};
return BitConverter.ToSingle(bytes, 0);
}
70台设备需要在10分钟内完成一轮采集,考虑:
实现代码:
csharp复制// 使用Timer定时触发
var timer = new System.Timers.Timer(60000); // 60秒
timer.Elapsed += (s, e) => {
Parallel.For(1, 71, deviceId => {
try {
var value = ReadElectricity((byte)deviceId);
SaveToDatabase(deviceId, DateTime.Now, value);
} catch (Exception ex) {
LogError($"设备{deviceId}读取失败: {ex.Message}");
}
});
};
timer.Start();
sql复制CREATE TABLE ElectricityData (
Id BIGINT PRIMARY KEY IDENTITY,
DeviceId INT NOT NULL, -- 电表编号
RecordTime DATETIME2 NOT NULL, -- 记录时间
Kwh FLOAT NOT NULL, -- 电度值
Status TINYINT DEFAULT 0 -- 0正常 1异常
);
CREATE INDEX IX_Device_Time ON ElectricityData(DeviceId, RecordTime);
使用SqlBulkCopy实现高效存储:
csharp复制void SaveToDatabase(List<ElectricityRecord> records)
{
using (var bulkCopy = new SqlBulkCopy(connectionString))
{
bulkCopy.DestinationTableName = "ElectricityData";
bulkCopy.BatchSize = 1000;
bulkCopy.WriteToServer(ConvertToDataTable(records));
}
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 部分设备无响应 | 地址冲突/线路故障 | 1. 检查设备地址 2. 用万用表测AB线电压 |
| 数据跳变异常 | 寄存器地址错误 | 核对电表说明书确认寄存器映射 |
| 通信时好时坏 | 终端电阻未接 | 检查总线两端的120Ω电阻 |
csharp复制float ReadWithRetry(byte deviceId, int maxRetry = 3)
{
for (int i = 0; i < maxRetry; i++)
{
try {
return ReadElectricity(deviceId);
} catch {
if (i == maxRetry - 1) throw;
Thread.Sleep(1000);
}
}
return float.NaN;
}
串口参数调优:
csharp复制port.ReadTimeout = 500; // 读取超时500ms
port.WriteTimeout = 500; // 写入超时500ms
port.ReceivedBytesThreshold = 1;
数据库连接池:
xml复制<connectionStrings>
<add name="MyDB"
connectionString="...;Pooling=true;Max Pool Size=100;..."
providerName="System.Data.SqlClient" />
</connectionStrings>
内存缓存:
csharp复制// 使用MemoryCache暂存最近数据
var cache = MemoryCache.Default;
cache.Add($"Device_{deviceId}", currentValue,
DateTimeOffset.Now.AddMinutes(15));
环境要求:
安装注意事项:
日志配置:
xml复制<log4net>
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="Logs\\System.log" />
<appendToFile value="true" />
<maximumFileSize value="10MB" />
<maxSizeRollBackups value="5" />
</appender>
</log4net>
电表地址规划:
信号质量检测:
csharp复制// 测量AB线电压应在1-5V之间
float voltage = (port.PinStates & SerialPin.Ring) ? 3.3f : 0;
冬季防静电措施:
数据校验技巧:
csharp复制bool IsValidData(float value)
{
return !float.IsNaN(value) &&
value >= 0 &&
value < 100000; // 根据电表量程调整
}
这个系统经过3个月的生产环境运行,数据采集完整率达到99.7%。最大的收获是:工业现场通信必须考虑各种异常情况,完善的日志和重试机制比追求理论性能更重要。下一步计划增加Web界面实现实时监控功能。