Blog / Embedded

  • esp32
  • adc
  • wifi
  • embedded
  • esp-idf
  • debugging

Why Your ESP32 ADC Readings Wander Once WiFi Is On

Your analogue sensor reads a nice steady value on the bench, then you add WiFi and it starts jumping about, or stops working altogether. Two separate problems hide under that one symptom, and they need different fixes. One is a hard hardware conflict; the other is plain old noise.

This is about the original ESP32 (the one with the 12-bit ADC and its famously grumpy linearity). Other chips in the family differ in the details, so check your datasheet before assuming any of this carries over.

Problem one: ADC2 belongs to the WiFi driver

The ESP32 has two ADC units. ADC2 is shared with the WiFi driver, so you cannot use it while WiFi is running. Espressif's documentation says this plainly, but it is easy to miss when you picked a pin because it was convenient on the board.

If your sensor sits on an ADC2 pin, reads either fail with an error or return junk once WiFi starts. It looks like wandering values if you never check the return code.

The fix is boring: move the sensor to an ADC1 pin. On the original ESP32 those are GPIO32 to GPIO39. GPIO34 to GPIO39 are input-only, which suits a sensor nicely.

  • ADC1: GPIO32 to GPIO39, safe alongside WiFi
  • ADC2: GPIO0, 2, 4, 12 to 15, 25 to 27, conflicts with WiFi
  • Also check the pin is not a strapping pin or wired to something on your dev board

Problem two: the radio makes the supply noisy

Once you are on ADC1 and it still wanders, you are looking at noise. WiFi transmit bursts draw a lot of current in short pulses, and that shows up as ripple on the 3.3 V rail. The ADC measures against its own reference, but the analogue front end does not enjoy a dirty supply.

A telltale sign is periodicity. In power-save mode the radio wakes for beacons, typically every 102.4 ms with the default interval. If your readings spike on a regular rhythm near that, the radio is your suspect.

(I covered the harsher version of this, where the rail sags far enough to reset the chip, in the post about brownouts when WiFi connects. This one is the milder cousin: no reset, just a dirty number.)

Quick detour: why the ADC is not that accurate anyway

Hang on, it is worth knowing that even with WiFi off, the original ESP32 ADC is not linear across its range. The internal reference varies from chip to chip, and readings near the very bottom and top of each attenuation range are compressed.

ESP-IDF handles part of this with calibration, using values burned into eFuse at the factory. Without calibration you are converting raw counts to millivolts with a guess. With it, you get a noticeably better answer, though not laboratory grade.

The fix: calibrate, then take a median

Noise from radio bursts tends to arrive as occasional outliers, not a smooth blur. That makes the median a better filter than the mean: one wild sample out of fifteen simply falls off the end of the sorted list, where an average would drag it in.

Here is a one-shot read using ESP-IDF's oneshot driver with line-fitting calibration. Channel 6 on ADC1 is GPIO34 on the original ESP32.

#include <stdlib.h>
#include "esp_err.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"

#define SAMPLES 15

static adc_oneshot_unit_handle_t adc;
static adc_cali_handle_t cali;

static int cmp_int(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;
}

void adc_setup(void)
{
    adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1 };
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &adc));

    adc_oneshot_chan_cfg_t chan_cfg = {
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc, ADC_CHANNEL_6, &chan_cfg));

    adc_cali_line_fitting_config_t cali_cfg = {
        .unit_id = ADC_UNIT_1,
        .atten = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_DEFAULT,
    };
    ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_cfg, &cali));
}

/* Median of SAMPLES raw reads, converted to millivolts. */
int adc_read_mv(void)
{
    int raw[SAMPLES];
    for (int i = 0; i < SAMPLES; i++) {
        ESP_ERROR_CHECK(adc_oneshot_read(adc, ADC_CHANNEL_6, &raw[i]));
    }
    qsort(raw, SAMPLES, sizeof raw[0], cmp_int);

    int mv;
    ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali, raw[SAMPLES / 2], &mv));
    return mv;
}

Note the median is taken on raw counts and converted afterwards. Converting once is cheaper, and the calibration curve is monotonic, so the order is the same either way.

ESP_ERROR_CHECK aborts on failure, which is fine for a demo and useful for catching an ADC2 mistake loudly. In real firmware you would handle the error and carry on.

Hardware helps more than software

Filtering in code is the cheap option. If the readings matter, a few components do more than any amount of clever sorting:

  • A 100 nF ceramic capacitor from the ADC pin to ground, close to the pin, to give the sampling stage a low-impedance reservoir
  • A source impedance that is low, roughly in the low kilohms or less; a high-value resistor divider feeding the pin will read badly and pick up noise
  • Decent decoupling on the 3.3 V rail, with a bulk capacitor near the module to soak up transmit pulses
  • Keep sensor wiring away from the antenna and from switching supply components

If you need proper accuracy, stop fighting the internal ADC. An external I2C converter such as an ADS1115 has its own reference and does not care what the radio is doing. It costs a couple of pounds and saves an afternoon.

How to tell which problem you have

A quick check separates the two causes:

  1. Log the return value of every read. Errors after WiFi starts mean ADC2.
  2. Read a fixed known voltage, such as a divider from the 3.3 V rail, with WiFi off, then on.
  3. Print the min and max of each batch of samples, not just the result. A tight spread that shifts is a reference issue; a mostly tight spread with occasional spikes is radio noise, and the median deals with it.

Nine times out of ten the culprit is simply a sensor wired to the wrong pin. It is worth checking that first, before you reach for the soldering iron.