Implementace J1 ve Verilogu
Kapitola z knihy Data, čipy, procesory
Začneme společnými definicemi. Vytvoříme soubor common.h, který budeme později vkládat, kde bude zapotřebí.
`default_nettype none
`define WIDTH 16 -- Autor upravil i pro 32 bitů
`define DEPTH 4
Procesor obsahuje dva zásobníky, a protože jsou to reálné paměťové struktury, pojďme si je nadeklarovat jako moduly:
`include "common.h"
module stack
#(parameter DEPTH=4)
(input wire clk,
/* verilator lint_off UNUSED */
input wire resetq,
/* verilator lint_on UNUSED */
input wire [DEPTH-1:0] ra,
output wire [`WIDTH-1:0] rd,
input wire we,
input wire [DEPTH-1:0] wa,
input wire [`WIDTH-1:0] wd);
reg [`WIDTH-1:0] store[0:(2**DEPTH)-1];
always @(posedge clk)
if (we)
store[wa] <= wd;
assign rd = store[ra];
endmodule
Modul je parametrizovaný jedním parametrem, DEPTH (defaultní hodnota je 4). Parametr udává počet bitů adresy, tedy položek na zásobníku (těch je 2DEPTH). Předna-stavená hodnota 4 tedy znamená, že zásobník je pro 16 položek. Zásobník má dvě adresní sběrnice a dvě datové sběrnice – jeden pár pro čtení, jeden pro zápis. Vstup we (Write Enable) povoluje zápis – pokud je aktivní, zásobník zapíše hodnotu z datového vstupu wd na adresu ze vstupu wa. Na výstupu rd je stále obsah paměti z adresy ra. Zásobník sám o sobě neobsahuje žádnou další logiku, která by se starala o ukazatel, o jeho zvyšování a snižování a o podobné činnosti.
A takhle vypadá celá implementace procesoru, jak ji publikoval autor James Bowman
Dovolím si ji okomentovat a zdůraznit zajímavé pasáže:
`include "common.h"
module j1(
input wire clk,
input wire resetq,
output wire io_wr,
output wire [15:0] mem_addr,
output wire mem_wr,
output wire [`WIDTH-1:0] dout,
input wire [`WIDTH-1:0] mem_din,
input wire [`WIDTH-1:0] io_din,
output wire [12:0] code_addr,
input wire [15:0] insn
);
Port procesoru kromě „povinné jízdy“, tedy hodinového vstupu a RESETu, obsahuje sběrnice pro čtení instrukcí (code_addr posílá adresu, insn je šestnáctibitová instruk-ce, přečtená z paměti) a pro práci s pamětí dat a periferií.
Paměť dat je opravdu pouze pro data, zásobníky jsou implementovány hardwarově uvnitř.
Procesor posílá 16bitovou adresu paměti mem_addr. Zapisovaná data posílá po sběrnici dout. Pomocí signálů mem_wr a io_wr říká, zda se zapisuje do paměti, nebo do prostoru periferií. Paměti a periferie naopak přivádějí data po sběrnicích mem_din a io_din.
Následují deklarace ukazatelů a přístupových signálů pro zásobníky. Datový zásobník má registr dsp (Data Stack Pointer) a registr st0 (hodnota na vrcholu zásobníku). Tyto registry jsou zdvojené – pracovní registry udržují hodnoty, ke kterým se v aktuálním cyklu přistupuje. Pokud instrukce vyvolá změnu, mění se hodnota registru dspN (Data Stack Pointer New) nebo st0N, a ta je na konci cyklu zkopírována do pracovních registrů.
reg [`DEPTH-1:0] dsp; // Data stack pointer
reg [`DEPTH-1:0] dspN;
reg [`WIDTH-1:0] st0; // Top of data stack
reg [`WIDTH-1:0] st0N;
reg dstkW; // D stack write
reg [`DEPTH-1:0] rsp, rspN;
reg rstkW; // R stack write
wire [`WIDTH-1:0] rstkD; // R stack write value
Signály dstkW a rstkW slouží k zapsání hodnoty na zásobníky. U zásobníku R se zapi-sovaná hodnota přivádí po datové sběrnici rstkD.
reg [12:0] pc, pcN;
Program counter a jeho pracovní zrcadlo.
reg reboot = 1;
wire [12:0] pc_plus_1 = pc + 1;
Signál pc_plus_1 obsahuje přesně tu hodnotu, kterou čekáte, a slouží jako pomocný signál pro nejčastější případ, totiž že je nutno zvýšit čítač adres instrukcí.
assign mem_addr = st0N[15:0];
assign code_addr = {pcN};
Jako adresa do paměti dat je použita hodnota TOS na datovém zásobníku (resp. dol-ních 16 bitů), adresa do paměti kódu je připojena na registr pcN.
wire [`WIDTH-1:0] st1, rst0;
Signál st1 vlastně odpovídá FORTHovskému NOS (Next On Stack), tedy položce pod aktuální položkou. Aktuální položka je TOS a je v registru st0.
stack #(.DEPTH(`DEPTH))
dstack(.clk(clk), .resetq(resetq), .ra(dsp), .rd(st1), .we(dstkW), .wa(dspN), .wd(st0));
Instancujeme modul stack s hloubkou danou parametrem DEPTH. Hodiny a RESET jsou samozřejmost (RESET tedy nemá žádný efekt). Čtecí adresa je daná ukazatelem dsp, zapisovací adresa je dspN (logicky: zapisuje se na konci cyklu na adresu, kterou si v rámci instrukce teprve spočítáme). Přečtená data tvoří signál st1, data k zápisu jsou daná registrem st0. Zápis povoluje signál dstkW.
stack #(.DEPTH(`DEPTH))rstack(.clk(clk), .resetq(resetq), .ra(rsp), .rd(rst0), .we(rstkW), .wa(rspN), .wd(rstkD));
Až na konkrétní signály je definice zásobníku R téměř totožná.
Teď začíná ta nejvíc cool pasáž, totiž proces, který počítá novou hodnotu na zásobníku, a to na základě načteného instrukčního kódu. Všimněte si, že rozhoduje pouze horních 8 bitů (15 až 8). Pokud má tvar „1xxx_xxxx“, jde o zápis literálu, tj. konstanty, která se má přenést do st0. Takže je doplněna nulami zleva na plnou šířku datového slova.
U instrukcí „000x“ a „010x“ (JUMP a CALL) se stav st0 nemění, u podmíněného skoku se načte hodnota NOS (podmíněný skok „zkonzumuje“ hodnotu TOS.
U instrukcí s ALU („011x_iiii“) se podle příslušných bitů „i“ zvolí požadovaná operace.
always @*
begin
// Compute the new value of st0
casez ({insn[15:8]})
8'b1??_?????: st0N = { {(`WIDTH - 15){1'b0}}, insn[14:0] }; // literal
8'b000_?????: st0N = st0; // jump
8'b010_?????: st0N = st0; // call
8'b001_?????: st0N = st1; // conditional jump
8'b011_?0000: st0N = st0; // ALU operations...
8'b011_?0001: st0N = st1;
8'b011_?0010: st0N = st0 + st1;
8'b011_?0011: st0N = st0 & st1;
8'b011_?0100: st0N = st0 | st1;
8'b011_?0101: st0N = st0 ^ st1;
8'b011_?0110: st0N = ~st0;
8'b011_?0111: st0N = {`WIDTH{(st1 == st0)}};
8'b011_?1000: st0N = {`WIDTH{($signed(st1) < $signed(st0))}};
`ifdef NOSHIFTER
// `define NOSHIFTER in common.h to cut slice
// usage in half and shift by 1 only
8'b011_?1001: st0N = st1 >> 1;
8'b011_?1010: st0N = st1 << 1;
`else
// otherwise shift by 1-any number of bits
8'b011_?1001: st0N = st1 >> st0[3:0];
8'b011_?1010: st0N = st1 << st0[3:0];
`endif
8'b011_?1011: st0N = rst0;
8'b011_?1100: st0N = mem_din;
8'b011_?1101: st0N = io_din;
8'b011_?1110: st0N = , rsp, dsp};
8'b011_?1111: st0N = {`WIDTH{(st1 < st0)}};
default: st0N = {`WIDTH{1'bx}};
endcase
end
Všimněte si znaku „?“ jako placeholderu pro hodnotu, která nás v tu chvíli nezajímá.
Pokud v common.h definujeme konstantu `define NOSHIFTER, můžeme ušet-řit logické buňky a místo shiftování až o 16 pozic (uvažují se 4 bity ST0) na-definovat operaci posunu tak, že posouvá vždy o jediný bit.
wire func_T_N = (insn[6:4] == 1);
wire func_T_R = (insn[6:4] == 2);
wire func_write = (insn[6:4] == 3);
wire func_iow = (insn[6:4] == 4);
Bity 6, 5 a 4 instrukčního slova kódují požadovanou operaci, jak jsme si popisovali dříve.
wire is_alu = (insn[15:13] == 3'b011);
Signál is_alu je 1, pokud instrukce byla typu „011“, tedy operace s daty (ne skoky ani literál).
assign mem_wr = !reboot & is_alu & func_write;
assign dout = st1;
Signál pro zápis do externí paměti přijde ve chvíli, kdy není stav reboot, proběhla operace s ALU a zároveň byla požadována funkce „write“. Data, která se zapisují, jsou vždy v NOS (tedy druhá nejvyšší položka na zásobníku).
assign io_wr = !reboot & is_alu & func_iow;
Pro zápis do periferií platí totéž.
assign rstkD = (insn[13] == 1'b0) ? , pc_plus_1, 1'b0} : st0;
Data pro zápis do RS se liší podle stavu bitu 13 instrukčního slova. Pokud je 0 (mají ji instrukce JUMP a CALL), připraví se hodnota PC+1, pokud je 1 (mají ji JUMPZ nebo ALU), připraví se TOS.
Může se stát, že instrukce obsahuje literál, který má bit 13 nastavený nebo vynulovaný – je to jedno, protože tyto instrukce nevyvolají zápis do RS. Stej-ně tak instrukce JMP – sice připraví návratovou adresu, ale nezapíše ji.
reg [`DEPTH-1:0] dspI, rspI;
Hodnoty dspI a rspI představují inkrement ukazatele DS, resp. RS. Nejčastěji bývá jedničkový (do zásobníku přibyla hodnota), nulový (zásobník nemění svůj stav), nebo minus jedna (ze zásobníku ubyla hodnota).
always @*
begin
casez ({insn[15:13]})
3'b1??: {dstkW, dspI} = {1'b1, 4'b0001};
3'b001: {dstkW, dspI} = {1'b0, 4'b1111};
3'b011: {dstkW, dspI} = {func_T_N, {insn[1], insn[1], insn[1:0]}};
default: {dstkW, dspI} = {1'b0, 4'b0000};
endcase
dspN = dsp + dspI;
Podle typu instrukce se nastavuje inkrement pro ukazatel DS a zároveň signál zápisu do DS. Instrukce „literál“ (1xx) nastaví zápis na 1 a inkrement jedničkový. Podmíněný skok konzumuje hodnotu z datového zásobníku, takže zápisový bit je 0 a inkrement -1. Pro ALU se zápis řídí příznakem požadavku T->N a inkrement je dán bity 1 a 0 instrukčního slova (dstack). V ostatních případech se hodnota nemění a nic se nezapisuje.
casez ({insn[15:13]})
3'b010: {rstkW, rspI} = {1'b1, 4'b0001};
3'b011: {rstkW, rspI} = {func_T_R, {insn[3], insn[3], insn[3:2]}};
default: {rstkW, rspI} = {1'b0, 4'b0000};
endcase
rspN = rsp + rspI;
Podobná logika řídí i zásobník návratových adres RS. Instrukce CALL (010) zvyšuje ukazatel RS o 1 a zapisuje (rstkW=1). Instrukce ALU zapisuje, pokud aktivuje funkci T->R, a změna ukazatele se řídí bity 3 a 2 instrukčního slova (rstack).
casez ({reboot, insn[15:13], insn[7], |st0})
6'b1_???_?_?: pcN = 0;
6'b0_000_?_?,
6'b0_010_?_?,
6'b0_001_?_0: pcN = insn[12:0];
6'b0_011_1_?: pcN = rst0[13:1];
default: pcN = pc_plus_1;
endcase
end
Nepřekvapí vás, že se podobně jednoduše řeší i hodnota PC. Její nová hodnota je většinou PC+1, s několika výjimkami. Pokud je „reboot = 1“, je nová hodnota PC rov-na nule. Pro instrukce CALL a JUMP je nová hodnota PC rovna bitům 13 – 0 z instrukčního slova.
Pro instrukci JUMPZ to platí, pokud je st0 nulový. Pokud jde o instrukci ALU a má nastavený bit 7 (RET), nastaví se hodnota ze zásobníku návratových adres.
always @(negedge resetq or posedge clk)
begin
if (!resetq) begin
reboot <= 1'b1;
{ pc, dsp, st0, rsp } <= 0;
end else begin
reboot <= 0;
{ pc, dsp, st0, rsp } <= { pcN, dspN, st0N, rspN };
end
end
endmodule
A toto je poslední proces procesoru J1. Je aktivní při sestupné hraně signálu resetq nebo při náběžné hraně hodin. Při sestupné hraně se nastavuje reboot na 1 a nulují se registry pc, dsp, rsp a st0.
Při náběžné hraně hodin se nuluje reboot a hodnoty pc, dsp, rsp a st0 se nastavují podle svých zrcadlových registrů pcN, dspN, rspN a st0N na novou hodnotu.
Příklady použití, stejně jako definice základních FORThových slov, najdete na autorově stránce