adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.1k stars 1.22k forks source link

nrf: implement touchio.TouchIn #1048

Closed dhalbert closed 5 years ago

dhalbert commented 6 years ago

The COMP (comparator) peripheral description says it does "Single-pin capacitive sensor support".

nickzoic commented 5 years ago

OK, having a look into this now ... progress will be at https://github.com/nickzoic/micropython/tree/nickzoic/circuitpython-nrf-touchin-1048

Nordic API ref: https://github.com/NordicPlayground/nrf52-capsense-example

nickzoic commented 5 years ago

Hmmm, request from @ptorrone was:

please use the "Single-pin capacitive sensor support" - no external resistor is required then!

... but in https://devzone.nordicsemi.com/tutorials/b/design-examples/posts/capacitive-touch-on-the-nrf52-series there's the following update:

Important updates:

Due to erratum 84 (COMP: ISOURCE not functional), capacitive sensing using the relaxation oscillator method is not usable in a real product with the nRF52832, as it will only work reliably in room temperature. The ISOURCE feature is not present in the nRF52840 nor nRF52810. After this tutorial was written, SDK 12 was released with support for capacitive touch using the Capacitive Sensor driver and Capacitive Sensor library.

That erratum being:

The programmable current source (ISOURCE) has too high varation. Variance over temp is >20 times specified nominal value ... but ISOURCE not being present at all in the '840 worries me more.

This is also mentioned at: https://www.nordicsemi.com/DocLib/Content/SDK_Doc/nRF5_SDK/v15-2-0/lib_csense_lowlevel

Note Until Anomaly 84 (ISOURCE not functional) is fixed, the COMP solution cannot provide production-level quality. Use the SAADC solution instead.

ladyada commented 5 years ago

seems unclear, whats the SAADC method?

nickzoic commented 5 years ago

That's the version with the resistors, using another pin to charge the pads and then the saadc to measure the rate at which it discharges

The comp method may be okay if we're clever about self-calibration, I'll try it out ...

dhalbert commented 5 years ago

The current CircuitPython "calibration" on SAMD21 is pretty simple: when the TouchIn object is created, we assume that it's not being touched. Touch threshold is that value + 100. But TouchIn.raw_value is available for people who want or need it, and the threshold can also be adjusted.

If the nRF errata are saying that the readings are really erratic, yeah, that could be a significant issue.

nickzoic commented 5 years ago

On Wed, Dec 5, 2018, at 00:32, Dan Halbert wrote:

The current CircuitPython "calibration" on SAMD21 is pretty simple: when the TouchIn object is created, we assume that it's not being touched. Touch threshold is that value + 100. But TouchIn.raw_value is available for people who want or need it, and the threshold can also be adjusted.> If the nRF errata are saying that the readings are really erratic, yeah, that could be a significant issue. Yeah, it sounds like the current source drifts a lot with die temperature, so so will the frequency of the relaxation oscillator.The SAADC approach is a kludge around this problem by using an I/O pin + resistor as the current source. BUT

Temperature should change relatively slowly, so assuming all the current sources drift the same amount, we can maybe get around this problem by providing relative touch sensing. HOWEVER

The other concern is that the 52840 doesn't have the current source available at all, so this technique won't work. If you're planning on migrating the nRF feathers to the '40 then we'd need to use the I/O pin

ladyada commented 5 years ago

yah we require it to work on all nrf boards. if we have to do it, we can make it a common module so any chips can use it? ive done the technique w the original cirplay board. you just need a 1mohm res and fast reading w/no interrupts, so it should be in c not python

nickzoic commented 5 years ago

On Wed, Dec 5, 2018, at 11:04, ladyada wrote:

yah we require it to work on all nrf boards. if we have to do it, we can make it a common module so any chips can use it? ive done the technique w the original cirplay board. you just need a 1mohm res and fast reading w/no interrupts, so it should be in c not python OK no worries, I'll scrap the ISOURCE approach and implement just the ADC approach.Yes, I think that this technique should work on pretty much any chips if their ADC is fast and precise enough.

nickzoic commented 5 years ago

OK so I've got as far as confirming that yes, charging and discharging A0 through a 1Mohm resistor gives enough of a signal that we could call it a touch sensor. Python code is actually (just) quick enough to catch this!

At least, I think that's what I'm seeing ... it's pretty naïve so it could also be measuring electrical noise coupled into the pin. Sadly my 'scope leads are too high capacitance to show a signal variation on the screen. I've got to try with a bigger resistance, too: it'll discharge slower but that'll give me more time to measure a difference. The C implementation should make this clearer though because I'll be able to get a bit more sophisticated with measuring the discharge rate.

A bigger problem though: the touchio.TouchIn API is all about wrapping a single Pin object. This method needs another pin as well, to be the "drive". This can be a spare digital pin, which can drive many touch pads each through a resistor. So for this version we might need to do something like:

t0 = touchio.TouchIn(pad=board.A0, drive=board.D15)
t1 = touchio.TouchIn(pad=board.A1, drive=board.D15)

... which would share D15 between the two pads on A0 and A1. A TouchIn created without a drive pin would have to work some other way (perhaps ISOURCE on the '832 if it works at all) or raise an exception on platforms with no such mechanism (like the '840)

(it occurs to me that you could use a pair of analog pins and a single resistor to provide two touch pads, by switching each pin between being an analog input and a digital output in turn. That would mean driving the capacitance of the touch pad directly though, which is a bit of a waste of electrons and increases the risk of shorting the driven pin accidentally.)

ladyada commented 5 years ago

yeah i like that API. i think you probably have it working but you need to do it in C for sure, you can see the arduino version of this code here: https://github.com/PaulStoffregen/CapacitiveSensor

nickzoic commented 5 years ago

Yep, no worries. I should be able to get this done in the next couple of days ...

tannewt commented 5 years ago

Do we expect others to hook up their own resistors or could we build the drive in configuration into the board info? That way we could leave the API as is.

ladyada commented 5 years ago

i think given you can use any combo, its best to let it be assigned at object instantiation. this will come in handy as we do more chips w/o native captouch - its a Good Thing to support :)

nickzoic commented 5 years ago

I guess it might make sense to have the resistors already in place on a board like a LilyPad etc where soldering isn't usually required.

ladyada commented 5 years ago

yes on the CPX they will be placed on the pcb!

nickzoic commented 5 years ago

Hey I just had a thought: maybe we could dispense with the 'drive' pin by just having a ~1Mohm drain resistor from 'sense' pin to ground, setting the pin to output digital high for a bit to charge and then set the pin to analog input mode to measure the discharge rate. It'd get rid of the need for a drive pin, and a 1Mohm resistor to ground would make little difference to the pin's behaviour when used for any other purpose. I'll try it out and see if it works ...

UPDATE: Switching the pin between out and hi-Z works, still trying to make the SAADC play nicely.

nickzoic commented 5 years ago

OK with a little experimentation that approach (having a single pin switch from digital out to analog in) works well! Even the 1Mohm 'drain' resistor to ground may not be necessary, as the pin has an R_INPUT of >1MΩ to ground (on my feather, this is about 5MΩ) which is enough to slowly drain the pin.

From there the plan is to rapidly take N readings from the SAADC at the start of the discharge curve and calculate the rate of change of voltage, giving us a pretty direct reading of pad capacitance. The signal is pretty noisy so I'm looking at a couple of different ways of separating the signal from the noise.

ladyada commented 5 years ago

k on your scope you should def be able to see the discharge curve. also you can try putting the wire in water, that should also be detectable

nickzoic commented 5 years ago

Certainly can: sds00010

Yellow: the touch pad voltage Purple: a clock signal I added in to show the sample rate.

Breadboards and flying leads is all a bit too messy to behave nicely with touch sensing so I'll make up a little test board like I did for the ESP32 one: https://nick.zoic.org/art/esp32-capacitive-sensors/ ... the above is just intended as a proof of concept for the "single pin solution" :-)

Shallower but noisier curve when touched: sds00004

ladyada commented 5 years ago

kk thanks! i do think its good to have both options, how about default to drive_pin=None and if it is, do the single-wire setup, otherwise, use the driver pin

nickzoic commented 5 years ago

OK so it's working nicely with the single pin setup described above. Here's the test setup: test setup

(UPDATE: That pad board is 1.6mm single-sided PCB measuring 75mm x 50mm, the top piece is sliced down the middle to make two pads and there's a second piece of board stuck to the back as a ground plane. The pads are covered in pallet strapping tape, which is a little thinner than insulating tape but not much.)

schematic:

A0 --- [ pad 0 ] --- 1MΩ --- [ ground plane ] --- 1MΩ --- [ pad 1 ] --- A1
                                     |
                                    GND

... and here's some test code (the thresholds are set manually)

import board
import touchio
t0 = touchio.TouchIn(board.A0)
t1 = touchio.TouchIn(board.A1)

t0.threshold = 170
t1.threshold = 165

while True:
    print("%4d %d %4d %d" % (t0.raw_value, t0.value, t1.raw_value, t1.value))
ladyada commented 5 years ago

kk have you been able to try it with nrf52840?

nickzoic commented 5 years ago

Sadly no, I've got a makerdiary 52840 'micro dev kit' here https://www.tindie.com/products/Zelin/nrf52840-micro-dev-kit-usb-dongle/ but I haven't got it to talk to me yet ...

ladyada commented 5 years ago

ok can you get a nrf52840 DK or something to test with? we aren't targeting the '832 so much - im sure its similar but we'll need to test on the '840!

nickzoic commented 5 years ago

OK yep, ordered a big devkit and a little dongle so hopefully I can actually build to the right targets this time!

Send us a '840 feather when they're available :-)

nickzoic commented 5 years ago

OK, thanks to Mouser for fast shipping :-)

The same code works on the NRF 52840 DK (PCA10056) with only minor modifications to pin numbers and thresholds. The tracks on this board from NRF to pins are really long, but it still picks up a good touch signal.

import board
import touchio
t0 = touchio.TouchIn(board.P0_04)
t1 = touchio.TouchIn(board.P0_29)

t0.threshold = 61700
t1.threshold = 61400

while True:
    print("%4d %d %4d %d" % (t0.raw_value, t0.value, t1.raw_value, t1.value))
ladyada commented 5 years ago

looks good, can you toss me a UF2 and ill test it on the nRF52840 feather?

nickzoic commented 5 years ago

OK, sent by email ... and the branch is at https://github.com/nickzoic/micropython/tree/nickzoic/circuitpython-nrf-touchin-1048 if you want to build it yourself. I tested it on the dev kit board with the pad PCB pictured above hooked up by short hookup wires.

nickzoic commented 5 years ago

I've attached UF2 builds for the development kit and for the much awaited feather 52840!

firmware_uf2_pca10056.zip firmware_uf2_feather_nrf52840_express.zip

ladyada commented 5 years ago

ok tested with a repl - works! do you want to start a PR?

nickzoic commented 5 years ago

I opened https://github.com/adafruit/circuitpython/pull/1412 but I should do some minor code cleanup of stuff like comments.

ladyada commented 5 years ago

ok so i did more testing tonite, and used the mu plotter to get a sense of the data and it seems really spikey, did you notice this too?

image

nickzoic commented 5 years ago

G'day Limor, Happy Holidays!

What kind of touchpad setup did you have? The physical setup is quite finicky, more so than for example the ESP32's self-oscillating method. To get decent readings out I used a grounded backplane and a thin insulating layer over the touch sensors. I've attempted to clean up the signal using a linear regression (of sorts) to estimate but we might need to do some experimentation and stick some constants into boards/*/mpconfigboard.h to tune the algorithm per board. I'll log some numbers out of the Feather and see if it's as spiky as you're seeing ... I didn't think it was quite that bad though, I was getting pretty reliable thresholds behaviour out of both the '32 Feather and the '40 DK. -----N

ladyada commented 5 years ago

just using a wire, but it needs to be stable no matter what the method - did you add a way to test with an optional charge gpio pin? that should work more consistantly :)

nickzoic commented 5 years ago

Hmmm. That's going to be hard whether there's a separate charge pin or not

We might be able to do some better signal processing, eg: a sliding window mean or median, I'll try a few things and see if I can pull a signal out of the noise.

nickzoic commented 5 years ago

In general though, capacitive touch detection works better and will always work better on the Feathers than on the DevKit, because the devkit has ~3" of track zigzagging its way between MCU and pin header and the Feather doesn't.

Looking forward to that Feather '840 arriving :-)

ladyada commented 5 years ago

image

using the technique linked above in the "capacitivesensor" library, it seems to work quite well, you need a drive pin but not ADC required

capacitive_test.txt

nickzoic commented 5 years ago

(back from vacation) That's really interesting! That's using just time-to-threshold and averaging over 10 cycles where I'm trying to get all fancy and stuff ... i'll try that out!

ladyada commented 5 years ago

ok! welcome back :)

nickzoic commented 5 years ago

Well, that is a fraction of the code size, works with non-analogue-capable pins, gets better touch sensing results than my fancy-pants math version and still works fine with a single pin rather than a separate drive pin!

Not quite done yet but will make it into a PR this weekend ...

tannewt commented 5 years ago

Done in #1499. Thanks!