Inhaltsverzeichnis
Betriebsystem für Bascom
Die im Text angesprochene Bascom-Library befindet sich im Community-Download-Bereich, zusammen mit einem einfachen Beispielprogramm.
Im Wesentlichen ist es die Aufgabe eine Betriebssystems, das Zusammenspiel der verschiedenen Programmteile, aus denen ja so eine µC-Applikation besteht, zu regeln. Die Nachteile des gewissen Overheads und der natürlich erforderlichen Restriktionen´sollten durch die Vorteile der Standardisierung und Modularisierung deutlich aufgewogen werden.
Bascom bietet für sehr viele AVR-intere Geräte und Peripherie Funktionen an, die oft ganz einfach durch "Config" Anweisungen initiiert werden können und dem Programmierer viel Detailentwicklungsarbeit abnehmen. Ein unschätzbarer Vorzug. Was aber immer wieder bleibt, ist das Problem des Zusammenwirkens dieser Komponenten und die gewünschte Mehrgleisigkeit, "Multithreading", des gesamten Projekts.
Es soll hier ein Konzept vorgestellt werden, das hauptsächlich in Hinblick auf eine Standardisierung der Rechner-Kommunikation entwickelt wurde. Also µC <-> PC und µC <-> Peripherie.
Das verwendete "Message-Queueing" ist natürlich keine neue Erfindung, jeder Windows-Programmierer kennt das. Das Problem war es, eine Form zu schaffen, die sich in Bascom so eingliedern läßt, daß auch die "normale" Verwendung des Compilers möglich ist. Nur an den Schnittstellen zu den Sys-Funktionen müssen die Konventionen eingehalten werden.
Anmerkung: Dieses Multi-tasking beruht natürlich darauf, daß so ein einzelnes "Task-Slice" kurz gehalten wird und keine wesentlichen Wait-States beinhaltet. Schließlich muß für die anderen ja auch was übrigbleiben. Auch ausführlich "FOR" Schleifen sollten zu Einzel-Slices umgewandelt werden, d.h. bei jedem Aufruf EIN Step. Was daraus folgt, ist, daß die einzelnen Tasks im Grunde als "State-Machine" arbeiten, z.B.
- FOR-Start
- FOR-Step
- FOR-Limit
Begriffe
- Units: Ein Programmteil, der sozusagen mitspielen will, wird als Unit od. "Einheit" bezeichnet. In den meisten Fällen wird so eine Unit die verschiedenen Funktionen umfassen, die für die Bedienung eines bestimmten Device od. "Gerät" erforderlich sind.
- Class/Ident: Es gibt Geräteklassen, die sich durch ein Set von "Methoden" definieren, die auf eines oder mehrere tatsächliche Geräte angewendet werden können. Anschauliches Beispiel: die Klasse "ADC", die für 8 Channels angewendet werden kann. Grundsätzlich werden die ADC-Channels ja völlig gleich behandelt, aber für jeden Kanal gelten unter Umständen verschiedene Parameter: Natürlich die Kanal-Nummer 0-7, aber auch Umrechnungsfaktoren und Grenzwerte
- Tags: Jeder Class/Ident-Ausprägung wird ein Tag ("Etikett") zugeordnet, das einen vom System verwalteten Bereich bezeichnet, der für die Kommunikation Unit <-> System erforderlich ist. Tags werden bei jedem Aufruf an das System verwednet und bei Aufrufen durch das System immer mitgeliefert.
- Message-Queueing: An sich können alle Funktionen eines Units "normal" verwendet, also aufgerufen werden. Doch jede Unit bietet wenigstens eine "Sub"-function an, die auch indirekt über Messages angesprochen werden kann und daher auch ein standardisiertes Format haben muß.
DECLARE SUB unit-entry ( Tag AS WORD, Byval cmd as BYTE, Byval Param as WORD )
Das ist auch die SUB, die beim Einlangen einer Nachricht über das Netzwerk aufgerufen wird.
- Tag ist das erwähnte Etikett, mit dem die SUB alle ihre Referenzen zu privaten Daten und individuellen Parametern (im Flash Speicher) verwaltet.
- Cmd ist eine Aufrufspezifischer Code, der über die Art des Aufrufes Auskunft gibt. Einige Codes sind fest durch das System vergeben, andere können beliebig von der Geräteklasse für spezielle Befehle festgelgt werden
- Param ist ein Beiwert zu CMD, der daher völlig unterschiedliche Bedeutung haben kann. (Beim Netzwerk-Nachrichten zeigt er auf die eingelangten Daten, die von der Unit zu bearbeiten sind).
TAG-Referenzen
Das System findet durch den Tag die zugeordnete SUB-Routine (Vector). Die Unit hingegen findet durch die System-Function "Unit_REF()" ihre privaten Daten, und kann auch durch "Unit_parbyte(tag), Unit_parword(tag),Unit_parlong(tag),Unit_parsing(tag)" ihre individuellen Parameter abfragen.
Die ersten beiden Bytes der Flash-Parameter sind genormt und beinhalten den Class-Code und Identifier der Unit. Die übrigen sind vom System nicht festgelegt und können beliebige Werte beinhalten.
Message-Queue
- Durch msg_enqueue() wird ein indirekter Aufruf in die Queue gestellt. Es gibt keine Festlegung darüber, wann und wo dieser Call erfolgen kann.
- Die Abarbeitung erfolgt FIFO durch msg_dequeue(). Bei jedem Aufruf wird EINE Message (wenn eine vorhanden ist) abgearbeitet. Auch für diesen Aufruf gibt es keinen zwingenden Platz, doch ist es zweckmäßig und empfehlenswert, ihn nur einmal an zentraler Stelle (DO..LOOP) zu tätigen.
Anmerkung: Enqueue() und Dequeue() führen sowohl Sub-Vector als auch Tag. "Vector" hat Vorrang, wenn er aber nicht angegeben wird (NULL), wird eben der Standard-Unit-Vector verwendet. Sonderfall: Bei Angabe von "Vector" ist die Befüllung von TAG nicht zwingend. Dadurch kann auch jede x-beliebige sub-function aufgerufen werden, nur das Declare-Format MUSS stimmen. ("TAG" ist beim Aufruf dann eben NULL).
Initialisierung
- Generell:
Die Library "mysyslib.lbx" muß in das Bascom-Library-Directory gestellt werden.
Const Max_c_unit = 4 Const Max_c_msg = 16 Const rx_buffersize = 24 $INCLUDE "mysyslib.bas"
Hier erfolgen die Definitionen für die Library
- Max_c_unit gibt an, wieviele Units maximal verwendet werden können.
- Max_c_msg gibt an, wieviele Messages gleichzeitig verwendet werden können.
Ein-queuen einer Message
Declare Function Msg_enqueue(Byval Vector As Word , Byval Tag As Word , Byval Cmd As Byte , Byval Param As Word) As Byte
Dim Enqresult As Byte Enqresult = Msg_enqueue(0, Tag, Cmd, Param)
Hier wurde der Call-Vector NULL angegeben, was der Normalfall ist. Daher wird dann auch der normale Unit-Entry verwendet.
Anmerkung: Das Enqueueing ist so abgesichtert, daß es auch in einer ISR verwendet werden kann, was ja auch eine typische Anwendung darstellt:
Abarbeiten der Message-Queue
Declare Sub Msg_dequeue(Byval Cmd As Byte , Byval Param As Word)
Klassisch und zweckmäßig ist das De-Queueing an zentraler Stelle, im Main- DO..LOOP
Do Call Msg_dequeue(0 , 0 , 0) Loop
Timed-Message-Queue
Hier handelt es sich um Queue-Entries, die verzögert durchgeführt werden. Param wird dann für die Angabe des Zählers verwendet.
Enqresult = Time_entry(0, Tag, Cmd, Counter)
Damit das funktioniert, muß natürlich ein Timer laufen. Vom Verfasser wird gerne der Timer0 für ein Millisekunden-Intervall eingestellt. Das heißt dann, daß der genannte Zähler in mS Einheiten heruntergezählt wird
Config Timer0 = Timer , Prescale = 64 'Timer 1mS Const Tmr_c_preload = 131 On Timer0 Interrupt_ticker ' Timer for Timer Queue
Interrupt_ticker: Timer0 = Tmr_c_preload Gosub Time_scan Return
Bei jedem Aufruf von "Time_scan" wird bei allen Timer-Einträgen eins heruntergezählt. Bei null wird dann der Queue-Eintrag aktiv gesetzt.
Anmerkung: Wenn eine Unit-Sub regelmäßig immer wieder aufgerufen werden will, muß sie jedesmal einen neuen Timer-entry aufsetzen. Das ist derzeit so gelöst, weil sonst zwei Arten Timer-Entries geführt werden müßten: Mit auto-repeat und single-shot. Da wird man in folgenden Versionen noch weitersehen.
Anlegen einer Unit
Als Beispiel eine eigentlich völlig sinnlose Klasse, die nur demonstrieren soll, wie das Ganze funktioniert
- Definitionen und Parameter
Const Cls_any = &H20
Irgendein Symbol, das allerdings in er gesamten µC Applikation eindeutig sein muß, wird als Klassen-Code festgelegt.
Declare Sub Anyclass(byval Tag As Word , Byval Cmd As Byte , Byval Param As Word)
Das ist der default-Unit-Entry
An einer geeigneten Stelle (nach "END") werden die Unit-Paramter deklariert.
Any_par: Data Cls_any , 1 , &H78 , 16255% , 300000& , 3.55!
Obligat als erstes der Klassencode, dann der Identifier. Der Rest ist eben irgendwas, was diese eine konkrete Unit zum arbeiten benötigt.
- Eintragen in der Tabelle
Natürlich NACH Unit-Init()
Dim Par_addr As Word 'Hilfsfeld Dim Anytag As Word Anytag = Unit_new(unitbase)
Die Funktion gibt den eindeutigen TAG der Unit zurück. Jetzt muß die Unit ihre Werte eintragen. Für den Aufruf übergeben wir die Adresse der Parameter
Par_addr = Loadlabel(any_par) ' individial parameter Call Anyclass(mytag , Cmd_setup , Par_addr) 'CMD_SETUP ist festgelegt
- Setup der Unit
Das macht die Unit am besten selbst, da sie durch die mitgelieferten Paramter modifizierbar ist.
'----------------------------------------------- ' Class ANY '------------------------------------------------- ' private class data '------------------------------------------------- Const Max_c_any = 2 Dim Anyidx As Byte Dim Any_bck(max_c_any) As Word Dim Any_dta(max_c_any) As Byte
WENN die Klasse /Unit private Daten im SRAM braucht, muß sie sie natürlich dimensionieren. Und zwar am besten in Tabellenform, das kommt Bascom entgegen. (Max_c_any ist die maximale Anzahl der möglichen Units dieser Klasse)
Mögliches Grundmuster der Funktion, die anhand CMD entscheidet, was zu tun ist
'--------------------------------------------------------------------------- Sub Anyclass(byval Tag As Byte , Byval Cmd As Byte , Byval Param As Word) Select Case Cmd Case Cmd_setup: Case Cmd_show: Case Else: Print Str(tag) ; " CMD1:" ; Str(cmd) ; " PAR1:" ; Str(param) End Select End Sub
Wenn die Klasse irgendein Command nicht kennt, zeigt sie es hier im Beispiel einfach an und macht sonst nichts.
Wichtig ist CMD_SETUP, das Setzen des Sprungzieles und der Parameter
Local Par_word As Word Par_word = Loadlabel(anyclass) Call Unit_vec_set(tag , Par_word) Call Unit_par_set(tag , Param)
Bascom läßt eine direkte Angabe eine Sprungzieles nicht zu. Daher der Umweg über eine word-variable. In dem Beispiel wird einfach die eigene Adresse, also der Name der SUB als Unit-Vector eingetragen. Die Unit-Parameter vom FLASH werden einfach direkt aus dem Aufruf übernommen. Gibt es keine Parameter(NULL), macht das zwar nichts, aber dann kann die Unit nicht von den Kommunikations-Funktionen gefunden werden.
Nun wird ein privates Datenbereich reserviert. Der Klassenspezifische Index wird (bei jedem setup) erhöht und als Datenreferenz in der Unit-Tabelle eingetragen
Incr Anyidx Call Unit_ref_set(tag , Anyidx) Any_bck(anyidx) = Tag Any_dta(anyidx) = 0
Die Klasse schreibt in ihre privaten Daten den Unit-Tag als Backlink dazu, damit sie auch von dort aus zu ihrem Tag findet. Das ist bei ISR- Funktionen oft nötig. any_dta(anyidx) = 0 ist eben irgendwas.
Ab nun ist die Unit im System eingetragen und verwendbar.
Als Beispiel ein anderes Command: CMD_SHOW
Local Par_class As Byte Local Par_ident As Byte Local Par_devad As Byte Local Par_word As Word Local Par_long As Long Local Par_sing As Single Case Cmd_show: Par_class = Unit_parbyte(tag , 0) ' Der Klassencode steht immer an der ersten Stelle der Parameter Par_ident = Unit_parbyte(tag , 1) ' ebenso Ident Par_devad = Unit_parbyte(tag , 2) ' die Parameter werden einfach geholt und angezeigt. Par_word = Unit_parword(tag , 3) ' das Daten-Offset muß man wissen, auch was dann dort steht Par_long = Unit_parlong(tag , 5) ' es ist sicher sinnvoll, "CONST" zu definieren Par_sing = Unit_parsing(tag , 9) Print "Class/Id:" ; Str(par_class) ; "/" ; Str(par_ident) ; " Device:0x" ; Hex(par_devad) Print " word:" ; Str(par_word) ; " Long:" ; Str(par_long) ; " Single:" ; Str(par_sing)
Aufrufbeispiele
'------------ AUFBAU----------------------------- Anytag = Unit_new(unitbase) Par_addr = Loadlabel(any_par) ' individial parameter1 Call Anyclass(mytag , Cmd_setup , Par_addr) Anytag = Unit_new(unitbase) Par_addr = Loadlabel(any_par2) ' individial parameter2 Call Anyclass(mytag , Cmd_setup , Par_addr) '------------ CALLS----------------------------- ' suchen des Tags nach Class und Ident Anytag = Unit_find_clsid(Cls_any , 1) ' Direkter Aufruf Call Anyclass(anytag , Cmd_show , 0) DIM Wrk_idx AS BYTE ' Indirekter Aufruf über die Queue Wrk_idx = Msg_enqueue(0 , Anytag , Cmd_show , 0) ' Auswerten der Queue Call Msg_dequeue(0 , 0 , 0) END Any_par: Data Cls_any , 1 , &H78 , 16255% , 300000& , 3.55! Any_par2: Data Cls_any , 2 , &H44 , 255% , 250000& , 1.44!