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
);
-------------------------------
i_cpu_req --->| |
i_cpu_write --->| |
i_cpu_addr --->| |---> o_cpu_ack
i_cpu_wdata --->| CACHE |---> o_cpu_rdata
| |
| |
i_mem_ack --->| |---> o_mem_req
i_mem_rdata --->| |---> o_mem_write
| |---> o_mem_addr
| |---> o_mem_wdata
-------------------------------
module cache(
input clk, rstn, //클럭, 리셋 신호
//캐시->메모리
input i_cpu_req, i_cpu_write, //cpu 요청 신호, cpu 쓰기 요청 신호
output o_cpu_ack, //CACHE->CPU : CACHE 사용 해도 된다는 응답
input [5:0] i_cpu_addr, //cpu 요청 주소
input [31:0] i_cpu_wdata, // cpu 요청 쓰기 데이터
output [31:0] o_cpu_rdata, //cpu 읽기 데이터
//메모리->캐시
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 //메모리 읽기 데이터
);
//캐시의 valid bit, dirty bit
reg [3:0] valid_mem, wb_mem;
wire cpu_en = i_cpu_req & o_cpu_ack ;//cpu_en: cpu 가 CACHE 사용하는 것 허용 신호
wire cpu_we = i_cpu_req & o_cpu_ack & i_cpu_write; //cpu_we: CPU의 쓰기를 허용하는 신호
wire cpu_re = i_cpu_req & o_cpu_ack & ~i_cpu_write; //cpu_re: CPU의 읽기를 허용하는 신호
wire mem_en = o_mem_req & i_mem_ack ; //메모리 사용해도된다는 신호
wire mem_we = o_mem_req & i_mem_ack & o_mem_write;//mem_we: 메모리 Write가능하다는 신호
wire mem_re = o_mem_req & i_mem_ack & ~o_mem_write;//mem_we: 메모리 read 가능하다는 신호
wire cc_we = mem_re | cpu_we; //cc_we: 캐시 쓰기 허용 신호. 메모리 읽기가 허용되거나 cpu의 WRITE가 허용되면 -> cache의 쓰기 기능을 허용
wire [1:0] cc_wa = i_cpu_addr[1:0]; // cpu가 보내준 주소에서 index부분만 뽑아서 cc_wa에 저장
wire cc_re = i_cpu_req; //cpu 요청 신호가 1이면 cache쓰기 허용 (cpu 읽기든 쓰기든 우선, cache에 내가 원하는 data가 있는지 확인하기위해 cache를 읽어야함)
wire [1:0] cc_ra = i_cpu_addr[1:0]; // cpu가 보내준 주소에서 index부분만 뽑아서 cc_ra에 저장
reg cc_re_d; //cache read 허용 신호를 1cc delay한 신호
reg valid_rd, wb_rd; //cache에서 읽은 데이터의 valid, dirty bit
wire [3:0] tag_rd; //cache에서 읽은 데이터의 tag 번호
wire [31:0] data_rd; //cache에서 읽은 데이터
reg mem_we_d; // memory write 허용 신호를 1cc delay한 신호
always @(posedge clk, negedge rstn)
if (!rstn) cc_re_d <= 0; //cache read 허용 신호를 0으로 설정
else cc_re_d <= cc_re; //cache read 허용 신호의 1cc delay를 update
wire hit = cc_re_d & valid_rd && tag_rd == i_cpu_addr[5:2]; //cache read 허용 & 읽은 데이터가 valid & tag 번호 일치 -> cache hit
wire empty = cc_re_d & ~valid_rd ; //empty : cache가 비었다는 신호. cache read 허용 & 읽은 데이터가 invalid -> 초기화 안되었으니 empty임 -> empty를 set
wire diff = cc_re_d & valid_rd && tag_rd != i_cpu_addr[5:2]; //특정 index의 tag가 내가 원하는 tag와 다름을 나타내는 신호. cache read 허용 & 읽은 데이터가 valid & tag 번호 다름 -> cache miss -> diff를 set
wire miss = empty | diff; //miss:cache miss가 발생했다는 신호.
wire mem_no = hit || cc_re_d & ~wb_rd & i_cpu_write;//mem_no: 메모리 접근이 필요없음을 나타내는 신호. cache hit이 발생했거나, (cache read 허용이고, clean이고, cpu 쓰기 요청이면) -> 메모리 접근할 필요 x -> mem_no를 set
wire mem_wb = diff & wb_rd; //mem_wb: memory에 write back 해야함을 나타내는 신호. 특정 index의 tag가 내가 원하는 tag와 다르고 dirty bit이 1인 경우 -> memory 에 write back해야함
wire mem_rd = miss & ~i_cpu_write; // mem_rd: 메모리를 읽어야함을 나타내는 신호. cache miss 가 발생했고, cpu 읽기 요청이면 memory를 read해야함 -> mem_rd를 set
//hit, write wo/ evict, read w/ mem_re,
assign o_cpu_ack = hit || mem_no || (mem_rd ? mem_re : mem_we);//CACHE 사용 해도 된다는 응답 신호. cache hit 발생했거나, 메모리 접근할 필요 없거나, 메모리 작업 완료한 경우 -> cpu 처리 완료 응답을 set
assign o_cpu_rdata = hit? data_rd: mem_re? i_mem_rdata: 'bX; //cache hit이면 cache에서 읽은 데이터를, cache miss면 메모리에서 읽은 데이터를 o_cpu_rdata 신호에 연결
always @(posedge clk, negedge rstn)
if (!rstn) mem_we_d <= 0;
else mem_we_d <= mem_we;
//write back + read miss
assign o_mem_req = ~mem_we_d & (mem_wb | mem_rd); //메모리 쓰기 미완료 & (메모리에 write이나 read해야하면) -> 메모리 요청 신호를 set
assign o_mem_addr = mem_wb ? {tag_rd,i_cpu_addr[1:0]} : i_cpu_addr; //메모리에 write back해야하면 읽은 cache에서 읽은 tag번호를, 그렇지 않으면 cpu에서 전달받은 주소를 연결
assign o_mem_wdata = data_rd; //캐시에서 읽은 데이터를 메모리 write data에 연결
assign o_mem_write = wb_rd; //캐시에서 읽은 데이터의 dirty bit를 o_mem_write에 연결
//V + TAG + DATA
always @(posedge clk, negedge rstn)
if (!rstn) valid_mem <= 0;
else if (cc_we) valid_mem[cc_wa] <= 1; //clk의 posedge마다 cache의 write 주소의 valid bit을 set
always @(posedge clk, negedge rstn)
if (!rstn) valid_rd <= 0;
else if (cc_re) valid_rd <= valid_mem[cc_ra]; //clk의 posedge마다 cache read가 enable 되어있다면 -> cache의 read 주소의 valid bit을 valid_rd에 저장 (valid bit update)
always @(posedge clk, negedge rstn)
if (!rstn) wb_mem <= 0;
else if (cpu_we) wb_mem[cc_wa] <= 1; //clk의 posedge마다 cpu쓰기 요청 처리가 완료되었으면 -> cache의 write주소에 해당하는 dirty bit에 1을 저장
else if (mem_re) wb_mem[cc_wa] <= 0; //clk의 posedge마다 memory read 요청 처리가 완료되었으면 -> cache의 write주소에 해당하는 dirty bit에 0을 저장
else if (mem_we) wb_mem[cc_wa] <= i_cpu_write; //clk의 posedge마다 memory write 요청 처리가 완료되었으면 -> cache의 write주소에 해당하는 dirty bit에 cpu쓰기/읽기 요청 신호를 저장
always @(posedge clk, negedge rstn)
if (!rstn) wb_rd <= 0; //
else if (cc_re) wb_rd <= wb_mem[cc_ra]; //clk의 posedge마다 cache read가 enable 되어있으면 읽은 데이터의 dirty_bit에 읽은 데이터의 실제 dirty bit을 저장 (dirty bit udpate)
wire [3:0] tag_wd = mem_re? o_mem_addr[5:2]: cpu_we? i_cpu_addr[5:2]: 'bX; //tag_wd: cache에 쓸 태그 번호. 메모리 읽기 요청 처리 완료면 메모리로 전달한 주소의 tag를, 아니면 -> cpu 쓰기 요청 처리 완료면 cpu에서 입력받은 tag 번호를 tag_wd에 연결
wire [31:0] data_wd = mem_re? i_mem_rdata: cpu_we? i_cpu_wdata: 'bX;//data_wd: cache에 쓸 data. 메모리 읽기 요청 처리 완료면 메모리에서 읽은 데이터를, cpu write요청 처리 완료면, 입력받은 cpu의 write data를 data_wd에 저장
tpsram #(.DEPTH(4),.WIDTH( 4)) u_tag( //tpsram 모듈에 클럭, cache write 허용 신호, cache write 주소, cache에 write할 태그 번호, cache read 허용 신호, cache read 주소, cache에서 읽은 데이터의 tag 번호를 연결
clk, cc_we, cc_wa, tag_wd, cc_re, cc_ra, tag_rd
);
tpsram #(.DEPTH(4),.WIDTH(32)) u_data( //tpsram모듈에 클럭, cache write 허용 신호, cache write 주소, cache에 write할 데이터, cache read 허용 신호, cache read 주소, cache에서 읽은 데이터를 연결
clk, cc_we, cc_wa, data_wd, cc_re, cc_ra, data_rd
);
endmodule
이외에 테스트벤치, tpsram 관련 system verilog 코드가 있다.


Modelsim으로 시뮬레이션을 진행한 결과이다.
사실 처음에 코드가 너무 길고, 각 모듈이 내부적으로 객체를 만들며 유기적으로 동작하는 전체 시스템을 이해하는 것이 상당히 어려웠다.
이틀에 걸쳐서 Verilog 코드와 modelsim 시뮬레이션 결과를 비교 분석하며
이 시간에 이 신호는 왜 1이되고 왜 0이되고 를 분석해보았다.
코드를 이해하는 과정에서 컴퓨터구조 과목에서 배웠던 지식이 정말 200% 활용되었다.
예를들어
과 같은 개념들을 머릿속으로 계속 생각하고 컴퓨터의 동작을 상상하며 코드를 이해하는 과정이 필요했다.
작년에 컴퓨터구조 수업에서 배웠던 이론을 Verilog로 실제 구현해보고, 합성해보고, Modelsim으로 Simulation 해보며 동작을 검증해보는 과정이 재미있었다.
이정도로 복잡한 코드를 처음 해석해봐서 그런지 Non blocking assignment의 경우 Verilog 의 Scheduling이 어떻게 되는지가 많이 헷갈렸다.
그래서 Verilog Scheduling 을 공부해보며 아래 순서로 해석한다는 것을 알게되었다.
순차 논리의 sensitivity list에 포함된 신호 발생
│
├─ 1. Active Region: 모든 순차 블록의 RHS 평가 (현재 값 기준)
│
├─ 2. NBA Region: 모든 <= (non-blocking)의 LHS 업데이트
│
└─ 3. 조합 논리 재평가 (새로운 값에 반응)
(*RHS 평가시, Modelsim Simulation으로 나오는 현재 시간의 파형은 원인이 아니라 결과이므로 파형으로 판단하면 안된다. 파형은 결과임을 잊지말자)

코드랑 시뮬레이션 결과랑 비교분석하며 많이 헷갈렸던 부분이다.
결국 파형으로 우리눈에 보이는것은 결과이다. 따라서 시점 T에서 RHS를 평가할때 시뮬레이션상으로 시점 T에 나타난 값을 사용하면 안된다. (결과이니까)
어려워서 이틀정도 고전했지만, Cache 설계를 대충 이해하고 넘어가면 어차피 뒤에것도
제대로 이해못할 것이라 판단하여 최대한 이해하려고 노력하였다.
이틀을 투자한것이 하나도 아깝지 않을정도로 유익하고 뜻깊은 시간이었다.
빨리 진도나가서 32bit RISC CPU 설계에 도전해보고싶다.