串口协议包的接收及解析处理

串口是单片机应用中应用最多的外设之一。很多电子硬件都会提供串口,如蓝牙模块、WIFI模块、串口屏等。如果我们想开发基于串口通信的产品或使用基于串口通信的电子硬件时,都避不开通信协议。每次听到通信协议这个词,总感觉很高大上。这篇文章我就要改变大家对通信协议在串口层次的神秘面纱。

通信协议

通信协议听起来很抽象,实际他就是两个通信设备之间交流的纽带。例如我们两个人互相对话,这就是一个通信的过程,为什么我们可以听懂对方的话,因为我们说的都是汉语,汉语就是我们之间的通信协议。

通信协议一般分为物理层协议层。对于物理层我这里就不介绍了,大家可以根据自己的情况翻阅其他资料,这里主要介绍基于串口的应用层协议。

串口应用层协议

串口应用层协议以下简称为串口协议。在大家购买的各类串口通信的产品时,官方都会提供一份通信协议。总的来说这些协议五花八门,下面我列举几个我见到的通信协议:

DWIN DGUS串口屏 读取内存地址值的指令:

        | Example 5A A5 06 83 20 01 01 78 01 ……
        |          / /  |  |   \ /   |  \     \
        |        Header |  |    |    |   \_____\_ DATA (Words!)
        |     DatagramLen  /  VPAdr  |
        |           Command          DataLen (in Words) 

某指纹模块使其进入休眠模式的指令:

        | Example F5 2C 00 00 00 00 01 F5
        |         /  |  |  |  |   |  |  \     
        |   Header  /   |  |  |   |   \  Tailer
        |     Command   P1 P2 P3  |    CheckSum
        |                         Alawys 0

3D打印机的G代码指令:

        |                (X,Y,Z Command)
        |                / | \   
        |              /   |   \
        |            |     |     |
        | Example G1 X10.0 Y10.0 Z10.0 ;注释
        |         /      \    |    /   |      
        |   Command        \  |  /     Comment
        |                 (Distance)                        

某平衡小车控制指令:

        | Example $0,0,0,0,1,1,AP23.54,AD85.45,VP10.78,VI0.26#
        |        / | \ \ | / /  |  |    |  |    |  |    | |   \     
        |  Header /    \\|//    |  data |  data |  data | data Tailer
        |  move cmd  other cmd  angel P angle D speed P speed I 

以上通信协议前两种属于十六进制格式的通信协议,后面两种属于字符串格式的通信协议。就实际应用而言,十六进制的通信协议格式应用更加广泛。字符串格式的通信协议更倾向于对于字符串的格式有明确标准的场合,如gcode代码。

实际上无论是十六进制格式的通信协议还是字符串格式的通信协议,只是使用过程中给人的直观感受不同而已(勾选16进制发送,不勾选十六进制发送),对于串口硬件传输而言都是一样的:数据一个字节一个字节的传输,这一个字节的数据我们既可以当做十六进制数据处理也可以当做字符数据处理。

对于通信协议我们一般都会包含一些包头、包尾、包长、校验码等必要的信息,以便于解析协议。没有这些标识信息我们怎么知道发过来的数据到底对还是不对,发过来的数据到底是什么含义。

串口数据的接收及协议解析处理

对于串口接收问题前面之前有文章介绍过串口缓存机制的应用。当然这里不应用缓存机制也是完全可行的。这里我们讲解基于不带串口缓存机制的处理。对于串口接收我们最常用的方式就是在串口中断中接收数据。

利用串口接收数据包信息大致分为下面三种情况:

  1. 接收一帧数据,对帧数据进行处理(可以利用串口接收非空中断和串口空闲中断实现)
  2. 中断中边接收边处理存储,并将有效数据存储起来,再对有效数据进行解析。
  3. 将接收到的数据全部存入缓存,从缓存中提取数据并做处理。

1. 接收帧数据

利用串口和ESP8266WIFI模块通信

2. 边接收边处理存储

以对上面平衡小车协议解析为例:

  • 串口数据接收
/* 变量说明 */
#define BUFFER_SIZE 4    //最多缓存4包数据
#define MAX_CMD_SIZE 80  //每包最多包含80个字节的数据

bool g_bStartBitFlag = 0; //开始接收数据包的标志位
char cmdBuffer[BUFFER_SIZE][MAX_CMD_SIZE]; 缓存数组
 int bufindr = 0; //数据包的读取索引
 int bufindw = 0; //数据包的写入索引
 int buflen  = 0; //缓存中数据包的个数
int serial_count = 0; //每包数据中的计数变量

/**
 * @brief  USART3串口中断服务函数
 * @param  none
 * @retval none
 */ 
void USART3_IRQHandler(void)
{
    uint8_t rec;
    if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)  //接收中断
    {
        rec = USART_ReceiveData(USART3);//(USART1->DR); //读取接收到的数据
        if(rec == '$')  //判断接收到的数据是否为包头信息
        {
            g_bStartBitFlag = 1; //数据包开始标志位
            serial_count = 0;    //计数变量
        }
        if(g_bStartBitFlag == 1)
        {
          cmdBuffer[bufindw][serial_count++] = rec; //命令缓存
        }
        if(g_bStartBitFlag == 1 && rec == '#') //接收到包尾数据,接收完成
        {
          g_bStartBitFlag = 0;
          bufindw = (bufindw + 1) & (BUFFER_SIZE - 1);
          buflen += 1;
        }
        if(serial_count >= 80) //接收长度溢出,重新接收
        {
          g_bStartBitFlag = 0;
          serial_count = 0;
        }
    } 
} 
  • 数据包解析:
void protocol_process(void)
{
 if(buflen)
 {
   switch(cmdBuffer[bufindr][1])
   {
     case '0':
       //...
       break;
     case '1':
       //...
       break;
   }

   /* 协议中其他数据的处理 */

   buflen -= 1; //该包数据已分析完缓存中数据包数减1
   bufindr = (bufindr + 1) & (BUFFER_SIZE - 1); //移到下一读取索引位置
 }
}

3. 先缓存后处理

以上面串口屏的协议为例:(该段代码来自于marlin固件)

/* 定义包头及指令信息 */
#define HEADER1 0x5A
#define HEADER2 0xA5
#define CMD_READVAR  0x83
#define CMD_WRITEVAR 0x82

/* 枚举读取数据报文的状态 */
typedef enum
{
    IDLE,
    SEEN_HEADER1,
    SEEN_HEADER2,
    DATA
}rx_datagram_state_t;

/* 定义数据包接收状态的变量,并初始化为空闲状态 */
rx_datagram_state_t rx_datagram_state = IDLE;

/* 协议数据处理函数 */
void processRx()
{
    uint8_t receivedbyte;
    while(serial_available())  //缓存中有数据
    {
        switch(rx_datagram_state)
        {
            case IDLE:
                receivedbyte = serial_read();
                if(HEADER1 == receivebyte)
                {
                    rx_datagram_state = SEEN_HEADER1;
                }
                break;
            case SEEN_HEADER1:
                receivedbyte = serial_read();
                rx_datagram_state = (HEADER2 == receivebyte) ? IDLE : SEEN_HEADER2
                break;
            case SEEN_HEADER2:
                rx_dategram_len = serial_read(); //数据包的长度
                rx_datagram_state = DATA;
                break;
            case DATA:
                if(serial_available() < rx_datagram_len) return;
                uint8_t command = serial_read();
                uint8_t readlen = rx_datagram_len - 1; //command is part of len
                uint8_t tmp[rx_datagram_len - 1];
                unsigned char *ptmp = tmp;
                while(readlen--)
                {
                    receivedbyte = serial_read();
                    *ptmp++ = receivedbyte;
                }
                // mostly we'll get this: 5A A5 03 82 4F 4B -- ACK on 0x82,so discard it.
                if(command == CMD_WRITEVAR && 'O' == tmp[0] && 'K' == tmp[1])
                {
                    rx_datagram_state = IDLE;
                    break;
                }
                /* AutoUpload, (and answer to) Command 0x83 :
                |      tmp[0  1  2  3  4 ... ]
                | Example 5A A5 06 83 20 01 01 78 01 ……
                |          / /  |  |   \ /   |  \     \
                |        Header |  |    |    |   \_____\_ DATA (Words!)
                |     DatagramLen  /  VPAdr  |
                |           Command          DataLen (in Words) */
                if(command == CMD_READVAR)
                {
                    const uint16_t vp = tmp[0] << 8 | tmp[1];
                    const uint8_t dlen = tmp[2] << 1; //Convert to Bytes.(Display works with words)

                    /* 数据处理 */

                    rx_datagram_state = IDLE;
                    break;
                }
            //discard anything else
            rx_datagram_state = IDLE;
        }
    }
}