Lassen Sie uns gemeinsam die hbird_e203 CPU lesen

EleCannonic

Copyright-Hinweis:

Dieser Artikel ist lizenziert unter CC BY-NC-SA 4.0.

Lizenzinformationen:

Die kommerzielle Nutzung dieser Inhalte ist strengstens untersagt. Weitere Details zur Lizenzpolitik finden Sie auf der Seite Über uns.

Hinweis: Dieser Artikel ist lediglich ein Gedankengang. Bitte verwenden Sie ihn nicht als Handout, da er nicht genügend Informationen für systematisches Lernen bietet. Als Referenz ist er jedoch in Ordnung.

Als Startpunkt für einen tiefen Einblick in die Welt der CPUs ist die hbird_e203 ein gutes Projekt.

Hier ist der Projektlink: Hummingbird E203

1. Befehlssatzarchitektur (ISA)

hbird_e203 verwendet die RISC-V ISA, die einfach konzipiert ist. Befehle in der RISC-V ISA sind streng geordnet.

Format von RISC-V-Befehlen

RISC-V unterstützt nur Little-Endian. Was ist Little-Endian und Big-Endian? Nun, lassen Sie uns eine Grafik zur Interpretation verwenden:

Interpretation von Little-Endian und Big-Endian

Wenn die ISA Little-Endian verwendet, sind die gespeicherten Daten 0x78563412. Bei Big-Endian sollte es 0x12345678 sein.

2. Standard DFF-Register

hbird_e203 verwendet modulare, Standard-DFF-Module zur Konstruktion von Registern anstelle eines always-Blocks.

1
2
3
4
5
6
wire flg_r; // Ausgangssignal
wire flg_nxt = ~flg_r; // Eingangssignal
wire flg_ena = (ptr == ('E203_OITF_DEPTH-1) & ptr_ena);

// Instanziierung von Standard-DFF-Modulen
sirv_gnrl_dfflr #(1) flg_dfflrs(flg_ena, flg_nxt, flg_r, clk, rst_n);

In einem anderen Modul ist sirv_gnrl_dfflr wie folgt aufgebaut:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
module sirv_gnrl_dfflr # (
parameter DW = 32 // Dies ist ein 32-Bit breiter DFF, durch Übertragung des Parameters DW kann er variiert werden.
) (

input lden,
input [DW-1:0] dnxt,
output [DW-1:0] qout,

input clk, // Takt
input rst_n // negativ gültiges Reset, synchron
);

reg [DW-1:0] qout_r;

always @(posedge clk or negedge rst_n)
begin : DFFLR_PROC // DFFLR_PROC ist nur ein Label für diesen always-Block. Hat keine Auswirkung auf die Funktionen.
if (rst_n == 1'b0)
qout_r <= {DW{1'b0}}; // Reset, Ausgang wird auf Null gesetzt
else if (lden == 1'b1)
qout_r <= #1 dnxt;
// #1: Verzögerung von 1 Zeiteinheit. Hat keine Auswirkung auf die Synthese. Debug-Methode in der Simulation.
// dnxt: zuzuweisende Daten
end

assign qout = qout_r;

// Einführung eines Prüfers, bedingte Kompilierung.
// Nur in der Simulation verwendet, keine Hardware-Schaltung erzeugt
`ifndef FPGA_SOURCE//{
`ifndef DISABLE_SV_ASSERTION//{
//synopsys translate_off
sirv_gnrl_xchecker # (
.DW(1)
) sirv_gnrl_xchecker(
.i_dat(lden),
.clk (clk)
);
//synopsys translate_on
`endif//}
`endif//}

endmodule

Durch die Verwendung von Standardmodulen ist es bequem, den Registertyp zu ersetzen oder Verzögerungen global einzufügen. Das xchecker-Modul erfasst undefinierte Zustände. Sobald ein solcher erkannt wird, meldet es einen Fehler und bricht die Simulation ab.

3. if-else und assign

Dieses Projekt empfiehlt, if-else durch assign zu ersetzen. Denn if-else hat zwei Hauptnachteile:

  • if-else kann den undefinierten Zustand X nicht übertragen.

    1
    2
    3
    4
    if(flg)
    out = in1;
    else
    out = in2;

    Wenn flg == X, wird Verilog dies als flg == 0 behandeln, und die endgültige Ausgabe wird out = in2 sein, was X nicht übertragen hat.

    Wenn jedoch assign verwendet wird:

    1
    assign out = flg ? in1 : in2;

    Der X-Zustand wird übertragen. Diese Übertragung erleichtert das Debugging.

  • if-else wird als Prioritäts-MUX synthetisiert, was zu einer großen Fläche und schlechterer Timing-Leistung führt. Nehmen wir den folgenden MUX als Beispiel:

    1
    2
    3
    4
    5
    6
    7
    8
    if (sel1)
    out = in1[3:0];
    else if (sel2)
    out = in2[3:0];
    else if (sel3)
    out = in3[3:0];
    else
    out = 4'b0;

    Nach der Synthese wird dieser Code zu:

    Prioritäts-MUX

3 MUX belegen offensichtlich mehr Fläche. Aber wenn wir assign verwenden:

1
2
3
assign out =   ({4{sel1}} & in1[3:0])
| ({4{sel2}} & in2[3:0])
| ({4{sel3}} & in3[3:0]);

Dies ist ein paralleler, maskierender MUX. Die sel-Signale fungieren als Maskierungssteuerungen, die parallel zu den drei in-Signalen sind. Er wird synthetisiert als:

Paralleler MUX

4. Daten-Hazard

  • RAW (Read After Write)
    Angenommen, Befehl j benötigt eine Operationsnummer, die von Befehl i bereitgestellt werden soll. Daher muss das WB von i vor dem Registerlesen von j ausgeführt werden.

    Zum Beispiel:

    1
    2
    i: ADD x1, x2, x3 ; (x2 + x3 -> x1)
    j: SUB x4, x1, x5 ; (x1 - x5 -> x4)

    In einer Pipeline, wenn j gerade die ID ausführt, könnte i noch die EX ausführen, das Ergebnis wurde noch nicht in die Registerdatei geschrieben. In dieser Situation liest j eine falsche Operationsnummer.

    Um das Problem zu lösen, kann die Pipeline ein Stalling anwenden, um nachfolgende Befehle anzuhalten und auf das WB von i zu warten. Die gebräuchlichste Methode ist jedoch Data Forwarding. Die CPU sendet das Ergebnis von EX oder MEM von i direkt an j, anstatt auf i zu warten. Diese Methode erhöht die Effizienz im Vergleich zum Stalling.

  • WAR (Write After Read)
    Befehl j versucht, in ein Register zu schreiben, aber ein anderer Befehl i muss die Operationsnummer in diesem Register lesen. Das Lesen von i muss vor dem Schreiben von j abgeschlossen sein.

    Beispiel:

    1
    2
    3
    i: SUB x4, x1, x5  ; liest x1
    j: ADD x1, x2, x3 ; schreibt x1
    k: MUL x6, x1, x7 ; liest x1

    Wenn die Pipeline In-Order ist, gibt es kein Problem. In einer Out-of-Order-Pipeline kann es jedoch passieren, dass j vor i fertig wird, wenn x2 und x3 früher bereit sind. Dann liefert i ein falsches Ergebnis.

    Zur Lösung benennt die CPU die Register um.

    1
    2
    3
    i: SUB x4, P1, x5   ; // P1 für alten x1-Wert
    j: ADD P2, x2, x3 ; // P2 für neuen x1-Wert, unabhängig von P1
    k: MUL x6, P2, x7 ; // Neuen Wert verwenden

    Um die Umbenennung zu erreichen, erstellt die CPU eine Zuordnung von der externen Registerdatei (ISA-Register) zu den internen Registern. Dann beeinflussen sich Schreiben und Lesen nicht mehr gegenseitig.

  • WAW (Write After Write)
    Zwei Befehle, i und j, müssen beide eine Nummer in dasselbe Register schreiben. Die korrekte Reihenfolge ist i zuerst und j danach. WAW tritt ebenfalls in einer Out-of-Order-Pipeline auf. Wenn j zuerst fertig wird, sollte das Endergebnis das von i sein, was falsch ist.

    Die Lösung ist ebenfalls die Umbenennung.

5. Instruction Fetch (IF)

Das Endziel von IF ist es, “schnell” und “kontinuierlich” zu sein.

ITCM

Um IF schneller zu machen, müssen wir die Leseverzögerung des Speichers verringern. Allgemeiner Speicher kann eine Verzögerung von Dutzenden von Taktzyklen haben, was weit davon entfernt ist, unsere Anforderungen zu erfüllen.

Im Allgemeinen erstellt eine moderne CPU einen kleinen Speicher (Dutzende von KB) zur Speicherung von Befehlen, der physisch nahe am Kern liegt. Dieser Speicher wird als ITCM (Instruction Tightly Coupled Memory) bezeichnet.

ITCM ist kein DDR oder Cache. Es ist nur ein kleiner Speicher mit einer bestimmten Adresse. Die Verzögerung ist im Vergleich zum Cache vorhersagbar. Daher bevorzugen Ingenieure in Situationen mit hohen Leistungsanforderungen die Verwendung von ITCM.

Nicht-ausgerichtete Befehle

RISC-V unterstützt komprimierte Befehle (C-Erweiterung). Die CPU muss mit einer Mischung aus 32-Bit- und 16-Bit-Befehlen umgehen. Wie weiß die CPU also, ob es sich um einen 32-Bit- oder einen 16-Bit-Befehl handelt?

Die beiden niedrigstwertigen Bits des Opcode für einen 32-Bit RISC-V-Befehl müssen 0b11 sein.

Die CPU unterscheidet die Befehle anhand der beiden niedrigstwertigen Bits (nennen wir sie unten LS2B). Wenn die LS2B 0b11 ist, ist es 32 Bit; wenn nicht, ist es 16 Bit.

Wie geht die CPU damit um? Lassen Sie uns den Ablauf im Detail klären.

  • Komponenten

    • Fetch Width: Aus Effizienzgründen holt die CPU mehr als ein Halbwort auf einmal aus dem ITCM. Sie holt normalerweise mehr, zum Beispiel 32 Bit.
    • Instruction Prefetch Queue (IPQ): Ein FIFO zwischen IFU und Decoder.
    • RISC-V-Regel: Wenn LS2B = 0b11, ist es ein 32-Bit-Befehl; andernfalls ist es ein 16-Bit-Befehl.
  • Arbeitsablauf

    • Gemäß dem PC-Wert holt die IFU ein Wort (32 Bit) aus dem ITCM und fügt es unten in die IPQ ein.

    • Die ID holt ein Halbwort (16 Bit) vom oberen Ende der IPQ und prüft dann, ob es sich um einen komprimierten Befehl handelt.

      • Situation A: Es ist ein 16-Bit-komprimierter Befehl
        Die ID verbraucht die ersten 16 Bit in der IPQ und sendet sie als vollständigen Befehl an die nachfolgenden Abschnitte. Der Zeiger der IPQ bewegt sich um 2 Bytes.
      • Situation B: Es ist Teil eines 32-Bit-Befehls
        Die ID benötigt mehr Daten. Sie verbraucht die ersten 32 Bit in der IPQ und sendet sie dann an den nachfolgenden Abschnitt. Der Zeiger der IPQ bewegt sich um 4 Bytes.
    • Diese Schritte werden wiederholt. Wenn die Daten in der IPQ weniger als 32 Bit betragen, führt die IFU den nächsten 32-Bit-Lesevorgang durch und füllt die Daten am Ende der IPQ auf.

Sprungbefehle

Es gibt zwei Arten von Sprungbefehlen in RISC-V.

  • Unbedingter Sprung: Urteilsbedingungen sind nicht erforderlich. Es gibt auch zwei Arten von unbedingten Sprüngen.

    • Direkt: Die Zieladresse kann direkt durch imm im Befehl berechnet werden.

      Beispiel: jal x5, imm, imm ist 20 Bit, Sprung zur Adresse 2*imm + PC.

    • Indirekt: Die Zieladresse muss aus Daten in der Registerdatei berechnet werden.

      Beispiel: jalr x1, x6, imm, imm ist 12 Bit, Sprung zur Adresse imm + x6.

  • Bedingter Sprung: Sprung mit Bedingungen
    Immer noch zwei Arten: Direkt und Indirekt. Aber es gibt keine indirekten Befehle in RISC-V.

Sprungvorhersage

Löst zwei Probleme:

  • Ob gesprungen werden soll (Richtung)
  • Was die Zieladresse ist (Adresse)

Statische Vorhersage: Immer die gleiche Ausfallwahrscheinlichkeit vorhersagen oder einem festen Muster folgen. (BTFN)

Sprungrichtung: Ziel-PC < Aktueller PC, genannt zurück; andernfalls vorwärts genannt.

Dynamisch:

  • 1-Bit-Sättigung: Verwendet die letzte Richtung zur Vorhersage. Wird bei Fehlern geändert.

  • 2-Bit-Sättigung:

    Betrachten Sie die Zustandsmaschine:

2-Bit-Sättigungszähler FSM-Diagramm

2-Bit-Sättigung ist effektiv bei der Vorhersage eines einzelnen Befehls. Aber für viele Befehle (an verschiedenen PC-Adressen) nicht. (Sie werden sich überschneiden) Idealerweise sollte jeder Sprungbefehl seinen eigenen Prädiktor haben, was zu inakzeptablen Hardwarekosten führen würde. Daher gibt es in der Praxis nur eine endliche Anzahl von Prädiktoren, die eine Tabelle bilden (Branch Prediction Table).

Genaues Vorhersageverfahren: Indizierung

  • Ein Befehl tritt in die Pipeline ein, mit PC = 0x12345678.
  • Die CPU nimmt die unteren sieben Bits (z. B. 10), Index 0x678 = 0d1656.
  • Die CPU greift mit dem Index 0d1656 auf die BPT zu und findet einen 2-Bit-Sättigungsprädiktor.
  • Vorhersage läuft durch den Prädiktor, Zustände werden aktualisiert, …

Tatsächlich ist die Anzahl der Befehle weitaus größer als die der Prädiktoren. Daher müssen viele verschiedene Befehle denselben Prädiktor verwenden. Dieses Problem wird als Aliasing bezeichnet.

Es gibt eine kompliziertere Methode mit besserer Leistung, die Correlation-Based Branch Predictor genannt wird.

  • Warum wir sie brauchen

Betrachten Sie einen Code

1
2
3
4
5
6
7
8
9
if (a > 10) { // Sprung A
Hajimi nameiluduo axigaaxi;
}

Dingdongji Dingdongji, Dagou Dagou Jiaojiaojiao;

if (b > 20 && a > 10) { // Sprung B
Axiga Yakunalu Hajimi Haji;
}

Ob B springt, hängt sowohl von b > 20 als auch vom Ergebnis von Sprung A ab. Wenn A nicht gesprungen ist, darf B nicht springen. Eine einzelne Prädiktortabelle kann diese Situation nicht bewältigen.

Zwei Komponenten:

  • Global History Register (GHR): Breite N, zeichnet Ergebnisse der letzten N Befehle auf.
  • Pattern History Table (PHT): Ein Array, das aus 2-Bit-Zählern besteht.

Indexmethode: PC ^ GHR.
Die 2-Bit-Zähler zeichnen auf, “wenn die globale Historie ein bestimmtes Muster aufweist, wie sich Sprung B verhält”, anstatt die Historie von Sprung B selbst.

Verfahren:

Angenommen, GHR hat eine Breite von 2 Bit. Anfangszustand 00.

  • Ausführung 1: Angenommen, a = 5, A springt nicht, aufgezeichnet als 0, GHR wird nach links verschoben, 0 wird in das LSB von GHR gefüllt.
    GHR = 00.
    B springt nicht, aufgezeichnet als 0, GHR wird nach links verschoben, 0 wird in das LSB von GHR gefüllt.

  • Ausführung 2: Angenommen, a = 15, b = 25, A springt, GHR = 01.
    Vor der Ausführung von B wird der Index erzeugt, idx = Hash(PC_B, 01).
    Finden Sie einen 2-Bit-Zähler (angenommen, der Anfangszustand ist 11, was bedeutet, dass B dazu neigt, nicht zu springen, wenn der letzte Sprung stattfand).

    Vorhersage machen: B wird nicht springen.

    Tatsächliches Ergebnis: Vorhersage fehlgeschlagen!
    Zähler: 11 -> 10.
    GHR: 01 -> 11.

Diese Schritte wiederholen sich.

6. E200 IFU-Implementierung

RISC-V platziert die Längenanzeige in den niedrigstwertigen Bits. Daher kann die IF-Logik die Länge erkennen, sobald sie die niedrigstwertigen Bits abruft. Darüber hinaus, da der komprimierte Befehlssatz optional ist, kann die CPU, wenn sie nicht für die Unterstützung des komprimierten Satzes ausgelegt ist, die niedrigstwertigen Bits direkt ignorieren, was etwa 6,25 % der I-Cache-Kosten spart.

Gesamtkonzept des Designs

Das IFU-Modul hat folgende Mikroarchitektur:

IFU Mikroarchitektur

Es versucht, Befehle “schnell” und “kontinuierlich” abzurufen. E203 geht davon aus, dass die meisten Befehle im ITCM gespeichert sind, da es für extrem stromsparende, eingebettete Fälle konzipiert ist und niemals lange Codes lädt. Normalerweise können alle Codes im ITCM geladen werden.

Das IF-Modul kann einen Befehl in nur einem Zyklus abrufen, was die Anforderung an Schnelligkeit erfüllt. Wenn es Befehle von der BIU abrufen muss, gibt es mehr Verzögerung, aber solche Fälle sind viel seltener als ITCM. Daher hat E203 keine Optimierungen für diese Fälle vorgenommen (für höhere Leistung wäre eine solche Optimierung jedoch möglicherweise notwendig).

Für “kontinuierlich” muss die IF jedes Mal den nächsten PC-Wert vorhersagen. Die IF dekodiert teilweise den abgerufenen Befehl und prüft, ob er springen muss. Wenn ja, läuft der Branch Predictor im selben Zyklus, und die IF verwendet das Ergebnis und die dekodierten Informationen, um den nächsten PC zu generieren.

Mini-Dekodierung

Dieses Modul muss nur prüfen, ob es sich um einen allgemeinen Befehl oder einen Sprungbefehl handelt. Um den Designprozess zu vereinfachen, wird dieses Modul durch Instanziierung eines vollständigen Dekodierungsmoduls mit nicht verbundenen Eingängen, die auf Masse gelegt sind, und nicht verbundenen Ausgängen implementiert. Synthesewerkzeuge optimieren die redundanten Logiken und erreichen schließlich eine Mini-Dekodierung.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
`include "e203_defines.v"

module e203_ifu_minidec(

// Die IR-Stufe zum Decoder
input [`E203_INSTR_SIZE-1:0] instr,

// Der dekodierte Info-Bus
output dec_rs1en,
output dec_rs2en,
output [`E203_RFIDX_WIDTH-1:0] dec_rs1idx,
output [`E203_RFIDX_WIDTH-1:0] dec_rs2idx,

output dec_mulhsu,
output dec_mul ,
output dec_div ,
output dec_rem ,
output dec_divu ,
output dec_remu ,

output dec_rv32, // zeigt Bits des Befehls an (16 Bit oder 32 Bit)
output dec_bjp, // allgemeiner oder Sprungbefehl
output dec_jal, // ob jal
output dec_jalr, // ob jalr
output dec_bxx, // ob bedingte Sprungbefehle
output [`E203_RFIDX_WIDTH-1:0] dec_jalr_rs1idx, // Index des rs1-Registers für jalr
output [`E203_XLEN-1:0] dec_bjp_imm // imm des bedingten Sprungs

);

// ein vollständiges Dekodierungsmodul
e203_exu_decode u_e203_exu_decode(

.i_instr(instr),
.i_pc(`E203_PC_SIZE'b0),
// ......
);

endmodule

Wir werden das Dekodierungsmodul in den folgenden Abschnitten im Detail untersuchen, nicht hier.

Ready/Valid Handshake

Der Ready/Valid-Handshake ist ein Protokoll zur Gewährleistung korrekter Datenübergänge zwischen zwei Geräten.

Skizze des Handshake-Protokolls

Die Regeln sind unkompliziert: Die Datenübertragung findet nur statt, wenn sowohl ready als auch valid im selben Taktzyklus auf ‘1‘ stehen. Der Handshake ist ein zustandsloses Protokoll. Keine der Parteien muss sich an frühere Taktzyklen erinnern, um zu bestimmen, ob in einem bestimmten Zyklus eine Datenübertragung stattfindet. Darüber hinaus müssen beide Parteien synchron arbeiten und die Steuersignale an derselben Taktflanke lesen. Aus diesem Grund ist Ready/Valid nicht für Clock Domain Crossing (CDC) geeignet.

Einfacher BPU-Sprungprädiktor

Um niedrigen Stromverbrauch zu erreichen, verwendet E203 die einfachste stationäre Vorhersage. Für bedingte direkte Sprungbefehle wird ein Rückwärtssprung als erforderlich vorhergesagt; andernfalls wird vorhergesagt, dass kein Sprung erforderlich ist. Gleichzeitig generiert die BPU die nächste PC über einen PC + offset-Addierer.

Die Datei befindet sich im Modul e203_ifu_litebpu.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
`include "e203_defines.v"

module e203_ifu_litebpu(

// Aktuelle PC
input [`E203_PC_SIZE-1:0] pc,

// Die Mini-dekodierten Informationen
input dec_jal, // ob jal
input dec_jalr, // ob jalr
input dec_bxx, // wo bedingter Sprung
input [`E203_XLEN-1:0] dec_bjp_imm, // imm des bedingten Sprungs, Zieladresse ist PC + imm
input [`E203_RFIDX_WIDTH-1:0] dec_jalr_rs1idx, // Index von rs1 bei jalr

// Der IR-Index und OITF-Status zur Überprüfung der Abhängigkeit
input oitf_empty, // ob OITF leer ist
input ir_empty, // ob IR leer ist
input ir_rs1en, // ob rs1 in IR verwendet werden soll
input jalr_rs1idx_cam_irrdidx, // ob der Index von rs1 mit dem von IR übereinstimmt
/*
Wenn der Befehl in IR und der nächste jalr dasselbe Register verwenden,
jalr_rs1idx_cam_irrdidx = 1
Dieses Signal wird verwendet, um einen RAW-Hazard zu verhindern.
*/

// Die Additionsoperation für den nächsten PC-Addierer
output bpu_wait,
/*
Wenn eine Abhängigkeit erkannt wird, bpu_wait = 1,
pausiert die Pipeline und wartet, bis der Befehl von IR ausgeführt wurde.
*/

output prdt_taken, // Ergebnis, Vorhersage zum Springen -> hoch
output [`E203_PC_SIZE-1:0] prdt_pc_add_op1,
output [`E203_PC_SIZE-1:0] prdt_pc_add_op2,
// vorhergesagte_pc = prdt_pc_add_op1 + prdt_pc_add_op2

input dec_i_valid, // gültig für Dekodierung

// Die RS1 zum Lesen der Registerdatei
output bpu2rf_rs1_ena, // Aktiviert die Registerdatei zum Lesen von rs1
input ir_valid_clr, // IR-Gültigkeit löschen
// Wenn der Befehl in IR gelöscht wird, kann die Abhängigkeit entfernt werden.

input [`E203_XLEN-1:0] rf2bpu_x1, // liest Register x1 ()
input [`E203_XLEN-1:0] rf2bpu_rs1, // andere Register für jalr

// Takt und Reset (negativ gültig)
input clk,
input rst_n
);

// JAL und JALR springen immer, bxxx rückwärts wird als genommen vorhergesagt
assign prdt_taken = (dec_jal | dec_jalr | (dec_bxx & dec_bjp_imm[`E203_XLEN-1]));
/*
`E203_XLEN ist die Breite des Registers
BPU sagt einen Sprung voraus, wenn der Befehl jal oder jalr ist.
Bei bedingten Befehlen bestimmt das MSB von dec_bjp_imm das Vorzeichen des Offsets.
1 für negative Zahl, Rückwärtssprung, Vorhersage als genommen;
0 für positive Zahl, Vorwärtssprung, Vorhersage als nicht genommen.
*/

// JALR mit rs1 == x1 hat eine Abhängigkeit oder xN hat eine Abhängigkeit
wire dec_jalr_rs1x0 = (dec_jalr_rs1idx == `E203_RFIDX_WIDTH'd0);
// wird als 1 zugewiesen, wenn jalr x0 verwendet (Wert ist immer 0), ein spezieller unbedingter Sprung

wire dec_jalr_rs1x1 = (dec_jalr_rs1idx == `E203_RFIDX_WIDTH'd1);
// wird als 1 zugewiesen, wenn jalr x1 verwendet (wird als Rücksprungadressregister verwendet)

wire dec_jalr_rs1xn = (~dec_jalr_rs1x0) & (~dec_jalr_rs1x1);
// wird als 1 zugewiesen, wenn jalr andere Register verwendet (allgemeine Register)

// Abhängigkeit prüfen
wire jalr_rs1x1_dep = dec_i_valid & dec_jalr & dec_jalr_rs1x1 & ((~oitf_empty) | (jalr_rs1idx_cam_irrdidx));
/*
Abhängigkeit tritt am Register x1 auf:
Dekodierung erfolgreich, ist ein jalr-Befehl, jalr und IR verwenden beide x1, OITF ist nicht leer.
*/
wire jalr_rs1xn_dep = dec_i_valid & dec_jalr & dec_jalr_rs1xn & ((~oitf_empty) | (~ir_empty));
/*
Wenn der Wert von xn erst in der EXU bestimmt werden kann, wird dies als Abhängigkeit betrachtet.
Es wird nur dann als Abhängigkeit betrachtet, wenn IR nicht leer ist, anstatt die Registernummer zu vergleichen.
Dies ist tatsächlich eine konservative Methode.
Ein solches Design vereinfacht die Hardware-Logik und reduziert die Kosten für Fläche und Stromverbrauch,
verringert jedoch gleichzeitig die Leistung.
Da E203 jedoch für extrem niedrigen Stromverbrauch entwickelt wurde,
ist ein geringer Leistungsverlust akzeptabel.
*/

wire jalr_rs1xn_dep_ir_clr = (jalr_rs1xn_dep & oitf_empty & (~ir_empty)) & (ir_valid_clr | (~ir_rs1en));
/*
Wenn nur eine Abhängigkeit zur IR-Stufe besteht (OITF ist leer), dann wenn IR gelöscht wird,
oder wenn der RS1-Index nicht verwendet wird, können wir es auch als keine Abhängigkeit behandeln.
*/

// Eine FSM, die bestimmt, wann der Wert von rs1 gelesen werden muss, um den korrekten Wert für jalr bereitzustellen
wire rs1xn_rdrf_r; // zeigt an, ob die CPU den Wert von rs1 lesen muss
wire rs1xn_rdrf_set = (~rs1xn_rdrf_r) & dec_i_valid & dec_jalr & dec_jalr_rs1xn & ((~jalr_rs1xn_dep) | jalr_rs1xn_dep_ir_clr);
wire rs1xn_rdrf_clr = rs1xn_rdrf_r;
wire rs1xn_rdrf_ena = rs1xn_rdrf_set | rs1xn_rdrf_clr;
wire rs1xn_rdrf_nxt = rs1xn_rdrf_set | (~rs1xn_rdrf_clr);

sirv_gnrl_dfflr #(1) rs1xn_rdrf_dfflrs(rs1xn_rdrf_ena, rs1xn_rdrf_nxt, rs1xn_rdrf_r, clk, rst_n);

assign bpu2rf_rs1_ena = rs1xn_rdrf_set;

assign bpu_wait = jalr_rs1x1_dep | jalr_rs1xn_dep | rs1xn_rdrf_set;

// Erzeugt die Operationsnummer 1
assign prdt_pc_add_op1 = (dec_bxx | dec_jal) ? pc[`E203_PC_SIZE-1:0]
// Wenn jalr x0 verwendet, konstanten Wert 0 verwenden
: (dec_jalr & dec_jalr_rs1x0) ? `E203_PC_SIZE'b0
: (dec_jalr & dec_jalr_rs1x1) ? rf2bpu_x1[`E203_PC_SIZE-1:0]
: rf2bpu_rs1[`E203_PC_SIZE-1:0];

// Erzeugt die Operationsnummer 2: Offset durch unmittelbare Zahl
assign prdt_pc_add_op2 = dec_bjp_imm[`E203_PC_SIZE-1:0];

endmodule

In der RISC-V-Struktur wird x1 standardmäßig als “Rücksprungadresse” verwendet. In den meisten Fällen geben jal und jalr die Adresse des nächsten Befehls an x1 zurück, wenn sie nicht speziell zugewiesen sind. Daher wird in den meisten Fällen die Adresse in x1 gespeichert. Zur Leistungssteigerung hat E203 eine spezielle Beschleunigung für x1 implementiert.

1
2
wire dec_jalr_rs1x1 = (dec_jalr_rs1idx == `E203_RFIDX_WIDTH'd1);
// wird als 1 zugewiesen, wenn jalr x1 verwendet (wird als Rücksprungadressregister verwendet)

Diese Zeile wird verwendet, um zu beurteilen, ob jalr x1 verwendet hat. Darüber hinaus muss beurteilt werden, ob eine RAW-Abhängigkeit besteht. Eine RAW-Abhängigkeit besteht, wenn

  • OITF nicht leer ist, was bedeutet, dass ein langer Befehl ausgeführt wird. Das Ergebnis könnte nach x1 zurückgeschrieben werden.
    (Natürlich kann es auch andere Register verwenden, aber hier wird eine konservative Schätzung angewendet, um die Fläche zu reduzieren. Der Leistungsverlust wird ignoriert.)
  • Der Befehl im IR-Register schreibt das Ergebnis zurück nach x1.

Daher

1
wire jalr_rs1x1_dep = dec_i_valid & dec_jalr & dec_jalr_rs1x1 & ((~oitf_empty) | (jalr_rs1idx_cam_irrdidx));

wird verwendet, um eine Abhängigkeit anzuzeigen. Die folgende Zeile

1
assign bpu_wait = jalr_rs1x1_dep | jalr_rs1xn_dep | rs1xn_rdrf_set;

aktiviert bpu_wait für einen Zyklus, wenn eine Abhängigkeit erkannt wird. Ein solches Signal stoppt die nächste PC-Generierung der IFU, bis der RAW verschwindet. Im Allgemeinen verursacht eine solche Verzögerung (Stall) einen Leistungsverlust von einem Stall-Zyklus.

Wenn jalr andere Register als x0 und x1 verwendet, hat E203 keine spezielle Beschleunigung angewendet. Um xn zu lesen, wird der erste Port der Registerdatei benötigt. Nur wenn der Port frei ist, kann xn gelesen werden. Gleichzeitig muss IR leer sein, um RAW zu verhindern (ähnlich wird der Leistungsverlust ignoriert). Wenn sowohl RAW als auch Leseport frei sind, wird der Port-Enable aktiviert und belegt.

1
2
3
4
5
6
7
8
wire rs1xn_rdrf_set = (~rs1xn_rdrf_r) & dec_i_valid & dec_jalr & dec_jalr_rs1xn & ((~jalr_rs1xn_dep) | jalr_rs1xn_dep_ir_clr);
wire rs1xn_rdrf_clr = rs1xn_rdrf_r;
wire rs1xn_rdrf_ena = rs1xn_rdrf_set | rs1xn_rdrf_clr;
wire rs1xn_rdrf_nxt = rs1xn_rdrf_set | (~rs1xn_rdrf_clr);

sirv_gnrl_dfflr #(1) rs1xn_rdrf_dfflrs(rs1xn_rdrf_ena, rs1xn_rdrf_nxt, rs1xn_rdrf_r, clk, rst_n);

assign bpu2rf_rs1_ena = rs1xn_rdrf_set;

Speicherzugriff

Zur Verbesserung der Code-Dichte unterstützt E203 16-Bit-komprimierte RISC-V-Befehlssätze. Daher werden 32-Bit-Befehle mit 16-Bit-Befehlen gemischt, was zu nicht ausgerichteten 32-Bit-Befehlen führt.

Nicht ausgerichtete 32-Bit-Befehle

Um damit umzugehen, verwendet E203 einen Restpuffer. Die IFU holt jedes Mal 32 Bit aus dem ITCM oder BIU. Wenn die IFU auf das ITCM zugreift, da das ITCM aus SRAM besteht, bleibt der Wert am Port nach dem Lesen gehalten (unverändert), bis zum nächsten Lesen. Eine solche Eigenschaft spart ein 64-Bit-Register.

Die Bitbreite des ITCM in E203 beträgt 64 Bit. Ein Lesezugriff liest 64 Bit Daten vom Port, genannt eine Lane. Beim Abrufen durch Erhöhen der Adresse holt die IFU Daten aus derselben Lane mehrmals, da RISC-V-Befehle in E203 höchstens 32 Bit lang sind. Dies reduziert die Anzahl der SRAM-Lesevorgänge, da die IFU Daten aus dem gehaltenen Portwert lesen kann, bis alle Daten gelesen sind.

Wenn ein 32-Bit-Befehl eine 64-Bit-Grenze überschreitet, werden die verbleibenden 16-Bit-Daten im Restpuffer gespeichert und lösen einen neuen SRAM-Zugriff aus. Die niedrigsten 16 Bit der neuen 64-Bit-Daten aus SRAM und die 16 Bit im Restpuffer werden zu einem vollständigen 32-Bit-Befehl verkettet. Dies entspricht dem Abrufen eines 32-Bit-Befehls in einem Zyklus, ohne Leistungsverlust.

Wenn ein Sprungbefehl oder eine Pipeline-Flush auftritt und der gewünschte Befehl eine 64-Bit-Grenze überschreitet, sind zwei kontinuierliche SRAM-Lesevorgänge erforderlich. Das bedeutet, dass das Abrufen zwei Zyklen kosten muss, was einen Zyklusverlust verursacht. E203 verzichtet auf die Optimierung, da sie zu viel zusätzlichen Flächen- und Kostenaufwand mit sich bringen würde.


Referenzen:

[1] 胡振波, RISC-V架构与嵌入式开发快速入门, 1. Auflage. Peking:人民邮电出版社, 2019.
[2] 胡振波, 手把手教你设计CPU——RISC-V处理器, 1. Auflage. Peking:人民邮电出版社, 2018.

Kommentare