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.
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
...
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;
}
Der Code für die Mess-Funktion hier noch einmal im Ganzen:
// 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.
// 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.
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 |
#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.