CID aplicatii 8 : Registre si memorii RAM

De la WikiLabs
Versiunea din 1 aprilie 2022 09:02, autor: Mihai.antonescu (Discuție | contribuții) (Pagină nouă: ==Teorie== Acest laborator are scopul de a prezenta circuitele secventiale simple: registre si memorii RAM. Pornind de la bistabilii prezentati/construiti in laboratorul ante...)
(dif) ← Versiunea anterioară | Versiunea curentă (dif) | Versiunea următoare → (dif)


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:

Registru exterior view.png


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:

Ram mXn 1read 1write exterior view.png


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:

Ram 64X8 2read 1write exterior view.png


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.

Registru paralel serie.png


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.

Registru serie paralel.png

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).