Forum: Compiler & IDEs AVR-GCC Programmstruktur (Tastenabfrage+ADC)


von Jimmy (Gast)


Lesenswert?

Hallo,

Ich programmiere gerade eine Display-ansteuerung. Auf der platine sind 4 
Tasten vorhanden, mit denen ich verschiede Werte in das Programm 
eingebe.

Im Endeffekt, möchte ich ADC auslesen und auf dem Display anzeigen 
(+andere Daten aktualisieren), aber auch auf Tasten reagieren. Im 
Prinzip blockiert ADC die Tastenabfrage, weil der C-code ja ziemlich 
linear ist:
1
while(1) {
2
1.Buttonabfrage + damit verbundene Funktionen ausführen
3
2.Adc lesen + Wert anzeigen
4
}
Ich frage mich, welche Möglichkeiten es da gibt, auf Tastendruck immer 
möglichst fix zu reagieren? Interrupts bringt irgendwie nicht viel, weil 
ich ja nicht in die Funktion, die mit der Taste verbunden ist, springen 
kann.

Oder gibts doch eine Möglichkeit Interrupts für diesen Code-Verlauf 
einzusetzen?

Gruß Jimmy

von Karl H. (kbuchegg)


Lesenswert?

Jimmy wrote:

> Oder gibts doch eine Möglichkeit Interrupts für diesen Code-Verlauf
> einzusetzen?

Doch, Interrupts sind die Lösung.

Das Prinzip ist einfach:
Jede Interruptfunktion benutzt eine jeweils eine globale
Variable um anzuzeigen, dass etwas passiert ist.
Dein Hauptprogramm sieht dann so aus:
1
volatile uint8_t Ereignis_1;
2
volatile uint8_t Ereignis_2;
3
4
int main()
5
{
6
  // 
7
  // alles aufsetzen, Ports initialisieren
8
  // alle möglichen Datenquellen so konfigurieren
9
  // das bei vorliegen eines Wertes ein Interrupt
10
  // ausgelöst wird (zb. Wenn eine Taste gedrückt wird
11
  // oder wenn der ADC mit einer Wandlung fertig ist
12
13
  while( 1 ) {
14
15
    if( Ereignis_1 == 1 ) {
16
      // behandle Ereignis_1.
17
      // dies tritt zb. ein, wenn der ADC mit einer
18
      // Messung fertig ist und die zugehörige ISR
19
      // den Wert geholt hat und Ereignis_1 auf 1
20
      // gesetzt hat
21
22
      // die zugehörige Aktion könnte daher sein:
23
      // ADC Wert am Display ausgeben
24
25
      Ereignis_1 = 0;
26
    }
27
28
    if( Ereignis_2 == 1 ) {
29
      // behandle Ereignis_2
30
      // Ereignis2 könnte zb. sein, dass eine bestimmte Taste
31
      // gedrückt wurde.
32
33
      Ereignis_2 = 0;
34
    }
35
  }
36
}

Jede Interrupt Funktion ist also nur dafür zuständig die
entsprechenden Werte bereit zu stellen (mglw. in globalen
Variablen) und das zugehörige Ereignis Flag zu setzen.
Wenn die while Schleife in der main() dann wieder zur
Abfrage kommt, sieht sie das gesetzte Flag und bearbeitet
dieses Ereignis.
Auf diese Art muss nirgends auf irgendetwas gewartet werden
und du kriegst maximale Programmdurchlaufzeit bzw.
minimale Programmantwortzeit auf alle Ereignisse. Die zur
verfügung stehende Rechenzeit teilt sich dynamisch auf die
einzelnen Aufgaben auf, je nachdem wie sie gerade anfallen.

von Simon K. (simon) Benutzerseite


Lesenswert?

Die Interrupts müssen doch im Moment des Flag-zurücksetzens gesperrt 
werden, oder nicht?

von Karl H. (kbuchegg)


Lesenswert?

Simon Küppers wrote:
> Die Interrupts müssen doch im Moment des Flag-zurücksetzens gesperrt
> werden, oder nicht?

Eigentlich sollten sie schon gesperrt werden, wenn die
entsprechende Bearbeitung anfängt. Also

   if( Ereignis_1 ) {
     cli();

     .....

     Ereignis_1 = 0;
     sei();
   }

Eigentlich auch nicht ganz richtig. Eigentlich muesste nur
der eine Interrupt gesperrt werden, der dieses spezielle
Ereignis auslösen kann.

Edit: Stimmt eigentlich auch nicht ganz. Die Interrupts müssen
gesperrt werden, solange auf die globalen Variablen die vom
ISR geschrieben werden, zugegriffen wird.

Wenn eine längere Bearbeitung ansteht, kann es also sinnvoll sein,
sich die Werte zunächst mal in Hifsvariable zu kopieren, damit
der ISR möglichst schnell wieder aktiv werden kann

   if( Ereignis_1 ) {
     char ReceivedString[IRGENDWAS];
     cli();
     strcpy( ReceivedString, StringVonUART_ISR );
     Ereignis_1 = 0;
     sei();

     // und jetzt gibt es viel Zeit um den String auszuwerten, zb.
     Parse( ReceivedString );
     EvaluateFormula();
     WasAuchImmer();
   }

von Jimmy (Gast)


Lesenswert?

Mhh, dass man mit Interrupts bestimmte Vars setzen kann und dann in der 
Haupschleife abfragen ist klar, aber wenn z.B. nun das Ereignis 1 
(z.B.ADC) bearbeitet wird, dann kann man doch keine Tasten abfragen bis 
der code durchlaufen ist? Das ist das Problem. Das sieht doch so aus:
1
///taste=get_key();
2
switch(taste) {
3
  Taste1: code (display-anzeige manipulation usw.)
4
  Taste2: code
5
}
6
7
adc_val=get_ADC();
8
zeige Wert

Wenn ich nun Flags setze, dann muss ich ja trotzdem auf die Abarbeitung 
warten?

Gruß
Jimmy

von Karl H. (kbuchegg)


Lesenswert?

Jimmy wrote:
> Mhh, dass man mit Interrupts bestimmte Vars setzen kann und dann in der
> Haupschleife abfragen ist klar, aber wenn z.B. nun das Ereignis 1
> (z.B.ADC) bearbeitet wird, dann kann man doch keine Tasten abfragen bis
> der code durchlaufen ist?

Das ist richtig.

>
1
> ///taste=get_key();
2
> switch(taste) {
3
>   Taste1: code (display-anzeige manipulation usw.)
4
>   Taste2: code
5
> }
6
> 
7
> adc_val=get_ADC();
8
>

Genau hier liegt der Knackpunkt.
Du kannst den ADC so einstellen, dass er einen Interrupt
auslöst, wenn er fertig ist. D.h. dein Programm muss
nicht aktiv auf das fertigwerden des ADC warten.

> Wenn ich nun Flags setze, dann muss ich ja trotzdem auf die Abarbeitung
> warten?

eben nicht.
Du startest den ADC. Dazu gibt es 2 Möglichkeiten:
entweder du machst das 'free running' (d.h. der ADC arbeitet
ständig) oder aber indem du selbst regelmässig den ADC loslegen
lässt. Wenn der ADC ein Ergebnis fertig hat, meldet er sich
mit einem Interrupt.

D.h. die Programmstruktur sieht zb. so aus
1
volatile uint8_t ADCfinished;
2
3
ISR( ADC_fertig )   // weiss grad nicht wie der ISR richtig heist
4
{
5
  ADCfinished = 1;
6
}
7
8
int main()
9
{
10
  ...
11
  // ADC konfigurieren (MUX und sonstiges einstellen und 
12
  // ganz wichtig: Dem ADC mitteilen, dass er einen interrupt
13
  // auslösen soll.
14
  ...
15
16
  sei();
17
18
  // den ADC erst mal loslegen lassen. StartADC wartet aber nicht
19
  // bis der ADC fertig ist!
20
  StartADC();
21
  // der ADC beginnt jetzt mit einer Wandlung, während das Programm
22
  // hier erst mal weitermacht.
23
24
  while( 1 ) {
25
26
     ...
27
28
     if( ADCfinished ) {
29
       // hole Wert vom ADC. Nur holen! Die Wandlung ist zu
30
       // diesem Zeitpunkt bereits erledigt
31
       // zeige ihn an
32
       
33
       // und dem ADC erneut Arbeit zuweisen
34
       ADCfinished = 0;
35
       StartADC();
36
     }
37
38
     ...
39
   }
40
}

und damit hast du eine Programmstruktur, in der du nicht
auf das Fertigwerden des ADC warten musst. Der ADC meldet
sich mit einem Interrupt wenn er fertig ist und in weiterer
Folge wird in der while der Abschnitt für das ADC Ereignis
kurz betreten um das Ergebnis abzuholen und eine neue
Wandlung anzustossen.

von Jimmy (Gast)


Lesenswert?

Klingt sehr gut, vielen Dank schon mal. Die Grundidee hab ich jetzt 
verstanden, versuche das jetzt umzusetzen.

Gruß
Jimmy

von Andreas Paulin (Gast)


Lesenswert?

Der ADC braucht aber nur ein paar Mikrosekunden für eine Wandlung. In 
der Praxis ists meistens egal, ob das Programm ein paar µs schneller 
oder langsamer reagiert, besonders bei Tastendrücken.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Nicht vergessen, dass die Tasten ohnehin entprellt werden müssen,
das braucht vernünftigerweise einen Timerinterrupt, der so alle
ca. 10 ms triggert.  Den braucht man auch für andere Dinge, und
der fragt die Tasten gleich mit ab.

Den größten Teil seines Lebens darf der AVR dann getrost im Schlaf
verbringen (der hat's gut :).

von Simon K. (simon) Benutzerseite


Lesenswert?

Andreas Paulin wrote:
> Der ADC braucht aber nur ein paar Mikrosekunden für eine Wandlung. In
> der Praxis ists meistens egal, ob das Programm ein paar µs schneller
> oder langsamer reagiert, besonders bei Tastendrücken.

Naja, einige Mikrosekunden? Einige Hundert-Mikrosekunden wohl eher :-)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Simon Küppers wrote:

> Naja, einige Mikrosekunden? Einige Hundert-Mikrosekunden wohl eher :-)

Habe gestern gerade einen Strommesser mit einem AVR gebaut, damit ich
mir den Stromverbrauch einer Schaltung parallel zu den anderen Daten
auf dem logic analyzer mit aufzeichnen lassen konnte.  OK, es waren
effektiv (Differenzmessung, die aber nur mit einer Polarität benutzt
wird, meine Schaltung liefert ja keinen Strom :) nur 7 Bit Auflösung,
aber dafür hab' ich ihn bis 28 µs Wandlungszeit (und dank free running
mode auch Abtastrate) bekommen.  Das ist immerhin dreimal so schnell
wie der kommerzielle Strommesser, den wir am Anfang nehmen wollten
(der hat aber weniger Speichertiefe als der LA, und man hätte seine
Messung nicht einfach mit den LA-Daten korrelieren können).  OK, der
hätte sicher eine deutlich bessere Dynamik gehabt, war aber hier nicht
wichtig.

von Simon K. (simon) Benutzerseite


Lesenswert?

Interessant. Aber wenn man vom worst-case ausgeht sind das 13 Takte 
(oder etwa das doppelte bei der ersten Messung) bei 50kHz Wandler-Takt. 
Macht 260µs.

Bei 16MHz ist eine Periode 62,5ns lang. Das heißt, dass etwa 4000 
Prozessortakte verstreichen, ehe der Prozessor weiterrechnet.*

*Wenn ich jetzt keinen Rechenfehler gemacht habe ;)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Simon Küppers wrote:

> Interessant. Aber wenn man vom worst-case ausgeht sind das 13 Takte
> (oder etwa das doppelte bei der ersten Messung) bei 50kHz Wandler-Takt.
> Macht 260µs.

Naja, was heißt ``worst case''?  Die (50...200) kHz sind ja eine
reine Empfehlung für den optimalen Betriebsbereich.  ``worst case''
kannst du ihn auch noch langsamer machen. ;-)  Dann wird aber
vermutlich die Haltezeit der S&H-Schaltung die Genauigkeit
reduzieren.  Nach oben gibt Atmel mittlerweile offiziell zu, dass
für reduzierte Genauigkeitsansprüche bis zu 1 MHz Takt möglich
sind, das wären 13 µs.  In meinem Fall waren aber bei 14 µs (die
Differenzmessung dauert einen Takt länger) die Messergebnisse für
ein konstantes Eingangssignal schon sehr wackelig geworden, daher
bin ich nur bis 500 kHz gegangen.

In der Praxis kommt man auf Grund des 2**N-Vorteilers wohl eher
selten an die Obergrenze 200 kHz des Optimalbereichs.

> Bei 16MHz ist eine Periode 62,5ns lang. Das heißt, dass etwa 4000
> Prozessortakte verstreichen, ehe der Prozessor weiterrechnet.*

Das ist ein wenig eine Milchmädchenrechnung, auf Grund des besagten
Vorteilerproblems.  16 MHz / 50 kHz = 320, der größtmögliche Vorteiler
ist aber 128.  Generell kannst du ganz einfach sagen, dass bei 13
ADC-Takten irgendwas zwischen 26 und 1664 CPU-Takten zwischen den
(minimal möglichen) Messungen liegt, in 1:2-Abstufungen gestaffelt.
Da ich im Interruptbetrieb (mit SLEEP_MODE_ADC) gearbeitet habe, sind
die 26 Takte (28 bei mir) schon ein wenig eine Herausforderung, damit
man überhaupt noch das Ergebnis ausgegeben bekommt, bevor das nächste
Ergebnis bereit gestellt wird. ;-)  Da die main loop bei mir aber nur
aus einer Endlosschleife von sleep_cpu()s bestand, habe ich mir den
Luxus einer `naked' ISR geleistet und damit jeglichen Prolog/Epilog
gespart.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.