■ CACHE 구조
https://insoobaik.tistory.com/661
CACHE에 대한 내용은 위 글을 참조하길 바랍니다.
■ Block Diagram
■ 설계 조건 및 입/출력 Port
- 설계 조건
Block size = 4B
Memory size = 256B (64 Blocks)
Cache size = 16B (4 Sets)
Write-back
req & ack handshaking
One req at a time
- 입/출력 Port
- clk : 동기화를 위한 Clock 신호를 전달하기 위한 Port
- rstn : 비동기 reset을 위한 Port
- i_cpu_req : CPU로부터 전달되는 req 신호를 전달하기 위한 Port
- i_cpu_write : CPU로부터 전달되는 Read/Write 신호를 전달하기 위한 Port
- o_cpu_ack : Cache에서 CPU로 req 신호에 대한 ack 신호를 전달하기 위한 Port
- i_cpu_addr : CPU가 전달하는 addr(Main Memory) 신호를 전달하기 위한 Port
- i_cpu wdata : CPU가 전달하는 Write Data 신호를 전달하기 위한 Port
- o_cpu_rdata : CPU에서 Read 작업이 이루어질 때 Cache에 있는 Data를 전달하기 위한 Port
- o_mem_req : Cache에서 Main Memory로 req 신호를 전달하기 위한 Port
- o_mem_write : Cache에서 Main Memory로 Read/Write 신호를 전달하기 위한 Port
- i_mem_ack : Main Memory로부터 전달되는 ack 신호를 받기 위한 Port
- o_mem_addr : Cache에서 Main Memory에 접근할 addr를 전달하기 위한 Port
- o_mem_wdata : Cache에서 Main Memory에 Write할 Data를 전달하기 위한 Port
- i_mem_rdata : : Main Memory로부터 Read Data를 전달받기 위한 Port
■ cache.v 코드 분석
module cache(
input clk, rstn,
input i_cpu_req, i_cpu_write,
output o_cpu_ack,
input [5:0] i_cpu_addr,
input [31:0] i_cpu_wdata,
output [31:0] o_cpu_rdata,
output o_mem_req, o_mem_write,
input i_mem_ack,
output [5:0] o_mem_addr,
output [31:0] o_mem_wdata,
input [31:0] i_mem_rdata
);
위 코드는 위에서 설명한 입출력 Port에 대한 정의를 나타낸다.
reg [3:0] valid_mem, wb_mem;
wire cpu_en = i_cpu_req & o_cpu_ack ;
wire cpu_we = i_cpu_req & o_cpu_ack & i_cpu_write;
wire cpu_re = i_cpu_req & o_cpu_ack & ~i_cpu_write;
wire mem_en = o_mem_req & i_mem_ack ;
wire mem_we = o_mem_req & i_mem_ack & o_mem_write;
wire mem_re = o_mem_req & i_mem_ack & ~o_mem_write;
wire cc_we = mem_re | cpu_we;
wire [1:0] cc_wa = i_cpu_addr[1:0];
wire cc_re = i_cpu_req;
wire [1:0] cc_ra = i_cpu_addr[1:0];
cache line 구성 조건에서 4개라고 정의를 해두었고, 위와 같이 4개의 line을 가지에 된다.
valid_mem은 현재 Cache Line에 Data가 유효한지 확인하는 변수다.
wb_mem은 현재 Cache Line에 Tag나 Data가 변경될 시, Write Back을 수행해야하는지 확인하는 변수다.
그 아래 CPU, Memory의 동작 조건을 정의하고 있다.
- en(enable) : req(요청), acK(응답)이 서로 완료되면 Data를 주고 받을 준비가 된 상태를 의미한다.
- we(wrtie enable) : 쓰기 상태를 의미한다.
- re(read enable) : 읽기 상태를 의미한다.
그 아래는 Cache 동작 조건을 정의하고 있다.
- we(write enable) : Cache가 we가 되기위한 조건은 Memory에서 읽어오거나, CPU에서 쓰기를 요청하거나 둘 중 하나만 만족하면 신호가 1로 변경된다.
- wa(write addr) : Cache는 Index 부분을 통해 Data를 확인하기 때문에 (위 조건에서는 CPU가 전달한 addr 중 하위 2비트를 사용) CPU가 전달한 addr 중 Index 부분을 확인한다.
- re(read enable) : CPU에서 요청(읽기)이 들어오게 되면 Cache에서 Read하기 위한 신호가 변경된다.
- ra(read addr) : 쓰기와 마찬가지로 쓰거나 읽을 때 전부 CPU에서 전달한 명령어 중 Index 부분을 확인해야하기 때문에 CPU addr의 하위 2비트를 가져온다.
reg cc_re_d;
reg valid_rd, wb_rd;
wire [3:0] tag_rd;
wire [31:0] data_rd;
reg mem_we_d;
always @(posedge clk, negedge rstn)
if (!rstn) cc_re_d <= 0;
else cc_re_d <= cc_re;
- cc_re_d : 위에서 정의된 cc_re는 레벨 트리거에 동작하기 때문에 cc_re 신호가 엣지 트리거와 동시에 변경되면 불안정한 신호가 전달될 수 있다.(메타스테이블 상태) 이를 방지하기 위해 cc_re를 cc_re_d에 엣지트리거에 전달하게되면 동기화 신호에 의해 안정된 신호를 전달할 수 있다.
- valid_rd, wb_rd : 읽어올 Valid가 유효한지, 읽어올 Write back가 유효한지 확인하는 신호에 해당한다.
- tag_rd, data_rd : 읽어올 tag 값이 유효한지, 읽어올 Data가 유효한지 확인하는 신호에 해당한다.
- mem_we_d : ce_re_d와 마찬가의 이유로 선언된 변수에 해당한다.
wire hit = cc_re_d & valid_rd && tag_rd == i_cpu_addr[5:2];
wire empty = cc_re_d & ~valid_rd ;
wire diff = cc_re_d & valid_rd && tag_rd != i_cpu_addr[5:2];
wire miss = empty | diff;
wire mem_no = hit || cc_re_d & ~wb_rd & i_cpu_write;
wire mem_wb = diff & wb_rd;
wire mem_rd = miss & ~i_cpu_write;
위 코드들은 Cache Hit, Miss와 관련된 상태들을 정의하고 있다.
- hit : cc_re_d(Cache가 읽을 준비가 된 상태에서) & valid_rd(Valid값이 유효한 상태에서) && tag_rd == i_cpu_addr[5:2](Tag의 값이 CPU에서 전달한 Tag값과 일치할 때) hit를 1로 변경한다.
- empty : cc_re_d(Cache가 읽을 준비가 되었지만) & ~valid_rd(Valid값이 없는 경우) -> 최초에 접근하여 읽어온 데이터가 없는 경우
- diff : cc_re_d & valid_rd && tag_rd != i_cpu_addr[5:2] hit과 동일하지만 Index에 대해 Tag 값이 다른 경우 diff 신호를 1로 변경한다.
- miss : empty(Data가 비어있거나) | diff(Tag값이 일치하지 않은경우) miss 신호를 1로 변경한다.
- mem_no : Memory가 동작하지 않는 상태를 정의하며, hit (Cache Hit 상태이거나) || cc_re_d & ~wb_rd & i_cpu_write(Cache가 Read enable 상태이지만 Write Back이 필요없고, CPU가 쓰기 작업을 요청했을 때 Memory가 동작할 필요 없다.) + Data 무결성을 위해 Cache에 Read 요청이 들어오게 되면 Cache Memory의 Data를 읽는 도중에 Data가 변경되면 안되기 때문이다. 또 i_cpu_write일 때도 CPU가 쓰기 요청을 보낸 것이기 때문에(CPU가 쓰는 상태에서 Main Memory의 값이 들어오면 안되기 때문에) Main Memory는 동작하면 안된다.
- mem_wb : Write Back이 동작하는 조건은 Tag 값이 다를 때 동작한다.
- mem_rd : Memory에서 Data를 읽어오는 조건은 Cache Miss가 일어나거나 && ~i_cpu_write(CPU에서 쓰기 작업이 일어나지 않았을 때)
assign o_cpu_ack = hit || mem_no || (mem_rd ? mem_re : mem_we);
assign o_cpu_rdata = hit? data_rd: mem_re? i_mem_rdata: 'bX;
- o_cpu_ack : req 신호에 대해 CPU에게 ack 신호를 보내는 조건은 hit, mem_no, mem_re, mem_we의 신호 중 하나만 만족에해도 CPU에게 응답 신호를 전달하게 된다.
- o_cpu_rdata : Cache가 Hit일 때 기존에 Cache에 저장된 Data를 가져오고, 아닌경우 Memory와 re(Read Enable) 통신이 가능한 상태이면, Main Memory로부터 Data를 전달 받는다. 만약 통신이 가능한 상태가 아니라면 X 값을 전달한다.
always @(posedge clk, negedge rstn)
if (!rstn) mem_we_d <= 0;
else mem_we_d <= mem_we;
위에서 cc_re_d와 같은 의미로 사용된 코드다.
assign o_mem_req = ~mem_we_d & (mem_wb | mem_rd);
assign o_mem_addr = mem_wb ? {tag_rd,i_cpu_addr[1:0]} : i_cpu_addr;
assign o_mem_wdata = data_rd;
assign o_mem_write = wb_rd;
- o_mem_req : Main Memory에 req 요청을 보내기 위해선 Memory에 Write Enable이 일어나지 않은 상태에서 wb이나 rd 신호가 발생하면 req 요청을 보낸다.
- o_mem_addr : wb인 상태면 tag 값이 변경되어야 하기 때문에 tag_rd + i_cpu_addr[1:0] 값을 저장하고, wb 상태가 아니라면 CPU로부터 전달된 addr 값을 그대로 사용한다.
- o_mem_wdata : Main Memory에 작성하는 Data 값은 Wrtie Back이 일어난 상황일 것이며, Write Back에 의해서 Main Memory에 작성하는 값은 Cache Memory에 저장된 Data 값에 해당한다.
- o_mem_write : Main Memory에 Write 신호를 보낼 때는 wb의 신호가 있을 때 Write 신호를 전달하게 된다.
always @(posedge clk, negedge rstn)
if (!rstn) valid_mem <= 0;
else if (cc_we) valid_mem[cc_wa] <= 1;
always @(posedge clk, negedge rstn)
if (!rstn) valid_rd <= 0;
else if (cc_re) valid_rd <= valid_mem[cc_ra];
always @(posedge clk, negedge rstn)
if (!rstn) wb_mem <= 0;
else if (cpu_we) wb_mem[cc_wa] <= 1;
else if (mem_re) wb_mem[cc_wa] <= 0;
else if (mem_we) wb_mem[cc_wa] <= i_cpu_write;
always @(posedge clk, negedge rstn)
if (!rstn) wb_rd <= 0;
else if (cc_re) wb_rd <= wb_mem[cc_ra];
위 부분이 실제 신호들을 조합하여 Cache Controller가 동작하는 부분이다.
1. 첫번째 always 구문의 valid_mem의 경우 cc_we 신호를 확인하여 Cache의 Write 신호가 1인 상태라면 CPU로부터 요청이 들어온 Cache Line의 Index를 확인하여 해당 인덱스의 valid 값을 1로 변경한다. (cc_we 신호가 1이라는 것은 Cache에서 Main Memory로 Data가 쓰여지는 상황이기 때문이다.)
2. 두번째 always 구문의 valid_rd의 경우 cc_re 신호를 확인하여 Cache의 Read 신호가 1인 상태라면 현재 CPU로부터 요청이 들어온 Cache Line에 Valid 값을 확인하여 valid_rd(valid 신호 유무 확인)변수에 값을 전달한다.
3. 세번째 always 구문의 wb_mem의 경우 3.1. cpu_we가 1일 경우 CPU가 쓰기 상태일 때 Cache에 새로운 값을 쓰는 것이기 때문에 wb_mem을 1로 변경하고 3.2. mem_re가 1일 경우 Main Memory가 읽기 상태이면 Wrtie Back 상태가 필요없기 때문에 wb_mem을 0으로 변경한다. 3.3. mem_we가 1일 경우 i_cpu_write 즉, Main Memory가 쓰기 동작일 때, CPU의 쓰기 요청 신호에 따라 wb_mem을 변경한다.
4. 네번째 always 구문의 wb_rd의 경우 cc_re - Cache가 Read 상태일 때, wb_mem를 확인하여 wb이 준비된 상태라면, wb_rd의 신호를 1 즉, Write Back이 가능한 상태를 전달한다.
wire [3:0] tag_wd = mem_re? o_mem_addr[5:2]: cpu_we? i_cpu_addr[5:2]: 'bX;
wire [31:0] data_wd = mem_re? i_mem_rdata: cpu_we? i_cpu_wdata: 'bX;
tpsram #(.DEPTH(4),.WIDTH( 4)) u_tag(
clk, cc_we, cc_wa, tag_wd, cc_re, cc_ra, tag_rd
);
tpsram #(.DEPTH(4),.WIDTH(32)) u_data(
clk, cc_we, cc_wa, data_wd, cc_re, cc_ra, data_rd
);
위 구문은 Tag와 Data 값을 저장하는 구문과, tpsram(Two Port SRAM) 모듈을 불러오는 구문이다. 아래 tpsram 모듈은 각각 Cache Memory에서 각 Index에 대한 Tag값과 Data의 값을 저장하고 있다.
- tag_wd : mem_re Main Memory가 읽기 상태라면 현재 Data를 가져와야 하는 Main Memory의 Tag 정보를 받아오고, Main Memory가 읽기 상태가 아니라면 cpu_we 신호를 확인하여 cpu가 쓰기 상태면 CPU에서 전달된 새로운 Tag 정보로 업데이트 하고, Main Memory도 읽기 상태가 아니고, CPU도 쓰기 상태가 아니라면 X 값을 전달한다.
- data_wd : Data의 경우에도 Tag과 같은 방식으로 Data를 저장한다.
인스턴스로 사용된 tpsram은 아래와 같다.
■ tpsram.v 코드 분석
module tpsram #(
parameter DEPTH=8,
parameter WIDTH=32,
parameter DEPTH_LOG=$clog2(DEPTH)
)(
input clk, //write clk
input we, //write enable
input [DEPTH_LOG-1:0] wa, //write addr
input [WIDTH-1:0] wd, //write data
input re, //read enable
input [DEPTH_LOG-1:0] ra, //read addr
output reg [WIDTH-1:0] rd //read data
);
reg [WIDTH-1:0] mem[DEPTH-1:0];
initial begin
for (int i=0;i<DEPTH;i++) mem[i] = 0;
end
always @(posedge clk)
if (we) mem[wa] <= wd;
always @(posedge clk)
if (re) rd <= mem[ra];
endmodule
tpsram은 Two Port SRAM으로 Din, Dout Port가 별도로 존재하게 된다.
만약 Port가 한개라면 Read, Write가 동시에 일어날 수 없고, 한번에 한동작만 가능하다.
Two Port일 경우 Read와 Write 동작을 별도로 수행할 수 있다.
위 tpsram 코드의 경우 기존의 sram 코드와 동작 방식은 거의 동일하기 때문에 별도의 설명은 첨부하지 않겠다.
■ Schematic & Simulation
- cache Schematic
cache.v 코드의 Schematic은 위와 같고, Tag와 Data를 저장하기 위해 사용된 TPSRAM 인스턴스가 추가로 생성된 것을 확인할 수 있다.
- tpsram Schematic
- Simulation
1.(빨강) cpu_req 신호에 맞춰 cc_re 신호가 변경된 것을 확인할 수 있다.
2. (파랑) cpu_write, cpu_ack 신호에 맟줘, cc_we 신호가 변경된 것을 확인할 수 있다.
3. (노랑) 최초에 Data가 없기 때문에 empty와 miss가 1로 변경되고, Wrtie Back이 일어날 일이 없기 때문에 mem_no도 1의 신호를 띄우는 것을 확인할 수 있다.
4. (회색) CPU에서 Data를 썼기 때문에 Tag 1에 대해 Data가 작성된 것을 확인할 수 있다
1. (빨강) mem_req, mem_ack 신호를 통해 Main Memory와 통신이 가능해진 시점에서 Main Memory에 쓰는 상황이 아니기 때문에 mem_re의 신호가 1이 되고, CPU가 전달한 000110 주소에 대한 데이터(00000060)을 가져와 Cache Memory에 저장한다.
2. (파랑) CPU의 경우 000110 주소에 대해 읽기 요청을 전달했기 때문에 Cache가 Main Memory에서 받아온 Data를 받아오는 것을 확인할 수 있다.
위 경우는 Cache Data도 유효하고, Write Back도 실행될 준비가 되어 있는 상태지만 CPU와 Main Memory와 req, ack 신호로 통신이 되고 있지 않은 상태이기 때문에 Data Read, Write Back이 실행되지 않은 것을 확인할 수 있다.
위 경우는 CPU에서 Read, Write 둘 다 동작되고 있으며, Main Memory와 통신 상태는 아니지만 Main Memory에게 Write Back에 의한 Data를 전송하고 있는 상태이다.
1. Read의 경우 CPU로부터 전달받은 명령데이터를 확인해보면 Index 01 Tag 1의 데이터를 요구하고 있고, 기존에 해당 Data가 있기 때문에 (valid도 1) Data를 CPU에게 전달한다.
2. Write의 경우 CPU가 01 Index의 0001 Tag에 0000501 데이터 쓰기를 요청하고 있다. tag_wd, data_wd에 CPU에서 쓰기 요청한 값들이 들어가게 되고, 다음 클럭에서 Cache Memory에 저장되는 것을 확인할 수 있다.
그 외에도 여러 신호들이 CPU, Memory가 전달하는 신호에 따라 값이 변하곤하지만 결과적으로 Data의 무결성을 유지하기 위해서는 Clock 신호를 기준으로 동기화 신호에 맞게 데이터의 Read, Write가 동작되어야 신호가 얽히지 않고 제대로 들어가는 것을 알 수 있다.
+ 그 외에도 Delay, Area에 대해 신경 쓸 부분이 많지만 이번 글에서는 Cache 동작을 중심적으로 다뤄보았고, 이전에 다른 글에서도 Delay, Area에 대해 다룬 글이 있을 것이고, 없다면 이후에 한번 작성해 볼 예정이다.
'Semiconductor > 0. RTL, Simulation' 카테고리의 다른 글
RTL - I2C (Master / Slave - Simulation, Code) (0) | 2024.08.17 |
---|---|
RTL - Two Port SRAM / Dual Port SRAM 구조 및 설계 (0) | 2024.08.07 |
RTL - SRAM (`ifdef를 이용한 FPGA, ASIC 코드 분리) (0) | 2024.08.06 |
RTL - SPI (Master, Slave, FSM) (2) FSM, MASTER, SLAVE & SLVAE 활용 (0) | 2024.06.02 |
RTL - SPI (Master, Slave, FSM) (1) FSM, SLAVE (0) | 2024.06.02 |