2014/08/26

Touch buttons with Atmel SAM4S (or similar)

I often use ATSAM4S microprocessor and one of the neat functions is capacitive sensing for touch buttons. There is a proprietary technology, QTouch, that does it. But you can make a simple touch button even easier and with half the pins. QTouch needs two pins, a capacitor and a resistor for each channel (button). I can do it with only the resistor (protection resistor, no need for a precise value) and one pin per channel.

The principle is very similar to QTouch sensing. First, you attach a button through a protection resistor (3k3 works fine) to a general purpose I/O pin. The button may be simply a piece of metal on a cable or it can be printed on the circuit board. It only has to be covered by an insulating layer. Solder mask, paper or a not very thick acrylic work well. This creates one of the plates of a capacitor and the dielectric. The second part is your finger. By approaching the button, your finger increases the capacitance and this can be measured to detect the presence of the finger thus producing a touch event.

Each pin has three possible states: high, low and high-impedance (measuring). In addition, each pin can be pulled down or high by an internal resistor, whose value is approximately 100 kOhm. We are going to use this. First, we need to discharge the capacitor. That can be easily done by pulling the pin low for sufficiently long time. A sufficiently long time is arbitrary but 5 time constants is more than enough. The RC time constant is dependent on the protection resistor and the total capacitance. A button together with parasitic capacitance can reach few tens of picofarads. So to be absolutely sure, lets use 100 pF as the capacitance value and 3k3 as the protection resistor. This translates to a time constant of 0.33 microseconds. Five time constants is 1.65 microseconds. Thus, if you rely on the built-in delay.h function delay_us(), then set two as its argument. You can see that only two microseconds are needed to prepare the button for measurement. Because you can do this in parallel for all channels, it is really fast.

Next thing is to measure the time needed to charge the capacitor. We set the pin to high-impedance mode and pull it high using the internal 100 kOhm resistor. This will form another RC series circuit, this time with a different time constant, RC = (100k + 3k3) × (button + pin capacitance). If the total capacitance is, say, 20 pF, then the time constant is 2.066 microseconds. But the internal resistor value is VERY approximate so for any accurate capacitance measurement a calibration would be needed. Having said that, we don’t need an accurate measurement at all! We only need to detect changes in capacitance. So after pulling the pin high, we simply wait until it reads 1. You can either continually poll the pin input register or set an interrupt and count the time it takes the pin to set. With 2-microsecond time constant, the pin will be high quickly so the whole measurement process is fast. One way to measure the time is to simply set a loop and wait until the pin sets. The iterator value corresponds to the charge-up time and is the result of measurement. You will need to keep a baseline time value for the untouched button. Then compare each measurement to this value to determine if the button has been (value will be higher) or hasn’t been touched. Do the measurement regularly to provide swift user interface experience. Protip: If you set the pin low after each measurement, you don't have to wait for discharge the next time.

Sample code (download here) uses touch.c and touch.h. Define the I/O port in touch.h, then call touch_init() once to setup the line and call touch_measure() to get a value proportional to port's capacitance. In the main program, watch for this value increasing, thus indicating a touch event.

touch.h:

/*
 * touch.h
 *
 * Created: 26.8.2014 19:00:00
 *  Author: Kaktus
 */

/*
 * Description:
 * Simple touch sensor demonstration.
 * Connect touch button to the defined port via approximately 3k3 resistor.
 * No other components needed.
 */

#ifndef TOUCH_H_
#define TOUCH_H_

#define TOUCH_PIOPORT PIOA
#define TOUCH_PIOLINE 0
#define TOUCH_PMC PMC_PCER0_PID11

void touch_init(void);
uint32_t touch_measure(void);

#endif /* TOUCH_H_ */


touch.c:

/*
 * touch.c
 *
 * Created: 26.8.2014 19:00:00
 *  Author: Kaktus
 */

#include <asf.h>
#include "delay.h"
#include "touch.h"

void start_discharge(void);

void touch_init()
{
    PMC->PMC_WPMR = PMC_WPMR_WPKEY_PASSWD;
    PMC->PMC_PCER0 |= TOUCH_PMC;
    TOUCH_PIOPORT -> PIO_WPMR = (0x50494F << 8); // registry unlock
    TOUCH_PIOPORT -> PIO_PER |= (1 << TOUCH_PIOLINE); // peripheral disable
    TOUCH_PIOPORT -> PIO_IFDR |= (1 << TOUCH_PIOLINE); // glitch filter disable
    TOUCH_PIOPORT -> PIO_PPDDR |= (1 << TOUCH_PIOLINE); // pull down disable
    start_discharge();
}

uint32_t touch_measure()
{
    TOUCH_PIOPORT -> PIO_ODR |= (1 << TOUCH_PIOLINE); // output disable
    TOUCH_PIOPORT -> PIO_PUER |= (1 << TOUCH_PIOLINE); // pull up enable
    uint32_t t = 0;
    while (!((TOUCH_PIOPORT -> PIO_PDSR) & (1 << TOUCH_PIOLINE)))
    {
        t++;
    }
    start_discharge();
    return t;
}

void start_discharge()
{
    TOUCH_PIOPORT -> PIO_PUDR |= (1 << TOUCH_PIOLINE); // pull up disable
    TOUCH_PIOPORT -> PIO_OER |= (1 << TOUCH_PIOLINE); // output enable   
    TOUCH_PIOPORT -> PIO_CODR |= (1 << TOUCH_PIOLINE); // output low
    delay_us(2); // set > 5 RC time constants; you can comment this line if you measure at longer intervals than the set delay
}