Created
February 26, 2024 14:21
-
-
Save nickovs/3229cca64f60d11167247fabc139155d to your computer and use it in GitHub Desktop.
RP2040 / Pi Pico touch interface using just one resistor
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
# Simple Pi Pico/RP2040 touch interface | |
# This implements a capacitive touch interface using two GPIO pins and just one external resistor. | |
# It work by using the user's body as a capacitor to ground; the output GPIO pin goes through the | |
# resistor to the touch point, which is connected directly to the input GPIO pin. A PIO program | |
# periodically toggels the output pin and then counts the number of cycles until the input pin | |
# changes level. The time constant for the response is proportional to both the fixed resistor | |
# value and the capacitance at the touch point, which varies depending on if the user is touching | |
# or not. With the default setting a 2.2 megaohm resistor works well. | |
from machine import Pin | |
from rp2 import PIO, StateMachine, asm_pio | |
from array import array | |
@asm_pio(sideset_init=(PIO.OUT_LOW,)) | |
def touch_pio(): | |
# Implement a capacitive touch interface using PIO | |
mov(isr, 31) # Preload OUT with a non-zero count | |
wrap_target() | |
# Fetch the maximum count, or use the existing one if there is nothing new | |
mov(x, osr) | |
pull(noblock) | |
set(y, 0) # Reset the total count | |
mov(x, osr).side(1) # Load the timeout value and drive the output high | |
label("high_phase_loop") # Loop is 3 cycles long | |
jmp(y_dec, "dec_y_high") # Count the iteration | |
label("dec_y_high") | |
jmp(pin, "finish_high") # Jump to low_phase if input pin is high | |
jmp(x_dec, "high_phase_loop") # Loop up the maximum count | |
jmp("low_phase") # Jump to low_phase | |
label("finish_high") # Count down the remaining cycles of the high phase | |
jmp(x_dec, "finish_high") [2] | |
label("low_phase") | |
mov(x, osr).side(0) # Load the timeout value and drive the output low | |
label("low_phase_loop") # The normal loop path is also 3 cycles long | |
jmp(y_dec, "dec_y_low") # Count the iteration | |
label("dec_y_low") | |
jmp(pin, "not_low_yet") # Jump to continue the countdown if input pin still high | |
label("finish_low") # Count down the remaining cycles of the high phase | |
jmp(x_dec, "finish_low") [2] | |
jmp("count_end") | |
label("not_low_yet") | |
jmp(x_dec, "low_phase_loop") # Loop up the maximum count | |
label("count_end") | |
mov(isr, invert(y)) # MOV Y to IN, with bit invert sinve we decremented | |
push(noblock) # Push inverted Y value without waiting | |
irq(rel(0)) # Raise an interrupt to let the CPU know we have data waiting | |
wrap() | |
# A Sentinel default value object so that None can be usefully passed to the irq() method | |
_DUMMY = object() | |
_DEFAULT_CONFIG = { | |
"rate": 100, # Effective sample rate | |
"oversample": 10, # Number of samples averaged for an effective sample | |
"idle_level": None, # Target level when not touched. None means self-calibrate | |
"touch_level": None, # Target level when touched. None means used idle_level * 3 | |
"hysteresis": 0.2, # With of hysteresis zone as a fraction of (touch_level - idle_level) | |
"auto": True, # Automatically calibrate by flitering reading | |
} | |
class Touch: | |
def __init__(self, pio_id, in_pin, out_pin, **config): | |
self._pio_id = pio_id | |
self.in_pin = in_pin | |
self.out_pin = out_pin | |
self._config = dict(_DEFAULT_CONFIG) | |
self._config.update(config) | |
self._samples = memoryview(array("I", [0] * self._config["oversample"])) | |
self._sample_index = 0 | |
self._last_state = False | |
self._irq_handler = None | |
self._irq = None | |
freq = machine.freq() | |
self._ticks = freq // (2 * self._config["rate"] * self._config["oversample"]) | |
self._sm = StateMachine( | |
pio_id, touch_pio, freq=freq, | |
jmp_pin=Pin(in_pin, Pin.IN), sideset_base=Pin(out_pin, Pin.OUT) | |
) | |
self._sm.put(self._ticks) | |
self._sm.active(1) | |
if self._config["idle_level"] is None: | |
self.calibrate(False) | |
def __repr__(self): | |
return "Touch({}, {}, {})".format(self._pio_id, self.in_pin, self.out_pin) | |
def raw_read(self): | |
sm = self._sm | |
buf = self._samples | |
sm.get(buf[:4]) | |
sm.get(buf) | |
return sum(buf) // len(buf) | |
def calibrate(self, touched): | |
sm = self._sm | |
buf = self._samples | |
sm.get(buf[:4]) | |
total = 0 | |
for _ in range(10): | |
sm.get(buf) | |
total += sum(buf) | |
base_level = total // (10 * len(buf)) | |
if touched: | |
self._config["touch_level"] = base_level | |
else: | |
self._config["idle_level"] = base_level | |
if self._config["touch_level"] is None: | |
self._config["touch_level"] = base_level * 3 | |
def _compute_state(self): | |
config = self._config | |
low = config["idle_level"] | |
high = config["touch_level"] | |
span = high - low | |
mid = (high + low) // 2 | |
hyst = int(span * config["hysteresis"] / 2) | |
auto = config["auto"] | |
buf = self._samples | |
val = sum(buf) // len(buf) | |
if val > mid + hyst: | |
state = True | |
if auto: | |
self._config["touch_level"] = (9 * high + val) // 10 | |
elif val < mid - hyst: | |
state = False | |
if auto: | |
self._config["idle_level"] = (9 * low + val) // 10 | |
else: | |
state = self._last_state | |
self._last_state = state | |
return state | |
def value(self): | |
sm = self._sm | |
buf = self._samples | |
sm.get(buf[:4]) | |
sm.get(buf) | |
return self._compute_state() | |
@property | |
def last_value(self): | |
return self._last_state | |
def irq(self, handler=_DUMMY): | |
if handler is not _DUMMY: | |
self._sample_index = 0 | |
self._irq_handler = handler | |
self._sm.irq(self._pio_irq if handler else None) | |
return self._irq_handler | |
def _pio_irq(self, sm): | |
buf = self._samples | |
l = len(buf) | |
n = sm.rx_fifo() | |
idx = self._sample_index | |
for i in range(n): | |
buf[idx] = sm.get() | |
idx += 1 | |
if idx == l: | |
old = self._last_state | |
new = self._compute_state() | |
if new != old: | |
self._irq_handler(new, self) | |
idx = 0 | |
self._sample_index = idx |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment