Ich habe mehrfach versucht, eine Touch-Sensor mit zu bauen, bei dem ein externer Kondensator per Ladungstransfer geladen wird und dann die Anzahl der notwendigen Ladungstransfers gezählt wurde. In diesem Thread werden einige Bespiele aufgeführt.

Leider hat das nie wirklich gut funktioniert. Im gleichen Thread gibt es jedoch auch einen Eintrag von Tim, der ein Messprinzip beschreibt, dass den ADC benutzt. Irgendwann bin auch dann auch auf diesen GitHub-Eintrag gestoßen. Ich hab's nachgebaut und es hat auf Anhieb funktioniert.

Ein wenig gestört hat mich, dass zur Messung mindestens zwei ADC-Eingänge notwendig sind. Wenn man mehr als eine Sensor-Fläche abfragen will oder genügend Pins zur Verfügung hat, spielt das keine Rolle. Bei Projekten mit einem kleinen ATtiny stehen aber i.d.R. nicht genügend frei Pins zur Verfügung. Da der Partner-Pin nur zur Verbesserung der Störunterdrückung dienen soll, habe ich es ohne ausprobiert. Mit Erfolg, es hat perfekt funktioniert.

Das Messprinzip

Sensor-Kapazität laden

Im ersten Schritt wird die Sensor-Kapazität über die ganz normale Port-Steuerung geladen (Pin auf Output High) und die Sample-and-Hold-Kapazität des ADC entladen indem als ADC-Eingang GND gewählt wird.

uint16_t readSingle(uint8_t AdcNo)
{ uint8_t SensorPinMask = _BV(AdcNo);

  // Touch-Sensor laden, S&H-Kondensator entladen, beide verbinden, Spannung messen
  // ------------------------------------------------------------------------------

  // Sensor-Kapazität entladen
  ADC_DDR  |= SensorPinMask;   // Sensor-Pin auf Output
  ADC_PORT |= SensorPinMask;   // Sensor-Pin auf High

  // S&H-Kondensator entladen
  ADMUX = MUX_GND;             // GND als ADC-Input
  _delay_us(CHARGE_DELAY);     // Warteschleife: S&H-Kondensator und Sensor-Kapazität vollständig entladen

...

Ladungstransfer

Im zweiten Schritt wird zunächst der mit dem Sensor verbundene Pin auf Input geschaltet. Danach wird er über den ADC-Multiplexer mit der S&H-Kapazität verbunden. Es findet ein Ladungstransfer von der Sensor-Kapazität zur S&H-Kapazität statt. Die S&H-Kapazität beträgt wenige pF, liegt also in der gleichen Größenordnung wie die Sensorkapazität und wird demzufolge nicht vollständig geladen. Die Ladung und damit die später gemessene Spannung hängt im Wesentlichen davon ab, wie groß die Sensor-Kapazität ist. Letztere wird durch in der Nähe befindliche leitende Gegenstände beeinflusst.

Als letztes wird die Spannung an der S&H-Kapazität gemessen. 

...

  ADC_DDR  &= ~SensorPinMask;  // Sensor-Pin als Input konfigurieren
  ADC_PORT &= ~SensorPinMask;  // Den internen PullUp-Widerstand abschalten, damit der Sensor-Pin auf HIGH-Z liegt


  // Zum Ladungstransfer S&H-Kondensator und Sensor-Kapazität verbinden
  ADMUX = MUX_REF_VCC | AdcNo; // Sensor-Pin als ADC-Input
  _delay_us(TRANSFER_DELAY);   // Warteschleife für Ladungstransfer

  // Messung
  ADCSRA |= (1<<ADSC);         // Messung starten
  while(ADCSRA & (1<<ADSC));   // Auf das Ende des Messvorgangs warten.

  return ADC;
}

Code

Der Code für die Mess-Funktion hier noch einmal im Ganzen:

TouchSensorADC.h

// Liest den Wert eines Touch-Sensors am angegebenen Pin des ADC aus.
// Beim ATMega328 gibt es 8 Eingänge für den ADMUX,
// gültige Werte für 'AdcPin' sind 0..7.
// Zur Rauschunterdrückung wird eine Summe aus vier Messungen gebildet.
// Ein unberührter Sensor liefert einen kleineren Wert als ein berührter.
// Der Unterschied der Messwerte zwischen berührtem und unberührten Sensor
// liegt bei etwa 300-400 bei einer Sensorfläche von ca. 2 cm² und
// einem Abstand von gut 1 mm.

uint16_t readTouchSensor(uint8_t AdcNo);

// Die Konfiguration des ADC wird vor dem Messvorgang gesichert
// und anschließend wieder hergestellt, wenn
// 'TOUCH_SAVE_ADC_CONFIG' definiert ist.
#define TOUCH_SAVE_ADC_CONFIG

#define CHARGE_DELAY   5 // Zeit für den Ladungsvorgang (µs)
#define TRANSFER_DELAY 5 // Zeit für den Ladungstransfer (µs)
#define PROBE_NUMBERS  4 // Anzahl Messungen, über die gemittelt werden soll

Über die angegebenen Konstanten kann der Messvorgang beeinflusst werden.

TouchSensorADC.c

// Die folgenden Prozessor-Typen sind alle im gleichen Datenblatt beschrieben
#if (defined __AVR_ATmega48A__)  || (defined __AVR_ATmega48PA__)  \
 || (defined __AVR_ATmega88A__)  || (defined __AVR_ATmega88PA__)  \
 || (defined __AVR_ATmega168A__) || (defined __AVR_ATmega168PA__) \
 || (defined __AVR_ATmega328__)  || (defined __AVR_ATmega328P__)
  #define ADC_DDR     DDRC        // DDR des IO-Ports mit dem ADC
  #define ADC_PORT    PORTC       // PORT des IO-Port  mit dem ADC
  #define MUX_GND     0b00001111  // ADMUX-Wert für GND als Input für den ADC
  #define MUX_REF_VCC 0b01000000  // ADMUX-Maske: VCC ist Referenz für ADC
  #define ADC_CLK_MIN  50000UL
  #define ADC_CLK_MAX 200000UL
#else
 #error Die Funktion ist fuer diesen Prozessor-Typ nicht konfiguriert
#endif

// Prescaler-Wert ermitteln
#include "ADC-Timing.h"

Für die Atmega?8-Prozessoren werden die ADC-Konstanten vorbelegt. Über ADC-Timing.h wird der ADC-Prescaler eingestellt.

uint16_t readTouchSensor(uint8_t AdcNo)
{ uint16_t ProbeSum = 0;  // Zum Summieren der Einzelergebnisse

#if defined (TOUCH_SAVE_ADC_CONFIG)
  // ADC-Konfiguration zwischen speichern
  uint8_t Save_ADMUX  = ADMUX;
  uint8_t Save_ADCSRA = ADCSRA;
  uint8_t Save_ADCSRB = ADCSRB;
  #pragma message "ADC-Konfiguration wird bei QTouch-Messung gesichert"
#endif

  // Die ADC-Einheit für Einzelmessungen konfigurieren
  ADCSRB = 0b00000000; // Kein automatischer Trigger
  ADCSRA = (1<<ADEN) | ADC_PRESCALER_MASK;
  ADMUX  = MUX_REF_VCC; // VCC als Spannungsreferenz, ADC0 als Input (es gleichgültig welcher genommen wird)

  ADCSRA |= (1<<ADSC);       // Messung starten
  while(ADCSRA & (1<<ADSC)); // Auf das Ende der Messung warten

  // Summe aus PROBE_NUMBERS Messungen, um den Rauschanteil zu verringern
  for (int i=0; i < PROBE_NUMBERS; i++)
  { ProbeSum += readSimpleTouchSensorADC(AdcNo); // Ergebnisse summieren
  }

#if defined (TOUCH_SAVE_ADC_CONFIG)
  // ADC-Konfiguration wieder herstellen
  ADMUX  = Save_ADMUX;
  ADCSRA = Save_ADCSRA;
  ADCSRB = Save_ADCSRB;
#endif

  return ProbeSum;
}

readTouchSensor führt die Messung durch. Übergeben wird die ADC-Nummer. Diese muss identisch mit der zugehörigen Pin-Nummer sein.

Bei entsprechender Konfiguration wird die aktuelle Einstellung des ADC gesichert. Der ADC wird konfiguriert und eine Blindmessung durchgeführt. Die vorgesehene Anzahl von Messungen werden durchgeführt und die Ergebnisse aufaddiert. Ggf. wird die ADC-Einstellung wiederhergestellt.

static uint16_t readSingle(uint8_t AdcNo)
{ uint8_t SensorPinMask = _BV(AdcNo);

  // Touch-Sensor laden, S&H-Kondensator entladen, beide verbinden, Spannung messen
  // ------------------------------------------------------------------------------

  // Sensor-Kapazität entladen
  ADC_DDR  |= SensorPinMask;   // Sensor-Pin auf Output
  ADC_PORT |= SensorPinMask;   // Sensor-Pin auf High

  // S&H-Kondensator entladen
  ADMUX = MUX_GND;             // GND als ADC-Input
  _delay_us(CHARGE_DELAY);     // Warteschleife: S&H-Kondensator und Sensor-Kapazität vollständig entladen

  ADC_DDR  &= ~SensorPinMask;  // Sensor-Pin als Input konfigurieren
  ADC_PORT &= ~SensorPinMask;  // Den internen PullUp-Widerstand abschalten, damit der Sensor-Pin auf HIGH-Z liegt


  // Zum Ladungstransfer S&H-Kondensator und Sensor-Kapazität verbinden
  ADMUX = MUX_REF_VCC | AdcNo; // Sensor-Pin als ADC-Input
  _delay_us(TRANSFER_DELAY);   // Warteschleife für Ladungstransfer

  // Messung
  ADCSRA |= (1<<ADSC);         // Messung starten
  while(ADCSRA & (1<<ADSC));   // Auf das Ende des Messvorgangs warten.

  return ADC;
}

Durchführung einer einzelnen Messung gemäß des obigen Prinzips.

Test

Zum Ausprobieren habe ich eine kleine kupferkaschierte Platine mit zwei Sensorflächen präpariert und an einen Arduino angeschlossen. Die Kupferflächen sind über einem 10 kΩ mit den AVR-Anschlüssen verbunden, dies soll Störungen unterdrücken.

Doppelter Touch-Sensor   Test-Aufbau   Messprotokoll
Doppelter Touch-Sensor   Test-Aufbau   Messprotokoll

Das Test-Programm:

#include "Arduino.h"
#include "AppVersion.h"
#include "TouchSensorADC.h"

#define TPIN1 1
#define TPIN2 0

int16_t QT1_Offset;
int16_t QT2_Offset;

#define OFFSET_COUNT  16 // Anzahl Messungen für die Offset-Bestimmung
#define OFFSET_SAVETY 10 // Subtrahend für Offset um immer positive Werte
                         //	nach Abzug des Offsets zu haben.

 void setup() {
   Serial.begin(9600);  // Serial setup
   pinMode(13, OUTPUT); // On-Board-LED vorbereiten

   // Offset für die einzelnen Sensorflächen ermitteln.
   for (uint8_t i = 0; i < OFFSET_COUNT; i++)
   { QT1_Offset += readTouchSensor(TPIN1);
     QT2_Offset += readTouchSensor(TPIN2);
   }

   QT1_Offset = QT1_Offset / OFFSET_COUNT - OFFSET_SAVETY;
   QT2_Offset = QT2_Offset / OFFSET_COUNT - OFFSET_SAVETY;
 }


void loop()
{ int16_t QT1 = readTouchSensor(TPIN1) - QT1_Offset; // Ersten Sensor auslesen
  int16_t QT2 = readTouchSensor(TPIN2) - QT2_Offset; // Zweiten Sensor auslesen

  int16_t QD = (QT2 - QT1) / 10; // Differenz ergibt Position des Fingers auf der Sensorflächen-Kombination

  char buffer[100]; // Werte ausgeben
  sprintf(buffer, "%3i|%3i| >> %3i <<", QT1, QT2, QD);
  Serial.println (buffer);

  if (QT1 > 80)
    digitalWrite(13,1);
  else
    digitalWrite(13,0);
  delay(500); // Zwei Messungen Pro Sekunde
 }

In der Setup-Funktion wird der Touch-Sensor initialisiert und der Offset bestimmt. Dabei wird vom Offset ein kleiner Wert abgezogen, so dass beim späteren Abzug des Offsets ein keine negativen Werte entstehen. Bei unberührtem Sensor werden also Messwerte in der Höhe dieses Abzugs erwartet.

Zum Test habe ich die Sensorfläche mit einer Keramik-Kachel bedeckt. Die Messwerte waren noch halb so hoch, aber eindeutig auslesbar. Werden weitere Abstände gewünscht, können größere Sensorflächen eingesetzt werden. Bei einer Fläche von 9 x 5 cm² reagiert der Sensor bereits deutlich bei Annährung von etwa 5 cm.

Das Atmel Studio Projekt zum download.