Echtzeit-Timer mit Delphi im Kernel-Modus
Mit der »RealTime Suite« können Teile des Anwendungscodes auf der Kernel-Ebene (Ring 0 des Prozessors) ausgeführt werden. Dadurch kann man mit den Möglichkeiten der Kernel-Programmierung zum Beispiel Echtzeit-Timer programmieren, die unmittelbar auf der Kernel-Ebene gerufen werden. Für den Kernel-Modus ist ein C- oder C++-Compiler geeignet (z.B. Borland C++Builder oder Microsoft Visual C++) – es lässt sich aber auch die weit verbreitete Pascal-Programmierumgebung Delphi von Borland (Version 2 bis 7 bzw. ab 8 im Native-Mode) verwenden, um Ring-0-Code für die Kernel-Ebene zu erstellen. Es wird keine Assembler-Programmierung benötigt.
Im Beispiel soll eine Timer-Funktion mit einer Frequenz von 10 kHz gerufen werden, die nach 10000 Aufrufen die Anwendung mithilfe eines Events benachrichtigt.
Alle dafür erforderlichen Mechanismen stehen mit der Produktreihe »RealTime Suite« zur Verfügung. Dazu gehören zum Beispiel die Erstellung von Kernel-Funktionen in Delphi-Quellcode, die Bereitstellung von Shared Memory für den Datenaustausch zwischen der Callback-Funktion und den übrigen Teilen der Anwendung sowie die Erzeugung wie auch das Setzen von Events von der Timer-Funktion aus. Weiterhin lassen sich auch Threads auf einfache Weise erzeugen, in denen auf das Eintreffen des vereinbarten Events gewartet werden kann, ohne die übrige Anwendung zu blockieren.
Die einzelnen Schritte werden im folgenden Beispiel näher erläutert, wobei wir hier auf jegliche Fehlerauswertung verzichten. Dem Datenaustausch zwischen Kernel- und Anwendungsebene dient ein im Shared Memory erzeugtes Record, das nur einen Zähler und das Event-Handle enthält:
type Data = packed record count: UInt; hEvent: Handle; end; PData = ^Data;
Außerdem werden folgende spezielle Variablen definiert:
var pAppPtr: PData; // Shared Memory (Ring 3) pSysPtr: PData; // Shared Memory (Ring 0) hCallBack: Handle; // Callback-Handle hTimer: Handle; // Timer-Handle period: UInt; // Timer-Periode in 100-ns
Die ersten beiden Zeiger dienen der Verwaltung des Shared Memory, da sich der System- und der Anwendungs-Adressraum unterscheiden. Dieses Shared Memory wird benötigt, weil von der Kernel-Ebene aus normale Variablen der Anwendung nicht erreichbar sind. Ebenso kann man von der Anwendung aus nicht auf Kernel-Speicher zugreifen. Die anzufordernde Länge ist die Größe des Records ‘Data’:
err := KS_createSharedMem(@pAppPtr, @pSysPtr, 'MyShared', SizeOf(Data), 0);
Über den Anwendungszeiger kann direkt auf die Elemente des Records zugegriffen werden:
pAppPtr^.count := 0;
Zur Benachrichtigung der Anwendung wird hier ein spezielles Event erzeugt, da normale Win32-Events nicht von der Kernel-Ebene benutzbar sind. Das resultierende Event-Handle wird in die entsprechende Variable des Records ‘Data’ gepackt:
err := KS_createEvent(@pAppPtr^.hEvent, 'MyEventName', 0);
Die Timer-Periode soll 100 µs betragen, entsprechend der gewünschten Frequenz von 10 kHz. Da sämtliche Zeitangaben im Toolkit in 100-ns-Einheiten anzugeben sind, setzen wir die Periode auf den Wert 1000:
period := 1000;
Nun widmen wir uns der Callback-Funktion, die auf der Kernel-Ebene mit einer Frequenz von 10 kHz auszuführen ist. Sie soll hier der Einfachheit halber lediglich einen Zähler erhöhen sowie nach 10000 Aufrufen - also etwa einer Sekunde - ein zur Signalisierung vereinbartes Event setzen:
function callback(
var data: Data; var context: TimerUserContext):
Error; stdcall;
begin
data.count := data.count + 1;
if data.count > 10000 then
KS_setEvent(data.hEvent);
callback := KS_OK;
end;
Zu beachten ist, dass die Funktion in der Regel den Wert 0 (hier als KS_OK bezeichnet) zurückliefern muss. Mit anderen Werten kann eine beliebige Fehlerbedingung signalisiert und der gesamte Vorgang abgebrochen werden.
Zunächst erzeugen wir ein Handle für die Callback-Funktion ‘callback’. Dabei wird als Referenzparameter an die Funktion der Zeiger auf das Shared Memory angegeben, der für Zugriffe seitens der Kernel-Ebene vorgesehen ist (‘pSysPtr’).
err := KS_createCallBack(@hCallback, @callback, pSysPtr, KSF_ASYNC_EXEC);
Nun soll zunächst die Realtime-Engine aktiviert werden. Dazu ist eine Kalibrierung vorzunehmen sowie der Hardware-Timer auf 10 kHz einzustellen:
err := KS_calibrateMachineTime(0, 0); err := KS_setTimerResolution(period, 0);
Abschließend ist nur noch der Timer zu starten:
err := KS_createTimer(@hTimer, period, hCallBack, KSF_REALTIME_EXEC);
Da der Timer jetzt läuft, wird die oben beschriebene Callback-Funktion also mit einer Frequenz von 10 kHz auf der Kernel-Ebene gerufen. Nun wollen wir einen speziellen Thread erzeugen, der auf das Event wartet. Zunächst die Thread-Funktion, in der als erstes eine relativ hohe Priorität eingestellt wird:
function thread(var data: Data): Error; stdcall; begin KS_setThreadPrio(28); KS_waitForEvent(data.hEvent, 0, timeout); ... // Aktionen thread := KS_OK; end;
Die Timeout-Variable ist ein Abbruchkriterium, um eine unendlich lange Thread-Blockierung im Fehlerfall zu verhindern. Bei Zeitüberschreitung würde die Funktion KS_waitForEvent den Fehlercode KSERROR_WAIT_TIMEOUT liefern.
Während dem Ring-0-Timer die Systemadresse ‘pSysPtr’ des Shared-Datenbereiches übergeben wurde, erhält der Thread die entsprechende Anwendungsadresse ‘pAppPtr’:
err := KS_createThread(@thread, pAppPtr, nil);
Nach korrekter Ausführung der Funktion läuft der Thread an und wartet auf die Signalisierung von der Kernel-Ebene.
Fazit
Die Erstellung von Ring-0-Funktionen für die Kernel-Ebene ist auch mit Delphi auf einfache Weise und ohne Assembler-Code möglich. Dadurch sind auch mit Delphi hardwarenahe und zeitkritische Industrieanwendungen realisierbar.