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 블록이 없다면:
- 시뮬레이션 시작.
- regs 배열의 값은 전부 X.
- 프로세서가 ADD x3, x1, x2를 수행.
- x1, x2가 X이므로 결과 x3도 X.
- 이 X가 전체 회로로 퍼져나가서 디버깅이 불가능해집니다.
따라서, FPGA의 실제 동작(전원 켜면 0임)을 시뮬레이션에 똑같이 반영하기 위해 설계 코드 안에 initial을 넣는 것이 Xilinx 가이드라인에 부합하는 방식입니다.
결론
사용자님의 우려와 달리, 이 프로젝트(PYNQ-Z2/Vivado)에서는 설계 파일 안에 initial을 넣는 것이 맞습니다.
ㅎㅎ 하나 더 배우네요.
TB는 따로 하지 않겠습니다. 워낙 간단한 블록이기에...
다음에는 ALU를 설계하도록 하겠습니다.
읽어주셔서 감사합니다.
'Project > RISC-V CPU Architecture Design' 카테고리의 다른 글
| [6] Immediate Generator 설계 (0) | 2026.01.03 |
|---|---|
| [5] ALU 설계 (0) | 2026.01.02 |
| [3] RISC-V 프로젝트 기획 (0) | 2026.01.02 |
| [2] 16bit CPU Design 이후 프로젝트 방향 feat. RISC-V (0) | 2024.11.15 |
| [1] 16bit CPU in Verilog (feat. Von Neumann Arch.) (2) | 2024.10.22 |