If you want to interface industrial-grade sensor instruments with a 3.3V microcontroller like the ESP32, you cannot connect them directly. The direct answer: you must use a precision shunt resistor (typically 165Ω) to drop the 4-20mA current loop into a 0.66V–3.3V range, and then sample that voltage with an external 16-bit ADC like the ADS1115. Relying on the ESP32’s internal ADC for precision sensor instruments will yield unusable data due to its inherent non-linearity and noise floor.

The 4-20mA Standard in Industrial Sensor Instruments

Industrial sensor instruments use a current loop rather than a voltage signal to transmit data over long distances. The internal transmitter circuitry varies the loop current between 4mA (representing 0% of the measured physical quantity) and 20mA (representing 100%). Because the signal is current-based, it is largely immune to voltage drop caused by wire resistance over hundreds of feet of cable, making it the undisputed backbone of process control and factory automation.

The actual sensing principle depends on the physical variable being measured. A pressure sensor instrument typically uses a piezoresistive Wheatstone bridge that deforms under stress, while a temperature instrument might use a PT100 RTD whose resistance changes predictably with heat. Regardless of the physical transduction method, the sensor's internal microcontroller measures this raw change, linearizes it, and drives the 4-20mA output transistor. This "live zero" at 4mA is a critical feature: it allows the receiving controller to distinguish between a true zero reading (4mA) and a broken wire or dead transmitter (0mA).

Table 1: Common 4-20mA Sensor Instruments and Specifications
Instrument Type Typical Model / Brand Measurement Range Excitation Voltage Accuracy / Output
Pressure Transmitter Keller PAA-33X 0 to 100 bar 8–32V DC ±0.05% FS (Linearity)
Temperature Transmitter Endress+Hauser TMT82 -200°C to +850°C (PT100) 12–36V DC ±0.15°C (Digital/Analog)
Electromagnetic Flowmeter Yokogawa ADMAG AXF 0 to 10 m/s flow velocity 100–240V AC / 24V DC ±0.2% of Reading
Ultrasonic Level Sensor Siemens SITRANS Probe LU 0.3m to 6m (Liquid) 24V DC (Loop Powered) ±3mm (Resolution)
Load Cell Transmitter HBM 1-PAD 0 to 50 kN (Tension/Comp) 24V DC ±0.02% of Rated Output

Hardware Wiring and Signal Conditioning

The output of a 4-20mA sensor instrument is a modulated DC current, not a voltage. A microcontroller's ADC cannot read current directly. You must convert this current into a voltage using a precision burden (shunt) resistor. While you could use a 250Ω resistor to get a 1-5V signal, that exceeds the 3.3V logic limit of the ESP32 and will fry your board if a fault pushes the loop to 24mA. Instead, we use a 165Ω, 0.1% tolerance precision resistor (such as the Vishay Y1485 series). This yields 0.66V at 4mA and exactly 3.30V at 20mA.

Bench Tip: Never use a standard 5% carbon film resistor for your shunt. The temperature coefficient (often ±200 ppm/°C) will cause your readings to drift wildly as your enclosure heats up. Spend the extra $4 on a 0.1% metal foil or thin-film resistor.

Because the ESP32's internal ADC is notoriously non-linear at the extremes of its range and suffers from high noise, we route the shunt voltage into an external ADS1115 16-bit I2C ADC. This provides clean, linear, and highly repeatable readings for your sensor instruments.

Table 2: Wiring Pinout for ESP32, ADS1115, and 4-20mA Loop
Component Pin / Terminal Connects To Notes / Supply Range
Sensor Instrument Red (+) / Power 24V DC Power Supply (+) Typical excitation: 12-30V DC
Sensor Instrument Black (-) / Signal 165Ω Shunt Resistor (Pin 1) Current return path
165Ω Shunt Pin 1 (High Side) ADS1115 A0 (Analog In) Measure voltage across shunt here
165Ω Shunt Pin 2 (Low Side) Common Ground (GND) Must share ground with ADS1115
ADS1115 Module VDD ESP32 3.3V Pin Supply range: 2.0V - 5.5V
ADS1115 Module GND Common Ground (GND) System ground reference
ADS1115 Module SCL / SDA ESP32 GPIO 22 / GPIO 21 I2C bus (add 4.7k pull-ups if missing)

The Raw-to-Unit Math (Scaling and Calibration)

Once the hardware is wired, you need to translate the raw digital values from the ADS1115 into meaningful engineering units (e.g., bar, °C, or liters-per-minute). The ADS1115 returns a 16-bit signed integer. With the internal Programmable Gain Amplifier (PGA) set to ±4.096V, a reading of 3.3V corresponds to a raw value of approximately 26666.

Here is the exact math to map the raw ADC reading to your physical unit:

  1. Calculate Raw Offsets: At 4mA (0.66V), the ADS1115 reads ~5333. At 20mA (3.30V), it reads ~26666.
  2. Normalize to Percentage: Percent = (Raw - Raw_4mA) / (Raw_20mA - Raw_4mA)
  3. Scale to Engineering Units: Value = (Percent * (Scale_Max - Scale_Min)) + Scale_Min

Below is the complete, copy-pasteable C++ code for the ESP32 using the Adafruit_ADS1X15 library. This includes a moving average filter to smooth out high-frequency noise common in industrial environments.

#include <Wire.h>
#include <Adafruit_ADS1X15.h>

Adafruit_ADS1115 ads;

// Calibration constants for 165-ohm shunt and ADS1115 @ 4.096V gain
const float RAW_4MA = 5333.0;   // Raw ADC value at 4mA (0.66V)
const float RAW_20MA = 26666.0; // Raw ADC value at 20mA (3.30V)

// Sensor instrument physical range (e.g., 0 to 100 bar pressure transmitter)
const float SCALE_MIN = 0.0;
const float SCALE_MAX = 100.0;

const int SAMPLE_SIZE = 10;
float readings[SAMPLE_SIZE];
int readIndex = 0;
float total = 0;

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22); // ESP32 default I2C pins
  
  if (!ads.begin(0x48)) {
    Serial.println("Failed to initialize ADS1115. Check wiring.");
    while (1);
  }
  
  // Set gain to +/- 4.096V (1 bit = 0.125mV)
  ads.setGain(GAIN_ONE);
  
  // Initialize filter array
  for (int i = 0; i < SAMPLE_SIZE; i++) {
    readings[i] = 0;
  }
}

void loop() {
  int16_t raw_adc = ads.readADC_SingleEnded(0); // Read from A0
  
  // Moving average filter implementation
  total = total - readings[readIndex];
  readings[readIndex] = raw_adc;
  total = total + readings[readIndex];
  readIndex = (readIndex + 1) % SAMPLE_SIZE;
  
  float average_raw = total / SAMPLE_SIZE;
  
  // Raw-to-Unit Math
  float normalized_pct = (average_raw - RAW_4MA) / (RAW_20MA - RAW_4MA);
  
  // Clamp values to handle broken wire (under 4mA) or over-pressure (over 20mA)
  if (normalized_pct < 0.0) normalized_pct = 0.0; 
  if (normalized_pct > 1.0) normalized_pct = 1.0;
  
  float physical_value = (normalized_pct * (SCALE_MAX - SCALE_MIN)) + SCALE_MIN;
  
  // Broken wire detection (Live Zero check)
  if (raw_adc < (RAW_4MA * 0.75)) {
    Serial.println("ERROR: Broken wire or dead transmitter (< 3mA detected)");
  } else {
    Serial.printf("Raw: %.0f | Pressure: %.2f bar\n", average_raw, physical_value);
  }
  
  delay(250); // 4Hz sampling rate
}

Interference Sources and Calibration Protocols

Industrial environments are electrically hostile. When your ESP32 is reading precision sensor instruments, you will inevitably encounter noise. The three most common interference sources are:

  • Variable Frequency Drives (VFDs): VFDs switching high currents create massive electromagnetic interference (EMI). This induces high-frequency common-mode noise on your sensor cables. Fix: Use twisted-pair shielded cable for the 4-20mA loop, and ground the shield at only one end (the controller side) to prevent ground loops.
  • Ground Loops: If the sensor instrument is powered by a 24V supply in a remote panel, and the ESP32 is powered by a USB supply on your desk, the two grounds will be at different potentials. This causes current to flow through the ADC ground reference, offsetting your readings. Fix: Use an isolated DC-DC converter (like the B0505S-2WR3) and an analog isolator (like the ISO124) between the shunt and the ADS1115.
  • Contactors and Relays: Switching inductive loads causes voltage spikes that can reset the ESP32 or corrupt I2C data lines. Fix: Keep I2C traces short, use 4.7kΩ pull-up resistors on SDA/SCL, and physically separate logic wiring from mains AC wiring.
Calibration Protocol: Never trust the default RAW_4MA and RAW_20MA constants without verifying them. To properly calibrate your system, inject a precise 4.000mA and 20.000mA signal using a dedicated loop calibrator (like the Fluke 707). Record the exact raw ADS1115 integers returned at both points, and update the constants in your code. This 2-point trim eliminates shunt tolerance errors and ADC offset drift.

For a deeper understanding of loop dynamics and burden resistor selection, refer to the Analog Devices primer on 4-20mA current loops. Additionally, if you decide to bypass the external ADC and attempt to use the ESP32's internal ADC (not recommended for precision work), consult the Espressif ESP-IDF ADC documentation to understand the required eFuse Vref calibration routines to minimize non-linearity.