Forum: FPGA, VHDL & Co. Grundsatzfrage zum Umgang mit RAM


von Peter (Gast)


Lesenswert?

Hallo,

nachdem ich die ersten, kleinen Projekte wie SD-Interface, 
VGA-Framebuffer und anderes dieser Art abgeschlossen habe, möchte ich 
eine kleine CPU entwerfen.

In meiner Architektur gibt es einen Daten- und einen Adressbus, mit dem 
CPU und RAM verbunden sind. Es soll kein Pipelining geben. Nun frage ich 
mich, wie ich es hinbekommen kann, dass die Fetch-Phase in einem 
einzelnen Takt abläuft. Durch Sprunganweisungen kann der Programmzähler 
einen beliebigen Wert haben, sodass ich an folgende Implementierung für 
Fetch dachte:

* Inhalt des Programmzählers auf den Adressbus legen und Leseanforderung 
setzen
* einen Takt warten - in diesem kann ich leider nichts sinnvolles tun, 
da keine Informationen über die folgende Anweisung bekannt sind
* jetzt übernimmt das RAM die Leseanforerung und lädt den Inhalt
* einen weiteren Takt warten
* erst jetzt liegt die Anweisung auf dem Datenbus

Bei meinen bisherigen Projekten habe ich Daten nur sequentiell gelesen, 
sodass ich einfach Takt vor der Auswertung mit dem Lesen anfangen konnte 
und dadurch alles synchron war. Hier sehe ich jetzt aber das Problem, 
dass der Programmzähler sich beliebig ändern kann und ich dann einen 
Takt länger warten muss.

Ich glaube, dass andere Prozessoren das anders lösen. Im 
Beispielprozessor des Buches "VHDL-Synthese" wird beispielsweise sowohl 
mit steigenden als auch mit fallenden Flanken gearbeitet. Ist das die 
übliche Lösung oder gibt es etwas "besseres"? Oder habe ich allgemein 
einen Denkfehler?

Viele Grüße

Peter

von Falk B. (falk)


Lesenswert?

@Peter (Gast)

>mich, wie ich es hinbekommen kann, dass die Fetch-Phase in einem
>einzelnen Takt abläuft.

Langsam genug takten.

>Bei meinen bisherigen Projekten habe ich Daten nur sequentiell gelesen,
>sodass ich einfach Takt vor der Auswertung mit dem Lesen anfangen konnte
>und dadurch alles synchron war. Hier sehe ich jetzt aber das Problem,
>dass der Programmzähler sich beliebig ändern kann und ich dann einen
>Takt länger warten muss.

Ist halt so. Ein Programm besteht ja nicht nur aus Sprungbefehlen.

>Ich glaube, dass andere Prozessoren das anders lösen.

So wie der 8051 mit seinen 12 Takten/Maschinenzyklus?
SCNR

> Im
>Beispielprozessor des Buches "VHDL-Synthese" wird beispielsweise sowohl
>mit steigenden als auch mit fallenden Flanken gearbeitet. Ist das die
>übliche Lösung

Nein.

>oder gibt es etwas "besseres"?

Pipelining.

von Fpgakuechle K. (Gast)


Lesenswert?

Peter schrieb:

> In meiner Architektur gibt es einen Daten- und einen Adressbus, mit dem
> CPU und RAM verbunden sind. Es soll kein Pipelining geben. Nun frage ich
> mich, wie ich es hinbekommen kann, dass die Fetch-Phase in einem
> einzelnen Takt abläuft. Durch Sprunganweisungen kann der Programmzähler
> einen beliebigen Wert haben, sodass ich an folgende Implementierung für
> Fetch dachte:


Ich hab mal vor Jahren eine Eintakt-maschine geschrieben, siehe dort:
PiBla

MfG,

von Strubi (Gast)


Lesenswert?

Moin,

Peter schrieb:
> Ich glaube, dass andere Prozessoren das anders lösen. Im
> Beispielprozessor des Buches "VHDL-Synthese" wird beispielsweise sowohl
> mit steigenden als auch mit fallenden Flanken gearbeitet. Ist das die
> übliche Lösung oder gibt es etwas "besseres"? Oder habe ich allgemein
> einen Denkfehler?

Das "Nein" von Herrn Brunner unterschreibe ich prinzipiell, aber man 
kann noch was dazu sagen: Man kann es schon so machen, aber man schiesst 
sich timingtechnisch u.U. ins Knie, sobald man etwas höhere 
Taktfrequenzen fährt. In so einigen Büchern sind merkwürdige 
Designkonstrukte beschrieben, die auf einem FPGA zumindest nicht immer 
optimal laufen. Das ganze wird mühsam zu debuggen, und bringt gegenüber 
Pipelining eher Unglück.

Andere (non-pipelined) Prozessoren warten einfach. Guck dir sonst mal 
die ZPU 'small' architektur, z.B. Zealot an, die braucht für einen 
Verarbeitungsschritt min. 4 Zyklen. Allerdings gibt es auch pipelined 
Varianten (ZPUng, ZPU extreme). Beispiele einer relativ optimal 
gepipeline'ten Architektur sind MIPS und ähnliche Designs.

Grüsse,

- Strubi

von Fpgakuechle K. (Gast)


Lesenswert?

Wenn du nicht den Ausgang des Counters sondern den internen Wert der 
Inkrement-logik an den Adressbus legt sparst du dir den Warte-Takt.


Alle Beispile skizzenhaft, ohne berücksichtigung typen
Beispiel 1 simples Standard Adress increment:
1
signal count_q := (others => '0');
2
3
process(clk) begin
4
 if rising_edge(clk) then
5
  if count_q = "1111" then
6
   count_q <= (others => '0');
7
  else
8
   count_q <= count_q + 1;
9
 end if;
10
end if;
11
end process;
12
13
prog_mem: BRAM port map (
14
addr => count_q,
15
clk  => clk,
16
--..
17
);



Beispiel 2:
Wie (1) nur aufgetrennt in Kombinatorik und Speicher
1
signal count_q := (others => '0');
2
signal next_count;
3
4
process(clk) begin
5
 if rising_edge(clk) then
6
   count_q <= next_count;
7
 end if;
8
end process;
9
10
next_count <= (others => '0') when count_q = "1111" else
11
               count_q + 1;
12
13
prog_mem: BRAM port map (
14
addr => count_q,
15
clk  => clk,
16
--..
17
);

Beispiel 3:
Jetzt wird wie oben genannt der "interne" Zählerwert als adresse 
verwendet. Damit die addresse immer noch bei 0 beginnt muss der initwert 
eins vorgezogen werden
1
signal count_q := (others => '1'); --init eins vor
2
signal next_count;
3
4
process(clk) begin
5
 if rising_edge(clk) then
6
   count_q <= next_count;
7
 end if;
8
end process;
9
10
next_count <= (others => '0') when count_q = "1111" else
11
               count_q + 1;
12
13
prog_mem: BRAM port map (
14
addr => next_count,  --Da ist der Trick
15
clk  => clk,
16
--..
17
);

Beispiel 4:
Jetzt wird das increment um das laden bei Sprungbefehlen erweitert:
1
signal count_q := (others => '1');
2
signal next_count;
3
signal cmd_word;
4
5
process(clk) begin
6
 if rising_edge(clk) then
7
   count_q <= next_count;
8
 end if;
9
end process;
10
11
next_count <= cmd_word(3 downto 0) when cmd_word(7 downto 4) = C_JMP else
12
              (others => '0')      when count_q = "1111" else
13
               count_q + 1;
14
15
prog_mem: BRAM port map (
16
addr  => next_count,
17
dat_o => cmd_word,  --obere 4 bits Befehl, unter argument (bspw Sprung_addr)
18
clk   => clk,
19
--..
20
);


Für den oben erwähnten Pibla kommt als Load_Values für den Programm 
counter
nach hinzu: IRQ-ServiceRoutinestartadresse und returnstack für 
Subroutinen.
Ist dort etwas audführlicher beschrieben:
https://www.mikrocontroller.net/articles/PiBla#Programm_counter_PC

MfG,

von Peter (Gast)


Lesenswert?

Danke für eure Tipps und Beispiele! Der PiBla benutzt distributed RAM, 
da tritt der Verzögerungstakt, der mich störte, ohnehin nicht auf.

Dass der 8051 bis zu 12 Takte benötigt, beruhigt mich. Dann mach ich es 
auch erstmal so. Es wird ein CISC-Prozessor in Von-Neumann-Architektur, 
also ähnlich x86. Durch die variablen Befehlsbreiten und die Belegung 
des Adressbusses auch während der Ausführungsphase wäre Pipelining sehr 
schwierig zu implementieren, deshalb lasse ich das weg.

von Falk B. (falk)


Lesenswert?

@ Peter (Gast)

>Danke für eure Tipps und Beispiele! Der PiBla benutzt distributed RAM,

Wirklich? Das wäre ja nicht sonderlich intelligent. Der ist doch viel zu 
klein. Und wenn man mehrere zusammenbaut wird er noch langsamer.

von Peter (Gast)


Lesenswert?

Falk B. schrieb:
> @ Peter (Gast)
>
>>Danke für eure Tipps und Beispiele! Der PiBla benutzt distributed RAM,
>
> Wirklich? Das wäre ja nicht sonderlich intelligent. Der ist doch viel zu
> klein. Und wenn man mehrere zusammenbaut wird er noch langsamer.

Ich hab nochmal nachgeschaut - im Core wird distributed RAM verwendet, 
aber das scheint eher ein Feature zu sein, um vom Programm auf schnell 
verfügbares RAM zuzugreifen. Der Programmcode ist anscheinend doch 
BlockRAM (als Xilinx-Komponente statt VHDL, deshalb beim ersten 
Anschauen übersehen). Dann schau ich's mir doch nochmal in Ruhe an.

von Fpgakuechle K. (Gast)


Angehängte Dateien:

Lesenswert?

Der Programmspeicher ist Block-Ram, wie man aus dem xco-file lesen kann:
https://www.mikrocontroller.net/svnbrowser/pibla/00_hw/src/core_mem.xco?view=markup

Distributed RAM wird für das Registerfile, scratchpad ram und return 
stack verwendet.Ich lad bei Gelgenheit den synthesereport hoch.

Der BRAM funktioniert wie oben beschreiben, erst mit Takt mit der 
datenausgang entsprechend dem adress-eingang aktualisiert.

Peter schrieb:
> * Inhalt des Programmzählers auf den Adressbus legen und Leseanforderung
> setzen

Das ist der Unterschied zu hier vorgeschlagenen Lösung, der adressbus 
wird nicht vom programm counter ausgangsbuffer getrieben sondern von der 
+1 Kombinatorik des counters, also mit der addresse des nächsten zu 
holenden Befehls.

Ich hab mal Beispiel 2 und 4 als Logic-wolke skiziert, siehe Anhang.

MfG,

von Peter (Gast)


Lesenswert?

Ahh, jetzt ist der Groschen gefallen.. Bei einem Sprungbefehl wird im 
gleichen Takt, der den Opcode decodiert, bereits der Teil mit dem neuen 
Programmzähler auf den Adressbus gelegt. Danke :-)

von Fpgakuechle K. (Gast)


Angehängte Dateien:

Lesenswert?

Peter schrieb:
> Ahh, jetzt ist der Groschen gefallen.. Bei einem Sprungbefehl wird im
> gleichen Takt, der den Opcode decodiert, bereits der Teil mit dem neuen
> Programmzähler auf den Adressbus gelegt. Danke :-)

Gern geschehen :-) !
Anbei der gekürzte Synthesereport. Der Programmspeicher -core_mem- ist 
nicht als BRAM aufgeführt da er nach der Synthese als Netzliste aus dem 
Core-generator zugeschlagen wird. Das macht IMHO das design portierbarer 
bspw nach Altera.

MfG,

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.