Elbrus besteigen - Aufklärung im Kampf. Technischer Teil 2. Interrupts, Ausnahmen, Systemtimer

Wir erkunden Elbrus weiter, indem wir Embox darauf portieren .

Dieser Artikel ist Teil zwei eines technischen Artikels zur Elbrus-Architektur. Der erste Teil befasste sich mit Stapeln, Registern und so weiter. Bevor Sie diesen Teil lesen, empfehlen wir Ihnen, den ersten Teil zu studieren, da er sich mit den grundlegenden Dingen der Elbrus-Architektur befasst. Dieser Abschnitt konzentriert sich auf Timer, Interrupts und Ausnahmen. Dies ist wiederum keine offizielle Dokumentation. Wenden Sie sich dazu an die Entwickler von Elbrus am ICST .
Um zum Studium von Elbrus zu gelangen, wollten wir den Timer schnell starten, da präventives Multitasking, wie Sie wissen, ohne ihn nicht funktioniert. Um dies zu tun, schien es genug zu sein, den Interrupt-Controller und den Timer selbst zu implementieren, aber wir stießen auf unerwartete erwartete Schwierigkeiten, wohin würden wir ohne sie gehen. Sie begannen nach Debugging-Funktionen zu suchen und stellten fest, dass die Entwickler sich darum kümmerten, indem sie mehrere Befehle einführten, mit denen Sie verschiedene Ausnahmesituationen auslösen können. Beispielsweise können Sie eine Ausnahme einer besonderen Art über die Register PSR (Prozessorstatusregister) und UPSR (Benutzerprozessorstatusregister) generieren. Für PSR ist das Bit exc_last_wish das Ausnahmeflag exc_last_wish, wenn von der Prozedur zurückgekehrt wird, und für UPSR ist exc_d_interrupt das Flag für verzögerte Unterbrechungen, das durch die VFDI-Operation erzeugt wird (Flag für verzögerte Unterbrechungen prüfen).

Der Code lautet wie folgt:

#define UPSR_DI (1 << 3) /*   .h  */ rrs %upsr, %r1 ors %r1, UPSR_DI, %r1 /* upsr |= UPSR_DI; */ rws %r1, %upsr vfdi /*      */ 

Gestartet. Aber nichts ist passiert, das System hing irgendwo, nichts wurde an die Konsole ausgegeben. Eigentlich haben wir dies gesehen, als wir versucht haben, den Interrupt über den Timer zu starten, aber dann gab es viele Komponenten, und hier war klar, dass etwas den sequentiellen Fortschritt unseres Programms unterbrach und die Steuerung auf die Ausnahmetabelle übertragen wurde (in Bezug auf die Elbrus-Architektur ist es richtiger, nicht über die Tabelle zu sprechen Unterbrechungen und über die Ausnahmetabelle). Wir gingen davon aus, dass der Prozessor dennoch eine Ausnahme auslöste, aber es gab einen „Müll“, bei dem er die Kontrolle übertrug. Wie sich herausstellte, überträgt er die Kontrolle genau an die Stelle, an der wir das Embox-Image abgelegt haben, was bedeutet, dass es einen Einstiegspunkt gab - die Eingabefunktion.

Zur Überprüfung haben wir Folgendes durchgeführt. Startete einen Zähler für Einträge in entry (). Anfänglich beginnen alle CPUs mit ausgeschalteten Interrupts, gehen in entry (), danach lassen wir nur einen Kern aktiv, der Rest geht in eine Endlosschleife. Nachdem der Zähler der Anzahl der CPUs entspricht, betrachten wir alle nachfolgenden Treffer im Eintrag als Ausnahmen. Ich erinnere Sie daran, dass es vorher so war, wie es in unserem allerersten Artikel über Elbrus beschrieben wurde

  cpuid = __e2k_atomic32_add(1, &last_cpuid); if (cpuid > 1) { /* XXX currently we support only single core */ while(1); } /* copy of trap table */ memcpy((void*)0, &_t_entry, 0x1800); kernel_start(); 

Hab das getan

  /* Since we enable exceptions only when all CPUs except the main one * reached the idle state (cpu_idle), we can rely that order and can * guarantee exceptions happen strictly after all CPUS entries. */ if (entries_count >= CPU_COUNT) { /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); ... } /* It wasn't exception, so we decide this usual program execution, * that is, Embox started on CPU0 or CPU1 */ e2k_wait_all(); entries_count = __e2k_atomic32_add(1, &entries_count); if (entries_count > 1) { /* XXX currently we support only single core */ cpu_idle(); } e2k_kernel_start(); } 

Und schließlich sahen wir die Reaktion auf das Eingeben des Interrupts (nur mit Hilfe von printf haben wir eine Zeile gedruckt).

Hier ist zu erklären, dass wir anfangs in der ersten Version erwartet hatten, die Ausnahmetabelle zu kopieren, aber erstens stellte sich heraus, dass sie an unserer Adresse war, und zweitens konnten wir nicht die richtige Kopie erstellen. Ich musste Linker-Skripte, den Einstiegspunkt in das System und den Interrupt-Handler neu schreiben, das heißt, ich brauchte den Assembler-Teil etwas später.

So sieht nun der Teil des geänderten Teils des Skript-Linkers aus:

 .text : { _start = .; _t_entry = .; /* Interrupt handler */ *(.ttable_entry0) . = _t_entry + 0x800; /* Syscall handler */ *(.ttable_entry1) . = _t_entry + 0x1000; /* longjmp handler */ *(.ttable_entry2) . = _t_entry + 0x1800; _t_entry_end = .; *(.e2k_entry) *(.cpu_idle) /* text */ } 

Das heißt, wir haben den Eintragsabschnitt für die Ausnahmetabelle entfernt. Dort befindet sich auch der Abschnitt cpu_idle für die nicht verwendeten CPUs.

So sieht die Eingabefunktion für unseren aktiven Kernel aus, auf dem Embox ausgeführt wird:

 static void e2k_kernel_start(void) { extern void kernel_start(void); int psr; /*    CPU “” */ while (idled_cpus_count < CPU_COUNT - 1) ; ... /*     ,     */ e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE); kernel_start(); /*   Embox */ } 

Nun, gemäß der VFDI-Anweisung wurde eine Ausnahme ausgelöst. Jetzt müssen Sie seine Nummer erhalten, um sicherzustellen, dass dies die richtige Ausnahme ist. Zu diesem Zweck verfügt Elbrus über TIR-Interrupt-Informationsregister (Trap Info-Register). Sie enthalten Informationen zu den letzten Befehlen, dh zum letzten Teil der Ablaufverfolgung. Trace sammelt sich während der Programmausführung und "friert" ein, wenn ein Interrupt eingegeben wird. TIR umfasst die Teile Low (64 Bit) und High (64 Bit). Das niedrige Wort enthält die Ausnahmeflags, und das hohe Wort enthält einen Zeiger auf den Befehl, der zur Ausnahme geführt hat, und die aktuelle TIR-Nummer. Dementsprechend ist in unserem Fall exc_d_interrupt das 4. Bit.

Hinweis Wir haben immer noch einige Missverständnisse hinsichtlich der Tiefe (Anzahl) der TIRs. Die Dokumentation enthält:
„Die TIR-Speichertiefe, dh die Anzahl der Trap-Info-Register, wird bestimmt
TIR_NUM-Makro, das der Anzahl der Prozessor-Pipeline-Stufen entspricht, die für erforderlich sind
Ausgabe aller möglichen besonderen Situationen. TIR_NUM = 19; ”
In der Praxis sehen wir die Tiefe = 1 und verwenden daher nur das TIR0-Register.

Die Spezialisten des MCST haben uns erklärt, dass alles korrekt ist und es nur TIR0 für „genaue“ Interrupts geben wird, aber für andere Situationen kann es etwas anderes geben. Aber da wir nur über Timer-Interrupts sprechen, stört uns das nicht.

Ok, jetzt schauen wir uns an, was erforderlich ist, um den Ausnahmebehandler korrekt einzugeben / zu beenden. Tatsächlich ist es notwendig, am Eingang zu speichern und die folgenden 5 Register am Ausgang wiederherzustellen. Drei Steuerübertragungsvorbereitungsregister sind ctpr [1,2,3], und zwei Zyklussteuerregister sind ILCR (Register der Anfangswerte des Zykluszählers) und LSR (Register des Zyklusstatus).

 .type ttable_entry0,@function ttable_entry0: setwd wsz = 0x10, nfx = 1; rrd %ctpr1, %dr1 rrd %ctpr2, %dr2 rrd %ctpr3, %dr3 rrd %ilcr, %dr4 rrd %lsr, %dr5 /* sizeof pt_regs */ getsp -(5 * 8), %dr0 std %dr1, [%dr0 + PT_CTRP1] /* regs->ctpr1 = ctpr1 */ std %dr2, [%dr0 + PT_CTRP2] /* regs->ctpr2 = ctpr2 */ std %dr3, [%dr0 + PT_CTRP3] /* regs->ctpr3 = ctpr3 */ std %dr4, [%dr0 + PT_ILCR] /* regs->ilcr = ilcr */ std %dr5, [%dr0 + PT_LSR] /* regs->lsr = lsr */ disp %ctpr1, e2k_entry ct %ctpr1 

Das ist alles, nachdem Sie den Ausnahmebehandler beendet haben, müssen Sie diese 5 Register wiederherstellen.

Wir machen das mit einem Makro:

 #define RESTORE_COMMON_REGS(regs) \ ({ \ uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \ ctpr3 = regs->ctpr3, lsr = regs->lsr, \ ilcr = regs->ilcr; \ /* ctpr2 is restored first because of tight time constraints \ * on restoring ctpr2 and aaldv. */ \ E2K_SET_DSREG(ctpr1, ctpr1); \ E2K_SET_DSREG(ctpr2, ctpr2); \ E2K_SET_DSREG(ctpr3, ctpr3); \ E2K_SET_DSREG(lsr, lsr); \ E2K_SET_DSREG(ilcr, ilcr); \ }) 

Es ist auch wichtig, nach der Wiederherstellung der Register nicht zu vergessen, die DONE-Operation aufzurufen (Rückkehr vom Hardware-Interrupt-Handler). Diese Operation ist insbesondere notwendig, um die unterbrochenen Steuerübertragungsoperationen korrekt zu verarbeiten. Wir machen das mit einem Makro:

 #define E2K_DONE \ do { \ asm volatile ("{nop 3} {done}" ::: "ctpr3"); \ } while (0) 

Tatsächlich kehren wir mit diesen beiden Makros direkt im C-Code vom Interrupt zurück.
  /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); RESTORE_COMMON_REGS(regs); E2K_DONE; 

Externe Interrupts


Beginnen wir mit der Aktivierung externer Interrupts. In Elbrus wird APIC (oder besser gesagt sein Analog) als Interrupt-Controller verwendet, Embox hatte diesen Treiber bereits. Daher war es möglich, einen System-Timer dafür abzurufen. Es gibt zwei Timer, von denen einer PIT sehr ähnlich ist, der andere LAPIC-Timer ist ebenfalls Standard, daher macht es keinen Sinn, darüber zu sprechen. Sowohl das als auch das sahen einfach aus, und das und das gab es bereits in Embox, aber der Treiber des LAPIC-Timers sah perspektivischer aus, abgesehen davon, dass die Implementierung des PIT-Timers uns nicht standardisierter erschien. Daher schien es einfacher zu vervollständigen. Darüber hinaus wurden in der offiziellen Dokumentation die Register APIC und LAPIC beschrieben, die sich geringfügig von den Originalen unterschieden. Sie mitzubringen macht keinen Sinn, wie Sie im Original sehen können.

Zusätzlich zum Zulassen von Interrupts in APIC müssen Sie die Interrupt-Behandlung über die PSR / UPSR-Register aktivieren. Beide Register haben Flags zum Aktivieren externer Interrupts und nicht maskierbarer Interrupts. ABER hier ist es sehr wichtig zu beachten, dass das PSR-Register lokal für die Funktion ist (dies wurde im ersten technischen Teil besprochen). Wenn Sie es in einer Funktion festlegen, wird es beim Aufrufen aller nachfolgenden Funktionen vererbt. Wenn Sie jedoch von der Funktion zurückkehren, kehrt es in den ursprünglichen Zustand zurück. Daher die Frage, aber wie man Interrupts verwaltet?

Wir verwenden die folgende Lösung. Mit dem PSR-Register können Sie die Verwaltung über UPSR aktivieren, das bereits global ist (was wir benötigen). Daher aktivieren wir die Steuerung über UPSR direkt (wichtig!) Vor der Embox-Core-Login-Funktion:

  /* PSR is local register and makes sense only within a function, * so we set it here before kernel start. */ asm volatile ("rrs %%psr, %0" : "=r"(psr) :); psr |= (PSR_IE | PSR_NMIE | PSR_UIE); asm volatile ("rws %0, %%psr" : : "ri"(psr)); kernel_start(); 

Irgendwie zufällig habe ich nach dem Refactoring diese Zeilen genommen und in eine separate Funktion eingefügt ... Und das Register ist lokal für die Funktion. Es ist klar, dass alles kaputt ist :)

Also scheint alles im Prozessor eingeschaltet zu sein, gehe zum Interrupt-Controller.

Wie wir oben gesehen haben, befinden sich Informationen über die Ausnahmenummer im TIR-Register. Ferner meldet das 32. Bit in diesem Register, dass ein externer Interrupt aufgetreten ist.

Nach dem Einschalten des Timers folgten einige Tage der Qual, da keine Unterbrechung erzielt werden konnte. Der Grund war amüsant genug. Es gibt 64-Bit-Zeiger in Elbrus, und die Adresse des Registers in APIC wurde in uint32_t eingegeben. Deshalb haben wir sie verwendet. Es stellte sich jedoch heraus, dass Sie, wenn Sie beispielsweise 0xF0000000 in einen Zeiger umwandeln müssen, nicht 0xF0000000, sondern 0xFFFFFFFFF0000000 erhalten. Das heißt, der Compiler erweitert Ihr vorzeichenloses int-Zeichen.

Hier war es natürlich notwendig, uintptr_t zu verwenden, da, wie sich herausstellte, im C99-Standard diese Art der Konvertierung implementierungsdefiniert ist.

Nachdem wir endlich das erhöhte 32. Bit in TIR gesehen hatten, begannen wir zu suchen, wie wir die Interrupt-Nummer erhalten können. Es stellte sich als recht einfach heraus, obwohl dies überhaupt nicht wie bei x86 ist, ist dies einer der Unterschiede zwischen den LAPIC-Implementierungen. Für Elbrus müssen Sie in das spezielle LAPIC-Register gelangen, um die Interrupt-Nummer zu erhalten:

  #define APIC_VECT (0xFEE00000 + 0xFF0) 

Dabei ist 0xFEE00000 die Basisadresse der LAPIC-Register.

Es stellte sich heraus, dass sowohl der System-Timer als auch der LAPIC-Timer erfasst wurden.

Fazit


Die Informationen in den ersten beiden technischen Teilen des Artikels über die Elbrus-Architektur reichen aus, um Hardware-Interrupts und präventives Multitasking in jedem Betriebssystem zu implementieren. Tatsächlich zeugen die angegebenen Screenshots davon.



Dies ist nicht der letzte technische Teil der Elbrus-Architektur. Jetzt beherrschen wir das Memory Management (MMU) in Elbrus und hoffen, bald darüber sprechen zu können. Wir benötigen dies nicht nur für die Implementierung virtueller Adressräume, sondern auch für die normale Arbeit mit Peripheriegeräten, da Sie durch diesen Mechanismus das Caching eines bestimmten Bereichs des Adressraums deaktivieren oder aktivieren können.

Alles, was im Artikel geschrieben steht, befindet sich im Embox- Repository. Sie können auch erstellen und ausführen, wenn natürlich eine Hardwareplattform vorhanden ist. Zwar wird hierfür ein Compiler benötigt, der nur beim MCST erhältlich ist . Dort können offizielle Unterlagen angefordert werden.

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


All Articles