Modbus Sensor Emulation Part 3: Master Request to Read

Part 3 of the guide moves beyond basic link verification and introduces practical Modbus RTU transactions in which the master issues read requests to the STM32-based slave. This section focuses on implementing and validating standard read function codes, register addressing, and response formatting so the emulated sensor can return meaningful data in a fully protocol-compliant manner which will be covered next.

In this guide, we shall cover the following:

  • Master Request to Read.
  • Firmware Development.
  • Testing and Results.

1. Master Request to Read:

When the master request to read data, the data frame as following:

  • First byte is the address of the slave.
  • Second byte is the function code.
  • Third and fourth bytes are the register address high and low byte respectively.
  • Fifth and sixth bytes are the number of points high and low byte respectively.
  • The fifth and last are the CRC ow and high byte respectively.

This is the typical frame for master request to read from slave.

2. Firmware Development:

First, we shall create header and source file for crc calculation.

Create header and and source file with name of modbus_crc.h and modbus_crc.c respectively as follows:

In modbus_crc.h file, within headerfile guard, declare the following function:

uint16_t crc16(uint8_t *buffer, uint16_t buffer_length);

This function shall calculate the CRC value on the fly.

Hence, the entire header file as follows:

#ifndef INC_MODBUS_CRC_H_
#define INC_MODBUS_CRC_H_

uint16_t crc16(uint8_t *buffer, uint16_t buffer_length);

#endif /* INC_MODBUS_CRC_H_ */

Next, in modbus_crc, include the following header files:

#include "stdint.h"
#include "modbus_crc.h"

Declare the following crc high table:

static const uint8_t table_crc_hi[] = {
    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
};

For the low:

static const uint8_t table_crc_lo[] = {
    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
};

Next, the function shall calculate the crc value as follows:

uint16_t crc16(uint8_t *buffer, uint16_t buffer_length)
{
    uint8_t crc_hi = 0xFF; /* high CRC byte initialized */
    uint8_t crc_lo = 0xFF; /* low CRC byte initialized */
    unsigned int i; /* will index into CRC lookup */

    /* pass through message buffer */
    while (buffer_length--) {
        i = crc_lo ^ *buffer++; /* calculate the CRC  */
        crc_lo = crc_hi ^ table_crc_hi[i];
        crc_hi = table_crc_lo[i];
    }

    return (crc_hi << 8 | crc_lo);
}

This function computes a 16-bit Modbus CRC (CRC-16/IBM) over a byte buffer, which is mandatory for Modbus RTU frames to detect transmission errors.

Here is what each part does:

Function Purpose

uint16_t crc16(uint8_t *buffer, uint16_t buffer_length)

Takes:

  • buffer → pointer to the data bytes
  • buffer_length → number of bytes to process

Returns:

  • The final 16-bit CRC value appended to Modbus frames.

CRC Initialization

uint8_t crc_hi = 0xFF;
uint8_t crc_lo = 0xFF;

Modbus RTU specifies that the CRC register starts at 0xFFFF, so both high and low bytes are initialized to 0xFF.

Processing Each Byte

while (buffer_length--) {

Iterates through every byte in the message.

Index Calculation

i = crc_lo ^ *buffer++;

The incoming data byte is XORed with the current low CRC byte.
The result forms an index into the lookup tables.

This avoids doing slow bit-by-bit polynomial calculations.

Lookup Table Update

crc_lo = crc_hi ^ table_crc_hi[i];
crc_hi = table_crc_lo[i];

Two precomputed tables are used:

  • table_crc_hi[]
  • table_crc_lo[]

They contain the pre-calculated CRC effects for all 256 possible byte values.

The new CRC:

  • crc_lo becomes the old high byte XORed with a lookup value.
  • crc_hi becomes the corresponding lookup value.

This updates the CRC register for the processed byte.

Final Return Value

return (crc_hi << 8 | crc_lo);

Combines the two bytes into a 16-bit result:

CRC = (high_byte << 8) | low_byte

Summary

This is a table-driven Modbus CRC-16 implementation that:

✔ Starts with 0xFFFF
✔ Processes each byte using XOR + lookup tables
✔ Produces the standard Modbus CRC
✔ Runs much faster than bitwise CRC algorithm.

Next, create new header and source file with name of modbus.h and modbus.h respectively as follows:

In the header file, we start by including the following header files:

#include "main.h"

#include "stdint.h"

Next, declare a data structure to hold the following:

  • Slave address.
  • Function code.
  • Register address.
  • Number of points.
typedef struct
{
	uint8_t Slave_Address;
	uint8_t Function_Code;
	uint16_t Register_Address;
	uint16_t Number_of_Data;

}modbusTypedefStruct;

Next, the function that send the master request to read frame as follows:

void Modbus_ReadRequest(modbusTypedefStruct * ModbusFrame);

Hence, the entire header file as follows:

#ifndef INC_MODBUS_H_
#define INC_MODBUS_H_

#include "main.h"

#include "stdint.h"

typedef struct
{
	uint8_t Slave_Address;
	uint8_t Function_Code;
	uint16_t Register_Address;
	uint16_t Number_of_Data;

}modbusTypedefStruct;

void Modbus_ReadRequest(modbusTypedefStruct * ModbusFrame);


#endif /* INC_MODBUS_H_ */

Next, in modbus.c file, we start by including the following:

#include <modbus.h>

#include "modbus_crc.h"

Next, declare the serial instant that shall be used, USART1 in this case:

extern UART_HandleTypeDef huart1;

Note: It needed to be defined as external since it has been declared in the main.c file.

Next, the function that will send the frame as follows:

void Modbus_ReadRequest(modbusTypedefStruct * ModbusFrame)
{
	uint8_t address	=	ModbusFrame->Slave_Address;

	uint8_t FunCode	=	ModbusFrame->Function_Code;

	uint16_t regAdd= 	ModbusFrame->Register_Address;

	uint16_t numData=	ModbusFrame->Number_of_Data;

	uint8_t buffer[8];


	buffer[0]=address;

	buffer[1]=FunCode;

	buffer[2]=(regAdd>>8)&0xFF;

	buffer[3]=(regAdd)&0xFF;

	buffer[4]= (numData>>8)&0xFF;

	buffer[5]= (numData)&0xFF;

	uint16_t tmp_crc=crc16(buffer,6);

	buffer[6]=tmp_crc&0xFF;
	buffer[7]=(tmp_crc>>8)&0xFF;

	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

	HAL_UART_Transmit(&huart1, buffer, 8, 100);

	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
}

This function builds and transmits a Modbus RTU read request frame from a master to a slave over UART using RS-485.

It extracts the slave address, function code, register address, and number of registers from the ModbusFrame structure, then places them into an 8-byte buffer following the Modbus RTU format. A CRC-16 is calculated over the first six bytes and appended in low-byte-first order, as required by Modbus. Before transmission, GPIOA pin 0 is set high to enable the RS-485 driver (transmit mode), the frame is sent using HAL_UART_Transmit, and the pin is cleared afterward to return the transceiver to receive mode.

Hence, the source file as follows:

#include <modbus.h>

#include "modbus_crc.h"

extern UART_HandleTypeDef huart1;


void Modbus_ReadRequest(modbusTypedefStruct * ModbusFrame)
{
	uint8_t address	=	ModbusFrame->Slave_Address;

	uint8_t FunCode	=	ModbusFrame->Function_Code;

	uint16_t regAdd= 	ModbusFrame->Register_Address;

	uint16_t numData=	ModbusFrame->Number_of_Data;

	uint8_t buffer[8];


	buffer[0]=address;

	buffer[1]=FunCode;

	buffer[2]=(regAdd>>8)&0xFF;

	buffer[3]=(regAdd)&0xFF;

	buffer[4]= (numData>>8)&0xFF;

	buffer[5]= (numData)&0xFF;

	uint16_t tmp_crc=crc16(buffer,6);

	buffer[6]=tmp_crc&0xFF;
	buffer[7]=(tmp_crc>>8)&0xFF;

	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

	HAL_UART_Transmit(&huart1, buffer, 8, 100);

	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
}

Thats all for the firmware.

3. Testing and Results:

In main.c, we shall start by including the following header file:

#include "modbus.h"

Next, declare the data structure as follows:

modbusTypedefStruct DataFrame;

Next, in user code begin 2 in main function, populate the data structure with values, the following is for example. Later, a proper values shall be used.

DataFrame.Slave_Address=0x11;
DataFrame.Function_Code=0x03;
DataFrame.Register_Address=0x01;
DataFrame.Number_of_Data=0x02;

In while 1 loop, in user code begin 3, transmit the data each 100ms as follows:

Modbus_ReadRequest(&DataFrame);

HAL_Delay(100);

Save, build and run the project as follows:

By probing the AB pins of the RS485, you should get the following:

Note: Values are in hex.

Next, we shall develop the firmware to response to request to read by following one of the sensor documentation.

Stay tuned.

Happy coding 😉

Add Comment

Your email address will not be published. Required fields are marked *