모든 이야기

start와 done 사이를 채우는 요청 완료 handshake

start를 한 사이클 올렸으니 명령을 보냈다. 그런데 연산기는 그때 busy였고 명령을 받지 않았다. 반대로 done이 켜졌다고 제어기를 Idle로 돌렸더니, 소비자가 읽기 전에 결과가 덮어써졌다. 요청 완료 handshake는 요청을 수락한 사건, 계산이 끝난 사건, 결과를 소비한 사건을 구분하는 제어 계약이다. handshake는 양쪽이 전달을 확인하는 신호 교환을 뜻한다.

연산기와 제어기 사이를 단순한 start와 done으로 연결할 수는 있다. 하지만 두 신호의 이름만으로 완료까지의 책임이 정해지지는 않는다. start를 언제 받아야 하는지, done을 얼마나 유지하는지, 결과를 누가 보관하는지 명확해야 한다.

수락은 '보냈다'보다 강한 사건이다

한 번에 요청 하나만 처리하는 가상의 연산기를 생각하자. 요청 포트에 req_valid와 req_ready를 두고, 둘이 함께 높은 에지에 요청을 수락한다. 그 에지에 피연산자도 함께 저장한다.

assign accept = req_valid && req_ready;
assign start_operator = accept;
assign operator_operand = req_operand;

always_ff @(posedge clk) begin
  if (rst_n && accept) begin
    operand_q <= req_operand;
    request_id_q <= req_id;
  end
end

이 예제에서 연산기는 시작 에지에 operator_operand를 직접 샘플링하고, operand_q는 제어기가 보유하는 요청 사본이다. 같은 에지에 새로 기록되는 operand_q를 연산기 입력으로 읽으면 이전 값이 보일 수 있으므로, 그 방식이라면 start를 다음 에지로 옮기는 제어가 필요하다. req_ready는 제어기와 연산기가 새 요청의 책임을 질 수 있을 때만 높다. start_operator는 수락한 요청 하나에 대한 시작 사건이지, valid가 유지되는 동안 매 에지 새 연산을 시작하는 신호가 아니다.

ready=0인 동안의 req_valid는 아직 처리 의무가 넘어가지 않은 제시다. 선택한 유지 규칙 아래에서 생산자는 피연산자와 식별자를 그대로 보관한다. “valid를 한 번 보였다”는 사실과 “연산기가 요청을 받아 저장했다”는 사실이 구분된다.

계산 완료 뒤에도 일이 남아 있다

상태를 Idle, Run, Response로 나누어 보자. Idle에서 accept하면 Run으로 간다. Run에서 계산 완료가 오면 결과를 저장하고 Response로 간다. Response에서는 결과 valid를 유지하며 소비자의 ready를 기다린다. 결과가 수락된 뒤에만 Idle로 돌아간다.

Idle --요청 수락--> Run --계산 완료--> Response --결과 수락--> Idle

Response가 필요한 이유는 완료 시각과 소비 시각이 다를 수 있기 때문이다. 결과 버퍼가 없다면 연산기 자체가 결과와 완료 표시를 유지해야 한다. 둘 중 어디서 보관하는지는 선택할 수 있지만, 소비될 때까지 책임을 맡는 주체는 있어야 한다.

가상의 작업 Q7을 다음처럼 추적할 수 있다. 각 값은 에지 직전이다.

에지상태사건에지 뒤의 책임
0IdleQ7 수락연산 진행
1Run계산 중Q7 보유
2Run계산 완료결과 저장, Response 진입
3Response소비 ready=0같은 결과 유지
4Response소비 ready=1결과 수락, Idle 복귀

에지 2의 계산 완료는 최종 소비 완료가 아니다. 에지 3에서 결과를 잃으면 이미 수락했던 요청의 결과를 전달하지 못한다. 새 요청을 받지 않는 이 단순 모델에서는 결과가 나갈 때까지 busy가 유지된다.

CIRCT의 Handshake 문서는 연산과 데이터 흐름을 유효한 입력과 수락 가능한 출력의 관계로 표현한다. 이 관점을 제어기에 적용하면 start라는 한 신호 대신, 요청과 결과 각각의 전달 사건을 생각할 수 있다.

done의 펄스와 레벨을 같은 방식으로 읽지 않기

연산기가 done을 한 사이클만 올린다면 제어기는 그 에지에 반드시 완료를 감지하고 결과를 저장해야 한다. 소비자가 준비되지 않았다는 이유로 done을 나중에 확인하면 신호가 이미 사라질 수 있다. 완료 펄스와 소비자 ready를 단순히 AND하는 방식은 결과 보관을 대신하지 못한다.

반대로 done이 결과가 읽힐 때까지 유지되는 레벨이라면, 매 사이클 done=1을 새로운 완료로 세면 안 된다. 앞의 Q7 결과가 기다리는 동안 완료 카운터가 여러 번 올라간다. 완료 사건은 상태 전이나 별도의 수락 조건과 함께 해석해야 한다.

새 작업의 시작 뒤에 이전 done 레벨이 남아 있는 경우도 조심해야 한다. 제어기가 Run으로 들어간 다음 에지에 오래된 done을 읽으면 새 작업이 즉시 완료된 것처럼 보인다. 연산기 계약에 start 이후 완료 표시의 해제 규칙을 두거나, 요청 식별과 완료 식별을 맞추어야 한다. done이 높다는 사실만으로 어느 작업이 끝났는지 알 수는 없다.

연속 요청을 받으려면 저장 범위가 달라진다

앞의 세 상태 모델은 계산 중과 결과 대기 중 새 요청을 받지 않는다. 단순하고 책임 범위가 명확하지만, 연산기가 다음 요청을 처리할 수 있어도 결과 소비 때문에 시작이 늦어질 수 있다.

입력 큐와 결과 큐를 추가하면 요청 수락, 실행, 결과 소비를 더 독립적으로 진행시킬 수 있다. 그러나 수락한 요청의 수가 완료 저장 능력을 넘으면 안 된다. 결과가 늦게 소비되어도 보관할 자리를 예약하거나, 완료 시점에 연산기를 멈출 수 있는 규칙이 필요하다. 처리량 개선은 더 많은 책임을 동시에 보유하는 구조를 요구한다.

Ibex의 instruction decode/execute 설명은 단일 사이클 연산과 여러 사이클 연산을 실행 단계에서 제어하는 구성을 보여 준다. 구체적인 CPU 제어는 더 복잡하지만, 명령을 시작한 사실과 여러 사이클 뒤 완료된 사실을 구분해야 한다는 요구는 같다.

요청을 취소했을 때도 결말이 있어야 한다

취소를 허용하는 인터페이스라면 수락 전과 후를 나누어야 한다. 수락 전에는 생산자가 제시를 철회할 수 있는 별도 규칙을 둘 수 있다. 수락 후에는 제어기가 맡은 요청을 어떻게 끝낼지 필요하다. 계산을 멈추는지, 결과를 버리는지, 취소 응답을 내는지가 달라진다.

소비자가 결과를 받을 때까지 기다리는 Response 상태를 아무 통지 없이 없애면 요청자는 결과를 계속 기다릴 수 있다. timeout으로 끝내려면 timeout도 해당 요청의 종료라는 사건으로 세고, 나중에 도착한 결과를 새 요청에 붙이지 않도록 해야 한다. 책임이 시작된 사건이 있으면 책임을 끝내는 사건도 있어야 한다.

검증에서도 수락한 요청마다 정상 결과 또는 정의한 취소 결과가 정확히 한 번 나타나는지를 볼 수 있다. 명령 펄스 개수만 세는 검사보다, 수락 식별자와 결과 식별자를 따라가는 검사가 소유권 오류를 더 직접적으로 드러낸다.

처음의 start는 전달을 시도한 신호였고 done은 계산이 끝났다는 신호였다. 둘 사이를 정확한 수락과 보관 규칙으로 연결해야 요청이 실제로 완료된다. 요청 완료 handshake의 핵심은 accept, 계산 완료, 결과 수락이라는 세 사건에 각각 상태와 책임을 대응시키는 것이다. 그 구분이 있으면 busy가 길어지거나 소비자가 늦어져도 요청의 결말을 잃지 않는다.