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
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.
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.
"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
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.
@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.
@Florian: Es gibt diverse freie RTOS. Konkret verwendet habe ich AvrX, interessant finde ich beispielsweise auch FreeRTOS und XMK.
"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
@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.
'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
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
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
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
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.
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 :)
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
"...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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.