|
| 1 | +package capacitive |
| 2 | + |
| 3 | +import ( |
| 4 | + "machine" |
| 5 | + "runtime/interrupt" |
| 6 | + "time" |
| 7 | +) |
| 8 | + |
| 9 | +const ( |
| 10 | + // How often to measure. |
| 11 | + // The Update function will wait until this amount of time has passed. |
| 12 | + measurementFrequency = 200 |
| 13 | + minTimeBetweenMeasurements = time.Second / measurementFrequency |
| 14 | + |
| 15 | + // How much to multiply values before averaging. A value higher than 1 will |
| 16 | + // help to avoid integer rounding errors and may improve accuracy slightly. |
| 17 | + oversampling = 8 |
| 18 | + |
| 19 | + // How many samples to use for the moving average. |
| 20 | + movingAverageWindow = 16 |
| 21 | + |
| 22 | + // After how many samples should the touch sensor be recalibrated? |
| 23 | + // This should be a power of two (for efficient division) and be a multiple |
| 24 | + // of movingAverageWindow. Ideally it should cause a recalibration every 5s |
| 25 | + // or so. |
| 26 | + recalibrationSamples = 1024 |
| 27 | +) |
| 28 | + |
| 29 | +type Array struct { |
| 30 | + // Time when the last update finished. This is used to make sure we call |
| 31 | + // Update() the expected number of times per second. |
| 32 | + lastUpdate time.Time |
| 33 | + |
| 34 | + // List of pins to measure each time. |
| 35 | + pins []machine.Pin |
| 36 | + |
| 37 | + // Raw values (non-smoothed) from the last read. |
| 38 | + values []uint16 |
| 39 | + |
| 40 | + hasFirstMeasurement bool |
| 41 | + |
| 42 | + // Static threshold. Zero if using a dynamic threshold. |
| 43 | + staticThreshold uint16 |
| 44 | + |
| 45 | + // How long to measure. |
| 46 | + measureCycles uint16 |
| 47 | + |
| 48 | + // Sensitivity (in promille) for the dynamic threshold. |
| 49 | + sensitivity uint16 |
| 50 | + |
| 51 | + // Capacitance trackers for dynamic capacitance measurement. |
| 52 | + trackers []capacitanceTracker |
| 53 | +} |
| 54 | + |
| 55 | +// Create a new array of pins to be used as touch sensors. |
| 56 | +// The pins do not need to be initialized. The array is immediately ready to |
| 57 | +// use. |
| 58 | +// |
| 59 | +// By default, NewArray configures a static threshold that is not very |
| 60 | +// sensitive. If you want the touch inputs to be more sensitive, use |
| 61 | +// SetDynamicThreshold. |
| 62 | +func NewArray(pins []machine.Pin) *Array { |
| 63 | + for _, pin := range pins { |
| 64 | + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) |
| 65 | + pin.High() |
| 66 | + } |
| 67 | + array := &Array{ |
| 68 | + pins: pins, |
| 69 | + values: make([]uint16, len(pins)), |
| 70 | + measureCycles: uint16(machine.CPUFrequency() / 125000), // 1000 on the RP2040 (which is 125MHz) |
| 71 | + lastUpdate: time.Now(), |
| 72 | + } |
| 73 | + |
| 74 | + // A threshold of 500 works well on the RP2040. Scale this number to |
| 75 | + // something similar on other chips. |
| 76 | + array.SetStaticThreshold(int(machine.CPUFrequency() / 250000)) |
| 77 | + |
| 78 | + return array |
| 79 | +} |
| 80 | + |
| 81 | +// Use a static threshold. This works well on simple touch surfaces where you'll |
| 82 | +// directly touch the metal. |
| 83 | +func (a *Array) SetStaticThreshold(threshold int) { |
| 84 | + if threshold > 0xffff { |
| 85 | + threshold = 0xffff |
| 86 | + } |
| 87 | + a.staticThreshold = uint16(threshold) |
| 88 | + a.trackers = nil |
| 89 | +} |
| 90 | + |
| 91 | +// Use a dynamic threshold (as promille), that will calibrate automatically. |
| 92 | +// This is needed when you want to be able to detect touches through a |
| 93 | +// non-conducting surface for example. Something like 100‰ (10%) will probably |
| 94 | +// work in many cases, though you may need to try different value to reliably |
| 95 | +// detect touches. |
| 96 | +func (a *Array) SetDynamicThreshold(sensitivity int) { |
| 97 | + a.sensitivity = uint16(sensitivity) |
| 98 | + a.staticThreshold = 0 |
| 99 | + a.trackers = make([]capacitanceTracker, len(a.pins)) |
| 100 | +} |
| 101 | + |
| 102 | +// Measure all GPIO pins. This function must be called very often, ideally about |
| 103 | +// 100-200 times per second (it will delay a bit when called more than 200 times |
| 104 | +// per second). |
| 105 | +func (a *Array) Update() { |
| 106 | + // Wait until enough time has passed to charge all pins. |
| 107 | + now := time.Now() |
| 108 | + timeSinceLastUpdate := now.Sub(a.lastUpdate) |
| 109 | + sleepTime := minTimeBetweenMeasurements - timeSinceLastUpdate |
| 110 | + time.Sleep(sleepTime) |
| 111 | + a.lastUpdate = now.Add(sleepTime) // should be ~equivalent to time.Now() |
| 112 | + |
| 113 | + // Measure each pin in turn. |
| 114 | + for i, pin := range a.pins { |
| 115 | + // Interrupts must be disabled during measuring for accurate results. |
| 116 | + mask := interrupt.Disable() |
| 117 | + |
| 118 | + // Switch to input. This will stop the charging, and let it discharge |
| 119 | + // through the resistor. |
| 120 | + pin.Configure(machine.PinConfig{Mode: machine.PinInput}) |
| 121 | + |
| 122 | + // Wait for the pin to go low again. |
| 123 | + // A longer duration means more capacitance, which means something is |
| 124 | + // touching it (finger, banana, etc). |
| 125 | + count := uint32(i) |
| 126 | + for i := 0; i < int(a.measureCycles); i++ { |
| 127 | + if !pin.Get() { |
| 128 | + break |
| 129 | + } |
| 130 | + count++ |
| 131 | + } |
| 132 | + |
| 133 | + interrupt.Restore(mask) |
| 134 | + |
| 135 | + a.values[i] = uint16(count) |
| 136 | + |
| 137 | + // Set the pin to high, to charge it for the next measurement. |
| 138 | + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) |
| 139 | + pin.High() |
| 140 | + } |
| 141 | + |
| 142 | + // The first measurement tends to be slightly off (too low value) so ignore |
| 143 | + // that one. |
| 144 | + if !a.hasFirstMeasurement { |
| 145 | + a.hasFirstMeasurement = true |
| 146 | + return |
| 147 | + } |
| 148 | + |
| 149 | + for i := 0; i < len(a.trackers); i++ { |
| 150 | + a.trackers[i].addValue(int(a.values[i]), int(a.sensitivity)) |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +// Return the raw value of the given pin index of the most recent call to |
| 155 | +// Update. This value is not smoothed in any way. |
| 156 | +func (a *Array) RawValue(index int) int { |
| 157 | + return int(a.values[index]) |
| 158 | +} |
| 159 | + |
| 160 | +// Return the value from the moving average. This value is only available when a |
| 161 | +// dynamic threshold has been set, it will panic otherwise. |
| 162 | +func (a *Array) SmoothedValue(index int) int { |
| 163 | + return int(a.trackers[index].avg) / oversampling |
| 164 | +} |
| 165 | + |
| 166 | +// Return whether the given pin index is currently being touched. |
| 167 | +func (a *Array) Touching(index int) bool { |
| 168 | + if a.staticThreshold != 0 { |
| 169 | + // Using a static threshold. |
| 170 | + return a.values[index] > a.staticThreshold |
| 171 | + } |
| 172 | + |
| 173 | + return a.trackers[index].touching |
| 174 | +} |
| 175 | + |
| 176 | +// Separate object to store calibration data and track capacitance over time. |
| 177 | +type capacitanceTracker struct { |
| 178 | + recentValues [movingAverageWindow]uint16 |
| 179 | + sum uint32 |
| 180 | + avg uint16 |
| 181 | + |
| 182 | + baseline uint16 |
| 183 | + noise uint16 |
| 184 | + valueCount uint8 |
| 185 | + touching bool |
| 186 | + |
| 187 | + recalibrationCount uint8 |
| 188 | + recalibrationPrevAvg uint16 |
| 189 | + recalibrationNoiseSum int32 |
| 190 | + recalibrationSum uint32 |
| 191 | +} |
| 192 | + |
| 193 | +func (ct *capacitanceTracker) addValue(value int, sensitivity int) { |
| 194 | + // Maybe increase the resolution slightly by oversampling. This should |
| 195 | + // increase the resolution a little bit after averaging and should reduce |
| 196 | + // rounding errors. |
| 197 | + // Typical input values on the RP2040 are 100-200 (or up to 1000 or so when |
| 198 | + // touching the metal) so multiplying by 4-8 should be fine. Other chips |
| 199 | + // generally have much lower values. |
| 200 | + value *= oversampling |
| 201 | + if value > 0xffff { |
| 202 | + value = 0xffff // unlikely, but make sure we don't overflow |
| 203 | + } |
| 204 | + |
| 205 | + // This does a number of things at the same time: |
| 206 | + // * Add the new value to the recentValues array. |
| 207 | + // * Calculate the moving sum (and average) of recentValues using a |
| 208 | + // recursive moving average algorithm: |
| 209 | + // https://www.dspguide.com/ch15/5.htm |
| 210 | + ptr := &ct.recentValues[ct.valueCount%movingAverageWindow] |
| 211 | + ct.sum -= uint32(*ptr) |
| 212 | + ct.sum += uint32(value) |
| 213 | + ct.avg = uint16(ct.sum / movingAverageWindow) |
| 214 | + *ptr = uint16(value) |
| 215 | + ct.valueCount++ |
| 216 | + |
| 217 | + // Do an initial calibration once the first values have been read. |
| 218 | + if ct.baseline == 0 && ct.valueCount == movingAverageWindow { |
| 219 | + ct.baseline = ct.avg |
| 220 | + |
| 221 | + // Calculate initial noise as an average absolute deviation: |
| 222 | + // https://en.wikipedia.org/wiki/Average_absolute_deviation |
| 223 | + // This is a quick and imprecise way to find the noise, better noise |
| 224 | + // detection happens during recalibration. |
| 225 | + var diffSum uint32 |
| 226 | + for _, sample := range ct.recentValues { |
| 227 | + diff := int(ct.avg) - int(sample) |
| 228 | + if diff < 0 { |
| 229 | + diff = -diff |
| 230 | + } |
| 231 | + diffSum += uint32(diff) |
| 232 | + } |
| 233 | + ct.noise = uint16(diffSum / (movingAverageWindow / 2)) |
| 234 | + } |
| 235 | + |
| 236 | + // Now determine whether the touch pad is being touched. |
| 237 | + |
| 238 | + if ct.baseline == 0 { |
| 239 | + // Not yet calibrated. |
| 240 | + ct.touching = false |
| 241 | + return |
| 242 | + } |
| 243 | + |
| 244 | + // Calculate the threshold. |
| 245 | + // Divide by 65536 (instead of 65500) to avoid a potentially expensive |
| 246 | + // division while still being close enough. |
| 247 | + threshold := (uint32(ct.baseline) * uint32(sensitivity+1000) * 65) / 65536 |
| 248 | + |
| 249 | + // Add noise to the threshold, to avoid toggling quickly. This mainly |
| 250 | + // filters out mains noise. |
| 251 | + threshold += uint32(ct.noise) |
| 252 | + |
| 253 | + // Implement some hysteresis: if the touch pad was previously touched, lower |
| 254 | + // the threshold a little to avoid bouncing effects. |
| 255 | + // TODO: let this hysteresis depend on the amount of noise. |
| 256 | + if ct.touching { |
| 257 | + threshold = (threshold*3 + uint32(ct.baseline)) / 4 // lower the threshold by 25% |
| 258 | + } |
| 259 | + |
| 260 | + // Is the pad being touched? |
| 261 | + ct.touching = uint32(ct.avg) > threshold |
| 262 | + |
| 263 | + // Do a recalibration after the sensor hasn't been touched for ~5s, to |
| 264 | + // account for drift over time (humidity etc). |
| 265 | + if ct.touching { |
| 266 | + // Reset calibration (start from zero). |
| 267 | + ct.recalibrationCount = 0 |
| 268 | + ct.recalibrationSum = 0 |
| 269 | + ct.recalibrationNoiseSum = 0 |
| 270 | + } else { |
| 271 | + // Add the last batch of samples to the sum. |
| 272 | + if ct.valueCount%movingAverageWindow == 0 { |
| 273 | + ct.recalibrationCount++ |
| 274 | + |
| 275 | + // Wait a few cycles before starting data collection for |
| 276 | + // calibration. |
| 277 | + cycle := int(ct.recalibrationCount) - 3 |
| 278 | + |
| 279 | + if cycle < 0 { |
| 280 | + // Store the previous average, to calculate the noise value. |
| 281 | + ct.recalibrationPrevAvg = ct.avg |
| 282 | + |
| 283 | + } else if cycle >= 0 { |
| 284 | + // Collect data for recalibration. |
| 285 | + ct.recalibrationSum += ct.sum |
| 286 | + |
| 287 | + // Add difference between two (averaged) samples as a measure of |
| 288 | + // the noise. |
| 289 | + diff := int32(ct.recalibrationPrevAvg) - int32(ct.avg) |
| 290 | + if diff < 0 { |
| 291 | + diff = -diff |
| 292 | + } |
| 293 | + ct.recalibrationNoiseSum += diff |
| 294 | + ct.recalibrationPrevAvg = ct.avg |
| 295 | + |
| 296 | + } |
| 297 | + |
| 298 | + // Do the recalibration after enough samples have been collected. |
| 299 | + // Note: the noise is basically the average of absolute differences |
| 300 | + // between two averaging windows. I don't know whether this |
| 301 | + // algorithm has a name, but it seems to work here to detect the |
| 302 | + // amount of noise. |
| 303 | + const totalRecalibrationCount = recalibrationSamples / movingAverageWindow |
| 304 | + if cycle == totalRecalibrationCount { |
| 305 | + ct.baseline = uint16(ct.recalibrationSum / recalibrationSamples) |
| 306 | + ct.noise = uint16(ct.recalibrationNoiseSum / (totalRecalibrationCount / 2)) |
| 307 | + ct.recalibrationCount = 0 |
| 308 | + ct.recalibrationSum = 0 |
| 309 | + ct.recalibrationNoiseSum = 0 |
| 310 | + } |
| 311 | + } |
| 312 | + } |
| 313 | +} |
0 commit comments