% Content-encoding: UTF-8 % Dummy \documentclass[ngerman,a4wide]{article} \usepackage[utf8]{inputenc} \usepackage{multicol,babel} \setcounter{secnumdepth}{0} \setcounter{tocdepth}{0} \begin{document} \renewcommand{\figurename}{Tabelle} \title{Interrupts in \texttt{\textbf{am}}Forth} \ifx\shorttitle\undefined\else \shorttitle{Interrupts} \fi \author{Matthias Trute} \maketitle \begin{abstract} Bei Mikrocontrollern sind Interrupts ein wichtiger Teil der Laufzeitumgebung. Für jeden Interrupt wird eine spezialisierte Routine aufgerufen, die sich kurz und knapp mit ihm beschäftigt. Bei compilierten Sprachen ist es unkompliziert. Wenn die Interrupts jedoch im Interpreter abgewickelt werden sollen, steigt der Aufwand. \end{abstract} \begin{multicols}{2} \section{Anfänge} Die ersten Versionen von amforth hatten schlicht keine Unterstützung für Interrupts. Der erste Interrupt war derjenige für die serielle Schnittstelle und war direkt in Assembler programmiert. Weitere Interrupts waren nach diesem Muster einzubeziehen. Beispiele sind in dem Artikel von Michael und Adolf in VD2010--01 zu sehen. Sehr bald kam der Wunsch auf, die Interrupt--Routinen nicht in Assembler, sondern in Forth zu implementieren. Dazu wurde eine Möglichkeit geschaffen, mit der der innere Interpreter (amforth ist eine ITC--Implementierung) mit dem Interruptsystem des Controllers synchronisiert wurde. Das hat grundsätzlich funktioniert, Timerticks wurden sauber verarbeitet. Sehr bald traten die ersten Probleme zu Tage: Interrupts konnten verloren gehen und einige Interrupttypen ließen den Controller einfrieren, so dass nichts mehr ging. Es hat einige Zeit gedauert, bis die Ursachen geklärt waren und eine Idee entwickelt wurde, wie man dem beikommen kann. Wesentliche Anregungen kamen von Wojciech und Al, denen hier gedankt sei. \section{Ablauf} Die Atmegas kennen zwei Betriebszustände: Normalbetrieb und Interruptbetrieb. Wenn der Controller eine Interruptbedingung erkennt, unterbricht er das laufende Programm, schaltet in den Interruptbetrieb und springt zu einer im Programmspeicher hinterlegten Adresse. Die Interrupt--Routine bearbeitet den Interrupt und sorgt zusätzlich dafür, dass sich nichts am Prozessorzustand (Flags, Register) ändert. Den Abschluss der Interruptverarbeitung bildet das Zurückschalten in den Normalbetrieb und der Rücksprung zum unterbrochenen Programm. Eine Interruptroutine in (ITC--) Forth erfordert, dass der/ein innerer Interpreter verfügbar ist, der mit Interrupts umgehen kann. Um die Interrupts von der Controller--Ebene in die Forth--(VM)--Ebene zu transferieren wird ein ansonsten ungenutztes Flag im Controller--Status--Register genutzt. Dieses Flag wird von keinem Assemblerbefehl beeinflusst, außer dem SET--Befehl natürlich. Damit ist der erste Schritt klar: Sobald der Controller einen Interrupt bearbeitet, wird dieses, T genannte, Flag gesetzt und die Nummer des verursachenden Interrupts gespeichert. Dann wird erst mal mit dem normalen Programm weitergemacht. Irgendwann ist dann der Interpreter wieder an der Reihe und erkennt das T--Flag. Die Forth--VM ist zu diesem Zeitpunkt in einem wohlbekannten Zustand. Das ermöglicht es, nicht mit dem nächsten Wort aus dem Standardprogramm weiterzumachen, sondern ein ganz anderes Wort einzuschieben: Die Interrupt--Routine. Anhand der gespeicherten Interruptnummer wird aus einer Tabelle das passende Execution Token ausgelesen und dieses aufgerufen. Nach Abschluss dieses Wortes ist automatisch dafür gesorgt, dass das unterbrochene Programm weitergeht. Da die einzige kritische Ressource der Forth--VM die beiden Stacks sind, muss die Forth--Interruptroutine lediglich vom Stackeffekt her leer sein. Die CPU--Register werden automatisch verwaltet. Mit diesem Ansatz laufen einfache Aufgaben, wie ein Time--Ticker, zufriedenstellend. Zu Problemen kommt es jedoch, wenn in der Zeitspanne zwischen dem ersten Interrupt und der Aktivierung der zugehörigen ISR ein zweiter Interrupt auftritt. Dann geht der erste schlicht verloren und wird durch den zweiten ersetzt. Eine Lösung hierfür wäre eine Queue, die dann im inneren Interpreter abgearbeitet wird. Das zweite Problem ist, dass einige Interruptquellen seitens der ISR bereinigt werden müssen. In der Regel erfolgt dies durch Auslesen eines Registers oder Löschen eines Flags. Erfolgt dies nicht, wird der Controller beim Wechsel in den Normalbetrieb den gleichen Interrupt erneut auslösen. Für den Nutzer friert das System ein\dots \section{Umsetzung} Interessanterweise lassen sich beide Probleme mit einer einzigen Maßnahme lösen: Der Controller wechselt erst dann vom Interruptbetrieb in den Normalbetrieb, wenn auch die Forth--ISR erledigt ist. Die Codeänderungen sind minimal. Die Auswirkungen jedoch weit reichend. Zum ersten Problem: Solange der Controller im Interruptbetrieb arbeitet, unterdrückt er alle weiteren Interrupts. Diese werden jedoch nachgereicht, sobald der Normalbetrieb wieder aktiviert wird. Damit gehen keine Interrupts mehr verloren. Das wird seitens des Herstellers garantiert. Das zweite Problem kann durch die entsprechenden Instruktionen in der Forth--ISR gelöst werden. Da muss der Programmierer wissen, was er machen muss. Die Lösung ist natürlich nicht nebenwirkungsfrei. Zum einen verbleibt der Controller verhältnismäßig lange im Interruptbetrieb. Das erhöht die Latenz für andere. Desweiteren wird ein Teil des Codes der Primitiva im Normalbetrieb und ein Teil im Interruptbetrieb ausgeführt. Das ist für die bestehenden Worte kein Problem, es muss aber bei selbst geschriebenen zumindest geprüft werden. Kritische Routinen wie das EEPROM/Flash--Schreiben sind gegen störende Interrupts geschützt. Ein weiterer Effekt tritt bei lang laufenden Assemblerroutinen, wie der Division oder dem Busy--Wait Loop in \texttt{1ms}, auf. Hier fängt die Forth--ISR erst an, wenn das Wort beendet ist\footnote{Der Loop in \texttt{1ms} wird übrigens auf das gesetzte T--Flag überwacht. Wenn das "`plötzlich"' gesetzt sein sollte, wird der Loop vorzeitig beendet. Bei der Division gibt es leider kein derartiges Ausstiegsszenario.}. Glücklicherweise sind die meisten Assemblerworte sehr schnell. \section{Beispiel} Wie sieht so eine Interuptroutine nun aus? \begin{quote} \begin{small} \listinginput[1]{1}{2011-02/Interrupt-1.fs} \end{small} \end{quote} Das Wort \texttt{timer-isr} ist das Wort, das im Interruptmodus ausgeführt wird. Aus diesem Grund ist es auch sehr kurz. Hier ist auch die Stelle, an der jegliche Interrupt--Gründe bereinigt werden müssen. Für den Timerinterrupt ist dies jedoch nicht notwendig. Die anderen Worte dienen dazu, das Timermodul im Controller zu initialisieren und den Interrupt zu (de--) aktivieren. Wie wird das genutzt? \begin{quote} \begin{small} \listinginput[1]{1}{2011-02/Interrupt-2.fs} \end{small} \end{quote} \section{Und sonst?} Controller--Kenner werden wissen, dass es eine Art globalen Interrupt--Schalter gibt. Der ist schon für den Kommandoprompt immer eingeschaltet. Es empfiehlt sich nicht, ihn abzuschalten. Ach ja: Die beste Möglichkeit, Interrupts zu bedienen, ist immer eine kurze Assemblerroutine. Michael und Adolfs Artikel sei zur Nachahmung empfohlen. \end{multicols} \end{document}