지난 포스팅에서는 RISC-V Single-Cycle CPU의 이론적인 데이터패스 구조와 전체를 통제하는 10가지 핵심 제어 신호(Control Signals)의 개념에 대해 알아보았습니다.
https://idkihg.tistory.com/179
[RISC-V] Single Cycle CPU 직접 설계하고 구현하기 1편
CPU의 동작 원리와 명령어가 실행되는 경로를 깊이 이해하는 가장 좋은 방법은 Verilog HDL을 이용해 직접 설계해 보는 것입니다. 이론으로만 접했던 데이터패스(Datapath)와 제어 장치(Control Unit)가 실
idkihg.tistory.com
이번 포스팅부터는 실제 Verilog HDL 소스 코드를 바탕으로 하드웨어가 어떻게 모델링되었는지 구체적으로 파헤쳐 보겠습니다. 첫 번째 시간으로, CPU에서 명령어 해석을 담당하는 두뇌인 제어 장치(Control Unit)와 실제 산술/논리 계산을 수행하는 심장인 ALU(Arithmetic Logic Unit)의 소스 코드를 정밀하게 분석해 보겠습니다.
1. 제어 장치 (Control.v) 설계 분석
제어 장치는 Fetch 단계에서 명령어 메모리로부터 주입된 32bit 명령어(command)와 Branch 단계의 상태 신호(BrEq, BrLt)를 입력받아 CPU 전역의 멀티플렉서와 활성화 신호를 생성하는 조합논리 회로입니다 .
1.1. 입력 신호 전처리 및 명령어 타입(Instruction Type) 디코딩
컨트롤러가 명령어를 받으면 가장 먼저 수행하는 것은 비트 분할과 타입 분류입니다. 코드 내에서는 func_opcode라는 9비트 와이어를 선언하여 디코딩에 필요한 핵심 비트들을 하나로 모아 조립합니다.
wire [8:0] func_opcode;
// command[30] (SRA/SUB 판별 변수), command[14:12] (funct3), command[6:2] (Opcode 주요 5비트) 취합
assign func_opcode = {command[30], command[14:12], command[6:2]};
이렇게 정렬된 데이터 중 최하위 5비트(func_opcode[4:0])인 Opcode를 기반으로 조합논리 블록(always @(*))을 가동하여 현재 명령어가 어떤 포맷 유형(R, I, S, B, U, J)에 속하는지 명확하게 분류합니다.
always @(*) begin
inst_type = Z; // 기본값 지정으로 Latch(의도치 않은 데이터 유지 회로) 생성 방지
case (func_opcode[4:0])
5'b01100: inst_type = R; // R-Type 산술 연산 (ADD, SUB, SLL 등)
5'b00100: inst_type = I; // I-Type 연산 (ADDI, XORI 등)
5'b00000: inst_type = I; // I-Type 메모리 로드 연산 (LW, LB, LH 등)
5'b11001: inst_type = I; // I-Type 레지스터 간접 점프 연산 (JALR)
5'b01000: inst_type = S; // S-Type 메모리 저장 연산 (SW, SB, SH)
5'b11000: inst_type = B; // B-Type 조건 분기 연산 (BEQ, BNE, BLT 등)
5'b01101: inst_type = U; // U-Type 상위 즉시값 로드 (LUI)
5'b00101: inst_type = U; // U-Type PC 상위 즉시값 더하기 (AUIPC)
5'b11011: inst_type = J; // J-Type 무조건 점프 연산 (JAL)
default: inst_type = Z;
endcase
end
1.2. 각 데이터패스 제어 신호(Control Signals)의 상세 생성 공식
디코딩을 통해 inst_type과 func_opcode 정보가 확정되면, 하드웨어 결선선(Wire)으로 매핑된 제어 신호들이 연쇄적인 불 연산(Boolean Logic)을 통해 실시간으로 출력됩니다. 이 부분이 Control.v 설계의 핵심 백미입니다.
1.2.1. RegWEn (레지스터 쓰기 활성화 신호)
결과 값을 레지스터 파일에 최종적으로 저장해야 하는 명령어 그룹을 필터링합니다. R, I, U, J 타입은 연산 결과나 메모리 로드 값, 혹은 점프 복귀 주소를 저장해야 하므로 신호가 고스란히 1로 활성화됩니다. 반면 Store(S)나 Branch(B) 타입은 상태를 저장하지 않으므로 무조건 0이 됩니다.
assign RegWEn = (inst_type == R) | (inst_type == I) | (inst_type == U) | (inst_type == J);
1.2.2. ASel / BSel (ALU 입력 선택 MUX 제어 신호)
ALU의 연산 대상(Operand)을 동적으로 바꾸는 신호입니다 .
- ASel: 1이 되면 기본 레지스터 데이터 대신 현재 PC 주소 값을 ALU의 A 입력으로 전달합니다. 상위 즉시값을 PC에 더하는 AUIPC(U-Type), 분기 주소를 계산하는 Branch(B-Type), 복귀 주소를 계산하는 JAL(J-Type)에서 활성화됩니다.
- BSel: 1이 되면 레지스터 데이터B 대신 확장된 즉시값(immOut)을 ALU의 B 입력으로 전달합니다. 레지스터끼리 순수하게 계산하는 R-Type을 제외한 모든 명령어 타입에서 활성화되도록 깔끔하게 설계되었습니다.
assign ASel = (inst_type == U) | (inst_type == B) | (inst_type == J);
assign BSel = (inst_type != R);
1.2.3 WBSel (Write-Back 선택 데이터 버스 신호)
연산이 끝난 후 레지스터에 최종 저장할 원본 소스를 고르는 2비트 멀티플렉서 제어선입니다 . 삼항 연산자를 활용한 조건 분기문으로 설계되어 있습니다 .
assign WBSel = (func_opcode[4:0] == 5'b00000) ? 2'd0 : // 1) Load 명령어 계열이면 메모리 데이터(00) 선택
((inst_type == J) | (func_opcode[4:0] == 5'b11001)) ? 2'd2 : // 2) JAL 혹은 JALR이면 복귀주소 PC+4(10) 선택
2'd1; // 3) 그 외의 일반적인 산술/논리 계산 명령어면 ALUOUT 연산 결과(01) 선택
1.2.4. PCSel (Program Counter 분기 제어 신호) & Branch 성공 판별
CPU의 실행 경로를 바꿀지 결정하는 핵심 마스터 신호입니다 . 무조건 점프 명령어(JAL, JALR)가 들어오거나, 조건 분기 명령어(B-Type) 상태에서 하단의 branch_taken 회로가 참(1)을 반환할 때 가동되어 다음 PC 값을 점프 주소로 점프시킵니다 .
// 조건 분기 명령어(funct3)와 외부 플래그(BrEq, BrLt)의 논리곱 조합 회로
assign branch_taken =
(func_opcode[7:5] == 3'b000 && BrEq) || // BEQ: 같으면 분기 성공
(func_opcode[7:5] == 3'b001 && ~BrEq) || // BNE: 다르면 분기 성공
(func_opcode[7:5] == 3'b100 && BrLt) || // BLT: signed 작으면 분기 성공
(func_opcode[7:5] == 3'b101 && ~BrLt) || // BGE: signed 크거나 같으면 분기 성공
(func_opcode[7:5] == 3'b110 && BrLt) || // BLTU: unsigned 작으면 분기 성공
(func_opcode[7:5] == 3'b111 && ~BrLt); // BGEU: unsigned 크거나 같으면 분기 성공
assign PCSel = (inst_type == J) | (branch_taken & (inst_type == B)) | (func_opcode[4:0] == 5'b11001);
1.2.5. ALUSel (ALU 상세 작동 연산 선택 버스)
최종 연산 코드를 ALU로 넘겨주는 데이터 제어 블록입니다. 메모리 주소 계산이나 분기 주소 계산이 필요한 Load, Store, Branch, JAL 등은 기본적으로 ALU가 '덧셈'을 하도록 4'b0000을 강제 주입합니다 . 반면 순수 R-Type 및 I-Type 계산 명령어들은 명령어 내부에 내장된 funct 코드 필드를 가변적으로 가공하여 전송하게 설계되어 하드웨어 자원을 극도로 아낍니다 .
always @(*) begin
case (func_opcode[4:0])
5'b01100: ALUSel = func_opcode[8:5]; // R-Type은 명령어의 funct7[5]+funct3 조합을 그대로 ALU 매핑
5'b00100: begin // I-Type 계산 명령어의 경우
if(func_opcode[7:5] == 3'b101) // SRLI / SRAI 시프트 명령어 분기라면 최상위 연산 비트 포함
ALUSel = func_opcode[8:5];
else // 일반 가감산 즉시값 명령어라면 funct3만 안전하게 확장 정렬
ALUSel = {1'b0, func_opcode[7:5]};
end
5'b01101: ALUSel = 4'b1111; // LUI 명령어 진입 시 ALU 전용 패스스루 코드 강제 주입
default: ALUSel = 4'b0000; // Load/Store/Branch/JAL 등 주소 계산은 모두 기본 덧셈 처리
endcase
end
2. 산술논리연산장치 (ALU.v) 설계 분석
ALU는 제어 장치로부터 연산 선택 버스 신호인 ALUSel[3:0]을 공급받아, MUX A와 MUX B에 의해 정렬된 두 피연산자(ALUA, ALUB)의 수학적/논리적 계산을 가동하는 CPU의 핵심 심장부입니다 .
2.1. 연산 선택 동작 사양 (Arithmetic & Logical Operations)
구현된 ALU는 가산기와 감산기뿐만 아니라 비트 연산, 시프트 연산, 대소 비교 연산까지 하나의 case 구조 내에서 완벽하게 처리하도록 조합논리(always @(*))로 설계되었습니다.
always @(*) begin
case(ALUSel)
4'b0000: ALUOUT = ALUA + ALUB; // ADD, ADDI (덧셈)
4'b1000: ALUOUT = ALUA - ALUB; // SUB (뺄셈)
4'b0111: ALUOUT = ALUA & ALUB; // AND (논리곱)
4'b0110: ALUOUT = ALUA | ALUB; // OR (논리합)
4'b0100: ALUOUT = ALUA ^ ALUB; // XOR (배타적 논리합)
4'b0001: ALUOUT = ALUA << ALUB[4:0]; // SLL, SLLI (논리 좌측 시프트)
4'b0101: ALUOUT = ALUA >> ALUB[4:0]; // SRL, SRLI (논리 우측 시프트)
4'b1101: ALUOUT = $signed(ALUA) >>> ALUB[4:0]; // SRA, SRAI (부호 확장 산술 우측 시프트)
4'b0010: ALUOUT = ($signed(ALUA) < $signed(ALUB)); // SLT, SLTI (부호 있는 대소 비교)
4'b0011: ALUOUT = (ALUA < ALUB); // SLTU, SLTUI (부호 없는 대소 비교)
4'b1111: ALUOUT = ALUB; // LUI (상위 즉시값 우회 통과)
default: ALUOUT = 32'd0;
endcase
end
2.2. 시프트 연산과 LUI의 설계 포인트
- 하위 5비트 마스킹 적용: SLL, SRL, SRA와 같은 시프트 연산 수행 시, 시프트 횟수를 지정하는 ALUB 입력의 하위 5비트(ALUB[4:0])만 사용하도록 명확하게 제약하여 32비트 범위를 벗어나는 데이터 오염을 하드웨어 레벨에서 방지하고 있습니다 .
- $signed 연산자 매핑: 산술 우측 시프트(SRA) 및 대소 비교(SLT) 명령어의 부호 확장을 Verilog 컴파일러가 인식할 수 있도록 피연산자에 $signed 캐스팅 연산자를 정밀하게 적용하여 정확한 하드웨어 부호 확장이 유도되도록 구현되었습니다 .
3. 핵심 모듈 간 제어 신호 상호작용 개요
이번 포스팅에서 분석한 Control.v와 ALU.v가 전체 아키텍처 다이어그램 안에서 서로 결선되는 유기적 매커니즘은 다음과 같습니다.
- Control.v가 명령어의 Opcode 정보를 수신하여 디코딩을 수행합니다 .
- 명령어 연산 종류가 R-Type 계산 명령어(5'b01100)일 경우 ALUSel 버스에 명령어 고유의 func_opcode[8:5](funct7 및 funct3 가 취합된 비트) 제어 데이터를 그대로 적재하여 ALU로 전달합니다 .
- I-Type 계산 명령어(5'b00100)일 경우에는 시프트 명령어(3'b101) 여부를 판단하여 ALUSel 제어 비트 조합을 동적으로 생성해 전송합니다 .
- ALU.v는 전달받은 신호대로 두 피연산자 연산을 동기적으로 수행하여 최종 데이터 주소선 혹은 연산 결과선인 ALUOUT 버스를 가동하게 됩니다 .
4. 설계 분석 요약
- Control Unit: 명령어의 디코딩과 분기 조건 판별을 담당하는 CPU의 조합논리 컨트롤 타워입니다 .
- ALU Core: 4비트 제어 버스 신호인 ALUSel의 사양에 따라 실제 가산, 감산, 논리 및 시프트 계산을 정확하게 처리하는 하드웨어 코어 엔진입니다 .
이것으로 CPU 설계의 핵심 중추인 제어부와 연산부 모듈의 Verilog 소스 코드 분석을 마치겠습니다.
다음 2편 [주변 하위 모듈 및 메모리 편]에서는 레지스터 보존 창고인 Reg.v, 비트 재배치를 담당하는 Imm_Gen.v, 조건 분기 판별 소자 Branch_Comp.v, 그리고 가변 데이터 메모리 Data_Mem.v 소스 코드를 차근차근 분석해 보도록 하겠습니다.
'RISC-V > RISC-V 설계' 카테고리의 다른 글
| [RISC-V] Single Cycle CPU 직접 설계하고 구현하기 4편 - 최상위 결선 및 시뮬레이션 검증 (0) | 2026.05.25 |
|---|---|
| [RISC-V] Single Cycle CPU 직접 설계하고 구현하기 3편 - 주변 하위 모듈 및 메모리 구조 설계 (0) | 2026.05.25 |
| [RISC-V] Single Cycle CPU 직접 설계하고 구현하기 1편 (0) | 2026.05.24 |