에피소드 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에서 수행하고 검사는 별도의 술어 구성 요소에서 수행하는 방식으로 계속됩니다(이 방식도 잘 동작하지만, 고전적인 설계가 실제로는 이해하기 더 쉬우므로 시간이 나면 모두 다시 작성하겠습니다).