본문 바로가기

RISC-V/RISC-V 설계

[RISC-V] Single Cycle CPU 직접 설계하고 구현하기 3편 - 주변 하위 모듈 및 메모리 구조 설계

 지난 2편에서는 CPU의 브레인과 심장을 담당하는 제어 장치(Control.v)와 산술논리연산장치(ALU.v)의 소스 코드를 밀도 있게 분석해 보았습니다.

https://idkihg.tistory.com/180

 

[RISC-V] Single Cycle CPU 직접 설계하고 구현하기 2편 - 핵심 연산 및 제어 회로 설계

지난 포스팅에서는 RISC-V Single-Cycle CPU의 이론적인 데이터패스 구조와 전체를 통제하는 10가지 핵심 제어 신호(Control Signals)의 개념에 대해 알아보았습니다. 이번 포스팅부터는 실제 Verilog HDL 소스

idkihg.tistory.com

 

 제어부와 연산부가 제 아무리 훌륭해도 계산할 데이터를 안정적으로 보관하고, 명령어 속에 숨겨진 상수를 올바른 크기로 확장하며, 메모리에서 필요한 만큼만 데이터를 읽고 쓸 수 없다면 CPU는 구동될 수 없습니다. 이번 2편에서는 CPU 데이터 흐름의 뼈대를 지탱하는 주변 하위 하드웨어 모듈 3가지와 데이터 메모리(Data_Mem.v)의 소스 코드를 심층 분석해 보겠습니다.

 

1. 명령어 메모리 (Inst_Mem.v) 내부 소스 코드 분석

명령어 메모리는 실행할 프로그램 코드(기계어 코드)가 저장되어 있는 공간입니다. 단일 사이클 CPU에서는 매 클럭마다 프로그램 카운터(PC)가 가리키는 주소의 명령어를 즉각적으로 읽어와야 하므로, 쓰기 기능이 배제된 비동기 읽기 전용 구조(ROM 형태)로 모델링됩니다.

1.1. 주소 인덱싱 및 명령어 비동기 로드 코드

module Inst_Mem (
    input  [31:0] PC,              // 현재 실행 중인 프로그램 카운터 주소 입력
    output [31:0] instruction      // 주소에 해당하는 32비트 기계어 명령어 출력
);

    reg [31:0] MEM [0:1023];       // 32비트 단위의 명령어 메모리 배열 (총 4KB 공간)
    
    assign instruction = MEM[PC[11:2]]; // 입력을 받아 주소 경계선과 매핑하여 실시간으로 출력하는 조합논리 회로

    integer i;
    initial begin
        for (i = 0; i < 1024; i = i + 1)
            MEM[i] = 32'h0000_0013;         // 시뮬레이션 시작 시 전체 영역을 NOP(ADDI x0, x0, 0) 명령어로 안전 초기화
    end
endmodule

 

  • PC[11:2] 주소 슬라이싱의 이유: RISC-V의 명령어는 4바이트(32비트) 크기이기 때문에, 실제 메모리 주소(PC)는 항상 0, 4, 8, 12... 형태로 4씩 증가합니다. 그러나 우리가 선언한 하드웨어 레지스터 배열 MEM은 0, 1, 2, 3... 형태의 워드 단위 인덱스를 사용하므로, 바이트 주소를 워드 주소로 변환하기 위해 하위 2비트를 버리는 주소 시프팅(PC[11:2]) 결선이 반드시 필요합니다.
  • 비동기 assign 구조: 명령어 추출은 클럭 에지까지 기다릴 여유가 없습니다. PC 주소가 갱신되는 순간 콤비네이셔널하게 즉시 instruction 버스에 기계어 비트가 실려야 다음 데이터패스(디코드 단계)를 한 사이클 안에 마칠 수 있습니다.

 

2. 레지스터 파일 (Reg.v) 내부 소스 코드 분석

레지스터 파일은 CPU 내부의 초고속 임시 보관함입니다. RISC-V 규격의 핵심인 "동시 2개 읽기(조합논리), 1개 쓰기(순차회로)"가 어떻게 코드로 매핑되어 있는지 잘라보겠습니다.

 

2.1. 클럭이 필요 없는 실시간 읽기 패스 코드

assign dataA = (addrA == 5'd0) ? 32'd0 : x[addrA];
assign dataB = (addrB == 5'd0) ? 32'd0 : x[addrB];

 RISC-V ISA 규격상 x0 레지스터는 무조건 0이어야 합니다. 주소 핀이 5'd0을 가리키면 물리적인 레지스터 배열(x[0])을 보지 않고 곧바로 하드웨어 상수 32'd0 선을 선택해 출력하도록 MUX 구조를 형성합니다.

 

2.2. 클럭 에지에서 동기화되는 쓰기 패스 코드

always @(posedge clk) begin
    if (rst) begin // 리셋 신호 시 32개 레지스터 전체를 0으로 초기화
        for(i = 0; i < 32; i = i + 1) begin
                x[i] <= 32'b0;
            end         
    end else begin
        if (we & (addrD != 5'd0)) begin // 쓰기 활성화(we) 상태이면서, 동시에 쓰려는 방이 x0(5'd0)가 아닐 때만 저장!
            x[addrD] <= dataD;
        end
    end
end

 외부에서 아무리 쓰기 활성화 신호(we)를 키고 x0 방에 값을 밀어 넣으려고 해도, addrD != 5'd0 이라는 하드웨어 조건문이 이를 필터링하여 x0 내부 데이터가 오염되는 것을 완벽하게 방어합니다.

 

 

3. 즉시값 확장기 (Imm_Gen.v) 내부 소스 코드 분석

 명령어 텍스트에 내장된 작은 상수(Immediate)들을 ALU 연산에 태우기 위해 32비트로 정렬하고 확장하는 모듈입니다.

 

3.1. 컨캣(Concatenation) 연산자를 통한 비트 셔플링 코드

always @(*) begin
    case (immSel)
        I:       immOut = {{20{imm[31]}}, imm[31:20]}; // I-Type
        B:       immOut = {{19{imm[31]}}, imm[31], imm[7], imm[30:25], imm[11:8], 1'b0}; // B-type
        J:       immOut = {{11{imm[31]}}, imm[31], imm[19:12], imm[20], imm[30:21], 1'b0}; //J-Type
        // ... 생략 ...
  • {20{imm[31]}}은 imm[31] 선을 똑같이 20개 복사해 병렬로 배치하라는 의미로, 하드웨어 레벨에서 완벽한 부호 확장(Sign Extension) 회로를 의미합니다.
  • RISC-V 명령어 주소는 항상 2의 배수 경계에 정렬되므로 분기/점프 오프셋의 최하위 비트(Bit 0)는 무조건 0입니다. 코드 공간을 아끼기 위해 명령어에는 이 비트가 없으므로, Imm_Gen이 출력할 때 끝자리에 의도적으로 1'b0 선을 덧붙여 결선해 줍니다.

 

4. 분기 비교기 (Branch_Comp.v) 내부 소스 코드 분석

 조건 분기 명령어(B-Type)가 들어왔을 때, 레지스터에서 뽑아낸 두 값의 대소를 실시간으로 비교하여 컨트롤러에게 참/거짓 플래그를 던져주는 전용 판독 모듈입니다.

 

4.1 단 한 줄로 구현하는 가변 부호 비교 회로 코드

assign BrEq = (dataA == dataB);
assign BrLT = BrUn ? (dataA < dataB) : ($signed(dataA) < $signed(dataB)); // BrUn 신호가 1이면 무부호(Unsigned), 0이면 부호 있는(Signed) 대소 비교 MUX 가동

 

컨트롤러가 던져주는 BrUn 제어선 하나로 BLT와 BLTU 명령어를 통합 제어합니다. 삼항 연산자 내부에서 $signed() 지시어를 유기적으로 결합해 줌으로써 최상위 비트(MSB)를 부호 비트로 해석할지 결정하는 정밀 하드웨어 비교기 멀티플렉싱을 유도해 냅니다.

 

5. 가변 데이터 메모리 (Data_Mem.v) 내부 소스 코드 분석

데이터패스의 최종 종착지 중 하나이며, RISC-V 가변 메모리 접근 규격(lb, lh, lw, sb, sh, sw)을 모두 소화해야 하므로 내부 주소 디코더의 해독 결선이 매우 정밀하게 설계되어야 합니다.

 

5.1. 주소선 분할 슬라이싱 및 인덱싱 가공 코드

reg [31:0] MEM_Data [0:1023];       // 32비트 워드 단위 메모리 배열 (총 4KB 공간)

wire [9:0] word_addr = ADDR[11:2];   // 상위 비트로 32비트 워드 배열 인덱스 추출
wire [1:0] byte_offset = ADDR[1:0]; // 하위 2비트로 1워드(4바이트) 내부 세부 위치 추적
wire [31:0] current_word = MEM_Data[word_addr];

 실제 하드웨어 메모리는 관리 효율성을 위해 기본적으로 32비트(4Byte) 워드 단위로 묶여 동작합니다. 따라서 입력된 실제 주소(ADDR)의 하위 2비트인 byte_offset을 칼날처럼 발라내어, 4바이트 중 정확히 몇 번째 바이트를 건드릴 것인지 추적하는 기틀을 마련합니다.

 

5.2. 가변 읽기(Load) 회로의 바이트 마스킹 코드

always @(*) begin
    case (WordSizeSel)
        3'b000: begin // LB (8-bit Sign Extended 로드)
            case (byte_offset)
                2'b00: ReadData = {{24{current_word[7]}},  current_word[7:0]};  // 0번째 바이트만 취합+부호확장
                2'b01: ReadData = {{24{current_word[15]}}, current_word[15:8]}; // 1번째 바이트만 취합+부호확장
                // ... 생략 ...

 값을 메모리에서 읽어올 때는 CPU 한 사이클 내에 빠르게 반응해야 하므로 비동기식 조합논리(always @(*))를 적용합니다. 주소가 들어오자마자 바이트 오프셋 MUX 트리를 거쳐 원하는 8비트 구역만 쏙 빼내고, 남은 상위 공간을 해당 데이터의 MSB 비트로 도배하여 출력 버스로 밀어내 줍니다.

 

5.3. 가변 쓰기(Store) 회로의 비트 보존 기술 코드

always @(posedge clk) begin
    if (MemWrite) begin
        case (WordSizeSel)
            3'b000: begin // SB (Store Byte): 특정 1바이트 구역만 핀포인트 덮어쓰기
                case (byte_offset)
                    2'b00: MEM_Data[word_addr][7:0]   <= WriteData[7:0];
                    2'b01: MEM_Data[word_addr][15:8]  <= WriteData[7:0];
                    // ... 생략 ...

 

 값을 새겨 넣을 때는 타이밍 마진과 데이터 보존을 위해 클럭 동기식 순차회로(always @(posedge clk)) 내부에서 구동합니다. 메모리 배열의 특정 비트 슬라이스 범위([15:8])에만 하드웨어 쓰기 활성화 신호선(Write Enable line)을 부분 엮어주어, 기존 메모리에 저장되어 있던 나머지 바이트 데이터들이 지워지거나 오염되지 않도록 완벽하게 보존합니다.

 

5. 단일 사이클 주변부 설계 마치는 글

  • Reg 파일 및 Imm_Gen: 하드웨어 고정 상수 회로(x0=0)와 아키텍처 규격에 맞춘 가변 즉시값 부호 확장 전처리 기지입니다.
  • Branch_Comp: 단 한 줄의 삼항 연산자로 부호/무부호 대소를 가리는 조합논리 플래그 판독기입니다.
  • Inst_Mem & Data_Mem: PC 주소 매핑을 통해 기계어를 실시간 인출하는 공간과 하위 주소 2비트를 쪼개어 가변 바이트 저장을 수행하는 데이터 저장 기지입니다.

 이것으로 CPU 설계의 핵심 중추를 보좌하는 주변 하위 모듈과 데이터 메모리 변환 회로의 Verilog 소스 코드 분석을 마치겠습니다.

다음 3편 [최상위 결선 및 시뮬레이션 편]에서는 개별 하위 모듈 버스들을 모아서 한 곳으로 밀집해 엮어내는 탑 모듈 RISC_V.v와, 모든 명령어 세트 시나리오가 완벽히 동작하는지 검증하는 테스트벤치 RISC_V_tb.v 소스 코드를 차근차근 분석해 보도록 하겠습니다 .

Branch_Comp.v
0.00MB
Data_Mem.v
0.00MB
Imm_Gen.v
0.00MB
Inst_Mem.v
0.00MB
Reg.v
0.00MB