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 se pot stoca date. Adaugand mai multi registri in paralel, unul langa altul, si cateva circuite de tip mux/demux se poate obtine o memorie de tip RAM (Random Access Memory), cu "m" locatii de "n" biti fiecare. Aceste memorii se cheama "Random Access" deoarece permit si scriere si citire. In functie de tehologia folosita, memoriile RAM pot fi construite din bistabili sau din latch-uri.
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 mai apoi in semnale de date si semnale de control. Interfata sa este:
- Semnalul de "clock": controleaza sincronizarea registrilor din memorie.
- Semnalul "addr_read": adresa de la care se citesc datele.
- Semnalul "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 "addr_write": adresa la care se scriu datele.
- Semnalul "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 este 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 aiba reset, utilizatorul trebuie apoi sa aiba 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.sv):
module register_8b
(
input logic clock,
input logic reset, // activ pe "1"
input logic we,
input logic [7:0] data_in,
output logic [7:0] data_out // data_out are aceeasi dimensiune ca data_in
);
always_ff @(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.sv):
module register_8b_v2
(
input logic clock,
input logic reset_n, // activ in "0"
// uzual semnalel cu n in fata sau la sfarsit sunt in logica negativa: nreset, resetn
input logic we,
input logic [7:0] data_in,
output logic [7:0] data_out
);
logic [7:0] memorie_efectiva;
assign data_out = memorie_efectiva;
always_ff @(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.sv):
`timescale 1ns / 1ps
module register_8b_tb();
logic clock_tb;
logic reset_tb;
logic we_tb;
logic [7:0] data_in_tb;
logic [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
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.sv):
module ram64x8_v1
(
input logic clock,
//interfata de citire
input logic [5:0] addr_read, // 64 locatii => 6 biti de adresa
output logic [7:0] data_read, // fiecare locatie are 8b
// interfata de scriere
input logic we,
input logic [5:0] addr_write,
input logic [7:0] data_write
);
logic [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_ff @(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.sv):
module ram64x8_v2 // varianta cu always combinational pe citire
(
input logic clock,
//interfata de citire
input logic [5:0] addr_read,
output logic [7:0] data_read,
// interfata de scriere
input logic we,
input logic [5:0] addr_write,
input logic [7:0] data_write
);
logic [7:0] memorie_efectiva [0:63];
always_comb // citire asincrona fata de clock
begin
data_read = memorie_efectiva[addr_read];
end
always_ff @(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.sv):
`timescale 1ns / 1ps
module ram64x8_tb();
logic clock_tb;
logic [5:0] addr_read_tb;
logic [7:0] data_read_tb;
logic we_tb;
logic [5:0] addr_write_tb;
logic [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 forma de unda de mai sus sunt marcate momentele de timp cand au loc scrieri in memorie (cand am write enable activ si valori cunoscute pentru data si adresa de scriere). Se salveaza valorile imediat la stanga frontului de ceas.
In simularea completa, se poate observa si scrierea in registrii din memorie a datelor dorite la adresa setata.
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 simularea si apoi testarea fizica a acestei memorii. Simularea noii memorii se face prin executarea a 3 scrieri la adrese diferite si citirea apoi a datelor respective. Sinteza si testarea fizica necesita micsorarea memoriei din cauza numarului limitat de switchuri/butoane al placii. Vom lucra acum 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.
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:
Generati forme de unda corespunzatoare pentru a testa aceasta memorie.
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.
Generati forme de unda corespunzatoare pentru a testa aceast circuit (minim 3 scrieri/serializari).
Bonus: Realizati acelasi circuit 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.
Generati forme de unda corespunzatoare pentru a testa aceast circuit (minim 3 scrieri/paralelizari).
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" cicli 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" cicli de ceas se obtine prin punerea in serie a "x" registri.
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).