Altifigence Academy

55 / 63 · 개념

동기식 메모리와 읽기·쓰기 충돌 처리

읽기 결과를 레지스터에 저장할 때의 응답 시점과 read-first·write-first·no-change를 구분하고, valid와 주소 초기화를 각각 처리하도록 설계합니다.

주소보다 먼저 정할 것: 응답 시점

메모리를 연결할 때는 용량뿐 아니라 읽기 지연과 충돌 시 동작도 정해야 합니다. 여기서는 16개 주소에 각각 8비트를 저장하고, 하나의 상승 에지에서 읽기 요청 하나와 쓰기 요청 하나를 받을 수 있는 동기식 메모리를 만듭니다. 두 요청은 같은 클록을 쓰지만 주소는 서로 독립적입니다. 용량은 16×8=128비트이고, 주소 폭은 log⁡216=4\log_2 16=4비트입니다.

읽기 주소와 rd_en을 상승 에지 전에 안정시키면, 그 에지 뒤에 레지스터에 저장된 rd_data와 rd_valid가 나옵니다. 이를 레지스터 1단을 거치는 읽기 지연이라고 부릅니다. 다른 always_ff가 같은 에지에서 이 출력을 읽으면 갱신 전 값을 읽으므로, 응답을 받는 레지스터레지스터 여러 비트의 상태를 저장하는 회로입니다. 이 과정의 동기식 레지스터는 클록 에지에서 정해진 입력을 저장합니다. 자세히 보기는 다음 상승 에지에서 값을 저장합니다. 요청 시점과 응답을 관찰하는 시점을 파형에 따로 표시해야 합니다. 새 요청은 매 클록 받을 수 있으므로, 레지스터 1단의 지연이 있다고 처리율처리율 단위 시간에 받아들이거나 완료하는 작업의 수입니다. 파이프라인은 단일 작업의 지연을 줄이지 않고도 처리율을 높일 수 있습니다. 자세히 보기이 절반이 되지는 않습니다.

같은 주소를 읽고 쓰면

에지 직전 M[3]=8'h25이고, 주소 3을 읽으면서 8'h9A를 쓴다고 합시다. read-first는 25를 반환하고 저장소를 9A로 바꿉니다. write-first는 새 값인 9A를 반환합니다. no-change는 출력 레지스터를 유지하므로, 그 출력이 주소 3의 이전 값이라는 보장은 없습니다. 세 방식 모두 가능한 동작이며, 어느 방식을 쓸지 정해야 합니다. AMD의 RAM 동기화 모드 문서도 이 세 모드를 구분합니다.

이번 예제는 read-first를 사용합니다. M−M^{-}를 에지 직전, M+M^{+}를 에지 뒤의 배열이라고 쓰면, reset이 아닌 에지에서 임의의 주소 aa에 대해 다음이 성립합니다.

M+[a]={wr_data,wr_en=1∧a=wr_addrM−[a],otherwiseM^{+}[a]=\begin{cases} \mathrm{wr\_data}, & \mathrm{wr\_en}=1\land a=\mathrm{wr\_addr}\\ M^{-}[a], & \text{otherwise} \end{cases}
rd_data+={M−[rd_addr],rd_en=1rd_data−,rd_en=0rd_valid+=rd_en\begin{aligned} \mathrm{rd\_data}^{+}&=\begin{cases} M^{-}[\mathrm{rd\_addr}], & \mathrm{rd\_en}=1\\ \mathrm{rd\_data}^{-}, & \mathrm{rd\_en}=0 \end{cases}\\ \mathrm{rd\_valid}^{+}&=\mathrm{rd\_en} \end{aligned}

충돌해도 읽기에는 M+M^{+}가 아닌 M−M^{-}를 사용합니다. 읽기 요청이 없을 때는 rd_data를 유지합니다.

SystemVerilog
module sync_mem16x8 (
  input  logic       clk, rst,
  input  logic       wr_en, rd_en,
  input  logic [3:0] wr_addr, rd_addr,
  input  logic [7:0] wr_data,
  output logic [7:0] rd_data,
  output logic       rd_valid
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:15];

  always_ff @(posedge clk) begin
    if (rst) begin
      rd_valid <= 1'b0;
    end else begin
      rd_valid <= rd_en;
      if (wr_en)
        mem[wr_addr] <= wr_data;
      if (rd_en)
        rd_data <= mem[rd_addr];
    end
  end
endmodule

두 대입의 오른쪽은 갱신 전 값을 사용하고, nonblockingnonblocking 순차 RTL에서 <=로 표현하는 대입입니다. 우변을 평가한 뒤 갱신을 예약하므로 같은 에지의 레지스터들이 이전 상태를 기준으로 계산할 수 있습니다. 자세히 보기 대입은 그 뒤에 반영됩니다. 따라서 쓰기 문장을 읽기 문장보다 먼저 적어도 새 값을 읽지는 않습니다. write-first가 필요하면 읽기 식을 (wr_en && wr_addr==rd_addr) ? wr_data : mem[rd_addr]로 바꾸어 새 쓰기 데이터를 선택할 수 있습니다. 이 우회 경로에도 읽기 출력 레지스터는 그대로 있습니다.

valid와 주소 초기화는 따로 확인해야 합니다

rst는 active-high 동기 reset입니다. 상승 에지에서 rst=1이면 두 요청을 무시하고 rd_valid만 0으로 만듭니다. 배열과 rd_data를 0으로 채우지는 않습니다. 이 설계에서는 전원을 켠 뒤 한 번도 쓰지 않은 주소의 값을 정의하지 않습니다. 시뮬레이터에서는 X가 보일 수 있고, 실제 하드웨어에서도 특정 값이라고 가정할 수 없습니다.

rd_valid=1은 “읽기 요청에 응답하는 주기”라는 뜻입니다. 해당 주소가 이미 초기화되어 값을 알 수 있다는 뜻은 아닙니다. 먼저 쓴 주소만 읽거나, 주소별로 초기화 여부를 기록하는 표를 따로 두어야 합니다. 최초 쓰기와 같은 에지에서 그 주소를 read-first로 읽어도 이전 값은 여전히 미정입니다. valid=0일 때 rd_data를 계산에 사용하지 않도록 막는 조건인 valid guard도 필요합니다. 초기화된 주소만 읽는 조건과 valid guard는 각각 확인해야 합니다.

reset은 저장소를 지우지 않으므로 이미 쓴 값은 RTL상 남습니다. 다만 시스템에서 reset 뒤 이전 내용을 사용하지 않기로 했다면, 외부 초기화 표도 그에 맞게 관리해야 합니다. 민감한 정보를 지우는 기능은 제어 reset과 별도로 설계해야 합니다.

각 열은 한 상승 에지상승 에지 클록이 0에서 1로 바뀌는 순간입니다. CLK=1인 구간 전체를 뜻하는 레벨과 구별합니다. 자세히 보기에서 관찰한 값을 보여 줍니다. 입력은 에지 직전의 값이고, 이름에 after가 붙은 상태는 갱신 직후의 값입니다. 열은 샘플링 순서대로 놓았으며, 실제 전파 지연전파 지연 입력이 바뀐 뒤 출력이 올바른 값으로 안정되기까지 걸리는 시간입니다. 논리식의 등가성과 시간 특성은 별개입니다. 자세히 보기을 나타내지는 않습니다. 모든 읽기·쓰기 주소는 3입니다. E0에서 25를 처음 쓰고 E1에서 9A로 덮어쓰며 읽습니다. data의 대시는 미정 값이고, valid=0인 버스는 응답으로 세지 않습니다. 데이터 표기는 16진수입니다.

read-first 충돌에서 이전 값을 읽기
파형 데이터 보기
파형 데이터: 각 문자는 한 구간, 점은 이전 상태 유지, p는 클록 한 주기입니다.
신호파형버스 값
edge2345E0 → E1 → E2 → E3
wr_en1.0.
wr_data23..25 → 9A
rd_en01.0
M[3] after23..25 → 9A
rd_data after234.- → 25 → 9A
rd_valid after01.0

네 번의 에지에서 값을 따라가 보기

E0에서 주소 3에 25를 쓰고 rd_en=0으로 둡니다. E1에서 주소 3에 9A를 쓰면서 같은 주소를 읽으면, E1 뒤에는 valid=1, data=25가 됩니다. E2에서 쓰기 없이 주소 3을 다시 읽으면 data=9A가 됩니다. E3에 rd_en=0이면 valid=0이고 data는 9A로 남습니다. E3의 출력 버스에 9A가 남아 있다는 이유로 새 응답으로 처리하면 같은 데이터를 두 번 처리하게 됩니다.

주소가 다르면 읽기와 쓰기는 독립적으로 동작합니다. 예를 들어 주소 7을 읽으면서 주소 3에 써도, 읽기 응답은 주소 7의 기존 값입니다. 실제 FPGA에서 이 RTL이 어떤 RAM 자원으로 구현되는지, 그 자원이 같은 주소의 읽기·쓰기 충돌을 어떻게 처리하는지는 합성 보고서와 소자 문서로 확인해야 합니다. RTL의 동작을 정하는 일과 특정 장치에서 그 구현을 검증하는 일은 별개입니다.

직접 생각해 보기

M[6]=8'h10이 이미 저장되어 있습니다. E0에서 주소 6을 읽으면서 8'h20을 쓰고, E1에는 쓰기 없이 다시 읽고, E2에는 rd_en=0으로 둡니다. read-first에서 각 에지 뒤의 data/valid를 구하세요. 같은 실험을 write-first로 바꾸면 무엇이 달라지나요? 한 번도 쓰지 않은 주소에서 첫 쓰기와 read-first 읽기가 겹치면 정해진 값을 얻을 수 있나요?

해설 보기

E0 뒤에는 data=10, valid=1이고 배열은 20으로 갱신됩니다. E1 뒤에는 data=20, valid=1입니다. E2에서는 data=20을 유지하지만 valid=0이므로 응답으로 세지 않습니다. write-first에서는 E0의 data만 20으로 달라집니다. 최초 쓰기와 겹친 read-first 읽기는 쓰기 전의 초기화되지 않은 값을 반환하므로 특정 값을 정답으로 둘 수 없습니다. valid=1도 초기화를 보장하지 않습니다.

선택은 이 브라우저에만 적용됩니다. 언제든 푸터에서 변경할 수 있습니다.