Amateurfunk verbindet die Welt

Taster am ADC

Erstellt: DL6GL, 31.08.2012, letzte Änderung 

« Taster-Interruptsteuerung
TOP » Taster kurz/lang

Taster am ADC-Port mit ADC-Auswertung

Für Matrixtastaturen bietet der ADC in einem AVR eine einfache Lösung, über eine entsprechende Widerstandsmatrix aus dem jeweils erzeugten Spannungsabfall die gedrückte Taste zu identifizieren. Das geht natürlich auch für eine Anzahl von Einzeltastern. Vorteil ist, dass man an einen ADC-Eingang mehrere Taster anschalten kann, 10 sollten bequem gehen. Als Nachteil zeigte sich in einem ersten Testaufbau die etwas zögerliche Tastenerkennung. Man muss etwas länger drücken als in den obigen Beispielen. Die ADC-Konvertierung mit der Bascom-Funktion GetADC braucht eine gewisse Zeit.  Ist zumindest ungewohnt. Diesem unbefriedigenden Umstand konnte schließlich mit einer besseren Lösung abgeholfen werden: mit direktem Zugriff auf die ADC-Register.

Mehrfachtaster am ADC

Zu beachten ist – bei allen Anwendungen mit dem ADC – die getrennte Verdrahtung des Analogteils, hier auf der rechten Seite, vom restlichen Digitalteil. Die Zusammenführung der analogen Masse (Pin 31 beim ATmega16/32) mit der Hauptmasse (Pin 11 beim ATmega16/32) erfolgt an einem Punkt. Damit lassen sich Störungen im ADC minimieren. Die SMD-Bauform des ATmega16/32 hat insgesamt 3 herausgeführte Pin-Paare VCC und GND. Hier sollten zur Störunterdrückung die VCC-Pins jeweils mit 100nF-Kondensatoren gegen Masse gepuffert werden, wobei die GND-Anschlüsse an einer Massefläche unter dem Gehäuse zusammengeführt werden. An dieser Massefläche erfolgt auch die Verbindung der analogen Masse AGND (Pin 31) an die gemeinsame Masse.

Oft findet man Widerstandsketten, bei denen entweder jeweils ein Widerstand der Kette durch einen Taster überbrückt wird oder ein Teilabgriff an den ADC-Eingang gelegt wird. Die hier gezeigte Anordnung ist meines Erachtens einfacher, da für jeden der Taster ein individuelles Teilerverhältnis gewählt werden kann ohne die gesamte Widerstandskette zu beeinflussen.

Da der 10 bit-ADC bei offenen Tastern maximal 210-1 = 1023 ausgibt, lassen sich aus den Teilerverhältnissen die zu erwartenden ADC-Werte ermitteln:

Schalter Spannungsteiler ADC-Wert
-- 4k7 -- 1023
S1 4k7 1k 180
S2 4k7 2k2 325
S3 4k7 3k9 460
S4 4k7 5k6 556
S5 4k7 10k 696
S6 4k7 22k 843

In der einfachsten Form wird die Tasterabfrage in die Do...Loop eingebunden. Vorweg die Erklärungen und Programmvorspann. Hier wird z.B. der ADC2 verwendet.

Dim wrdADCNoKey As Word                         'ADC value no key pressed
Dim wrdADC As Word 'ADC value key pressed
Dim strADC As String * 4 'String for LCD output
Dim bytKeyPressed As Byte 'Key number

'Configure LCD on Port D ------------------------------------------------------
'Common 2x16 Text LCD with HD44780 controller
'Config Lcd = 16 * 2
'Config Lcdpin = Pin , Db7 = PortD.5 , Db6 = PortD.4 , Db5 = PortD.3
'Config Lcdpin = Pin , Db4 = PortD.2 , E = PortD.1 , Rs = PortD.0
'Config Lcdbus = 4

'Configure ADC ---------------------------------------------------------------
Config ADC = Single , Prescaler = Auto , Reference = AVCC

'Declare subs & functions -----------------------------------------------------
Declare Function CheckKeys(byVal bytADCNum As Byte) As Byte

Cls
Cursor Off

Start ADC
wrdADCNoKey = GetADC(2) 'No key pressed
strADC = str(wrdADCNoKey)
Locate 1 , 1
LCD strADC

Do

'Do something else...

'Evaluate Keys
bytKeyPressed = CheckKeys(2) 'Get key number from ADC2
If bytKeyPressed > 0 Then
strADC = Str(wrdADC)
strADC = Format(strADC , " 0")
Locate 2 , 1
LCD "Key " ; bytKeyPressed; ": " ; strADC
End If

Loop

Die zugehörige Function CheckKeys:

Function CheckKeys(byVal bytADCNum As Byte) As Byte
'Check which key was pressed @ ADC resistor chain
'Input: bytADCNum Number of ADC (0...7)
'Output: CheckKeys Number of key pressed

CheckKeys = 0
wrdADC = GetADC(bytADCNum) 'Get ADC value
If wrdADC < wrdADCNoKey Then 'Any key was pressed
Select Case wrdADC 'Get key pressed
Case Is > 750
CheckKeys = 6
Case Is > 620
CheckKeys = 5
Case Is > 500
CheckKeys = 4
Case Is > 400
CheckKeys = 3
Case Is > 290
CheckKeys = 2
Case Is > 100
CheckKeys = 1
End Select
End If
End Function

Ausprobieren... Für meine Begriffe reagieren die Taster zu langsam, d.h. müssen relativ lange gedrückt gehalten werden. Der Grund liegt in der BASCOM-Funktion GetADC, da sie mit jedem Aufruf den Multiplexer auf den jeweiligen ADC-Port einstellt, den Vorteiler einstellt, den ADC startet, wartet, bis die Messung ausführt ist, und schließlich den ADC ausliest. Das braucht Zeit. Wenn man den ADC statt im Single-Mode (den verwendet GetADC) freilaufend betreibt und statt der nicht so ganz durchsichtigen BASCOM-Befehle gleich in die ADC-Register schreibt und sie ausliest, sollte die ganze Sache flotter von der Hand gehen. Aber ein ADC-Pin verkraftet mindestens 10 Taster. Ist ja auch schon was.

 

Nun aber richtig, ohne den GetADC-Murks

Zunächst die Erklärungen für Controller (hier Atmega16), Variable und Routinen:

'Taster4_ADC.bas
$regfile = "m16def.dat" 'ATmega16
$crystal = 16000000 '16 MHz xtal
$hwstack = 40
$swstack = 40
$framesize = 40

'Configure LCD on Port D ------------------------------------------------------
Config Lcd = 16 * 2
Config Lcdpin = Pin , Db7 = PortD.5 , Db6 = PortD.4 , Db5 = PortD.3
Config Lcdpin = Pin , Db4 = PortD.2 , E = PortD.1 , Rs = PortD.0
Config Lcdbus = 4
Const bytChar = 16 '16 char LCD

'Configure PortA (ADC port, FWD & REV @ ADC0, ADC1, Keys @ ADC2) --------------
DDRA = &B00000000 'PortA = Input (=ADC-Port)
PortA = &B11111000 'activate Pullup 3...7

'Common Variables, multiply used ----------------------------------------------
Dim bytTmp0 As Byte
Dim bytKeyPressed As Byte
Dim wrdTmp0 As Word
Dim strTmp6 As String * 6

'ADC Variables ----------------------------------------------------------------
Dim bytADMUX As Byte
Dim wrdADC As Word
Dim bytADCLo As Byte At wrdADC Overlay
Dim bytADCHi As Byte At wrdADC + 1 Overlay
Dim ADCVal(3) As Word '1=FWD, 2=REV, 3=keys
Dim ADChannel As Byte '1=FWD, 2=REV, 3=keys
Dim bytADCRun As Byte 'ADC runs for mean value
Dim ADCFinished As Bit '=1: Conversion finished
Dim sngADCKey As Single 'Key ADC value
Const bytADCRep = 5 'Repetitive ADC runs for mean value

Declare Sub ClearLCDLine(byVal bytRow As Byte , byVal bytChars As Byte)
Declare Function CheckKeys(byVal sngKey As Single) As Byte

Den ADC stellen wir Bit für Bit über die Register ein. Kurze Erkärungen sind hier angegeben. Für ausführliche Informationen ist das jeweilige Controller-Datenblatt zu Rate zu ziehen. Je nach Controllergeneration unterscheiden sich die ADC-Register. Bei neueren Controllern sind weitere Register hinzugekommen.

'Configure ADC, Reference = AVCC (ATmega16/32), divider 128 -------------------
bytADMUX = &B01000000
ADMUX = bytADMUX 'ADC Multiplexer Selection Register
' Bit 7 , 6 : REFS1 , REFS0
' 00: Reference = AREF
' 01: Reference = AVCC
' 10: Int. 1.10V Reference
' 11: Int. 2.56V Reference
' Bit 5, ADLAR=0: data right adjusted
' =1: data left adjusted
' Bit 4-0, MUX4...MUX0
' =0000: ADC0 is input
' =0001: ADC1 is input
' =0010: ADC2 is input
' =0011: ADC3 is input
' =0100: ADC4 is input
' =0101: ADC5 is input
' =0110: ADC6 is input
' =0111: ADC7 is input
ADCSRA = &B11001111 'ADC Control & Status Register A
' Bit 7, ADEN = 1: enable ADC
' Bit 6, ADSC = 1: Start ADC
' Bit 5, ADATE = 1: Auto trigger enabled
' Bit 4, ADIF = 1 when conversion completed
' Bit 3, ADIE = 1: Conversion completion
' interrupt activated
' Bit 2-0, ADPS2...ADPS0, ADC prescaler
' =000: Div. factor 2
' =001: Div. factor 2
' =010: Div. factor 4
' =011: Div. factor 8
' =100: Div. factor 16
' =101: Div. factor 32
' =110: Div. factor 64
' =111: Div. factor 128
'SFIOR = &B00000000

On ADC ADC_isr 'Go to interrupt service routine
SREG.7 = 1 'Enable interrupts

Nach jeder ADC-Konversion erfolgt ein Sprung in die Interrupt-Routine ADC_isr (siehe unten).

Im Register ADMUX haben wir eingestellt:

  • Reference = AVCC, passend zur obigen Schaltung
  • ADLAR=0, 10 Bit-Messergebnisse in ADCL/ADCH rechtsbündig
  • Start mit ADC0

Im Register ADCSRA haben wir eingestellt:

  • ADEN = 1, ADC aktiviert
  • ADSC = 1, Starte ADC
  • ADIE = 1, Interrupt bei Beendigung einer Konversion
  • ADPS2...ADPS0 = 111, Teilerfaktor 128. Der ADC-Takt soll für 10 Bit-Auflösung zwischen 50 und 200 kHz liegen. Aus Controller-Takt 16 MHz und Teiler 128 erhält man 125 kHz.

Nun der Hauptprogrammteil:

Cls
Cursor Off
Call ClearLCDLine(1 , bytChar)
LCD "Taster4_ADC"
Call ClearLCDLine(2 , bytChar)
LCD "Press any key..."
Wait 2
Call ClearLCDLine(2 , bytChar)

sngADCKey = 0
bytADCRun = 0
ADChannel = 1 'First ADC channel
ADCFinished = 0

Do 'Start of main program ******

'Do something else...

If ADCFinished = 1 Then
ADCFinished = 0

'Accumulate ADC values -----------------------------------------------------
If bytADCRun < bytADCRep Then 'Accumulate bytADCRep times
sngADCKey = sngADCKey + ADCVal(3) 'Key
Incr bytADCRun
Else
bytADCRun = 0
sngADCKey = sngADCKey / bytADCRep 'Mean value

'Show Keys --------------------------------------------------------------
bytKeyPressed = CheckKeys(sngADCKey) 'Check ADC2 (Keys)
If bytKeyPressed > 0 Then 'Any key was pressed
wrdTmp0 = Int(sngADCKey)
strTmp6 = Str(wrdTmp0)
strTmp6 = Format(strTmp6 , " 0")
Locate 2 , 1
LCD "Key " ; bytKeyPressed ; " " ; strTmp6
End If
sngADCKey = 0
End If
End If

Loop 'End of main program *******

End

Die Interrupt-Service-Routine ADC_isr setzt mit ADCFinished =1 ein Flag, was bedeutet, dass ein neu gemessener ADC-Wert zur Verfügung steht. Zum Entprellen der Taster werden zunächst einmal bytADCRep=5 Messungen ausgeführt und aufaddiert, bevor das Ergebnis mit dem Mittelwert daraus ausgewertet wird. Die Auswertung der Taster erfolgt in der Funktion "CheckKeys". Hier wird aus den Spannungsteilerwerten bzw. den zugehörigen 10 Bit ADC-Messwerten von 0 bis 1023 festgestellt, welcher Taster gedrückt wurde.

Function CheckKeys(byVal sngKey As Single) As Byte
'Check which key was pressed @ ADC resistor chain
'Input: sngKey ADC value of key line
'Output: CheckKeys Number of key pressed

CheckKeys = 0
wrdTmp0 = Int(sngKey) 'Key ADC value
If wrdTmp0 < 1022 Then 'Any key was pressed
Select Case wrdTmp0 'Get key pressed
Case 750 to 850
CheckKeys = 6
Case 620 to 700
CheckKeys = 5
Case 500 to 570
CheckKeys = 4
Case 400 to 500
CheckKeys = 3
Case 290 to 350
CheckKeys = 2
Case 100 to 200
CheckKeys = 1
End Select
End If
End Function

Bleibt noch die ADC-Interrupt-Routine ADC_isr:

ADC_isr:
'Interrupt Service Routine for ADC, conversion complete
'Read 10bit ADC Data Registers ADCL & ADCH
'bytADCLo and bytADCHi are overlayed with wrdADC

bytADCLo = ADCL 'Read ADCL first...
bytADCHi = ADCH '... then ADCH
ADCVal(ADChannel) = wrdADC '10bit ADC value
Incr ADChannel 'Set next MUX channel first
If ADChannel > 3 Then
ADChannel = 1
End If
bytTmp0 = ADChannel - 1
Admux = bytADMUX Or bytTmp0 'Set next MUX channel
ADCFinished = 1
ADCSRA.6 = 1 'Start ADC again
Return

Diese wird mit jeder durchgeführten ADC-Konversion angesprungen. Das Ergebnis steht in den beiden ADC Data Registern ADCL und ADCH. Mit ADLAR=0 (Bit 5 im ADMUX-Register) haben wir eine rechtsbündige Anordnung festgelegt. Die 8 untersten Bits 0 bis 7 stehen daher im Register ADCL, die beiden oberen Bits 8, 9 im Register ADCH. Mit dem Overlay der beiden Bytes bytADCLo und bytADCHi auf die Word-Variable wrdADC haben wir auch schon beide Register eingelesen.

In diesem Beispiel werden 3 ADC-Ports verwendet, wobei die Taster am Port 3 (ADC2) liegen und an ADC0/ADC1 vor- und rücklaufendes Signal für die SWR-Bestimmung gemessen werden. Für die Auswertung der Taster alleine sollte auch eine 8 Bit-Auflösung ausreichen. Hierzu wird ADLAR=1 (Bit 5 im ADMUX-Register) gesetzt, womit eine linksbündige Ausrichtung in den ADC Data Registern ADCL und ADCH erreicht wird. Die 8 höchsten Bits 9 bis 2 liegen also im Register ADCH. Will man nur diese auslesen, entfallen die zwei Zeilen "bytADCLo = ADCL" und "bytADCHi = ADCH". ADCH wird gleich in ADCVal(ADChannel) geschrieben. In "CheckKeys" müssen natürlich auch die Grenzwerte für die Tastererkennung auf den Bereich von nunmehr 0 ... 255 angepasst werden.

Nach dem Auslesen des aktuellen ADC-Kanals wird der nächste wieder direkt über das ADMUX-Register eingestellt. bytADMUX wurde oben mit "&B01000000" (Reference = AVCC) zugewiesen. bytTmp0 kann die Werte &B00000000 (ADC0) bis &B00000010 (ADC2) einnehmen. Mit der Or-Verknüpfung wird also nur der jeweilige ADC-Kanal im Register ADMUX neu gesetzt, womit der ADC-Multiplexer auf einen neuen ADC-Eingang schaltet.

Mit ADCFinished = 1 signalisiert die Routine dem Hauptprogramm, dass ein neuer Wert gemessen wurde. Mit ADCSRA.6 = 1 wird das sechste Bit (ADSC) im Register ADCSRA gesetzt, womit der ADC die nächste Messung startet.

Der Vollständigkeit halber noch die Routine zum Löschen einer LCD-Zeile:

Sub ClearLCDLine(byVal bytRow As Byte , byVal bytChars As Byte)
'Clear LCD bytChars characters in line bytRow
Locate bytRow , 1
LCD SPC(bytChars)
Locate bytRow , 1
End Sub

Die simple Entprellung mit der Mittelwertbildung aus jeweils 5 Einzelmessungen ist halbwegs zuverlässig. Vor allem aber ist eine sofortige Reaktion auf Tastendrücke mit dem Verzicht auf "GetADC" erreicht.

Inzwischen, Ende 2015, ist mir hier in Abschnitt 6 mit der Funktion "ADCOneShot" eine mehr als doppelt so schnelle Alternative zu "GetADC" eingefallen, die auch wie oben direkt mit den ADC-Registern arbeitet.


Taster am ADC-Port ohne ADC-Auswertung

Was wenn – wie im augenblicklich in der Neuplanung befindlichen Antennentuner – der ADC für Messaufgaben genutzt wird und sich die Frage stellt, was man mit den restlichen ADC-Pins anfangen kann?

Situation: ADC0 und ADC1 haben Analog-Messaufgaben, hier im Antennentuner die Vorlauf- und Rücklauf-Spannung für SWR und HF-Power zu messen. Alle anderen Ports sind belegt. Es stehen nur noch ADC2 bis ADC7 eines ATmega16 zur Verfügung. An die sollen 6 Taster angeschlossen werden.

Die erste Lösung wie im vorherigen Kapitel "Taster am ADC mit ADC-Auswertung" beschrieben blieb unbefriedigend wegen der langen Reaktionszeit auf Tastenbetätigungen bei der Verwendung von "GetADC". Der direkte Zugriff auf die ADC-Register brachte eine deutliche Verbesserung. Kann man neben den echten ADC-Auswertungen, hier an ADC0 und ADC1, die restlichen ADC-Pins für reine Schalteranwendungen nutzen? Man kann, auch wenn irgendein AVR-Guru im Web davon abrät.

Eingesetzt wurde die vorher beschriebene "Dannegger-Methode" mit Timer-Interrupts, und es ging tatsächlich, ohne die Messungen an ADC0 und ADC1 zu stören. Hier die Codeschnipsel aus dem an anderer Stelle beschriebenen Controller zum  Antennentuner zur Kombination ADC-Messung und Tasterauswertung an einem ATmega16, ADC und weitere 4 Tastern an PortA als Test:

'Configure ADC  ---------------------------------------------------------------
Config ADC = Single , Prescaler = Auto , Reference = AVCC

'Configure PortA (Key port, 6 keys @ Port A.2 ... A.7) -------------------------
'PA.0 & PA.1 are ADC-Pins ADC0 & ADC1 measuring voltages (real ADC function)
'4 test keys @ Port A.2...A.5 (key0, key1, key2, Key3)
DDRA = &B00000000 'PortA = Input (=ADC-Port)
PortA = &B11111100 'activate Pullup 2...7, not 0, 1!
Key_port Alias PinA 'Input-Port Keys
Const Key0 = 2 'Key0 @ PINA.2
Const Key1 = 3 'Key1 @ PINA.3
Const Key2 = 4 'Key2 @ PINA.4
Const Key3 = 5 'Key3 @ PINA.5

'Configure Timer0-Interrupt for Keys (Timer0 = 8bit, 2^8=256 counts) ----------
'Overflow time: Overflow-Counts * Prescale / crystal frequency
'= 255 * 1024 / 16.000.000 = 16,3 msec = ~ 61 Hz (crystal 16 MHz)
'Possible Prescales: 8, 64, 256, 1024, here 1024
Config Timer0 = Timer , Prescale = 1024
Const Presettimer0 = 56 'Initialize Timer0 (shorten time, >0)
On Timer0 Timer0_isr 'On Overflow go to Timer0_isr

Da ADC0 und ADC1 (PA.0 und PA.1 am ATmega16) Messaufgaben haben, stehen die ADC-Ports ab ADC2 (PA.2) anderweitig zur Verfügung. Mit "PortA = &B11111100" werden also die Pullups für PA.2 bis PA.7 eingeschaltet. Damit können bis zu 6 Taster von PA.2 bis PA.7 nach Masse geschaltet werden.

Der Rest geht wie oben unter 'Die "Dannegger-Methode" mit Timer-Interrupts' beschrieben, also Initialisierung der Taster-Variablen, Timer-Konfigurierung, Timer- und Interrupt-Start und Timer0_isr-Routine. Die Timer0_isr wertet den gesamten Key_port (=PortA) aus, also auch die als ADC0 und ADC1 fungierenden PA.0 und PA.1. Daher werden die Taster Key0, Key1 usw. ab 2 hochgezählt, oben z.B. mit "Const Key0 = 2", womit PA.2 angesprochen wird.

Die Abfrage der Taster geht dann genau so wie mit der Dannegger-Methode, z.B.

Do

'Do something else

'Disable/enable timer0 may be omitted, test it. Omitted here.
If bytKey_state <> bytKey_save Then 'Any key was pressed
'Disable timer0 'Stop Timer0 for display
'Check Key Key0 --------------
If bytKey_press.Key0 = 1 Then 'key0 is pressed
Call ClearLCDLine(2 , 16)
LCD "Key 0"
End If
'Check Key Key1 --------------
If bytKey_press.Key1 = 1 Then 'key1 is pressed
Call ClearLCDLine(2 , 16)
LCD "Key 1"
End If

'Check the other keys...

bytKey_press = 0 'reset Key_press
'Enable Timer0 'Restart Timer0
End If
Loop

Die ADC-Funktionalität für die ADC-Ports A.0 und A.1 kann mit "GetADC" oder variabler und mit besserem Zeitverhalten, aber mit mehr zu schreibendem Code, über die ADC-Register realisiert werden, wie es in 'ADC mal ohne BASCOM-"GetADC"' beschrieben ist.

Der Vollständigkeit halber noch die Sub ClearLCDLine, nichts Tiefsinniges:

Sub ClearLCDLine(byVal bytRow As Byte , byVal bytChars As Byte)
'Clear LCD bytChars characters in line bytRow
Locate bytRow , 1
LCD SPC(bytChars)
Locate bytRow , 1
End Sub

Also - geht doch: PA0 und PA1 arbeiten als ADC0 und ADC1 und messen Gleichspannungen, währenddessen die restlichen Pins des ADC-Port A auf Tastendruck reagieren. Und das so flott und sicher wie oben mit der "Dannegger-Methode" beschrieben.


Gemischte Input-/Output-Funktionen an Port A

Sollen die Ports PA.2 bis PA.7 ganz oder teilweise als Output-Ports dienen, lässt sich das mit dem Data Direction Register DDRA für den Port A entsprechend einstellen, z.B.

DDRA = &B11000000                                 'Port A.0...A.5 = Input, A.6, A.7 = Output
PortA = &B00111100 'activate Pullup 2...5, not 0, 1, 6, 7!

Die Ports A.0 bis A.5 sind Input-Ports, A.0 und A.1 als ADC, A.2...A.5 als Digitaleingänge. Für letztere werden die Pullup-Widerstände nach +5V aktiviert. Die Ports A.5, A.6 sind Output-Ports, z.B. zur Ansteuerung von Funktionsanzeigen mit LED.


Download

Taster mit ADC-Auswertung: taster4_adc.zip


« Taster-Interruptsteuerung
TOP » Taster kurz/lang