Capacitive Touch Sensing with AVR and a Single ADC Pin

I've been thinking of a project that needs a little bit more elegant user interface than your usual push buttons. Partly inspired by a video blog on Dave Jones' EEVblog, I decided to look into capacitive touch buttons. The big issue unfortunately for me was that you usually need a separate chip for capacitive touch sensing. With some tricks, you can however use a normal microcontroller to do the job. Even using only a single pin and resistor.

Atmel actually provides a library for doing capacitive touch sensing on Atmel microcontrollers, it's called QTouch. Unfortunately, it's closed source, which is something I'd rather not use for my own projects. I did quicky peruse through some of the documentation, and found out something cool. The library supports using a single ADC input and a resistor to detect capacitive touch. How do they do that? Can I do it myself?


After some thinking and googling, I figured out at least some sort of a way of doing it. I'm not sure if this is how it was done at Atmel, but it works. Here's an image of the relevant bits of the input circuit in the AVR for my solution:
The capacitor on the left represents the capacitive touch button, a large circular copper pad. It always has some stray capacitance to ground, and a lot more capacitance when a finger is nearby it. Everything to the right of the dotted black line is inside the microcontroller. Nominally, you should have a resistor before the signal goes to the AVR for ESD and EMC reasons, but I skipped it for the sake of clarity in the image.

The pullup is part of the normal I/O circuitry of an AVR. The AD multiplexer is used to select from which pin a signal is routed to the ADC. the multiplexer can also be used to route ground directly to the ADC.

The sample & hold circuitry is part of the ADC. At the beginning of an AD conversion, the switch is closed, allowing the capacitor to charge to the voltage currently being applied to the selected input. After the capacitor is charged, the switch is opened and the actual AD converter can do the conversion on the sample being held in the capacitor.

What we essentially want to do is measure the value of the capacitor outside. To do this, we can use the internal sampling capacitor.


By enabling the pullup resistor, we charge the external capacitor, the copper pad that may or may not have a finger currently pressed on it, to 5 volts. At the same time, by setting the ADMUX to ground and doing an AD conversion, we charge the sampling capacitor to 0 volts. We now how one capacitor charged to 5 volts and another one to 0 volts.


After that, we disable the pullup, set the ADMUX to the input of the pin, and start a conversion. What we are essentially doing, is connecting the two capacitors in parallel. What should happen is that some of the charge from the capacitor charged to 5 volts should flow to the other capacitor, until the voltage across them is equal. The final voltage is going to depend on the capacitance of the two capacitors. Because the sampling capacitor has a fixed value of 14pF, the voltage is going to depend on capacitance of the touch button.

If the capacitance of the touch button is 14pF the voltage should be 2.5V.
If the capacitance of the touch button is smaller than 14pF the voltage should be <2.5V.
If the capacitance of the touch button is larger than 14pF the voltage should be >2.5V.

Ideally, we should see the ADC values being somewhat small when nothing is touching the button, and then rise up when something is touching the button.

At least that's my understanding of this issue.


After those enlightening thoughts, it was time to do some hardware to put this theories to the test! A couple of hours in front of KiCAD designing a circuit board, then milling the board at my university, followed by some rigorous soldering, meant I had a nice looking board in my hands in no time.


Some highlights:

  • ATMega32u4 microcontroller with integrated Full Speed USB controller
  • 3 capacitive buttons
  • capacitive slider consisting of 4 segments
  • 10-bit ADC
  • RGB LED for messing around with
  • 2 indicator LEDs

The capacitive touch buttons are simply connected through a ~10k resistor to an ADC-capable pin on the AVR. The reason there is no ground plane behind the capacitive buttons is because that would increase the capacitance of those buttons to ground, and we want the capacitance of the buttons themselves to be as low as possible.

The buttons are covered with Kapton tape because I found out that touching them with bare hands caused noise on the ADC output, and on a real PCB they'd be covered with soldermask and some silkscreen anyways.


For the record, I'm using AVR GCC for compiling and I'm coding in C. I'm not going to post all of the code for this test here, alot of it is related to the USB connection (I used the open source LUFA library to create a virtual serial link for looking at the raw values) and RGB LED PWMing. I'll just focus on the actual capacitive touch sensing.

I try to make sure my code is modular and at least decent-looking from the get go. That means I begin with the function definitions and defining needed structs.

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <avr/io.h>
  4. #include <util/delay.h>
  6. /** Holds information related to a single touch channel */
  7. typedef struct {
  8. volatile uint8_t *port; //PORTx register for pin
  9. volatile uint8_t portmask; //mask for the bit in port
  10. volatile uint8_t mux; //ADMUX value for the channel
  11. } touch_channel_t;
  13. /**Initializing the touch sensing */
  14. void touch_init(void);
  16. /**Doing a measurement */
  17. uint16_t touch_measure(touch_channel_t *channel);

Each touch sense button is going to get it's own instance of the struct. *port and portmask are used for setting the pullup on and off. mux is used to set the AD multiplexer to the right ADC channel.
  1. void touch_init(void){
  2. ADMUX |= (1<<REFS0); //reference AVCC (5v)
  4. ADCSRA |= (1<<ADPS2)|(1<<ADPS1); //clockiv 64
  5. //final clock 8MHz/64 = 125kHz
  7. ADCSRA |= (1<<ADEN); //enable ADC
  8. }

Initialization is exactly what you would do for normal ADC operation.

Then it's time for the actual implementation.

  1. static inline void adc_channel(uint8_t channel){
  2. ADMUX &= ~(0b11111);
  3. ADMUX |= 0b11111 & channel;
  4. }
  6. static inline uint16_t adc_get(void){
  7. ADCSRA |= (1<<ADSC); //start conversion
  8. while(!(ADCSRA & (1<<ADIF))); //wait for conversion to finish
  9. ADCSRA |= (1<<ADIF); //reset the flag
  10. return ADC; //return value
  11. }
  12. uint16_t touch_measure(touch_channel_t *channel){
  13. uint8_t i;
  14. uint16_t retval;
  16. retval = 0;
  18. //Do four measurements and average, just to smooth things out
  19. for (i=0 ; i<4 ; i++){
  20. *(channel->port) |= channel->portmask; // set pullup on
  21. _delay_ms(1); // wait (could probably be shorter)
  22. *(channel->port) &= ~(channel->portmask); // set pullup off
  24. adc_channel(0b11111); //set ADC mux to ground;
  25. adc_get(); //do a measurement (to discharge the sampling cap)
  27. adc_channel(channel->mux); //set mux to right channel
  28. retval += adc_get(); //do a conversion
  29. }
  30. return retval /4;

and here's how you might use it:

  1. static touch_channel_t btn1 = {
  2. .mux = 7,
  3. .port = &PORTF,
  4. .portmask = (1<<PF7),
  5. };
  7. int main(void){
  8. uint16_t sample;
  9. touch_init();
  10. for(;;){
  11. sample = touch_measure(&btn1);
  12. //Do something with the sample...
  13. }
  14. return 0;
  15. }

There's a lot of room for improvement, ofcourse. At the moment a lot of time is spent on waiting for AD conversions to finish and so on. Using some interrupts and timers would make the whole process a lot more efficient. Also using a running average filter instead of doing 4 measurements every time might be a good idea.


Well, you saw the youtube video in the beginning. It works.

As far as the actual values coming out of the setup, well the 10-bit ADC has a value range of 0-1023. On that scale the "idle" value of all buttons was floating somewhere around 500 (slightly higher on the righternmost button, probably because the trace to it travels next to the ground plane), and during touch it was around 800, less if pressed more lightly, more if pressed strongly. Also, placing the board directly on my ESD mat increased the idle value by a small but not insignificant amount (~30).

Next thing is trying to get that slider working.