gabriel stacey-chartrand

Week 09: input devices

This week I worked with five different inputs: a custom mutual capacitance proximity detector, a velostat pressure sensor, a KY-040 rotary encoder, the BNO085 IMU as an accelerometer for yaw sensing, and the BNO085 magnetometer as a magnetic rotary encoder. I didn't have a dedicated encoder IC like an AS5600 on hand, so I used the magnetometer directly with a small cube magnet.

Mutual capacitance touch / proximity sensor

I wanted to make a sensor that could detect proximity, not just touch. Mutual capacitance works by transmitting a charge on one electrode and measuring how much coupling makes it across to the receiving electrode, a nearby hand disturbs that coupling in a measurable way.

initial breadboard setup, two copper foil pads connected to Barduino (ESP32-S3) for mutual capacitance sensing

My starting point was the Arduino Starter Kit's "Touch Sensor Lamp" example, which uses the CapacitiveSensor library on two pins. I shared it with Claude and asked about getting it running on the Barduino 4.0.2.

Download capacitive-sensor-library-example.ino

CapacitiveSensor library not available on ESP32, it's AVR-only, so I had to write the charge-transfer logic from scratch

The CapacitiveSensor library only works on AVR boards, not the ESP32. I asked Claude:

"Yes but I'd like to try mutual capacitance using 2 pins. Ideally I want to sense proximity as well as direct touch. Or for it to work through plastic for example."

Claude wrote a charge-transfer sketch that drives a TX pad and times how long it takes the charge to couple across to an RX pad. This was the first version that actually ran, values sitting around 22 at rest and spiking to about 195 on direct pad touch.

Download mutual-cap-test-01.ino

I reported back: "Ok I used a 1M resistor and the reading seems to sit at around 21-22 when left alone. It goes to around 22-23 when I place my finger about 1 cm above the two copper pads. And it goes to 0 when I touch both. It shows 20000 and TOUCH when I touch the one connected to RX_PIN (13)". Claude switched to an analogRead approach with a brief charge window to simplify things. The readings came out completely inverted, the sensor was reading TOUCH at rest and dropping when I actually touched the pad.

Download mutual-cap-test-02.ino

I told Claude: "It's constantly reading 4000 something and saying TOUCH when untouched and it's reading 2000 something and saying PROXIMITY when I touch one of the pads. Nothing changes when my finger approaches though." To try to fix the inverted readings, Claude added a startup baseline calibration and more averaging. The delta values stayed at 0–2 regardless of where my hand was. analogRead just wasn't working.

Download mutual-cap-test-03.ino

After I said "Ok touch works when I touch the RX_PIN (13) but nothing happens when I approach the pads", Claude offered a few honest fallback options (built-in ESP32 touch hardware, bigger pads, or accepting touch-only). I pushed back: "No, I want to create a mutual capacitance sensor to place into a pomodoro timer watch I'm designing. I want this to work." Claude had me strip everything back to bare analogRead with no averaging to rule out any software artifacts. The output was completely flat at around 3750, zero response to hand proximity. The ESP32-S3 ADC is just too noisy for this.

Download mutual-cap-test-04.ino

I told Claude that nothing was changing no matter how close I got, but the very first version had shown some change, so something must have broken along the way. Claude went back to the charge-counting approach with a much lower timeout ceiling. I went back and got real proximity data, delta 1 at 9cm, delta 2 at 3cm, delta 3 at 1cm. The signal was there but weak.

Download mutual-cap-test-05.ino

I reported the readings back to Claude, and it cranked SAMPLES up to 2000 to try to average out the noise, but the response barely moved. Claude pointed out that the pad geometry itself was the bottleneck, two small squares sitting side by side don't have enough shared edge to build up a meaningful signal. I mentioned the pads were about 12x12mm and 7mm apart, sized to fit inside my pomodoro watch, and Claude suggested switching to an interdigitated comb pattern to massively increase the shared edge between TX and RX.

Download mutual-cap-test-06.ino

This demo was still running mutual-cap-test-06.ino, with the original simple side-by-side copper pads, just before I switched over to interdigitated pads.

At this point I cut an interdigitated electrode pattern with a utility knife. The comb geometry puts much more shared edge between the TX and RX pads, which dramatically increases the coupling area.

I found this fanatastic document on capactive touch sensor design: Capacitive Touch Sensor Design Guide

close-up of the original copper foil pads, TX to GPIO 12, RX to GPIO 13, 1MΩ resistor bridging both signal lines
interdigitated TX/RX copper pad, the comb pattern maximises coupling area between the two electrodes

This next recording is from the same mutual-cap-test-06.ino code (linked above), now running with the new interdigitated pads.

I also tested the sensor through a piece of 2mm thick PLA from a test print that was laying around and it was able to detect proximity as well as touch, still running the same mutual-cap-test-06.ino code.

Velostat pressure sensor

I then made my own pressure sensor from copper tape and velostat, a carbon-black film that gets more conductive when you compress it. The plan was to eventually replace the tactile button for my custom pomodoro timer watch board. I wired it as a voltage divider with a 3.3K resistor and connected it to GPIO 12 on the Barduino.

soldering station set up, copper tape squares, flux, solder, and wire ready to go
velostat sensor components, two copper foil squares wired up, with a square of velostat
completed velostat sensor, velostat sandwiched between copper foil electrodes, wrapped in rubbery tape for extra travel when pressing
breadboard circuit, voltage divider wired up and connected to the Barduino for testing

The pressure sensor readout worked well in the serial monitor, from 0-4095 depending on how much force I used.

Download velostat-pressure-sensor-test-01.ino

I then mapped the pressure input reading to control the brightness of the NeoPixel on the Barduino.

Download velostat-pressure-sensor-test-02-LED.ino

KY-040 rotary encoder

The KY-040 was straightforward to wire up, CLK on GPIO 13, DT on GPIO 14, SW on GPIO 15, but had a lot of bounce. I fixed that with the ESP32Encoder library. Once it was reading cleanly I mapped the position to NeoPixel brightness, then to colour temperature.

Download rotary-encoder-test-02.ino

Download rotary-encoder-LED-dim.ino

Download rotary-encoder-LED-colourTemp.ino

Adafruit BNO085 IMU

I wanted to explore using the BNO085 as a more interesting input. It's a 9-DOF IMU with an accelerometer, gyroscope, and magnetometer all in one. I asked Claude:

"I'd like to play around with an accelerometer, specifically the Adafruit BNO085 board. I don't have a dedicated magnetic encoder like the AS5600 on hand, so I'd like to use the BNO085 as if it were a rotary encoder, having it rotate on a knob to tell the position of the knob. I'm using my XIAO ESP32-S3 Sense board connected via STEMMA QT."

Claude suggested starting with reading orientation as a stand-in rotary encoder, since it would be faster to get working than the magnetometer approach, and we could try that second.

Adafruit BNO085, 9-DOF IMU
BNO085 I2C pin documentation, STEMMA QT connector as an option
wiring a STEMMA QT connector to connect the XIAO ESP32-S3 to the BNO085
BNO085 connected to the XIAO via STEMMA QT, green LED confirms power
Adafruit Library Manager documentation for the BNO08x
installing the required libraries, Adafruit Unified Sensor, BusIO, and BNO08x
"BNO08x not found, check wiring!", the XIAO needs an explicit Wire.begin(5, 6) to use the right I2C pins
I2C scanner coming up empty, confirmed a wiring config issue, not a code problem

First I2C check code

        cpp#include 

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);
  Wire.begin();
  Serial.println("Scanning I2C...");

  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
      Serial.print("Device found at 0x");
      Serial.println(addr, HEX);
    }
  }
  Serial.println("Done.");
}

void loop() {}
      
after adding Wire.begin(5, 6): BNO085 shows up at address 0x4A

Correct I2C check code

        #include 

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);
  Wire.begin(5, 6); // SDA=GPIO5, SCL=GPIO6
  Serial.println("Scanning I2C...");

  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
      Serial.print("Device found at 0x");
      Serial.println(addr, HEX);
    }
  }
  Serial.println("Done.");
}

void loop() {}
      

The yaw data readout was very precise and quite stable.

Download BNO085-accel-test-01.ino

BNO085 magnetometer as rotary encoder

Since I didn't have an AS5600 or similar dedicated magnetic encoder IC on hand, I asked Claude:

"I want to try using the BNO085's magnetometer as an encoder. Please tell me how I should proceed and which chip on the board is responsible for it."

Claude explained the magnetometer is built into the BNO085 itself, not a separate chip, and that holding a diametrically magnetized magnet above it and computing atan2(y, x) on the X/Y readings gives an absolute angle, the same principle dedicated chips like the AS5600 use in hardware. After a couple of compile errors, the correct constant turned out to be SH2_MAGNETIC_FIELD_CALIBRATED, not SH2_MAGNETIC_FIELD, I had a working raw output.

compile error: SH2_MAGNETIC_FIELD not declared, the correct constant is SH2_MAGNETIC_FIELD_CALIBRATED

Download BNO085-magneto-test-01.ino

Getting the readings into the Serial Plotter took one more fix, comma-separated Label:value pairs instead of plain text, since that's the format the plotter expects.

Download BNO085-magneto-test-02.ino

The angle was working but quite jittery. Claude suggested a few fixes and recommended starting with a simple rolling average since I was already set up for it, so I asked it to add that.

Download BNO085-magneto-test-03-smooth.ino

Magnetic rotary encoder, proof of concept

I threw together a quick proof of concept from parts I had lying around to validate the BNO085 + rotating magnet approach. I also tried soldering wires directly to a NeoPixel to use as an output indicator, but held the iron on it for too long and fried it.

proof-of-concept encoder knob with cube magnet and piece of tape
BNO085 seated in the proof-of-concept enclosure with parts on hand
BNO085 in the base enclosure and rotating knob with magnet
side view of the proof-of-concept encoder
proof-of-concept in hand
attempted to solder directly to a NeoPixel, held the iron on too long and fried it

I decided to modify my week 02 braille learning device to accomodate the BNO085 and a magnet to be able to read the position of the knob. The magnet would be embedded in the knob print. I created a pocket for affixing the BNO085 with a threaded hatch for access to the board and a hole for the wires to go to my XIAO. This was only a concept that would be used for testing purposes as I continue to iterate on this design.

Fusion 360 cross-section of the encoder housing, BNO085 pocket in the base and rotating knob mechanism
Download braille learning device v4 (.f3d)
slicer preview of the encoder knob, ready to print
Group page for week 09
Country roads...