-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
touch: add capacitive touch sensing on normal GPIO pins
Tested on the following chips/boards: * RP2040 (Raspberry Pi Pico) * ATSAMD21 (Adafruit PyBadge) * NRF52840 (PCA10056 developer board) * ESP8266 (NodeMCU) * ATmega328p (Arduino Uno) * ESP32C3 (WaveShare ESP-C3-32S-Kit) * FE310 (SiFive HiFive1 rev B) The sensitivity threshold in the example may need to be adjusted per board though, the default value of 100 typically recognizes when a cable is being touched but the RP2040 for example is capable of doing much more precise measurements if the power supply is sufficiently noise-free.
- Loading branch information
Showing
3 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Capacitive touch sensing example. | ||
// | ||
// This capacitive touch sensor works by charging a normal GPIO pin, then slowly | ||
// discharging it through a 1MΩ resistor and seeing how long it takes to go from | ||
// high to low. | ||
// | ||
// Use as follows: | ||
// - Change touchPin below as needed. | ||
// - Connect this pin to some metal surface, like a piece of aluminimum foil. | ||
// Make sure this surface is covered (using paper, Scotch tape, etc). | ||
// - Also connect this same pin to ground through a 1MΩ resistor. | ||
// | ||
// This sensor is very sensitive to noise on the power source, so you should | ||
// probably try to limit it by running from a battery for example. Especially | ||
// phone chargers can produce a lot of noise. | ||
package main | ||
|
||
import ( | ||
"machine" | ||
"time" | ||
|
||
"tinygo.org/x/drivers/touch/capacitive" | ||
) | ||
|
||
const touchPin = machine.GP16 // Raspberry Pi Pico | ||
|
||
func main() { | ||
time.Sleep(time.Second * 2) | ||
println("start") | ||
|
||
led := machine.LED | ||
led.Configure(machine.PinConfig{Mode: machine.PinOutput}) | ||
led.Low() | ||
|
||
// Configure the array of GPIO pins used for capacitive touch sensing. | ||
// We're using only one pin. | ||
array := capacitive.NewArray([]machine.Pin{touchPin}) | ||
|
||
// Use a dynamic threshold, meaning the GPIO pin is automatically calibrated | ||
// and re-calibrated to adjust for varying environments (e.g. changing | ||
// humidity). | ||
array.SetDynamicThreshold(100) | ||
|
||
wasTouching := false | ||
for i := uint32(0); ; i++ { | ||
// Update the GPIO pin. This must be called very often. | ||
array.Update() | ||
touching := array.Touching(0) | ||
|
||
// Indicate whether the pin is touched via the LED. | ||
led.Set(touching) | ||
|
||
// Print something when the touch state changed. | ||
if wasTouching != touching { | ||
wasTouching = touching | ||
if touching { | ||
println(" touch!") | ||
} else { | ||
println(" release!") | ||
} | ||
} | ||
|
||
// Print the current value, as a debugging aid. It's not really meant to | ||
// be used directly. | ||
if i%128 == 32 { | ||
println("touch value:", array.RawValue(0)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
package capacitive | ||
|
||
import ( | ||
"machine" | ||
"runtime/interrupt" | ||
"time" | ||
) | ||
|
||
const ( | ||
// How often to measure. | ||
// The Update function will wait until this amount of time has passed. | ||
measurementFrequency = 200 | ||
minTimeBetweenMeasurements = time.Second / measurementFrequency | ||
|
||
// How much to multiply values before averaging. A value higher than 1 will | ||
// help to avoid integer rounding errors and may improve accuracy slightly. | ||
oversampling = 8 | ||
|
||
// How many samples to use for the moving average. | ||
movingAverageWindow = 16 | ||
|
||
// After how many samples should the touch sensor be recalibrated? | ||
// This should be a power of two (for efficient division) and be a multiple | ||
// of movingAverageWindow. Ideally it should cause a recalibration every 5s | ||
// or so. | ||
recalibrationSamples = 1024 | ||
) | ||
|
||
type Array struct { | ||
// Time when the last update finished. This is used to make sure we call | ||
// Update() the expected number of times per second. | ||
lastUpdate time.Time | ||
|
||
// List of pins to measure each time. | ||
pins []machine.Pin | ||
|
||
// Raw values (non-smoothed) from the last read. | ||
values []uint16 | ||
|
||
hasFirstMeasurement bool | ||
|
||
// Static threshold. Zero if using a dynamic threshold. | ||
staticThreshold uint16 | ||
|
||
// How long to measure. | ||
measureCycles uint16 | ||
|
||
// Sensitivity (in promille) for the dynamic threshold. | ||
sensitivity uint16 | ||
|
||
// Capacitance trackers for dynamic capacitance measurement. | ||
trackers []capacitanceTracker | ||
} | ||
|
||
// Create a new array of pins to be used as touch sensors. | ||
// The pins do not need to be initialized. The array is immediately ready to | ||
// use. | ||
// | ||
// By default, NewArray configures a static threshold that is not very | ||
// sensitive. If you want the touch inputs to be more sensitive, use | ||
// SetDynamicThreshold. | ||
func NewArray(pins []machine.Pin) *Array { | ||
for _, pin := range pins { | ||
pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) | ||
pin.High() | ||
} | ||
array := &Array{ | ||
pins: pins, | ||
values: make([]uint16, len(pins)), | ||
measureCycles: uint16(machine.CPUFrequency() / 125000), // 1000 on the RP2040 (which is 125MHz) | ||
lastUpdate: time.Now(), | ||
} | ||
|
||
// A threshold of 500 works well on the RP2040. Scale this number to | ||
// something similar on other chips. | ||
array.SetStaticThreshold(int(machine.CPUFrequency() / 250000)) | ||
|
||
return array | ||
} | ||
|
||
// Use a static threshold. This works well on simple touch surfaces where you'll | ||
// directly touch the metal. | ||
func (a *Array) SetStaticThreshold(threshold int) { | ||
if threshold > 0xffff { | ||
threshold = 0xffff | ||
} | ||
a.staticThreshold = uint16(threshold) | ||
a.trackers = nil | ||
} | ||
|
||
// Use a dynamic threshold (as promille), that will calibrate automatically. | ||
// This is needed when you want to be able to detect touches through a | ||
// non-conducting surface for example. Something like 100‰ (10%) will probably | ||
// work in many cases, though you may need to try different value to reliably | ||
// detect touches. | ||
func (a *Array) SetDynamicThreshold(sensitivity int) { | ||
a.sensitivity = uint16(sensitivity) | ||
a.staticThreshold = 0 | ||
a.trackers = make([]capacitanceTracker, len(a.pins)) | ||
} | ||
|
||
// Measure all GPIO pins. This function must be called very often, ideally about | ||
// 100-200 times per second (it will delay a bit when called more than 200 times | ||
// per second). | ||
func (a *Array) Update() { | ||
// Wait until enough time has passed to charge all pins. | ||
now := time.Now() | ||
timeSinceLastUpdate := now.Sub(a.lastUpdate) | ||
sleepTime := minTimeBetweenMeasurements - timeSinceLastUpdate | ||
time.Sleep(sleepTime) | ||
a.lastUpdate = now.Add(sleepTime) // should be ~equivalent to time.Now() | ||
|
||
// Measure each pin in turn. | ||
for i, pin := range a.pins { | ||
// Interrupts must be disabled during measuring for accurate results. | ||
mask := interrupt.Disable() | ||
|
||
// Switch to input. This will stop the charging, and let it discharge | ||
// through the resistor. | ||
pin.Configure(machine.PinConfig{Mode: machine.PinInput}) | ||
|
||
// Wait for the pin to go low again. | ||
// A longer duration means more capacitance, which means something is | ||
// touching it (finger, banana, etc). | ||
count := uint32(i) | ||
for i := 0; i < int(a.measureCycles); i++ { | ||
if !pin.Get() { | ||
break | ||
} | ||
count++ | ||
} | ||
|
||
interrupt.Restore(mask) | ||
|
||
a.values[i] = uint16(count) | ||
|
||
// Set the pin to high, to charge it for the next measurement. | ||
pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) | ||
pin.High() | ||
} | ||
|
||
// The first measurement tends to be slightly off (too low value) so ignore | ||
// that one. | ||
if !a.hasFirstMeasurement { | ||
a.hasFirstMeasurement = true | ||
return | ||
} | ||
|
||
for i := 0; i < len(a.trackers); i++ { | ||
a.trackers[i].addValue(int(a.values[i]), int(a.sensitivity)) | ||
} | ||
} | ||
|
||
// Return the raw value of the given pin index of the most recent call to | ||
// Update. This value is not smoothed in any way. | ||
func (a *Array) RawValue(index int) int { | ||
return int(a.values[index]) | ||
} | ||
|
||
// Return the value from the moving average. This value is only available when a | ||
// dynamic threshold has been set, it will panic otherwise. | ||
func (a *Array) SmoothedValue(index int) int { | ||
return int(a.trackers[index].avg) / oversampling | ||
} | ||
|
||
// Return whether the given pin index is currently being touched. | ||
func (a *Array) Touching(index int) bool { | ||
if a.staticThreshold != 0 { | ||
// Using a static threshold. | ||
return a.values[index] > a.staticThreshold | ||
} | ||
|
||
return a.trackers[index].touching | ||
} | ||
|
||
// Separate object to store calibration data and track capacitance over time. | ||
type capacitanceTracker struct { | ||
recentValues [movingAverageWindow]uint16 | ||
sum uint32 | ||
avg uint16 | ||
|
||
baseline uint16 | ||
noise uint16 | ||
valueCount uint8 | ||
touching bool | ||
|
||
recalibrationCount uint8 | ||
recalibrationPrevAvg uint16 | ||
recalibrationNoiseSum int32 | ||
recalibrationSum uint32 | ||
} | ||
|
||
func (ct *capacitanceTracker) addValue(value int, sensitivity int) { | ||
// Maybe increase the resolution slightly by oversampling. This should | ||
// increase the resolution a little bit after averaging and should reduce | ||
// rounding errors. | ||
// Typical input values on the RP2040 are 100-200 (or up to 1000 or so when | ||
// touching the metal) so multiplying by 4-8 should be fine. Other chips | ||
// generally have much lower values. | ||
value *= oversampling | ||
if value > 0xffff { | ||
value = 0xffff // unlikely, but make sure we don't overflow | ||
} | ||
|
||
// This does a number of things at the same time: | ||
// * Add the new value to the recentValues array. | ||
// * Calculate the moving sum (and average) of recentValues using a | ||
// recursive moving average algorithm: | ||
// https://www.dspguide.com/ch15/5.htm | ||
ptr := &ct.recentValues[ct.valueCount%movingAverageWindow] | ||
ct.sum -= uint32(*ptr) | ||
ct.sum += uint32(value) | ||
ct.avg = uint16(ct.sum / movingAverageWindow) | ||
*ptr = uint16(value) | ||
ct.valueCount++ | ||
|
||
// Do an initial calibration once the first values have been read. | ||
if ct.baseline == 0 && ct.valueCount == movingAverageWindow { | ||
ct.baseline = ct.avg | ||
|
||
// Calculate initial noise as an average absolute deviation: | ||
// https://en.wikipedia.org/wiki/Average_absolute_deviation | ||
// This is a quick and imprecise way to find the noise, better noise | ||
// detection happens during recalibration. | ||
var diffSum uint32 | ||
for _, sample := range ct.recentValues { | ||
diff := int(ct.avg) - int(sample) | ||
if diff < 0 { | ||
diff = -diff | ||
} | ||
diffSum += uint32(diff) | ||
} | ||
ct.noise = uint16(diffSum / (movingAverageWindow / 2)) | ||
} | ||
|
||
// Now determine whether the touch pad is being touched. | ||
|
||
if ct.baseline == 0 { | ||
// Not yet calibrated. | ||
ct.touching = false | ||
return | ||
} | ||
|
||
// Calculate the threshold. | ||
// Divide by 65536 (instead of 65500) to avoid a potentially expensive | ||
// division while still being close enough. | ||
threshold := (uint32(ct.baseline) * uint32(sensitivity+1000) * 65) / 65536 | ||
|
||
// Add noise to the threshold, to avoid toggling quickly. This mainly | ||
// filters out mains noise. | ||
threshold += uint32(ct.noise) | ||
|
||
// Implement some hysteresis: if the touch pad was previously touched, lower | ||
// the threshold a little to avoid bouncing effects. | ||
// TODO: let this hysteresis depend on the amount of noise. | ||
if ct.touching { | ||
threshold = (threshold*3 + uint32(ct.baseline)) / 4 // lower the threshold by 25% | ||
} | ||
|
||
// Is the pad being touched? | ||
ct.touching = uint32(ct.avg) > threshold | ||
|
||
// Do a recalibration after the sensor hasn't been touched for ~5s, to | ||
// account for drift over time (humidity etc). | ||
if ct.touching { | ||
// Reset calibration (start from zero). | ||
ct.recalibrationCount = 0 | ||
ct.recalibrationSum = 0 | ||
ct.recalibrationNoiseSum = 0 | ||
} else { | ||
// Add the last batch of samples to the sum. | ||
if ct.valueCount%movingAverageWindow == 0 { | ||
ct.recalibrationCount++ | ||
|
||
// Wait a few cycles before starting data collection for | ||
// calibration. | ||
cycle := int(ct.recalibrationCount) - 3 | ||
|
||
if cycle < 0 { | ||
// Store the previous average, to calculate the noise value. | ||
ct.recalibrationPrevAvg = ct.avg | ||
|
||
} else if cycle >= 0 { | ||
// Collect data for recalibration. | ||
ct.recalibrationSum += ct.sum | ||
|
||
// Add difference between two (averaged) samples as a measure of | ||
// the noise. | ||
diff := int32(ct.recalibrationPrevAvg) - int32(ct.avg) | ||
if diff < 0 { | ||
diff = -diff | ||
} | ||
ct.recalibrationNoiseSum += diff | ||
ct.recalibrationPrevAvg = ct.avg | ||
|
||
} | ||
|
||
// Do the recalibration after enough samples have been collected. | ||
// Note: the noise is basically the average of absolute differences | ||
// between two averaging windows. I don't know whether this | ||
// algorithm has a name, but it seems to work here to detect the | ||
// amount of noise. | ||
const totalRecalibrationCount = recalibrationSamples / movingAverageWindow | ||
if cycle == totalRecalibrationCount { | ||
ct.baseline = uint16(ct.recalibrationSum / recalibrationSamples) | ||
ct.noise = uint16(ct.recalibrationNoiseSum / (totalRecalibrationCount / 2)) | ||
ct.recalibrationCount = 0 | ||
ct.recalibrationSum = 0 | ||
ct.recalibrationNoiseSum = 0 | ||
} | ||
} | ||
} | ||
} |