Terminal-Kodes

Wozu braucht man das?

Was ist ein Terminal?

Ein Terminal besteht aus Tastatur und Bildschirm sowie einer seriellen Schnittstelle. Tastendrücke gehen zur seriellen Schnittstelle hinaus, Zeichen von außen landen auf dem Bildschirm. Zumindest grundsätzlich.

Es sieht also so ähnlich aus wie ein Computer, ist es aber nicht. (In aller Regel befindet sich darin ein fest programmierter Computer; man kann darauf kein beliebiges Programm ausführen.)

Die Erfindung reicht in die Computersteinzeit zurück (die ich nicht erlebt habe), als man mit dem Mainframe über ein Modem (ja, genau so wie gestern fürs Internet) und über eine Telefonleitung als einer von 'zig Benutzern (etwas von der teuren) Rechenzeit angeknabbert hatte.

Terminals gab es mit Druckerschnittstelle (zum Protokollieren, ein Diskettenlaufwerk gab es nicht) und später auch mit Farbbildschirm. Stets präsentieren sie sich mit einem Zeichenraster aus 80 Spalten und 24 Zeilen (nicht 25 wie unter DOS) und weißer Schrift auf schwarzem Grund, neuere auch umgekehrt. Sie sind so gut wie nie vollgrafikfähig.

Heutzutage sind sie allesamt museumsreif, denn die sog. Terminal-Emulationen sind viel verbreiteter und auch einfacher zu handhaben. Als Vorteil von echten Terminals kann man aber herausstellen, dass sie absolut idiotensicher sind, weil es keine Vireninfektionsgefahr gibt.

Übrigens, jeder TELNET-Client ist gleichzeitig eine Terminal-Emulation; die Funktion der seriellen Schnittstelle nimmt nun der TCP/IP-Socket ein (und ist viel schneller).

Wozu heute noch?

Mainframes sind so gut wie ausgestorben. Terminal-Emulatoren nicht. In der Ära vor dem Internet brauchte man sie zur Bedienung von sog. Mailboxen (der Name irritiert, sie sind eher mit FTP-Servern vergleichbar). Aber auch diese sind so gut wie tot.

Sehr gerne benutzt man einen Terminal-Emulator für Mikrocontroller-Projekte zum Anzeigen interner Werte und (wenn man weiß wie) zu ihrer Fernbedienung. Im Folgenden liegt daher das Schwergewicht auf Mikrocontroller. Sie sollten groß und komfortabel genug sein für C-Programmierung, denn in Assembler artet eine interaktive serielle Schnittstelle in richtig viel Arbeit aus.

Richtig ausprogrammiert kann man eine Mikrocontroller-Lösung basteln, die zur Diagnose und Wartung nur die serielle Schnittstelle braucht; kein Handbuch, weil der Seitenaufbau komfortabel und übersichtlich ist. Setup-Werte können damit ähnlich einem BIOS-Setup eingesehen und verändert werden, ohne den Mikrocontroller neu zu brennen oder ein spezielles Kommunikationsprogramm (für welches Betriebssystem?) zu schreiben.

Ähnlich verhalten sich auch Laserdrucker der gehobenen Preisklasse, die über ihren Ethernet-Anschluss ein TELNET-Port anbieten, mit dem man alle Setup-Werte viel klarer einstellen kann als mit dem Mini-Display und den wenigen Tasten am Drucker. (Noch schöner ist es mit einem HTML-Interface.)

Die Arbeit mit Terminals ist nichts für Leute, die ohne Maus nicht auskommen. Im Zweifelsfall prüfen Sie sich bitte im Selbstversuch und ziehen die Maus vom PC ab und legen los. Wenn Sie 10 Minuten durchhalten, können wir weitermachen.

Terminals, Emulatoren und Emulationen

„Dumme“ Terminals

Vor dem Bildschirmterminal gab es das Schreibmaschinenterminal: Tastenanschläge gingen zum seriellen Port, und Daten vom seriellen Port wurden ausgedruckt. Eine Bimmel (für den Kode 07h) gab es damals schon. (Für das Echo bspw. bei der Zeicheneingabe muss der Server sorgen.)

Die ersten Bildschirmterminals sparten Papier, aber verhielten sich genau so: Zeichenausgabe nur von links nach rechts und von oben nach unten sowie „Rollbetrieb“, was oben herausrollte, war weg. Man nennt solche Terminals „dumm“ (“dumb terminal”).

Die meisten Unix-Kommandos arbeiten mit „dummen“ Terminals, denn die Ausgaben lassen sich hervorragend weiterverarbeiten (filtern) und problemlos ausdrucken.

Es ist aber offensichtlich, dass man so keinen vernünftigen Text-Editor realisieren kann. Dazu bedarf es Steuerkommandos, die auf einen Bildschirm zugeschnitten sind.

„Richtige“ Terminals

Offensichtlich benötigt man für das Beispiel Text-Editor in etwa folgende Steuersequenzen: Ohne Zeilen löschen oder einfügen zu können würde der Bildaufbau beim sonst fälligen (fast) kompletten Neuzeichnen unerträglich lange dauern.

Weitere nützliche Steuersequenzen sind:

Zum Verdruss der Programmierer hat aber jeder Mainframe-Hersteller sein eigenes Süppchen gekocht. Mal sind die Terminals verschieden fähig, und auch bei gleichen Fähigkeiten gibt es völlig verschiedene Steuerkodes.

Zwei Terminal-Systeme sind jedoch (zumindest in ihrer Emulation) vergleichsweise weit verbreitet:

Ihre Steuerkodes sind sogar einander ähnlich, sodass für ein Mikrocontroller-Projekt nur diese beiden Steuersätze interessant sind.

Erwähnenswert ist, dass DOS mit geladenem ANSI-Treiber und umgelenkten Ein- und Ausgabeströmen einem „richtigem“ ANSI-Terminal recht nahe kommt, jedoch die Tastaturunterstützung der Sondertasten ist komplett verkehrt.

Terminal-Emulatoren

M

an braucht kein echtes Terminal zur Kommunikation mit seinem Mikrocontroller. (Obwohl das natürlich geht!) Dazu gibt es Terminal-Emulatoren. In der Regel können sie verschiedene (reale) Terminals emulieren.

Sowohl erstere als auch letztere verhalten sich unterschiedlich, und so ist die Komplexität quadratisch! Beispielsweise kann ein zeichenbasierter (DOS-)Emulator auch im VT100-Modus keine doppelt hohen oder breiten Zeichen, die VGA-Karte kann das nicht. Manche Steuerkodes werden von dem einen oder anderen Programm nicht oder nur in einem Modus verstanden. Auto-detect-fähige Programme wiederum erwarten bestimmte Sequenzen, bevor sie sich auf einen Modus festlegen.

Die „feinen Unterschiede“ herauszufinden und ein Rezept für folgende Mikrocontroller-Programmierungen zu erarbeiten ist Hauptanliegen dieser Arbeit.

Aus Aufwandsgründen wurde die Zahl der getesteten Terminal-Emulatoren ziemlich eingeschränkt durch die Auswahl von:

Als Emulationen kamen nur VT100 und ANSI in Frage. Das Windows-3.x-Programm unterstützt kein ANSI, während HyperTerminal ein Auto-Detect anbietet.

Beiläufig erwähnt werden muss, dass es sich bei diesen Programmen allesamt um wahre Krücken handelt, die Exaktheit aller Emulationen lässt sehr zu wünschen übrig. Nur der allgemeinen Verfügbarkeit wegen wurden sie ausgewählt; ein extra Programm herunterladen zu müssen sollte ja gerade vermieden werden.

Ab Windows Vista gibt es keinen Terminal-Emulator vorinstalliert. Da man ohnehin etwas herunterladen muss, kann man gleich zum besten greifen: PuTTY. Es kann sicherlich alles. Endlich auch UTF-8. Sogar Mausbedienung! Vielleicht sogar Vollgrafik. Wurde hier aber nicht getestet.

Übersicht der Steuerkommandos VT100 und ANSI

Was ist überhaupt drin?

Bevor es zu den Steuerkommandos geht, erst mal die Grenzen von VT100 und ANSI (in den untersuchten Emulatoren):
HyperTerminal-VT100
HyperTerminal im VT100- und VT100J-Modus.   Die Maus zeigt die Sequenzen.
ESC # 3 (obere Hälfte) ESC # 4 (untere Hälfte) ESC # 6 ESC # 5 CR LF ESC # 3 M CR LF ESC # 4 SPC M ESC ( 0 `abcdefghijklmnopqrstuvwxyz{|}~ ESC ( B  (nur VT100) ESC [ 0 m ESC [ 1 m ESC [ 4 m ESC [ 22 m ESC [ 7 m ESC [ 8 m ESC [ 31 m 7-bit-Umlaut-Problem bei VT100 ESC [ 1 K ESC [ 1 m ESC 7 ESC 3 ; 40 H ESC 0 m ESC 8
HyperTerminal-ANSI
HyperTerminal im ANSI- und ANSIW-Modus.   Die Maus zeigt die Sequenzen.
ESC [ 0 m ESC [ 1 m ESC [ 4 m ESC [ 5 m ESC [ 7 m ESC [ 7 ; 1m ESC [ 30 m (schwarzer Adler auf schwarzem Grund) ESC [ 30 ; 1 m ESC [ 31 m ESC [ 31 ; 1 m ESC [ 32 m ESC [ 32 ; 1 m ESC [ 33 m ESC [ 33 ; 1 m ESC [ 34 m ESC [ 34 ; 1 m ESC [ 35 m ESC [ 35 ; 1 m ESC [ 36 m ESC [ 36 ; 1 m ESC [ 37 m ESC [ 37 ; 1 m ESC [ 43 ; 30 m ESC [ 44 ; 33 ; 1 m ESC [ 7 m ESC [ 27 m CR LF ESC [ K ESC [ 7 ; 1 m ESC [ 30 m ESC [ 30 ; 1 m ESC [ 32 m ESC [ 32 ; 1 m ESC [ 34 m ESC [ 34 ; 1 m ESC [ 36 m ESC [ 36 ; 1 m ESC [ 31 m ESC [ 31 ; 1 m ESC [ 33 m ESC [ 33 ; 1 m ESC [ 35 m ESC [ 35 ; 1 m ESC [ 37 m (grauer Adler auf grauem Grund) ESC [ 37 ; 1 m CR LF ESC [ K

Der Kode ESC ist hexadezimal 1Bh, dezimal 27, als Control-Code ^[ geschrieben und kann auch über die Esc-Taste direkt eingegeben werden.

Weitere Eingabemöglichkeiten bestehen über die Tastenkombination Ctrl+[ (auf der deutschen Tastatur Strg+Ü, weil dort die eckige Klammer der amerikanischen Tastatur liegt - und deshalb diese Control-Schreibweise), sowie über den numerischen Tastenblock durch Festhalten der linken Alt-Taste, Drücken der Tasten 2 und 7 und Loslassen der Alt-Taste. (Das sind eigentlich Binsenweisheiten, die es am PC schon seit 20 Jahren gibt, aber kaum jemand weiß das heute noch!)

Die Groß- und Kleinschreibung ist bei den Steuersequenzen unbedingt einzuhalten! Numerische Parameter sind stets dezimal.

An Stelle von „ESC [“ kann bei einer 8-bit-Verbindung auch ein „ESC“ mit gesetztem Bit 7 ( CSI = 9Bh, 155) übertragen werden; die getesteten Emulatoren interpretieren solche Kodes jedoch als Zeichen.

Die wirklich wichtigen und nützlichen Kommandos sind fett hervorgehoben. Es bedeuten:

3.2 Abfragen

BefehlKodeAntwort *Emul:HTW3NCBemerkung
IdentifizierungESC [ c
ESC Z
ESC [ ? 1 ; x cVT100
ANSI
OK
nein
OK
-
OK
nein
Auto-Detect zu VT100J
x=2 unter HT und W3, x=0 unter NC
ESC [ > cVT100
ANSI
OK
nein
nein
-
nein
nein
Anwesenheit testenESC [ 5 nESC [ ? 0 nVT100
ANSI
OK
OK
OK
-
nein
nein
Auch im Auto-Detect-Modus
Kursorposition abfragenESC [ 6 nESC [ y ; x RVT100
ANSI
OK
OK
OK
-
nein
OK
(x:y) = Kursorposition, (1:1) = linke obere Ecke
Kennung ???ESC [ xESC [2;1;1;112;112;1;0xVT100
ANSI
OK
nein
nein
-
nein
nein
Auto-Detect zu VT100J
Drucker bereit?ESC [ ? 15 nESC [ ? 10 nVT100
ANSI
nein
nein
nein
-
nein
nein
Offenbar nirgends Drucker-Unterstützung
Benutzerdefinierte Tasten gesperrt?ESC [ ? 25 nESC [ ? 20 nVT100
ANSI
OK
OK
nein
-
nein
nein
Auch im Auto-Detect-Modus. Antwort: »Tasten nicht gesperrt«
TastaturspracheESC [ ? 26 nESC [ ? 27 ; 1 nVT100
ANSI
OK
OK
nein
-
nein
nein
Auch im Auto-Detect-Modus. Antwort: »US-Tastatur«
HyperTerminal sendet nach jeder Antwort einige Bytes Schrott!

Schalter

BefehlKodeEmul:HTW3NCBemerkung
Tastatur-SperreESC [ 2 hVT100
ANSI
OK
nein
nein
-
nein
nein
Bei Sperre werden Tastendrücke ohne Pieps ignoriert
Tastatur freigegeben *ESC [ 2 l
Einfüge-ModusESC [ 4 hVT100
ANSI
OK
OK
OK
-
OK
nein
Bei Zeichenausgabe rutscht Rest der Zeile nach rechts, aber nie in nächste Zeile
Überschreib-Modus *ESC [ 4 l
Zeilenvorschub (0Ah) bei Zeilenende (0Dh)ESC [ 20 hVT100
ANSI
OK
nein
nein
-
nein*4
nein*4
= Schalter »Beim Empfang Zeilenvorschub am Zeilenende anhängen«
kein Zeilenvorschub anhängen *ESC [ 20 l
Kursortasten im Application-Modus *²ESC [ ? 1 hVT100
ANSI
OK
nein
OK
-
nein
nein
Siehe unten
Kursortasten im Kursor-Modus *ESC [ ? 1 l
VT100-Modus *ESC <VT100
ANSI
OK
nein
OK
-
nein
nein
Wirkung insbesondere auf Kursortasten
VT52-ModusESC [ ? 2 l
132 SpaltenESC [ ? 3 hVT100
ANSI
OK
nein
nein*4
-
nein*5
nein
80 Spalten *ESC [ ? 3 l
Sanftes (langsames) RollenESC [ ? 4 hVT100
ANSI
nein
nein
nein
-
nein
nein
Springendes (schnelles) Rollen *ESC [ ? 4 l
Schrift schwarz auf weiß *8ESC [ ? 5 hVT100
ANSI
OK
nein
nein
-
ja*6
nein
Betrifft ganzen Bildschirm. Häufig genutzt als »sichtbare Klingel«.
Bug? Die oberen zwei Zeilen bei HyperTerminal bleiben unbeeinflusst
Schrift weiß auf schwarz *ESC [ ? 5 l
Relative Kursor-Platzierung *7ESC [ ? 6 hVT100
ANSI
OK
nein
OK
-
OK
nein
Kursor springt auf (1:1) = linke obere Ecke (absolut) oder linke obere Rollbereichs-Ecke (relativ)
Absolute Kursor-Platzierung *ESC [ ? 6 l
Umbruch: Bei voller Zeile rutscht der Kursor auf nächste Zeile *ESC [ ? 7 hVT100
ANSI
OK
OK
OK
-
nein!
OK
Die Standardeinstellung gibt alle Zeichen einer zu langen Zeile
auf der rechten Position übereinander aus
wie eine dumme Schreibmaschine
kein Umbruch: Bei voller Zeile bleibt Kursor rechts stehen *³ESC [ ? 7 l
Automatische Tastenwiederholung *ESC [ ? 8 hVT100
ANSI
nein
nein
nein
-
nein
nein
keine TastenwiederholungESC [ ? 8 l
Bildschirm im Zeilensprungverfahren (Interlaced)ESC [ ? 9 hVT100
ANSI
nein
nein
nein
-
nein
nein
Unsinn bei Windows-Emulatoren
kein Zeilensprungverfahren *ESC [ ? 9 l
Kursor sichtbar *ESC [ ? 25 hVT100
ANSI
OK
nein
nein
-
nein
nein*5
Vermisste Funktion!
Kursor unsichtbarESC [ ? 25 l
Grafik-Option(?)ESC 1VT100
ANSI
nein
nein
nein
-
nein
nein
normaler Betrieb *ESC 2
Num. Tastenblock im Application-ModusESC =VT100
ANSI
OK
nein
OK
-
nein
nein
Siehe Numerischer Tastenblock
Num. Tastenblock im Numeric-Modus *ESC >
Zeichensatz G0 - VorwahlESC ( xVT100
ANSI
OK*9
nein
OK**
-
OK
nein
x: A = britisch (#=£), B = amerik. *, 0 od. 2 = grafisch
Siehe unten
Zeichensatz G1 - VorwahlESC ) x
Zeichensatz G0 nehmen *SI (=0Fh, 15, ^O)VT100
ANSI
OK*9
nein
OK
-
OK
nein
Wer hat sich bloß diesen Müll ausgedacht?
Siehe Grafik
Zeichensatz G1 nehmenSO (=0Eh, 14, ^N)
Standardeinstellung (bei den übrigen Emulatoren)
Standardeinstellung in HT im VT100J-Modus
Standardeinstellung in HT
*4 Obwohl Schalter im Menü oder Konfigurationsdialog verfügbar
*5 Sichtbare Steuerzeichenausgabe
*6 Lässt sich nicht zurückstellen, Wirkung nur auf neue Zeichen (Bug!)
*7 Standardeinstellung in NC (nur relevant im VT100-Modus)
*8 Standardeinstellung in HT und W3
*9 Nicht im VT100J-Modus! Nur im VT100-Modus. (VT100J ist der 8-bit-Modus und braucht keine Zeichensatzumschaltung)
** Keine Funktion der Amerikanisch/Britisch-Umschaltung; diese ist im Konfigurationsdialog festgelegt

Steuern und Löschen

BefehlKodeEmul:HTW3NCBemerkung
Terminal rücksetzenESC cVT100
ANSI
OK
nein
OK*
-
nein
nein
Auto-Detect zu VT100J
Zeile doppelt groß und obere Hälfte darstellenESC # 3VT100
ANSI
OK
nein
OK*²
-
nein
nein*³
Das Programm sollte die gleichen Buchstaben ausgeben!
Zeile doppelt groß und untere Hälfte darstellenESC # 4
Zeile normalESC # 5Voreinstellung
Zeile doppelt breitESC # 6Doppelt groß ist stets auch doppelt breit
Die 40 (66) ersten Zeichen sind sichtbar, die anderen fallen heraus
Bildschirm mit "E"s füllen (Bildschirm-Test)ESC # 8VT100
ANSI
OK
nein
OK
-
nein
nein
ESC-Sequenz abbrechenCAN (=18h, 24, ^X)
SUB (=1Ah, 26, ^Z)
VT100
ANSI
?
?
?
-
OK
OK
ungetestet
Piep ausgebenBEL (=07h, 7, ^G)VT100
ANSI
OK
OK
OK
-
OK
OK
Ton ist oft abschaltbar
Tastatur-Leuchtdioden schaltenESC [ x qVT100
ANSI
nein
nein
nein
-
nein
nein
x = Nummer der Leuchtdiode, 0 = aus
Einfügen von Zeilen, Runter-RollenESC [ n LVT100
ANSI
OK
OK
OK*4
-
nein
nein
n=0 oder weggelassen = eine Zeile
Es wird nach bzw. von unten gerollt
Löschen von Zeilen, Hoch-RollenESC [ n MVT100
ANSI
OK
OK
nein
-
nein
nein
Löschen von ZeichenESC [ n PVT100
ANSI
OK
OK
OK
-
OK
nein
n=0 oder weggelassen = ein Zeichen
Zeichen rutschen von rechts nach
Löschen vom Kursor zum ZeilenendeESC [ KVT100
ANSI
OK
OK
OK
-
OK
OK
Kursorposition bleibt unverändert.
Das Zeichen unter dem Kursor wird stets mit gelöscht
Löschen vom Zeilenanfang zum KursorESC [ 1 K
Löschen der Zeile, die Kursor enthältESC [ 2 KVT100
ANSI
OK
OK
OK
-
OK
OK*4
Löschen vom Kursor nach rechts und bis zum BildendeESC [ JVT100
ANSI
OK
OK
OK
-
OK
OK
Löschen vom Bildanfang und von links bis zum KursorESC [ 1 J
Löschen des BildschirmsESC [ 2 JVT100OKOK*5OK*5
ANSIOK-OKKursorposition danach (1:1), LF = wie LineFeed
FF (=0Ch, 12, ^L)VT100
ANSI
LF
OK
LF
-
OK
OK
Bildschirm wird gelöscht
Fehldarstellung: Die darunterliegende Zeile wird auch bemalt
Sichtbare Steuerzeichenausgabe
*4 Kursorposition danach links
*5 Kursorposition danach (1:1)

Kursorsteuerung

BefehlKodeEmul:HTW3NCBemerkung
Position und Attribute speichernESC 7VT100
ANSI
OK
OK
OK*
-
OK
nein
ESC [ sVT100
ANSI
OK
OK
nein
-
nein
OK*
Nur diese Kombination geht mit DOS' ANSI.SYS
Position und Attribute wiederherstellenESC 8VT100
ANSI
OK
OK
OK*
-
OK
nein
ESC [ uVT100
ANSI
OK
OK
nein
-
nein
OK*
Nur diese Kombination geht mit DOS' ANSI.SYS
Roll-Bereich setzen (immer volle Breite!)ESC [ y1 ; y2 rVT100
ANSI
OK
OK
OK
-
OK
nein
y1, y2 = Roll-Bereich (Standard 1,24), setzt Kursor auf (1:1) (linke obere Ecke oder Roll-Ecke je nach Relativ-Modus)
Roll-Bereich löschenESC [ rVT100
ANSI
OK
OK
OK
-
nein
nein
Kursor positionierenESC [ y ; x H
ESC [ y ; x f
VT100
ANSI
OK
OK
OK
-
OK
OK
Linke obere Ecke (bzw. Roll-Rand im Relativ-Modus) ist (1:1)
Weglassen von x führt manchmal zu (1:y)
Kursor nach links obenESC [ H
ESC [ f
Erforderlich nach VT100' ESC [ 2 J
Tabulator an Kursorspalte setzenESC HVT100
ANSI
OK
nein
OK
-
OK
nein
Standardmäßig ist alle 8 Spalten ein Tabulator.
Tabulator an Kursorspalte löschenESC [ g
Alle Tabulatoren löschenESC [ 3 gAuch alle Standard-Tabulatoren werden gelöscht!
Kursor y Zeilen hochESC [ y AVT100
ANSI
OK
OK
OK
-
OK
OK
Niemals rollen.
Wenn x/y = 0 ist oder weggelassen wird, wird dennoch um eine Zeile/Spalte bewegt.
Beim Erreichen von Bildschirmrändern bleibt der Kursor dort stehen.
Kursor y Zeilen runterESC [ y B
Kursor x Spalten rechtsESC [ x C
Kursor x Spalten linksESC [ x D
Kursor zum ZeilenanfangCR (=0Dh, 13, ^M)Text bleibt stehen! Ein Terminal-Programm kann automatisch Zeilenvorschub (LF) ausführen.
Kursor 1 Zeichen nach linksBS (=08h, 8, ^H)Text bleibt stehen - Nur zur Erinnerung!
Bei TAB ohne Tabulator springt der Kursor ans Zeilenende wie bei einer dummen Schreibmaschine, bei NC ist TAB dann wirkungslos.
Kursor zum nächsten TabstoppHT (=09h, 9, ^I)
Nächste Zeile, Hoch-Rollen beim Erreichen des Roll-RandesLF (=0Ah, 10, ^J)
VT (=0Bh, 11, ^K)
Kursor-Spalte unverändert; ein Terminal-Programm kann automatisch Wagenrücklauf (CR) ausführen.
ESC DVT100
ANSI
OK
OK
OK
-
OK
nein
Kursor-Spalte unverändert
ESC EKursor-Spalte danach stets = 1
Vorherige Zeile, Runter-Rollen beim Erreichen des Roll-RandesESC M
Speichert Attribute nicht

Attribute

ModusKodeEmul:HTW3NCBemerkung
Zeichenattribute setzenESC [ x ; y ; z msiehe untenx,y,z sind (beliebig viele) Parameter, siehe nachfolgend.
Auto-Detect zu ANSIW bei >=30
alles AUSESC [ 0 m
ESC [ m
VT100
ANSI
OK
OK
OK
-
OK
OK
stellt hellgraue Schrift auf schwarzem Grund ein
fett EINESC [ 1 mVT100: fette Schrift
ANSI: hellere (bspw. weiße) Schrift
unterstrichen EINESC [ 4 mNC: dunkelblaue Schrift wie ESC [ 34 m
Blinken EINESC [ 5 mW3: Fettschrift wie ESC [ 1 m
Invers EINESC [ 7 mVertauscht Vorder- und Hintergrundfarbe, vergisst Intensität
unsichtbarESC [ 8 mVT100
ANSI
OK
OK
nein
-
nein
nein
Vordergrundfarbe = Hintergrundfarbe, vergisst Intensität
fett AUSESC [ 22 mVT100
ANSI
OK
OK
nein
-
OK
OK
ANSI: dunkle Schrift
unterstrichen AUSESC [ 24 m
Blinken AUSESC [ 25 m
Invers AUSESC [ 27 m
sichtbarESC [ 28 mVT100
ANSI
OK
OK
nein
-
nein
nein
TextfarbeESC [ 30 mVT100
ANSI
nein
OK
nein
-
OK
OK
Vordergrund schwarz / dunkelgrau
ESC [ 31 mVordergrund dunkelrot / rot
ESC [ 32 mVordergrund dunkelgrün / grün
ESC [ 33 mVordergrund braun / gelb
ESC [ 34 mVordergrund dunkelblau / blau
ESC [ 35 mVordergrund blaurot / hell-blaurot
ESC [ 36 mVordergrund zyan / hellzyan
ESC [ 37 mVordergrund hellgrau* / weiß
Hintergrundfarbe
der Zeichenzelle
ESC [ 40 mHintergrund schwarz*
ESC [ 41 mHintergrund dunkelrot
ESC [ 42 mHintergrund dunkelgrün
ESC [ 43 mHintergrund braun
ESC [ 44 mHintergrund dunkelblau
ESC [ 45 mHintergrund blaurot
ESC [ 46 mHintergrund zyan
ESC [ 47 mHintergrund hellgrau
Standardeinstellung, jedoch unter HT nicht Vorgabe. Dort ist der Hintergrund hellweiß und kann, einmal verstellt, mit keiner Sequenz wieder so eingestellt werden.

Tastatur-Kodes

In der umgekehrten Richtung muss das Mikrocontroller-Programm nicht nur die Antwort-Strings der Abfragen (Kursor-Position) verarbeiten, sondern auch die Sondertasten, die es auf einem Schreibmaschinenterminal noch nicht gab. Die »höheren« Funktionstasten sind hier aufgeführt, weil Tasten bisweilen belegt und so VT200-kompatibel gemacht werden können. Für 8-bit-Mikrocontroller ist eine sinnvolle und häufig genutzte Transkodierung in der Spalte 8bit angegeben. Mit der Transkodierung können ganz nebenbei ältere Tastaturen ohne Sonder- und Funktionstasten unterstützt werden; man muss dann „nur noch“ die Control-Codes parat haben.

TasteKode*Emul:HTW3NCPuTTYBemerkung
Cursor ModeApp ModeVT528bitMan tut gut daran, einfach alle Modi zu unterstützen
Pfeil hochESC [ AESC O AESC A^PVT100
ANSI
OK
OK
OK
-
OK
OK
ESC [ AWirken bei Echo (= Zurücksendung zum Terminal) schlauerweise genau so wie erwartet.
Pfeil runterESC [ BESC O BESC B^NESC [ B
Pfeil rechtsESC [ CESC O CESC C^FESC [ C
Pfeil linksESC [ DESC O DESC D^BESC [ D
Pos1ESC [ H^AVT100
ANSI
nein
OK
?
-
OK
OK
ESC [ 1 ~Als Bildschirmsteuerkode geht der Kursor nach (1:1)
EndeESC [ K^EVT100
ANSI
nein
OK
?
-
ESC [ J
ESC [ J
ESC [ 4 ~Als Bildschirmsteuerkode wird der Rest der Zeile ab Kursor gelöscht
RückschrittBS (=08h, 8, ^H)VT100
ANSI
OK
OK
OK
-
OK
OK
DEL (0x7F, 127)Nur zur Erinnerung! Das Echo hat andere Wirkung als unter DOS/Windows!
ESC [ 1 ~^HVT200Nie gesehen
TabulatorTAB (=09h, 9, ^I)VT100
ANSI
OK
OK
OK
-
OK
OK
TAB (0x09, 9, ^I)Nur zur Erinnerung!
EnterCR (=0Dh, 13, ^M)LF (0x0A, 10, ^J)
Strg+EnterLF (=0Ah, 10, ^J)?
EscESC (=1Bh, 27, ^[)2x: ^CESC (0x1B, 27, ^[)
FindESC [ 2 ~-VT200-Nicht auf der PC-Tastatur
EinfgESC [ 2 ~ESC [ 2 ~Ggf. mit diesem Kode belegen
EntfESC [ 3 ~7Fh, 127ESC [ 3 ~
DEL (=7Fh, 127)VT100
ANSI
OK
nein
OK
-
OK
OK
SelectESC [ 4 ~-VT200-Nicht auf der PC-Tastatur
BildAufESC [ 5 ~^UESC [ 5 ~Ggf. mit diesem Kode belegen
BildAbESC [ 6 ~^VESC [ 6 ~
F1ESC O PESC P^\ = 1ChVT100
ANSI
OK
OK
OK
-
nein
nein
F2ESC O QESC Q^] = 1Dh
F3ESC O RESC R^^ = 1Eh
F4ESC O SESC S^_ = 1Fh
F5ESC [ 15 ~VT200Ggf. mit diesem Kode belegen
F6ESC [ 17 ~
F7ESC [ 18 ~
F8ESC [ 19 ~
F9ESC [ 20 ~
F10ESC [ 21 ~
F11ESC [ 23 ~
F12ESC [ 24 ~
Num ModeApp ModeVT52
Num00ESC O pESC ? pVT100
ANSI
OK
OK
OK
-
OK
OK
Umschaltung in App Mode mit „ESC =“, in VT52 mit „ESC [ ? 2 l“
Zurück mit „ESC >“ bzw. „ESC [ ? 2 h“
Num11ESC O qESC ? q
Num22ESC O rESC ? r
Num33ESC O sESC ? s
Num44ESC O tESC ? t
Num55ESC O uESC ? u
Num66ESC O vESC ? v
Num77ESC O wESC ? w
Num88ESC O xESC ? x
Num99ESC O yESC ? y
Num//Diese Tasten gibt es wohl am echten Terminal nicht
Num**ESC O lESC ? l
Num--ESC O mESC ? m
Num++ *²ESC O MESC ? Mim Gegensatz zur Dokumentation
NumEnterCRCRCR
Num,,ESC O nESC ? n
Gängige TastenFreie Tasten: ^K, ^O, ^T, ^W, ^X, ^Y
Eingabe-Abbruch^CCopy für diese Taste ist hirnrissig!
Bild neu zeichnen^LDrucker: Seiten- oder Formularvorschub
Mikrocontroller: Reset^RVorzugsweise in Empfangs-ISR verarbeiten
XOFF = Ausgabe anhalten^SZwingend in Empfangs-ISR verarbeiten (siehe unten)
XON = Ausgabe fortsetzen^Q
Dateiende, manchmal Entf^D (^Z)
Bimmel-Test^G
Eingabezeile (bspw. Passwort) löschen^U8 bit: Überlagerung mit BildAuf, kein Problem
Mikrocontroller: Urlader anspringen^Z (^D)
HyperTerminal sendet nach jeder Sondertaste einige Null-Bytes an Müll!
In W3 liefert diese Taste irritierenderweise CR

Alte Terminal-Emulatoren: Die Kombination der Sondertasten mit (Shift) oder Strg (Ctrl) funktioniert leider nicht, sodass Markieren und wortweises Springen gängiger Texteditoren nicht realisierbar ist.

Bei PuTTY kann man die Funktionstasten auf Xterm 216+ einstellen. Und die Pfeiltasten auf xterm-style bitmap. Beides verlangt Änderungen in der Terminal-Software, jedoch ist jede dieser Tasten mit , Alt und Strg (in dieser Reihenfolge und in allen Kombinationen) versehen. Zumeist in der Form ESC [ <Tastennummer> ; <CAS-Bitmap> ~. Die „<CAS-Bitmap> - 1“ ergibt die echte Bitmap, von 0 bis 7. Die 0 (keine Modifiziertaste), also <CAS-Bitmap> = 1, wird nicht übertragen. In der Originaleinstellung kann man nicht und nirgends mit +Pfeiltasten Textmarkierungen erweitern. Hingegen Strg (Wortspringen) und Alt funktionieren in der Originaleinstellung, jedoch mit grässlich zu dekodierenden Sequenzen.

Die Betätigung der ESC-Taste (bzw. ^[, deutsch Strg+ü) zum Verlassen von Dialogen kann man nur über fehlende Folgebytes (nach einer gewissen Zeit) feststellen.

Der Grafikzeichensatz

** Zeichensatzumschaltung **
		ESC ( 0		VT100, nicht VT100J
		ESC ( B		zurück
Der Grafikzeichensatz enthält statt der Kleinbuchstaben die beliebten
Rahmensymbole sowie einige wenige mathematische Symbole (VT220):
`	Diamant			p	waag. Linie halb-oben
a	Schachbrett		q	─ waag. Linie Mitte
b	␉ (klein)		r	waag. Linie halb-unten
c	␌			s	waag. Linie unten
d	␍			t	├ Abzweig links (nach rechts)
e	␍			u	┤ Abzweig rechts (nach links)
f	° (Grad)		v	┴ Abzweig unten (nach oben)
g	± (Plusminus)		w	┬ Abzweig oben (nach unten)
h	␤			x	│ (senkr. Strich)
i	␋			y	≤ (Kleiner-Gleich)
j	┘ (Ecke rechts unten)	z	≥ (Größer-Gleich)
k	┐ (Ecke rechts oben)	{	π (pi)
l	┌ (Ecke links oben)	|	≠ (Ungleich)
m	└ (Ecke links unten)	}	£ (Pfund-Symbol)
n	┼ (Ramenkreuz)		~	· (Punkt in Mitte)
o	waag. Linie oben	DEL	DEL					nur was der Terminal-Font hergibt
Der „deutsche Zeichensatz“ ersetzt in bekannter Manier "@[\]{|}~"
durch "§ÄÖÜäöüß".
Siehe auch www.vt100.net.

Die Verwendung & das XON/XOFF-Protokoll

Die folgenden Darlegungen gehen davon aus, dass alle serielle Kommunikation komplett interruptgesteuert über Puffer abläuft. Ansonsten würde der Mikrocontroller wechselweise bei printf() arbeiten und dann einzelzeichenweise an der Schnittstelle warten. Daher sollte die Puffergröße dem längsten printf()-Ergebnis entsprechen.
bool esc;
bool control;
char inchar;
int ansi_arg;
// eine Zeichen-Eingabe-Routine
bool peekchar(void) {
 char c;
 while (getc(&c)) {
  if (esc) {
   switch (c&0x7F){
    case '[':			// bei VT52 kommt kein [
    case 'O':			// im App Mode
    case '?': continue;		// VT52 NumPad
   }
   if ('0'<=c && c<='9') {
    ansi_arg=ansi_arg*10+c-'0';
    continue;
   }
   if (c==';') {
    ansi_arg=0;
    continue;
   }
   if (c>' ') {
    esc=0; control=1;
    inchar=c;
    return TRUE;
   }
   continue;			// Doppel-Escapes etwa
  }
  switch (c) {
   case 27: esc=1; ansi_arg=0; continue;
  }
  control=0;
  inchar=c;
  return TRUE;
 }
 return FALSE;		// kein weiteres Zeichen in Warteschlange
}  

Die Initialisierung

Der ANSI-Modus ist wegen seiner Farbfähigkeit im (wohl am meisten verwendeten) HyperTerminal am geeignetsten. Deshalb ist die optimale Sequenz:

printf("\033[37;40m" "\033[2J" "\033[H" "\033[6n");

Sie schaltet auf ANSI um, löscht den Bildschirm schwarz (für bessere Brillanz der Farben zu empfehlen) und fragt die Kursorposition ab. Die Darstellung in Einzelzeichenketten pro Escape-Sequenz verbessert die Lesbarkeit. Der C-Compiler setzt sie beim Kompilieren zusammen. Für das Escape-Zeichen wird die Oktal-Kodierung "\033" verwendet; das Metazeichen "\e" ist nicht portabel.

Denkbar ist auch folgende Schreibweise:

#define ESC "\033"	/* das Escape-Zeichen */
#define CSI ESC"["	/* die sog. CSI-Sequenz */
printf(CSI"37;40m" CSI"2J" CSI"H" CSI"6n");

Danach muss folgender String zurück kommen:

"\033[?1;1R"

Nun hat HyperTerminal einen Fehler und sendet grundlos weitere sinnlose Bytes (einige Null-Bytes, Steuerzeichen und Zeichen > 80h) hintendrein. (Die anderen getesteten Programme tun das nicht.) Deshalb muss man etwas warten und dann den Eingabepuffer leeren, sonst bleibt unweigerlich Müll im Eingabepuffer.

Nun kann das Terminal-Programm bereits im VT100-Modus sein. Das prüft man durch folgende Sequenz:

printf("\033[c");

Das führt zu "\033[?1;2c", wobei die zweite Ziffer anders sein kann. Bei HyperTerminal kommt danach wieder ein Rattenschwanz aus Müll. Im ANSI-Modus bleibt die Antwort aus.

Üblicherweise wird man einige Werte auf feste Positionen (bspw. oben) ausgeben und einen anderen Bereich des Bildschirms (bspw. unten, stets in voller Breite) rollbar für die Ausgabe von Fehlermeldungen u. ä. verwenden. Dazu nützt die Festlegung des rollbaren Bereichs; der Kursor muss »zu Fuß« hineingesetzt werden:

printf("\033[%d;%dr" "\033[%d;1H",start,ende,start);

Natürlich können die "%d" durch Konstanten ersetzt werden.

Das laufende Programm

Da das Abfragen der Kursorposition lästig ist, sollte sie im Programm mitgeführt werden.

Per Schalter sollte eine Zeichensatz-Auswahl erfolgen, DOS (OEM CP437) oder Windows/Linux (»ANSI«-CP1252 bzw. ISO-Latin1). Je nachdem, womit das Programm erstellt wird, braucht man eine der beiden folgenden Umkodierungs-Tabellen für Zeichenkodes >=80h:

unsigned char oem2ansi[0x80]={
0xC7,0xFC,0xE9,0xE2,0xE4,0xE0,0xE5,0xE7,0xEA,0xEB,0xE8,0xEF,0xEE,0x3C,0xC4,0xC5,
0xC9,0xE6,0xC6,0xF4,0xF6,0xF2,0xFB,0xF9,0xFF,0xD6,0xDC,0xA2,0xA3,0xA5,0x80,0x81,
0xE1,0xED,0xF3,0xFA,0xF1,0xD1,0xAA,0xBA,0xBF,0x82,0xAC,0xBD,0xBC,0xA1,0xAB,0xBB,
0x83,0x84,0x85,0xA6,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F,0x90,0x91,
0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D,0x9E,0x9F,0xA8,0xA9,
0xAD,0xAE,0xAF,0xB3,0xB4,0xB6,0xB8,0xB9,0xBE,0xC0,0xC1,0xC2,0xC3,0xC8,0xCA,0xCB,
0xCC,0xDF,0xCD,0xCE,0xCF,0xD0,0xB5,0xD2,0xA4,0xD3,0xD4,0xA7,0xD5,0xD7,0xD8,0xD9,
0xDA,0xB1,0xDB,0xDD,0xDE,0xE3,0xF7,0xF0,0xB0,0xF5,0xB7,0xF8,0xFD,0xB2,0xFE,0xA0};
unsigned char ansi2oem[0x80]={
0x9E,0x9F,0xA9,0xB0,0xB1,0xB2,0xB4,0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xBB,0xBC,0xBD,
0xBE,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,
0xFF,0xAD,0x9B,0x9C,0xE8,0x9D,0xB3,0xEB,0xCE,0xCF,0xA6,0xAE,0xAA,0xD0,0xD1,0xD2,
0xF8,0xF1,0xFD,0xD3,0xD4,0xE6,0xD5,0xFA,0xD6,0xD7,0xA7,0xAF,0xAC,0xAB,0xD8,0xA8,
0xD9,0xDA,0xDB,0xDC,0x8E,0x8F,0x92,0x80,0xDD,0x90,0xDE,0xDF,0xE0,0xE2,0xE3,0xE4,
0xE5,0xA5,0xE7,0xE9,0xEA,0xEC,0x99,0xED,0xEE,0xEF,0xF0,0xF2,0x9A,0xF3,0xF4,0xE1,
0x85,0xA0,0x83,0xF5,0x84,0x86,0x91,0x87,0x8A,0x82,0x88,0x89,0x8D,0xA1,0x8C,0x8B,
0xF7,0xA4,0x95,0xA2,0x93,0xF9,0x94,0xF6,0xFB,0x97,0xA3,0x96,0x81,0xFC,0xFE,0x98};

Die übliche Methode, nur die Umlaute umzusetzen, ist für Mikrocontroller kein Armutszeugnis, sondern bisweilen aus Platzmangel nicht anders möglich.

Der Verzicht auf Umlaute erspart Aerger, aber es sieht haesslich aus!

Im Hinblick auf die Zukunft ist als weitere Kodierung UTF-8 dringend angeraten!! Für den Mikrocontroller genügt es aber, intern mit OEM oder ANSI oder Hitachi-Display-Kodes zu arbeiten. Ungültige UTF-8-Kodes sollte der Mikrocontroller schlauerweise (und entgegen der RFC2279) als OEM oder ANSI verarbeiten, um fehlertolerant zu bleiben. Bestimmt wird auch in zehn Jahren nicht jeder Anwender wissen, was UTF-8 ist. Obwohl es wirklich wichtig ist.

Ein besonders hübsches Programm kann sich an wechselnde Fenstergrößen anpassen. Dazu positioniert es den Kursor regelmäßig nach »janz weit draußen« (=jwd), bspw. mit:

printf("\033[99;99H" "\033[6n");

Das Terminal wird (hoffentlich!) den Kursor in die rechte untere Ecke stellen, und die nachfolgende Abfrage liefert dann mit:

if (scanf("\033[?%d;%dR",&y,&x)==2) ...;
in x und y die Koordinaten. Sicherheitshalber stellt man den Kursor vorher in die Standard-Ecke, oder ignoriert Rückgabewerte kleiner als 24 bzw. 80.

Ein Stückchen Kode, den man sonst ständig neu erfinden müsste, ist die Eingabezeile mit Editierfunktionen. Das Verhalten wurde weitestgehend an Windows-Editfelder angelehnt, insbesondere das Markieren der Vorgabe. Die globale Variable inputstring erspart für Mikrocontroller lästige Schaufelarbeiten.

static void deleteright() {printf(CSI"K");}		// löschen rechts
static void gotoxy(int x, int y) {printf(CSI"%d;%dH",y,x);}	//wie Pascal

static char inputstring[80];	// einzige statische Variable
static void inputclear(int x) {
 gotoxy(x,21); 
 deleteright();
 inputstring[0]=0;
}

// Eingabe -- während der Eingabe steht der Rest des Bildschirms still
// Angezeigt und zurückgegeben wird  -- hier feste Zeile 21
static bool input(const char *prompt) {
 int edleft=strlen(prompt)+3;
 int inlen=strlen(inputstring);
 int x=0, nx=inlen;	// Momentaner und neuer Kursor
 SaveCurs();
 printf(XY(1,21) CSI"30;43m" CSI"K" "%s: ",prompt);
 high=1;	// Zunächst alles markieren (wie Windows)
rep:
 StartHi();
 printf("%s",inputstring+x);
 EndHi(); if (high) printf(CSI"30;43m");	// Farbkode wiederherstellen
 if (nx<=x) putc(' ');		// 1 Zeichen dahinter löschen (für BS und DEL)
rep1:
 x=nx;
 gotoxy(x+edleft,21);		// Kursor positionieren
 for(;;) {
  if (!peekchar()) _idle_(); else switch (UART_Inchar) {
   case 'C'+256: nx++; goto move;	//RIGHT
   case 'D'+256: nx--; goto move;	//LEFT
   case 'H'+256: pos1: nx=0; goto move;	//HOME
   case 'K'+256: ende: nx=inlen; move:{	//END
    nx=limit(nx,0,inlen);
    if (_testclear_(high)) goto rep;	// komplett zeichnen
   }goto rep1;
   case '~'+256: switch(argv[0]){	// VT200-Tasten
    case 1: goto bs;
    case 3: goto del;
   }goto b;
   case 27+256:			// 2x Escape (sehr intuitiv)
   case 3:			// ^C, Programm-Ende = Abbruch
   case 4:			// ^D, Eingabe-Ende Unix = Abbruch
   case 0x1A:			// ^Z, Eingabe-Ende DOS = Abbruch
   case 13: goto ret;		// ENTER, übernehmen
   case 8: bs: if (!x) goto b;	// Löschen nach links, ignorieren am Anfang
    nx=--x; putc(8); nobreak;	// 1 Zeichen nach links rücken fürs Löschen
   case 0x7F: del:{		// Löschen nach rechts, ignorieren am Ende
    if (_testclear_(high)) {
     x=nx=inlen=0; inputclear(edleft);	// Kursor positionieren, rechts löschen
    }
    if (x==inlen) goto b;
    memmove(inputstring+x,inputstring+x+1,inlen-x);
    inlen--;
   }goto rep;			// rechts neuzeichnen
   case 1: goto pos1;		// ^A, Zeilenanfang
   case 5: goto ende;		// ^E, Zeilenende
   default: if (UART_Inchar>=' ') {
    if (_testclear_(high)) {
     x=nx=inlen=0; inputclear(edleft);	// Kursor positionieren, rechts löschen
    }
    if (inlen+edleft<79) inlen++;	// neue Länge
    memmove(inputstring+x+1,inputstring+x,inlen-x);
     	// Platz machen ODER letztes Zeichen herausschieben
    inputstring[x]=UART_Inchar;	// Zeichen einfügen
    nx++;			// Kursor nach rechts
    goto rep;			// rechts neuzeichnen
   }else b: putc(7); break;	// ^G, Bimmel-Test
  }
 }
ret:
 high=0;
 printf(CSI"0m" CSI"2K");	// Zeile mit Normalattributen löschen
 RestoreCurs(); 
 return UART_Inchar==13;
}
Keine Panik bei goto! Ohne wäre der Quelltext und der Maschinenkode dreimal so lang... Pardon, high ist noch eine globale Bitvariable, die in den Funktionen StartHi() und EndHi() wirksam wird.

Die Funktion peekchar() verwaltet alle ANSI-Buchstaben und erzeugt ggf. Zeichenkodes >=100h für eine einfache Fallunterscheidung. (Bei 8-bit-Prozessoren sollte man solche "Optimierungen" tunlichst unterlassen!)

Die lästigen Unterschiede

Der Leser mag bitte diesen unfertigen Abschnitt überspringen…

### HyperTerminal ###
Das dumme HyperTerminal kann eigentlich nur im ANSI-Modus betrieben werden
und unterstützt nur folgende Sondertasten:
Pfeiltasten, Pos1, Ende, F1..F4; insbesondere KEIN PgUp, PgDn!
Im VT100-Modus entfallen zudem Pos1 und Ende, Grau+ ergibt Komma.
Der VT52-Modus kann genauso viel, mit kürzeren Kodes.
Keine Funktion der Entf-Taste. Backspace liefert standardmäßig 08h
ANSIW brachte keine sichtbaren Veränderungen (von wegen Unicode).
Kermit: 01 2D 20 53 7E 25 20 40 2D 23 59 31 7E 28 43 0D
	01 "- S~% @-#Y1~(C" 0D
### Windows 3.1 Terminal ###
Im VT100-Modus nur Pfeiltasten und F1..F4; muss man aber erst aktivieren
(="Funktions-, Richtungs- und Strg-Tasten für Windows" AUS), kein Pos1/Ende!
Erstaunlicherweise funktioniert die Entf-Taste und liefert 7Fh,
Backspace liefert 08h, 20h, 08h
XMODEM: 18 18 18 18 18 08 08 08 ...
### NC Term90 ###
Auch hier gibt es keine Unterstützung für PgUp/PgDn; das ist (sinnvollerweise)
mit Upload/Download belegt! Entf liefert 7Fh, Backspace 08h
Kermit: 01 29 20 53 7E 2A 20 40 2D 23 57 0D
	01 "9 S~* @-#W" 0D

Das XON/XOFF-Protokoll

Für den Fall, dass der Mikrocontroller das Terminal mit Daten überschüttet, ist zur Synchroni­sierung das XON/XOFF-Protokoll das geeignetste, weil es keine weitere Leitung benötigt. Es ist auch für Binär­daten­ströme geeignet, da diese in aller Regel nur in eine Richtung laufen und die (regulär im Binär­daten­strom auftretenden) Zeichen für XON und XOFF nur im Rück­kanal relevant sind und vom Binär­daten­strom-Sender ausgewertet werden.

Nicht geeignet ist es für Halbduplex-Systeme, etwa RS485 oder OneWire. Da ist die Festlegung einer maximalen Paketlänge erste Voraussetzung für die Synchronisation. Unzweckmäßig ist der Einsatz von XON/XOFF, wenn die Datenströme über ein Transportmedium mit eingebauter Synchronisation, etwa TCP/IP oder USB-CDC, abgewickelt werden.

XON/XOFF-Darstellung
Datenfluss beim XON/XOFF-Protokoll

Ist der Datenpuffer des Terminals fast voll, sendet es (statt eines Tastendrucks) den Kode XOFF (=13h, 19, ^S). Daraufhin sollte der Mikrocontroller den Datenausstoß anhalten; am einfachsten, wenn bei Empfang von XOFF der Interrupt „Sendehalteregister frei“ abgeschaltet wird.
Etwas »Kopffreiheit« (= Abstand zwischen XOFF-Limit und Puffer voll) von mindestens zwei Zeichen benötigt der Empfangspuffer, weil das Aussenden von XOFF und das Verarbeiten in der Gegenstelle etwas Zeit erfordert.

Bei genügend leerem Datenpuffer sendet das Terminal den Kode XON (=11h, 17, ^Q). Das Mikrocontrollerprogramm sollte nun den (vorher gesperrten) Sendehalteregister-Interrupt freigeben.
Das XON-Limit darf mit Puffer leer zusammenfallen; es schadet allenfalls dem Durchsatz.

Wichtiges implementatorisches Detail: Das Aussenden von XOFF und XON darf keinesfalls über den Sendepuffer laufen, sondern geht priorisiert an diesem vorbei, es sind sogenannte OOB-(out-of-band-)Daten. Diese Zeichen werden auch dann gesendet, wenn die Gegenstelle selbst per XOFF-Befehl (in Gegenrichtung) die Datenannahme verweigert. Auch beim Zeichenempfang gehen diese Zeichen niemals in den Empfangspuffer, sondern werden sofort in der ISR verarbeitet.

Von der Funktion des XON/XOFF-Protokolls im Mikrocontroller kann man sich ganz einfach durch Betätigen von Strg+S im Terminalprogramm überzeugen: Die Ausgaben müssen anhalten, und - richtig programmiert - verbraucht der im Schlafmodus "hängende" Controller auch weniger Strom als sonst. Aufwecken (hier im wahrsten Sinne des Wortes) erfolgt dann mit Strg+Q.

Für die umgekehrte Richtung ist das XON/XOFF-Protokoll zwar auch vorgesehen, aber der Eingabestrom von einer Tastatur sollte nicht so riesig werden. (Ausnahme: Eingabeumleitung! Dazu muss die Sende-ISR so gestaltet werden, dass ein Zeichen an der Warteschlange vorbei bevorzugt gesendet werden kann.)

Zu beachten ist, dass XON/XOFF für die Dauer einer (im folgenden Abschnitt beschriebenen) Binärdatenübertragung abzuschalten ist!

Für einen AT90S4433 oder ATmega kann es aus Aufwandsgründen bei „halbem“ XON/XOFF-Protokoll bleiben, wie in diesem funktionierenden Assemblerquelltext dargestellt.

Der Quelltext für „volles“ XON/XOFF auf dem 80C167:

#define BAUDRATE 19200	// Datenbits fest 8, Stopbits fest 1
#define UART_OBUFLEN 64	// Puffer für interruptgesteuerte Zeichenausgabe
#define UART_IBUFLEN 32	// dito für Zeichen-Eingabe
#define  XON_THRESHOLD (UART_IBUFLEN/2)// Schwelle für Senden von XON
#define XOFF_THRESHOLD (UART_IBUFLEN-8)// Schwelle für Senden von XOFF
#define  XON 0x11	// ^Q
#define XOFF 0x13	// ^S
sbit  P3_10= P3^10;	// Port-Pin für TxD (muss HIGH sein)
sbit DP3_10=DP3^10;	// Port-Richtung für TxD (muss 1 = Output sein)
sbit DP3_11=DP3^11;	// Port-Richtung für RxD (muss 0 = Input sein)
bool UART_Tx_Allow;	// true wenn XON und false wenn XOFF empfangen wurde
bool UART_XonXoff;	// XON/XOFF-Flusssteuerung (AUS während XMODEM)
bool UART_Xoff_sent;	// XON nur senden, wenn auch XOFF gesendet wurde
bool UART_Xon_sent;	// XON-Stopp, wenn XON gesendet + Zeichen eintreffen
char UART_Bypass;	// XON/XOFF-Zeichen, hochpriorisiert einzufügen
word UART_Inchar;	// low=letztes Zeichen von peekc() (optimiert 16bit)

static char obuf[UART_OBUFLEN];
static int obufwr,obufrd;	// zunächst Null
static char ibuf[UART_IBUFLEN];
static int ibufwr,ibufrd;	// zunächst Null

void UART_Init(void) {
 P3_10=1;		// Port 3.10 auf High (TxD)
 DP3_10=1;		// Port 3.10 als Ausgang (TxD)
 			// Port 3.11 bleibt Eingang (RxD)
 S0BG =CPUCLK/32/BAUDRATE-1;
 S0CON=0x8011;		// set serial mode
//	    ^-- 1 Stopbit (0), 8bit Daten (001)
//	   ^--- Überlauf, Rahmenfehler, Parity AUS (000), Empfänger EIN (1)
//	  ^---- (keine Fehler-Flags setzen)
//	 ^----- Baudratengenerator EIN (1), Loopback, Baudrate/3, Odd AUS (000)
 S0TBIC=MKIC(1,0,2,0);	// niedrige Priorität, Interrupts noch gesperrt
 S0RIC =MKIC(0,1,2,1);	// ebenso niedrige Priorität
 UART_Tx_Allow=1;
 UART_XonXoff=1;
}

void UART_Transmit(void) interrupt S0TBINT=0x47 {
 ISR_TIC();
 if (UART_Bypass) {		// Notfall-Zeichen (XON oder XOFF)
  S0TBUF=UART_Bypass;
  UART_Bypass=0;
 }else if (obufrd!=obufwr) {
  S0TBUF=obuf[obufrd];
//  _bfld_(S0TBUF,255,obuf[obufrd]);	// besser, aber crasht den Compiler!!
  RING_INC(obufrd,UART_OBUFLEN);
 }else{
  S0TBIE=0;	// keine Interrupts = Puffer leer
  S0TBIR=1;	// aber bei EI sofort ISR aufrufen
 }
 ISR_TOC();
}

void UART_Receive(void) interrupt S0RINT=0x2B {
 char c;
 int fill;
 ISR_TIC();
 c=S0RBUF;
 if (UART_XonXoff) switch (c) {		// XON und XOFF nicht in Empfangspuffer
  case  XON: S0TBIE=UART_Tx_Allow=1; goto toc;
  case XOFF: S0TBIE=UART_Tx_Allow=0; goto toc;
 }
 fill=ibufwr-ibufrd; if (fill<0) fill+=UART_IBUFLEN;
 if (UART_XonXoff && fill>=XOFF_THRESHOLD) {
  UART_Bypass=XOFF;	// Notfalls bei jedem Zeichen einfordern
  UART_Xoff_sent=1;
  S0TBIE=1;
 }
 if (_testclear_(UART_Xon_sent)) UART_Xoff_sent=0;
 	// wenn ein Zeichen kommt, XON nicht mehr wiederholen lassen
 if (fill==UART_IBUFLEN-1) goto toc;	// Puffer voll, Zeichen ignorieren
 ibuf[ibufwr]=c;
 RING_INC(ibufwr,UART_IBUFLEN);
toc:
 ISR_TOC();
}

char putc(char c) {	// binäre Zeichenausgabe auf 1. serielle Schnittstelle
 if (IEN) {	// interruptgetrieben wenn globale Interrupts ein
  int o_next=obufwr;
  obuf[o_next]=c;	// diese Stelle ist stets frei
  RING_INC(o_next,UART_OBUFLEN);
  for (;;_idle_()) if (o_next!=obufrd) break;
  obufwr=o_next;
  S0TBIE=UART_Tx_Allow; // ISR anschubsen, wenn nicht per XOFF blockiert
 }else{		// klassisch (mit Handbremse, OHNE Xon/Xoff)
  while (!_testclear_(S0TBIR));
  S0TBUF=c;
 }
 return c;
}

bool peekc(void) {	// Zeichen im Empfangspuffer?
 int fill=ibufwr-ibufrd;
 if (fill<0) fill+=UART_IBUFLEN;
 if (UART_Xoff_sent && fill<=XON_THRESHOLD) {
  UART_Bypass=XON;	// Notfalls bei jedem peekc() einfordern
  UART_Xon_sent=1;
  S0TBIE=1;
 }
 if (!fill) return false;	// wenn Puffer leer
 UART_Inchar=(byte)ibuf[ibufrd];
 RING_INC(ibufrd,UART_IBUFLEN);
 return true;
}

Das Arbeiten mit einem globalen Zeichenpuffer (UART_Inchar) erweist sich als wesentlich zweckmäßiger, als das Zeichen ständig als Funktions-Rückgabewert herumzuschleppen. Der Rückgabetyp bool (dasselbe wie bit) ist beim C166er Compiler recht günstig implementiert.

Binärdatenübertragung mit XMODEM

Bisweilen benötigt man die Möglichkeit, Binärdaten aus dem Mikrocontroller zu lesen bzw. hinein zu schreiben.
Beispielsweise die nächste Programmversion. Dazu musste man immer zu einem Brennprogramm wechseln. Darauf kann man künftig verzichten, wenn man nur einen Urlader im Mikrocontroller behält, der sogar eine Entschlüsselung der (geheimen) Brenndaten vornehmen kann.
Umständlich ist auch, dass das Brennprogramm oftmals die gleiche Schnittstelle braucht, sodass man das Terminal-Emulationsprogramm derweil beenden muss.

Auch das Auslesen des RAM-Bereichs als eine Art "Arme-Leute-Debugger" oder bei der Fehlersuche im Feldeinsatz (Stichwort: Bananensoftware, reift beim Anwender) ist sicher irgendwann nützlich.

Jede Terminal-Emulation bietet mehrere Binärdatenübertragungen an. Am einfachsten zu implementieren ist das XMODEM-Protokoll:

  1. Der Empfänger beginnt und sendet ein ^U = 15h
  2. Der Sender schickt einen Block aus 132 Bytes mit:
  3. Der Empfänger bestätigt den korrekten Empfang mit ^F = 06h, weiter bei 2. oder 5.
  4. Der Empfänger bestätigt den falschen Empfang mit ^U = 15h, weiter bei 2.
  5. Der Sender sendet ein Ende-Byte ^D = 04h (EOT), wenn fertig
  6. Der Empfänger quittiert durch Senden von ^F = 06h
Von Nachteil ist die feste Blockgröße von 128 Bytes (jaja, die CP/M-Zeiten).

Es gibt auch eine CRC-Variante mit besserer Fehlerabsicherung; diese überträgt 133-Byte-Blöcke mit einer 2-Byte-CRC. Das CRC-fähige Empfangsprogramm signalisiert sein Extra durch Senden von 'C' = 43h an Stelle von ^U = 15h, nach einem TimeOut sendet es dann ^U = 15h und fällt in den einfachen Prüfsummen-Modus zurück.
Die Norton Commander TERM9x.EXE unterstützen keine CRC. Auf der anderen Seite hat HyperTerminal den Fehler, beim Empfang über eine Minute lang CRC zu erwarten und dann auf Prüfsumme zurückzuschalten; bis dahin hat der Mikrocontroller (laut Vorschrift) längst aufgegeben.

Auch gibt es vergrößerte Blöcke; diese haben als erstes Byte ein ^B = 02h und sind 1 KByte lang. In Verbindung mit Mikrocontrollern genügen die kurzen Blöcke.

Während einer Datenübertragung ist keine Bildschirmausgabe möglich!

Einmal in den Sende- oder Empfangsmodus gebracht kann der Mikrocontroller nur durch Druck aufs richtige Knöpfchen vorzeitig erlöst werden:

Hier folgt eine (getestete!) Implementierung für den SAB80C167-Controller:
enum {SOH=1,EOT=4,ACK=6,NAK=0x15,CAN=0x18};

static bool crcmode;
static word crc;
static void new_crc(word c) {
 if (crcmode) {
  int j;
  c<<=8; crc^=c; j=8;do{crc<<=1; if(C)crc^=0x1021;}while(--j);
 }else crc+=c;		// C (=Carry) enthält das soeben herausgeschobene Bit
}

word xmodem_send(const char huge* buf, word buflen) {
 bool eot;
 word t,bytes,bn,i;	// Compiler stellt sich bei CHAR blöd an!
 
 UART_XonXoff=0;
 bn=1; bytes=i=0; eot=0; crcmode=0;
rep:
 t=t8h; do{
  if (peekc()) switch(UART_Inchar) {
   case ACK: {			// 6, ^F
    if (eot) goto ret;
    if (i>buflen) i=buflen;
    bytes+=i; buf+=i; bn++;
    if (buflen-=i) goto sendblock;
    putc(EOT);		// innerhalb 60s das ^D bestätigt absenden
    eot=1;
   }break;
   case 'C': crcmode=1; nobreak;// Bug in HyperTerminal: geht nicht ohne CRC
   case NAK: {			// 0x15, ^U; nur Prüfsumme
sendblock:
    RED_LED=~RED_LED;		// Bei jedem ACK oder NAK:  EIN oder AUS
    putc(SOH);			// 1, ^A
    putc(bn); putc(~bn);	// Blocknummer, inverse Blocknummer
    crc=0; i=0; do{
     word c=0x1A;		// Terminator (Dateiendekennung) ^Z
     if (i<buflen) c=buf[i];	// sonst "richtiges" Zeichen
     putc(c);
     new_crc(c);
    }while (++i<128);
    if (crcmode) putc(crc>>8);	// High-Teil zuerst
    putc(crc);
   }goto rep;			// wieder 1 Minute TimeOut
   case CAN: goto ret;		// Abbruchwunsch des Empfängers
  }else _idle_();
 }while (t8h-t<60*T8H_TICPERSEC);	// 1 Minute TimeOut
ret:
 UART_XonXoff=1;
 return bytes;
}

static bool getc1s(void) {	// Zeichen mit 1 Sekunde TimeOut empfangen
 word t=t8h;
 do{
  if (peekc()) return true;
  _idle_();
 }while (t8h-t<T8H_TICPERSEC);
 return false;
}

word xmodem_recv(char huge *buf, word buflen) {
 word bytes,bl,i,t,rcs;
 byte ak,bn,rbn,ibn;

 UART_XonXoff=0;
 bytes=0; crcmode=1; ak='C'; bn=1;
rep:
 i=10; do{
  putc(ak);			// versucht XMODEM/CRC
  t=t8h; do{
   bl=128;			// Standard-Blocklänge
   if (getc1s()) switch (UART_Inchar) {
    case 2: bl=1024; nobreak;	// akzeptiert XMODEM/1K
    case SOH: goto soh;
    case EOT: ak=ACK; goto eot;
   }
  }while (t8h-t<6*T8H_TICPERSEC);	// 6 "innere" Sekunden
  if (i==7 && _testclear_(crcmode)) ak=NAK; // nach 4 Versuchen zurückschalten
 }while (--i);			// 1 Minute lang
 goto ret;
soh:
 RED_LED=~RED_LED;		// Bei jedem Blockanfang:  EIN oder AUS
 ak=NAK;
 if (!getc1s()) goto rep;	// Blocknummer
 rbn=UART_Inchar;
 if (!getc1s()) goto rep;	// Inverse Blocknummer
 ibn=UART_Inchar;		// Tests erst am Blockende!
 crc=0; i=0; do{
  byte c;			// besserer Kode
  if (!getc1s()) goto rep;
  c=UART_Inchar;
  new_crc(c);
  if (i<buflen) buf[i]=c;	// gleich abspeichern
 }while (++i<bl);
 if (!getc1s()) goto rep;
 rcs=UART_Inchar;		// rcs = gelesene Prüfsumme
 if (crcmode) {
  if (!getc1s()) goto rep;
  rcs=rcs<<8 | UART_Inchar;
 }else crc&=0xFF;	// Block empfangen, jetzt kommen die Tests:
 if (rbn!=~ibn) goto rep;	// ungültige Blocknummer
 if (crc!=rcs) goto rep;		// ungültige Daten
 if (rbn==(byte)(bn-1)) goto ack; // ACK wiederholen
 if (!buflen			// keine Daten mehr erwartet (nicht fatal)
 || rbn!=bn) {ak=CAN; goto eot;} // fataler Sync-Fehler
 if (i>buflen) i=buflen;	// Minimum
 bytes+=i; buf+=i; ++bn;  buflen-=i;
ack:
 ak=ACK;
 goto rep;
eot:
 putc(ak);
ret:
 UART_XonXoff=1;
 return bytes;
}
Der Quelltext ist leidlich handoptimiert, deshalb einige merkwürdige Konstrukte und die vielen gotos.

Die volatile Variable t8h ist der Interruptzähler für den Zeitgeber T8 und macht bei 20 MHz Takt und Vorteiler 8 38 Schritte pro Sekunde; hervorragend geeignet für solche Zeitmessungen.

Alles zusammen im Mikrocontroller

Wenn wir schon mal bei dem SAB80C167 sind, dann könnte man die Daten auch gleich wegbrennen (AMD-Flash auf Phytec MiniModul-167):
static void preamble(word huge *adr, word third) {
// Interrupts sperren und AMD-Präambel-Bytes ausgeben
 disable();
 *(word*)&adr=0xAAAA;	// Low-Teil setzen
 *adr=0xAAAA;		// beide Flash-Chips via EXTS ansprechen
 *(word*)&adr=0x5554;
 *adr=0x5555;
 if (third) {
  *(word*)&adr=0xAAAA;
  *adr=third;
 }
}

static bool flashreset(word huge *adr) {
// AMD-Flash-Reset ausgeben und Interrupts freigeben
 *adr=0xF0F0;		// Flash-RESET auslösen (= Array-Lese-Modus)
 enable();
 return false;
}

static bool polling(word huge* addr, word data) {
 word r,r2;
 for(;;) {			// DATA# Polling
  r=*addr;
  if (r==data) {
   enable();
   return true;
  }
  r2=*addr;
  if (r&0x0020 && (r2^data)&0x00FF) break;	// Low-Teil defekt
  if (r&0x2000 && (r2^data)&0xFF00) break;	//High-Teil defekt
 }
 return flashreset();			// Flash-RESET auslösen
}

bool far ProgWord(word huge* addr, word data) {	// addr muss gerade sein!
 preamble(0xA0A0);		// Kommando: Byte schreiben
 *addr=data;
 return polling(addr,data);
}

bool far EraseSector(word huge* addr) {	// addr sollte durch 32K teilbar sein!
 preamble(0x8080); 		// Kommando: Sektor löschen
 preamble(0);
 *addr=0x3030;
 return polling(addr,0xFFFF);
}

bool far IsAMD256K(void) {
 word MID,DID;			// Hersteller- und Chip-ID
 preamble(0x9090);		// Kommando: Autoselect-Modus
 MID=*(word huge*)0;
 DID=*(word huge*)2;
 flashreset();
 if (MID==0x0101 && DID==0x2020) return true;
 return false;
}

bool ProgSector(const word huge* src, word huge* dst, word len) {
// 1 Sektor löschen und programmieren; dst sollte am Sektoranfang liegen
// (durch 32K teilbar sein), len darf bis zu 16K (=32KByte) groß sein.
// Prozeduren in RAM kopieren und diese anspringen!
// Da alle CALLs und JMPs absolut sind(?), verbietet sich dabei eine
// Adressverschiebung, also müssen o.g. Prozeduren in ein extra Segment,
// bspw. ab 400h, und im RAM braucht man eine ebenso große Lücke.
 word huge *cp=preamble;
 word plen=(ProgSector-preamble+1)/2;
 do { *(cp|RAM_BASE)=*cp; cp++; } while (--plen);
 if (!(IsAMD256K|RAM_BASE)()) return false;
 if (!(EraseSector|RAM_BASE)(dst)) return false;
 if (len) do{
  if (!(ProgWord|RAM_BASE)(dst,*src)) return false;
  dst++;
  src++;
 }while (--len);
 return true;
}

Serielles Plug-And-Play

Siehe dort.