Skip to content

Instantly share code, notes, and snippets.

@nickovs
Created February 26, 2024 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nickovs/3229cca64f60d11167247fabc139155d to your computer and use it in GitHub Desktop.
Save nickovs/3229cca64f60d11167247fabc139155d to your computer and use it in GitHub Desktop.
RP2040 / Pi Pico touch interface using just one resistor
# 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