캐시의 태그/데이터 저장 배열 모델. 작지만 이 프로젝트 버그 스토리의 핵심이라,
동기 읽기·read-before-write·forwarding이 왜 한 세트인지 여기서 이해하면 면접에서
크게 쓴다.
캐시가 태그와 데이터를 담는 SRAM의 동작 모델. 실칩에선 벤더의 컴파일된 SRAM
매크로를 쓰지만, 시뮬레이션에선 그 동작을 흉내 내는 behavioral 모델이 필요하다.
모듈 2개:
| 모듈 | 용도 | 특징 |
|---|---|---|
sram_macro | 태그 배열 | 평범한 read/write |
sram_macro_be | 데이터 배열 | byte enable → partial write 지원 |
4-way라서 l1_cache_top.sv에서 way당 하나씩, 총 8개
(태그 4 + 데이터 4)를 generate로 인스턴스화한다.
always_ff @(posedge clk) begin
rdata <= mem[raddr]; // 주소는 이번 엣지에, 데이터는 다음 엣지에
...
end
"N번째 클럭에 주소(
raddr)를 주면, N+1번째 클럭에 데이터(rdata)가 나온다."
rdata가 플롭(always_ff의 <=)이라서다. 조합이 아니라 레지스터를 거쳐 나온다.
이게 2단 파이프라인을 성립시킨다:
주소를 준 사이클과 데이터가 나오는 사이클이 1칸 어긋난 것 = 파이프라인 단 경계.
이전 버전은 비동기 읽기였다:
assign rdata = mem[raddr]; // 조합 — 주소 주면 즉시 데이터
두 가지가 망가졌다:
1. 조합 루프: stall → raddr → rdata → hit → stall 이 닫힌 고리 → 합성 시 발진
2. 파이프라인 모델 불일치: S1 주소 → S2 데이터 구조인데 즉시 읽기라 타이밍 안 맞음
→ read 1495건 중 675건(45%) 오답
assign → always_ff @(posedge clk) 한 줄로 675 에러 → 0.
"동기 vs 비동기 SRAM 읽기 차이"를 물으면 이 실제 경험으로 답한다.
sram_macro 블록별logic [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1];
1<<ADDR_WIDTH = 2^ADDR_WIDTH (비트 시프트로 2의 거듭제곱)mem[0:255] = 256워드, 각 워드 DATA_WIDTH 비트[DATA_WIDTH-1:0] = packed(워드 안 비트), [0:...] = unpacked(워드 개수)always_ff @(posedge clk) begin
rdata <= mem[raddr]; // 읽기
if (we) mem[waddr] <= wdata; // 쓰기 (we=1일 때만)
end
we(write enable)가 1일 때만 waddr에 기록주석의 "sampled before this edge's write"가 이 부분. 같은 사이클에 같은 주소를
읽고 쓰면?
rdata <= mem[raddr]; // ①
if (we) mem[waddr] <= wdata; // ②
<=(non-blocking)의 규칙: 우변(RHS)을 먼저 전부 계산 → 좌변(LHS)을 나중에 한꺼번에 갱신.
raddr == waddr 이고 we=1 일 때:
mem[raddr]은 쓰기 전 옛날 값으로 계산됨mem 갱신→ rdata는 옛날 값을 내보낸다 (방금 쓴 값이 아니라).
포인트: 두 문장 순서를 바꿔도 결과 동일. NBA는 우변을 전부 먼저 평가하니까.
이게 "read-before-write" / "read-old-data" 동작이고, 실제 SRAM의 흔한 모드다.
배열이 "쓰기 직전 값"을 주므로, 직전 사이클에 쓴 위치를 지금 읽으면 stale.
l1_cache_core.sv의 safe_data_rdata/safe_tag_rdata가 우회:
"직전 사이클에 이 주소에 썼나? 그러면 배열 값 대신 그때 쓴 데이터를 써라"
이 SRAM의 read-before-write 성질과 코어의 forwarding은 한 세트.
하나를 이해하면 다른 하나가 왜 있는지 보인다.
타이밍 정리
cycle N : mem[A]에 write (엣지 끝에서 mem 갱신)
같은 A를 raddr로도 인가
cycle N+1 : rdata = (N 시점의 옛날 mem[A]) ← stale! forwarding이 여기서 개입
cycle N+2 : rdata = (새 값) ← 이제 배열 자체가 새 값
forwarding이 딱 1사이클 깊이면 충분한 이유가 이 표에 있다.
sram_macro_be — byte enable 버전데이터 배열은 partial write(워드 일부 바이트만)를 지원해야 한다. 차이는 쓰기부:
if (we) begin
for (int b = 0; b < BE_WIDTH; b++)
if (wbe[b]) mem[waddr][b*8 +: 8] <= wdata[b*8 +: 8];
end
BE_WIDTH = DATA_WIDTH/8 = 4 (32비트 = 4바이트)wbe[b]=1인 레인만 갱신wbe=4'b0001 → 최하위 바이트만, 4'b1111 → 전체 워드예: 워드 0xAABBCCDD, wdata=0x11223344, wbe=4'b0011
→ 결과 0xAABB3344 (상위 2바이트 유지, 하위 2바이트 교체)
+: 연산자 (indexed part-select) — 문법 단골wdata[b*8 +: 8] // "b*8 비트부터 위로 8비트"
| b | b*8 +: 8 | 실제 비트 | 바이트 |
|---|---|---|---|
| 0 | 0 +: 8 | [7:0] | 0 |
| 1 | 8 +: 8 | [15:8] | 1 |
| 2 | 16 +: 8 | [23:16] | 2 |
| 3 | 24 +: 8 | [31:24] | 3 |
for 변수로 비트 범위를 [b*8+7 : b*8] 처럼 쓰면 변수 범위 불가로 문법 에러.
그래서 +:(또는 -:)를 쓴다.
CPU 요청 ──▶ [코어 S1] ──raddr──▶ [이 SRAM] ──rdata(1사이클 뒤)──▶ [코어 S2] ──▶ hit/miss
▲
read-before-write 성질
│
[코어 forwarding이 보완]
| 질문 | 답 |
|---|---|
| 동기 vs 비동기 읽기? | 동기=주소→데이터 1사이클 지연(플롭), 비동기=즉시(조합). 실 SRAM은 동기. 비동기는 조합 루프·타이밍 문제 |
<=와 = 차이가 왜 중요? | NBA라 우변 먼저 전부 평가 → 같은 주소 read/write 시 읽기가 옛날 값(read-before-write) |
| read-before-write가 문제되나? | 직전 사이클 쓴 값을 바로 읽으면 stale → 코어 forwarding으로 해결 |
| 왜 데이터 배열만 byte enable? | 태그는 통째 갱신, 데이터는 CPU가 바이트 단위 write 가능 |
1<<ADDR_WIDTH? | 2^ADDR_WIDTH, 메모리 깊이 |
| 합성되나? | block RAM으로 추론(infer)됨. 실칩에선 보통 벤더 매크로로 교체 |
+:는 왜? | for 변수로 비트 범위 잡을 때 [a:b]는 변수 불가라 indexed part-select 사용 |
raddr==waddr, we=1 일 때 rdata에 나오는 건 새 값인가 옛날 값인가? 왜?assign rdata = mem[raddr] 로 바꾸면 어떤 두 가지가 깨지나?wbe=4'b1010, 워드 0x11223344, wdata=0xAABBCCDD → 결과는?mem[raddr]이 쓰기 전에 평가됨.stall→raddr→rdata→hit→stall), 파이프라인 타이밍 불일치(675 오답).0xAA22CC44 (레인 3,1만 교체: AA·CC, 레인 2,0은 유지: 22·44).wbe[3]=1→byte3=AA, wbe[1]=1→byte1=CC, 나머지 유지.