mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
30f540c29f
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.
314 lines
9.6 KiB
Go
314 lines
9.6 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|