Modbus is a serial communication protocol, which is widely used in industry. There is a lot of information about Modbus on the Internet, so I won’t go into it here. At the beginning, I read the introduction of Modbus. There were hundreds of pages of protocol introduction, various commands and applications of link layer. After a few days of reading, I became more confused and less able to use it.

Finally, I felt Modbus protocol was not so complicated after the successful transplantation on the single chip microcomputer. If you just started to learn, there is no need to understand every function in the Modbus protocol. Think of it as a simple serial port protocol, using only the simplest of commands. After familiar with the other functions slowly understand.

The following is to understand Modbus protocol from the MCU serial communication perspective, and how to transplant the protocol to MCU.

Let’s look at the Modbus protocol

Broadly speaking, the protocol consists of four parts: address, function, data, and verification.

Address 1 byte, that is, the device address range is 0-255.

The function code is also a command and is also a byte in the range of 0-255.

Data bits have different lengths in different cases.

The check bit is the CRC check.

Let’s see what the function codes are

The commonly used function code is the table above these, can be understood as a number represented by a command. When transplanted to the single chip microcomputer, 03, 06, 16 these three commands are enough.

There are reading coils, writing single coils, writing single registers, etc. What is a coil? What is a register? What does all this mean?

The simple understanding of coil is bit operation. For example, the single-chip microcomputer controls the relay output of 8, in order to facilitate the state of the relay, it uses 8 bits to represent the state of 8 relays, such as 0 indicates the relay disconnect, 1 indicates the relay pull. In this way, 0x00 means that all 8 relays are disconnected, and 0xFF means that all 8 relays are pulled in.

Registers are byte operations. For example, when the sensor collects temperature, it uses one byte to indicate the current temperature. For example, the current temperature of 28℃ is denoted by 0x1C.

If you do not understand the meaning of the register and the coil, do not care about it. Consider it as a command. When used in the single chip microcomputer, the three commands 03, 06 and 16 can meet the basic needs.

The number of readings can be set. For example, there are 8 groups of temperature sensors to collect data. To read the temperature values, you can read them in a group or read multiple values at a time.

Take a look at the 03 command format

The request is the data sent by the single chip microcomputer host, and the normal response is the data returned by the slave machine when the command format sent by the host is correct. When the data sent by the host cannot be correctly identified by the slave machine, the slave machine should return abnormal response data to tell the host that the command sent is incorrect.

Here is the meaning of each bit in the command. It is to collect the values of 8 groups of temperature sensors. If a slave machine has 8 temperature sensors, the address of the slave machine is defined as 0x01, which can be defined according to the actual project. The function code is 0x03, and the Modbus specified function code is used here, which means to read multiple registers. The starting address is two bytes, indicating the number of temperature sensors to be read. The number of registers is also two bytes, indicating the number of temperature sensors to be read. Since there are only eight temperature sensors, the starting address range is 0x0000 —- 0x0007. The number of registers ranges from 0x0001 to 0x0008. At least one register and at most eight registers must be read. Finally, the CRC check, the specific CRC check mode here do not care about, when using the direct call check function line.

Note that when requesting data, you send the starting address and the number of bytes requested. When returning data, you do not send the address of the request, only the number of register bytes sent.

For example, to read the value of the first temperature sensor, the request data format is as follows:

Secondary station address Function code Start address High start address Low register number High register number low CRC check High CRC check low

0x01             0x03            0x00              0x00                   0x00                   0x01                 xx                   xx

Starting at address 0, read the value of 1 register, that is, read the value of the first temperature sensor.

The normal response returns data in the following format

Slave station address Function Code Number of bytes Number of registers High register number of registers low CRC check High CRC check low

0x01              0x03      0x02            0x00                  0x1E                  XX                      XX

The register value is 0x001E, and the decimal number corresponding to 0x001E is 30, indicating that the temperature value of the first temperature sensor is 30 ° C.

So when is the exception response used? If the request data is sent to read the value of the 9th temperature sensor, and the machine finds that there is no 9th sensor after receiving the data, it means that the address value sent by the host is out of range, so the slave machine will send an abnormal response to the host at this time. Common exception response codes are as follows

According to the exception response code, if the address value is not in the range, the exception code is 0x02. Modbus stipulates that when the exception response is returned, the error code value is the value of the function code plus 0x80. The current function code is 0x03, so the error code value returned is 0x83 and the error code value returned is 0x02.

Request data:

Secondary station address Function code Start address High start address Low register number High register number low CRC check High CRC check low

0x01             0x03            0x00              0x09                   0x00                   0x01                 xx                   xx

Abnormal response:

Secondary address error code Exception code CRC check

0x01            0x83         0x02    xx

Here’s another example of reading multiple register values:

Write to a single hold register (0x06). The communication format is as follows:

The following is an example of the communication:

It makes sense to see that the request command to write a single hold register is identical to the normal response command. The value of error code is the value of function code plus 0x80. The current function code is 0x06, so the value of error code is 0x86.

Let’s see 16 (0x10) write multiple hold registers. Writing multiple save registers is basically the same as reading multiple registers, except one is read and one is write.

The value of the error code is the value of the function code plus 0x80. The current function code is 0x10, so the value of the error code is 0x90.

The following is an example of the communication:

The response command only returns the number of registers written, not the value of the register written, which is different from writing to a single register.

Through the above analysis of Modbus will have a general understanding, it is not as complex as thought.

Let’s take a look at how these three commands work in code.

First look at the serial port to send and receive code implementation

#include "uart.h" #include "stdio.h" #include "main.h" u8 ReceiveBuf[MaxDataLen] = {0}; u8 RecIndexLen = 0; void Uart1_IO_Init( void ) { PD_DDR |= ( 1 << 5 ); / / output mode TXD PD_CR1 | = (1 < < 5); // Push pull output PD_DDR &= ~(1 << 6); // Input mode RXD pd_cr1&= ~(1 << 6); Void Uart1_Init(unsigned int baudrate) {unsigned int baud; baud = 16000000 / baudrate; Uart1_IO_Init(); UART1_CR1 = 0; UART1_CR2 = 0; UART1_CR3 = 0; UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) ); UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) ); UART1_CR2_bit.REN = 1; Uart1_cr2_bit. TEN = 1; // Enable sending uart1_cr2_bit. RIEN = 1; Void SendChar(unsigned char dat) {while((uart1_sr&0x80) == 0x00); // Send data register empty UART1_DR = dat; } void Uart1_Send(unsigned char* DataAdd, unsigned char len) {unsigned char I; for( i = 0; i < len; i++ ) { SendChar( DataAdd[i] ); } //SendChar(0x0d); //SendChar(0x0a); //SendChar(0x0a); } * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * res = UART1_DR; ReceiveBuf[RecIndexLen++] = res; return; }Copy the code

The serial port code is the same as the normal usage, initializes the IO port and the baud rate, and then receives the data with interrupt. The ReceiveBuf array is used to store the received data, and the RecIndexLen is used to calculate the length of the received data.

After a set of data has been received, the data handler function is called to process the received data.

// Process the data received // receive: Void DisposeReceive(void) {u16 CRC16 = 0, CRC16Temp = 0, CRC16Temp = 0, CRC16Temp = 0; If (ReceiveBuf[0] == SlaveID) {CRC16 = App_Tab_Get_CRC16(ReceiveBuf, RecIndexLen - 2); / / low byte in the high byte CRC check in the back High byte for message last CRC16Temp = ((under-16) (ReceiveBuf] [RecIndexLen - 1 < < 8) | ReceiveBuf [RecIndexLen - 2] ); if( CRC16 ! = CRC16Temp ) { err = 4; / / CRC check error} StartRegAddr = (under-16) (ReceiveBuf [2] < < 8) | ReceiveBuf [3]. if( StartRegAddr > 0x07 ) { err = 2; } if(err == 0) {switch(ReceiveBuf[1]) // Function code {case 3: // Read multiple registers {Modbus_03_Slave(); break; } Case 6: // write to a single register {Modbus_06_Slave(); break; } case 16: // write multiple registers {Modbus_16_Slave(); break; } default: { err = 1; // The function code break is not supported; } } } if( err > 0 ) { SendBuf[0] = ReceiveBuf[0]; SendBuf[1] = ReceiveBuf[1] | 0x80; SendBuf[2] = err; // Send error code CRC16Temp = App_Tab_Get_CRC16(SendBuf, 3); SendBuf[3] = CRC16Temp & 0xFF; //CRC low SendBuf[4] = (CRC16Temp >> 8); //CRC high Uart1_Send(SendBuf, 5); err = 0; // Clear error flags after sending data}}}Copy the code

According to Modbus protocol, the first data is the address. If the address is equal to the address of the local machine, the data will be processed. Otherwise, the data will not be processed. If the address is correct, check whether the check bit is correct. Check the received data through THE CRC check, and then compare the calculated check bit and the received check bit to see if they are the same. If they are the same, the received data is correct. Next, the start address is read to see if it is in the range. When the start address is correct, it reads the function code and calls the corresponding function according to the different function code. Finally, it handles the exception response, sending a set of exception response data when the received data is incorrect.

And then the function code handler

/* Read hold register 03 master request message: 0x01 0x03 0x0000 0x0001 0x840A Read 1 hold register starting from 0 0x01 0x03 0x02 0x09C4 0xBF87 Read 2 bytes of data 0x09C4 */ void Modbus_03_Slave(void) {u16 RegNum = 0; u16 CRC16Temp = 0; u8 i = 0; RegNum = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5]; If ((StartRegAddr + RegNum) < 9) // Register address + register number within the specified range {SendBuf[0] = ReceiveBuf[0]; SendBuf[1] = ReceiveBuf[1]; SendBuf[2] = RegNum * 2; for( i = 0; i < RegNum; {SendBuf[3 + I * 2] = HoldReg[StartRegAddr * 2 + I * 2]; SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1]; } CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 ); SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF; SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF; SendBuf[RegNum * 2 + 4] = (CRC16Temp >> 8); //CRC high Uart1_Send(SendBuf, RegNum * 2 + 5); } else { err = 3; // The number of registers is not in the specified range}}Copy the code

If you are reading multiple registers, you need to know the start address and the number of registers to read. Since the start address is already calculated in the receiver function, you only need to count the number of registers. It then reads data from the hold register based on the start address and the number of registers. The hold register values are stored in the HoldReg array, where the temperature values read by the temperature sensor are stored. After the register data is read, the check value to be sent is calculated. The check value is calculated from the first data to the first bit before the check value. The CRC check value is calculated by calling App_Tab_Get_CRC16(). Finally, the read register data is returned. The data is sent through the Uart1_Send() function.

The following is to write to a single register

/* Write to a single hold register 06 master request message: 0x01 0x06 0x0000 0x1388 0x849C Write to register 0 with the value 0x1388 slave normal response message: */ void Modbus_06_Slave(void) {u16 RegValue = 0; u16 CRC16Temp = 0; RegValue = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5]; If (RegValue < 1001) // The register value does not exceed 1000 {HoldReg[StartRegAddr * 2] = ReceiveBuf[4]; HoldReg[StartRegAddr * 2 + 1] = ReceiveBuf[5]; SendBuf[0] = ReceiveBuf[0]; SendBuf[1] = ReceiveBuf[1]; SendBuf[2] = ReceiveBuf[2]; SendBuf[3] = ReceiveBuf[3]; SendBuf[4] = ReceiveBuf[4]; SendBuf[5] = ReceiveBuf[5]; CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); SendBuf[6] = CRC16Temp & 0xFF; //CRC low SendBuf[7] = (CRC16Temp >> 8); //CRC high Uart1_Send(SendBuf, 8); } else { err = 3; // Register value is not in the specified range}}Copy the code

This is a simple function to write the value of the register directly to the corresponding position to keep the register.

Finally, write multiple registers

16 Master request message: 0x01 0x10 0x7540 0x0002 0x04 0x0000 0x2710 0xB731 Write a total of 4 bytes of two hold register values starting from address 0x7540. 0x01 0x10 0x7540 0x0002 0x5A10 Write 2 hold register values from address 0x7540 */ void Modbus_16_Slave(void) {u16 RegNum = 0; u16 CRC16Temp = 0; u8 i = 0; RegNum = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5]; If ((StartRegAddr + RegNum) < 9) {for(I = 0; i < RegNum; // Storage register setting value {HoldReg[StartRegAddr * 2 + I * 2] = ReceiveBuf[I * 2 + 7]; HoldReg[StartRegAddr * 2 + 1 + i * 2] = ReceiveBuf[i * 2 + 8]; } SendBuf[0] = ReceiveBuf[0]; SendBuf[1] = ReceiveBuf[1]; SendBuf[2] = ReceiveBuf[2]; SendBuf[3] = ReceiveBuf[3]; SendBuf[4] = ReceiveBuf[4]; SendBuf[5] = ReceiveBuf[5]; CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 ); SendBuf[6] = CRC16Temp & 0xFF; //CRC low SendBuf[7] = (CRC16Temp >> 8); //CRC high Uart1_Send(SendBuf, 8); } else { err = 3; // The number of registers is not in the specified range}}Copy the code

According to the address will be written to the corresponding values of maintain registers, due to the starting address and register number is change, so it must be dynamic calculation to register address, the starting address and register number is two, so the computation to multiply 2, here is bad to understand, just plug in a fixed value, calculate.

So that’s the end of Modbus protocol processing, and finally let’s look at the main function

while( 1 ) { if( RecIndexLen_tem ! = RecIndexLen) {RecIndexLen_tem = RecIndexLen; time_cnt = 0; } if(time_cnt > 5) // time exceeds 5ms {if(RecIndexLen_tem > 0) // Data length is greater than 0 {RecIndexEnd = RecIndexLen; //Uart1_Send(ReceiveBuf, RecIndexEnd); // Send the received data DisposeReceive(); RecIndexLen = 0; } else // No data was received {time_cnt = 0; }}}Copy the code

Because Modbus protocol does not specify the start flag and end flag, channel data can not directly determine the start and end of a set of data. Time intervals are used to determine whether a set of data has been received. The idea is to add 1 to the counter every 1ms in the timer. If the serial port has data coming in, the counter will be cleared to 0. If the serial port has been receiving data, the value of this counter will always be cleared to zero. If the serial port is finished receiving data, the counter is not cleared and will keep accumulating. When the counter accumulates to a certain value, it means that there is no new data coming into the serial port during this time, then it is considered that a group of serial port data is received.

RecIndexLen is the length of the data received by the serial port, and RecIndexLen_tem is the length of the data last received by the serial port. If the two values are not equal, it indicates that the serial port has received new data. Store the new data length and clear the counter. If the serial port does not receive any new data, and the counter value is 5, it indicates that the serial port does not receive any new data at 5ms, and a set of data is considered to have been received, and the processing of the received data starts. This length of time is defined according to the actual situation. The reference standard is that this time should be greater than the interval between the two bits sent. The longer the interval, the better, considering the line transmission and the system delay. The longer the interval, the more accurate it is to judge that a set of data has been received, and the lower the probability of misjudgment. But it should not be too long. If the interval is too long, the response speed of the system will be slow. For example, if the baud rate is 9600, 1200 bytes of data can be sent per second, and it takes about 0.83ms to send one byte. The data interval of 5ms is sufficient. However, it should be noted that the transmission interval between two sets of data should also be greater than 5ms. Otherwise, the frequency of data transmission is too high and the interval between two sets of data is less than 5ms. The program will not be able to distinguish the end of receiving one set of data, causing errors.

From the above analysis, it can be seen that Modbus protocol is not so difficult from the perspective of serial communication. As long as you learn to use one of the function codes, the use of other function codes will become easy.

STM8S003 MCU modbus protocol simple communication example