Università degli studi di Modena e Reggio Emilia Facoltà di Ingegneria “Enzo Ferrari”
_______________________________________________________________________
SINTESI IN VHDL DEL
PROCESSORE ADE8 SU FPGA
A CURA DI MANUEL IADEROSA
In questa sezione verrà trattata l'implementazione del processore ADE8 in versione pipeline su scheda programmabile dal punto di vista hardware,
usando un linguaggio di descrizione dell'hardware.
Capitolo 1 – FPGA e VHDL
In questa sezione si parla dell'implementazione di ADE8 su scheda programmabile dal punto di vista hardware, chiamata FPGA.
Un Field Programmable Gate Array (FPGA) è un circuito integrato digitale programmabile direttamente dall'utente via software. E' composto da una grande matrice di gate elementari
configurabili e riconfigurabili implementando funzioni logiche complesse, per ottenere un sistema digitale su singolo chip. E' inoltre dotato di un elevato numero di blocchi di I/O (detti piedini) che vengono utilizzati per inviare o ricevere segnali sulla scheda.
Le FPGA si programmano attraverso un progetto di sintesi logica mediante l'uso dei cosiddetti linguaggi di descrizione dell'hardware, solitamente il VHDL, ovvero VHSIC Hardware Description Language (a sua volta VHSIC sta per Very High Speed Integrated Circuits).
Più precisamente il procedimento di sviluppo di circuiti logici digitali mediante tali linguaggi si compono di 4 fasi:
– Analisi: si verifica che non ci siano errori di sintassi e di semantica, si analizza ogni unità funzionale del circuito in maniera separata (conviene avere un blocco logico per ogni unità funzionale) e si inseriscono in una libreria le unità analizzate.
– Elaborazione: si creano le porte logiche i segnali e i processi dell'architettura, fino a realizzare ogni singolo componente
– Simulazione: si eseguono i processi realizzati nel modello elaborato, generando eventi quando un segnale cambia valore durante istruzioni di wait
– Sintesi: traduce il circuito utilizzato a livello RTL (Register Transfer Level) in una netlist a livello di gate, tradotta poi da un software apposito nel bitstream implementato nel
dispositivo hardware finale.
In questo caso viene utilizzata la scheda Altera DE2 che ospita la FPGA Altera Cyclone II 2C35.
E' anche dotata di una SRAM da 512 kbyte ,una SDRAM da 8Mbyte, una memoria flash da 4 Mbyte, un oscillatore da 50 Mhz e uno da 27 Mhz e vari dispositivi di I/O (pulsanti, interruttori, led, display ecc). La scheda è mostrata in figura 1.2.
Figura 1.2 Scheda Altera DE2 con FPGA Cyclone II
La scheda viene programmata in VHDL, il linguaggio maggiormente usato nella progettazione di sistemi elettronici digitali. Come già detto è un linguaggio di descrizione dell'hardware e non di programmazione. Sebbene presenti i tipici costrutti di un linguaggio di programmazione come il C (if, else, when, case...) esso descrive la costituzione di un componente specifico, dove tutti i costrutti vengono eseguiti contemporaneamente e non in maniera sequenziale.
Per meglio comprendere il codice presentato nei paragrafi successivi è possibile consultare il materiale fornito alla seguente pagina.
Il software utilizzato nella realizzazione del progetto è Quartus II versione 13.0 Service Pack 1 offerto dalla casa produttrice Altera. Tale software consente di progettare un circuito sia attraverso l'uso del VHDL, sia attraverso l'utilizzo di un ambiente di programmazione grafico, molto simile a quello messo a disposizione da Logisim, permettendo all'utente di lavorare con componenti di base definiti o precaricati come esempi.
Capitolo 2 – Realizzazione dei componenti
Viene di seguito riportata la realizzazione dei singoli componenti in VHDL realizzati mediante codice o schemi a blocchi, seguendo in ogni caso con un approccio bottom-up il progetto ottenuto con Logisim.
2.1 – Registro a 8 bit
Viene riportato il codice VHDL per la realizzazione di un generico registro a 8 bit. Il dato in ingresso viene campionato al fronte di salita del clock se il segnale IE è attivo. Vi sono poi 2 porte di uscita, una sempre abilitata al solo scopo di mostrare il contenuto del registro in ogni momento
(ad uno strumento di debug descritto in seguito), l'altra collegata al bus interno e pilotata dal segnale OE.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity RegisterN is port (
IE : in std_logic:='1';
OE : in std_logic:='1';
clk : in std_logic;
reset : in std_logic:='0';
D : in std_logic_vector((DATA_WIDTH -1) downto 0);
Q1 : out std_logic_vector((DATA_WIDTH -1) downto 0);
Q2 : out std_logic_vector((DATA_WIDTH -1) downto 0) );
end RegisterN;
architecture behav of RegisterN is
signal DataIn : std_logic_vector((DATA_WIDTH -1) downto 0):=(others=>'0');
begin
process(clk) begin
if(clk'event and clk='1') then if (reset='1') then
DataIn <= (others=>'0');
elsif (IE='1') then DataIn <= D;
end if;
end if;
end process;
Q1 <= DataIn;
Q2 <= DataIn when OE='1' else (others=>'Z');
end behav;
2.2 – ALU
Per la realizzazione dell'ALU a 8 bit si segue il modello ideato su LogiSim su cui 8 ALU a 1 bit vengono messe in cascata. Vengono riportati rispettivamente il codice VHDL di una generica ALU a 1 bit, e il codice VHDL utilizzato per la concatenazione delle singole ALU a 1 bit.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ALU_1bit is port (
A : in std_logic;
B : in std_logic;
S : in std_logic_vector (1 downto 0);
O : out std_logic;
Cout : out std_logic;
Cin : in std_logic );
end ALU_1bit;
architecture behav of ALU_1bit is begin
MainOut : with S select
O <= (A and B) when "00", (A or B) when "01",
(A xor B xor Cin) when "10",
(A xor (not B) xor Cin) when others;
CarryOut : with S select
COut <= ((A and B) or (A and Cin) or (B and Cin)) when "10",
((A and (not B)) or (A and Cin) or ((not B) and Cin)) when "11", '0' when others;
end behav;
_________________________________________________________
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity ALU_nbit is
port
(
A: in std_logic_vector((DATA_WIDTH -1) downto 0);
B: in std_logic_vector((DATA_WIDTH -1) downto 0);
O: out std_logic_vector((DATA_WIDTH -1) downto 0);
Cin: in std_logic;
S : in std_logic_vector (1 downto 0);
Flags : out std_logic_vector(7 downto 0):=(others=>'0') );
end ALU_nbit;
architecture behav of ALU_nbit is
signal carry_internal: std_logic_vector(DATA_WIDTH-1 downto 0):=(others=>'0');
signal O_internal: std_logic_vector(DATA_WIDTH-1 downto 0);
begin -- first bit
REG0 : work.ALU_1bit port map(
A => A(0), B => B(0), S => S,
O => O_internal(0), Cout => carry_internal(0), Cin => Cin);
bits: for N in 1 to (DATA_WIDTH -1) generate -- other bits
REGX : work.ALU_1bit port map(
A => A(N), B => B(N), S => S,
O => O_internal(N), Cout => carry_internal(N), Cin => carry_internal(N-1));
end generate;
-- output
O <= O_internal;
-- Zero flag
Flags(0) <= '1' when O_internal = ("00000000") else '0';
-- Negative flag
Flags(1) <= O_internal(DATA_WIDTH-1);
-- overflow flag
Flags(2) <= carry_internal(DATA_WIDTH-1) xor carry_internal(DATA_WIDTH- 2);
-- carry flag
Flags(3) <=carry_internal(DATA_WIDTH-1);
end behav;
2.3 – Incrementatore
I codici sottostanti mostrano il codice VHDL dell'adder che banalmente incrementa il contenuto del PC e la rete logica combinatoria che gestisce l'aggiornamento dei registri MAR e PC con le
informazioni presenti sul bus interno o con il valore del PC incrementato.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity INC is port (
I: in std_logic_vector((DATA_WIDTH -1) downto 0);
O: out std_logic_vector((DATA_WIDTH -1) downto 0) );
end INC;
architecture behav of INC is begin
O <= std_logic_vector(to_unsigned( to_integer(unsigned(I)) + 1, DATA_WIDTH));
end behav;
_________________________________________
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity INCSel is port (
I1 : in std_logic_vector((DATA_WIDTH -1) downto 0);
I2 : in std_logic_vector((DATA_WIDTH -1) downto 0);
IP : in std_logic_vector((DATA_WIDTH -1) downto 0);
INC : in std_logic;
O1 : out std_logic_vector((DATA_WIDTH -1) downto 0);
O2 : out std_logic_vector((DATA_WIDTH -1) downto 0) );
end INCSel;
architecture behav of INCSel is begin
O1 <= I1 when INC='0' else IP;
O2 <= I2 when INC='0' else IP;
end behav;
2.4 – Multiplexer e demultiplexer
I seguenti codici in VHDL corrispondono rispettivamente ai multiplexer e ai demultiplexer. I primi sono utilizzati, oltre che per selezionare il bus di provenienza da cui leggere le informazioni, anche per pilotare l'ingresso dell'ALU e del registro MDR.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity MuxBus is port (
BF : in std_logic_vector((DATA_WIDTH -1) downto 0);
BE: in std_logic_vector((DATA_WIDTH -1) downto 0);
S: in std_logic;
O: out std_logic_vector((DATA_WIDTH -1) downto 0) );
end MuxBus;
architecture behav of MuxBus is begin
O <= BF when S='0' else BE;
end behav;
______________________________________
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity DeMuxBus is port
(
B: in std_logic_vector((DATA_WIDTH -1) downto 0);
S: in std_logic;
O1: out std_logic_vector((DATA_WIDTH -1) downto 0);
O2: out std_logic_vector((DATA_WIDTH -1) downto 0) );
end DeMuxBus;
architecture behav of DeMuxBus is begin
O1 <= B when S='0' else (others=>'Z');
O2 <= B when S='1' else (others=>'Z');
end behav;
2.5 – Datapath
La figura 2.3 mostra il datapath completo, realizzato con uno schema a blocchi.
Figura 2.3 Schema a blocchi datapath completo.
2.6 – ROM di decodifica
Il seguente codice VHDL viene utilizzato per realizzare la ROM di decodifica dell'opcode. Da notare che il contenuto della memoria viene salvato sul file "DecodeROMContent.mif".
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity DecodeROM is
port (
opcode: in std_logic_vector((MP_ADDR_WIDTH-1) downto 0);
decopcode : out std_logic_vector((MP_ADDR_WIDTH-1) downto 0) );
end DecodeROM;
architecture rtl of DecodeROM is
-- Build a 2-D array type for the RAM
subtype word_t is std_logic_vector((MP_ADDR_WIDTH-1) downto 0);
type memory_t is array(2**MP_ADDR_WIDTH-1 downto 0) of word_t;
-- function init_ram
-- return memory_t is
-- variable tmp : memory_t := (others => (others => '0'));
-- begin
-- for addr_pos in 0 to 2**MP_ADDR_WIDTH - 1 loop -- -- Initialize each address with the address itself
-- tmp(addr_pos) := std_logic_vector(to_unsigned(addr_pos, MP_ADDR_WIDTH));
-- end loop;
-- return tmp;
-- end init_ram;
-- Declare the RAM signal and specify a default value. Quartus II -- will create a memory initialization file (.mif) based on the
-- default value.
signal dataram : memory_t; -- := init_ram;
attribute ram_init_file : string;
attribute ram_init_file of dataram :
signal is "DecodeROMContent.mif";
-- Register to hold the address
signal opcode_reg : natural range 0 to 2**MP_ADDR_WIDTH-1;
begin
opcode_reg <= to_integer(unsigned(opcode));
decopcode <= dataram(opcode_reg);
end rtl;
2.7 – Control Unit
Il codice sottostante è utilizzato per l'unità di controllo. Si può notare la mappatura dei segnali di controllo e la loro gestione tramite l'unità di controllo microprogrammata.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity ControlUnitMicro is port
(
IRP_EX : out std_logic;
PC_EX: out std_logic;
MAR_EX : out std_logic;
MDR_EX : out std_logic;
INC : out std_logic;
IRO_IE : out std_logic;
IRO_OE: out std_logic;
IRP_IE : out std_logic;
IRP_OE : out std_logic;
PC_IE : out std_logic;
PC_OE : out std_logic;
MAR_IE : out std_logic;
MDR_IE : out std_logic;
MDR_OE : out std_logic;
MDR_BUSIE : out std_logic;
MDR_BUSOE : out std_logic;
ACC_IE : out std_logic;
ACC_OE : out std_logic;
SP_IE : out std_logic;
SP_OE : out std_logic;
ALUOUT_IE : out std_logic;
ALUOUT_OE: out std_logic;
ALUA_IE: out std_logic;
FLAG_IE: out std_logic;
--FLAG_OE ZA: out std_logic;
ALU_S1: out std_logic;
ALU_S0: out std_logic;
Cin: out std_logic;
clk : in std_logic;
FLAGS: in std_logic_vector (3 downto 0);
Opcode: in std_logic_vector (7 downto 0);
uPc: out std_logic_vector (7 downto 0);
Tc: out std_logic_vector (15 downto 0);
reset: in std_logic );
end ControlUnitMicro;
architecture behav of ControlUnitMicro is
-- internal signal from the Microprogram memory signal jmpOpCode : std_logic :='0';
signal MC2 : std_logic :='0';
signal MC1 : std_logic :='0';
signal MC0: std_logic :='0';
signal currentMicroOp : std_logic_vector (MP_DATA_WIDTH-1 downto 0);
signal currentMicroPC : std_logic_vector (MP_ADDR_WIDTH-1 downto 0);
signal futureMicroPC : std_logic_vector (MP_ADDR_WIDTH-1 downto 0);
signal currentTickCounter : std_logic_vector (15 downto 0);
signal futureTickCounter : std_logic_vector (15 downto 0);
signal tmpInc : std_logic:='0';
signal MC:std_logic_vector(2 downto 0);
begin
MEM_MP: work.MicroROM port map(
data=>currentMicroOp, addr=>currentMicroPC);
process (clk) is
begin
if (clk='1') then
if (reset = '1') then
currentMicroPC <= (others=>'0');
currentTickCounter <= (others=>'0');
else
currentMicroPC <= futureMicroPC;
currentTickCounter <= futureTickCounter;
end if;
end if;
end process;
-- read of the current microOp IRP_EX <= currentMicroOp (31);
PC_EX <= currentMicroOp (30);
MAR_EX <= currentMicroOp (29);
MDR_EX <= currentMicroOp (28);
INC <= currentMicroOp (27);
IRO_IE<= currentMicroOp (26);
IRO_OE <= currentMicroOp (25);
IRP_IE <= currentMicroOp (24);
IRP_OE <= currentMicroOp (23);
PC_IE <= currentMicroOp (22);
PC_OE <= currentMicroOp (21);
MAR_IE <= currentMicroOp (20);
MDR_IE <= currentMicroOp (19);
MDR_OE <= currentMicroOp (18);
MDR_BUSIE <= currentMicroOp (17);
MDR_BUSOE <= currentMicroOp (16);
ACC_IE <= currentMicroOp (15);
ACC_OE <= currentMicroOp (14);
SP_IE <= currentMicroOp (13);
SP_OE <= currentMicroOp (12);
ALUOUT_IE <= currentMicroOp (11);
ALUOUT_OE <= currentMicroOp (10);
ALUA_IE <= currentMicroOp (9);
FLAG_IE <= currentMicroOp (8);
--FLAG_OE
ZA <= currentMicroOp (7);
ALU_S1 <= currentMicroOp (6);
ALU_S0 <= currentMicroOp (5);
Cin <= currentMicroOp (4);
-- internal signal
JmpOpCode <= currentMicroOp (3);
MC2 <= currentMicroOp (2);
MC1 <= currentMicroOp (1);
MC0<= currentMicroOp (0);
-- compute tmpInc
MC <=MC2&MC1&MC0;
FlagSel : with (MC) select tmpInc <= '0' when "000",
'1' when "001",
FLAGS(0) when "010", --z FLAGS(1) when "011", -- n FLAGS(2) when "100", -- o FLAGS(3) when "101", -- c not FLAGS(3) when "110", -- c
FLAGS(1) xor FLAGS(2) when others;
-- selection of the future micro program counter based on internal signals futureMicroPC <= Opcode when JmpOpCode='1' else
std_logic_vector(to_unsigned( to_integer(unsigned(currentMicroPC)) + 1, MP_ADDR_WIDTH))
when tmpInc='1' else (others=>'0');
futureTickCounter <=
std_logic_vector(to_unsigned( to_integer(unsigned(currentTickCounter)) + 1, 2*MP_ADDR_WIDTH));
uPc <= currentMicroPC;
Tc <= currentTickCounter;
end behav;
2.8 – MicroROM
Il codice VHDL della memoria del microcodice è mostrato qui sotto (contenuto della microrom nel file "ROMContent.mif").
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ade8config.all;
entity MicroROM is
port
(
addr : in std_logic_vector((MP_ADDR_WIDTH-1) downto 0);
data : out std_logic_vector((MP_DATA_WIDTH-1) downto 0) );
end MicroROM;
architecture rtl of MicroROM is
-- Build a 2-D array type for the RAM
subtype word_t is std_logic_vector((MP_DATA_WIDTH-1) downto 0);
type memory_t is array(2**MP_ADDR_WIDTH-1 downto 0) of word_t;
-- function init_ram
-- return memory_t is
-- variable tmp : memory_t := (others => (others => '0'));
-- begin
-- for addr_pos in 0 to 2**MP_ADDR_WIDTH - 1 loop -- -- Initialize each address with the address itself
-- tmp(addr_pos) := std_logic_vector(to_unsigned(addr_pos, MP_DATA_WIDTH));
-- end loop;
-- return tmp;
-- end init_ram;
-- Declare the RAM signal and specify a default value. Quartus II -- will create a memory initialization file (.mif) based on the
-- default value.
signal dataram : memory_t; -- := init_ram;
attribute ram_init_file : string;
attribute ram_init_file of dataram : signal is "ROMContent.mif";
-- Register to hold the address
signal addr_reg : natural range 0 to 2**MP_ADDR_WIDTH-1;
begin
addr_reg <= to_integer(unsigned(addr));
data <= dataram(addr_reg);
end rtl;
2.9 – ADE8
La figura 2.4 mostra lo schema a blocchi contenente le macro componenti di ADE8
Figura 2.4 Schema a blocchi dei macrocomponenti di ADE8
2.10 – Motherboard
La figura 2.5 mostra la scheda madre che ospita tutti i componenti che si collegano ADE8, il cui codice o schema a blocchi può essere consultato nella relativa sessione del sito.
Figura 2.5 Schema a blocchi ADE8 e periferiche esterne.
Si può notare che il segnale di clock che riceve in ingresso ADE8 è modificato in modo da permetterne il debug durante l'esecuzione di un programma.Più precisamente in base allo stato in cui si trova l'interruttore (denominati run e debug) ADE8 può ricevere il segnale originale generato dall'oscillatore a 50 Mhz, oppure un clock fittizio generato manualmente all'attivarsi di un altro interruttore ,opportunamente attenuato per evitare disturbi dovuti alla meccanica dell'interruttore (si veda la tabella 2.6).
SW[17] (reset) SW[16](run/debug) SW[15] (step) Funzione
0 0 - Esecuzione normale
0 1 0 Debug – clock basso
0 1 1 Debug – clock alto
1 - - Reset ADE8
Tabella 2.6 Modalità di esecuzione programmi.
Come è stato introdotto in precedenza, i valori che assumono i registri sono consultabili in un qualunque momento grazie ad un multiplexer, che in base ai 3 segnali di selezione (3 segnali per 8 registri) redirige l'uscita del registro desiderato su un display a 7 segmenti (tabella 2.7).
SW[14] SW[13] SW[12] Output su display
0 0 0 ACC
0 0 1 ALUOUT
0 1 0 IRO
0 1 1 IRP
1 0 0 MAR
1 0 1 MDR
1 1 0 PC
1 1 1 SP
Tabella 2.7 Debug dei registri.
Gli altri display sono utilizzati per l'output dei dati (7 segmenti o lcd) mentre un altro viene pilotato da un interruttore (SW[11]) per mostrare il conteggio dei cicli di clock su 4 valori a 7 segmenti oppure per mostrare l'opcode corrente su 2 valori a 7 segmenti .
Per l'input a 8 bit è invece possibile configurare 8 interruttori al momento dell'istruzione di lettura da parte della CPU.
Il file per realizzare la memoria RAM è molto simile a quello utilizzato per la ROM di decodifica e per la memoria del microcodice, ma in questo caso il contenuto della memoria centrale viene aggiornato di volta in volta nel file "RAMContent.mif" in base al programma desiderato. Ogni volta che il progetto verrà caricato sulla scheda Altera, verrà eseguito il programma presente il quel momento sulla memoria centrale. Siccome tale maniera di utilizzare la CPU può risultare scomoda, si può utilizzare un'ulteriore funzionalità offerta dalla scheda Altera DE2, che è la programmazione seriale.
Essa consiste nel poter inviare dati sulla scheda direttamente da un qualsiasi terminale connesso ad essa mediante la porta RS-232. Lo standard per tale comunicazione impone un baudrate di 9600 bps con un campionamento di 8 bit ogni 125 microsecondi. Non vengono usati bit di parità per la
verifica degli errori e si utilizza 1 bit di stop per separare le parole (tale specifica si indica con la notazione 8N1). Il blocco logico SerialProg in alto nella figura permette di effettuare questa comunicazione (si omette il codice per la sua dimensione, è tuttavia consultabile nel sito). Così facendo, una volta stabilita la comunicazione, basterà utilizzare un qualunque terminale (nel caso specifico si utilizza RealTerm), per iniziare a inviare dati sulla seriale che verranno poi scritti, in base alle specifiche di progetto, sulla memoria centrale.
Il blocco logico SerialProg è strutturato in modo tale che tenga il sistema in attesa durante tutta la trasmissione lasciando attivo il segnale di reset ricevuto da tutte le componenti. Dopo che nessun dato viene inviato per 5 secondi, il segnale di reset viene messo a 0 e il processore inizia a leggere le istruzioni caricate in memoria.
La mappatura dei dispositivi della scheda con le loro funzionalità è riportata in tabella 2.8.
I/O Funzione
SW[17] Reset
SW[16] Modalità run/debug
SW[15] Clock manuale
SW[14] Debug registri
SW[13] Debug registri
SW[12] Debug registri
SW[11] Se non attivo mostra microPC su HEX0 e
HEX1, se attivo mostra il TickCounter su HEX0, HEX1, HEX2 ed HEX3
SW[7..0] Input a 8 bit
KEY[3] Polling
HEX0 e HEX1 Output microPC o TickCounter
HEX2 e HEX3 Output TickCounter
HEX4 e HEX5 Output registri
HEX6 e HEX7 Output bus dei dati
LCD Output bus dei dati
LEDR[3..0] Output registro di FLAG
LEDR[4] Stato programmazione seriale
LEDR[5] Stato reset CPU
LEDR[8] Stato RD
LEDR[9] Stato WR
LEDG[7..0] Contenuto RAM all'indirizzo corrente
Tabella 2.8. Mappatura completa pin-funzionalità.