---
title: 4-20 mA into an ESP32 — the 165 Ω trick (and the dead zone you must avoid)
description: Why 165 Ω is the correct shunt value for reading 4-20 mA industrial sensors on an ESP32, how to wire it, how to calibrate it into NVS, and why you must never use a 250 Ω shunt with a stock ESP32 ADC.
author: Akshay Sarode
published: 2025-10-22
updated: 2026-03-14
cluster: c6-esp32
tags: [esp32, analog, 4-20ma, industrial, hardware]
reading: 9 min
hero: 4 mA × 165 Ω = 0.66 V. 20 mA × 165 Ω = 3.30 V. The full ESP32 ADC range, no clipping, no dead zone. Here's why every other resistor value is wrong.
---

# 4-20 mA into an ESP32 — the 165 Ω trick

If you Google "ESP32 4-20 mA" you will find roughly fifteen tutorials that tell you to use a 250 Ω shunt resistor "because that gives you 1-5 V". Every one of those tutorials is wrong, and following them will produce a measurement chain with **15% non-linearity in the bottom quarter of the range**. Here is the correct value, why it works, and how to calibrate it.

The short answer: use a **165 Ω 1% precision metal-film resistor**, not 250 Ω, not 100 Ω. 4 mA across 165 Ω gives **0.66 V**. 20 mA across 165 Ω gives **3.30 V**. That is exactly the maximum range of the ESP32 ADC at 11 dB attenuation — no clipping at the top, no dead zone at the bottom.

## Why 250 Ω fails on an ESP32

The 250 Ω value comes from PLC-land where the ADC reference is 1-5 V or 0-10 V and 250 Ω gives you a clean 1 V at 4 mA, 5 V at 20 mA. That works fine on a Schneider Electric Modicon or an Allen-Bradley CompactLogix. It does not work on an ESP32, for two reasons:

1. **The ESP32 ADC tops out at 3.3 V.** A 5 V signal will be clipped — anything above 16.5 mA on a 250 Ω shunt is invisible. You lose the top 22% of your sensor's range.

2. **The ESP32 ADC has a non-linear "dead zone" from roughly 0 V to 800 mV.** Below 800 mV the ENOB drops sharply and the readings drift with temperature and supply voltage. With a 250 Ω shunt and a 4-5 mA reading, you're sitting right in that dead zone.

Both problems disappear with 165 Ω.

## Why 165 Ω specifically

The arithmetic:

```
V_min = 4 mA  × 165 Ω = 0.660 V
V_max = 20 mA × 165 Ω = 3.300 V
ESP32 ADC max (11 dB att) = ~3.30 V
```

That places the entire 4-20 mA span comfortably inside the ESP32's linear ADC region (Espressif's [ADC calibration guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc_calibration.html) lists the linear range as roughly 100 mV to 3.0 V at 11 dB attenuation, with usable readings out to 3.3 V if you use the calibration table). 0.66 V is well above the 100 mV non-linear floor. 3.30 V uses the full top end without clipping.

The other contender — 150 Ω — gives you 0.60 V to 3.00 V, which is fine but wastes 9% of your dynamic range at the top. The other one — 100 Ω — gives 0.40 V to 2.00 V, which puts the bottom in the dead zone and wastes 39% of the dynamic range. 165 Ω is the sweet spot.

## Wiring it up

The 4-20 mA "loop" is current-driven. The sensor (a pressure transmitter, temperature transmitter, level probe, whatever) sits in series with a 24 V DC loop power supply. The current through the loop encodes the measurement. To turn that current into a voltage the ESP32 can read, you put the shunt resistor between the loop's signal-return line and ground, then take the ADC tap at the top of the shunt.

> [!NOTE]
> Diagram placeholder: 4-20 mA loop schematic — 24V loop supply at top, sensor in series (loop+ to sensor input, sensor output to ADC node), 165Ω resistor from ADC node to GND, ADC node also tapped to ESP32 GPIO 4 (ADC1_CH3). GND of ESP32 and GND of loop supply tied together.

A few wiring rules that matter:

- **Tie the ESP32 GND to the loop power supply GND.** The shunt resistor is referenced to that common ground. If the grounds float, you'll see noise that looks like 50 Hz mains pickup but is actually a ground loop.
- **Use a 1% metal-film resistor, not a carbon-film or 5% ceramic.** A 5% tolerance gives you 8.25 Ω of error at the top of the range, which is 1% of full-scale. Metal film 1% drift over the industrial temperature range is roughly 100 ppm/°C — over a 40 °C swing, that's 0.4% drift, which is well below the inherent transmitter accuracy.
- **Don't add a series capacitor for "filtering".** People do this hoping to suppress noise. It introduces a low-pass with a sloppy time constant that wrecks step-response. If you need filtering, do it in software with a 10-sample boxcar.

## Calibrating into NVS

The ESP32-S3 ships with eFuse-stored ADC calibration constants. The Arduino-ESP32 core surfaces these via `analogReadMilliVolts()` — use that, not the raw `analogRead()` which gives you uncalibrated 0-4095 counts.

But factory calibration only gets you to about ±2% at any given point. For better accuracy you do a two-point field calibration with a current source (a Fluke 705 calibrator or a Hart 375 — but a $30 RD6006 bench supply with a current limit also works for a bench setup) and store the offset/gain in NVS.

```cpp
#include <Preferences.h>

Preferences prefs;

struct AdcCal {
  float offset_mv;   // measured at 4 mA
  float scale;       // (20mA - 4mA) / (V_at_20mA - V_at_4mA)
};

AdcCal cal;

void loadCal() {
  prefs.begin("420ma", true);
  cal.offset_mv = prefs.getFloat("offset", 660.0);
  cal.scale     = prefs.getFloat("scale",  16.0 / (3300.0 - 660.0));
  prefs.end();
}

void saveCal(float offset_mv, float scale) {
  prefs.begin("420ma", false);
  prefs.putFloat("offset", offset_mv);
  prefs.putFloat("scale",  scale);
  prefs.end();
}

float readMilliamps(int pin) {
  float mv = analogReadMilliVolts(pin);
  return 4.0f + (mv - cal.offset_mv) * cal.scale;
}
```

The calibration ritual: inject 4.000 mA from your calibrator, read `analogReadMilliVolts()` ten times and average — that is `offset_mv`. Inject 20.000 mA, average again — call that `mv_20`. Compute `scale = 16.0 / (mv_20 - offset_mv)`. Save to NVS. The next boot loads it.

This brings the chain to roughly ±0.3% of full-scale at the ADC, which is below the typical industrial transmitter spec of ±0.5%. You're now limited by the sensor, which is the right place to be limited.

## Avoiding the dead zone

The ESP32-S3 ADC at 11 dB attenuation is non-linear below about 100 mV in the SAR core's response, and Espressif's eFuse calibration helps but doesn't fully fix it. Below 800 mV — depending on board, supply, temperature — you can see ±15 mV of repeatability error. With a 165 Ω shunt, the lowest signal voltage at the legitimate 4 mA sensor floor is 660 mV, comfortably above the danger region.

If you ever see the loop current dip below 3.5 mA you're either looking at a broken sensor (the LED on most 4-20 mA transmitters trips a fault current of ~3.6 mA when the sensor is faulted, by the [NAMUR NE 43 standard](https://www.namur.net/en/recommendations-and-worksheets/current-recommendations.html)) or a wiring issue. Either way that signal is data: emit a `device_fault` event upstream and let your alerting handle it. Don't try to "rescue" it with software interpolation.

## Does it really matter?

Yes. We deploy this in real plants now via the [bench kit](/use-cases/esp32-industrial-bench) and the [pillar build tutorial](/blog/esp32-industrial-monitoring-pillar). The difference between a 250 Ω chain and a 165 Ω chain on the same Endress+Hauser pressure transmitter is roughly 8% measurement error in the bottom third of the range and outright clipping in the top quarter. 165 Ω makes the chain match the sensor's stated accuracy. 250 Ω makes it look like the sensor is broken.

There's a related trick for [0-10 V signals into the ESP32](/blog/esp32-industrial-monitoring-pillar#step-3) — a 1:3 voltage divider — that solves the same kind of clipping problem at the top end. Same math, different circuit.

## External references

- Espressif ADC calibration — [https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/adc_calibration.html](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/adc_calibration.html)
- NAMUR NE 43 fault-signal recommendation — [https://www.namur.net/en/recommendations-and-worksheets/](https://www.namur.net/en/recommendations-and-worksheets/)
- esp32io ADC tutorial — [https://esp32io.com/tutorials/esp32-adc](https://esp32io.com/tutorials/esp32-adc)
- embeddedthere ESP32 + RS485 Modbus — [https://embeddedthere.com/how-to-interface-esp32-with-rs485-modbus-sensors-with-example-code/](https://embeddedthere.com/how-to-interface-esp32-with-rs485-modbus-sensors-with-example-code/)
- microdigisoft ESP32 Modbus tutorial — [https://microdigisoft.com/esp32-with-modbus-rtu-rs485-protocol-using-arduino-ide/](https://microdigisoft.com/esp32-with-modbus-rtu-rs485-protocol-using-arduino-ide/)
- Endress+Hauser 4-20 mA application notes — [https://www.endress.com/](https://www.endress.com/)
