CID aplicatii 8 : Registre si memorii RAM
Teorie
Acest laborator are scopul de a prezenta circuitele secventiale simple: registre si memorii RAM.
Pornind de la bistabilii prezentati/construiti in laboratorul anterior, apare notiunea de registru, acesta fiind o grupare de bistabili. Astfel in loc sa se memoreze un singur bit de informatie (bistabil), se pot memora acum numere pe mai multi biti (registru).
Din exterior, un registru este vazut astfel:
- Semnalul de "clock" controleaza sincronizarea registrilor din tot sistemul.
- Semnalul de "reset" aduce registrul la o valoare initiala (uzual 0).
- Semnalul de "we" (write enable) controleaza salvarea unor date noi. Cand acesta este activ, data de pe intrarea "data_in" se salveaza in registru.
- Semnalul "data_in" reprezinta datele ce se doresc a fi scrise in registru.
- Semnalul "data_out" reprezinta valoarea stocata in registru.
Semnalele "data_in" si "data_out" pot fi pe oricat de multi biti se doreste, in mod uzual multipli de 8.
Pornind de la notiunea de registru, care poate fi vazut ca o memorie cu o locatie si avand "m" biti, se doreste cresterea acestei memorii astfel incat aceasta sa curpinda mai multe locatii adresabile, unde sa se poata stoca date. Adaugand mai multi registri in paralel, unul langa altul si cateva circuite de tip mux/demux se obtine o memorie de tip RAM (Random Access Memory), aceasta fiind cu "m" locatii de "n" biti fiecare. Aceste memorii se cheama "Random Access" deoarece permit si scriere si citire.
Din exterior, o memorie RAM este vazuta astfel:
Semnalele acestui circuit se pot imparti in semnale ce tin de interfata de scriere sau interfata de citire si apoi mai in semnale de date si semnale de control. Interfata sa este:
- Semnalul de "clock" : controleaza sincronizarea registrilor din memorie.
- Semnalul de "addr_read" : adresa de la care se citesc datele.
- Semnalul de "data_read" : data ce se citeste.
- Semnalul de "we" : semnal de control ce controleaza activarea scrierii. Scrierea are loc doar cand acest semnal este activ.
- Semnalul de "addr_write" : adresa la care se scriu datele.
- Semnalul de "data_write" : data ce urmeaza a fi scrisa atunci cand semnalul "we" este activ.
Observatie: Orice memorie are "m" locatii de "n" biti. Semnalele de "data_read" si "data_write" au aceeasi dimensiune, "n", numarul de biti ai fiecarei locatii. Semnalele de adresa au dimensiunea log2(m). Pentru o memorie cu 16 locatii va fi nevoie de 4 biti de adresa pentru a putea selecta orice locatie, pentru 32 locatii 5b s.a.m.d.
Observatie: In unele situatii se mai poate pune un registru suplimentar pe iesirea datelor, cu rol in sincronizare si evitarea timpilor de propagare prea lungi intre elemente de memorare (ajuta implementarea conceptului de pipeline).
Observatie: Exista mai multe variante de memorii ram (cu registru pe iesire/fara, multiport/single port citire, adrese separate sau nu pentru citire/scriere). Cea de mai sus fiind o memorie cu un singur port de citire, fara registru suplimentar pe iesire, ce foloseste adrese distincte pentru citire si pentru scriere (la fel de bine se poate lucra si cu o singura adresa, comuna pentru scriere si citire).
Observatie: Memoriile RAM pot sa nu aibe reset, utilizatorul trebuie apoi sa aibe grija sa citeasca doar din locatii scrise de el anterior. In mod uzual ele nu au reset.
Exemple
Exemplul 1 : Registrul
In urmatorul exemplu se implementeaza registrul descris mai sus:
Descrierea registrului (fisierul register_8b.v):
module register_8b
(
input wire clock,
input wire reset, // activ pe "1"
input wire we,
input wire [7:0] data_in,
output reg [7:0] data_out // data_out are aceeasi dimensiune ca data_in
);
always@(posedge clock) // clock sincronizeaza actiunile circuitului
begin // doar pe edge-ul pozitiv circuitul actioneaza
if(reset == 1)
begin
data_out <= 0;
end
else
begin
if(we == 1) // comanda de scriere
begin
data_out <= data_in;
end
else // puteam sa omit acest else
begin
data_out <= data_out; // raman datele salvate anterior
end
end
end
endmodule
Un alt mod de a scrie un registru este dat mai jos, cu observatia ca aici am pus semnalul de reset activ in logica negativa:
Descrierea registrului (fisierul register_8b_v2.v):
module register_8b_v2
(
input wire clock,
input wire reset_n, // activ in "0"
// uzual semnalel cu n in fata sau la sfarsit sunt in logica negativa: nreset, resetn
input wire we,
input wire [7:0] data_in,
output wire [7:0] data_out
);
reg [7:0] memorie_efectiva;
assign data_out = memorie_efectiva;
always@(posedge clock)
begin
if( reset_n == 0)
begin
memorie_efectiva <= 0;
end
else
begin
if(we == 1)
begin
memorie_efectiva <= data_in;
end
end
end
endmodule
Descrierea test bench-ului registrului pe 8biti: (fisierul register_8b_tb.v):
`timescale 1ns / 1ps
module register_8b_tb();
reg clock_tb;
reg reset_tb;
reg we_tb;
reg [7:0] data_in_tb;
wire [7:0] data_out_tb;
register_8b dut // varianta cu reset activ in "1"
(
.clock(clock_tb),
.reset(reset_tb),
.we(we_tb),
.data_in(data_in_tb),
.data_out(data_out_tb)
);
initial
begin
clock_tb = 0;
forever
begin
#5 clock_tb = ~clock_tb; // perioada totala 10 !!!
end
end
initial
begin
reset_tb = 0;
we_tb =0;
data_in_tb = 0;
// observatie: pana la primul reset sau prima scriere, valoarea din registru va fi necunoscuta (in simulare X)
// dau reset la circuit
@(posedge clock_tb); // astept sa treaca 1 clock cycle
reset_tb = 1;
@(posedge clock_tb);
reset_tb = 0;
repeat(5) // dupa 5 cicli de ceas
begin
@(posedge clock_tb);
end
// incep sa fac scrieri
we_tb =1;
data_in_tb = 5;
@(posedge clock_tb);
we_tb =0;
data_in_tb = 10; // scrierea asta nu se face deoarece nu am write enable activ
@(posedge clock_tb);
data_in_tb = 11; // nici asta
@(posedge clock_tb);
data_in_tb = 12; // nici asta
@(posedge clock_tb);
we_tb =1;
data_in_tb = 42; // asta da
@(posedge clock_tb);
data_in_tb = 51; // si asta da
@(posedge clock_tb);
we_tb =0;
repeat(5) // dupa 5 cicli de ceas
begin
@(posedge clock_tb);
end
$stop();
end
endmodule
Proiectul complet se poate descarca de aici: Exemplu_registru_8b.zip
Exemplul 2: Memorie RAM
In urmatorul exemplu se implementeaza memoria RAM descrisa mai sus, particularizata pentru 64 de locatii a cate 8b fiecare :
Descrierea memoriei RAM: (fisierul ram64x8_v1.v):
module ram64x8_v1
(
input wire clock,
//interfata de citire
input wire [5:0] addr_read, // 64 locatii => 6 biti de adresa
output wire [7:0] data_read, // fiecare locatie are 8b
// interfata de scriere
input wire we,
input wire [5:0] addr_write,
input wire [7:0] data_write
);
reg [7:0] memorie_efectiva [0:63]; // memorie cu locatiile de la 0 la 63, fiecare avand 8 biti
assign data_read = memorie_efectiva[addr_read]; // fara registru pe iesire => citire asincrona fata de clock
always@(posedge clock)
begin
if(we == 1)
begin
memorie_efectiva[addr_write] = data_write; // scriu la locatia data de "addr_write" din memoria efectiva datele "data_write"
end
end
endmodule
Acesta va fi folosit si pe post de modul de "top".
Echivalent se poate folosi si sintaxa cu always ca mai jos.
Descrierea memoriei RAM: (fisierul ram64x8_v2.v):
module ram64x8_v2 // varianta cu always combinational pe citire
(
input wire clock,
//interfata de citire
input wire [5:0] addr_read,
output reg [7:0] data_read,
// interfata de scriere
input wire we,
input wire [5:0] addr_write,
input wire [7:0] data_write
);
reg [7:0] memorie_efectiva [0:63];
always@(*) // citire asincrona fata de clock
begin
data_read = memorie_efectiva[addr_read];
end
always@(posedge clock)
begin
if(we == 1)
begin
memorie_efectiva[addr_write] = data_write; // scriu la locatia data de "addr_write" din memoria efectiva datele "data_write"
end
end
endmodule
Descrierea test bench-ului memoriei RAM: (fisierul ram64x8_v1_tb.v):
`timescale 1ns / 1ps
module ram64x8_tb();
reg clock_tb;
reg [5:0] addr_read_tb;
wire [7:0] data_read_tb;
reg we_tb;
reg [5:0] addr_write_tb;
reg [7:0] data_write_tb;
ram64x8_v1 dut
(
.clock(clock_tb),
//interfata de citire
.addr_read(addr_read_tb), // 64 locatii => 6 biti de adresa
.data_read(data_read_tb), // fiecare locatie are 8b
// interfata de scriere
.we(we_tb),
.addr_write(addr_write_tb),
.data_write(data_write_tb)
);
initial
begin
clock_tb = 0;
forever
begin
#5 clock_tb = ~clock_tb; // perioada totala 10 !!!
end
end
initial
begin
we_tb =0;
data_write_tb = 0;
addr_read_tb = 0; // citind de la o adresa nescrisa inca, iesirea e necunoscuta
addr_write_tb = 0;
repeat(5) // dupa 5 cicli de ceas
begin
@(posedge clock_tb);
end
// incep sa fac scrieri
we_tb =1; // scriu data 5 la adresa 10
addr_write_tb = 10;
data_write_tb = 5;
addr_read_tb = 11; // citind de la o adresa nescrisa inca, iesirea e necunoscuta
@(posedge clock_tb);
we_tb =0; // scrierea asta nu se face deoarece nu am write enable activ
addr_write_tb = 11;
data_write_tb = 10;
@(posedge clock_tb);
data_write_tb = 11; // nici asta
@(posedge clock_tb);
data_write_tb = 12; // nici asta
@(posedge clock_tb);
we_tb =1;
addr_write_tb = 20; // scriere ok
data_write_tb = 42; // scriu data 42 la adresa 20
@(posedge clock_tb);
data_write_tb = 51; // si asta; suprascriu datele anterioare la adresa 20.
@(posedge clock_tb);
addr_write_tb = 21;
data_write_tb = 11; // scriere ok
addr_read_tb = 20; // citesc de la adresa 20, scrisa anteior deci voi vedea date cunoscute pe iesire.
@(posedge clock_tb);
addr_write_tb = 23;
data_write_tb = 14; // scriere ok
addr_read_tb = 21; // variez adresa de citire si pot parcurge memorie locatie cu locatie
@(posedge clock_tb);
we_tb =0; // opresc scrierea
repeat(5) // dupa 5 cicli de ceas
begin
@(posedge clock_tb);
end
$stop();
end
endmodule
In simulare, se poate observa scrierea in registrii din memorie a datelor dorite.
Proiectul complet se poate descarca de aici: Exemplu_ram64x8.zip
Exercitii
Exercitiul 1: RAM cu registru la iesire
Pornind de la exemplul anterior, se doreste adaugarea unui registru la citirea datelor, astfel citirea devenind sincrona cu semnalul de ceas. In cod acest lucru se poate face foarte usor, punand operatia de citire pe ceas.
Se doreste de asemenea testarea fizica a acestei memorii si din cauza numarului limitat de switchuri/butoane al placii, se doreste micsorarea ei, astfel incat sa lucram cu o memorie de 8 locatii X 4 biti. Adresa va fi comandata de switch-uri si datele de intrare din butoane. Se va folosi Button[0] pentru semnalul de "we" (write enable). Afisarea datelor se va face pe leduri.
Inainte de testare fizica se doreste si simularea noii memorii prin executarea a 3 scrieri la adrese diferite si citirea apoi a datelor respective.
Exercitiul 2: RAM multiport citire
Pornind de la exercitiul 1 se doreste modificarea memoriei astfel incat sa poata fi citite 2 locatii simultan si independent de adresa la care se face scrierea. Se pastreaza citirea sincrona.
Se obtine astfel un circuit a carui interfata este la randul ei compusa din 3 interfete (+semnal comun "clock"):
- - interfata de scriere
- - interfata de citire 0
- - interfata de citire 1
Interfata circuitului arata ca in figura de mai jos:
Observatie: Blocul de registri (register file) din interiorul procesoarelor (vedeti AMP) poate fi o astfel de memorie RAM. Are 2 adrese pentru citirea celor 2 operanzi care intra in ALU si o adresa pentru rezultatul calculului ce este salvat.
Exercitiul 3: Registrul paralel-serie
Descrieti comportamental un registru paralel-serie. Acesta are rolul de a salva datele de la intrare pe "n" biti (aici 8) si apoi de a le scoate la iesire bit cu bit.
Interfata acestuia este data in desenul de mai jos.
- Semnalul de "en" (enable) are rolul de a controla deplasarea, care se executa prin operatia de shiftare la dreapta ">>". Daca acesta are valoarea "1", datele salvate se muta la dreapta cu o pozitie.
- Semnalul de write_en/start/save are rolul de a salva cei 8 biti care urmeaza a fi serializati.
Bonus: Realizati acelasi circuit si folosind o descriere structurala.
Exercitiul 4: Registrul serie-paralel
Descrieti comportamental un registru serie-paralel. Acesta are rolul de a salva date introduse bit cu bit ca apoi sa fie scoase cate "n" biti deodata.
Interfata acestuia este data in desenul de mai jos.
Semnalul de "en" (enable) are rolul de a controla deplasarea, care se executa prin operatia de shiftare la dreapta ">>". Daca acesta are valoarea "1", datele salvate se muta la dreapta cu o pozitie.
Citirea se poate face oricand, desi doar dupa "n" pasi, va fi cu valoarea corecta.
Exercitiul 5: Registrul de intarziere pe 8b
Prin conectarea in serie a mai multor registri pe 8b astfel incat data ce iese din unul sa fie intrare pentru urmatorul, se pot construi registri de intarziere cu "x" ciclii de ceas. Cum fiecare registru salveaza datele pe ceas, se poate spune ca datele de la iesire sunt intarziate cu un ceas fata de datele de la intrare, astfel o intarziere cu "x" ciclii de ceas se obtine prin punerea in serie a "x" registrii.
Pornind de la un registru simplu pe 8b (exemplul 1), construiti structural un registru de intarziere cu 4 cicli de ceas.
Testati functionarea acestuia prin simulare. Datale introduse trebuie sa iasa defazate cu 4 cicli de ceas (adica cu 4 cicli de ceas mai tarziu).