Implementation Method of Custom UART Communication Protocol

When we study microcontrollers, the first thing we may encounter is lighting up (GPIO), and then comes the serial port (UART).

The serial port is a commonly used communication interface and is a necessary knowledge to master in embedded systems. However, I have found that many friends only know how to use the serial port to output or print some data, but do not know how to use the serial port for data transmission and communication.

Here I will share with you the serial communication protocol, custom communication protocol, and the principles of implementation.

What is a Communication Protocol?

A communication protocol is not difficult to understand; it is a protocol that must be followed for communication between two (or more) devices.

The explanation from Baidu Baike:

A communication protocol refers to the rules and agreements that both parties must follow to complete communication or service. Multiple data communication systems connected through communication channels and devices in different geographical locations must have a common language to work together to achieve information exchange and resource sharing. What to communicate, how to communicate, and when to communicate must follow some mutually acceptable rules. This rule is the communication protocol.

Many readers must have purchased some modules based on serial communication. Many of the modules on the market use custom communication protocols, some of which are relatively simple, while others are a bit more complex.
Here is a very simple example of a serial communication protocol: for instance, transmitting only a temperature value, which consists of only three bytes:
  • Frame Header
  • Temperature Value
  • Frame Tail

5A

One-byte value

3B

Doesn’t this seem very simple? It is also a type of communication protocol.
However, this communication protocol is relatively simple in application (between two devices), and it has many drawbacks.

Problems with Simple Communication Protocols

The aforementioned three-byte communication protocol is understood by everyone. Although it can communicate and transmit data, it has a series of problems.

For example: How do you determine who to transmit to when multiple devices are connected on a single bus (like RS-485)? (No device information)

Additionally: In an interference environment, can you ensure the correctness of the transmitted data? (No verification information)

Furthermore: If I want to transmit multiple data of uncertain length, what should I do? (No length information).

Friends who have done custom communication will understand this series of problems.

Therefore, more ‘protocol information’ must be agreed upon in communication protocols to ensure the integrity of communication.

Common Contents of Communication Protocols

Serial-based communication protocols usually cannot be too complex due to serial communication speed, anti-interference capability, and other factors. Compared to TCP/IP communication protocols, it is a lightweight communication protocol.

Thus, in serial communication, besides some common communication protocols (like Modbus, MAVLink), engineers often customize communication protocols according to their project needs.
Below, I will briefly describe some key points of common custom communication protocols.
Implementation Method of Custom UART Communication Protocol
(Note: These are some common protocol contents; the actual protocol contents may vary in different situations)
1. Frame Header
The frame header is the beginning of a frame of communication data.
Some communication protocols have only one frame header, while others have two, such as: 5A, A5 as frame headers.
Implementation Method of Custom UART Communication Protocol
2. Device Address/Type
The device address or type is usually used between multiple devices to facilitate distinguishing different devices.
Implementation Method of Custom UART Communication Protocol
This situation requires describing various device type information in the protocol or appendix to facilitate developers in coding and querying.
Of course, some fixed communication between two devices may not have this option.
3. Command/Instruction
Commands/instructions are quite common; generally, different operations are distinguished by different commands.
Implementation Method of Custom UART Communication Protocol
For example: Temperature: 0x01; Humidity: 0x02;
4. Command Type/Function Code
This option further supplements the command. For example: read or write operations.
Implementation Method of Custom UART Communication Protocol
For example: Read Flash: 0x01; Write Flash: 0x02;
5. Data Length
The data length option may be placed before the device address in some protocols, counting command information as part of the ‘length’.
This is mainly to facilitate the protocol (receiving) parsing, to count the length of received data.
Implementation Method of Custom UART Communication Protocol
For example: Sometimes you need to transmit a valid piece of data, sometimes you need to transmit multiple valid data, or even transmit an array of data. In this case, the transmission of a frame of data is variable-length data, which must have [ data length ] to constrain it.
Some lengths are one byte, with a range of: 0x01 ~ 0xFF, while others may require more to be transmitted at once, using two bytes, with a range of 0x0001 ~ 0xFFFFF.
Of course, some communication lengths are fixed (for example, only transmitting temperature and humidity data), and their protocols may not have this option.
6. Data
Data does not need further description; it is the actual data you are transmitting, such as temperature: 25℃.
7. Frame Tail
Some protocols may not have a frame tail; this should be an optional part.
8. Checksum
The checksum is a relatively important content; generally, more formal communication protocols have this option. The reason is simple: communication is easily affected by interference or other reasons, leading to transmission errors.
If there is a checksum, it can effectively prevent data transmission errors.
Implementation Method of Custom UART Communication Protocol
There are many ways to calculate the checksum, with checksum and CRC checks being relatively common methods used in custom protocols.

Additionally, some protocols may place the checksum second to last, with the frame tail at the last position.

Code Implementation of Communication Protocol

There are many ways to implement custom communication protocols in code. How to say, ‘All roads lead to Rome’; you just need to write implementation code according to your protocol.

Of course, while implementing, you need to consider the actual situation of your project. For example, if there is a lot of communication data, you may need to use a message queue (FIFO). Additionally, if the protocol is complex, it is best to encapsulate structures, etc.

Below, I will share some code I have used before. It may not describe more details, but some ideas can be referenced.

1. Message Data Sending

(1) Directly Send Each Byte Through the Serial Port

This is something even beginners can understand. Here is an example from a previous DGUS serial screen:

#define DGUS_FRAME_HEAD1          0xA5                     //DGUS screen frame header 1#define DGUS_FRAME_HEAD2          0x5A                     //DGUS screen frame header 2
#define DGUS_CMD_W_REG            0x80                     //DGUS write register instruction#define DGUS_CMD_R_REG            0x81                     //DGUS read register instruction#define DGUS_CMD_W_DATA           0x82                     //DGUS write data instruction#define DGUS_CMD_R_DATA           0x83                     //DGUS read data instruction#define DGUS_CMD_W_CURVE          0x85                     //DGUS write curve instruction
/* DGUS register addresses */#define DGUS_REG_VERSION          0x00                     //DGUS version#define DGUS_REG_LED_NOW          0x01                     //LED backlight brightness#define DGUS_REG_BZ_TIME          0x02                     //Buzzer duration#define DGUS_REG_PIC_ID           0x03                     //Display page ID#define DGUS_REG_TP_FLAG          0x05                     //Touch coordinates update flag#define DGUS_REG_TP_STATUS        0x06                     //Coordinate status#define DGUS_REG_TP_POSITION      0x07                     //Coordinate position#define DGUS_REG_TPC_ENABLE       0x0B                     //Touch enable#define DGUS_REG_RTC_NOW          0x20                     //Current RTC
//Write one byte of data to the specified register of DGDS screenvoid DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data){  DGUS_SendByte(DGUS_FRAME_HEAD1);  DGUS_SendByte(DGUS_FRAME_HEAD2);  DGUS_SendByte(0x04);
  DGUS_SendByte(DGUS_CMD_W_REG);                 //Instruction  DGUS_SendByte(RegAddr);                        //Address
  DGUS_SendByte((uint8_t)(Data>>8));             //Data  DGUS_SendByte((uint8_t)(Data&0xFF));}
//Write one byte of data to the specified address of DGDS screenvoid DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data){  DGUS_SendByte(DGUS_FRAME_HEAD1);  DGUS_SendByte(DGUS_FRAME_HEAD2);  DGUS_SendByte(0x05);
  DGUS_SendByte(DGUS_CMD_W_DATA);                //Instruction
  DGUS_SendByte((uint8_t)(DataAddr>>8));         //Address  DGUS_SendByte((uint8_t)(DataAddr&0xFF));
  DGUS_SendByte((uint8_t)(Data>>8));             //Data  DGUS_SendByte((uint8_t)(Data&0xFF));}
C
(2) Send via Message Queue

On the basis above, use a buffer to hold the message, then ‘package’ it into the message queue and send it out via the message queue method (FIFO).

static uint8_t  sDGUS_SendBuf[DGUS_PACKAGE_LEN];
//Write one byte of data to the specified register of DGDS screenvoid DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data){  sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //Frame header  sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;  sDGUS_SendBuf[2] = 0x06;                       //Length  sDGUS_SendBuf[3] = DGUS_CMD_W_CTRL;            //Instruction  sDGUS_SendBuf[4] = RegAddr;                    //Address  sDGUS_SendBuf[5] = (uint8_t)(Data>>8);         //Data  sDGUS_SendBuf[6] = (uint8_t)(Data&0xFF);
  DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);  sDGUS_SendBuf[7] = sDGUS_CRC_H;                //Checksum  sDGUS_SendBuf[8] = sDGUS_CRC_L;
  DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);}
//Write one byte of data to the specified address of DGDS screenvoid DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data){  sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //Frame header  sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;  sDGUS_SendBuf[2] = 0x07;                       //Length  sDGUS_SendBuf[3] = DGUS_CMD_W_DATA;            //Instruction  sDGUS_SendBuf[4] = (uint8_t)(DataAddr>>8);     //Address  sDGUS_SendBuf[5] = (uint8_t)(DataAddr&0xFF);  sDGUS_SendBuf[6] = (uint8_t)(Data>>8);         //Data  sDGUS_SendBuf[7] = (uint8_t)(Data&0xFF);
  DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);  sDGUS_SendBuf[8] = sDGUS_CRC_H;                //Checksum  sDGUS_SendBuf[9] = sDGUS_CRC_L;
  DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);}
C
(3) Use ‘Structure’ Instead of ‘Array SendBuf’

Structures are more convenient for referencing and managing than arrays; therefore, using structures is a higher-level and more practical method compared to arrays. (Of course, if there are many members, using temporary variables can lead to excessive stack usage.)

For example:

typedef struct{  uint8_t  Head1;                 //Frame header 1  uint8_t  Head2;                 //Frame header 2  uint8_t  Len;                   //Length  uint8_t  Cmd;                   //Command  uint8_t  Data[DGUS_DATA_LEN];   //Data  uint16_t CRC16;                 //CRC Checksum}DGUS_PACKAGE_TypeDef;
C

(4) More Other Methods

There are many ways to send data via serial port, such as using DMA instead of the message queue method.
2. Message Data Receiving
Serial message reception is usually done through serial interrupt reception, although there are rare cases where polling is used to receive data.
(1) Regular Interrupt Reception
Still using the DGUS serial screen as an example, here is a simple and common interrupt reception method:
void DGUS_ISRHandler(uint8_t Data){  static uint8_t sDgus_RxNum = 0;                //Count  static uint8_t sDgus_RxBuf[DGUS_PACKAGE_LEN];  static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
  sDgus_RxBuf[gDGUS_RxCnt] = Data;  gDGUS_RxCnt++;
  /* Check Frame Header */  if(sDgus_RxBuf[0] != DGUS_FRAME_HEAD1)       //Received Frame Header  {    gDGUS_RxCnt = 0;    return;  }  if((2 == gDGUS_RxCnt) && (sDgus_RxBuf[1] != DGUS_FRAME_HEAD2))  {    gDGUS_RxCnt = 0;    return;  }
  /* Determine Frame Data Length */  if(gDGUS_RxCnt == 3)  {    sDgus_RxNum = sDgus_RxBuf[2] + 3;  }
  /* Received Complete Frame Data */  if((6 <= gDGUS_RxCnt) && (sDgus_RxNum <= gDGUS_RxCnt))  {    gDGUS_RxCnt = 0;
    if(xDGUSRcvQueue != NULL)                    //Parsing successful, add to queue    {      xQueueSendFromISR(xDGUSRcvQueue, &sDgus_RxBuf[0], &xHigherPriorityTaskWoken);      portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);    }  }}
C
(2) Add Timeout Detection

Receiving data may result in half-received data, and if the interrupt is interrupted for some reason, timeout detection is also very necessary.

For example: Use an extra MCU timer to handle a timeout count. When receiving data, start timing; if no next data is received within 1ms, discard this packet (the previously received).

static void DGUS_TimingAndUpdate(uint16_t Nms){  sDGUSTiming_Nms_Num = Nms;  TIM_SetCounter(DGUS_TIM, 0);                   //Set count value to 0  TIM_Cmd(DGUS_TIM, ENABLE);                     //Start timer}
void DGUS_COM_IRQHandler(void){  if((DGUS_COM->SR & USART_FLAG_RXNE) == USART_FLAG_RXNE)  {    DGUS_TimingAndUpdate(5);                     //Update timing (prevent timeout)    DGUS_ISRHandler((uint8_t)USART_ReceiveData(DGUS_COM));  }}
C
(3) More
Similar to sending, there are many ways to implement receiving, such as using structures. However, all methods need to be coded according to your actual needs.

Final Words

All the contents of the custom protocol are for reference only, and the actual number of bytes used depends on your actual needs.

Custom communication protocols based on serial ports vary greatly, depending on factors such as MCU processing capability, number of devices, communication content, etc.

Some may only require a very simple communication protocol to meet the requirements, while others may need a more complex protocol to satisfy.

Finally, two points to emphasize:

First, the examples above are not complete codes (some details are not described), mainly to provide everyone with learning about this programming idea or implementation method.

Second, a good communication protocol code must have certain fault tolerance handling, such as: send completion detection, receive timeout detection, data error detection, etc. Therefore, the above code is not complete.

Implementation Method of Custom UART Communication Protocol

END

Source: strongerHuang
Copyright belongs to the original author. If there is any infringement, please contact for deletion.
Recommended Reading
Japan’s Operating System Almost Dominated the World…
Understanding the Difference Between hex, bin, and axf Files in One Article
No Foreign Authorization Required! Fully Autonomous Design of Domestic CPU Milestone is Here~

→Follow to Avoid Getting Lost←

Leave a Comment