Project/RISC-V CPU Architecture Design

[4] Single-Cycle 프로세서 Register File 설계

by 한PU 2026. 1. 2.
반응형

Single-Cycle RV32I 프로세서 블록 다이어그램

Single-Cycle 프로세서는 모든 명령어가 단 한 번의 클럭 사이클 안에 Fetch부터 Execute, Writeback까지 완료되는 구조입니다.

따라서 데이터의 흐름이 파이프라인 레지스터 없이 하나의 긴 경로로 연결되어 있습니다.

 

Bottom-Up 방식으로 Register File부터 만들어봅시다.

 

Register File

스펙

- 크기: 32x32

32bit 레지스터 32개를 만들겁니다. x0 ~ x31까지겠네요.

 

- Port 구성

Read Port 2개, Write Port 1개를 뚫겠습니다.

Read를 위해서 주소가 입력되면 클럭에 상관없이 즉시 데이터가 나와야하므로 Async하게 설계합니다.

Write Ports는 Pos Edge에 맞춰 저장될 수 있게 Sync하게 설계합니다.

 

- x0

x0는 항상 0을 출력하는 Zero Register입니다.

 

설계 코드

`timescale 1ns / 1ps

module register_file (
    input  wire        clk_i           , // Clock input

    // Read Port 1
    input  wire [4:0]  rs1_addr_i      , // Source register 1 address
    output wire [31:0] rs1_data_o      , // Source register 1 data

    // Read Port 2
    input  wire [4:0]  rs2_addr_i      , // Source register 2 address
    output wire [31:0] rs2_data_o      , // Source register 2 data

    // Write Port
    input  wire [4:0]  rd_addr_i       , // Destination register address
    input  wire [31:0] rd_data_i       , // Write data
    input  wire        reg_wen_i         // Write enable
);

    reg [31:0] regs_r [0:31]           ; // 32 registers
    integer    i                       ;

    // Initialization
    initial begin
        for (i = 0; i < 32; i = i + 1)
            regs_r[i] = 32'b0          ;
    end

    // Asynchronous Read (x0 is hardwired to 0)
    assign rs1_data_o = (rs1_addr_i == 5'b0) ? 32'b0 : regs_r[rs1_addr_i] ;
    assign rs2_data_o = (rs2_addr_i == 5'b0) ? 32'b0 : regs_r[rs2_addr_i] ;

    // Synchronous Write (Prevent writing to x0)
    always @(posedge clk_i) begin
        if (reg_wen_i && (rd_addr_i != 5'b0)) begin
            regs_r[rd_addr_i] <= rd_data_i ;
        end
    end

endmodule

대부분 이해 가능하실텐데, initial 블록에서 의문점이 생길것 같습니다.

사실 제가 의문점이 생겨서 제미나이한테 물어봤어요. 답변은 아래와 같습니다.

 

왜 initial 블록을 쓰는가?

매우 날카롭고 좋은 질문입니다. 보통 교과서나 ASIC(주문형 반도체) 설계 강의에서는 "initial 블록은 합성이 안 되니 테스트벤치(Testbench)에만 써라"라고 가르치기 때문입니다.

하지만 FPGA 설계, 특히 Xilinx Vivado 환경에서는 이야기가 다릅니다. 이 부분을 명확히 짚고 넘어가야 나중에 하드웨어 리소스 낭비를 막을 수 있습니다.

이유는 크게 3가지입니다.


1. FPGA의 특성: 비트스트림 초기화 (Bitstream Initialization)

ASIC과 달리, FPGA는 전원이 켜지고 '비트스트림(Bitstream)' 파일이 로드될 때 모든 메모리 소자(Flip-flop, BRAM, LUTRAM)의 초기값을 설정할 수 있습니다.

  • ASIC: 전원이 켜지면 레지스터 값은 랜덤(Unknown, 0 또는 1)입니다. 반드시 하드웨어 리셋(rst_n) 신호로 0을 만들어줘야 합니다.
  • FPGA: Vivado 합성 툴은 RTL 코드 내의 initial 블록을 해석해서, **"아, 이 메모리는 전원이 켜질 때 0으로 채워져 있어야 하는구나"**라고 인식하고 비트스트림에 그 정보를 심어줍니다. 즉, FPGA에서는 initial 블록이 부분적으로 합성 가능(Synthesizable)합니다.

2. 리소스 최적화: LUTRAM vs Flip-Flop

이것이 가장 기술적인 이유입니다. Register File을 어떻게 코딩하느냐에 따라 FPGA 내부에서 사용되는 물리적 자원이 달라집니다.

  • 하드웨어 리셋(rst_n)을 넣을 경우:모든 레지스터 비트(32개 x 32비트 = 1024개)에 리셋 배선이 연결되어야 합니다. 이렇게 되면 FPGA의 고밀도 메모리인 **LUTRAM(Distributed RAM)**을 사용하지 못하고, 개별 Flip-Flop 1024개를 사용하게 됩니다. 면적이 커지고 속도가 느려집니다.
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) regs[i] <= 0; // 모든 비트에 리셋 연결
    end
    
  • 리셋 없이 initial만 쓸 경우:리셋 배선이 필요 없으므로 Vivado가 이를 LUTRAM으로 아주 작게 뭉쳐서 구현해 줍니다. 속도가 빠르고 면적을 훨씬 적게 차지합니다.
  • 전문가의 조언: CPU의 레지스터 파일(x1~x31)은 굳이 하드웨어 리셋 버튼을 눌렀을 때 0이 될 필요가 없습니다. 어차피 소프트웨어가 실행되면서 새로운 값을 쓰기(Write) 전에는 읽지(Read) 않는 것이 원칙이기 때문입니다.
    initial begin ... end // 전원 켤 때만 0, 이후 리셋 버튼 눌러도 0 안 됨
    

3. 시뮬레이션의 'X' (Unknown) 방지

시뮬레이터(Vivado Xsim, ModelSim 등)에서 변수를 초기화하지 않고 읽으면 'X' (빨간색) 상태가 됩니다. 만약 initial 블록이 없다면:

  1. 시뮬레이션 시작.
  2. regs 배열의 값은 전부 X.
  3. 프로세서가 ADD x3, x1, x2를 수행.
  4. x1, x2가 X이므로 결과 x3도 X.
  5. 이 X가 전체 회로로 퍼져나가서 디버깅이 불가능해집니다.

따라서, FPGA의 실제 동작(전원 켜면 0임)을 시뮬레이션에 똑같이 반영하기 위해 설계 코드 안에 initial을 넣는 것이 Xilinx 가이드라인에 부합하는 방식입니다.


결론

사용자님의 우려와 달리, 이 프로젝트(PYNQ-Z2/Vivado)에서는 설계 파일 안에 initial을 넣는 것이 맞습니다.

 

ㅎㅎ 하나 더 배우네요.

TB는 따로 하지 않겠습니다. 워낙 간단한 블록이기에...

 

다음에는 ALU를 설계하도록 하겠습니다.

읽어주셔서 감사합니다.

반응형