ESP32 & IoT24 October 202620 mins

Build a Subsurface Soil Moisture & Salinity Profile Array with ESP32 & RS485 Modbus

Comprehensive step-by-step engineering guide for deploying multi-depth industrial Modbus RTU soil moisture and salinity sensor chains using ESP32 and MAX485.

M
M F Haque
Founder & Hardware Architect, Mentora
Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Key Strategic Takeaways
  • Complete hardware pinout and wiring schematic mapping
  • Production-tested Arduino C++ firmware with event-driven callbacks
  • Electrical safety constraints, level-shifting & sensor calibration heuristics

2. Overview & Engineering Objectives

Standard hobbyist capacitive soil moisture probes corrode when exposed to mineralized farm soil and cannot transmit analog signals beyond 2 meters without signal attenuation. This project implements an industrial-grade subsurface agricultural monitoring network that daisy-chains multiple soil probes along a rugged RS485 Modbus RTU differential bus up to 500 meters across open fields.

Core Engineering Capabilities:

Multi-Depth Stratified Sensing: Polls discrete sensor nodes buried at 15cm, 30cm, 60cm, and 90cm depths to profile root-zone water percolation and fertilizer salinity build-up.

Differential Noise Immunity: RS485 differential signaling (V_A - V_B) cancels out electromagnetic interference generated by high-voltage irrigation pump motors.

Deterministic UART Flush Timing: Solves the classic DE/RE timing glitch where premature transceiver state toggling clips outgoing Modbus packets.

Deep Sleep Power Gating: Completely isolates the 12V sensor power bus between measurement intervals, achieving multi-year battery life.

3. Bill of Materials (BOM)

Component

Quantity

Specification / Rating

Est. Cost (INR)

Sourcing / Affiliate Link

ESP32 DevKit V1 Board

1

30-Pin Dual-Core Xtensa LX6 (Deep Sleep Capable)

₹380

Buy on Mentora Store

Industrial RS485 Soil Probe

1–3

Stainless steel prongs, Modbus RTU (Moisture, Temp, EC)

₹1,650

Buy on Mentora Store

MAX485 / SP3485 Module

1

3.3V/5V TTL to RS485 transceiver breakout board

₹65

Buy on Mentora Store

120Ω 1/4W Metal-Film Resistor

2

Bus termination resistors for cable ends

₹4

Buy on Mentora Store

IRF9540 P-Channel MOSFET

1

High-side 12V sensor power gate switch

₹45

Buy on Mentora Store

12V 2A DC Power Supply

1

Wall adapter or 12V solar lead-acid/LiFePO4 battery

₹260

Buy on Mentora Store

LM2596 DC-DC Buck Converter

1

Drops 12V rail to 5V/3.3V for ESP32

₹90

Buy on Mentora Store

Shielded Twisted Pair Cable

20m

24 AWG foil-shielded RS485 communication cable

₹320

Buy on Mentora Store

Total Hardware Cost

~₹2,814

4. Hardware Engineering: The MAX485 DE/RE Timing Glitch

The MAX485 uses half-duplex differential communication. Two pins control transceiver direction:

DE (Driver Enable, Active-HIGH)

RE (Receiver Enable, Active-LOW)

Typically, both pins are tied together to a single ESP32 pin (GPIO 4).

The Critical Software Flaw:

// INCORRECT (Triggers Modbus CRC Mismatch):

digitalWrite(DEREPIN, HIGH); // Transmit mode

Serial2.write(modbusFrame, 8); // Pushes bytes into hardware FIFO

digitalWrite(DEREPIN, LOW); // BUG! Toggles to receive mode immediately!

In ESP32 hardware UART, Serial2.write() returns immediately once bytes are copied to the internal ring buffer—before the final byte has been shifted onto the physical wire.

Toggling DE_RE LOW prematurely truncates the final CRC byte, causing slave sensors to drop the packet!

The Production Fix:

// CORRECT (Deterministic Bus Direction Control):

digitalWrite(DEREPIN, HIGH);

Serial2.write(modbusFrame, 8);

Serial2.flush(); // WAITS UNTIL THE LAST STOP BIT HAS LEFT THE HARDWARE SHIFT REGISTER!

digitalWrite(DEREPIN, LOW);

5. Modbus RTU Protocol & CRC16 Mathematics

A standard Modbus query frame requesting 3 holding registers (Moisture, Temp, EC) from Slave Node 0x01:

[0x01] Slave Address

[0x03] Function Code (Read Holding Registers)

[0x00] Starting Address High

[0x00] Starting Address Low

[0x00] Quantity High

[0x03] Quantity Low (3 Registers)

[0x05] CRC-16 Low Byte

[0xCB] CRC-16 High Byte

6. Real-World Troubleshooting FAQs

Q1: Why do sensors fail to respond on long cable runs (>50m)?

Root Cause: Missing 120Ω Line Termination: High-speed differential edges reflect off open cable ends, corrupting packet bits.

Fix: Place a 120Ω resistor directly across lines A and B at the extreme ends of the physical bus.

Q2: Why does the MAX485 output garbled characters on ESP32 3.3V logic?

Root Cause: Standard 5V MAX485 modules require 5V VCC, outputting 5V on RO into the ESP32. While tolerant, it introduces threshold distortion. Use an SP3485 (3.3V native transceiver) or power the MAX485 with 5V while using a 3.3V logic divider.

7. Mentora Monetization & Student Support Hooks

🛒 Official Mentora Modbus Agritech Kit: ESP32 DevKit, industrial stainless NPK/EC probe, SP3485 transceiver, 120Ω terminators, and 12V buck supply for ₹3,499.

⚡ ₹49 Mentora Token Vault: Instantly unlock the companion multi-depth soil layer percolation graphing code, ThingsBoard IoT telemetry dashboard, and Modbus RTU scanner sketch.

🛠️ ₹299 1-on-1 Mentora Live Video Debugging: 30-minute session with an industrial automation engineer to debug Modbus CRC errors, tune RS485 bus timing, and configure multi-drop slave addresses.


Hardware Pinout & Wiring Connections

Hardware Pinout & Wiring Connection Guide

Project 21: Subsurface Soil Moisture & Salinity Profile Array with RS485 Modbus

Microcontroller: ESP32 DevKit V1 (30-pin / 36-pin)Bus Transceiver: MAX485 / SP3485 TTL-to-RS485 Half-Duplex ModuleSensors: Industrial Multi-Depth Modbus RTU Soil Probes (Moisture, Temp, Salinity EC)Bus Architecture: Half-Duplex Differential A/B Lines with 120Ω End-of-Line Termination Resistors

1. Complete Fritzing Pin Connection Mapping Table

Table A: MAX485 Transceiver to ESP32 Hardware UART2

MAX485 Pin

ESP32 DevKit Pin

Wire Color

Bus Signal / Role

Operational Constraints & Notes

VCC

5V (VIN Rail)

Red

5V DC Supply Rail

Powers internal differential driver stage.

GND

GND Rail

Black

System Ground

Common ground reference.

RO (Receiver Out)

GPIO 16 (U2RXD)

Green

UART Hardware Receive

Connects to ESP32 Hardware Serial2 RX.

DI (Driver In)

GPIO 17 (U2TXD)

Yellow

UART Hardware Transmit

Connects to ESP32 Hardware Serial2 TX.

DE (Driver Enable)

GPIO 4

Orange

Transmit Enable (Active-HIGH)

Tied directly to RE pin.

RE (Receiver Enable)

GPIO 4

Orange

Receive Enable (Active-LOW)

Driven HIGH to send, LOW to receive.

Table B: Industrial RS485 Field Bus Wiring

Field Terminal

Connected Cable Core

Destination Node

Function & Electrical Rules

MAX485 Terminal A

Twisted Pair Lead A (+)

Sensors 1..N Line A

Non-inverting differential signal line.

MAX485 Terminal B

Twisted Pair Lead B (-)

Sensors 1..N Line B

Inverting differential signal line.

120Ω Termination Resistor

Across A and B

At Furthest Sensor

MANDATORY: Absorbs signal reflections at cable end.

Sensor Power (+12V)

12V Positive Rail

Sensors 1..N VCC (Brown)

External 12V DC power bus for industrial probes.

Sensor Ground (GND)

12V Negative Rail

Sensors 1..N GND (Black)

Common ground tied to ESP32 GND.

2. Circuit Connection Schematic Diagram (ASCII Art)

+-------------------------------------------------------------+

| ESP32 DevKit V1 |

| |

| [ 5V ] [ GND ] [GPIO 16] [GPIO 17] [GPIO 4] |

+----+-------+----------+-----------+-----------+----+--------+

textcode snippet
|       |          |           |           |

+5V | GND | RO | DI | DE/RE |

Rail | Rail | RX2 | TX2 | Direction |

=======+=======+==========+===========+===========+=============

textcode snippet
|       |          |           |           |
textcode snippet
|       |          |           |     +-----+
textcode snippet
|       |          |           |     |     |

+--+-------+----------+-----------+-----+-----+----+

| VCC GND RO DI DE RE |

| MAX485 TRANSCEIVER |

| [ A ] [ B ] |

+-------------------+-------------+----------------+

| |

| A (+) | B (-)

| |

==================+=============+================= (Twisted Pair Bus)

| |

| 120 Ohm |

| Termination |

+-------------[120R]------------+

| |

+--+---+ +--+---+

| A B | | A B |

| SOIL PROBE #1 | SOIL PROBE #2

| (15cm Depth, Addr: 0x01) | (30cm Depth, Addr: 0x02)

+------+ +------+

3. Golden Engineering Rules for Modbus RS485 Arrays

Hardware UART Flush Before Listening:

Always call modbusSerial.flush() before driving GPIO 4 (DE/RE) LOW. If you do not wait for the UART hardware shift register to empty, the final CRC-16 byte gets clipped.

Daisy-Chain Topology vs Star Routing:

RS485 must be routed in a strict daisy-chain (bus) topology. Never wire sensors in a "star" configuration, as stubs longer than 30cm create impedance discontinuities that destroy packet readability.

120Ω End Termination:

Install a 120Ω resistor across terminals A and B at the master node and at the final slave probe on the cable.


Complete Tested Arduino C++ Firmware

``cpp

/*

  • ==============================================================================
  • Project: Subsurface Soil Moisture & Salinity Profile Array with RS485 Modbus
  • Microcontroller: ESP32 DevKit V1 (Xtensa 32-bit Dual-Core)
  • Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
  • License: MIT

*

  • Hardware Peripherals:
  • - MAX485 / SP3485 TTL-to-RS485 Transceiver:
  • RO (RX) -> GPIO 16 (Hardware UART2 RX)
  • DI (TX) -> GPIO 17 (Hardware UART2 TX)
  • DE & RE -> GPIO 4 (Direction Control Pin)
  • - Industrial Multi-Depth Modbus RTU Soil Probes (Slaves 0x01, 0x02, 0x03)
  • ==============================================================================

*/

#include <HardwareSerial.h>

#include <WiFi.h>

#include <HTTPClient.h>

// -----------------------------------------------------------------------------

// USER CONFIGURATION (RS485 & MODBUS ADDRESSES)

// -----------------------------------------------------------------------------

const char* WIFISSID = "YOURWIFI_SSID";

const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";

const char* TELEMETRYURL = "http://api.mentora.in/telemetry/soilprofile";

#define RS485RXPIN 16

#define RS485TXPIN 17

#define RS485DERE_PIN 4

#define STATUSLEDPIN 2

#define RS485BAUDRATE 4800 // Standard baud rate for industrial soil probes

#define SLEEPINTERVALSEC 900ULL // 15-minute sleep interval (900s)

HardwareSerial modbusSerial(2);

// Number of depth probes chained on the RS485 bus

const int SENSOR_COUNT = 3;

const uint8t SLAVEIDS[SENSOR_COUNT] = {0x01, 0x02, 0x03}; // 15cm, 30cm, 60cm depths

// Telemetry Storage Structure

struct SoilReading {

uint8_t slaveId;

float moisturePct;

float temperatureC;

uint16_t ecConductivityUsCm;

bool valid;

};

SoilReading sensorData[SENSOR_COUNT];

RTCDATAATTR int profileCycleCount = 0;

// -----------------------------------------------------------------------------

// MODBUS CRC-16 GENERATOR ALGORITHM (IBM POLYNOMIAL 0xA001)

// -----------------------------------------------------------------------------

uint16t calculateModbusCRC(const uint8t *buffer, size_t length) {

uint16_t crc = 0xFFFF;

for (size_t pos = 0; pos < length; pos++) {

crc ^= (uint16_t)buffer[pos];

for (int i = 8; i != 0; i--) {

if ((crc & 0x0001) != 0) {

crc >>= 1;

crc ^= 0xA001;

} else {

crc >>= 1;

}

}

}

return crc;

}

// -----------------------------------------------------------------------------

// MODBUS RTU QUERY DISPATCHER

// -----------------------------------------------------------------------------

bool queryModbusProbe(uint8t slaveId, float &moisture, float &tempC, uint16t &ec) {

// Query frame: [Slave][Func 0x03][Start 0x00, 0x00][Qty 0x00, 0x03][CRCL][CRCH]

uint8_t queryFrame[8];

queryFrame[0] = slaveId;

queryFrame[1] = 0x03; // Read Holding Registers

queryFrame[2] = 0x00; // Register Address High

queryFrame[3] = 0x00; // Register Address Low

queryFrame[4] = 0x00; // Number of Registers High

queryFrame[5] = 0x03; // Number of Registers Low (Moisture, Temp, EC)

uint16_t crc = calculateModbusCRC(queryFrame, 6);

queryFrame[6] = crc & 0xFF; // CRC Low

queryFrame[7] = (crc >> 8) & 0xFF; // CRC High

// 1. Flush any stale bytes from RX buffer

while (modbusSerial.available()) modbusSerial.read();

// 2. Set MAX485 to TRANSMIT mode

digitalWrite(RS485DERE_PIN, HIGH);

delayMicroseconds(50);

// 3. Transmit Frame

modbusSerial.write(queryFrame, 8);

// CRITICAL: Wait for transmission to physically finish before releasing bus!

modbusSerial.flush();

// 4. Set MAX485 to RECEIVE mode

digitalWrite(RS485DERE_PIN, LOW);

// 5. Await Response (Expected 11 bytes: [Slave][0x03][ByteCount 0x06][Data 6 bytes][CRC 2 bytes])

unsigned long startWait = millis();

uint8_t responseBuffer[11];

size_t bytesReceived = 0;

while (millis() - startWait < 500 && bytesReceived < 11) {

if (modbusSerial.available()) {

responseBuffer[bytesReceived++] = modbusSerial.read();

}

}

if (bytesReceived < 11) {

Serial.printf("[MODBUS] Slave #%d timeout! Bytes received: %d\n", slaveId, bytesReceived);

return false;

}

// 6. Verify Response CRC

uint16_t responseCRC = calculateModbusCRC(responseBuffer, 9);

uint16_t expectedCRC = (responseBuffer[10] << 8) | responseBuffer[9];

if (responseCRC != expectedCRC) {

Serial.printf("[MODBUS] CRC mismatch on Slave #%d! Calc: 0x%04X, Recv: 0x%04X\n",

slaveId, responseCRC, expectedCRC);

return false;

}

// 7. Parse Sensor Registers

uint16_t rawMoisture = (responseBuffer[3] << 8) | responseBuffer[4];

uint16_t rawTemp = (responseBuffer[5] << 8) | responseBuffer[6];

uint16_t rawEC = (responseBuffer[7] << 8) | responseBuffer[8];

moisture = (float)rawMoisture / 10.0; // Scaled 0.1%

tempC = (float)rawTemp / 10.0; // Scaled 0.1C

ec = rawEC; // uS/cm

return true;

}

// -----------------------------------------------------------------------------

// ARDUINO SETUP

// -----------------------------------------------------------------------------

void setup() {

Serial.begin(115200);

delay(100);

profileCycleCount++;

Serial.println("\n========================================================");

Serial.printf(" MENTORA RS485 MODBUS SUBSURFACE SOIL ARRAY (CYCLE #%d)\n", profileCycleCount);

Serial.println("========================================================");

pinMode(RS485DERE_PIN, OUTPUT);

digitalWrite(RS485DERE_PIN, LOW); // Start in receive mode

pinMode(STATUSLEDPIN, OUTPUT);

digitalWrite(STATUSLEDPIN, HIGH);

// Initialize Hardware UART 2 for RS485 communication

modbusSerial.begin(RS485BAUDRATE, SERIAL8N1, RS485RXPIN, RS485TX_PIN);

delay(100);

// Poll each sensor in the daisy chain

for (int i = 0; i < SENSOR_COUNT; i++) {

sensorData[i].slaveId = SLAVE_IDS[i];

sensorData[i].valid = queryModbusProbe(SLAVE_IDS[i],

sensorData[i].moisturePct,

sensorData[i].temperatureC,

sensorData[i].ecConductivityUsCm);

if (sensorData[i].valid) {

Serial.printf("[SOIL DEPTH #%d] Moisture: %4.1f%% | Temp: %4.1f C | Salinity EC: %4d uS/cm\n",

i + 1, sensorData[i].moisturePct, sensorData[i].temperatureC, sensorData[i].ecConductivityUsCm);

}

delay(50); // Inter-frame delay on bus

}

// Connect to Wi-Fi and upload profile

Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);

WiFi.mode(WIFI_STA);

WiFi.begin(WIFISSID, WIFIPASSWORD);

int attempts = 0;

while (WiFi.status() != WL_CONNECTED && attempts < 20) {

delay(500);

Serial.print(".");

attempts++;

}

if (WiFi.status() == WL_CONNECTED) {

Serial.println("\n[WIFI] Connected! Dispatching profile telemetry...");

HTTPClient http;

http.begin(TELEMETRY_URL);

http.addHeader("Content-Type", "application/json");

String json = "{\"profileid\":\"FARMBLOCK_A\",\"cycle\":" + String(profileCycleCount) + ",\"layers\":[";

for (int i = 0; i < SENSOR_COUNT; i++) {

json += "{\"depth_layer\":" + String(i + 1);

json += ",\"moisture\":" + String(sensorData[i].moisturePct, 1);

json += ",\"temp_c\":" + String(sensorData[i].temperatureC, 1);

json += ",\"ec\":" + String(sensorData[i].ecConductivityUsCm) + "}";

if (i < SENSOR_COUNT - 1) json += ",";

}

json += "]}";

int res = http.POST(json);

Serial.printf("[HTTP] Dispatched. Server response: %d\n", res);

http.end();

WiFi.disconnect(true);

WiFi.mode(WIFI_OFF);

}

digitalWrite(STATUSLEDPIN, LOW);

// Enter Deep Sleep

Serial.printf("[SYSTEM] Entering deep sleep for %llu seconds...\n\n", SLEEPINTERVALSEC);

espsleepenabletimerwakeup(SLEEPINTERVALSEC * 1000000ULL);

espdeepsleep_start();

}

void loop() {

}

``

Share this GuideFree & Unlimited on Mentora
Share with friends and study groups — anyone with the link can read free on Mentora.
Microcontroller Hub: ESP32 DevKit V1 (Deep-Sleep Enabled Agritech Node)Industrial Soil Sensors: RS485 Modbus RTU Subsurface Probes (Volumetric Water Content [VWC], Electrical Conductivity [EC/Salinity], Temperature, NPK)Bus Transceiver: MAX485 / SP3485 Differential Half-Duplex Transceiver (Hardware UART2 on GPIO 16/17 with GPIO 4 DE/RE Direction Switching)Power Infrastructure: Switched High-Side P-Channel MOSFET Power Gate + 120Ω Differential Line TerminationTarget Search Intent: "MAX485 DE/RE control pin timing glitch ESP32", "Modbus CRC16 calculation mismatch ESP32", "RS485 120 ohm bus termination resistor", "Industrial soil salinity EC sensor Modbus code" Token-Gated Asset

Build a Subsurface Soil Moisture & Salinity Profile Array with ESP32 & RS485 Modbus Firmware & Schematics Vault

Download verified Arduino C++ firmware, pinout table, and BOM for Build a Subsurface Soil Moisture & Salinity Profile Array with ESP32 & RS485 Modbus.

Access Fee
₹49₹199

Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.

ESP32_Subsurface_Soil_RS485_Modbus.ino(Locked Asset)
Token-Gated
#include<Wire.h>
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized

void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }

void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }

float calculatePID(float error, float kp, float ki, float kd) { ... }

void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }

1 Code Snippets & 0 Schematics Locked

Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.

Fritzing & CAD Schematics Pack.FZZ & .PDF

Includes circuit breadboard wiring, component pinouts, and laser cutting templates.

Stuck on Hardware or Code? Book a 1-on-1 Video SessionLive Mentor

Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.

Peer Review & Rewards

Review This Build & Claim a Free ₹49 Token

Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.

5/5 - Outstanding Build

Have a Reward Voucher?

Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.

Ambassador Program

Earn ₹50 Per Friend You Refer

Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!

Frequently Asked Questions

Why do sensors fail to respond on long cable runs (>50m)?

Root Cause: Missing 120Ω Line Termination: High-speed differential edges reflect off open cable ends, corrupting packet bits. Fix: Place a 120Ω resistor directly across lines A and B at the extreme ends of the physical bus.

Why does the MAX485 output garbled characters on ESP32 3.3V logic?

Root Cause: Standard 5V MAX485 modules require 5V VCC, outputting 5V on RO into the ESP32. While tolerant, it introduces threshold distortion. Use an SP3485 (3.3V native transceiver) or power the MAX485 with 5V while using a 3.3V logic divider.

Live Evening Cohort • Interactive Micro-Cohort (Max 6–8 Kids)

Learn this in our Junior Tech: Game Design & Scratch

Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.

Tuesday & Thursday • 5:30 PM – 6:30 PM IST • Monthly After-School Pass: ₹800/mo ₹1,500
Topics:#ESP32#ESP32 & IoT#Circuit Wiring#Arduino Code#DIY Electronics#ESP32 RS485 Modbus soil sensor#MAX485 DE RE timing flush ESP32#Modbus CRC16 calculation Arduino#Soil electrical conductivity EC salinity#Agricultural multi-depth soil profile