Project 1: Capacitive Touch D-Pad

Difficulty: Beginner    Version: V2


This Week: Capacitive Touch Deep Dive

Last week you learned to display things on your OLED. This week, you'll learn to sense touch - the input side of the equation. By the end of this progression, you'll control animated graphics with just your fingertips.

The Big Picture: Traditional buttons require physical switches, wiring, and often external resistors. Capacitive touch uses the electrical properties of your body to detect contact - no moving parts, no extra components, just a wire and your finger.

What is Capacitive Touch?

Your body is slightly conductive and carries a small electrical charge. When you touch a conductive surface (like a wire connected to a GPIO pin), you change the capacitance of that circuit. The ESP32-S3 can measure this change.

Term Plain English
Capacitance The ability to store electrical charge. Think of it like a tiny battery that charges/discharges very fast.
touchRead() Arduino function that returns a number representing how much capacitance the pin sees.
Threshold The dividing line between "touched" and "not touched" - a number you choose.
Calibration Finding the right threshold for YOUR setup (wire length, humidity, etc. all affect readings).

Capacitive Touch FAQ

General Capacitive Touch Questions

Question Answer
Why does it work through thin materials? Capacitance works through insulators! Your phone screen has glass over the sensors. You can put tape or thin plastic over your touch wires.
Why doesn't it work with gloves? Thick insulating materials (rubber, leather) block the capacitive coupling. Special "touchscreen gloves" have conductive fingertips.
Does humidity affect it? Yes! Humid air is more conductive. Your thresholds might need adjustment on rainy days vs dry winter days.
Can I make larger touch areas? Yes - use copper tape, aluminum foil, or conductive fabric. Larger area = more capacitance change = easier to detect.
Why do readings fluctuate? Electrical noise from nearby devices, fluorescent lights, and even your body's static charge cause variation. That's why we use thresholds, not exact values.

ESP32-S3 Specific Questions

Question Answer
Which pins support touch? On XIAO ESP32-S3: GPIO 1-7 (D0-D6). That's 7 touch inputs! Not all ESP32 pins have touch capability.
What values does touchRead() return? On ESP32-S3: roughly 20,000-30,000 when NOT touched, 80,000-150,000+ when touched. (Original ESP32 was opposite - lower when touched!)
Why is ESP32-S3 different from original ESP32? Different touch sensing hardware. S3 uses a newer design where touched = HIGHER values. Old tutorials might have it backwards!
Can I use touch and I2C on the same pins? Tricky! GPIO 5 and 6 are default I2C (SDA/SCL) for our OLED. While they CAN do touch, we avoid them to prevent conflicts.
How fast can I read touch? touchRead() takes about 0.5ms. At 60fps (16ms per frame), you can easily read 4+ pins with time to spare.

The Touch Pin Mapping

We're using 4 of the 7 available touch pins to create a D-Pad (directional pad):

XIAO Label GPIO Number Our Use Wire Color Suggestion
D0GPIO 1LEFTRed
D1GPIO 2UPGreen
D2GPIO 3DOWNBlue
D3GPIO 4RIGHTYellow
Hardware Setup: Just connect a jumper wire to each pin. The wire itself IS the touch sensor! Let the wires hang freely - you'll touch the exposed metal ends or the insulation near the end.

Install the Library

This project uses Adafruit SH110X (different from U8g2 we used in Week 2). Why? It's simpler for beginners and works great with Adafruit's graphics library.

  1. Go to: Sketch → Include Library → Manage Libraries
  2. Search: Adafruit SH110X
  3. Install: "Adafruit SH110X" by Adafruit
  4. When prompted, also install "Adafruit GFX Library"

The Code

Copy this entire sketch, paste into Arduino IDE, and upload:

/*
 * CapTouchPinsIntro.ino
 * V2: D-Pad Visualizer
 *
 * Mapping:
 * D0 (GPIO 1) -> LEFT
 * D1 (GPIO 2) -> UP
 * D2 (GPIO 3) -> DOWN
 * D3 (GPIO 4) -> RIGHT
 */

#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Wire.h>

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

// Touch Pins
const int PIN_LEFT = 1;  // D0
const int PIN_UP = 2;    // D1
const int PIN_DOWN = 3;  // D2
const int PIN_RIGHT = 4; // D3

int vL, vU, vD, vR;

void setup() {
  Serial.begin(115200);

  // XIAO ESP32S3 I2C pins
  Wire.begin(5, 6);
  Wire.setClock(400000);

  display.begin(0x3C, true);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.println("D-Pad Init...");
  display.display();
  delay(500);
}

void drawArrow(int x, int y, int dir, bool active) {
  // dir: 0=L, 1=U, 2=D, 3=R
  if (active)
    display.fillCircle(x, y, 10, SH110X_WHITE);
  else
    display.drawCircle(x, y, 10, SH110X_WHITE);

  int color = active ? SH110X_BLACK : SH110X_WHITE;

  if (dir == 0) { // Left
    display.drawLine(x + 4, y, x - 4, y, color);
    display.drawLine(x - 4, y, x, y - 4, color);
    display.drawLine(x - 4, y, x, y + 4, color);
  } else if (dir == 1) { // Up
    display.drawLine(x, y + 4, x, y - 4, color);
    display.drawLine(x, y - 4, x - 4, y, color);
    display.drawLine(x, y - 4, x + 4, y, color);
  } else if (dir == 2) { // Down
    display.drawLine(x, y - 4, x, y + 4, color);
    display.drawLine(x, y + 4, x - 4, y, color);
    display.drawLine(x, y + 4, x + 4, y, color);
  } else if (dir == 3) { // Right
    display.drawLine(x - 4, y, x + 4, y, color);
    display.drawLine(x + 4, y, x, y - 4, color);
    display.drawLine(x + 4, y, x, y + 4, color);
  }
}

void loop() {
  vL = touchRead(PIN_LEFT);
  vU = touchRead(PIN_UP);
  vD = touchRead(PIN_DOWN);
  vR = touchRead(PIN_RIGHT);

  // Detect touch - threshold calibrated for ESP32-S3
  // Touched = value > 80000
  bool tL = vL > 80000;
  bool tU = vU > 80000;
  bool tD = vD > 80000;
  bool tR = vR > 80000;

  display.clearDisplay();

  // D-Pad Layout
  int cx = 64;
  int cy = 32;
  int spacing = 22;

  drawArrow(cx - spacing, cy, 0, tL); // Left
  drawArrow(cx, cy - spacing, 1, tU); // Up
  drawArrow(cx, cy + spacing, 2, tD); // Down
  drawArrow(cx + spacing, cy, 3, tR); // Right

  // Show raw values (for calibration)
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);

  display.setCursor(0, 0);
  display.print(vL);
  display.setCursor(50, 0);
  display.print(vU);

  display.setCursor(0, 56);
  display.print(vD);
  display.setCursor(50, 56);
  display.print(vR);

  display.display();
  delay(16);
}

Calibrating Your Threshold

The threshold value (80000) might need adjustment for your specific setup:

  1. Upload the code and watch the numbers in the corners of the display
  2. Note values when NOT touching (typically ~20,000-30,000)
  3. Note values when touching (typically ~80,000-150,000)
  4. Set your threshold halfway between these values
Problem Solution
Buttons feel unresponsive Lower the threshold (try 50000 or 40000)
Triggers without touching Raise the threshold (try 100000 or 120000)
Works sometimes, not others Your threshold is too close to idle values - increase the gap
Different pins need different thresholds Use separate threshold variables for each pin

Want to Build This Yourself?

Instead of copying, try building piece by piece with Gemini:

Step Gemini Prompt to Try
1. Read touch values "Write ESP32-S3 code using touchRead() to read 4 capacitive touch pins and print values to Serial Monitor"
2. Add threshold "Add boolean variables that are true when touch value exceeds 80000"
3. Draw circles "Using Adafruit SH1106G, draw 4 circles in a D-pad layout on a 128x64 OLED"
4. Make interactive "Fill the circle when its corresponding touch pin is active"
5. Add arrows "Draw arrow symbols inside each circle pointing in the appropriate direction"

Key Concepts to Understand

Next Up: In Project 2, you'll use this D-Pad to control a cursor and draw on screen - building an Etch-A-Sketch!