1. 项目背景与需求分析
在工业自动化领域,PLC数据采集是常见的需求场景。最近我在一个工厂设备监控项目中,遇到了需要通过以太网采集欧姆龙PLC DM区数据并持久化存储的需求。这个需求看似简单,但实际开发过程中涉及多个技术环节的配合:
- 工业通信协议:欧姆龙PLC使用专用的FinsTCP协议
- 网络通信:需要通过以太网建立稳定连接
- 数据处理:需要将原始字节数据转换为可读格式
- 数据存储:需要设计合理的数据库结构存储历史数据
传统做法是使用组态软件,但考虑到成本控制和功能定制需求,我决定用C#开发独立的上位机程序。这个方案有以下优势:
- 开发成本低(VS社区版免费)
- 可完全自定义功能
- 便于与现有MES系统集成
- 数据存储格式自主可控
2. 技术方案设计
2.1 整体架构设计
程序采用典型的三层架构:
code复制[表示层] Windows Forms界面
↓
[业务逻辑层] 数据采集处理模块
↓
[数据访问层] ACCESS数据库操作
2.2 关键技术选型
2.2.1 通信协议实现
欧姆龙FinsTCP协议基于标准TCP/IP,但有其特殊的报文格式。经过分析协议文档,主要需要处理:
-
命令帧结构:
- 头部固定为"FINS"ASCII码(0x46,0x49,0x4E,0x53)
- 后续跟4字节命令码
- 地址信息采用特殊编码格式
-
数据区处理:
- DM区地址需要转换为内存偏移量
- 数据按字(16bit)或双字(32bit)读取
2.2.2 数据库设计
考虑到数据特点,ACCESS数据库设计要点:
- 主表包含56个字段(Field1-Field56)
- 添加时间戳字段记录采集时间
- 建立索引提高查询效率
- 设置字段类型为INTEGER(对应PLC的16位整数)
3. 核心代码实现
3.1 FinsTCP通信模块
csharp复制public class FinsTcpClient : IDisposable
{
private readonly TcpClient _client;
private readonly NetworkStream _stream;
private readonly byte[] _nodeInfo = new byte[8];
public FinsTcpClient(string ip, int port = 9600)
{
_client = new TcpClient();
_client.Connect(ip, port);
_stream = _client.GetStream();
InitializeNodeInfo();
}
private void InitializeNodeInfo()
{
// 填充默认节点信息
Array.Fill(_nodeInfo, (byte)0);
_nodeInfo[0] = 0x01; // 本地节点号
_nodeInfo[2] = 0x01; // 目标节点号
}
public ushort[] ReadDMArea(int startAddress, int wordCount)
{
// 构造读取命令帧
var command = BuildReadCommand(startAddress, wordCount);
_stream.Write(command, 0, command.Length);
// 接收响应
var response = new byte[14 + wordCount * 2];
_stream.Read(response, 0, response.Length);
// 解析数据
var result = new ushort[wordCount];
for (int i = 0; i < wordCount; i++)
{
int offset = 14 + i * 2;
result[i] = (ushort)((response[offset] << 8) | response[offset + 1]);
}
return result;
}
private byte[] BuildReadCommand(int address, int count)
{
var buffer = new byte[26];
// 填充FINS头部
buffer[0] = 0x46; // F
buffer[1] = 0x49; // N
buffer[2] = 0x4E; // S
buffer[3] = 0x53; // S
// 填充命令码
buffer[11] = 0x01; // 读取命令
// 填充地址信息
buffer[18] = 0x82; // DM区标识
buffer[19] = (byte)(address >> 8);
buffer[20] = (byte)address;
// 填充读取数量
buffer[24] = (byte)(count >> 8);
buffer[25] = (byte)count;
return buffer;
}
public void Dispose()
{
_stream?.Dispose();
_client?.Dispose();
}
}
3.2 数据库操作模块
csharp复制public class PlcDataRepository : IDisposable
{
private readonly OleDbConnection _connection;
public PlcDataRepository(string dbPath)
{
string connStr = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={dbPath}";
_connection = new OleDbConnection(connStr);
_connection.Open();
}
public void SaveData(DateTime timestamp, ushort[] values)
{
// 确保不超过56个字段
int count = Math.Min(values.Length, 56);
// 构建参数化SQL
var cmdText = new StringBuilder("INSERT INTO PlcData (Timestamp");
for (int i = 1; i <= count; i++)
{
cmdText.Append($", Field{i}");
}
cmdText.Append(") VALUES (@Timestamp");
for (int i = 1; i <= count; i++)
{
cmdText.Append($", @Value{i}");
}
cmdText.Append(")");
using var command = new OleDbCommand(cmdText.ToString(), _connection);
command.Parameters.AddWithValue("@Timestamp", timestamp);
for (int i = 0; i < count; i++)
{
command.Parameters.AddWithValue($"@Value{i+1}", values[i]);
}
command.ExecuteNonQuery();
}
public void Dispose()
{
_connection?.Dispose();
}
}
4. 界面实现与交互逻辑
4.1 主界面设计
采用Windows Forms实现,主要控件:
- 文本框:IP地址、起始地址、读取数量
- 按钮:连接、读取、停止
- DataGridView:数据显示
- StatusStrip:状态信息
4.2 核心交互代码
csharp复制public partial class MainForm : Form
{
private FinsTcpClient _plcClient;
private PlcDataRepository _repository;
public MainForm()
{
InitializeComponent();
// 初始化UI状态
btnStop.Enabled = false;
}
private void btnConnect_Click(object sender, EventArgs e)
{
try
{
_plcClient = new FinsTcpClient(txtIP.Text);
_repository = new PlcDataRepository("Data.accdb");
UpdateStatus("已连接到PLC");
btnConnect.Enabled = false;
btnStop.Enabled = true;
}
catch (Exception ex)
{
MessageBox.Show($"连接失败: {ex.Message}");
}
}
private void btnRead_Click(object sender, EventArgs e)
{
if (!int.TryParse(txtStartAddr.Text, out int startAddr) ||
!int.TryParse(txtCount.Text, out int count))
{
MessageBox.Show("请输入有效的地址和数量");
return;
}
try
{
var data = _plcClient.ReadDMArea(startAddr, count);
DisplayData(data);
_repository.SaveData(DateTime.Now, data);
UpdateStatus($"成功读取{count}个字数据");
}
catch (Exception ex)
{
MessageBox.Show($"读取失败: {ex.Message}");
}
}
private void DisplayData(ushort[] data)
{
dataGridView.Rows.Clear();
for (int i = 0; i < data.Length; i++)
{
dataGridView.Rows.Add(
$"DM{i}",
$"0x{data[i]:X4}",
data[i].ToString());
}
}
private void UpdateStatus(string message)
{
toolStripStatusLabel.Text = $"{DateTime.Now:HH:mm:ss} - {message}";
}
private void btnStop_Click(object sender, EventArgs e)
{
_plcClient?.Dispose();
_repository?.Dispose();
UpdateStatus("连接已断开");
btnConnect.Enabled = true;
btnStop.Enabled = false;
}
}
5. 关键问题与解决方案
5.1 通信超时处理
问题现象:网络不稳定时程序会卡死
解决方案:
csharp复制// 在FinsTcpClient类中添加超时设置
_client.ReceiveTimeout = 5000;
_client.SendTimeout = 5000;
// 修改读取方法加入超时处理
try
{
_stream.Write(command, 0, command.Length);
// ...读取代码...
}
catch (IOException ex) when (ex.InnerException is SocketException se
&& se.SocketErrorCode == SocketError.TimedOut)
{
throw new TimeoutException("PLC通信超时", ex);
}
5.2 大数据量处理
问题现象:当读取大量数据时性能下降
优化方案:
- 采用分批读取策略
- 使用后台线程避免UI冻结
- 添加进度显示
csharp复制private async void btnBatchRead_Click(object sender, EventArgs e)
{
btnBatchRead.Enabled = false;
progressBar.Visible = true;
await Task.Run(() =>
{
int total = 1000; // 总读取点数
int batchSize = 100; // 每批读取量
for (int i = 0; i < total; i += batchSize)
{
var data = _plcClient.ReadDMArea(i, batchSize);
_repository.SaveData(DateTime.Now, data);
// 更新进度
this.Invoke((Action)(() =>
{
progressBar.Value = i * 100 / total;
}));
}
});
progressBar.Visible = false;
btnBatchRead.Enabled = true;
}
5.3 数据库性能优化
问题现象:频繁插入导致速度变慢
优化措施:
- 使用事务批量提交
- 优化索引设计
- 定期压缩数据库
csharp复制public void BatchSave(IEnumerable<ushort[]> dataCollection)
{
using var transaction = _connection.BeginTransaction();
try
{
foreach (var data in dataCollection)
{
SaveData(DateTime.Now, data, transaction);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
private void SaveData(DateTime timestamp, ushort[] values, OleDbTransaction transaction)
{
// ...相同保存逻辑...
command.Transaction = transaction;
command.ExecuteNonQuery();
}
6. 部署与使用指南
6.1 环境准备
-
运行环境:
- .NET Framework 4.0+
- Access Database Engine 2010+
- 网络可达PLC
-
配置步骤:
- 修改App.config中的数据库路径
- 设置PLC IP白名单
- 配置Windows防火墙规则
6.2 操作流程
- 启动应用程序
- 输入PLC IP地址(如192.168.1.100)
- 设置起始地址(如DM1000输入1000)
- 设置读取数量(最大56)
- 点击"连接"按钮
- 点击"读取"获取数据
- 数据自动保存到数据库
6.3 数据查询
ACCESS数据库查询示例:
sql复制-- 查询最新100条记录
SELECT TOP 100 * FROM PlcData ORDER BY Timestamp DESC;
-- 统计Field1的平均值
SELECT AVG(Field1) FROM PlcData
WHERE Timestamp BETWEEN #2023-01-01# AND #2023-12-31#;
7. 扩展与改进方向
-
功能扩展:
- 添加数据导出功能(Excel/CSV)
- 实现定时自动采集
- 增加数据报警功能
-
性能优化:
- 改用SQLite替代Access
- 实现异步IO操作
- 添加内存缓存层
-
界面增强:
- 添加数据趋势图
- 支持多PLC同时监控
- 实现配置保存/加载
-
工业级改进:
- 添加OPC UA支持
- 实现断线重连机制
- 增加数据校验功能
这个项目从需求分析到最终实现,让我对工业通信协议和实时数据采集有了更深入的理解。在实际部署时,有几点特别值得注意:
- 网络稳定性对采集系统至关重要,建议使用工业级交换机
- PLC的DM区地址规划需要提前做好,避免地址冲突
- 数据库要定期维护,建议每周执行一次压缩修复
- 对于关键数据,建议实现双重存储机制
通过这个案例,我们不仅实现了基本的数据采集功能,还建立了一个可扩展的框架,后续可以方便地添加新的功能模块。这种自主开发的方案相比商业组态软件,在灵活性和成本控制方面具有明显优势。