C# 勇哥关于多线程读写plc内存的研究

勇哥注:
多线程读写非全双工的硬件资源,是个实现比较困难的任务。
有不服气的同学可以尽管一试。


我们说一个硬件如果是全双工,则表示它的读写是两个信道,可以同时进行。

但是像很多品牌的plc,一般是不能全双工进行通讯的;另外还有串口,我们也不能全双工通讯。

如果你的系统只有一个串口,你如何做到能让多线程读写呢?

这要求你写的代码能实现:

  1.   多线程可同时访问,但是取得结果是分时取得。

  2.  读写功能必须互斥。

  3. 每个线程的访问和取得的结果必须匹配。



勇哥今天写的这个类,经测试可以很好的满足上面的需求。

勇哥是以欧姆龙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文件

image.png


由log文件可以看到,对同一个地址的读写,总会拉开时间差,保证不会同时进行。

log文件中,最后一列是itemCount,它是ReadCmdList,WriteCmdList 列表的数量,在处理完一次读写后,会从列表删除这些压入的读写指令。它的值必须为0,否则就是bug。

image.png


勇哥继续看一下代码的运行效率,我们先看一下cpu占用的情况。

看以看到,程序的独占样本为0。还是不错的。

另外,独占样本%最高的是一个叫Monitor.Enter的函数。它其实是lock锁。

另外一个clr.dll占12.26%,其实是垃圾回收器。

image.png

点进去查看,系统已经描红出一些高耗cpu的代码段了。

不过除了这些高耗能的系统对象,程序本身是没啥子问题了。

lock锁会降低效率,这是一个需要优化的地方。

image.png


大家如果实际用到项目上去时要做一些修改。

比如:log记录是用于测试的,实际不要使用。

另外,实际上读plc是假设读写都是ok的,没有考虑失败的情况。

这个是要大家再扩展的。

另外,如果有童鞋对此代码有意见,请在本贴留言,大家一起讨论。



源代码下载:


支付5元或购买VIP会员后,才能查看本内容!立即支付升级会员查询订单


--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2025年4月    »
123456
78910111213
14151617181920
21222324252627
282930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864