Forum: Mikrocontroller und Digitale Elektronik Verständnisfrage Interrupt UART


von Tom (Gast)


Lesenswert?

Hallo zusammen,

in meinem Projekt kommunizieren zwei ATmega8 via RS232 zusammen. Einer 
liest ständig einen Sensor aus und schickt den aktuellen Wert (immer 
zwei Datenpakete) an den zweiten ATmega8. Dieser empfängt die beiden 
Datenpakete, rechnet sie geeignet um. Dieser Wert wird wiederum 
verrechnet und ausgegeben.

Meine Frage ist nun:
Ich möchte immer zwei Datenpakete einlesen, den Wert speichern, die 
nächsten zwei einlesen und speichern usw. Wie kreiere ich hierzu passend 
die Interrupt-Steuerung?

anbei der Code der Arbeitsschleife:

while(1)
  {
    //lokale Variablen zur Visualisierung des aktuellen Winkels
    uint8 zehner;
    uint8 einer;
    uint8 nachkomma;

    SCA_HH = (SCA_H<<3); //Bitmanipulation MSB
    SCA_LL = (SCA_L>>5); //Bitmanipulation LSB
    SCA_wert=SCA_HH+SCA_LL; //Addition beider Bytes


    //Erfassung dreier Werte um Maximum zu ermitteln
    if((SCA_wert1==0)&&(SCA_wert2==0)&&(SCA_wert3==0))
    {
    SCA_wert1=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    if((SCA_wert1!=0)&&(SCA_wert2==0)&&(SCA_wert3==0))
    {
    SCA_wert2=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    if((SCA_wert1!=0)&&(SCA_wert2!=0)&&(SCA_wert3==0))
    {
    SCA_wert3=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    winkel = asin((SCA_wert-1024)/1638); //Winkel errechnen über Formel 
aus Datenblatt des SCA61T
    winkel=(winkel*180)/M_PI; //Umrechnen von RAD in DEG

    winkel = winkel*10;

    set_cursor(3,1);
    lcd_string("d&b J-Serie");

    if(winkel >= 0)
    {
    set_cursor(2,2);
    lcd_string("Winkel: ");
    zehner=winkel/100;
    lcd_data(zehner+0x30);
    einer=(winkel-(zehner*100))/10;
    lcd_data(einer+0x30);
    lcd_data('.');
    nachkomma=(winkel-(zehner*100)-(einer*10));
    lcd_data(nachkomma+0x30);
    lcd_data(0xdf);
    lcd_data(' ');
    }

    if(winkel < 0)
    {
    winkel=winkel*(-1);

    set_cursor(2,2);
    lcd_string("Winkel: -");
    zehner=winkel/100;
    lcd_data(zehner+0x30);
    einer=(winkel-(zehner*100))/10;
    lcd_data(einer+0x30);
    lcd_data('.');
    nachkomma=(winkel-(zehner*100)-(einer*10));
    lcd_data(nachkomma+0x30);
    lcd_data(0xdf);
    }

    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei

    _delay_ms(100);
  }
  return 0;
}

ist der Ansatz i. O. oder totaler Schwachsinn?

Gruß

Tom

von Peter D. (peda)


Lesenswert?

Was sollen denn die vielen Interruptfreigaben?

Eine reicht völlig, der MC ist ja nicht dement.

Und ohne Interrutphandler schmiert Dir die Kiste eh ab.


Peter

von Stefan B. (stefan) Benutzerseite


Lesenswert?

Von deinen Interruptroutinen sieht man nicht viel. Ich würde es mit dem 
klassischen interruptverfahren machen. Dazu gibt es auch schon massig 
Beispielcode, deshalb hier ganz kurz in Prosa:

Vor der while(1)-Schleife im Userprogramm würde ich die UART 
initialisieren und deren Interrupts einmalig aufsetzen. (Fast) die 
ganzen UART- und Interruptfummeleien in deinem while(1) entfallen.

Der RX-Interrupthandler schreibt empfangene Bytes in einen Puffer und 
führt Buch, ob der Puffer leer, teilgefüllt oder gar voll ist. Die 
Puffergrösse kannst du frei festlegen. Ich würde den so dimensionieren, 
dass keine Zeichen verloren gehen, aber nicht zuviel Platz vom kostbaren 
RAM verloren geht.

Den aktuellen Pufferstand (per volatile Variable) kannst du im 
Userprogramm benutzen, um festzustellen, ob mindestens 2 Bytes (= ein 
Datenpaket) vorhanden sind und abgearbeitet werden können. Die 
notwendige Interruptsperrung beim Transfer vom Empfangspuffer in den 
Verarbeitungspuffer wurde ich so kurz wie möglich halten und auch 
zentral an einer Stelle im Userprogramm unterbringen.

Ich würde mich nicht allein auf die Zählung der Bytes verlassen, sondern 
auch eine Synchronisation einbauen. Im einfachsten Fall ein Start- oder 
Endebyte für ein Datenpaket nach jedem/jedem x.ten Datenpaket.

Ich sehe mömentan nicht das Bild hinter dem 2 AVR Konzept, d.h. weshalb 
der Sensor-AVR nicht die Aufgaben des LCD-AVR mit übernehmen kann. 
Denkbar wäre vielleicht ein Mangel an IO-Pins oder eine räumliche 
Trennung von Sensor-AVR und LCD-AVR.

Vorausgesetzt 2 AVRs sind nötig, würde ich den Datentransfer in ASCII 
machen, d.h. dem sensor-AVR auch die itoa Wandlung aufbürden. Dann kann 
man auch einen PC, PDA, Laptop... anschliessen und den Sensor-AVR 
debuggen. Den LCD-AVR würdest du dann quasi als einfaches *aber 
universelles* RS232-LCD-Terminal betreiben. Ich schätze da gibt es sogar 
fertige Projekte zu.

von Tom (Gast)


Lesenswert?

Danke für Deine schnelle Antwort.

Wie würdest Du dieses Vorhaben realisieren?

Rauskommen sollte eine Maximalwertbestimmung der erhaltenen Werte (wie 
gesagt je zwei Datenpakete). Wenn der Maximalwert gefunden wurde, dann 
einige Werte aufzeichnen bis zum Minimum und aus diesen n Werten den 
Durchschnitt berechnen.

Ich habe leider keine Ahnung, wie ich das mit der Interruptsteuerung 
realisieren soll, bzw. wann/wo das Programm nach Empfang von UART 
weitermacht.

Gruß

Tom

von Tom (Gast)


Lesenswert?

Hier nochmal der ganze Code:


//HEADERDATEIEN
#include <avr/io.h>
#include <util/delay.h>
#include <avr/own_lcd.h>
#include <avr/own_uart.h>
#include <avr/interrupt.h>
#include <avr/stdint.h>
#include <math.h>

//FESTLEGUNG UNABHÄNGIGER TYPENNAMEN
typedef unsigned int uint16;
typedef signed int sint16;
typedef unsigned char uint8;
typedef signed char sint8;

//DEFINITION GLOBALER VARIABLEN
volatile uint8 SCA_L; //unteres Register vom SCA
volatile uint8 SCA_H; //oberes Register vom SCA
uint16 SCA_HH; //oberes Register nach Bitmanipulation
uint16 SCA_LL; //unteres Register nach Bitmanipulation
double SCA_wert=0;
double SCA_wert1=0;
double SCA_wert2=0;
double SCA_wert3=0;
double winkel;

//FUNKTIONSPROTOTYPEN
void nm_intro (void);


//SERVICEROUTINE FÜR EMPFÄNGERINTERRUPT
SIGNAL(SIG_UART_RECV)
{
  //Einlesen zweier Werte über RS232 und Übergabe an Hilfsvariablen
  getch();
  SCA_H = got_cha;
  getch();
  SCA_L = got_cha;
  cli();  //Alle Interrupts global sperren
}




//HAUPTPROGRAMM
int main (void)
{
  lcd_init(); //Initialisieren des Displays
  nm_intro(); //Anzeige nach Start des Controllers
  initusart(); //RS232 initialisieren (in own_uart.h)

  lcd_clear();
  set_cursor(0,1);

  while(1)
  {
    //lokale Variablen zur Visualisierung des aktuellen Winkels
    uint8 zehner;
    uint8 einer;
    uint8 nachkomma;

    SCA_HH = (SCA_H<<3); //Bitmanipulation MSB
    SCA_LL = (SCA_L>>5); //Bitmanipulation LSB
    SCA_wert=SCA_HH+SCA_LL; //Addition beider Bytes


    //Erfassung dreier Werte um Maximum zu ermitteln
    if((SCA_wert1==0)&&(SCA_wert2==0)&&(SCA_wert3==0))
    {
    SCA_wert1=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    if((SCA_wert1!=0)&&(SCA_wert2==0)&&(SCA_wert3==0))
    {
    SCA_wert2=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    if((SCA_wert1!=0)&&(SCA_wert2!=0)&&(SCA_wert3==0))
    {
    SCA_wert3=SCA_wert;
    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei
    }

    //Ermittlung des Maximums
    if((SCA_wert3<SCA_wert2)&&(SCA_wert2<SCA_wert1))
    {
      set_cursor(0,1);
      lcd_string("XXXXXXX");
    }

    winkel = asin((SCA_wert-1024)/1638); //Winkel errechnen über Formel 
aus Datenblatt des SCA61T
    winkel=(winkel*180)/M_PI; //Umrechnen von RAD in DEG

    winkel = winkel*10;

    set_cursor(3,1);
    lcd_string("d&b J-Serie");

    if(winkel >= 0)
    {
    set_cursor(2,2);
    lcd_string("Winkel: ");
    zehner=winkel/100;
    lcd_data(zehner+0x30);
    einer=(winkel-(zehner*100))/10;
    lcd_data(einer+0x30);
    lcd_data('.');
    nachkomma=(winkel-(zehner*100)-(einer*10));
    lcd_data(nachkomma+0x30);
    lcd_data(0xdf);
    lcd_data(' ');
    }

    if(winkel < 0)
    {
    winkel=winkel*(-1);

    set_cursor(2,2);
    lcd_string("Winkel: -");
    zehner=winkel/100;
    lcd_data(zehner+0x30);
    einer=(winkel-(zehner*100))/10;
    lcd_data(einer+0x30);
    lcd_data('.');
    nachkomma=(winkel-(zehner*100)-(einer*10));
    lcd_data(nachkomma+0x30);
    lcd_data(0xdf);
    }

    UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
    sei();          //Alle Interrupts global frei

    SCA_wert1=0;
    SCA_wert2=0;
    SCA_wert3=0;

    _delay_ms(100);
  }
  return 0;
}

von Stefan B. (stefan) Benutzerseite


Lesenswert?

Tom wrote:

> Ich habe leider keine Ahnung, wie ich das mit der Interruptsteuerung
> realisieren soll, bzw. wann/wo das Programm nach Empfang von UART
> weitermacht.

Dann lies dich ein. Startpunkt ist z.B. 
http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Interruptbetrieb

Leider noch eine Baustelle, aber unter den sort angegebenen Links ist 
schon was hilfreiches.

Eine weitere Infoquelle ist 
http://www.roboternetz.de/wissen/index.php/UART_mit_avr-gcc#Variante_2:_Mit_Interrupts

von Stefan B. (stefan) Benutzerseite


Lesenswert?

Tom wrote:

> //SERVICEROUTINE FÜR EMPFÄNGERINTERRUPT
> SIGNAL(SIG_UART_RECV)
> {
>   //Einlesen zweier Werte über RS232 und Übergabe an Hilfsvariablen
>   getch();
>   SCA_H = got_cha;
>   getch();
>   SCA_L = got_cha;
>   cli();  //Alle Interrupts global sperren
> }

Das gefällt mir nicht, weil hier eine Lowlevel-Routine 
(Interrupthandler) und Highlevelroutine (getch()) gemixt werden und die 
Aufrufsequenz (1 Iterrupt pro Zeichen) nicht beachtet wird. Nebenbei ist 
die SIGNAL Schreibweise inzwischen durch die modernere ISR Schreibweise 
abgeläst (s. AVR-GCC Manual)

Nach
http://winavr.scienceprog.com/avr-gcc-tutorial/interrupt-driven-avr-usart-communication.html

würde der Interrupthandler z.B. so aussehen

volatile char rx_bytes;

ISR(USART_RXC_vect)
{
  switch (rx_bytes)
  {
    case 2:
      // Zeichen empfangen ohne dass Userprogramm die vorhergehenden
      // verarbeitet hat. Lösung hier: Altes Datenpaket verwerfen.
      rx_bytes = 0;
      /* Durchfall */
    case 0:
      SCA_H = UDR;
      break;
    case 1:
      SCA_L = UDR;
      break;
  }
  rx_bytes++;
}

Und in dem Userprogramm

while(1)
{
   if (rx_bytes == 2)
   {
     // 2 Bytes empfangen, jetzt auslesen

     // Während Zugriff auf SCA_H und SCA_L und rx_buffer INTs sperren!
     // Hier Brute-Force-Lösung. Kann man mit SREG schöner lösen
     // s. Literatur bei atomic Zugriff
     cli();
     SCA_HH = SCA_H;
     SCA_LL = SCA_L;
     rx_bytes = 0;
     sei();

     // Restliche Berechnungen...
     SCA_HH <<= 3; //Bitmanipulation MSB
     SCA_LL >>= 5; //Bitmanipulation LSB
     SCA_wert=SCA_HH+SCA_LL; //Addition beider Bytes

     // ...
   }
}

von ... .. (docean) Benutzerseite


Lesenswert?

auch bei case 2: muss das UDR Register gelesen werden, sonst wird der 
IRQ nicht gelöscht..

ich würde am anfang UDR in ei9ne temp. Var auslesn und dann erst switch 
case

von Tom (Gast)


Lesenswert?

Hallo zusammen,

also nochmals vielen Dank für Eure Hilfe. Mittlerweile verstehe ich die 
ganze Interrupt-Sache besser.

Jetzt habe ich meine Code folgendermaßen realisiert:



//SERVICEROUTINE FÜR EMPFÄNGERINTERRUPT
ISR(USART_RXC_vect)
{
  rx_temp = UDR;

    switch (rx_bytes)
    {
       case 2:
        // Zeichen empfangen ohne dass Userprogramm die vorhergehenden
        // verarbeitet hat. Lösung hier: Altes Datenpaket verwerfen.
       rx_bytes = 0;
       /* Durchfall */
       case 0:
       SCA_H = UDR;
        break;
      case 1:
       SCA_L = UDR;
        break;
    }
    rx_bytes++;
}





//HAUPTPROGRAMM
int main (void)
{
  lcd_init(); //Initialisieren des Displays
  nm_intro(); //Anzeige nach Start des Controllers
  initusart(); //RS232 initialisieren (in own_uart.h)

  lcd_clear();
  set_cursor(0,1);

  while(1)
  {
    //lokale Variablen zur Visualisierung des aktuellen Winkels
    uint8 zehner;
    uint8 einer;
    uint8 nachkomma;

    if(rx_bytes==2)
    {
      cli();
      SCA_HH = SCA_H;
      SCA_LL = SCA_L;
      rx_bytes=0;
      UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
      sei();          //Alle Interrupts global frei

      SCA_HH <<= 3; //Bitmanipulation MSB
      SCA_LL >>= 5; //Bitmanipulation LSB
      SCA_wert=SCA_HH+SCA_LL; //Addition beider Bytes

      winkel = asin((SCA_wert-1024)/1638); //Winkel errechnen über 
Formel aus Datenblatt des SCA61T
      winkel=(winkel*180)/M_PI; //Umrechnen von RAD in DEG

      winkel = winkel*10;

      ///.... WEITERER CODE


      _delay_ms(100);
    }

    else
    {
      cli();
      UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
      sei();
    }

  }
  return 0;
}



Das Programm mach nun genau das, was es vor der Änderung gemacht hat. 
Was ja schon mal nicht schlecht ist.

Fragen:
Was ist in den Fällen rx_bytes==1 oder rx_bytes==0?
Wie schaffe ich es, viele Werte einzulesen und daraus den Mittelwert zu 
bilden?

Besten Dank für Eure Hilfe!!

von Stefan B. (stefan) Benutzerseite


Lesenswert?

rx_bytes ist ein Zähler, wieviele Bytes die RX Interruptroutine in die 
beiden Variablen SCA_L und SCA_H gefüllt hat.

Bei rx_bytes == 0 und rx_bytes == 1 ist der Inhalt der variablen nicht 
legal auswertbar, da entweder noch keine neuen oder nur teilweise 
empfangene Bytes eingetragen sind.

Nur bei rx_bytes == 2 wurden die beiden zuletzt empfangenen Bytes 
eingetragen und das Userprogramm darf das auswerten. Der "Durchfall" bei 
rx_bytes == 2 zum Fall case 0: in der ISR berücksichtigt die Anmerkung 
von Jan-h. B.. UDR wird in diesem Fall auch gelesen - allerdings startet 
in dem Fall ein neues Datenpaket. Das letzte Datenpaket ist somit für 
das Userprogramm verloren.

In dem Zusammenhang halte ich auch das _delay_ms(100); für ungeschickt. 
Während dieses Trödelns können Zeichen verloren gehen. Wenn die Pause 
nur dazu da ist, eine stabilere LCD anzeige zu gewährleisten (kein 
Flackern) würde ich das anders machen z.b. den aktuellen Wert der 
Anzeige zwischenspeichern und ein Update der Anzeige bei einem neuen 
Wert nur machen, wenn sich zwischengespeicherter Wert und neuer Wert 
unterscheiden.

Ich habe oben dem Userprogramm die Möglichkeit gegeben, anch dem 
Auswerten (Auslesen) rx_bytes zu "Nullen". Wenn kein neues Zeichen oder 
Datenpaket empfangen wird, so verhindert, dass der alte Wert nochmal 
ausgelesen wird.

Das Fummeln im Userprogramm an

      UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei

und
    else
    {
      cli();
      UCSRB |= (1<<RXCIE);  //Empfängerinterrupt frei
      sei();
    }

würde ich unterlassen.

Der RX-Interrupt läuft (fast, s.u.) immer durch. Die Freigabe des 
Interrupts sollte in initusart(); //RS232 initialisieren (in own_uart.h) 
geschehen. Wenn in initusart() kein sei() gemacht wird, ist vor dem 
while(1) ein sei() einzufügen.

Nur das Auslesen der 16-Bit in SCA_L und SCA_H und das Manipulieren von 
rx_bytes im Userprogramm darf nicht durch den RX-Interrupt unterbrochen 
werden. Daher hier die cli()/sei() Klammer wenn man nur den UART 
Interrupt betreibt. Wenn man weitere Interrupts hat (Timer, ...), die 
man global stoppen oder einschalten will, arbeitet man mit dem SREG.
http://www.mikrocontroller.net/articles/Interrupt#Atomarer_Datenzugriff

Die Mittelwertbildung würde ich im Userprogramm machen und zwar ab der 
Stelle ///.... WEITERER CODE, wenn eine Mittelwertbildung für winkel 
gemacht werden soll. Eine einfache Variante wäre es z.b. die einelnen 
Winkel aufzuaddieren und durch die Anzahl der Additionen zu teilen. Das 
kann für alle Einzelmessungen geschehen oder nur z.B. für die letzten 
10, 100, o.ä. Messungen. Für eine speicherschonende, gleitende 
Mittelwertbildung sollte es eigentlich genug Rechenvorschriften und 
Democode im Netz geben (habe aber selbst noch nicht gesucht).

Edit: Beitrag "Mittelwert über 3 Werte bilden"

von Peter D. (peda)


Lesenswert?

Das Ganze wird nie zuverlässig funktionieren.

Woher weiß denn der Empfänger, welches das 1. und welches das 2. Byte 
sein soll ???

Ein Störimpuls oder einer wird resettet oder später eingeschaltet und 
schon sind sie asynchron und die Bytes bedeuten nur noch Mist.


Du mußt Dir also zuerst mal ein Protokoll ausdenken, wie der Empfänger 
die beiden Bytes eindeutig unterscheiden kann.

Eine Möglichkeit wäre, da Du ja nur 11 Bits übertragen willst, das Bit 7 
zur Unterscheidung zu nehmen.
Oder Du nimmst den 9Bit-Mode.

Eine andere, daß beide Bytes innerhalb einer bestimmten Zeit (z.B. 10ms) 
reinkommen müssen und zum nächsten Datensatz immer eine größere Zeit 
(z.B. 20ms) Abstand ist. Dann kann man einen Timer aufsetzen, um das zu 
erkennen.

Oder Du überträgst den Wert als Text mit "\n" als Endekennzeichen.


Peter

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.