Dry goods | how to formulate communication protocol and how to analyze protocol data

Source code acquisition

Based on the minimum system board of STM32F103RET6, open source link: fallingStar board

What is a communication protocol?

Communication protocol, also known as communication procedure, refers to an agreement on data transmission control between communication parties. The agreement includes unified provisions on data format, synchronization mode, transmission speed, transmission steps, detection and error correction mode, control character definition and other issues. Both communication parties must abide by it. It is also called link control procedure.

The communication between computers must speak the same language before they can transmit information to each other. When natural data are transmitted on the Internet, each copy must meet certain specifications (i.e. the same language). Otherwise, how can the data sent by China be accepted in the United States?

The provisions of these specifications (Languages) were agreed at the meeting in advance. Generally, we call them "Protocol" (in English, protocol), and this protocol responsible for defining data transmission specifications on the network is collectively referred to as communication protocol.

In a word, both parties do one thing according to the same agreement.

How to define communication protocol

Here, little Feige only briefly introduces the idea and relatively simple communication protocol, so that the little partners can have an understanding and learn to draw inferences from one instance.

Taking MODBUS protocol as an example, let's look at the components of the general protocol:

Take 16 function codes and write multiple register instructions as an example:

These include:

Address code: 1 byte

Function code: 1 byte

Starting address: 2 bytes

Number of registers: 2 bytes (i.e. length of data segment)

Bytes: number of registers * 2

Register value: Data

CRC check: 2 bytes

To sum up, it includes address code, function code, data length, data, check code and other elements

Data sending generally requires the receiver to have an echo to confirm whether the data is received and whether the data is correct, that is, the corresponding PDU and error response above

Imitating the modbus protocol, let's formulate the byte communication protocol. The communication protocol here is the application layer. The serial port itself is a protocol, which is defined in the following format:

Data header (2 bytes) + data length (1 byte) + function code + data + check code (CRC16-MODBUS)

Data header: the commonly used 5a, A5, AA, 55, AA and so on can be used. There is a certain emphasis on why these two values are used. The purpose of adding data header is to confirm that the data packet is what we need. If the data header is disturbed and wrong, it should be easier to identify. From the perspective of binary system

0xaa is 1010 1010

0x55 is 0101

In the principle of communication coding, we should avoid too many repeated 0 or 1 as far as possible, because when your transmission becomes a long 0 / 1, a pulse interference will truncate your data and increase the chance of error code.

In this way, we will take the following data format as an example for analysis:

Data format:

AA 55 07 01 11 23 88 98 8A 9C

CRC16-MODBUS verification calculation

This part is not nonsense. Just look at the code directly. You can use the look-up table method or calculate directly

Look up table method:

#include "crc.h"

static const unsigned char aucCRCHi[] = {
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40
};

static const unsigned char aucCRCLo[] = {
    0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
    0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
    0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
    0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
    0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
    0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
    0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
    0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 
    0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
    0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
    0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
    0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
    0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 
    0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
    0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
    0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
    0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97,
    0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E,
    0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
    0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
    0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
    0x41, 0x81, 0x80, 0x40
};

uint16_t CRC16( unsigned char * pucFrame, uint16_t usLen )
{
    unsigned char           ucCRCHi = 0xFF;
    unsigned char           ucCRCLo = 0xFF;
    int             iIndex;

    while( usLen-- )
    {
        iIndex = ucCRCLo ^ *( pucFrame++ );
        ucCRCLo = ( unsigned char )( ucCRCHi ^ aucCRCHi[iIndex] );
        ucCRCHi = aucCRCLo[iIndex];
    }
    return ( uint16_t )( ucCRCHi << 8 | ucCRCLo );
}

Direct calculation method:

uint16_t CRC_Compute(uint8_t *puchMsg, uint16_t usDataLen) 
{ 
 uint8_t uchCRCHi = 0xFF ; 
 uint8_t uchCRCLo = 0xFF ; 
 uint32_t uIndex ; 
 while (usDataLen--) 
 { 
  uIndex = uchCRCHi ^ *puchMsg++ ; 
  uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ; 
  uchCRCLo = auchCRCLo[uIndex] ; 
 } 
 return ((uchCRCHi) <<8 | (uchCRCLo) ) ; 
}

Protocol analysis

The highlight is how to parse the protocol. In fact, it is also simple. You can make a state machine and constantly switch states

In this section, we use the method of serial port interrupt + queue to analyze the data. In addition, if the MCU has DMA, it is strongly recommended to use DMA to reduce the MCU load. Later, we will talk about the combination of DMA and the CUBEMX configuration. The configuration is relatively simple, so we can directly skip it

Let's first define some related variables, basically macro definitions and structural variables, which are bound by commands and function callback functions

#define UART_RXBUFFER_SIZE 256
#define UART_FRAME_SIZE  2

/*Command code*/
#define CMD_READREG  0x01
#define CMD_WRITEDREG 0x02
#define CMD_CONFIGURE 0x03
#define CMD_IAP     0x04
/*Agreement related*/
#define FRAME_ LEN_ POS 2 / / data frame length index
#define FRAME_ CMD_ POS 3 / / command code index
#define FRAME_HEAD1 0xAA
#define FRAME_HEAD2 0x55

typedef enum {
 frame_head1status = 0,
 frame_head2status = 0x01,
 frame_lenstatus = 0x02,
 frame_datastatus = 0x03
}_E_FRAME_STATUS;



typedef struct {
 uint8_t len;  //Data receiving length
 uint8_t rxbuffer[UART_RXBUFFER_SIZE];//Data receiving cache

}_S_UART_RX;


typedef struct{
 uint8_t queue_head;//Queue header
 uint8_t queue_tail;//End of column
}_S_QUEUE;

typedef struct{
 uint8_t cmd;//command
 uint8_t (*callback_func)(uint8_t cmd, uint8_t *msg, uint8_t len);//Function corresponding to command

}_S_FUNCCALLBACK;

In the case of serial port interruption, we do this:

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
 #if 0
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
 #else
 if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE)!= RESET)
 {
  __HAL_UART_CLEAR_FLAG(&huart1,UART_FLAG_RXNE); //Clear flag
  s_uart_rx[s_queue.queue_tail].rxbuffer[(s_uart_rx[s_queue.queue_tail].len)++] = (uint8_t)(USART1->DR & (uint8_t)0x00FF);
 }
 #endif
  /* USER CODE END USART1_IRQn 1 */
}

In the function function, we mainly encapsulate the following functions, which are written hastily. The core idea is no problem, ha

/***********************************************
*Function name: User_UartIRQInit
*Function function: serial port interrupt initialization
*Entry parameter: CMD
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
void User_UartIRQInit(uint8_t CMD)
{
 if(ENABLE==CMD)
 {
  __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);
 }
 if(DISABLE==CMD)
 {
  __HAL_UART_DISABLE_IT(&huart1,UART_IT_RXNE);
 }
}

In the top-level design, we constantly rotate the serial port task, mainly to judge whether there is data in the queue:

/***********************************************
*Function name: User_UartPoll
*Function: serial port task polling
*Entry parameter: CMD
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_UartPoll(void)
{
 if(0 == s_uart_rx[s_queue.queue_head].len)
 {
  return 0;
 }
 
 if(s_queue.queue_head == s_queue.queue_tail)
 {
  if(s_queue.queue_tail>UART_RXBUFFER_SIZE-1)
  {
   s_queue.queue_tail = 0;
  }
  else
  {
   s_queue.queue_tail++;
  }
 }
 
 for(uint8_t i = 0;i<s_uart_rx[s_queue.queue_head].len;i++)
 {
  User_UartDataParse(s_uart_rx[s_queue.queue_head].rxbuffer[i]);
 }

 s_uart_rx[s_queue.queue_head].len = 0;
 
 if(s_queue.queue_head == s_queue.queue_tail)
 {
  if(s_queue.queue_head>UART_RXBUFFER_SIZE-1)
  {
   s_queue.queue_head = 0;
  }
  else
  {
   s_queue.queue_head++;
  }
 }
 return 1;
}

The highlight of this function is a state machine, which constantly switches the current state by judging different data:

/***********************************************
*Function name: User_UartDataParse
*Function: serial port data analysis
*Entry parameter: NULL
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_UartDataParse(uint8_t data)
{
 static uint8_t e_frame_status = frame_head1status;
 static uint8_t frame_len = 0;
 static uint8_t index = 0;
 static uint8_t rx_bufftemp[256] = {0};
 uint16_t crc_temp = 0;

 
 switch (e_frame_status){
  case frame_head1status: //Judgment header 1
   if(data == FRAME_HEAD1)
   {
    e_frame_status = frame_head2status;
    rx_bufftemp[index] = data;
    index++;
   }
   else
   {
    e_frame_status = frame_head1status;
    index = 0;
    memset(rx_bufftemp,0,256);
   }
   break;
  case frame_head2status://Header data 2
   if(data == FRAME_HEAD2)
   {
    e_frame_status = frame_lenstatus;
    rx_bufftemp[index] = data;
    index++;
   }
   else
   {
    e_frame_status = frame_head1status;
    index = 0;
    memset(rx_bufftemp,0,256);
   }
  break;
  case frame_lenstatus://Judge data length
   if(data>0 && data <= 255)
   {
    e_frame_status = frame_datastatus;
    rx_bufftemp[index] = data;
    index++;
    
   }
   else
   {
    e_frame_status = frame_head1status;
    index = 0;
    memset(rx_bufftemp,0,256);
   }
   break;
  case frame_datastatus://receive data 
   if(index>0 && index <= 255)
   {
    rx_bufftemp[index] = data;
    index++;

    if(index == (rx_bufftemp[FRAME_LEN_POS] + 3))//Judge whether the reception of a frame of data is completed according to the data length
    {
     crc_temp = rx_bufftemp[index-2]+(rx_bufftemp[index-1]<<8);
     if(crc_temp == CRC16(rx_bufftemp+FRAME_CMD_POS,index-5))//CRC verification is the same
     {
       User_UartFrameParse(rx_bufftemp[FRAME_CMD_POS],rx_bufftemp,index);
       e_frame_status = frame_head1status;
       index = 0;
       memset(rx_bufftemp,0,256);
      User_UartFrameParseEnd();
     }
     else//Different
     {
      //Different check values, data errors, execution of error logic, return of error code, etc
     }
    }    
   }
   else
   {
    e_frame_status = frame_head1status;
    index = 0;
    memset(rx_bufftemp,0,256);
   }
  break;
   default:
    e_frame_status = frame_head1status;
    index = 0;
    memset(rx_bufftemp,0,256);
   break;
 }
}

The following is the function function for use. This part mainly uses the way of callback function. The command code is bound to the task. Four groups of commands are randomly defined. The partners can modify them according to their own needs without moving the framework:

/***********************************************
*Function name: User_ReadRegCallback
*Function function:
*Entry parameters:
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_ReadRegCallback(uint8_t cmd, uint8_t *msg, uint8_t len)
{
 uint8_t TestData[5] = {0x01,0x02,0x03,0x04,0x05};
 User_UartFrameSend(cmd,TestData,msg,5);
}
/***********************************************
*Function name: User_WriteRegCallback
*Function function:
*Entry parameters:
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_WriteRegCallback(uint8_t cmd, uint8_t *msg, uint8_t len)
{
 uint8_t TestData[5] = {0x01};
 User_UartFrameSend(cmd,TestData,msg,5);
}
/***********************************************
*Function name: User_ConfigCallback
*Function function:
*Entry parameters:
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_ConfigCallback(uint8_t cmd, uint8_t *msg, uint8_t len)
{
 uint8_t TestData[5] = {0x01,0x02,0x03};
 User_UartFrameSend(cmd,TestData,msg,5);
}
/***********************************************
*Function name: User_IAPCallback
*Function function:
*Entry parameters:
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_IAPCallback(uint8_t cmd, uint8_t *msg, uint8_t len)
{
 uint8_t TestData[5] = {0x01,0x02,0x03,0x04};
 User_UartFrameSend(cmd,TestData,msg,5);
}
_S_FUNCCALLBACK callback_list[]=
{
    {   CMD_READREG,User_ReadRegCallback},
    {   CMD_WRITEDREG,User_WriteRegCallback},
    {   CMD_CONFIGURE,User_ConfigCallback},
    {   CMD_IAP,User_IAPCallback},

};


/***********************************************
*Function name: User_UartFrameParse
*Function: serial port function response function
*Entry parameter: NULL
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
void User_UartFrameParse(uint8_t cmd, uint8_t *msg, uint8_t len)
{
 uint8_t cmd_indexmax = sizeof(callback_list) / sizeof(_S_FUNCCALLBACK);
  uint8_t cmd_index = 0;
 
 for (cmd_index = 0; cmd_index < cmd_indexmax; cmd_index++)
 {
  if (callback_list[cmd_index].cmd == cmd)
  {
   if(callback_list[cmd_index].callback_func != NULL)
   {
    callback_list[cmd_index].callback_func(cmd, msg, len);
   }
  }
 }
}

Then the reply function:

/***********************************************
*Function name: User_UartFrameSend
*Function function: send data packet through serial port
*Entry parameter: NULL
*Return parameter: NULL
*explain:
*Scope: internal
***********************************************/
uint8_t User_UartFrameSend(uint8_t cmd,uint8_t *pdata, uint8_t *msg, uint8_t len)
{
 uint8_t index = 0;
 uint16_t crc_temp = 0;
 
 msg[index++] = FRAME_HEAD1;
 msg[index++] = FRAME_HEAD2;
 msg[index++] = len;
 msg[index++] = cmd;

 for(uint8_t i = 0;i<len;i++)
 {
  msg[index++] = pdata[i];
 }
 
 crc_temp = CRC16(msg+FRAME_CMD_POS,index-3);
 msg[index++] = crc_temp & 0x00FF;
 msg[index++] = crc_temp>>8 & 0x00FF;

 HAL_UART_Transmit(&huart1,msg,index,100);
 
 return  index;
}

It's over here. It's still relatively simple. I hope it can help some small partners who are not confused about data analysis

Added by T2theC on Sat, 05 Mar 2022 01:50:56 +0200