>=
\newline
: div ( ud udiv -- uqout umod )
\newline
com >r >r >a r> r> over 0 # +
\newline
/- /- /- /- /- /- /- /- /-
\newline
/- /- /- /- /- /- /- /-
\newline
nip nip a >r -cIF *+ r> ;
\newline
THEN 0 # + *+ $8000 # + r> ;
\newline
@
\layout Standard
Das nächste Beispiel ist etwas komplizierter, weil ich hier eine serielle
Schnittstelle emuliere.
Bei 10MHz muß jedes Bit 87 Takte brauchen, damit die Schnittstelle 115200
Baud schnell ist.
Erst hinter dem zweiten Stop-Bit haben wir Ruhe, die Gegenseite wird sich
schon wieder synchronisieren, wenn das nächste Bit kommt.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<
>=
\newline
: send-rest ( c -- c' ) *+
\newline
: wait-bit
\newline
1 # $FFF9 # BEGIN over + cUNTIL drop drop ;
\newline
: send-bit ( c -- c' )
\newline
nop
\backslash
delay at start
\newline
: send-bit-fast ( c -- c' )
\newline
$FFFE # >a dup 1 # and
\newline
IF drop $0001 # a@ or a!+ send-rest ;
\newline
THEN drop $FFFE # a@ and a!+ send-rest ;
\newline
: emit ( c -- )
\backslash
8N1, 115200 baud
\newline
>r 06 # send-bit r>
\newline
send-bit-fast send-bit send-bit send-bit
\newline
send-bit send-bit send-bit send-bit
\newline
drop send-bit-fast send-bit drop ;
\newline
@
\layout Standard
Der
\family typewriter
;
\family default
hat hier wie bei ColorForth die Funktion des EXITs, so wie der
\family typewriter
:
\family default
nur ein Label einleitet.
Steht vor dem
\family typewriter
;
\family default
ein Call, so wird der in einen Sprung umgewandelt.
Das spart Returnstack-Einträge, Zeit und Platz im Code.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Section
Der Rest der Implementierung
\layout Standard
Zunächst einmal den Rumpf der Datei.
\layout Scrap
<>=
\newline
/*
\newline
* b16 core: 16 bits,
\newline
* inspired by c18 core from Chuck Moore
\newline
*
\newline
<>
\newline
*/
\newline
\newline
`define L [l-1:0]
\newline
`define DROP { sp, T, N } <= { spinc, N, toN }
\newline
`timescale 1ns / 1ns
\newline
\newline
<>
\newline
<>
\newline
<>
\newline
@
\layout Scrap
<>=
\newline
* Instruction set:
\newline
* 1, 5, 5, 5 bits
\newline
* 0 1 2 3 4 5 6 7
\newline
* 0: nop call jmp ret jz jnz jc jnc
\newline
* /3 exec goto ret gz gnz gc gnc
\newline
* 8: xor com and or + +c *+ /-
\newline
* 10: A!+ A@+ R@+ lit Ac!+ Ac@+ Rc@+ litc
\newline
* /1 A! A@ R@ lit Ac! Ac@ Rc@ litc
\newline
* 18: nip drop over dup >r >a r> a
\newline
@
\newline
\layout Subsection
Toplevel
\layout Standard
Die CPU selbst besteht aus verschiedenen Teilen, die aber alle im selben
Verilog-Modul implementiert werden.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
module cpu(clk, reset, addr, rd, wr, data, T,
\newline
intreq, intack, intvec);
\newline
<>
\newline
<>
\newline
<>
\newline
<>
\newline
<>
\newline
<>
\newline
<>
\newline
<>
\newline
\newline
always @(posedge clk or negedge reset)
\newline
<>
\newline
\newline
endmodule // cpu
\newline
@
\layout Standard
Zunächst braucht Verilog erst mal Port Declarations, damit es weiß, was
Input und Output ist.
Die Parameter dienen dazu, auch andere Wortbreiten oder Stacktiefen einfach
einzustellen.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
parameter show=0, l=16, sdep=3, rdep=3;
\newline
input clk, reset;
\newline
output `L addr;
\newline
output rd;
\newline
output [1:0] wr;
\newline
input `L data;
\newline
output `L T;
\newline
input intreq;
\newline
output intack;
\newline
input [7:0] intvec; // interrupt jump vector
\newline
@
\layout Standard
Die ALU wird mit der entsprechenden Breite instanziiert, und die nötigen
Leitungen werden deklariert
\layout Scrap
<>=
\newline
wire `L res, toN;
\newline
wire carry, zero;
\newline
\newline
alu #(l) alu16(res, carry, zero,
\newline
T, N, c, inst[2:0]);
\newline
@
\layout Standard
Da die Stacks nebenher arbeiten, müssen wir noch ausrechnen, wann ein Wert
auf den Stack gepusht wird (also
\series bold
nur
\series default
wenn etwas gespeichert wird).
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
reg dpush, rpush;
\newline
\newline
always @(clk or state or inst or rd)
\newline
begin
\newline
dpush <= 1'b0;
\newline
rpush <= 1'b0;
\newline
if(state[2]) begin
\newline
dpush <= |state[1:0] & rd;
\newline
rpush <= state[1] & (inst[1:0]==2'b10);
\newline
end else
\newline
casez(inst)
\newline
5'b00001: rpush <= 1'b1;
\newline
5'b11100: rpush <= 1'b1;
\newline
5'b11?1?: dpush <= 1'b1;
\newline
endcase // case(inst)
\newline
end
\newline
@
\layout Standard
Zu den Stacks gehören nicht nur die beiden Stack-Module, sondern auch noch
inkrementierter und dekrementierter Stackpointer.
Beim Returnstack kommt erschwerend dazu, daß der Top of Returnstack manchmal
geschrieben wird, ohne daß sich die Returnstacktiefe ändert.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
wire [sdep-1:0] spdec, spinc;
\newline
wire [rdep-1:0] rpdec, rpinc;
\newline
\newline
stack #(sdep,l) dstack(clk, sp, spdec,
\newline
dpush, N, toN);
\newline
stack #(rdep,l) rstack(clk, rp, rpdec,
\newline
rpush, toR, R);
\newline
\newline
assign spdec = sp-{{(sdep-1){1'b0}}, 1'b1};
\newline
assign spinc = sp+{{(sdep-1){1'b0}}, 1'b1};
\newline
assign rpdec = rp+{(rdep){(~state[2] | tos2r)}};
\newline
assign rpinc = rp+{{(rdep-1){1'b0}}, 1'b1};
\newline
@
\layout Standard
Der eigentliche Kern ist das voll synchrone Update der Register.
Die brauchen einen Reset-Wert, und für die verschiedenen Zustände müssen
die entsprechenden Zuweisungen codiert werden.
Das meiste haben wir weiter oben schon gesehen, nur das Befehlsholen und
die Zuweisung des nächsten States und des Wertes von
\family typewriter
incby
\family default
bleibt noch zu erledigen.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
if(!reset) begin
\newline
<>
\newline
end else if(state[2]) begin
\newline
<>
\newline
end else begin // if (state[2])
\newline
if(show) begin
\newline
<>
\newline
end
\newline
if(nextstate == 3'b100)
\newline
{ addr, rd } <= { P, 1'b1 };
\newline
state <= nextstate;
\newline
incby <= (inst[4:2] != 3'b101);
\newline
<>
\newline
end // else: !if(reset)
\newline
@
\layout Standard
Als Reset-Wert stellen wir die CPU so ein, daß sie sich gerade den nächsten
Befehl holen will, und zwar von der Adresse 0.
Die Stacks sind alle leer, die Register enthalten alle 0.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
state <= 3'b011;
\newline
incby <= 1'b0;
\newline
P <= 16'h0000;
\newline
addr <= 16'h0000;
\newline
A <= 16'h0000;
\newline
T <= 16'h0000;
\newline
N <= 16'h0000;
\newline
I <= 16'h0000;
\newline
c <= 1'b0;
\newline
rd <= 1'b0;
\newline
wr <= 2'b00;
\newline
sp <= 0;
\newline
rp <= 0;
\newline
intack <= 0;
\newline
@
\layout Standard
Der Übergang zum nächsten State (das NEXT innerhalb eines Bundles) wird
getrennt erledigt.
Das ist nötig, weil die Zuweisungen der anderen Variablen zum Teil nicht
nur abhängig vom aktuellen State sind, sondern auch vom nächsten (z.B.
wann das nächste Befehlswort geholt werden soll).
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
reg [2:0] nextstate;
\newline
\newline
always @(inst or state)
\newline
if(state[2]) begin
\newline
<>
\newline
end else begin
\newline
casez(inst)
\newline
<>
\newline
endcase // casez(inst[0:2])
\newline
end // else: !if(state[2]) end
\newline
@
\layout Scrap
<>=
\newline
nextstate <= state[1:0] + { 2'b0, |state[1:0] };
\newline
@
\layout Scrap
<>=
\newline
5'b00000: nextstate <= state[1:0] + 3'b001;
\newline
5'b00???: nextstate <= 3'b100;
\newline
5'b10???: nextstate <= { 1'b1, state[1:0] };
\newline
5'b?????: nextstate <= state[1:0] + 3'b001;
\newline
@
\layout Subsection
ALU
\layout Standard
Die ALU berechnet einfach die Summe mit den verschiedenen möglichen Carry-ins,
die logischen Operationen, und ein Zero-Flag.
Zwar können hier gemeinsame Resourcen verwendet werden (die XORs des Volladdier
ers können auch die XOR-Operation machen, und die Carry-Propagation könnte
OR und AND berechnen), dieses Quetschen von Logik überlassen wir aber dem
Synthesetool.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
module alu(res, carry, zero, T, N, c, inst);
\newline
<>
\newline
\newline
wire `L sum, logic;
\newline
wire cout;
\newline
\newline
assign { cout, sum } =
\newline
T + N + ((c | andor) & selr);
\newline
assign logic = andor ?
\newline
(selr ? (T | N) : (T & N)) :
\newline
T ^ N;
\newline
assign { carry, res } =
\newline
prop ? { cout, sum } : { c, logic };
\newline
assign zero = ~|T;
\newline
\newline
endmodule // alu
\newline
@
\layout Standard
Die ALU hat die Ports T und N, carry in und die untersten 3 Bits des Befehls
als Input, ein Ergebnis, carry out und der Test auf 0 als Output.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
parameter l=16;
\newline
input `L T, N;
\newline
input c;
\newline
input [2:0] inst;
\newline
output `L res;
\newline
output carry, zero;
\newline
\newline
wire prop, andor, selr;
\newline
\newline
assign #1 { prop, andor, selr } = inst;
\newline
@
\layout Subsection
Stacks
\layout Standard
Die Stacks werden im FPGA als Block-RAM implementiert.
Dazu sollten sie am besten nur einen Port haben, denn solche Block-RAMs
gibt's auch in kleinen FPGAs.
Im ASIC wird diese Art von Stack mit Latches implementiert.
Dabei könnte man auch Read- und Write-Port trennen (oder für FPGAs, die
dual-ported RAM können), und sich den Multiplexer für
\family typewriter
spset
\family default
sparen.
\begin_inset ERT
status Collapsed
\layout Standard
\backslash
filbreak
\end_inset
\layout Scrap
<>=
\newline
module stack(clk, sp, spdec, push, in, out);
\newline
parameter dep=3, l=16;
\newline
input clk, push;
\newline
input [dep-1:0] sp, spdec;
\newline
input `L in;
\newline
output `L out;
\newline
reg `L stackmem[0:(1@<:
\emph default
Programmiere den Speicherbereich ab
\emph on
addr
\emph default
mit
\emph on
len
\emph default
Datenbytes
\layout Description
1
\emph on
addr, len:
\emph default
Lese
\emph on
len
\emph default
Bytes vom Speicherbereich ab
\emph on
addr
\emph default
zurück
\layout Description
2
\emph on
addr:
\emph default
Führe das Wort an
\emph on
addr
\emph default
aus.
\layout Standard
Diese drei Befehle reichen aus, um den b16 interaktiv zu bedienen.
Auch auf der Host-Seite reichen ein paar Befehle aus:
\layout Description
comp Kompiliert bis zum Ende der Zeile, und schickt das Ergebnis an das
Eval-Board
\layout Description
eval Kompiliert bis zum Ende der Zeile, schickt das Ergebnis ans Eval-Board,
führt den Code aus, und setzt den RAM-Pointer des Assemblers zurück an
den Ausgangspunkt
\layout Description
sim Wie
\family typewriter
eval
\family default
, nur wird das Kompilat nicht vom FPGA ausgeführt, sondern vom Emulator
\layout Description
check ( addr u --- ) Liest den entsprechenden Speicherbereich vom Eval-Board,
und zeigt ihn mit
\family typewriter
dump
\family default
an
\layout Section
Ausblick
\layout Standard
Mehr Material gibt's auf meiner Homepage
\begin_inset LatexCommand \cite{web}
\end_inset
.
Alle Quellen sind unter GPL verfügbar.
Wer ein bestücktes Board haben will, wendet sich am besten an
\noun on
Hans Eckes
\noun default
.
Und wer den b16 kommerziell verwenden will, an mich.
\layout Bibliography
\bibitem {c18}
\emph on
c18 ColorForth Compiler,
\emph default
\noun on
Chuck Moore
\noun default
,
\begin_inset Formula $17^{\mathrm{th}}$
\end_inset
EuroForth Conference Proceedings, 2001
\layout Bibliography
\bibitem {web}
\emph on
b16 Processor,
\emph default
\noun on
Bernd Paysan
\noun default
, Internet Homepage,
\begin_inset LatexCommand \url[http://www.jwdt.com/~paysan/b16.html]{http://www.jwdt.com/~paysan/b16.html}
\end_inset
\the_end