에피소드 IV: ALU와 술어

ALU는 이 설계에서 또 하나의 단순한 요소입니다. RISC-V 참조 매뉴얼 130쪽의 표를 보면 가능한 연산이 8가지라는 것을 알 수 있습니다(그중 일부는 ADD/SUB와 SRL/SRA처럼 두 가지 변형이 있습니다). 이제 RISC-V 참조 매뉴얼 130쪽의 표를 살펴봅시다. 이 표는 매우 유용합니다. 명령어의 32비트가 명령어의 매개변수를 인코딩하는 데 어떻게 사용되는지 보여 주기 때문입니다. 최하위 7비트 [6:0]은 명령어를 인코딩하는 데 사용됩니다. 나중에 이 비트들을 기준으로 하는 switch 문을 가진 구성 요소(명령어 디코더)가 필요합니다. 이제 실제로는 서로 다른 명령어가 11개뿐이라고 제가 말한 이유도 아실 수 있습니다. 서로 다른 opcode가 11개뿐이기 때문입니다. 표에서는 소스 레지스터와 목적지 레지스터의 인덱스(rs1,rs2,rd)가 어디에 인코딩되는지, 그리고 즉시값(imm)이 어디에 인코딩되는지도 볼 수 있습니다. 이에 대해서는 나중에 이야기하겠습니다. 지금은 레지스터-즉시값 ALU 명령어(코드 0010011)와 레지스터-레지스터 ALU 명령어(코드 0110011)에서 달라지는 부분에 집중합시다. 표를 보면 명령어의 [14:12] 비트에서 3비트 op를 얻을 수 있습니다. ADD/SUB와 SRLI/SRAI(논리 시프트 즉시값 / 산술 시프트 즉시값)를 구별하는 1비트 opqual은 명령어의 30번 비트에 해당합니다. 따라서 ALU를 조합 함수로 작성할 수 있습니다. 다음은 ALU의 단순화한 버전입니다:

module NrvSmallALU (
  input [31:0]      in1,
  input [31:0]      in2,
  input [2:0]       op,     // Operation
  input         opqual, // Operation qualification (+/-, Logical/Arithmetic)
  output reg [31:0] out     // ALU result. 
);
   always @(*) begin
      case(op)
        3'b000: out = opqual ? in1 - in2 : in1 + in2;                       // ADD/SUB
        3'b010: out = ($signed(in1) < $signed(in2)) ? 32'b1 : 32'b0 ;       // SLT
        3'b011: out = (in1 < in2) ? 32'b1 : 32'b0;                          // SLTU
    3'b100: out = in1 ^ in2;                                            // XOR
    3'b110: out = in1 | in2;                                            // OR
    3'b111: out = in1 & in2;                                            // AND
        3'b001: out = in1 << in2[4:0];                                      // SLL
        3'b101: out = $signed({opqual ? in1[31] : 1'b0, in1}) >>> in2[4:0]; // SRL/SRA
      endcase 
   end
endmodule

ALU는 32비트 값 두 개 in1과 in2, 그리고 연산 op,opqual을 입력으로 받아 결과를 반환합니다.

사실 이렇게 하는 것은 그다지 좋지 않습니다. SLL과 SRL/SRA를 위해 생성되는 배럴 시프터가 LUT를 많이 차지하기 때문입니다. 그래서 ALU 안에 클록 틱마다 한 자리씩 시프트하는 내부 레지스터를 두고, ALU가 계산 중일 때 활성화되는 busy 신호를 둘 수 있습니다. 또한 FemtoRV32에는 덧셈/뺄셈/비교를 하나의 가산기로 묶는 기법(@mecrisp가 제안함)도 있습니다. 하지만 지금은 ALU가 위와 같다고 생각합시다.

RISC-V 참조 매뉴얼 130쪽의 표로 다시 돌아갑시다. 분기 명령어(코드 1100011)도 6개 있으며, [14:12] 비트에 있는 가변 부분이 검사 종류를 나타냅니다. 이 검사들을 구현하려면 ALU와 비슷하지만 두 값을 비교하기만 하는 또 다른 구성 요소가 필요합니다:

module NrvPredicate(
   input [31:0] in1,
   input [31:0] in2,
   input [2:0]  op, 
   output reg   out
);
   always @(*) begin
      case(op)
        3'b000: out = (in1 == in2);                   // BEQ
        3'b001: out = (in1 != in2);                   // BNE
        3'b100: out = ($signed(in1) < $signed(in2));  // BLT
        3'b101: out = ($signed(in1) >= $signed(in2)); // BGE
        3'b110: out = (in1 < in2);                    // BLTU
        3'b111: out = (in1 >= in2);                   // BGEU
    default: out = 1'bx; // don't care...
      endcase
   end 
endmodule

이 구성 요소는 32비트 값 두 개 in1과 in2, 그리고 검사 op를 입력으로 받아 검사 결과를 나타내는 단일 비트를 출력합니다.

여기서도 마찬가지로 @mecrisp가 제안한 대로 비교를 하나의 가산기로 묶을 수 있지만(FemtoRV32 소스 참조), 지금은 이를 무시하겠습니다.

참고: ALU 연산과 술어를 같은 유닛에서 처리하고 주소 계산은 별도의 가산기에서 처리하는 다른 설계들도 본 적이 있습니다. 예를 들어 이 글을 참조하십시오(RISC-V 프로세서는 아니지만 상당히 비슷합니다). 제 설계가 차지하는 면적이 더 작다고 생각하지만, 테스트가 필요합니다

2021년 4월 4일 업데이트: 여러 설계를 테스트해 본 결과, 고전적인 설계(술어는 ALU에서 계산하고 주소 계산은 별도의 가산기에서 수행하는 설계)가 더 낫습니다. 이 설계는 이 그림에 나와 있으며 이 파일에 구현되어 있습니다. 이 튜토리얼의 나머지 부분은 주소 계산을 ALU에서 수행하고 검사는 별도의 술어 구성 요소에서 수행하는 방식으로 계속됩니다(이 방식도 잘 동작하지만, 고전적인 설계가 실제로는 이해하기 더 쉬우므로 시간이 나면 모두 다시 작성하겠습니다).

다음