---
title: "Reading a 4-20 mA pressure transmitter with X-AI4: loop power, scaling, alarms"
description: "## Introduction Industrial processes rely on accurate pressure monitoring to protect people and equipment. Most plants send pressure as a 4-20 mA current loop, because the signal stays stable over long cable runs and resists electrical noise. A SCADA system…"
url: "https://norvi.io/docs/esp32-4-20ma-input-pressure-x-ai4/"
updated: "2026-10-23"
category: NORVI X / NORVI X TUTORIALS
---

# Reading a 4-20 mA pressure transmitter with X-AI4: loop power, scaling, alarms

## Introduction

Industrial processes rely on accurate pressure monitoring to protect people and equipment. Most plants send pressure as a 4-20 mA current loop, because the signal stays stable over long cable runs and resists electrical noise. A SCADA system gives plant-wide oversight, but operators on the factory floor still need instant local feedback and clear threshold warnings.

This tutorial builds a complete ESP32 4-20mA input on the **NORVI X-CPU-ESPS3-X1** with the **NORVI X-AI4** module. You connect a 0-10 bar pressure transmitter, convert the loop current to bar, raise low, high and sensor-fault alarms, and show the result on the built-in TFT display and front-panel LEDs. The same method turns the controller into an ESP32 pressure sensor monitor for pumps, tanks and compressors.

### What you will learn

- How to connect a 4-20 mA pressure transmitter to the X-AI4 and read it with the 16-bit ADS1115 ADC
- How to convert loop current to bar, and how to detect broken wires and out-of-range signals
- How to build Low, Normal, High and Fault alarm logic with hysteresis
- How to draw a live gauge on the TFT display and drive the front-panel LEDs without blocking the main loop

### What you will need

- NORVI X-CPU-ESPS3-X1 controller and NORVI X-AI4 module
- 24 V DC supply rated at 1 A or higher
- 4-20 mA pressure transmitter, 0-10 bar, with a signal wire and a ground wire (other ranges need two edits in the sketch)
- USB cable and the Arduino IDE with ESP32 board support
- Optional: a 4-20 mA loop calibrator for commissioning

## System Architecture

The system forms a short, deterministic signal chain from the field sensor to the operator display.

![Figure 1: Signal path of the ESP32 4-20 mA input](https://norvi.io/wp-content/uploads/2026/10/Signal-path-of-the-ESP32-4-20-mA-input.png)
Figure 1: Signal path of the ESP32 4-20 mA input

### Signal flow

1. **Sensing.** The transmitter varies its loop current linearly from 4 mA at 0 bar to 20 mA at 10 bar.
2. **Conversion.** The X-AI4 conditions the loop current, and its 16-bit ADS1115 digitizes the result.
3. **Processing.** The ESP32-S3 reads the ADC over I²C, converts the value to bar, and evaluates the alarm limits.
4. **Indication.** The controller draws a gauge on the TFT display and drives the front-panel LEDs through the PCA9536 I/O expander.

### Hardware components

| Component | Role | Key details |
| --- | --- | --- |
| NORVI X-CPU-ESPS3-X1 | Controller | ESP32-S3-WROOM-1U-N16R2 with 16 MB flash and 2 MB QSPI PSRAM; 2.0 in ST7789 TFT (240 × 320); 24 V DC supply |
| NORVI X-AI4 | Analog input | 4 current-loop inputs (4-20 mA / 0-20 mA); ADS1115 16-bit ADC at I²C address 0x49 |
| PCA9536 I/O expander | Front-panel I/O | I²C address 0x41; two configurable LEDs (P1, P2, active LOW) and two push buttons (P0, P3) |
| Pressure transmitter | Field sensor | 0-10 bar, 4-20 mA output, signal wire and ground wire |

### Pin allocation

Correct pin assignment prevents conflicts between the on-board peripherals and the expansion module.

| Function | GPIO | Notes |
| --- | --- | --- |
| I²C bus (SDA / SCL) | IO8 / IO9 | Shared by the ADS1115 (0x49), PCA9536 (0x41) and the touch controller |
| TFT display (CS / DC / RST) | IO45 / IO46 / IO47 | ST7789 panel on the SPI bus |
| Shared SPI bus (MOSI / MISO / SCLK) | IO11 / IO13 / IO12 | Display, Ethernet controller and SD card share these lines |
| Ethernet CS and SD card CS | IO1 and IO42 | Held HIGH so both devices ignore display traffic |
| Expander reset (PORT_RST) | IO38 | Drive HIGH to keep the I/O expanders running |

## Hardware and Wiring

### X-AI4 at a glance

| Parameter | Value |
| --- | --- |
| Inputs | 4 current-loop channels, AIN1 to AIN4 (4-20 mA / 0-20 mA) |
| Input limit | 20 mA per channel |
| ADC | ADS1115, 16-bit, I²C (SDA IO8, SCL IO9) |
| I²C address | Set by DIP switch: 0x49 (DIP 1 ON, default), 0x4B, 0x4A or 0x48 |
| Terminals | AIN1, AIN2, AIN3, AIN4, GND |
| Supply | 24 V DC through the expansion port |

### Connect the module and the transmitter

1. Power off the system.
2. Mount the X-AI4 on the DIN rail beside the X-CPU and seat it on the expansion bus connector.
3. Set DIP 1 ON and DIP 2 to 4 OFF for address 0x49.
4. Connect the transmitter's signal wire to **AIN1** and its ground wire to **GND**.
5. Connect the 24 V DC supply to the X-CPU power terminals, then power on.

![Figure 2: Wiring the 24 V supply, X-CPU, X-AI4 and pressure transmitter (signal to AIN1, ground to GND)](https://norvi.io/wp-content/uploads/2026/10/Wiring-the-24-V-supply-X-CPU-X-AI4-and-pressure-transmitter-signal-to-AIN1-ground-to-GND.png)
Figure 2: Wiring the 24 V supply, X-CPU, X-AI4 and pressure transmitter (signal to AIN1, ground to GND)

### Sensor wiring and loop power

The transmitter has two wires: a signal wire and a ground wire. It has no separate 24 V power line. The X-AI4 supplies the loop through the AIN1 terminal, so current flows out of AIN1, through the transmitter, and back into GND, where the module measures it.

Use shielded twisted-pair cable, ground the shield at one end, and route the cable away from motor and VFD wiring. AIN2 to AIN4 wire the same way. Change the channel number in readADC_SingleEnded() to read them. The X-AV4 voltage module ships with the same default address (0x49), so change the DIP setting on one module if you fit both.

## The 4-20 mA Current Loop

A 4-20 mA loop encodes the measurement in the current itself. The transmitter holds the loop current at the correct value regardless of cable resistance, so long cables add no error and electromagnetic interference has little effect. This makes the loop the standard choice for pressure instrumentation.

- **Noise immunity.** Current stays constant along the cable, whereas a voltage signal sags and picks up interference.
- **Live zero.** Normal operation never drops below 4 mA, so a reading near 0 mA reveals a broken wire or a power failure.
- **Long cable runs.** Voltage drop along the cable does not change the measurement.

The X-AI4 converts the loop current into a voltage, amplifies it, and passes it to the ADS1115. The firmware then converts the ADC count back to milliamps, which gives a precise base for scaling to engineering units.

## Scale 4-20 mA to Pressure

For a 0-10 bar transmitter, current and pressure have a linear relationship. 4.0 mA equals 0 bar (the lower range limit) and 20.0 mA equals 10 bar (the upper range limit). The current span is 16.0 mA and the pressure span is 10.0 bar. Any 4-20 mA ESP32 input converts to pressure with this formula:

*Pressure = P_min + (I − 4 mA) ÷ 16 mA × (P_max − P_min)*

The formula subtracts the 4 mA live zero, divides by the 16 mA span to get the fraction of full scale, and multiplies by the pressure span. For example, a reading of 10.4 mA gives (10.4 − 4) ÷ 16 × 10 = 4.0 bar. Edit P_MIN and P_MAX in the sketch for another transmitter range.

The ADS1115 returns raw counts. At GAIN_ONE (±4.096 V), NORVI's X-AI4 test code converts counts to milliamps with a factor of 4.096 / 3269.826, which equals 0.001253 mA per count. The sketch averages eight conversions at 860 samples per second to reduce noise. Use the values below to check the conversion during commissioning.

| Loop current | Pressure | Expected ADC count |
| --- | --- | --- |
| 4.0 mA | 0.0 bar | 3193 |
| 8.0 mA | 2.5 bar | 6386 |
| 12.0 mA | 5.0 bar | 9580 |
| 16.0 mA | 7.5 bar | 12773 |
| 20.0 mA | 10.0 bar | 15966 |

## Alarms and Fault Detection

### Pressure zones

The ESP32 pressure sensor monitor sorts every reading into one of four states. A hysteresis of 0.2 bar (0.32 mA) keeps an alarm active until the pressure clears its limit. This stops the display and LEDs from chattering when the pressure sits near a threshold.

![Figure 3: Pressure zones, current values and hysteresis bands](https://norvi.io/wp-content/uploads/2026/10/Pressure-zones-current-values-and-hysteresis-bands.png)
Figure 3: Pressure zones, current values and hysteresis bands

| State | Condition | Gauge and badge | LEDs |
| --- | --- | --- | --- |
| Low | Below 3 bar (clears above 3.2 bar) | Orange, Low Pressure | LED 1 on |
| Normal | 3 to 7 bar | Green, Normal Pressure | Both off |
| High | Above 7 bar (clears below 6.8 bar) | Red, High Pressure | LED 2 on |
| Sensor fault | Below 3.8 mA or above 20.2 mA | SENSOR ERROR, --.-- | Both blink |

### Sensor fault detection

Real installations suffer broken wires, loose terminals and failed sensors. The firmware therefore treats any current outside 3.8 to 20.2 mA as a fault. It skips the normal calculation, replaces the value with dashes, shows a SENSOR ERROR banner and blinks both LEDs.

| Loop current | Meaning | Firmware action |
| --- | --- | --- |
| About 0 mA | Broken wire or power loss | Fault state |
| Below 3.8 mA | Under-range or sensor fault | Fault state |
| 3.8 to 20.2 mA | Valid measurement | Scale to bar and evaluate zones |
| Above 20.2 mA | Over-range or sensor fault | Fault state |

The 3.8 mA limit follows NAMUR NE43, and the 20.2 mA limit keeps the signal close to the 20 mA input rating of the X-AI4. A dead loop therefore never looks like a valid 0 bar reading.

### Front-panel LEDs

The X-CPU provides two configurable front-panel LEDs on its PCA9536 I/O expander. The application decides what they mean. This tutorial uses LED 1 for low pressure, LED 2 for high pressure, and both blinking for a sensor fault. The LEDs are active LOW: the sketch writes LOW to turn one on and HIGH to turn it off.

## Arduino Code for the ESP32 4-20 mA Input

### Set up the IDE

- Select **ESP32S3 Dev Module**, then set **USB CDC On Boot** to Enabled, **Flash Size** to 16MB (128Mb) and **PSRAM** to QSPI PSRAM.
- Install **Adafruit ADS1X15** and **LovyanGFX** from the Library Manager. Install **PCA9536D** from the libraries folder of the NORVI GitHub repository. Wire and SPI come with the ESP32 core.

### How the sketch works

- **Sampling.** Every 150 ms the loop averages eight ADS1115 conversions on AIN1, then scales and classifies the result.
- **Non-blocking timing.** millis() controls both the update interval and the fault blink, so the LEDs never stall the sensor or the display.
- **Display engine.** LovyanGFX drives the ST7789 on the shared SPI bus with the SPI2 host, a 40 MHz write speed, the DC, CS and RST pins, and inverted colors to match the panel. Bus sharing stays enabled because the Ethernet controller and SD card use the same lines.
- **Static layout.** The setup routine draws the header, range label, zone ring and scale marks once. The loop redraws only the value, gauge fill and status badge, which prevents flicker.
- **Radial gauge.** fillArc() draws a 270-degree speedometer arc from 135 to 405 degrees. The fill takes the zone color, and the value prints with two decimals in the center.

The complete sketch follows.

```cpp

                /*
  ESP32 4-20 mA input: pressure monitor, 0-10 bar
  NORVI X-CPU-ESPS3-X1 + NORVI X-AI4 (ADS1115, channel AIN1)
*/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_ADS1X15.h>
#include <PCA9536D.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>

// ---------- Sensor and alarm settings ----------
const float P_MIN = 0.0, P_MAX = 10.0;          // bar at 4 mA and at 20 mA
const float LOW_ALARM = 3.0, HIGH_ALARM = 7.0;  // bar
const float HYSTERESIS = 0.2;                   // bar, stops alarm chatter
const float FAULT_LOW_MA = 3.8;                 // below = wire break or sensor fault (NAMUR NE43)
const float FAULT_HIGH_MA = 20.2;               // above = over-range, near the 20 mA input rating
const float MA_PER_COUNT = 4.096 / 3269.826;    // NORVI X-AI4 factor at GAIN_ONE

// ---------- Pins ----------
#define I2C_SDA  8
#define I2C_SCL  9
#define SPI_MISO 13
#define SPI_MOSI 11
#define SPI_SCLK 12
#define ETH_CS   1    // keep the other SPI devices idle
#define SD_CS    42
#define DSP_CS   45
#define PORT_RST 38   // I/O expander reset: HIGH = run
#define IO_LED1  1    // configurable LED 1 (PCA9536 P1, active LOW): low-pressure alarm
#define IO_LED2  2    // configurable LED 2 (PCA9536 P2, active LOW): high-pressure alarm
#define ADS_ADDR 0x49 // X-AI4 DIP 1 ON

Adafruit_ADS1115 ads;
PCA9536 io;

// ---------- Display (ST7789 240x320 on the shared SPI bus) ----------
class LGFX : public lgfx::LGFX_Device {
  lgfx::Panel_ST7789 _panel;
  lgfx::Bus_SPI _bus;
public:
  LGFX() {
    auto b = _bus.config();
    b.spi_host = SPI2_HOST;  b.spi_mode = 0;
    b.freq_write = 40000000; b.freq_read = 16000000;
    b.spi_3wire = false;     b.use_lock = true;
    b.dma_channel = SPI_DMA_CH_AUTO;
    b.pin_sclk = 12; b.pin_mosi = 11; b.pin_miso = 13; b.pin_dc = 46;
    _bus.config(b);
    _panel.setBus(&_bus);

    auto p = _panel.config();
    p.pin_cs = 45;  p.pin_rst = 47;  p.pin_busy = -1;
    p.panel_width = 240;  p.panel_height = 320;
    p.offset_x = 0;  p.offset_y = 0;  p.offset_rotation = 0;
    p.dummy_read_pixel = 8;  p.dummy_read_bits = 1;
    p.readable = true;  p.invert = true;  p.rgb_order = false;
    p.dlen_16bit = false;  p.bus_shared = true;
    _panel.config(p);
    setPanel(&_panel);
  }
};
LGFX tft;

// ---------- State ----------
enum State { ST_NORMAL, ST_LOW, ST_HIGH, ST_FAULT };
State state = ST_NORMAL, ledState = ST_NORMAL;
int lastBadge = -1;
float lastPressure = -999.0;
unsigned long lastUpdate = 0, lastBlink = 0;
bool blinkOn = false;
const unsigned long UPDATE_MS = 150, BLINK_MS = 500;

// ---------- Measurement ----------
float readCurrentMa() {                  // average of 8 conversions on AIN1
  long sum = 0;
  for (int i = 0; i < 8; i++) sum += ads.readADC_SingleEnded(0);
  return (sum / 8.0f) * MA_PER_COUNT;
}

float currentToPressure(float mA) {      // 4 mA = P_MIN, 20 mA = P_MAX
  float bar = P_MIN + (mA - 4.0f) / 16.0f * (P_MAX - P_MIN);
  return constrain(bar, P_MIN, P_MAX);
}

State classify(float mA, float bar, State prev) {
  if (mA < FAULT_LOW_MA || mA > FAULT_HIGH_MA) return ST_FAULT;
  if (prev == ST_LOW  && bar < LOW_ALARM + HYSTERESIS)  return ST_LOW;
  if (prev == ST_HIGH && bar > HIGH_ALARM - HYSTERESIS) return ST_HIGH;
  if (bar < LOW_ALARM)  return ST_LOW;
  if (bar > HIGH_ALARM) return ST_HIGH;
  return ST_NORMAL;
}

// ---------- LEDs (LOW = ON) ----------
void setLeds(bool led1, bool led2) {
  io.digitalWrite(IO_LED1, led1 ? LOW : HIGH);
  io.digitalWrite(IO_LED2, led2 ? LOW : HIGH);
}

void updateLeds() {
  if (state == ST_FAULT) {               // blink both LEDs without blocking
    if (millis() - lastBlink >= BLINK_MS) {
      lastBlink = millis();
      blinkOn = !blinkOn;
      setLeds(blinkOn, blinkOn);
    }
  } else if (state != ledState) {        // write only when the state changes
    setLeds(state == ST_LOW, state == ST_HIGH);
  }
  ledState = state;
}

// ---------- Display ----------
void drawStaticUI() {
  tft.fillRect(0, 0, 240, 35, tft.color565(0, 51, 102));
  tft.setTextDatum(middle_center);
  tft.setTextColor(TFT_WHITE);
  tft.setFont(&fonts::FreeSansBold9pt7b);
  tft.drawString("Pressure Monitoring", 120, 17);
  tft.setTextColor(TFT_LIGHTGREY);
  tft.setFont(&fonts::FreeSans9pt7b);
  tft.drawString("Measuring Range: 0 - 10 Bar", 120, 48);

  // Zone ring: 270-degree sweep from 135 to 405 degrees
  tft.fillArc(120, 160, 82, 85, 135, 216, TFT_YELLOW);               // 0-3 bar
  tft.fillArc(120, 160, 82, 85, 216, 324, tft.color565(0, 200, 0));  // 3-7 bar
  tft.fillArc(120, 160, 82, 85, 324, 405, TFT_RED);                  // 7-10 bar
  tft.fillArc(120, 160, 60, 79, 135, 405, TFT_DARKGREY);             // empty track

  tft.setTextColor(TFT_WHITE);
  tft.drawString("0", 55, 235);
  tft.drawString("10", 185, 235);
}

void updateDisplay(float bar) {
  uint16_t color, arcColor;              // badge colour, gauge fill colour
  const char* text;
  switch (state) {
    case ST_LOW:   color = TFT_ORANGE;  arcColor = TFT_ORANGE;  text = "Low Pressure";    break;
    case ST_HIGH:  color = TFT_RED;     arcColor = TFT_RED;     text = "High Pressure";   break;
    case ST_FAULT: color = TFT_RED;     arcColor = TFT_DARKGREY; text = "ERROR";          break;
    default:       color = tft.color565(0, 200, 0);  arcColor = tft.color565(0, 255, 0);
                   text = "Normal Pressure"; break;
  }

  // Gauge and numeric value
  if (state == ST_FAULT || fabs(bar - lastPressure) >= 0.01) {
    tft.fillRect(72, 134, 96, 38, TFT_BLACK);        // clear the old value
    tft.setTextDatum(middle_center);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);
    tft.setFont(&fonts::FreeSansBold18pt7b);
    if (state == ST_FAULT) {
      tft.drawString("--.--", 120, 150);
      tft.fillArc(120, 160, 60, 79, 135, 405, TFT_DARKGREY);
      lastPressure = -999.0;                         // force a redraw on recovery
    } else {
      char buf[10];
      snprintf(buf, sizeof(buf), "%4.2f", bar);
      tft.drawString(buf, 120, 150);
      float angle = 135.0 + (bar - P_MIN) / (P_MAX - P_MIN) * 270.0;
      tft.fillArc(120, 160, 60, 79, 135, angle, arcColor);
      tft.fillArc(120, 160, 60, 79, angle, 405, TFT_DARKGREY);
      lastPressure = bar;
    }
    tft.setFont(&fonts::FreeSans9pt7b);
    tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
    tft.drawString("Bar", 120, 185);
  }

  // Status badge
  if (state != lastBadge) {
    tft.fillRoundRect(15, 250, 210, 60, 8, color);
    tft.setTextColor(TFT_WHITE, color);
    tft.setTextDatum(middle_center);
    tft.setFont(&fonts::FreeSansBold12pt7b);
    if (state == ST_FAULT) {
      tft.drawString("SENSOR ERROR", 120, 265);
      tft.drawString("Out of Range", 120, 290);
    } else {
      tft.drawString(text, 120, 280);
    }
    lastBadge = state;
  }
}

// ---------- Setup and loop ----------
void setup() {
  Serial.begin(115200);
  delay(200);

  pinMode(PORT_RST, OUTPUT);  digitalWrite(PORT_RST, HIGH);
  delay(100);
  pinMode(ETH_CS, OUTPUT);  digitalWrite(ETH_CS, HIGH);
  pinMode(SD_CS, OUTPUT);   digitalWrite(SD_CS, HIGH);
  pinMode(DSP_CS, OUTPUT);  digitalWrite(DSP_CS, HIGH);

  Wire.begin(I2C_SDA, I2C_SCL);
  delay(100);

  if (!io.begin()) Serial.println("PCA9536 not detected");
  io.pinMode(IO_LED1, OUTPUT);
  io.pinMode(IO_LED2, OUTPUT);
  setLeds(false, false);

  if (!ads.begin(ADS_ADDR)) Serial.println("ADS1115 not detected");
  ads.setGain(GAIN_ONE);                       // +/-4.096 V range
  ads.setDataRate(RATE_ADS1115_860SPS);

  SPI.begin(SPI_SCLK, SPI_MISO, SPI_MOSI);
  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  drawStaticUI();
  Serial.println("Pressure monitor ready");
}

void loop() {
  if (millis() - lastUpdate >= UPDATE_MS) {
    lastUpdate = millis();
    float mA = readCurrentMa();
    float bar = currentToPressure(mA);
    state = classify(mA, bar, state);
    updateDisplay(bar);
    Serial.printf("%.2f mA | %.2f bar | state %dn", mA, bar, state);
  }
  updateLeds();
}
```

## Commissioning and Test

### Commissioning checklist

1. **Power supply.** Verify that the 24 V DC supply is stable and correctly wired to the X-CPU before you energize the system.
2. **I²C addresses.** Confirm that the X-AI4 DIP switches set 0x49 and that the Serial Monitor shows "Pressure monitor ready" with no "not detected" messages. For a full check, run the I²C scan below.
3. **Current injection.** Disconnect the transmitter and connect a loop calibrator in transmitter-simulation mode between AIN1 and GND. Inject 4, 12 and 20 mA, and confirm that the display reads 0.00, 5.00 and 10.00 bar.
4. **Thresholds and LEDs.** Step the current through 8.8 mA (3 bar) and 15.2 mA (7 bar). Confirm the Low, Normal and High states, the badge colors and the LEDs.
5. **Fault response.** Reconnect the transmitter, then disconnect its signal wire. Confirm that SENSOR ERROR appears within about 150 ms and that both LEDs blink.

To scan the I²C bus, call this function from setup() after Wire.begin(). The scan lists 0x49 (X-AI4), 0x41 (PCA9536), 0x15 (touch controller), 0x68 (RTC) and 0x75 (I/O expander).

```
void i2cScan() {
  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) Serial.printf("I2C device found at 0x%02Xn", addr);
  }
}
```

### Expected output

The Serial Monitor prints mA | bar | state for every update, where state 0 is normal, 1 low, 2 high and 3 fault. The display shows the screens in Figure 4.

![Figure 4: Low, normal, high and sensor-fault screens on the X-CPU display](https://norvi.io/wp-content/uploads/2026/10/Low-normal-high-and-sensor-fault-screens-on-the-X-CPU-display.png)
Figure 4: Low, normal, high and sensor-fault screens on the X-CPU display

## Troubleshooting

| Symptom | Likely cause | Remedy |
| --- | --- | --- |
| Display blank or white | Wrong SPI pins, uninitialized display bus, or Tools settings | Confirm CS 45, DC 46 and RST 47 in the LGFX class. Set USB CDC On Boot, Flash Size and PSRAM as in section 7.1. |
| ADS1115 or PCA9536 not detected | Module not seated, address mismatch, or expander in reset | Power off and reseat the X-AI4. Match ADS_ADDR to the DIP setting. Keep GPIO 38 (PORT_RST) HIGH. Run the I²C scan. |
| Reads 0 mA or shows SENSOR ERROR | Broken or loose signal wire | Check the signal wire on AIN1 and the ground wire on GND. Test the cable for continuity. |
| Reading stuck at maximum or wrong | Short across the input, or a changed gain or scale factor | Check for a short between signal and ground. Confirm GAIN_ONE and MA_PER_COUNT are unchanged, then verify with a calibrator. |
| Reading jumps around | Electrical noise or a missing ground reference | Use shielded twisted pair, tie the sensor ground to the system ground, keep the cable away from motor and VFD wiring, and raise the number of averaged samples. |
| LEDs behave in reverse | Active-LOW polarity | Write LOW to turn an LED on and HIGH to turn it off. |
| Alarm flickers near a limit | Pressure hovers at a threshold | Increase HYSTERESIS. |

**Safety.**Disconnect all power before wiring, and let a qualified electrician install the system. This monitor provides indication only. Never use it as a safety function.

## FAQ

**Can an ESP32 read a 4-20 mA signal directly?**

No. The ESP32 ADC reads voltage, not current. The X-AI4 conditions the loop current and digitizes it with a 16-bit ADS1115, so your ESP32 4-20mA input code reads clean values over I²C.

**How do I connect a 4-20 mA ESP32 pressure sensor?**

Connect the transmitter's signal wire to AIN1 and its ground wire to GND, as Figure 2 shows. The sensor needs no separate 24 V power line.

**Can I use a different pressure range or a 0-20 mA sensor?**

Yes. Edit P_MIN and P_MAX for another range. For a 0-20 mA sensor, change the 4 mA offset and 16 mA span in currentToPressure() to 0 and 20, and lower the fault limit.

**Can I read more than one transmitter?**

Yes. The X-AI4 has four channels. Read AIN2 to AIN4 with readADC_SingleEnded(1) to (3), and keep a separate scale and alarm state for each sensor.

## Conclusion

You now have a working ESP32 4-20 mA input that reads a pressure transmitter, scales the loop current to bar, raises low, high and sensor-fault alarms, and shows the result on the X-CPU display and LEDs. To go further, publish the pressure over MQTT or log it to the SD card.

**Resources**

[NORVI X-CPU-ESPS3-X1 user guide](https://norvi.io/docs/norvi-x-cpu-esps3-x1-user-guide/) | [X-CPU datasheet](https://norvi.io/docs/norvi-x-cpu-esps3-x1-datasheet/)

[NORVI X-AI4 user guide](https://norvi.io/docs/norvi-x-ai4-user-guide/) | [X-AI4 datasheet](https://norvi.io/docs/norvi-x-ai4-datasheet/) | [Firmware examples and libraries (GitHub)](https://github.com/NORVIControllers/NORVI-X-Version-02)
