勇哥注: 多线程读写非全双工的硬件资源,是个实现比较困难的任务。 有不服气的同学可以尽管一试。
我们说一个硬件如果是全双工,则表示它的读写是两个信道,可以同时进行。
但是像很多品牌的plc,一般是不能全双工进行通讯的;另外还有串口,我们也不能全双工通讯。
如果你的系统只有一个串口,你如何做到能让多线程读写呢?
这要求你写的代码能实现:
多线程可同时访问,但是取得结果是分时取得。
读写功能必须互斥。
每个线程的访问和取得的结果必须匹配。
勇哥今天写的这个类,经测试可以很好的满足上面的需求。
勇哥是以欧姆龙cp1h做实验的,它带一个网络模块,因此我使用了netMarketing类的tcp fins的访问模块。
netMarketing类库可以网上下载,见 http://47.98.154.65/?id=202
几点说明:
1. PlcReadWrite类内部有一个读写Thread,它负责响应调用者发出的读写任务。
2. 调用者(可以是多个线程)调用Read和Write函数发出的读写指令分别由 ReadCmdList,WriteCmdList 这两个列表缓存
3. 读和写各有一个记时器,sw1,sw2,当时间大于50毫秒,则通知读写Thread进行读写
4. Read()和Write()通过lock进行互斥, 因为它们锁了同一个object,以保证不可能同时读和写
5. 读plc内存是一次读取全部内存区域,调用者的读请求,通过uid保证在ReadCmdList中取到的是自己的结果。
6. 写plc内存根据情况决定是写一个地址,还是一次写入全部内存地址,这主要看调用者的写请求的数量。
7. 这个类使用前需要对这个字段赋值:
StartAddr 读写起始地址;ReadWriteCount 内在地址的数量;
PlcIPAddr plc的IP地址; PlcPort plc的端口号
using netMarketing.automation.plc.Omron.FinsLibrary;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace plcreadmode
{
public class PlcReadWrite
{
public static log plclog = new log();
static List<int> MemBuffer = new List<int>();
/// <summary>
/// 读plc的指令列表
/// </summary>
static List<ReadCmd> ReadCmdList = new List<ReadCmd>();
/// <summary>
/// 写plc的指令列表
/// </summary>
static List<WriteCmd> WriteCmdList = new List<WriteCmd>();
static Thread t1 = new Thread(new ThreadStart(doWork)) { IsBackground = true };
static readonly ManualResetEvent[] mr = new ManualResetEvent[2];
static Stopwatch sw1 = new Stopwatch();
static Stopwatch sw2 = new Stopwatch();
static FinsSocket cp1h = null;
/// <summary>
/// 读写起始地址
/// </summary>
static public ushort StartAddr { get; set; } = 2000;
/// <summary>
/// 读写最大数量
/// </summary>
static public ushort ReadWriteCount { get; set; } = 4;
static public string PlcIPAddr { get; set; } = "192.168.250.1";
static public int PlcPort { get; set; } = 9600;
static bool isRead { get; set; } = false;
private static readonly object readwriteLock = new object();
static PlcReadWrite()
{
if (!t1.IsAlive)
{
cp1h = new FinsSocket();
cp1h.IP = PlcIPAddr;
cp1h.Port = PlcPort;
for (int i=0;i<ReadWriteCount;i++)
{
MemBuffer.Add(0);
}
mr[0] = new ManualResetEvent(false);
mr[1] = new ManualResetEvent(false);
sw1.Start();
sw2.Start();
t1.Start();
}
}
static private void doWork()
{
while (true)
{
mr[0].WaitOne();
if (isRead)
{
readPlc();
}
else
{
writePlc();
}
mr[0].Reset();
}
}
public static void writePlc()
{
if (WriteCmdList.Count > 0 && WriteCmdList.Count <= 3)
{
//单个写
for (int i = 0; i < WriteCmdList.Count; i++)
{
var offsetAddr = WriteCmdList[i].addr;
var data = WriteCmdList[i].writeData;
cp1h.Write(_eMemory.DM, (ushort)(StartAddr + offsetAddr), data);
WriteCmdList[i].writeStatus = 1;
}
}
else
{
var list1 = new List<short>();
for (int i = 0; i < ReadWriteCount; i++)
{
list1.Add(0);
}
var plcdataBuffer = list1.ToArray();
//多个写
if (WriteCmdList.Count >= ReadWriteCount)
{
for (int i = 0; i < ReadWriteCount; i++)
{
plcdataBuffer[i] = (short)WriteCmdList[i].writeData;
}
//如果要写的数量刚好等于读写最大数量ReadWriteCount,则直接写
cp1h.WriteMultiple(_eMemory.DM, StartAddr, plcdataBuffer);
}
else
{
//先读出全部地址数据
cp1h.Read(_eMemory.DM, StartAddr, ref plcdataBuffer);
//再由待写数据更新缓存,再把缓存一次写入地址
for(int i=0;i< ReadWriteCount;i++)
{
plcdataBuffer[WriteCmdList[i].addr] = (short)WriteCmdList[i].writeData;
}
cp1h.WriteMultiple(_eMemory.DM, StartAddr, plcdataBuffer);
}
for (int i = 0; i < WriteCmdList.Count; i++)
{
WriteCmdList[i].writeStatus = 1;
}
}
}
public static void readPlc()
{
var list1 = new List<short>();
for(int i=0;i< ReadWriteCount;i++)
{
list1.Add(0);
}
var plcdataF = list1.ToArray();
cp1h.Read(_eMemory.DM, StartAddr, ref plcdataF);
for (int i = 0; i <4; i++)
{
MemBuffer[i] = plcdataF[i];
}
for(int i=0;i< ReadCmdList.Count;i++)
{
ReadCmdList[i].readStatus = 1;
ReadCmdList[i].readResult = MemBuffer[ReadCmdList[i].addr];
}
}
public int Write(int addr,short data, Guid _id)
{
lock (readwriteLock)
{
isRead = false;
WriteCmdList.Add(new WriteCmd()
{
addr = addr,
id = _id,
writeData = data,
writeStatus = -1
});
var idx = -1;
int js1 = 0;
while (true)
{
if (sw2.ElapsedMilliseconds > 50 && js1 == 0)
{
//通知写plc
mr[0].Set(); sw2.Restart(); ++js1;
}
idx = WriteCmdList.FindIndex(s => s.id == _id && s.writeStatus == 1);
if (idx >= 0)
{
//写成功了,返回1
WriteCmdList.RemoveAt(idx);
plclog.AddLog(new logdata()
{
id = _id,
readOrWrite = readwriteEnum.write,
plcAddr = (short)addr,
recTime = DateTime.Now,
itemCount =WriteCmdList.Count
});
return 1;
}
Thread.Sleep(1);
}
}
}
public int Read(int addr,Guid _id)
{
lock (readwriteLock)
{
isRead = true;
ReadCmdList.Add(new ReadCmd()
{
id = _id,
addr = addr,
readResult = -1,
readStatus = 0
});
var idx = -1;
int js1 = 0;
while (true)
{
if (sw1.ElapsedMilliseconds > 50 && js1 == 0)
{
//通知读plc
mr[0].Set(); sw1.Restart(); ++js1;
}
idx = ReadCmdList.FindIndex(s => s.id == _id && s.readStatus == 1);
if (idx >= 0)
{
//读成功后返回读到的值
var resData = ReadCmdList[idx].readResult;
ReadCmdList.RemoveAt(idx);
plclog.AddLog(new logdata()
{
id = _id,
readOrWrite = readwriteEnum.read,
plcAddr = (short)addr,
recTime = DateTime.Now,
itemCount= ReadCmdList.Count
});
return resData;
}
Thread.Sleep(1);
}
}
}
}
public class WriteCmd
{
public Guid id { get; set; }
public int addr { get; set; } = -1;
public short writeData { get; set; } = 0;
public int writeStatus { get; set; } = 0;
}
public class ReadCmd
{
public Guid id { get; set; }
public int addr { get; set; } = -1;
public int readResult { get; set; } = 0;
public int readStatus { get; set; } = 0;
}
}程序执行后,Random read按钮模拟了两个线程不间断读取plc内存,起始地址2000开始的4个地址。
Random write按钮则开线程往这个4个地址写随机整数。
按钮rnd stop则停止随机读写,并且记录log文件

由log文件可以看到,对同一个地址的读写,总会拉开时间差,保证不会同时进行。
log文件中,最后一列是itemCount,它是ReadCmdList,WriteCmdList 列表的数量,在处理完一次读写后,会从列表删除这些压入的读写指令。它的值必须为0,否则就是bug。

勇哥继续看一下代码的运行效率,我们先看一下cpu占用的情况。
看以看到,程序的独占样本为0。还是不错的。
另外,独占样本%最高的是一个叫Monitor.Enter的函数。它其实是lock锁。
另外一个clr.dll占12.26%,其实是垃圾回收器。

点进去查看,系统已经描红出一些高耗cpu的代码段了。
不过除了这些高耗能的系统对象,程序本身是没啥子问题了。
lock锁会降低效率,这是一个需要优化的地方。

大家如果实际用到项目上去时要做一些修改。
比如:log记录是用于测试的,实际不要使用。
另外,实际上读plc是假设读写都是ok的,没有考虑失败的情况。
这个是要大家再扩展的。
另外,如果有童鞋对此代码有意见,请在本贴留言,大家一起讨论。
源代码下载:
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!


少有人走的路


















