Forum: Mikrocontroller und Digitale Elektronik Generelles vorgehen bei multiplen Interrupts


von Florian Bantner (Gast)


Lesenswert?

Hallo,

ich schreibe gerade an einem größeren Programm auf avr5. Dabei
'verbrauche' ich ziemlich viele Interrupts (3 timer, can, uart,
twi).
Die Programmlogik läuft fast ausschliesslich in den Interrupts ab. Mit
zunehmender Komplexität des Programms passiert es jetzt aber, dass sich
die Interrupts gegenseitig in die Quere kommen (vermute ich).

Auf der Suche nach Literatur zum allgemeinen Umgang mit solchen
Problemen bin ich bisher nicht fündig geworden. Anleitungen scheinen
sich immer nur entweder auf den Fall "kleines Programm mit wenigen
Interrupts" oder "Arbeiten mit einem Betriebssystem (z.B. ethernuts
etc.) zu beziehen.

Nachdem dies mein erstes Microcontroller-Projekt ist und ich sonst nur
(meist größere) Softwareprojekte auf PC mit OO und allem Schnickschnack
gemacht habe und mit Interrupts als zeitkritischen Faktor noch überhaupt
keine Erfahrungen habe:

Das einzige was mir als mögliche Lösung eingefallen ist, ist eine
Tabelle anzulegen mit Interrupt-Routinen, (max) Zeitbedarf, (min)
kritische Zeit, etc. Hat sich aber schnell als extrem aufwändig
herausgestellt.

Wie geht Ihr vor, wenn Ihr Programme mit mehreren Interrupt-Quellen in
eine übersichtliche Form bringen wollt? Wie weisst man nach, dass keine
(kritischen) Überschneidungen vorkommen? Also eine Art 'Best
practice'?

Für Tipps, Anregungen, Literaturhinweise wäre ich sehr dankbar.

Schoene Gruesses,

Florian

von A.K. (Gast)


Lesenswert?

Wenn zur Laufzeit eines Interrupts weitere eintreffen, in das ja
zunächst kein Problem. Die meisten Interrupts vom AVR werden bereits
beim Start der Intr-Routine zurückgesetzt, also kann das Int-Flag
gleich wieder eingeschaltet werden. Wichtig ist dann vor allem:
- aussreichende Stack-Grösse,
- es sollte nicht wieder der gleiche Int auftreten während der alte
noch nicht durch ist. Aber wenn das passiert, geht wahrscheinlich eh
was über Bord. Kann man handhaben, indem in der Routine das Int-Enable
zum eigenen Interrupt am Anfang gelöscht und erst am Ende wieder
eingeschaltet wird. Priorisierung "zu Fuss" sozusagen.

Die fehlende Priorisierung lässt sich ggf. auch durch ein RTOS ersetzen
(z.B. AvrX). Also die Interrupts so knapp wie möglich halten (Semaphore
signalisieren) und das meiste in entsprechend priorisierten Prozessen
erledigen (die auf ebd. Semaphore warten). Es geht etwas an Leistung
dabei flöten, es kann aber an Übersichtlichkeit gewinnen (wohl auch
Geschmacksache), zumal auf die Art manch eine State-Maschine dort
landet wo sie m.E. am lesbarsten ist: im Program Counter.

von Santa Klaus (Gast)


Lesenswert?

Hallo A. K.,

ich habe Deinen interessanten Beitrag gelesen und auch alles
verstanden.   Nur die Aussage des letzten Satzes

>zumal auf die Art manch eine State-Maschine dort
>landet wo sie m.E. am lesbarsten ist: im Program Counter.

wollte sich mir nicht erschließen.  Wie hast Du das gemeint? Könntest
Du mir das mit ein paar weiteren Worten erläutern?

Vielen Dank im voraus.

von Peter D. (peda)


Lesenswert?

"Die Programmlogik läuft fast ausschliesslich in den Interrupts ab."

Sowas sollte man generell vermeiden, das schreit geradezu nach
Problemen.
Der Großteil an CPU-Rechenzeit sollte immer in der Mainloop
verbleiben.

Also jede Interruptaufgabe in 2 Teile aufteilen, den zeitkritischen
Interruptteil und den unkritischen Teil, der später gemacht werden
kann.


"Das einzige was mir als mögliche Lösung eingefallen ist, ist eine
Tabelle anzulegen mit Interrupt-Routinen, (max) Zeitbedarf, (min)
kritische Zeit, etc. Hat sich aber schnell als extrem aufwändig
herausgestellt."

Das ist ein sehr guter Ansatz und bei Sicherheitssoftware sogar
Pflicht.
Sowas nennt man worst-case, d.h. man geht davon aus, daß alle
Interrupts mit der maximal möglichen Interruptrate reinkommen und die
maximale Zeit dauern und trotzdem kein Interrupt verloren gehen darf.

Und wenn die Interrupts kurz gehalten werden ist das auch nicht sehr
aufwendig.


Am wichtigsten ist eine gute Programmplanung, d.h. welche Sachen müssen
wie schnell und wie oft gemacht werden.

Es macht z.B. keinen Sinn auf ein LCD 1000 mal je Sekunde auszugeben,
da niemand so schnell lesen kann, alle 0,2...0,5s reicht völlig.

Auch Tastendrücke müssen nicht sofort bearbeitet werden, da der Mensch
selber schon >0,3s Verzögerung hat.


CAN und UART sollte man generell über Puffer machen, die man dann
später im Main auswerten kann.

I2C kann man auch puffern, muß man aber nicht, da der I2C-Slave ja den
Master warten lassen kann, bis er die Daten ausgewertet hat.

Bei den Timern sollte man untersuchen, ob man wirklich alle braucht.
Oftmals ist es sinnvoller, einen Timer alle zeitabhängigen Aufgaben
zusammen in Software machen zu lassen.


Peter

von Florian Bantner (Gast)


Lesenswert?

Vorweg: Was ich suche, ist 'beweisbare' richtigkeit. Dabei lege ich
zwar keine reihne mathematische Strenge zugrunde, aber immerhin muss es
'einleuchtend' stabil sein. Abstürze kann ich mir nämlich auch im
worst-case nicht leisten.

@A.K.:
"Wenn zur Laufzeit eines Interrupts weitere eintreffen, in das ja
zunächst kein Problem. ... - aussreichende Stack-Grösse" ist imho nur
soweit richtig, als dass man eben auch nachweisen kann, dass der Stack
nicht irgendwann überläuft. Das ist wohl noch schwerer, als
nachzuweisen, dass sich Interrupts nicht in die Quere kommen. (Das
Wissen, wie oft sie sich in die Quere kommen, ist nämliche zwingende
Voraussetzung dafür.)

"- es sollte [in der Int-Routine] nicht wieder der gleiche Int
auftreten ..." wäre nach obiger Argumentation auch kein Problem,
solange der Stack groß genug und die Routinge reentrant ist. Dann aber
auch wieder Problem: Wie oft? Mir auf jeden Fall lieber, wenn es
garnicht auftritt.

"Die fehlende Priorisierung lässt sich ggf. auch durch ein RTOS
ersetzen (z.B. AvrX)" Hatte ich noch nichts davon gehört & schau ich
mir auf jeden Fall mal an (die müssen sich ja auch mit dem Problem
beschäftigen. Frei?) Aber das ist eben genau mein Problem: Doku
(Bücher) für Entwicklung mit einem Embedded-Os gibt einige. Literatur
zum Thema 'Komplexe Anwendung ohne OS' dagegen kaum (ich habe noch
garkeine gefunden).

@Peter:
"... generell vermeiden ..." war ich eigentlich auch ein Fan von. Ist
bei kleinen Sachen auch nicht so das Problem. Bei meinem jetzt größer
gewachsenen Projekt kommen aber auch dadurch eine reihe von Problemen
dazu:
  * viele Module müssen sowohl mit Aufrufen aus Ints als auch mit
aufrufen aus 'main' klarkommen.
  * daher muss kritischer Code überall Interrupt-sicher gemacht werden.
(Evtl. mach ich dazu noch einen eigenen Thread auf. Wo überall Probleme
auftrreten können durhc eine Unterbrechung übersehe ich nämlich auch
noch nicht 100%)
  * An vielen Stellen muss eine Sicherung eingebaut werden, die ein
überlaufen von Buffern verhindert & eine dazugehörige Strategie (delete
first, delete last, reset, ... )

Aber generell bin ich auch noch am überlegen, ob ich nicht doch
Funktionalität nach main verlagere (dort überhaupt nichts zu haben war
zumindest in gewisser hinsicht ein Plus an Übersicht. Z.B. habe ich
Routinen, die dann sowohl von Ints als auch von main aufgerufen
werden.). Aber vermutlich überwiegen die Vorteile kleinerer Interrupts.
So habe ich im Moment z.B. starke Probleme, die Laufzeit eines
Interrupts zu bestimmen (nur mit Oszi). Das sollte mit kurzen
Interrupts auf jeden Fall besser werden.

"Das ist ein sehr guter Ansatz und bei Sicherheitssoftware sogar
Pflicht." Das ist ja interessant. Was ist denn da noch Pflicht? Gibts
da Doku zu? Evtl. kann ich mir da ja noch Anregungen holen. Ich bin
zwar nicht 'Sicherheitskritisch' im engeren Sinne, aber es wäre auf
jeden Fall dem Projekt alles andere als zuträglich, sollten sich im
Betrieb probleme ergeben.

[Restlicher Text]: Uart ist gebuffert (ringbuffer). Can muss direkt
bearbeitet werden, deswegen nur 1-Frame input Buffer (in Software). I2C
(twi) hat tatsächlich die Probleme erst ausgelöst. Da bin ich nämlich
Slave-Receiver. Ledeider schert sich der Master einen Dreck um sclk
low. Das kombiniert mit einem hirntoten Timing erzeugt ziemlich viel
Stress in den Interrupts. Aber leider kann ichs mir nicht aussuchen.

Timer hab ich 3 weil: 1x System-Timer mit Tasks in einer Linked-List.
Calls jede 1ms. (Da kommt auch viel Last im Sinne von zwar selten, dann
aber heftig, in die Interrupts.). 1x Timer ist das generieren von einem
Steuerungssignal das ein sehr exaktes Timing in 900us mit wenigen % +/-
hat. Da hilft zum Glück an toggle on compare-match um das ganze nicht
ausarten zu lassen. Der 3. Timer erzeugt ein varibles
frequenzmoduliertes Signal in der Größenordung von 1s-5ms. Ließe sich
evtl in den System-Timer nehmen. Kommt aber mit Glück ohne Ints aus
(Habs noch nicht probiert, steht aber hier irgendwo im Forum.)

Danke euch Beidne auf jeden Fall für die Tips.

Sollte noch jemand Ideen haben, ich bin für jede dankbar. Besonders
suche ich immer noch 'Fachliteratur' zu dem Thema.

von A.K. (Gast)


Lesenswert?

@Santa Klaus:

Jedes ganz gewöhnliche Programm ist bereits eine (implizite) State
Machine. Der Status steckt dabei im Program Counter. Während sich der
Ablauf expliziter State Machines bisweilen recht unübersichtlich
gestaltet, ist das bei Programmabläufen in strukturierter
Programmierung meist leichter erkennbar.

Vollends unübersichtlich werden explizite State Machines, wenn sich
mehrere solcher ursprünglich unabhängiger Gestalten zusammentun und
eine einzige gesamte State Machine bilden. Sowas lässt sich m.E. nur
per Metaprogrammierung mit entsprechenden Design-Tools noch
beherrschen.

Per RTOS bleiben die einzelnen State Machines weiterhin voneinander
getrennt (separate Prozesse) und im normalen Programmablauf einer
Hochsprache codiert. Unabhängige Dinge bleiben so unabhängig und
modular. Vorrang bestimmer Ereignisse lässt sich leicht über die
Priorität der entsprechenden Prozesse regeln.

Allerdings gehe ich davon aus, dass Metaprogrammierung über Design
Tools eher die Beweisbarkeit eines korrekt funktionierenden Systems
erlaubt.

von A.K. (Gast)


Lesenswert?

@Florian: Es gibt diverse freie RTOS. Konkret verwendet habe ich AvrX,
interessant finde ich beispielsweise auch FreeRTOS und XMK.

von Peter D. (peda)


Lesenswert?

"ob ich nicht doch Funktionalität nach main verlagere (dort überhaupt
nichts zu haben war zumindest in gewisser hinsicht ein Plus an
Übersicht."

Also das Main leer zu lassen ist schlichtweg Nonsens, übersichtlicher
wird dadurch garnichts, man verschenkt nur einen Ausführungslevel
völlig nutzlos.

Bei den 8051 mit 4 Interruptprioritäten könnte man es machen, da dort
trotzdem wichtige Sachen die weniger wichtigen unterbrechen können.
Aber wie gesagt, es macht einfach keinen Sinn.



"Z.B. habe ich Routinen, die dann sowohl von Ints als auch von main
aufgerufen werden."

Das ist pures Gift, die Seiteneffekte sind kaum zu durchschauen oder
man müßte die Aufrufe vom Main komplett unter Interruptverbot
ausführen.



"Ledeider schert sich der Master einen Dreck um sclk
low."

Dann kannste alles vergessen !
Wenn der Master sich nicht an die I2C-Spezifikation hält, ist eine
zuverlässige Kommunikation schlichtweg unmöglich !

Es kommt ja auch keiner auf die Idee, sich ein eigenes CAN-Protokoll
auszudenken und dann zu erwarten, daß andere CAN-ICs es beherrschen.

Die einzige Lösung wäre dann ein extra ATMega8, der keine weiteren
Interrupts hat und z.B. über SPI mit Deinem eigentlichen Slave die
Daten austauscht.


Reentrante Funktionen sollte man grundsätzlich vermeiden. Das gibt nur
hohen Stackverbrauch, viel CPU-Zeitverbrauch und einen
unübersichtlichen und deshalb fehleranfälligen Programmablauf.

Schau Dir mal an, was Interrupts so an Tod und Teufel umher-pushen und
popen. Und dann mal, wie schnell eine Funktion ausgeführt wird, wenn
sie im Main ist.
In der Summe kann daher ein Programm viel mehr echtzeitiger sein, wenn
es hauptsächlich im Main läuft.

Auch braucht man Mainfunktionen nirgends gegeneinander zu kapseln, da
sie ja nur scheinbar gleichzeitig ablaufen, in Wirklichkeit aber in
sehr kurzer Abfolge hintereinander ausgeführt werden.

D.h. man muß nur die wenigen Stellen kapseln, wo Daten mit Interrupts
ausgetauscht werden.

Je mehr man im Main macht, umso schneller, einfacher und
übersichtlicher wird also Dein Programm.

Je besser man den Programmablauf plant und je mehr man die Funktionen
in einzelne Module unterteilt, umso besser kann man entscheiden, was in
den Interrupt gehört und was ins Main.

Ich vermute mal, da Du alles in den Interrupts machst, daß Du große
Schwierigkeiten damit hast, die einzelnen Aufgaben zu modularisieren,
d.h. in kleine und kleinste Funktionsblöcke zu unterteilen.
Und dann kommt es natürlich schnell zu Monsterinterrupts, die
programmtechnisch und vor allem CPU-Zeit mäßig kaum zu beherrschen
sind.


Ich habe auch Anwendungen mit UART und CAN. Dabei gibt es keinerlei
zeitliche Probleme, da beide streng Nachrichten orientiert arbeiten.
D.h. es wird genügend Puffer bereitgestellt, um mindestens eine
Nachricht zu empfangen und dann wird dem Main signalisiert, daß eine
Nachricht im Puffer ist.
Das Main wertet dann die Nachricht aus und sendet eine Quittung oder
Antwort zurück.
Damit ist es dann kein Problem, wenn die Quittung z.B. erst 10ms später
erfolgt, da das Main noch mit was anderem beschäftigt war.

In der Praxis benutze ich aber die ersten 8 Puffer des CAN
(AT89C51CC03) und 256 Byte für die UART.
D.h. die Gegenstelle kann bis zu 8 CAN-Nachrichten bzw. UART-Kommandos
bis zu 255 Byte am Stück senden, ehe sie eine Quittung abwarten muß.


Peter

von Florian Bantner (Gast)


Lesenswert?

@peter:

Also an der Modularisierung soll es nicht scheitern. Jede
'IO-Baugruppe' hat bei mir ihr eigenes Modul, das in etwa die
Funktionen 'init( (*callback)())', 'send( *msg )' unterstützt. Das
habe ich ursprünglich so angelegt, um einerseits allgemeingültige
Module zu haben, andererseites die Möglichkeit sowohl in den Interrupts
als auch in Main damit zu arbeiten. In Main.c (jetzt meine ich
ausnahmsweise mal die Datei) und zwar da in der Callback-Funktion, habe
ich also die Möglchkeit, entweder weitere Module aufzurufen, oder die
Daten z.B. in eine msg[2] Buffer abzulegen und dann in main (jetzt die
Ausführungsebene) die Daten weiter zu verarbeiten. Also hier soll es
nicht scheitern.

Wo ich jetzt aber (zumindest bisher) den Vorteil von Ausführung in den
INterrupts gesehen habe, lässt sich am besten an einer IO-Funktion
darstellen: 'sendMsg(*msg)' im Can-Modul beinhaltet mehrere
Interrupt-kritische Bereiche:
1. Kopieren der Daten in den Sendebuffer. Hierbei darf kein Interrupt
'CANPAGE' ändern.
2. Zugriff auf die übergebenen 'Zeiger-Parameter'. Da darf die Quelle
in der Zwischenzeit von keinem Interrupt geändert werden.
3. Zugriff auf weitere globale Variablen. (vor allem durch Reentranz
1xInt 1xMain).

Nun sieht meine Anwendung (übrigens ein Adapter zw. verschiedenen
Geräten, also kann ich mir nicht aussuchen, wie die Protokolle
aussehen, leider) so aus: Im Can-Bereich gibt es essentiell 2
verschiedene Typen von Nachrichten: Textübertragung für ein Display,
hierbei ist Zeit unkritisch. Ließe sich also hervorragend nach main
verfrachten. Zweitens eine Art von 'Ping' Nachrichten (leider keine
Remote), die immer gleich (und schnell) beantwortet werden. Diese sind
in den Interrupts sehr gut aufgehoben (kurz+schnell). Ausserdem ergibt
sich so ein gewisser Filter zw. Funktionalität und Protokoll, den ich
gar nicht schlecht finde. Das ist auch der Grund, warum meine
Funktionen sowohl von Interrupts als auch von Main aufrufbar sein
sollten.

Also habe ich dann folgende Möglichkeiten:
1. Zwei Versionen der Funktion: sendMsg() und sendMsgIntSave()
2. Sichern der kritischen Bereich innerhalb der Funktion mit cli/sei
+ SREG.

Beide Möglichkeiten haben spezifische Nachteile (die Vorteile sollten
klar sein):

1. läuft praktisch auf das duplizieren des Moduls hinaus, da es eben in
kleine/kleinste Funktionen aufgesplittet ist, und hierbei die
'Eigenschaft IntSave' an alle weiteren Funktionen weitergegeben
werden muss. Die läuft sogar über mehrere Module (Buffer, MOb, Can).

2. Setzt eine sehr sorgfälltige Arbeit und vor allem auch eine tiefe
Einsicht in den erzeugten Code voraus. Siehe hier meinen anderen
Thread: http://www.mikrocontroller.net/forum/read-1-163691.html über
mögliche Probleme bei der Unterbrechung durch Interrupts. Da dieses
mein erstes großes MCU Projekt ist, kann ich fast sicher davon
ausgehen, irgendetwas wichtiges zu Übersehen. (z.B. die Problematik mit
einem simplen glob++ war mit bis vor kurzen noch nicht bewusst.)

Aber wie es im Moment aussieht, werde ich wohl keine andere Möglichkeit
haben, als in den saueren Apfel zu beissen. Um eine genaue Zeitstudie
der Interrupts werde ich wohl nicht herum kommen. Und wenn ich dann
schon dabei bin, sollte ich wohl die besonders schwer
'abzuzählenden'/abzuschätzenden Bereiche nach Main verschieben.

Eine Idee hatte ich gerade noch wärend des schreibens: In alten, nicht
preämtiven Multitaskingssystemen war es üblich, die Ausführberechtigung
an andere Threads explizit abzugeben. So könnte man in nicht
zeitkritische Bereiche der Interrupts ein sei();cli(); einbauen, quasi
als Sollbruchstelle, um anderen Interrupts explizit Stellen zu geben,
an denen sie die Ausführung unterbrechen können.
Ist aber nur eine spontane Idee, über die ich mal noch weiter
nachdenken muss.

Wegen der ATmega8 Idee: Über so etwas habe ich auch schon ernsthaft
nachgedacht. Es wird aber vermutlich zu teuer werden. Bzw.: Hast Du
eine Idee für eine kleine, günstige MCU mit twi (in 1000er
Stückzahlen)? Zwei twis wären natürlich am besten. Noch besser wäre es,
mein Zeitproblem in den Griff zu kriegen ;)

Auf jeden Fall schon mal Danke für die lange und erhellende Antwort.
Ich suche weiterhin noch nach ein wenig Literatur zum nachlesesn.

von Peter D. (peda)


Lesenswert?

'init( (*callback)())'

Damit kann ich nicht so richtig was anfangen, ich habe das
Programmieren mehr von der Hardwareseite her gelernt
(TTL...Z80-CPU...Assembler...C).

Ich hab mal in meinen CAN-Code geschaut. Wie es scheint, haben die
AVR-Leute das CAN einfach vom 8051 übernommen.

Ich habe im Prinzip 4 Funktionen:

Init
Interrupthandler
Senden
Empfangen

Das Senden schreibt eine Nachricht in den Sendepuffer und das Empfangen
holt eine Nachricht aus dem Empfangspuffer. Beide werden aber
ausschließlich vom main aufgerufen. Und sobald sie das CANPAGE ändern,
wird zuvor nur der CAN-Interrupt gesperrt.
Globale Interruptsperre würde ich generell versuchen zu vermeiden.

Da ich im Main keinerlei harte Warteschleifen habe, wird es auch sehr
schnell durchlaufen (wenige ms) und die CAN-Nachrichten schnell
bearbeitet. Z.B. geben selbst Funktionen, die auf den EEPROM schreiben
die Schreibzykluszeit (10ms) wieder an das Main ab.

Wie schnell muß denn das ping beantwortet werden ?


Am billigsten dürfte es sein, die I2C-Mastersoftware zu korrigieren.
Es ist ja wohl offensichtlich, daß da jemand großen Mist gebaut und
sich nicht an den I2C-Standard gehalten hat.

Aber der ATMega8 ist doch nicht sonderlich teuer im Vergleich zum Rest
der Schaltung.
Bzw. der ATMega48 soll ja noch billiger werden, wenn es ihn endlich
gibt.

Ansonsten kämen noch die Philips LPC900-Serie (nur VCC<=3,3V) oder
LPC700-Serie (nur OPT) in Betracht, die werden ja als Low-Cost
beworben. Da das AVR-TWI ja auch von den Philips-8051 abgekupfert ist,
dürfte die Programmierung leicht fallen.


Unterfunktionen sollte man in Interrupts vermeiden bzw. wenigstens ins
gleiche Object davor schreiben.
Damit weiß der Compiler dann, welche Register im Interrupt verwendet
werden und muß nicht komplett alle 32 Register sichern.

Ich sehe mir öfters mal das Assembler-Listing an, um zu sehen, ob mein
Konstrukt den Compiler zum Schwitzen bringt oder nicht.


Peter

von Florian Bantner (Gast)


Lesenswert?

Hallo Peter,

init( (*callback)() ); ist die Funktion Init mit einem Zeiger auf die
Funktion callback(void) als Übergabeparameter. Den Zeiger hebe ich mir
im CAN-Modul auf und kann somit Main signalisieren, dass neue Daten
vorhanden sind. Du wirst das ganze vermutlich direkt mit Polling lösen
(in der Art von while( ! byte = Empfangen() ); ). Du Möglichkeit mit
dem Callback hat den Vorteil größerer Flexibilität, natürlich auf
Kosten von mehr Last im Interrupt (den Funktionsaufruf). In main steht
dann sowas:

uint8_t _byte;

void _CALLBACK_can(){
  _byte = can_empfangen();

  //evtl. etwas damit tun oder auch nicht
};

main(){

  can_init( &_CALLBACK_can() );

  for( ever ){

    if( _byte ){
       // etwas machen
    }
  }
}

Oder eben anstelle von if( _byte ) in main() direkt in der Callback
eine Weiterverarbeitung und überhaupt nichts in main().

:Unterfunktionen: Sollte er ja auch bei weiteren Modulaufrufen nicht
müssen (theoretisch, nicht nachgeprüft), da die weitere Unterfunktion
ja selbst die Register sichert -- bzw. eben jede Unterfunktion sowieso
nur die Register sichern muss, die sie selbst benötigt, also auch
verschachtelt.

Die Master-Software kommt in großen Stückzahlen aus Japan. (Kann gerade
nicht mehr dazu sagen.) Auf jeden Fall unänderbar. Sollte ich
Gelegenheit haben jemanden dafür ans Kreuz zu nageln, werde ich sie
ergreifen. Ausser dem Irgnorieren von sclk kommt nämlich auch noch ein
elendes Timing dazu (100 bit in 3ms, dann 100ms Pause)

:ATmega8: bekähme ich wohl unter 1,6 / Stück. ATtiny26 f. 1,20, also
tiny45 wohl auch in der Gegend. 1200 Euro +/- ist schon noch ein
Betrag, für den man ein bischen Alternativen probieren kann. Wenn
nichts hilft, dann muss man die Kohle eben in die Hand nehmen.

:Ping: Antwortzeiten im ms-Bereich genügen auf jeden Fall. Von daher
wäre es auch kein (zumindest großes) Problem, das ganze nach Main zu
nehmen. Was hier für mich schwerer wiegt ist, dass ich es im Int schon
recht gut handlen kann (Ist tatsächlch kein Aufwand: einen Frame
empfangen, einen anderen zum senden freischalten, also im us Bereich)
und ich damit diesen sowieso eher Uninteressanten bereich von der
tatsächlichen Anwendung fern halte.

:ASM: Danke dass Du mich erinnerst ;) Werde ich wohl auch nicht drum
rum kommen, alleine schon um sicher zu gehen, was der Compiler wieder
anstellt. Zum Lesen reichen meine Assembler-Kenntnisse noch gerade so
aus. Werde ich aber solange vor mir herschieben, biss es nciht mehr
anders geht.

:Andere MCUs: Ich bin der Meinung, dass es besser für mich (und meine
Programme) ist, wenn ich mich mit wenigeren Teilen, dafür genauer
auskenne. Deswegen wirds wohl wieder ein avr werden. Aber mir kann ein
Blick über den Tellerrand sicher auch nicht schaden. Hat der
Philips-8051 auch diese Gehirntoten Adressregister für CAN?

Schoene Gruesse,

Florain

von Peter D. (peda)


Lesenswert?

Ich füchte mal, so ein Callback wird der Compiler nicht zurück verfolgen
können, er wird also im Interrupt mehr sichern als notwendig.

Und wenn der Callback mehr macht, als nur ein Flag zu setzen, dann
bedeutet das ja an beliebiger Stelle des Main, d.h. beliebig viele
Konfliktquellen.

Das schlimmste ist jedoch, daß er eine Unterfunktion des
Interrupthandlers ist, d.h er läuft auf Interruptlevel !!!

Damit ist eine vernünftige Analyse der Interruptzeit nun wirklich nicht
mehr möglich und andere Interrupts könnten bis ins Nirwana verzögert
werden.

Ich käme daher erst garnicht auf die Idee, sowas überhaupt zu
versuchen.

Ein Flag im Main zu testen dauert dagegen nichtmal 0,4µs (bei 16MHz).


Ich habs eben gerne, wenn die Programme klar verständlich strukturiert
sind und deshalb vermeide ich Funktionspointer soweit es geht.
Einem Funktionspointer sieht man nunmal nicht an, was er gerade (gutes
oder böses ?) im Schilde führt.

Funktionspointer verwende ich nur an 2 Stellen im Main, d.h. es gibt
keine Konflikte mit anderen Funktionen.
Einmal im Kommandointerpreter (UART) und einmal im Scheduler.
Obwohl so ein Scheduler schon ein bischen tricky ist, überall können ja
Funktionen in ihn hineingestellt werden.



"ATtiny26 f. 1,20, also tiny45 wohl auch in der Gegend."

Das wirds wohl nicht bringen. Du brauchst ja ein Hardware I2C, das in
0,nix alles abarbeiten kann, d.h. der alleinige Interrupt ist.
Dann brauchst Du ja noch die Kommunikation zu Deinem AT90CAN128, also
SPI oder UART, was diese Typen aber nicht haben.



"Hat der Philips-8051 auch diese Gehirntoten Adressregister für
CAN?"

Was meinst Du damit ?

Das CAN im Philips ist anders aufgebaut, es hat einen FIFO. Den gibts
auch als externen Controller (SJA1000). Ich verwende aber den 8051 von
Atmel (AT89C51CC03).


Peter

von Florian Bantner (Gast)


Lesenswert?

Hi Peter,

Evtl. übersehe ich ja etwas, aber soweit ich den erzeugten Code
verstehe, hat jede Funktion die aufgabe, genau die Register zu
sicheren, die sie selbst verändert. Also vom generellen Aufbau:

sub f1
   save Rn
   arbeit mit Rn
   restore Rn
endsub

Wobei Rn genau die Register sind, mit denen auch gearbeitet wird. Kommt
nun ein weiter Unterfunktions-Aufruf dazu, sollte es so aussehen:

sub f2
   save Rm
   arbeite mit Rm
   call f1
   restore Rm
endsub

Wobei auch hier nur die Register gesichert werden müssen, mit denen f2
arbeitet (Rm). Das sichern von Rn übernimmt dann ja f1.

Das sollte bei Interrupts exakt das selbe sein. Wenn ich Dich richtig
versetehe (tue ich das?), dann geht es Dir darum, dass die Funktion
noch weniger Register sichert, nämlich nur die, die zu ihrem
Aufrufzeitpunkt gerade in Gebrauch sind?

Diese Optimierung (sofern es sie gibt) verbietet sich aber
grundsätzlich bei Interrupts, da hier ja der Aufrufzeitpunkt gerade
eben nicht fest steht. Einzig eine Optimierung im Sinne von: Dieses
Register wird sonst nirgends benötigt, also muss ich es auch nicht
sichern, wäre denkbar -- würde aber auch nicht mit Modulen (.o)
funktionieren, da diese ja gerade unabhängig voneinander übersetzt
werden.

:Callbacks: Das die auf Interrupt-Level laufen (d.h. konkret das I-Flag
ist 0 und die letztendliche Rücksprungadresse führt 'irgendwo' nach
main) ist mir klar. Deswegen sage ich ja auch, mein Programm läuft im
Moment ausschlisslich in Interrupts.

Funktionsaufrufe in Interrupts halte ich generell nicht für verkehrt.
Aber eben mit bestimmten Voraussetzungen:
  1. You know what you're doing. D.h. ich kann genau sagen, wie lange
sie dauern. (Kann ich im Moment nicht, deswegen bei mir gerde
schlecht).
  2. Die Zeit dafür ist vorhanden.
  3. Ich muss sie nicht benutzen.

Vielleicht zur genaueren Erläuterung. Ich komme aus der
Anwendungs-Entwickler-Ecke wo es üblich ist, unter einem hohen
Abstraktionsgrad an bestimmte Aufgaben heranzugehen. (s. z.B.
"Entwurfsmuster", Gamma et. al.) Dabei wird zuerst ein relativ
allgemeines und wiederverwertbares Framework aufgebaut und darauf erst
die Anwendung 'aufgesetzt'. Leider lassen sich die meisten dieser
Techniken nicht 1:1 in C & auf MCUs verwiklichen. Was ich aber bisher
als Ansatz verfolgt habe ist folgender:
Als erstes Aufbau der verschiedenen Anwendungsschichten. Also erst die
Hardwareschicht, d.h. z.B. das Can-Modul. Dazu festlegen der
Schnittstellen. Hier also sowas wie init, send, receive, etc. und die
dazugehörigen Datenstrukturen also z.B. struct MOb( reg, address, data,
etc. );

Dabei ist jeder Hardware-Zugriff in ein Modul (.o) gekapselt und
erfüllt von sich aus noch überhaupt keine Funktionalität. Ähnlich
viellicht zur implementierung von stdio in der avr-libc, wo vor einem
printf(...) erst eine initialisierung mit fdevopen( &lcdPut,0,0)
erfolgen muss.

Die tatsächliche Anwendung kommt dann in den 2 nächst höheren
Schichten. Also (bei mir) z.B. in einem Modul "CanHandler", welches
das Can-Modul initialisiert und sich selbst als callback registriert.
Hier findet dann die Arbeit mit dem speziellen can-protokoll statt.
Auch dieses Modul hat wieder für spezielle Aufgaben die Möglichkeit,
Callbacks aufzurufen.

Letztendlich bleibt noch das Hauptprogramm (main.c) selber. Ausser der
Initialisierung der High-Level Module ist hier auch noch der
'Klebstoff', der die einzelnen Module zusammenschweisst.

Im Endeffekt läuft es wohl auf Delegation der einzelnen Aufgaben an das
zuständige Modul hinaus.

Wären nun meine Module alle 'Interrupt-Save', könnte ich mit diesem
Aufbau ohne weiteres fast alle Funktionalität zw. Interrupts-Ebene und
Main-Ebene ohne großen Aufwand hin & her schieben.

So, jetzt kommt das grosse ABER: Das ganze geht natürlich nur, wenn man
es sich auch leisten kann. D.h. wenn sich die gesammte Ausführungszeit
der Interrupt-Routinen nicht so sehr verlängert, dass dadurch Probleme
entstehen. Bei mir ist das wohl aber erst dadurch passiert, dass ich
alle Funktionalität direkt in den Interrupts habe.

Wegen des systematischen Aufbaus, kann ich es jetzt noch ohne
schwierigkeiten Ändern, muss mich dafür aber mit den Schwierigkeiten
auseinandersetzen, die entstehen, wenn man 'Interrupt-Save'
programmieren muss. (Was ich ja bisher hoffte zu vermeiden.)

:ATmega8: Würde ich so einsetzen, dass ich sozusagen das TWI damit
repariere. Also auf einer seite ein Software-Twi. Meine Bittime liegt
bei 3us, sollte also auch in Software hinzubekommen sein. Dies nimmt
das kaputte TWI vom Master an und buffert es. Die Kommunikation mit den
can128 übernimmt dann ein funktierendes TWI. Ist zwar zeitlich alles
knapp, sollte aber hinzubekommen sein. (Muss ich noch ausprobieren.)
Was ich ncoh nicht 100% verstanden habe ist, was das 'eingeschränkte'
TWI vom tiny45 macht und ob es sich überhaupt dafür eignet.

:Adressregister: Der can128 hat für can ein Adressregister, in dem die
Adresse (abhängig von A/B Protokoll) um 3 bzw. 5 bit verschoben
abgelegt wird. Das ist immer eine rießen Frickelei, gerade wenn man
auch noch vergleiche mit dynamischen Adressen machen muss. Warum das so
ist, wissen wohl nur die Entwickler. Gut dagegen finde ich den Aufbau
mit 15 unabhängigen Sende- / Empfangsbuffern.

:klar strukturiertes Programm: da sind wir uns einig, sind das große
Ziel und die Voraussetzung für einen einwandfreien Betrieb. Das
Vorgehen ist evtl. leicht unterschiedlich ;)

(Und ich habe mit Sicherheit nicht die reine Wahrheit gepachtet. Dafür
bin ich noch viel zu kurz mit MCUs unterwegs und muss erst noch
herausbekommen, welche Mechanismen sich von der
PC-Anwendungsentwicklung überhaupt übertragen lassen. Ich bin also
wirklich dankbar für gute Anregungen und ich fasse Deine als solche
auf.)

Schoene Gruesse,

Florian

von Michael (Gast)


Lesenswert?

Vermutlich wiederhole ich Einiges, was bereits gesagt wurde.

Ob es gute Literatur zum Thema gibt, wage ich zu bezweifeln. Die
praktischen Probleme sind zu speziell, als daß man sie allgemein
abhandeln könnte. Fachartikel in Zeitschriften erscheinen mir entweder
'geschwätzig' oder bestätigen meine Auffassung, daß man selber
praktische Erfahrungen erarbeiten muß.

Die Interrupts solltest Du nur dazu verwenden, entsprechende
Datenpuffer zu füllen/entleeren und ein Flag zu setzen, wenn eine
Weiterverarbeitung in main() (oder untergeordneten Routinen) möglich
ist. Alles andere wird in der Praxis unüberschaubar !

Neben dem Ansatz, vom Ganzen aufs Detail zu planen, solltest Du auch
die Details parallel dazu planen. Das sind die Interruptroutinen
selber. Wenn deren Funktion nicht von Anfang an sichergestellt ist,
wird Dir die ganze Planung nichts nützen.
Wenn das Timing der Interrupts klappt, kannst Du den Rest des
Programmes gestalten wie Du willst. Abhängig, wie geschickt man
programmiert, wird das Programm schneller oder langsamer arbeiten. Das
ist aber zweitrangig, auch ob Du 100Byte oder 2kB an Stack benötigst.

Zur Praxis.
An einer Stelle schreibst Du von 100 Bits in 3ms (30µs/Bit) dann aber
von 3µs/Bit. Die 30µs/Bit kann man 'bequem' per Interrupt einlesen,
wenn sichergestellt ist, daß andere Interrupts nur das Allernötigste
ausführen und dann <10µs dauern. Die INTx-Eingänge haben dazu
entsprechend hohe Priorität (Planung der Hardware).

Wenn 3µs/Bit erlaubt sind, würde ich beim AVR wie folgt vorgehen: jeder
Interrupt schaltet sein ENABLE-Bit am Anfang der Interruptroutine ab und
gibt mit SEI alle anderen sofort wieder frei. Abhängig vom Compiler (und
im Int aufgerufener Routinen) werden verschieden viele Register
gerettet, sodaß bis zum SEI µSekunden vergehen können. In diesem Fall
muß der Funktionskopf jeder Int-Routine in Assembler geschrieben
werden, damit die Register erst nach DISABLE_INT_BITn und SEI gerettet
werden.
Die Int-Routine, die alle 3µs aufgerufen wird, darf nicht 'tüddeln'.
Daher liest sie das komplette PINx-Byte ein und schreibt es in einen
Puffer, der entsprechend groß ist, und setzt ein Flag, wenn alle Bits
empfangen wurden. Das macht man am besten auch in Assembler.
Die Auswertung des betreffenden Bits in den Bytes, kann man dann in
Ruhe in main() erledigen.
MCUs mit DMA-Controller sind hier groß im Vorteil !

Sofern Variable im Interrupt verändert werden, muß jeder Zugriff
entsprchend geschützt werden. Nehmen wir einen 'long timer', der per
Int erhöht wird, aber auch gesetzt und gelesen werden soll. Schützen
kann man den Zugriff am besten, wenn nur das ENABLE-Bit gesperrt wird,
welches 'timer' betrifft.

long get_timer() // timer++ über z.B. hardware-timer1, compa-int
{
long temp;
  SPERRE_T1_COMPA;
  temp = timer;
  FREIGABE_T1_COMPA;
  return(temp);
}

Die Funktion braucht ein bißchen Platz und Zeit: was solls ! Fürs
Schreiben gilt das Gleiche.
Auch hier sind wieder die 'dickeren' MCUs im Vorteil, die intern
16/32 Bit Daten zulassen. Das Lesen/Schreiben von 16/32 Typen wird
nicht durch Ints unterbrochen und geht daher auch unproblematischer +
schneller.

von Florian Bantner (Gast)


Lesenswert?

Hallo Michael,

danke für Deine ausführliche Antwort.

Wegen der Literatur bin ich inzwischen darauf verfallen, mir
tatsächlich ein Buch zu Betriebssystemen zuzulegen. (Den Tannenbaum
wollte ich sowieso schon lange haben :) Mal sehen, ob ich da noch ein
bischen erhellende Erkenntnis beziehen kann. Grundsätzlich denke ich
schon dass es auch ein weites Feld allgemeiner Betrachtungen gibt, wie
man sich dem Problem nähern kann. Das auch durchaus über
"Kochrezepte" hinausgehend, auf eher mathematisch-abstrakter Ebene.
(Eigentlich wollte ich jetzt hier einen Link posten, aber leider finde
ich ihn gerade nicht)

Zum konkreten Problem: 30µs sind (natürlich) richtig. Da hatte ich wohl
wieder schneller getippt, als gedacht.

Meine ursprüngliche Idee war ja gewesen, mir das Problem mit den
Interrupts vom Halse zu halten, indem ich alles in ihnen
bewerkstellige. Aber was warscheinlich das wichtigste Stichwort in dem
Zusammenhang ist: Übersichtlichkeit.


::Anekdotische & theoretische Abschweifung::

Von der Anwendungsentwicklung kenne ich ein ähnliches Problem, nämlich
die Testbarkeit. Nachdem wir Unit-Tests eingeführt haben (Im Prinzip
Einzeltests von jeder Funktion) hat sich relativ schnell unser
Programmierstiel gändert, da jeder Seiteneffekt die Testbarkeit rapide
verschlechter hat. Das Ergebniss waren ganz klare und einfache
Funktionen mit einem einfachen und klar umrissenen Einsatzzweck.

Ähnlich würde ich jetzt gerade für die Interrupt-Programmierung als
Kriterum haben: Jeder Interrupt muss eine 1. klare Aufgabe erfüllen und
2. (in us, ms, takte etc.) genau angegeben werden können. Und natürlich
sollte er möglichst kurz sein. Aber das ergibt sich schon aus der den
ersten beiden Forderung, da jede Verlängerung oder verkomplizierung die
Angabe der genauen Ausführungszeit erschwert.

So wie ich es (theoretisch) im Moment sehe, gibt es 3 wichtige
Kenngrößen zu Interrupts:
  1. die maximale Ausführungszeit,
  2. die minimale erlaubte Latenz bis zum Aufruf bzw. bis zum
wieder-einschalten
  3. die Priorität.

Bezogen auf den AVR lässt sich damit schon einmal allgemein Sagen:
Liegt die Summe der (max.) Ausführungszeiten der höher priorisierten
Interrupts + der Ausführungszeit des betrachteten Interrupts unter
dessen minimal erlaubter Latenz, und gilt dies für alle Interrupts, hat
man keine Probleme.

Sonderbetrachtungen muss man erst anstellen, wenn man diese Bedingung
nicht mehr erfüllen kann. Ausnahmen wären z.B. länger dauernde
Interrupts am Ende einer Datenübertragung (wie bei mir z.B. nach den
3ms).

::Ende Abschweifungen::

Zum Glück habe ich mit 16Mhz und _30_µs/bit bzw. beim Twi dann sogar
8*30µs genug Zeit, es auch gemütlich in C zu realisieren.

Inzwischen bin ich ja auch überzeugt, dass die Idee mit 'alles im
Interrupt' keine besonders gute war. Jetzt werde ich noch ein
bischchen Abwarten, ob noch 'interessante' Vorschläge kommen, welche
Problemfälle bei Unterbrechung durch Interrupts auftreten können; und
dann werde ich wohl | übel in den Saueren Apfel beissen & noch einmal
meine Funktionen durchgehen, um sie 'save' zu bekommen. Die Umstellung
auf die Datenbuffer ist dann zum Glück kein großer Akt mehr.

Florian

PS: Wollte eben fragen, dann aber noch selbst etwas gelernt: Meine
Tastatur hat ein µ-Zeichen. Toll :)

von Florian Bantner (Gast)


Lesenswert?

Gerade aufgefallen, dass meine theoretische Betrachtung natürlich nicht
korrekt ist, da die höher prorisierten Interrupts den betrachteten auch
ohne weiteres ganz blockieren können, wenn sie häufiger auftreten.
Die Rate ist wohl da noch das entscheidende. Also:

4. Die Rate mit der sie auftreten.

Damit wirds dann auch schon etwas unübersichtlicher.

Wollt ich nur gesagt haben :)

Florian

von Peter D. (peda)


Lesenswert?

"...da die höher prorisierten Interrupts den betrachteten auch ohne
weiteres ganz blockieren können..."


Die AVRs haben leider keine Interruptprioritäten, d.h. jeder Interrupt
blockiert alle anderen !

Was im Datenblatt fälschlich als Priorität bezeichnet wird, ist nur die
Abarbeitungsreihenfolge.
D.h. wenn während eines Interrupts 2 andere getriggert werden, wird
nach Ende dieses Interrupts mit der Abarbeitungsreihenfolge
entschieden, welcher als nächstes behandelt wird.

Die Latenzzeit (worst case) eines Interrupts ergibt sich somit aus der
Laufzeit des längsten Interrupts + der Laufzeit aller Interrupts, die
in der Abarbeitungsreihenfolge davor stehen (unter der Annahme, daß
dabei kein Interrupt zweimal getriggert wird).


Damit dürfte klar sein, warum das knausern um jede µs in jedem
Interrupt essentiell ist.


Es besteht zwar die Möglichkeit, Interrupts in Interrupts freizugeben,
aber das birgt in der Regel mehr Konfliktpotential als Vorteile in
sich.
Es entsteht dabei auch keine Priorität, da die Freigabe ja global
wirkt.


Ich habe sowas einmal gemacht, um mit einem Timer (Interrupt alle 64
Zyklen) ein Analogsignal zu generieren.
Dazu mußte ich für den I2C-Interrupt eine Assemblerfunktion erstellen,
die nur den I2C-Interrupt sperrt und dann den eigentlichen Handler
aufruft (sieht scheußlich aus, läuft aber).



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.