Absolute Adressen

Neben den BIOS-Daten im Segment 0x0040 gibt es auch einige
Interruptvektoren, die keine sind. Die Interruptprogrammierung in C
werden wir in einer eigenen Folge besprechen aber jene Interruptvektoren,
die keine Sprungziele fr Programme, sondern nur Zeiger auf Tabellen sind,
passen besser zu diesem Abschnitt.

Was sind Interruptvektoren?
Vektoren sind eine bewhrte Mglichkeit, Tabellen und Programme
auf verschieblichen Adressen unterzubringen, denn der Vektor ist
ortsfest, sein Ziel je nach System variabel. Die ursprngliche
Aufgabe von Interruptvektoren ist, da sie durch Hardwareereignisse
ausgelst werden und unabhngig vom gerade laufenden Programm
ein kurzes Interruptserviceprogramm ausfhren, welches das laufende
Programm unmerklich unterbricht und danach wieder zu diesem zurckkehrt.
D.h. der Benutzer bemerkt (fast) nichts von diesen Hintergrund-'Heinzelmnnchen',
es sei denn, es gibt zu viele dieser Unterbrechungen, dann kann die
Vordergrundaktivitt auch einmal stark verlangsamt werden.

Die Interruptvektoren bei 80x86 CPUs beginnen bei der absoluten Adresse
0x00000 und enden bei der absoluten Adresse 0x003ff. Diese Anordnung
ist durch den Befehlssatz der CPU vorgegeben und kann nicht gendert werden.
Jeder Vektor
besteht aus 4 Bytes, es gibt daher 256 oder 0x100 Vektoren.
Ein Interruptvektor besteht aus Segment und Offset in umgekehrter
Anordnung und auch die Halbbytes von Segment und Offset sind jeweils
umgekehrt, das niederwertige Byte kommt zuerst.

+======+
|off lo| <---Adresse des Vektors = 4 * Interruptnummer
+------+
|off hi|
+======+
|seg lo|
+------+
|seg hi|
+======+

Jeder Vektor belegt 4 Bytes im Speicher. Normalerweise enthlt ein
Interruptvektor eine Adresse eines Interruptserviceprogramms.

Befehl zum Aufruf der Interruptserviceroutine in Assembler

  INT Interruptnummer

oder in C

  int86(int intno,...);

Die Aufgabe die diesen Interrupts zukommt sind bei den ersten bereits
durch die CPU-Konzeption vorgegeben, alle weiteren zunchst durch das BIOS
dann durch das jeweils geladene Betriebssystem, oder zustzlich geladene
Treiber (Maus, Netzwerk, EMS, XMS..).

Einige dieser Vektoren sind nicht Zeiger auf Programme sondern auf Daten.
Diese wollen wir hier besprechen.

Interrupt  Adresse   Inhalt
Nummer

0x1D       0x00074   Video Initialisierung
0x1E       0x00078   Diskette Parameter Table
0x1F       0x0007C   CGA Graphics Font

0x41       0x00104   Disk #1 Parm Table
0x43       0x0010C   EGA Parm Table Ptr
0x44       0x00110   EGA Graphics Font
0x46       0x00118   Disk #2 Parm Table

Diese Adressen wurden dem XT-AT-Handbuch entnommen.

Wie kann man nun diese Daten ansehen? Hndisch ganz einfach:
Mit DEBUG! Beispiel Disketten-Parameter-Tafel 2, die die
Parameter der Festplatten enthlt:

C:\>DEBUG
-D0:118
0000:0110                          01 E5 00 F0 E7 9B 00 F0           ........
0000:0120  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0130  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0140  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0150  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0160  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0170  E7 9B 00 F0 E7 9B 00 F0-E7 9B 00 F0 E7 9B 00 F0   ................
0000:0180  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
0000:0190  00 00 00 00 00 00 00 00                           ........
-df000:e500
F000:E500  00 D1 03 05 00 00 2C 01-00 00 00 00 00 D1 03 11   ......,.........
F000:E510  00 D1 03 07 00 00 FF FF-00 00 00 00 00 D1 03 11   ................
F000:E520  00 00 04 07 00 00 00 02-00 00 00 00 00 FF 03 11   ................
F000:E530  00 DD 02 05 00 00 2C 01-00 00 00 00 00 DC 02 11   ......,.........
F000:E540  00 DD 02 07 00 00 2C 01-00 00 00 00 00 DC 02 11   ......,.........
F000:E550  00 DD 02 05 00 00 2C 01-00 00 00 00 00 DD 02 11   ......,.........
F000:E560  00 32 01 04 00 00 00 00-00 00 00 00 00 50 01 11   .2...........P..
F000:E570  00 64 02 04 00 00 31 01-00 00 00 00 00 97 02 11   .d....1.........
-q
Die ab F000:E500 stehenden Daten sind die Kennwerte der Festplattenparameter.
Typisch ist die letzte Eintragung 0x11 = 17 = Anzahl der Sektoren.

Wie wird man diesen Zugriff mit Mitteln von C bewerkstelligen?


Das Programm HC05MM3.CPP gibt die Uhrzeit in Vielfachen von 53ms aus.
Die entstehende Zahl ist nur fr Differenzmessungen, nicht aber fr
die Auswertung in Stunden, Minuten, Sekunden geeignet. Das Programm
HC05MM3A.CPP bercksichtigt das Dastellungsformat und gibt in Stunden,
Minuten und Sekunden aus.

In beiden Programmen erfolgt der Hauptspeicherzugriff ber Pointer,
die zuerst gebildet werden mssen:

time_p=(ULONG far *)MK_FP(0x0040,0x006c);

und auf deren Ziel mit *time_p zugegriffen wird. Einfacher wird der
Zugriff durch eine eigene Klasse, die das Arbeiten mit Pointern
erspart (HC05MM3B.CPP). Hat man oft mit Zeitmessungen zu tun,
ist es ntzlich die oft wiederkehrenden Umwandlungen zwischen
den einzelnen Zeitkomponenten einer eigenen Zeit-Klasse zu bertragen
(HC05MM3C.CPP).

HC05MM4.CPP, HC05MM4.C
Da die Anzeige aller Interruptvektoren gleichzeitig auf einem Bildschirm
nicht mglich ist, werden hier zwei unterschiedliche Lsungen gezeigt.
In HC05MM4.CPP erfolgt die Anzeige der 256 Vektoren in 4 Bildschirmseiten,
nach jedem Bildschirm wird auf eine Taste gewartet. HC05MM4.C erlaubt die
Eingabe einer Vektornummer (dezimal), ab der die nchsten 64 Vektoren
angezeigt werden.

HC05MM5.CPP
Die BIOS-Variablen knnen gleichzeitig auf einem Bildschirm angezeigt
werden. Um sie zu interpretieren, bentigt man noch eine Tabelle, wie
etwa jene im selben Heft; noch besser wre es, die Texte aus der
Erklrung der BIOS-Daten mit den Daten selbst zu verknpfen, wie das
auch bei verschiedenen Systemanalyseprogrammen geschieht.

Beiden Programmen gemeinsam ist die Darstellung der Daten in Zeilen und
Spalten. Die Konstante COLW legt die Spaltenbreite, COL die Spaltenzahl
und ROW die Zeilenzahl. Die jeweilige Position wird in zwei Variablen
row und col gehalten, die durch col=(num/ROW)%COL und row=num%ROW
berechnet werden. Die Kursorposition fr den Schreibbeginn findet man mit
setxy(1+col*COLW,row+2), wobei 1 und 2 feste Abstnde vom Rand darstellen.


HC05MM6.C
Das Programm PORTEX erlaubt es die Adressen von seriellen
oder parallelen Schnittstellen auszutauschen. Diese Mglichkeit
ist im Betriebssystem nicht vorgesehen, daher mu man
es mit geeigneten Utilities - wie diesem - ermglichen.

Was in einem PC LPT1 (oder COM1) ist, bestimmt das BIOS beim
Initialisieren des BIOS-Datenbereichs. Leider gibt es hier eine
Unklarheit bei der Namensgebung: einerseits gibt es in einem
System eine bis mehrere parallele und serielle Schnittstellen.
Bezeichnen wir diese mit LPT1h...LPT3h, COM1h..COM4h. Anderseits
gibt es fr diese Kanle je einen Speicherplatz im BIOS-Datenbereich.

Der MSDOS-Druckkanal PRN
wird LPT1 zugewiesen. Jedes Mitprotokollieren mit ^P oder
jede Druckerausgabe in C an stdprn wird an LPT1 gesendet. MSDOS
erfhrt ber die vom BIOS initialisierten Adressen, was LPT1 ist.
So wie DOS sollte es auch jedes andere 'saubere' Programm machen:
nicht die Hardware selbst an 0x3bc ansprechen, sondern ber die
MSDOS-Druckausgabe (MSDOS-Funktion 5). Unglaubwrdig wird dieses
Verfahren, wenn man an einen anderen Kanal ausgeben will, z.B. LPT2.
Dann mu man dafr sorgen, da MSDOS die Adresse von LPT2

PORTEX erwartet in der Kommandozeile die Angabe zweier Argumente,
die Ports, die es auszutauschen gilt. Erlaubt sind LPT1, LPT2, LPT3,
COM1, COM2, COM3, COM4. Jede unerlaubte Kombination wird durch
die groe if..else if..-Kombination am Eingang abgefangen und ein
entsprechender Fehlertext ausgegeben. Fr eine saubere Trennung
von Text und Kode wird eingangs ein Array von Meldetexten in Form
von errtxt[] angelegt. Zur Vereinfachung der Anzeige wird der
Text nicht direkt ber printf() angezeigt, sondern ber die Funktion
error(); damit wird erreicht, da alle Anzeigen gleichartig erfolgen
und jede nderung des Ausgabeformats sich auf alle Ausgaben auswirkt.

Der Austauschvorgang selbst wird mit der Funktion swap() bewerkstelligt,
die zwei Pointer auf die auszutauschenden Speicherstellen als Parameter
bernimmt.

Bei einem Fehler in der Kommandozeile oder wenn die Kommandozeile
keinen Parameter enthlt, wird die Funktion display_status() aufgerufen,
die die aktuellen Adressen anzeigt. display_status() wird auch vor und
nach Autausch der Portadressen ausgefhrt.

Vorbemerkung zu M...
Der schnellste Zugriff auf absolute Speicheradressen ist nach wie vor
der zuvor beschriebene. Kommt es aber weniger auf die Schnelligkeit,
sondern eher auf einen verstndlichen Kode an, kann man die folgenden
Klassen MEM... benutzen.

Der Phantasie sind bei der Konstruktion von Klassen keine Grenzen
gesetzt, man kann sie, wie diese Beispiele zeigen, auch erfolgreich
fr hardwarenahes Programmieren einsetzen. Ob sich diese Konstruktionen
bewhren, ob man sie in dieser Form einsetzen kann, wird die Kompiler-
entwicklung zeigen. Derzeit bietet nur der BORLAND-Kompiler Templates
an. Die Kompiler fr Mikrokontroller kennen noch nicht einmal C++.
Ob sie es je lernen werden? Wir werden sehen!

Die Klasse MEM hat die Aufgabe eine absolute Speicheradresse zuverwalten.
Nicht einfach nur ein Byte, sondern auch 2 oder 4 Bytes, je nach Bedarf.
Dazu wird die Klasse MEM, wie auch alle folgenden Klassen, um einen fiktiven
Typ T konstruiert, der erst bei der Definition eines Objekts dieser
Klassen eingesetzt wird, je nachdem, welchen Typ die absolute Speicherstelle
enthlt.

MEM besitzt zwei Variablen: Val und p. p ist ein far-Pointer auf die
gewnschte absolute Speicherstelle und Val ist der aus dieser Speicherstelle
zuletzt gelesene Wert. Bei den spter verfaten Klasse MEM_P hat es sich als
praktisch erwiesen, sich den letzten Wert innerhalb der Klasse, Old
zu merken.

Es gibt vier Konstruktoren fr MEM:

MEM(T far *tp)      // Mit einem Pointer
MEM(UINT s, UINT o) // Mit Segment und Offset
MEM(T far *tp, T t) // Mit einem Pointer und einem Initialisierungswert
MEM(UINT s, UINT o) // Mit Segment und Offset und einem Initialisierungswert

Vier Meldefunktionen geben ber den Zustand der Klasse Auskunft: val()
liefert den zuletzt gelesenen Wert, addr() liefert den Wert des Pointers,
seg() liefert das Segment und off() den Offset.

Die Funktion operator=() dient zum Initialisieren der absoluten Adresse,
die Funktion operator()() liefert den aktuellen Wert der absoluten
Adresse.

Die absolute Adresse kann mit den Operatorfunktionen

++,--,<<,>>,~,+=,-=,*=,/=,%=,&=,|=,^=

so wie jede andere Variable auch bearbeitet werden.

Ein Objekt der Klasse MEM wird etwa wie folgt angewendet:

Die absolute Adresse bildanfang zeigt auf das Zeichen links oben
am Bildschirm.

  MEM<UINT> bildanfang(0xb800,0);

Die Operatorfunktion operator() liefert den aktuellen Wert.

  cout << "Links oben steht: " << hex << bildanfang() << endl;

Die absolute Adresse bildanfang kann wie alle anderen eingebauten
Typen bearbeitet werden:

  bildanfang++;

Jetzt steht links oben ein anderes Zeichen:

  cout << "Links oben steht: " << hex << bildanfang() << endl;

Die Klasse MEM_P ist von MEM abgeleitet. Die adressierte absolute
Speicherstelle enthlt einen far Pointer. MEM_P verwaltet nicht
den Zahlenwert auf einer absoluten Adresse, sondern den Wert jener
Adresse auf die der Pointer zeigt, der auf MEM_P gespeichert ist.
Neben dem indirekten Zugriff ist auch eingebaut, da der jeweils zuletzt
gelesene Wert in Old gespeichert wird. Damit im Zusammenhang ist auch die
Funktion changed(), die meldet, ob eine Zustandsnderung seit
der letzten Befragung der Adresse stattgefunden hat.

Es hat sich als erforderlich erwiesen, zustzlich zu den Elemetfunktionen
seg(), off() und addr() auch noch pointseg(), pointoff() und pointaddr()
einzusetzen, die den Wert des Pointers zurckmelden.

Die Klassen MEM und MEM_P werden durch die Programme HC05MM7,
HC05MM8, HC05MM9, HC05MM10 getestet. Die Programme HC05MM7,
HC05MM8 und HC05MM10 knnen auch gleichzeitig zum Testen von
der spter besprochenen Klasse BIOS verwendet werden, die ein
Spezialfall der Klasse MEM fr den Bereich der BIOS-Daten darstellt.

HC05MM7
Das einfachste Beispiel zeigt, wie man den Inhalt absoluter Speicherstellen
ausliest. Es sollen die Adressen der seriellen Schnittstelle angezeigt
werden. Man definiert ebensoviele MEM-Objekte als man bentigt,im Beispiel
die Schnittstellen COM1..COM4. Die Funktion operator() liefert
den aktuellen Wert der Speicherstelle als Typ UINT.

HC05MM8
Bei der Konstruktion von Klassen ist es, genauso
wie bei allen anderen Programmprojekten, ratsam, diese durch geeignete
Testsituationen einer Bewhrungsprobe zu unterziehen und die
Testprogramme bei jeder nderung der Klasse wieder auszufhren und
gegebenenfalls zu erweitern.

Im Beispiel HC05MM8.CPP werden alle definierten Operatoren ausgefhrt.
In den Beispielen wird das MEM-Typ-Objekt a auf die selten benutzte
Adresse des seriellen Ports 4 gelegt. In jeder Anweisungsgruppe
wird die Adresse mit dem Zuweisungsoperator operator=() initialisiert,
der Wert mit der Operator-Funktione operator() zurckgelesen.
Danach wird die Operation ausgefhrt und das Resultat ausgegeben.
Lediglich bei den Operatoren << und >> ergibt sich eine Zweideutigkeit
mit den ebenso berladenen Operatoren des Ausgabestreams cout, daher
werden die Ausdrcke a<<2 geklammert.

HC05MM9

#ifndef __TINY__
  #error must use TINY model
#else

Dieses Beispiel kompiliert nur im Speichermodell TINY, daher mu
dieses Modell in Optionen-Compiler-MemoryModel eingestellt werden.

  UINT *mem = new UINT[80];

Erzeugt einen Speicherbereich mit 160 Bytes; der Pointer mem zeigt
auf den Anfang.

  cout << "at " <<hex<< mem <<endl;

  cout << "contents (read by pointers):\n";

Der Speicher wird wie folgt belegt, um damit die Memory-Klassen MEM
und MEM_P testen zu knnen.

       +-------+
mem    | mem+2 |\
       +-------+ far-Pointer auf Adresse mem+2
mem+1  | DS    |/
       +-------+
mem+2  | 0xab  |
       +-------+

  *mem = (UINT)(mem+2);
  *(mem+1) = _DS;
  *(mem+2) = 0xab;

Diese Speicherbelegung wird zuerst durch Verendung des Pointer mem
angezeigt.

Danach werden drei MEM-Objekte off,seg und dat gebildet und die im
Speicher befindlichen Daten angezeigt:

  MEM<UINT> off(_DS,(UINT)mem);
  MEM<UINT> seg(_DS,(UINT)(mem+1));
  MEM<UINT> dat(_DS,(UINT)(mem+2));
  cout <<hex<< off.off() << ':' <<hex<< off() <<endl;
  cout <<hex<< seg.off() << ':' <<hex<< seg() <<endl;
  cout <<hex<< dat.off() << ':' <<hex<< dat() <<endl;

Dasselbe leistet auch ein MEM_P-Objekt m, das auf die
Adresse des Pointers initialisiert wird. Das MEM_P-Objekt
verwaltet daher zwei Pointer, die auch angezeigt werden:
einerseits der in der Ausgabe als 'Primary-Pointer'
angegebene, der mit seg() und off() ausgegeben wird, anderseits
der Pointer auf den er zeigt, dessen Wert mit pointseg() und
pointoff() ausgegeben wird. Die Daten, auf die der Pointer zeigt,
werden mit den berladenen Operatoren probehalber manipuliert.

  cout << "creating MEM_P object, reading back data\n";
  MEM_P<UINT> m(_DS,(UINT)mem);
  cout << "Primary-Pointer:";
  cout <<hex<< m.seg() << ':'
       <<hex<< m.off() <<endl;
  cout << "points to      :";
  cout <<hex<< m.pointseg() << ':'
       <<hex<< m.pointoff() <<endl;
  cout << "containing data: " <<hex<< m() <<endl;
  cout << "data ++, --, &0x0f, |0xf0" <<endl;
  ++m; cout << m() << ' ';
  --m; cout << m() << ' ';
  m&=0x0f; cout << m() << ' ';
  m|=0xf0; cout << m() << ' ';
  cout <<endl;

Dieselben Tests knnen auch mit einem dynamischen MEM_P-Objekt
durchgefhrt werden, wie die folgenden Zeilen zeigen:

  cout << "creating MEM_P object, reading back data\n";
  MEM_P<UINT> *mp = new MEM_P<UINT> (_DS,(UINT)mem);
  cout << "Primary-Pointer:";
  cout <<hex<< mp->seg() << ':'
       <<hex<< mp->off() <<endl;
  cout << "points to      :";
  cout <<hex<< mp->pointseg() << ':'
       <<hex<< mp->pointoff() <<endl;
  cout << "containing data: " <<hex<< (*mp)() <<endl;
  cout << "data ++, --, &0x0f, |0xf0" <<endl;
  ++(*mp); cout << (*mp)() << ' ';
  --(*mp); cout << (*mp)() << ' ';
  (*mp)&=0x0f; cout << (*mp)() << ' ';
  (*mp)|=0xf0; cout << (*mp)() << ' ';
  cout <<endl;
}
#endif

Das MEM_P-Objekt ist bei Behandlung von Datenstrukturen von DOS
vorteilhaft anwendbar und wird in einer spteren Folge,
die sich mit diesen Zusammenhngen beschftigt, weiter verwendet.

HC05MM10
Abschlieend werden der Beginn und das Ende der Tastatur-Warteschlange
ausgelesen.


MEMBIOS.HPP
MEMBIOS.HPP hat zwei Aufgaben: Einerseits stellt die Klasse
BIOS eine Spezialisierung der Klasse MEM dar, da fr das
Segment der Wert 0x0040 eingesetzt wird. Anderseits erspart
die Klasse dem Programmierer, das Wlzen von Tabellen, da der
Aufzhlungstyp BIOSA ein Verzeichnis aller BIOS-Adressen darstellt.
BIOSA hat folgenden Aufbau:

typedef enum BIOSA
{
  rs232_port_1       = 0x4100, // UINT
			   ^^...Offset der Variablen
			  ^.....Typ der Variablen
				1...UINT
				2...UCHAR
			 ^......Segment (4 oder 5)
  ....
  prn_scrn_stat      = 0x5200  // UCHAR
};

Dieser Aufzhlungstyp ist nicht nur gemeinsam mit der Klasse BIOS
- was nur in C++-Programmen ginge -
sondern auch unabhngig davon mit den Makros BIOSA_FP, MKB_FP und
BIOS_OFF anwendbar.

BIOS_FP erzeugt einen VOID-far-Pointer und erhlt als Argument
einen der BIOSA-Elemente. Beispiel:

UINT far *Timer_lo = (UINT far *)BIOSA_FP(timer_lo);

BIOS_OFF liefert die Offset-Adresse einer BIOSA-Variablen.

UINT timer_lo_offset = BIOS_OFF(timer_lo);

Die Klasse BIOS wurde gegenber der Klasse MEM um die
Mglichkeit erweitert, ein Objekt mit einem Offset oder
mit einer BIOSA-Variablen zu initialisieren.

Die Funktion changed() stellt fest, ob sich der Speicherwert
zwischen zwei Ablesungen verndert hat.

Die Klasse IBIOS geht davon aus, da der Inhalt der BIOS-Adresse ein
Pointer ist, hnlich wie bei der Klasse MEM_P. Die Klasse IBIOS wird
in erster Linie bei der Handhabung des Tastaturbuffers verwendet.

