모든 이야기

ready-valid에서 데이터의 책임이 넘어가는 한 에지

valid가 1로 올라갔는데 소비자가 데이터를 받지 않는다. 몇 사이클 뒤 ready가 켜지면 같은 데이터를 다시 보내야 할까, 이미 보냈다고 생각해야 할까. ready-valid에서 전송은 신호 하나의 상승이 아니라, 정해진 클록 에지에 두 신호가 함께 1인 사건이다. 이 사건을 먼저 정의하면 기다리는 동안 데이터의 책임이 누구에게 있는지도 분명해진다.

여기서는 하나의 클록을 공유하고 상승 에지에서 거래를 세는 링크를 생각한다. 또한 생산자가 valid를 제시한 뒤 수락될 때까지 valid와 payload를 유지하는 규칙을 선택한다. ready-valid라는 신호 이름만으로 이 강한 유지 규칙이 자동으로 정해지는 것은 아니다. 이 글에서는 규칙을 명시적으로 채택한 것이다.

전송을 세는 식은 하나다

assign fire = valid && ready;

이 식을 에지에서 샘플링했을 때 1이면 payload 한 개가 전달된다. valid만 높으면 제시했지만 수락되지 않은 상태이고, ready만 높으면 공간이 있지만 전달할 데이터가 없는 상태다. 둘 다 0이면 아무 거래도 없다. 다음은 가상의 에지별 추적이며, A와 B는 서로 다른 데이터 항목이다.

에지 직전validreadypayload해당 에지의 수락
001의미 없음없음
110A없음
210A없음
311AA
411BB

에지 1과 2에서는 A를 유지한다. 에지 3에서 A가 수락된 뒤 생산자는 다음 항목 B를 제시할 수 있다. valid를 0으로 내렸다가 다시 올릴 필요는 없다. valid가 계속 1이어도 ready와 함께 높은 각 에지마다 별개의 전송이다. valid의 상승 횟수만 세는 테스트벤치는 에지 4의 B를 놓친다.

소비자도 valid만 보고 자신의 포인터를 증가시키면 안 된다. 에지 1부터 세면 실제로 받은 항목보다 두 개를 더 센다. 데이터, 주소, 완료 개수처럼 전송에 따라 바뀌는 상태는 fire를 기준으로 갱신해야 한다.

수락 전까지의 소유권

유지 규칙 아래에서 생산자는 에지 3까지 A의 책임을 가진다. 자신의 큐에서 A를 제거하거나, 출력 레지스터를 B로 덮어쓰면 안 된다. 소비자는 ready를 내리지 않았다는 이유만으로 A를 받았다고 주장할 수 없고, fire가 발생한 에지의 payload를 처리해야 한다.

다음은 생산자 쪽 포인터를 움직이는 설명용 조각이다. 큐의 상세 구조는 생략했다.

always_ff @(posedge clk) begin
  if (rst_n && fire)
    read_index <= next_read_index;
end

이 갱신은 수락 후 다음 항목으로 넘어가게 한다. 다른 곳에서 valid만 보고 포인터를 움직이면, 이 한 줄이 올바르더라도 전체 계약은 깨진다. 거래의 소유권이 끝나는 사건을 하나로 정하고 모든 관련 상태가 같은 사건을 사용해야 한다.

프로토콜에 따라서는 data가 기다리는 동안 바뀔 수 있다. Chisel의 ready-valid 설명은 DecoupledIO가 bits의 안정성이나 valid의 지속을 보장하지 않는 관례임을 밝힌다. 반면 더 강한 유지 관례도 존재한다. 따라서 다른 블록을 붙일 때에는 “ready-valid니까 당연히 A를 유지한다”고 추측하지 말고, 실제 계약을 확인해야 한다.

생산자가 ready를 기다리면 생기는 조용한 정지

valid를 “수신자가 받을 준비가 됐을 때 보내는 신호”로 이해하면, 생산자가 ready를 본 뒤에야 valid를 올리는 구현을 만들기 쉽다. 소비자 역시 valid를 보고 나서 ready를 올리는 구현이라면 양쪽이 영원히 기다릴 수 있다. 데이터와 공간은 있지만 첫 신호를 내는 주체가 없다.

이 글의 지속 제시 규칙에서는 생산자가 데이터가 준비되면 ready와 무관하게 valid를 올린다. 소비자는 자신의 수락 능력에 따라 ready를 정한다. valid와 ready가 어느 방향에 의존해도 되는지는 선택한 프로토콜의 규칙을 따라야 하지만, 양쪽이 서로의 시작을 요구하는 구조는 진행을 보장하지 못한다.

또 ready를 미리 올려 두었다고 미래의 모든 데이터까지 받을 수 있다는 약속은 아니다. 일반적인 에지별 ready-valid에서는 각 에지의 값이 그 에지의 수락을 정한다. ready를 낮추어 현재 항목을 기다리게 할 수 있다. 더 강한 ready 지속 규칙이 있는 인터페이스라면 그 의무까지 문서에 포함해야 한다.

한 칸 버퍼에서 드러나는 동시 입출력

버퍼가 차 있으면 무조건 입력 ready=0이라고 정하면 구현은 단순하다. 하지만 현재 항목을 내보내는 에지에 새 항목을 받을 기회를 놓친다. 한 칸 버퍼에서도 “비었거나, 현재 항목을 내보낼 수 있으면 새 입력을 받을 수 있다”는 조건을 사용할 수 있다.

input_ready = (NOT full) OR output_ready

full=1이고 output_ready=1이면 출력 fire와 입력 fire가 같은 에지에 성립할 수 있다. 기존 항목은 소비되고 새 항목이 그 자리를 채운다. 입력 valid가 없다면 버퍼가 비고, 있다면 full을 유지하면서 payload가 바뀐다. PULP의 stream register는 이런 조건을 실제 구현에서 확인할 수 있는 사례다.

다만 이 구조는 하류 ready가 상류 ready까지 조합으로 전달된다. 여러 단계를 잇는 경우에는 타이밍 경로가 길어질 수 있다. 한 사이클에 한 항목을 받을 수 있다는 기능적 처리량과, 그 회로가 원하는 클록을 만족하는지는 별도 판단이다.

파형보다 거래 기록을 읽기

검증할 때에는 매 에지의 값 전체보다 fire가 발생한 입력과 출력의 순서를 기록해 보는 편이 직관적이다. stall 길이가 달라도 수락된 항목은 동일한 순서로 나와야 한다. 유지 규칙을 선택했다면 valid=1, ready=0 구간의 안정성도 별도로 검사한다. 값의 순서 검사와 제시 의무 검사는 서로 다른 실수를 잡는다.

reset과 취소가 거래의 책임을 언제 끝내는지도 필요하다. reset 중 제시한 valid를 거래로 세지 않는다면 양쪽과 검사기가 같은 경계를 사용해야 한다. 준비된 데이터가 없는데 valid만 초기화되지 않는 경우에는 존재하지 않는 거래를 만들 수 있다.

처음의 A는 valid가 올라간 시점에 이미 전달된 것이 아니다. 에지 3의 fire까지 생산자가 유지하고, 그 에지에서 소비자에게 책임이 넘어간다. ready-valid를 제대로 읽는 기준은 신호의 모양보다 수락 사건과 그 전후의 소유권이다. 이 기준이 맞으면 긴 stall도, 끊김 없는 연속 valid도 같은 규칙으로 해석할 수 있다.