Die ganze Wahrheit über RTOS. Artikel 30. Initialisierungs- und Startverfahren für Nucleus SE



Jedes Betriebssystem verfügt über einen bestimmten Startmechanismus. Das Funktionsprinzip dieses Mechanismus ist für jedes System unterschiedlich. Normalerweise sagen sie, dass das System bootet (Eng. Boot), dies ist eine Abkürzung für "Bootstrap", was sich auf den Ausdruck "sich mit den Bootstraps über einen Zaun ziehen" bezieht. wie das System unabhängig von einem Zustand, in dem der Speicher voller Leere ist ( Anmerkung des Übersetzers: wenn absolut genau, dann Müll ), zu einer stabilen Programmausführung übergeht. Traditionell wird ein kleiner Teil des Programms in den Speicher geladen und kann im ROM gespeichert werden. In der Vergangenheit konnte die Eingabe über die Schalter an der Vorderseite des Computers erfolgen. Dieser Bootloader hat ein komplexeres Bootprogramm gelesen, das das Betriebssystem bereits geladen hat. Heute startet der Desktop-Computer wie folgt: Der BIOS-Code sucht nach Geräten (Festplatten, CD-ROMs, USB-Sticks), von denen Sie booten können, und anschließend startet das Betriebssystem.

Auf ähnliche Weise kann auch das Betriebssystem für eingebettete Systeme initialisiert werden. Tatsächlich werden eingebettete Betriebssysteme geladen, die auf der Basis von Desktop-Betriebssystemen entwickelt wurden. In den meisten "klassischen" RTOS wird jedoch eine viel einfachere (und daher schnellere) Methode verwendet.

Das Betriebssystem ist Teil der Software. Wenn sich diese Software bereits im Speicher befindet (z. B. in der einen oder anderen Form des ROM), müssen Sie nur sicherstellen, dass die Folge der CPU-Befehle nach dem Zurücksetzen mit der Ausführung des Betriebssysteminitialisierungscodes endet. So funktionieren die meisten RTOS, einschließlich Nucleus SE ( Anmerkung des Übersetzers: Dies gilt auch für unser RTOS MAX ).

Die meisten eingebetteten Softwareentwicklungstools enthalten den erforderlichen Startcode für das Zurücksetzen der CPU und die Übertragung der Steuerung an die Entry Point-Funktion in der main () -Funktion. Der weiterverteilbare Nucleus SE-Code behandelt diesen Prozess nicht, da er so portabel wie möglich sein muss. Stattdessen enthält es die Funktion main () , die die Steuerung der CPU übernimmt und das Betriebssystem initialisiert und startet. Diese Funktion wird nachstehend ausführlich erläutert.

Frühere Artikel in der Reihe:

Speicherinitialisierung


Deklarationen aller statischen Variablen im Nucleus SE-Code beginnen mit dem ROM- oder RAM- Präfix, um anzugeben, wo sie sich befinden sollen. Diese beiden Direktiven #define sind in der Datei nuse_types.h definiert und müssen unter Berücksichtigung der Besonderheiten der verwendeten Entwicklungstools (Compiler und Linker) konfiguriert werden. Normalerweise sollte das ROM vom Typ const sein ( Anmerkung des Übersetzers: Nach meiner Erfahrung ist const nicht immer genug, statisch ist besser ) und RAM ist ein leerer Wert.

Alle ROM- Variablen werden statisch initialisiert, was logisch ist. RAM- Variablen werden nicht statisch initialisiert (da dies nur mit bestimmten Toolboxen funktioniert, die so konfiguriert sind, dass sie automatisch vom ROM in den RAM kopieren). Ein expliziter Initialisierungscode ist in der Anwendung enthalten und wird nachstehend ausführlich beschrieben.

Nucleus SE speichert keine „konstanten“ Daten im RAM, was in kleinen Systemen normalerweise Mangelware ist. Anstatt komplexe Datenstrukturen zur Beschreibung von Kernelobjekten zu verwenden, werden Tabellensätze (Arrays) verwendet, die je nach Bedarf leicht in ROM oder RAM abgelegt werden können.

Main () Funktion


Das Folgende ist der vollständige Code für die main () - Funktion von Nucleus SE:

void main(void) { NUSE_Init(); /* initialize kernel data */ /* user initialization code here */ NUSE_Scheduler(); /* start tasks */ } 

Die Reihenfolge der Operationen ist recht einfach:

  • Zunächst wird die Funktion NUSE_Init () aufgerufen . Es initialisiert alle Nucleus SE-Datenstrukturen und wird nachstehend ausführlich beschrieben.
  • Anschließend kann der Benutzer einen beliebigen Anwendungsinitialisierungscode einfügen, der ausgeführt wird, bevor der Taskplaner gestartet wird. Weitere Informationen darüber, was mit diesem Code erreicht werden kann, finden Sie weiter unten in diesem Artikel.
  • Schließlich wird der Nucleus SE-Scheduler ( NUSE_Scheduler () ) gestartet . Dies wird später in diesem Artikel ebenfalls ausführlich erläutert.

NUSE_Init () Funktion


Diese Funktion initialisiert alle Nucleus SE-Kernelvariablen und Datenstrukturen.

Unten finden Sie den vollständigen Funktionscode:
 void NUSE_Init(void) { U8 index; /* global data */ NUSE_Task_Active = 0; NUSE_Task_State = NUSE_STARTUP_CONTEXT; #if NUSE_SYSTEM_TIME_SUPPORT NUSE_Tick_Clock = 0; #endif #if NUSE_SCHEDULER_TYPE == NUSE_TIME_SLICE_SCHEDULER NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS; #endif /* tasks */ #if ((NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER) || NUSE_SIGNAL_SUPPORT || NUSE_TASK_SLEEP || NUSE_SUSPEND_ENABLE || NUSE_SCHEDULE_COUNT_SUPPORT) for (index=0; index<NUSE_TASK_NUMBER; index++) { NUSE_Init_Task(index); } #endif /* partition pools */ #if NUSE_PARTITION_POOL_NUMBER != 0 for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++) { NUSE_Init_Partition_Pool(index); } #endif /* mailboxes */ #if NUSE_MAILBOX_NUMBER != 0 for (index=0; index<NUSE_MAILBOX_NUMBER; index++) { NUSE_Init_Mailbox(index); } #endif /* queues */ #if NUSE_QUEUE_NUMBER != 0 for (index=0; index<NUSE_QUEUE_NUMBER; index++) { NUSE_Init_Queue(index); } #endif /* pipes */ #if NUSE_PIPE_NUMBER != 0 for (index=0; index<NUSE_PIPE_NUMBER; index++) { NUSE_Init_Pipe(index); } #endif /* semaphores */ #if NUSE_SEMAPHORE_NUMBER != 0 for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++) { NUSE_Init_Semaphore(index); } #endif /* event groups */ #if NUSE_EVENT_GROUP_NUMBER != 0 for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++) { NUSE_Init_Event_Group(index); } #endif /* timers */ #if NUSE_TIMER_NUMBER != 0 for (index=0; index<NUSE_TIMER_NUMBER; index++) { NUSE_Init_Timer(index); } #endif } 


Zunächst werden globale Variablen initialisiert:
  • NUSE_Task_Active - Index der aktiven Aufgabe, initialisiert auf Null; später kann dies den Scheduler ändern.
  • NUSE_Task_State - wird mit dem Wert NUSE_STARTUP_CONTEXT initialisiert, wodurch die Funktionalität der API für jeden nachfolgenden Anwendungsinitialisierungscode eingeschränkt wird.
  • Wenn die Systemzeitunterstützung aktiviert ist, wird NUSE_Tick_Clock auf Null gesetzt.
  • Wenn der Time Slice Scheduler aktiviert ist, wird NUSE_Time_Slice_Ticks der konfigurierte Wert NUSE_TIME_SLICE_TICKS zugewiesen .

Dann werden die Funktionen aufgerufen, um die Kernelobjekte zu initialisieren:

  • NUSE_Init_Task () wird aufgerufen, um die Datenstrukturen jeder Aufgabe zu initialisieren. Dieser Aufruf wird nur übersprungen, wenn der Scheduler "Run to Completion" verwendet wird und die Signale, die Taskpause und der Planungszähler nicht konfiguriert sind (da diese Funktionskombination dazu führt, dass diese Taskstrukturen im RAM nicht vorhanden sind, wird keine Initialisierung durchgeführt).
  • NUSE_Init_Partition_Pool () wird aufgerufen, um jedes Partitionspoolobjekt zu initialisieren. Diese Aufrufe werden übersprungen, wenn keine Partitionspools konfiguriert sind.
  • NUSE_Init_Mailbox () wird aufgerufen, um jedes Postfachobjekt zu initialisieren. Diese Anrufe werden übersprungen, wenn keine Postfächer konfiguriert sind.
  • NUSE_Init_Queue () wird aufgerufen, um jedes Warteschlangenobjekt zu initialisieren. Diese Anrufe werden übersprungen, wenn keine Warteschlangen konfiguriert sind.
  • NUSE_Init_Pipe () wird aufgerufen, um jedes Kanalobjekt zu initialisieren. Diese Anrufe werden übersprungen, wenn keine Kanäle konfiguriert sind.
  • NUSE_Init_Semaphore () wird aufgerufen, um jedes Semaphorobjekt zu initialisieren. Diese Aufrufe werden übersprungen, wenn keine Semaphoren konfiguriert sind.
  • NUSE_Init_Event_Group () wird aufgerufen, um jedes Ereignisgruppenobjekt zu initialisieren. Diese Aufrufe werden übersprungen, wenn keine Ereignisgruppen konfiguriert sind.
  • NUSE_Init_Timer () wird aufgerufen, um jedes Timer-Objekt zu initialisieren. Diese Anrufe werden übersprungen, wenn keine Timer konfiguriert sind.

Aufgabeninitialisierung


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Task ():
 void NUSE_Init_Task(NUSE_TASK task) { #if NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER NUSE_Task_Context[task][15] = /* SR */ NUSE_STATUS_REGISTER; NUSE_Task_Context[task][16] = /* PC */ NUSE_Task_Start_Address[task]; NUSE_Task_Context[task][17] = /* SP */ (U32 *)NUSE_Task_Stack_Base[task] + NUSE_Task_Stack_Size[task]; #endif #if NUSE_SIGNAL_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Signal_Flags[task] = 0; #endif #if NUSE_TASK_SLEEP || NUSE_INCLUDE_EVERYTHING NUSE_Task_Timeout_Counter[task] = 0; #endif #if NUSE_SUSPEND_ENABLE || NUSE_INCLUDE_EVERYTHING #if NUSE_INITIAL_TASK_STATE_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Status[task] = NUSE_Task_Initial_State[task]; #else NUSE_Task_Status[task] = NUSE_READY; #endif #endif #if NUSE_SCHEDULE_COUNT_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Schedule_Count[task] = 0; #endif } 


Wenn der Run to Completion-Scheduler nicht konfiguriert wurde, wird der Kontextblock für die Task NUSE_Task_Context [task] [] initialisiert. Den meisten Elementen werden keine Werte zugewiesen, da sie allgemeine Maschinenregister darstellen, die beim Starten einer Aufgabe einen Zwischenwert haben sollten. Im Beispiel (Freescale ColdFire) der Nucleus SE-Implementierung (bei anderen Prozessoren ist der Mechanismus jedoch ähnlich) werden die letzten drei Einträge explizit festgelegt:

  • NUSE_Task_Context [task] [15] enthält das Statusregister ( SR , Statusregister) und hat den Wert der Anweisung #define NUSE_STATUS_REGISTER .
  • NUSE_Task_Context [Aufgabe] [16] enthält den Programmzähler ( PC , Programmzähler) und hat den Adresswert des Eingabepunkts des Aufgabencodes: NUSE_Task_Start_Address [Aufgabe] .
  • NUSE_Task_Context [Aufgabe] [17] enthält den Stapelzeiger ( SP , Stapelzeiger) und wird mit dem Wert initialisiert, der als Summe aus der Adresse des Aufgabenstapels ( NUSE_Task_Stack_Base [Aufgabe] ) und der Größe des Aufgabenstapels ( NUSE_Task_Stack_Size [Aufgabe] ) berechnet wird .

Wenn die Signalunterstützung aktiviert ist, werden Task-Signal-Flags ( NUSE_Task_Signal_Flags [Task] ) auf Null gesetzt.

Wenn die Task-Suspendierung aktiviert ist ( dh der API-Serviceaufruf NUSE_Task_Sleep () ), wird der Task-Timeout-Zähler ( NUSE_Task_Timeout_Counter [Task] ) auf Null gesetzt.

Wenn der Task-Suspend-Status aktiviert ist, wird der Task-Status ( NUSE_Task_Status [Task] ) initialisiert. Dieser Anfangswert wird vom Benutzer festgelegt (in NUSE_Task_Initial_State [Aufgabe] ), wenn die Unterstützung für den Anfangszustand der Aufgabe aktiviert ist. Andernfalls wird der Status NUSE_READY zugewiesen.

Wenn der Planungszähler aktiviert ist, wird der Aufgabenzähler ( NUSE_Task_Schedule_Count [Aufgabe] ) auf Null gesetzt.

Partitionspools initialisieren


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Partition_Pool () :

 void NUSE_Init_Partition_Pool(NUSE_PARTITION_POOL pool) { NUSE_Partition_Pool_Partition_Used[pool] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Partition_Pool_Blocking_Count[pool] = 0; #endif } 

Der "verwendete" Partitionspoolzähler ( NUSE_Partition_Pool__Partition_Used [Pool] ) wird auf Null gesetzt.

Wenn die Task-Sperre aktiviert ist, wird der Zähler für blockierte Aufgaben von Partitionspools ( NUSE_Partition_Pool_Blocking_Count [Pool]) auf Null gesetzt.

Postfächer initialisieren


Unten finden Sie den vollständigen Code für NUSE_Init_Mailbox () :

 void NUSE_Init_Mailbox(NUSE_MAILBOX mailbox) { NUSE_Mailbox_Data[mailbox] = 0; NUSE_Mailbox_Status[mailbox] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Mailbox_Blocking_Count[mailbox] = 0; #endif } 

Der Postfachdatenspeicher ( NUSE_Mailbox_Data [Postfach] ) wird auf Null gesetzt, und der Status ( NUSE_Mailbox_Status [Postfach] ) wird "nicht verwendet" (dh Null).

Wenn die Aufgabensperre aktiviert ist, wird der Zähler für blockierte Postfachaufgaben ( NUSE_Mailbox_Blocking_Count [Postfach] ) auf Null gesetzt.

Initialisierung der Warteschlange


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Queue () :

 void NUSE_Init_Queue(NUSE_QUEUE queue) { NUSE_Queue_Head[queue] = 0; NUSE_Queue_Tail[queue] = 0; NUSE_Queue_Items[queue] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Queue_Blocking_Count[queue] = 0; #endif } 

Zeiger auf den Anfang und das Ende der Warteschlange (tatsächlich sind dies die Indizes NUSE_Queue_Head [Warteschlange ] und NUSE_Queue_Tail [Warteschlange] ) werden Werte zugewiesen, die den Beginn des Datenbereichs der Warteschlangen angeben ( dh einen Wert von Null annehmen). Der Zähler in der Warteschlange ( NUSE_Queue_Items [Warteschlange] ) wird ebenfalls auf Null gesetzt.

Wenn die Task-Sperre aktiviert ist, wird der Zähler für blockierte Warteschlangen-Tasks ( NUSE_Queue_Blocking_Count [Warteschlange] ) auf Null gesetzt.

Kanalinitialisierung


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Pipe () :

 void NUSE_Init_Pipe(NUSE_PIPE pipe) { NUSE_Pipe_Head[pipe] = 0; NUSE_Pipe_Tail[pipe] = 0; NUSE_Pipe_Items[pipe] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Pipe_Blocking_Count[pipe] = 0; #endif } 

Zeigern auf den Anfang und das Ende des Kanals (tatsächlich sind dies die Indizes - NUSE_Pipe_Head [Pipe] und NUSE_Pipe_Tail [Pipe] ) wird ein Wert zugewiesen, der den Beginn des Kanaldatenbereichs angibt (dh einen Nullwert annimmt). Der Kanalzähler ( NUSE_Pipe_Items [Pipe] ) wird ebenfalls auf Null gesetzt.

Wenn die Task-Sperre aktiviert ist, wird der Zähler für blockierte Task des Kanals ( NUSE_Pipe_Blocking_Count [Pipe] ) auf Null gesetzt.

Semaphorinitialisierung


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Semaphore () :

 void NUSE_Init_Semaphore(NUSE_SEMAPHORE semaphore) { NUSE_Semaphore_Counter[semaphore] = NUSE_Semaphore_Initial_Value[semaphore]; #if NUSE_BLOCKING_ENABLE NUSE_Semaphore_Blocking_Count[semaphore] = 0; #endif } 

Der Semaphorzähler ( NUSE_Semaphore_Counter [Semaphor] ) wird mit dem vom Benutzer festgelegten Wert ( NUSE_Semaphore_Initial_Value [Semaphor] ) initialisiert.

Wenn die Task-Sperre aktiviert ist, wird der Task-Zähler für gesperrte Semaphore ( NUSE_Semaphore_Blocking_Count [Semaphor] ) auf Null gesetzt.

Ereignisgruppen initialisieren


Das Folgende ist der vollständige Code für die Funktion NUSE_Init_Event_Group () :

 void NUSE_Init_Event_Group(NUSE_EVENT_GROUP group) { NUSE_Event_Group_Data[group] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Event_Group_Blocking_Count[group] = 0; #endif } 

Die Flags der Ereignisgruppe werden zurückgesetzt, d.h. NUSE_Event_Group_Data [Gruppe] wird ein Nullwert zugewiesen.

Wenn die Task-Sperre aktiviert ist, wird der Zähler für blockierte Tasks der Ereignisflag-Gruppe ( NUSE_Event_Group_Blocking_Count [Gruppe] ) auf Null gesetzt.

Timer-Initialisierung


Unten finden Sie den vollständigen Code von NUSE_Init_Timer () .

 void NUSE_Init_Timer(NUSE_TIMER timer) { NUSE_Timer_Status[timer] = FALSE; NUSE_Timer_Value[timer] = NUSE_Timer_Initial_Time[timer]; NUSE_Timer_Expirations_Counter[timer] = 0; } 

Der Status des Timers ( NUSE_Timer_Status [Timer] ) wird auf "nicht verwendet" gesetzt, d. H. FALSCH

Der Countdown-Wert ( NUSE_Timer_Value [Timer ]) wird durch den vom Benutzer festgelegten Wert ( NUSE_Timer_Initial_Time [Timer ]) initialisiert.

Der Abschlusszähler ( NUSE_Timer_Expirations_Counter [Timer] ) wird auf Null gesetzt.

Anwendungscode initialisieren


Nachdem die Nucleus SE-Datenstrukturen initialisiert wurden, kann der für die Initialisierung der Anwendung verantwortliche Code ausgeführt werden, bevor die Aufgabe gestartet wird. Diese Funktion kann für die folgenden Aufgaben hilfreich sein:

  • Initialisierung von Anwendungsdatenstrukturen. Das explizite Füllen von Datenstrukturen ist im Vergleich zur automatischen Initialisierung statischer Variablen einfacher zu verstehen und zu debuggen.
  • Zuweisung von Kernelobjekten. Da alle Kernelobjekte in der Erstellungsphase statisch erstellt und anhand von Indexwerten identifiziert werden, kann es hilfreich sein, einen „Eigentümer“ zuzuweisen oder die Verwendung dieser Objekte zu bestimmen. Dies kann mit der Direktive #define erfolgen. Wenn jedoch mehrere Instanzen von Aufgaben vorhanden sind, ist es besser, Objektindizes über globale Arrays zuzuweisen (indiziert nach Aufgaben-ID).
  • Geräteinitialisierung. Dies kann bei der Erstinstallation von Peripheriegeräten hilfreich sein.

Natürlich können viele dieser Ziele vor der Initialisierung von Nucleus SE erreicht werden, aber der Vorteil am Speicherort des Anwendungscodes besteht darin, dass Sie jetzt die Kerneldienste (API-Aufrufe) verwenden können. Beispielsweise kann eine Warteschlange oder ein Postfach mit Daten gefüllt sein, die beim Start der Aufgabe verarbeitet werden müssen.

API-Aufrufe haben eine Einschränkung: Alle Aktionen, die normalerweise zur Aktivierung des Schedulers führen, sind verboten (z. B. Pausen- / Blockierungsaufgaben). Die globale Variable NUSE_Task_State wurde auf NUSE_STARTUP_CONTEXT gesetzt , um diese Einschränkung anzuzeigen .

Starten Sie den Scheduler


Nach Abschluss der Initialisierung muss nur noch der Scheduler ausgeführt werden, um die Ausführung der Anwendungscode-Tasks zu starten. Die Konfiguration des Schedulers und die Arbeit verschiedener Schedulertypen wurde in einem der vorherigen Artikel ( Nr. 9 ) ausführlich beschrieben, sodass hier nur eine kurze Zusammenfassung erforderlich ist.
Die Reihenfolge der Schlüsselschritte ist wie folgt:

  • Setzen der globalen Variablen NUSE_Task_State auf NUSE_TASK_CONTEXT .
  • Wählen Sie den Index der ersten auszuführenden Aufgabe aus. Wenn die Unterstützung für die erste Aufgabe aktiviert ist, wird die Suche nach der ersten abgeschlossenen Aufgabe ausgeführt, andernfalls wird ein Nullwert verwendet.
  • Der Scheduler heißt - NUSE_Scheduler () .

Was genau im letzten Schritt passiert, hängt davon ab, welcher Scheduler ausgewählt ist. Bei Verwendung des Schedulers "Run to Completion" wird ein Planungszyklus gestartet und Aufgaben werden nacheinander aufgerufen. Bei Verwendung anderer Scheduler wird der Kontext der ersten Aufgabe geladen und die Steuerung auf die Aufgabe übertragen.

Der folgende Artikel beschreibt die Diagnose und Fehlerprüfung.

Über den Autor: Colin Walls ist seit über dreißig Jahren in der Elektronikindustrie tätig und widmet sich die meiste Zeit der Firmware. Heute ist er Firmware-Ingenieur bei Mentor Embedded (einer Abteilung von Mentor Graphics). Colin Walls spricht häufig auf Konferenzen und Seminaren, Autor zahlreicher technischer Artikel und zweier Bücher über Firmware. Lebt in Großbritannien. Colins professioneller Blog , E-Mail: colin_walls@mentor.com.

Source: https://habr.com/ru/post/de460617/


All Articles