Integrating an RS485 Sensor with ESP-IDF
RS485 is a reliable communication standard, and Modbus is a protocol that enables devices to exchange data efficiently. ESP-IDF is Espressif’s development framework, which is the most reliable if you are using an ESP32.
The XY-MD02 sensor measures temperature and humidity using the RS485 Modbus communication protocol.
This article simplifies the process of monitoring the sensor data through ESP IDF, explaining each step and piece of code in detail.
What You Need
- ESP32 Development Board
- XY-MD02 Sensor: Measures temperature and humidity.
- Max485 Module: Converts UART signals from the ESP32 into RS485 signals.
- Jumper Wires
- USB Cable: Connects the ESP32 to your computer for programming and power.
Circuit Diagram
Setting Up ESP-IDF
- Install ESP-IDF (https://docs.espressif.com/projects/esp-idf/).
- Set up your development environment (VS Code was used by us).
You can follow the below article, when setting up ESP-IDF,
https://medium.com/p/8fff7913dcef
Code Explanation
Here is the complete code,
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"
// RS485 UART and Modbus parameters
#define UART_NUM UART_NUM_1 // UART port number for RS485
#define TXD_PIN GPIO_NUM_17 // GPIO number for TX pin
#define RXD_PIN GPIO_NUM_16 // GPIO number for RX pin
#define RTS_PIN GPIO_NUM_18 // GPIO for RTS pin (controls DE/RE)
#define BAUD_RATE 9600 // Baud rate
#define UART_BUF_SIZE 1024 // Buffer size for UART
// Modbus function codes
#define MODBUS_FUNC_READ_HOLDING_REGISTERS 0x04
// XY-MD02 Modbus address and registers
#define SENSOR_ADDRESS 3 // Modbus address of the XY-MD02 sensor
#define TEMP_HUMIDITY_REG 0x0001 // Register address for temperature and humidity
static const char *TAG = "RS485_MODBUS";
// Initialize UART for RS485
void uart_init() {
uart_config_t uart_config = {
.baud_rate = BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, RTS_PIN, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM, UART_BUF_SIZE, UART_BUF_SIZE, 0, NULL, 0);
// Enable RS485 half duplex mode
uart_set_mode(UART_NUM, UART_MODE_RS485_HALF_DUPLEX);
}
// Function to calculate Modbus CRC16
uint16_t modbus_crc16(uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
for (int pos = 0; pos < length; pos++) {
crc ^= (uint16_t)data[pos];
for (int i = 8; i != 0; i--) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
// Function to send Modbus request
void modbus_send_request(uint8_t function_code, uint16_t start_address, uint16_t quantity) {
uint8_t request[8];
request[0] = SENSOR_ADDRESS; // Sensor Modbus address
request[1] = function_code; // Function code (Read Holding Registers)
request[2] = (start_address >> 8) & 0xFF; // Start address high byte
request[3] = start_address & 0xFF; // Start address low byte
request[4] = (quantity >> 8) & 0xFF; // Quantity of registers high byte
request[5] = quantity & 0xFF; // Quantity of registers low byte
// Calculate CRC
uint16_t crc = modbus_crc16(request, 6);
request[6] = crc & 0xFF; // CRC low byte
request[7] = (crc >> 8) & 0xFF; // CRC high byte
// Send the Modbus request over UART
uart_flush(UART_NUM); // Clear UART buffer before sending
uart_write_bytes(UART_NUM, (const char *)request, 8);
ESP_LOGI(TAG, "Sent request to sensor");
}
// Task to read data from sensor
void modbus_read_task(void *arg) {
while (1) {
// Send request to read temperature and humidity registers
modbus_send_request(MODBUS_FUNC_READ_HOLDING_REGISTERS, TEMP_HUMIDITY_REG, 2);
// Wait for sensor response
uint8_t response[256];
int len = uart_read_bytes(UART_NUM, response, sizeof(response), pdMS_TO_TICKS(1500));
if (len > 0) {
ESP_LOGI(TAG, "Received %d bytes from sensor", len);
// Log each byte in the response array
ESP_LOGI(TAG, "Full Response Data:");
for (int i = 0; i < len; i++) {
ESP_LOGI(TAG, "Byte %d: 0x%02X", i, response[i]);
}
// Verify the CRC
uint16_t crc_received = (response[len - 1] << 8) | response[len - 2];
uint16_t crc_calculated = modbus_crc16(response, len - 2);
if (crc_received == crc_calculated) {
// Parse response to extract temperature and humidity without any modification
int temperature = (response[3] << 8) | response[4];
int humidity = (response[5] << 8) | response[6];
ESP_LOGI(TAG, "Temperature: %.1f °C, Humidity: %.1f %%", temperature / 10.0, humidity / 10.0);
} else {
ESP_LOGE(TAG, "CRC error in response");
}
} else {
ESP_LOGE(TAG, "No response from sensor");
}
vTaskDelay(pdMS_TO_TICKS(4000)); // Delay for a while before the next read
}
}
// Main application
void app_main() {
uart_init();
xTaskCreate(modbus_read_task, "modbus_read_task", 4096, NULL, 10, NULL);
}
Let’s go deep into each function of the code,
Initializing UART for Communication
UART (Universal Asynchronous Receiver-Transmitter) is how the ESP32 talks to the RS485 module.
void uart_init() {
uart_config_t uart_config = {
.baud_rate = 9600, // Speed of communication
.data_bits = UART_DATA_8_BITS, // Data size: 8 bits
.parity = UART_PARITY_DISABLE, // No parity checking
.stop_bits = UART_STOP_BITS_1, // One stop bit
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE // No hardware flow control
};
// Apply configuration to UART port 1
uart_param_config(UART_NUM_1, &uart_config);
// Assign GPIO pins for UART communication
uart_set_pin(UART_NUM_1, GPIO_NUM_17, GPIO_NUM_16, GPIO_NUM_18, UART_PIN_NO_CHANGE);
// Enable UART buffer and install driver
uart_driver_install(UART_NUM_1, 1024, 1024, 0, NULL, 0);
// Enable RS485 half-duplex mode
uart_set_mode(UART_NUM_1, UART_MODE_RS485_HALF_DUPLEX);
}
- Baud Rate and Data Format: According to the XY-MD02 datasheet, the sensor communicates using a baud rate of 9600 bps with 8 data bits, no parity, and 1 stop bit (often referred to as 8N1). This information guided the configuration in uart_config_t.
- RS485 Mode: The sensor uses RS485 half-duplex communication. The device cannot send and receive data simultaneously, which is typical for RS485 communication. Therefore, we set the UART mode to UART_MODE_RS485_HALF_DUPLEX.
- GPIO Pins: The choice of GPIO pins for TX (GPIO_NUM_17), RX (GPIO_NUM_16), and RTS (GPIO_NUM_18) was based on the ESP32’s pin capabilities and the need to control the RS485 transceiver’s transmit and receive modes. The RTS pin controls the DE (Driver Enable) and RE (Receiver Enable) pins on the MAX485 module, allowing the ESP32 to manage the direction of communication.
Modbus Communication Setup
// Modbus function codes
#define MODBUS_FUNC_READ_HOLDING_REGISTERS 0x04
// XY-MD02 Modbus address and registers
#define SENSOR_ADDRESS 3 // Modbus address of the XY-MD02 sensor
#define TEMP_HUMIDITY_REG 0x0001 // Register address for temperature and humidityModbus Function Code: The function code 0x04 corresponds to Read Input Registers, as specified in the Modbus protocol. The datasheet indicates that the sensor’s temperature and humidity data are accessible via input registers, necessitating the use of this function code.
- Sensor Address (Slave ID): The default Modbus address for the XY-MD02 sensor is 1. This is specified in the datasheet under the device’s default settings. If your sensor has a different address, you should adjust the SENSOR_ADDRESS accordingly. We changed the slave ID to 3. You can follow the below article in order to change the slave ID,
https://medium.com/@kumarasenau/changing-the-slave-id-of-a-rs485-sensor-d77d4d8b557e
- Register Addresses: The temperature and humidity data are located starting at register address 0x0001. This information is crucial and is provided in the datasheet under the register mapping section. Knowing the exact register addresses allows us to request the correct data from the sensor.
Modbus CRC16 Calculation
uint16_t modbus_crc16(uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
for (int pos = 0; pos < length; pos++) {
crc ^= (uint16_t)data[pos];
for (int i = 8; i != 0; i--) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
- CRC Algorithm: The Modbus protocol specifies the use of CRC16 for error checking. The datasheet confirms that the sensor uses standard Modbus CRC16 (also known as CRC-16-IBM or CRC-16-Modbus). Implementing this function correctly ensures reliable communication and data integrity.
Now, let’s discuss on request sending and receiving the repsonse,
We are using continuous readings of data for temperature and humidity. In there we can get data for temperature and humidity with one request.
Sending Modbus Requests
void modbus_send_request(uint8_t function_code, uint16_t start_address, uint16_t quantity) {
uint8_t request[8];
request[0] = SENSOR_ADDRESS; // Sensor Modbus address
request[1] = function_code; // Function code (e.g., Read Input Registers)
request[2] = (start_address >> 8) & 0xFF; // Start address high byte
request[3] = start_address & 0xFF; // Start address low byte
request[4] = (quantity >> 8) & 0xFF; // Quantity high byte
request[5] = quantity & 0xFF; // Quantity low byte
// Calculate CRC
uint16_t crc = modbus_crc16(request, 6);
request[6] = crc & 0xFF; // CRC low byte
request[7] = (crc >> 8) & 0xFF; // CRC high byte
// Send the Modbus request over UART
uart_flush(UART_NUM); // Clear UART buffer before sending
uart_write_bytes(UART_NUM, (const char *)request, 8);
ESP_LOGI(TAG, "Sent request to sensor");
}
- Function Code and Register Addresses: Using the function code 0x04 and starting address 0x0001 (both derived from the datasheet), we construct the Modbus request to read the temperature and humidity registers.
- Quantity of Registers: We request 2 registers because the temperature and humidity data are stored in two consecutive registers. This is specified in the datasheet’s register mapping.
- Message Structure: The Modbus message structure follows the standard format:
- Address: 1 byte (sensor’s Modbus address)
- Function Code: 1 byte
- Start Address(register Address): 2 bytes (high byte first)
- Quantity: 2 bytes (number of registers to read)
- CRC: 2 bytes (low byte first)
- CRC Calculation: The CRC is calculated over the first 6 bytes of the message, as per the Modbus protocol, ensuring the integrity of the request.
Reading and Parsing Sensor Data
void modbus_read_task(void *arg) {
while (1) {
// Send request to read temperature and humidity registers
modbus_send_request(MODBUS_FUNC_READ_HOLDING_REGISTERS, TEMP_HUMIDITY_REG, 2);
// Wait for sensor response
uint8_t response[256];
int len = uart_read_bytes(UART_NUM, response, sizeof(response), pdMS_TO_TICKS(1500));
if (len > 0) {
ESP_LOGI(TAG, "Received %d bytes from sensor", len);
// Log each byte in the response array
ESP_LOGI(TAG, "Full Response Data:");
for (int i = 0; i < len; i++) {
ESP_LOGI(TAG, "Byte %d: 0x%02X", i, response[i]);
}
// Verify the CRC
uint16_t crc_received = (response[len - 1] << 8) | response[len - 2];
uint16_t crc_calculated = modbus_crc16(response, len - 2);
if (crc_received == crc_calculated) {
// Parse response to extract temperature and humidity
int temperature = (response[3] << 8) | response[4];
int humidity = (response[5] << 8) | response[6];
ESP_LOGI(TAG, "Temperature: %.1f °C, Humidity: %.1f %%", temperature / 10.0, humidity / 10.0);
} else {
ESP_LOGE(TAG, "CRC error in response");
}
} else {
ESP_LOGE(TAG, "No response from sensor");
}
vTaskDelay(pdMS_TO_TICKS(4000)); // Delay before the next read
}
}
- Response Format: According to the datasheet, the sensor’s response to a 0x04 function code request includes:
- Address: 1 byte
- Function Code: 1 byte
- Byte Count: 1 byte (number of data bytes)
- Data: 2 bytes per register (temperature and humidity)
- CRC: 2 bytes
- Parsing Data,
- Temperature: Extracted from bytes at positions 4 and 5. The datasheet specifies that the temperature value is a 16-bit integer, with the high byte first (big-endian format). The actual temperature is obtained by dividing this value by 10.0, as the sensor provides the temperature in tenths of a degree.
- Humidity: Extracted from bytes at positions 6 and 7, following the same format as the temperature.
- CRC Verification: Before parsing the data, we verify the CRC to ensure the response is valid. The datasheet specifies the CRC calculation method and confirms that the CRC is the last two bytes of the response, with the low byte first.
- Handling Byte Order: Since Modbus protocol transmits data in big-endian format, we combine the high and low bytes accordingly,
int temperature = (response[3] << 8) | response[4];
This aligns with the datasheet’s indication of the data format.
Main Application Entry Point
void app_main() {
uart_init();
xTaskCreate(modbus_read_task, "modbus_read_task", 4096, NULL, 10, NULL);
}
Deploying and Testing
Compile the Code
idf.py build
Flash the Code
idf.py flash
Monitor the Output
idf.py monitor
Here is what you can see as the output,
Now you can observe temperature and humidity values logged to the console.
Integrating the XY-MD02 sensor with ESP-IDF may seem complex, but breaking it into manageable steps simplifies the process. By understanding the code line by line, even beginners can build robust industrial IoT systems. With this knowledge, you can explore adding more sensors or developing advanced applications.
Now, you can connect any RS485 sensor with ESP IDF by following our detailed explanations on the code.
Hope you enjoyed the article. Please comment below or send us an email to info@protonest.co, if you face any issues when implementing.
We’ve launched the IoT System Design Tool by Protonest to help you build complete IoT systems, with resources along the way.
Use it for your next IoT project and streamline your design process!
https://iot-system-design-tool.protonest.co
Contact us for any consultations or projects related to IoT and embedded systems.
Email: info@protonest.co
Protonest for more details.
Protonest specializes in transforming IoT ideas into reality. We offer prototyping services from concept to completion. Our commitment ensures that your visionary IoT concepts become tangible, innovative, and advanced prototypes.
Our Website: https://www.protonest.co/
Cheers!