RS485网络的整帧数据收发

 2020-7-28     作者:黄志超     [nemail]    
[lablebox]

  背景:RS485是最常用的工业现场通讯手段,它的传输字节采用了异步串口UART的规范。在通常的工控应用中,需要传输由多个字节组成的数据帧,而RS485并没有对数据帧有任何规范,需要应用程序自己做数据帧的鉴别。


  本文介绍在ESM6800、ESM7000和ESM8000主板上,利用iMX6/7/8串口的9bit RS485模式,实现RS485通讯网络的整帧数据收发的功能。该功能可大大简化应用程序接收线程的复杂性,提高RS485通讯的效率。


  整帧数据拥有固定的数据长度,由地址和数据构成,地址为一个字节,其余都为数据字节,如下图:


RS485网络的整帧数据收发.png


  9bit RS485模式使用了串口固定校验位的功能,定义了地址字节和数据字节,地址字节是指固定校验位始终为1的字节。而数据字节则是指固定校验位始终为0的字节。同时9bit RS485模式实现了一些硬件过滤的功能,在接收的时候,必须要先接收到地址字节才会开始接收数据字节,否则硬件会将收到的数据字节全部过滤掉,通过这种方式降低了设备的负载。所以9bit RS485模式顾名思义,通常使用在RS485模式上面,因为RS485可以作为总线挂接多个设备,在多路设备通讯的情况下通过这种校验方式可以有效的降低设备负载和软件的复杂程度。


  英创工控主板中,能够支持9bit RS485模式的主板和串口如下表,其中ES6801/ES6801L和ESM6800L这三款核心板能够满足低成本的需求,可以考虑作为RS485网络中的Slave端:


主板型号支持9bit RS485模式的串口备注
ES6801(L)ttyS1—ttyS6适合作为Slave
ESM6800(H)ttyS1—ttyS5适合作为Master
ESM6800EttyS1—ttyS6适合作为Master
ESM6800LttyS1—ttyS6适合作为Slave
ESM6802ttyS1—ttyS4适合作为Master
ESM7000ttyS1—ttyS6适合作为Master


  如果要使用9bit RS485模式,需要在程序中进行使能,使能后串口就会进入到该模式中,在发送数据的时候,可以支持两种方式,一种是发送地址字节,另一种是发送数据字节。在接收数据字节的时候,分为master和slave两种模式,这两种模式都需要先接收地址字节,才能够接收数据字节,如果没有接收到地址字节,会自动将数据字节自动全部滤掉。他们的区别在于master模式下,只要接收到地址字节,就会将这之后的数据字节全部接收,并交给应用程序处理。而在slave模式下,需要先设置设备地址,只有接收到的地址字节和设备地址相同时,才会开始接收数据字节。


  master模式下,接收数据示意图:


RS485网络的整帧数据收发-2.png


  Slave模式下,接收数据示意图:


RS485网络的整帧数据收发-3.png


  采用9bit RS485模式,有两个优点,第一点是不需要判断是否接收到地址字节,因为串口要在接收到地址字节(校验位为1)后,才会接收数据字节,特别是在slave模式下,只有当地址字节和设置的设备地址相等时,才会接收数据。第二点是不需要切换校验方式,当串口启用了9bit RS485模式,就可以正常接收所有地址字节和数据字节了,只有在发送地址字节和数据字节的时候需要切换不同的设置,可以减少软件上的操作。


  英创公司在提供的例程Step2_serialtest中封装的串口类CSerial的基础上派生出一个专用于9bit RS485的类CRS485,在这个类中我们增加使能9bit RS485模式的函数,让客户可以直接调用来实现相关功能。


/**
 *    派生用于9bit RS485的类
 *
**/
class CRS485 : public CSerial
{
private:
       //串口模式、设备地址和接收超时时间
       int serial_mode;
       int serial_addr;
 
public:
       //接收数据缓存和长度
       char frame[100];
       int  frame_len;
 
       /**
        *    派生类的构造函数
        *
        *    在构造函数中初始化变量,以及设置9-bit RS485模式下的串口是处于master还是slave模式
        *
        *    参数说明:
        *    mode:值为0对应master模式,值为1对应slave模式
        *    addr:设备地址,大小为8bit,当且仅当mode为1是有效。
        *
       **/
       CRS485(int mode, int addr);
 
       /**
        *    发送9bit RS485整包数据
        *
        *    函数会将地址字节和数据字节填写,并设置为相应的模式一并发送
        *
        *    参数说明:
        *    addr:设备地址,大小为8bit,填入发送数据的地址字节中
        *    Buf:发送的数据字节
        *    len:发送数据字节的长度
        *
        *    返回值说明:
        *    len:成功
        *    -1:失败
        *
       **/
       int send_rs485_frame(char addr, char *Buf, int len);
 
       /**
        *    接收9bit RS485整包数据
        *
        *    函数会阻塞接收指定长度的数据,可以设置超时时间,如果超过超时时间没有接收到指定长度的数据,则返回-1
        *
        *    参数说明:
        *    Buf:接收的数据字节
        *    len:发送数据字节的长度
        *    timeout:超时时间,单位毫秒。如果在超时时间内没有收到指定长度的数据,则返回-1。值为0则不阻塞,读取不到数据立即返回。值为-1则没有超时时间,如果接受不到指定长度数据会一直等待
        *
        *    返回值说明:
        *    成功则返回接收到的数据长度
        *    -1:超时
        *
       **/
       int recv_rs485_frame(char *Buf, int len, int timeout);
 
       /**
        *    继承自CSerial类的接收处理函数
        *
        *    在CSerial类的接收线程中会调用这个函数,可以在函数中调用recv_rs485_frame()函数,并处理接收到的数据字节
        *     
        *
       **/
       int PackagePro();
};


  在类实例化的时候,代入参数就可以决定串口是处于master模式还是slave模式,如果是出于slave模式可以一起代入需要设定的设备地址:


//master模式
class CRS485  m_Serial(0, 0);
 
//slave模式,设备地址为0x55
class CRS485  m_Serial(1, 0x55);



  接收处理的时候,数据的长度通过宏DATA_LEN定义,客户可以在PackagePro()函数中可以定义超时时间,然后调用recv_rs485_frame()函数来接收整包数据,recv_rs485_frame()函数会阻塞,直至收到指定长度的数据,或者到达超时时间才会返回。接收到整包数据后,就可以开始进行数据的处理,在接收线程调中循环调用PackagePro函数:


#define DATA_LEN 10                    // 数据长度
 
// 接收串口数据处理函数
int CRS485::PackagePro()
{
       int i1, timeout;
 
       //设置超时时间,单位毫秒
       timeout = 500;
 
       //调用接收函数来获取指定长度的整包数据
       i1 = recv_rs485_frame(DatBuf, m_DatLen, timeout);
 
       //接收到整包数据,调用处理程序,这里只是简单的打印
       if(i1 != -1)
       {
              printf("frame addr = 0x%x\n", frame[0]);
              printf("frame data = ");
              for(i1=1; i1<DATA_LEN; i1++) {
                     printf("0x%x ", frame[i1]);
              }
              printf("\n");
 
              //处理完数据,清除各个变量,重新设置串口以等待下一包数据
              memset(frame, 0, 100);
              frame_len = 0;
       }
       else
              printf("time out!\n");
 
       return i1;
}


  在线程中的处理,循环调用接收处理函数即可,因为recv_rs485_frame()函数会阻塞,直至收到指定长度的数据,或者到达超时时间才会返回:


int CSerial::ReceiveThreadFunc(void* lparam)
{
       CSerial *pSer = (CSerial*)lparam;
 
       //定义读事件集合
       fd_set fdRead;
       int ret;
       struct timeval     aTime;
 
       while( 1 )
       {
              //接收处理函数
              pSer->PackagePro( pSer->DatBuf, pSer->m_DatLen);
 
       }
 
       printf( "ReceiveThreadFunc finished\n");
       pthread_exit( NULL );
       return 0;
}



  串口在发送的时候,比较简单,直接调用send_rs485_frame()函数,填入需要发送的地址和数据即可,使用下面的代码来测试:


char        addr = 0x55;
char    Buf[2];
 
Buf[0] = 0x55;
Buf[1] = 0xaa;
 
//发送地址字节和数据字节
m_Serial.send_rs485_frame(addr, Buf, sizeof(Buf));


  主板实际输出的波形如下:


RS485网络的整帧数据收发.png


  感兴趣的客户可以和英创的工程师联系,索取完整的测试工程。

[lablebox]