% 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}
