Bare Conductive Beginner Article 1

The MPR121 Sensor in Depth

Understand how the MPR121 capacitive touch controller works, including filtered data, baseline tracking, and reading the 12-bit touch bitmask.

⏱ 12 min read mpr121 capacitive-touch sensor i2c

The MPR121 Sensor in Depth

The heart of the Bare Conductive Touch Board is the MPR121, a 12-channel capacitive touch sensor IC made by NXP (formerly Freescale). Understanding how this chip actually works will save you hours of debugging and unlock advanced behaviours that go far beyond simple “touched / not touched” logic.

What the MPR121 Actually Does

Capacitive sensing works by measuring how much an electrode’s capacitance changes when a human body (or any conductive material) comes near it. The MPR121 doesn’t measure capacitance directly — it measures charge and discharge cycles on each electrode and converts that into a 10-bit count value. Lower counts mean higher capacitance, which typically means something conductive is close to the electrode.

The chip runs all 12 channels continuously in a round-robin scan. At its default settings the scan cycle takes roughly 35 ms for all 12 electrodes, which translates to about a 28 Hz effective sample rate per electrode. That’s fast enough for most interactive applications.

The MPR121 communicates over I2C at up to 400 kHz. On the Touch Board the address is fixed at 0x5C (though the chip supports four addresses via the ADDR pin). The Touch Board library initialises this for you, but knowing the address is useful when you want to add a second MPR121 to the same bus.

Raw Data vs. Filtered Data

The chip maintains two parallel data streams for each electrode:

Raw data is the direct 10-bit capacitance count, updated every scan cycle. It’s noisy — it picks up 50/60 Hz mains interference, nearby radio signals, and any mechanical vibration in the enclosure. You generally never read raw data directly.

Filtered data is the output of a first-order IIR (infinite impulse response) digital filter running inside the MPR121. The filter parameters — charge current, charge time, sample interval, and the first/second filter coefficients — are all configurable via I2C registers. The Touch Board library sets sensible defaults, but you can tune them by writing directly to the AFE (Analogue Front End) registers if you need faster response or better noise rejection.

To read filtered data in code:

#include <MPR121.h>

void setup() {
  Serial.begin(9600);
  MPR121.begin(0x5C);
}

void loop() {
  for (int i = 0; i < 12; i++) {
    int filtered = MPR121.getFilteredData(i);
    Serial.print("E");
    Serial.print(i);
    Serial.print(": ");
    Serial.print(filtered);
    Serial.print("  ");
  }
  Serial.println();
  delay(100);
}

Open the Serial Monitor at 9600 baud and watch the numbers. Touch each electrode in turn and note how much the filtered value drops. That drop size tells you how sensitive each electrode is and is the starting point for calibrating touch thresholds.

Baseline Tracking

Here’s the part that surprises most people: the MPR121 doesn’t just compare filtered data to a fixed reference. It maintains a baseline for each electrode that slowly adapts over time.

The baseline tracks slow environmental changes — temperature drift, humidity, gradual contamination of the electrode surface, even the capacitance added by a nearby object that’s been sitting still for a while. The tracking algorithm is asymmetric by design: the baseline rises quickly when the filtered value is above it (hand moves away), but rises slowly when the filtered value is below it (hand is touching). This asymmetry means that a sustained touch eventually gets absorbed into the baseline, which is why holding your finger on an electrode for several seconds can cause it to stop registering as touched.

You can read the baseline with getBaselineData():

for (int i = 0; i < 12; i++) {
  int baseline  = MPR121.getBaselineData(i);
  int filtered  = MPR121.getFilteredData(i);
  int deviation = baseline - filtered;   // positive means "touched"
  Serial.print("E");
  Serial.print(i);
  Serial.print(" dev=");
  Serial.println(deviation);
}

The touch detection logic is: if (baseline - filtered) > touchThreshold, report a touch. If (baseline - filtered) < releaseThreshold, report a release. Those two thresholds are what you set with setTouchThreshold() and setReleaseThreshold(), which we cover in the Sensitivity Calibration tutorial.

The 12-Bit Touch Bitmask

When you call MPR121.updateTouchData(), the library reads a two-byte register from the chip (registers 0x00–0x01) that contains the current touch status as a 16-bit word. Only the lower 12 bits are used — one bit per electrode. Bit 0 is electrode 0, bit 11 is electrode 11.

This bitmask is efficient: reading the touch state for all 12 electrodes takes a single 2-byte I2C read. You can inspect it directly:

MPR121.updateTouchData();
uint16_t touchBits = MPR121.getTouchData();

for (int i = 0; i < 12; i++) {
  bool touched = (touchBits >> i) & 1;
  Serial.print(touched ? "1" : "0");
}
Serial.println();

You can also check individual electrodes, test multiple at once, or use the bitmask as a key to look up sounds in a table — all without any looping overhead.

Using the MPR121 Library Functions

The Bare Conductive MPR121 library wraps all of this into a clean API. Here’s a reference for the most important calls:

Function What it returns
MPR121.begin(addr) Initialises the chip; returns false if not found
MPR121.updateTouchData() Reads fresh touch status from chip
MPR121.getFilteredData(i) 10-bit filtered capacitance count for electrode i
MPR121.getBaselineData(i) 10-bit baseline value for electrode i
MPR121.getTouchData(i) true if electrode i is currently touched
MPR121.getTouchData() Full 12-bit bitmask
MPR121.touchStatusChanged() true if status differs from last updateTouchData()
MPR121.isNewTouch(i) true if electrode i became touched since last update
MPR121.isNewRelease(i) true if electrode i was just released

A minimal working sketch that prints events looks like this:

#include <MPR121.h>

void setup() {
  Serial.begin(9600);
  if (!MPR121.begin(0x5C)) {
    Serial.println("MPR121 not found. Check wiring.");
    while (true);
  }
  Serial.println("MPR121 ready.");
}

void loop() {
  if (MPR121.touchStatusChanged()) {
    MPR121.updateTouchData();
    for (int i = 0; i < 12; i++) {
      if (MPR121.isNewTouch(i)) {
        Serial.print("Touched: E");
        Serial.println(i);
      }
      if (MPR121.isNewRelease(i)) {
        Serial.print("Released: E");
        Serial.println(i);
      }
    }
  }
}

Practical Tips

Let the baseline settle. After powering up, give the MPR121 around one second before accepting touch events. The baseline needs time to stabilise. Add a delay(1000) at the end of setup().

Electrodes affect each other. Two electrodes placed close together will increase each other’s capacitance (mutual capacitance). Keep electrode traces at least 5 mm apart, or use the MPR121’s built-in guard channel feature on electrode 11 if you need tighter spacing.

Watch the filtered data, not just the events. Open a Serial plotter and log filtered data from all 12 electrodes for a few minutes in your actual installation environment. You’ll often spot interference patterns or coupling between electrodes that aren’t visible in a quiet lab setting.

The chip needs a clean 3.3 V supply. The Touch Board provides this, but if you’re powering from a noisy USB supply or a switching regulator without a capacitor on the rail, you’ll see erratic baseline drift. A 10 µF electrolytic plus a 100 nF ceramic capacitor placed close to the chip’s VDD pin makes a significant difference.

Now that you understand the sensor’s internals, the next tutorial covers how to read touch data efficiently inside your sketch — polling versus interrupt-driven approaches, and how to detect holds and multi-electrode gestures.