Schreiben eines einfachen Prozessors und einer Umgebung dafür

Hallo! In diesem Artikel werde ich Ihnen erklären, welche Schritte Sie ausführen müssen, um einen einfachen Prozessor und eine Umgebung dafür zu erstellen.


Befehlssatzarchitektur (ISA)


Zuerst müssen Sie entscheiden, wie der Prozessor aussehen soll. Folgende Parameter sind wichtig:


  • Die Größe des Maschinenworts und der Register (Bit / "Bit" des Prozessors)
  • Maschinenanweisungen (Anweisungen) und deren Größe

Prozessorarchitekturen können je nach Größe der Anweisungen in zwei Typen unterteilt werden (tatsächlich gibt es mehr davon, andere Optionen sind jedoch weniger beliebt):



Ihr Hauptunterschied besteht darin, dass RISC- Prozessoren dieselbe Befehlsgröße haben. Ihre Anweisungen sind einfach und werden relativ schnell ausgeführt, während CISC- Prozessoren unterschiedliche Befehlsgrößen haben können, von denen einige eine Weile dauern können.


Ich habe mich für einen RISC- Prozessor entschieden, der MIPS sehr ähnlich ist.


Ich habe dies aus mehreren Gründen getan:


  • Es ist ganz einfach, einen Prototyp eines solchen Prozessors zu erstellen.
  • Die gesamte Komplexität dieses Prozessortyps wird auf Programme wie Assembler und / oder Compiler übertragen.

Hier sind die Hauptmerkmale meines Prozessors:


  • Maschinenwort- und Registergröße - 32 Bit
  • 64 Register (einschließlich Befehlszähler )
  • 2 Arten von Anweisungen

Der Registertyp (im Folgenden Registertyp) sieht folgendermaßen aus:


rtype


Die Besonderheit solcher Anweisungen besteht darin, dass sie mit drei Registern arbeiten.


Sofortiger Typ :


itype


Anweisungen dieses Typs arbeiten mit zwei Registern und einer Nummer.


OP ist die Nummer des auszuführenden Befehls (oder um anzuzeigen, dass dieser Befehl vom Registertyp ist ).


R0 , R1 , R2 sind Registernummern, die als Operanden für den Befehl dienen.


Func ist ein zusätzliches Feld, das den Typ der Registertypanweisungen angibt .


Imm ist das Feld, in das der Wert geschrieben wird und in dem explizit Anweisungen als Operand angegeben werden sollen.


  • Nur 28 Anweisungen

Eine vollständige Liste der Anweisungen finden Sie im Github-Repository .


Hier sind nur einige davon:


nor r0, r1, r2 

NOR ist eine Anweisung vom Registertyp , die ein logisches ODER NICHT für die Register r1 und r2 ausführt, wonach das Ergebnis in das Register r0 geschrieben wird.


Um diese Anweisung verwenden zu können, müssen Sie das OP- Feld im Binärzahlensystem in 0000 und das Func- Feld in 0000000111 ändern .


 lw r0, n(r1) 

LW ist ein Befehl vom Soforttyp, der einen Speicherwert bei r1 + n in das Register r0 lädt.


Um diese Anweisung verwenden zu können, müssen Sie wiederum das OP- Feld in 0111 ändern und die Nummer n in das IMM- Feld schreiben.


Prozessorcode schreiben


Nach dem Erstellen der ISA können Sie mit dem Schreiben des Prozessors beginnen.


Dafür benötigen wir Kenntnisse in einer Art Gerätebeschreibungssprache. Hier sind einige davon:


  • Verilog
  • VHDL (nicht zu verwechseln mit dem vorherigen!)

Ich habe mich für Verilog entschieden, weil Das Programmieren war Teil meines Universitätskurses.


Um einen Prozessor zu schreiben, müssen Sie die Logik seiner Funktionsweise verstehen:


  1. Anweisungen am Command Counter (PC) erhalten
  2. Dekodierungsanweisungen
  3. Befehlsausführung
  4. Hinzufügen der Befehlsgröße der ausgeführten Anweisung zum Zähler

Und so weiter bis ins Unendliche.


Es stellt sich heraus, dass Sie mehrere Module erstellen müssen:



Wir werden jedes Modul einzeln analysieren.


Datei registrieren


Eine Registerdatei bietet Zugriff auf Register. Damit müssen Sie die Werte einiger Register abrufen oder ändern.


In meinem Fall habe ich 64 Register. In eines der Register wird das Ergebnis der Operation für die beiden anderen geschrieben, daher muss ich die Möglichkeit bieten, nur eines zu ändern und Werte von den beiden anderen zu erhalten.


Decoder


Ein Decoder ist die Einheit, die für das Decodieren von Anweisungen verantwortlich ist. Es zeigt an, welche Operationen von ALU und anderen Einheiten ausgeführt werden müssen.


Zum Beispiel sollte die addi-Anweisung den Wert des Registers $ zero (es speichert immer 0 ) und 20 addieren und das Ergebnis in das Register $ t0 einfügen.


 addi $t0, $zero, 20 

Zu diesem Zeitpunkt bestimmt der Decoder, dass dieser Befehl:


  • Sofortiger Typ
  • Muss das Ergebnis in das Register schreiben

Und überträgt diese Informationen an die folgenden Blöcke.


ALU


Nachdem die Kontrolle an ALU übergeben wurde. Es führt normalerweise alle mathematischen, logischen Operationen sowie Operationen zum Vergleichen von Zahlen aus.


Das heißt, wenn wir den gleichen Addi- Befehl betrachten, dann tritt in diesem Stadium die Addition von 0 und 20 auf.


Andere


Zusätzlich zu den oben genannten Blöcken sollte der Prozessor in der Lage sein:


  • Werte im Speicher abrufen und ändern
  • Führen Sie bedingte Sprünge durch

Hier und da können Sie sehen, wie es im Code aussieht.


Assembler


Nach dem Schreiben des Prozessors benötigen wir ein Programm, das Textbefehle in Maschinencode konvertiert, um dies nicht manuell zu tun. Daher müssen Sie Assembler schreiben.


Ich habe beschlossen, es in der Programmiersprache C zu implementieren.


Da mein Prozessor über eine RISC- Architektur verfügt, habe ich mich zur Vereinfachung meines Lebens entschlossen, den Assembler so zu gestalten, dass ich problemlos meine eigenen Pseudoanweisungen hinzufügen kann (Kombinationen mehrerer elementarer Anweisungen oder anderer Pseudoanweisungen).


Sie können dies mithilfe einer Datenstruktur implementieren, in der der Befehlstyp, sein Format, ein Zeiger auf eine Funktion, die Maschinenbefehlscodes zurückgibt, und ihr Name gespeichert sind.


Ein reguläres Programm beginnt mit einer Segmentdeklaration.


Zwei Segmente .text reichen für uns aus - in denen der Quellcode unserer Programme gespeichert wird - und .data - in denen unsere Daten und Konstanten gespeichert werden.


Eine Anweisung könnte folgendermaßen aussehen:


 .text jie $zero, $zero, $zero #  addi $t1, $zero, 2 # $t1 = $zero + 2 lw $t1, 5($t2) # $t1 = *($t2 + 5) syscall 0, $zero, $zero # syscall(0, 0, 0) la $t1, label# $t1 = label 

Zuerst wird der Name des Befehls angegeben, dann die Operanden.


In .data werden Datendeklarationen angegeben.


 .data .byte 23 #   1  .half 1337 #   2  .word 69000, 25000 #   4  .asciiz "Hello World!" #     ( ) .ascii "12312009" #   ( ) .space 45 #  45  

Eine Anzeige muss mit einem Punkt und einem Datentypnamen beginnen, gefolgt von Konstanten oder Argumenten.


Es ist praktisch, die Assembler-Datei wie folgt zu analysieren (zu scannen):


  1. Scannen Sie zunächst das Segment
  2. Wenn es sich um ein .data- Segment handelt, analysieren wir verschiedene Datentypen oder ein .text- Segment
  3. Wenn es sich um ein .text- Segment handelt, analysieren wir Befehle oder ein .data-Segment

Um zu arbeiten, muss der Assembler die Quelldatei zweimal durchlaufen. Wenn er zum ersten Mal überlegt, an welchen Offsets sich die Links befinden (für die sie dienen), sehen sie normalerweise folgendermaßen aus:


  la $s4, loop #   loop  s4 loop: # ! mul $s2, $s2, $s1 # s2 = s2 * s1 addi $s1, $s1, -1 # s1 = s1 - 1 jil $s3, $s1, $s4 #  s3 < s1     

Und im zweiten Durchgang können Sie bereits eine Datei generieren.


Zusammenfassung


In Zukunft können Sie die Ausgabedatei vom Assembler auf unserem Prozessor ausführen und das Ergebnis auswerten.


Außerdem kann im C-Compiler ein vorgefertigter Assembler verwendet werden. Aber das ist später.


Referenzen:


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


All Articles