串口通信开发

一开始做串口通信开发时,觉得并不难,无非就是发送,然后等一会,再接收就完事了。其实里面的水很深,特别是在各种设备都有的情况下。我们在整个开发过程中,遇到了以下的几个主要问题:

1、设备出现严重的延迟。

2、接收过程出现数据粘包或截断。

3、多设备共用一个串口。

4、使用RTU的情况,接受到数据传给所有程序处理。

5、采数和反控不能相互影响。

6、多线程并发采数。

 

一、设备出现严重的延迟

正常的设备,在100ms之内就会返回,但某些设备,因为硬件原因,响应指令的速度非常慢,有可能达到10s。

对于这种情况,我们需要延长等待的时间。但直接延长,对于响应快的设备,就不公平。我们可以采用以下的方法:

int count = 0;
while (count < 20)
{
    Thread.Sleep(WRITE_READ_INTERVAL);
    //接收数据
    //如果数据合法,跳出循环
}

具体代码我们结合第二个问题给出。

二、接收过程出现数据粘包或截断

调用一次接收函数,收到的数据不一定是完整的,特别是字节数比较多的情况。我们需要把几次接收到的内容,拼成一个包,再判断这个包是否合法的。根据第一、第二个问题,我们给出以下代码解决:

int count = 0;//循环次数
byte[] total = new byte[1024];//总接收到的内容
int pointer = 0;//新内容的起始指针
while (count < 20)//循环20次,一次100ms,也就是最长等待2s
{
    Thread.Sleep(WRITE_READ_INTERVAL);
    byte[] temp = ReceiveByte(control);//接收数据,存放在一个临时变量中
    if (temp != null && temp.Length > 0)
    {
        Array.Copy(temp, 0, total, pointer, temp.Length);//合并数据
        pointer += temp.Length;

        if (ModbusHelper.CheckRecvValid(total, pointer))//判断数据是否合法,一旦合法,说明一条完整的数据已经接收完成
        {
            break;
        }
    }

    count++;
}
if (pointer < 5)
{
    return null;
}
byte[] recv = new byte[pointer];
Array.Copy(total, recv, pointer);

return recv;

上述方法是专门针对Modbus协议的,如果是其他协议的设备,还需要修改判断合法的条件。

三、多设备共用一个串口

一开始,我们给每一台设备分配一个串口,这台设备负责串口的打开、传数和关闭等操作。但多设备共同串口的情况下,这种方法就行不通了。串口不再属于某一台设备。基于这种情况,我们把串口从设备类里抽离出来,转而使用一个串口管理类去操作串口。

串口管理类的示例代码如下:

private static Dictionary<string, ComPortHelper> ComPortDict = new Dictionary<string, ComPortHelper>();//串口字典

public static bool IsInit(string PortName)//串口是否就绪
{
    if (ComPortDict.ContainsKey(PortName))
    {
        return ComPortDict[PortName].IsInit();
    }
    return false;
}

public static void Send(string PortName, byte[] cmd)//发送数据
{
    long nowTime = DateTime.Now.Ticks;

    if (ComPortDict.ContainsKey(PortName))
    {
        ComPortDict[PortName].Send(cmd);
    }
    else
    {
        //初始化之后再进行发送
    }
}

其基本思路很简单,就是使用一个字典记录串口,设备在每次操作串口时,在字典里寻找,如果能找到,执行操作,如果找不到,就先初始化串口。

 

共用串口需要注意两个问题:

(1)从机地址必须不一样。

(2)对于某些设备,上一个设备接收完,需要等一会再发送,才能接收成功。还有一种办法是用其他设备把这些共用串口的设备隔开,这样他们有休息的时间,也可以接收成功。

四、使用RTU的情况,接受到数据传给所有程序处理

正常情况下,工控机与设备的通信流程是这样的:

设备1发送数据

设备1接收数据,处理

设备2发送数据

设备2接收数据,处理

……

但在使用RTU的时候,这种方法就不适用了。如果添加了几个平台,由于接收数据是异步的,程序1可能接收到平台2的数据,而且,程序1接收之后,程序2不会再接收到数据,那数据就丢失了。

对于这种情况,我们使用复制数据的方法。如果是同一个串口接收到的数据,复制给其他共用串口的设备。

if (ComPortDict.ContainsKey(PortName))
{
    byte[] recv = ComPortDict[PortName].Receive();//接收数据,保存到临时变量

    if (recv != null && recv.Length != 0)//数据不为空
    {
        ComDataDict[PortName] = recv;//把数据保存起来
        ComCounterDict[PortName].count = 0;//重置计数器
        return recv;
    }
    else//数据为空
    {
        ComCounterDict[PortName].count++;//计数器加1
        if (ComCounterDict[PortName].count < ComCounterDict[PortName].total)//如果计数超过了共用这个串口的设备,说明这一次的通信确实失败了
        {
            return ComDataDict[PortName];
        }
        else
        {
            ComDataDict[PortName] = recv;//使用上一次保存的数据
            ComCounterDict[PortName].count = 0;
            return recv;
        }
    }
}
return null;

这里我们需要注意:

(1)通信是有可能失败的,不能一直使用保存起来的数据。需要使用一个计数器,在所有共用串口的设备都取完数后,就重置这个计数器。

(2)可以看出,这种方法在处理第三个问题的时候,是有冲突的。所以我们需要分开处理,第三个问题和第四个问题使用不同的处理方法。

五、采数和反控不能相互影响

采集数据是在一个线程里面循环完成的。用户随时会插入一个反控操作。执行反控时,发送和接收必须跟采集区分开,一个工作完成了才能做另外一个工作。我们采用以下方法:

采集和反控都放在一个类里面,然后一个工作正在执行的时候,使用Monitor,让另外一个工作进行等待。

六、多线程并发采数

在处理第六个问题的时候,我们推翻了第五个问题的方法。我们发现,Monitor会锁住所有线程,而不是一个线程内容的锁定。我们把每台设备的采集和反控放到不同的类里面,每个类独立占据一个线程进行运行。如果没有Monitor,线程运行正常,各个线程互不干扰。但Monitor的加入,破坏了这种秩序。一个线程使用了Monitor.Enter,其他线程都不动了,即使他们锁的对象并不一样。

对于这个问题,我们选择抛弃Monitor,而使用另外一种方法来区分采数和反控。方法流程图如下图所示:

这里需要指出的是,这种方法对于多设备共用串口的情况并不适用。所以在多设备共用串口时,我们没办法使用多线程采数。

 

 

 

 

  • 6
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值