勇哥注: 多线程读写非全双工的硬件资源,是个实现比较困难的任务。 有不服气的同学可以尽管一试。
我们说一个硬件如果是全双工,则表示它的读写是两个信道,可以同时进行。
但是像很多品牌的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
版权声明:本文为博主原创文章,转载请附上博文链接!

