RV32I - Verification

손은상·2026년 1월 15일

개요

Verification 방법을 설명한다.

구현한 모든 명령어 타입을 검증한다.

코드 링크
https://github.com/liquetxnx/RV32I_single_cycle_processor


RV32I Signature 기반 검증용 C Test Program (R/I/Branch/JALR/JAL/AUIPC + Fibonacci)

이번 테스트 프로그램은 RV32I single-cycle CPU를 검증하기 위해 작성한 signature 기반 C 코드이다.

CPU가 실행한 연산 결과를 고정된 MMIO 주소(0x200)에 저장하고, testbench가 해당 메모리 값을 읽어 PASS/FAIL을 판단하는 방식이다.


1) Signature 방식이란?

RISC-V CPU 검증에서 자주 쓰는 방식으로,

프로그램이 결과를 sig[] 배열에 저장

testbench가 메모리의 특정 위치를 읽어서 기대값과 비교

즉, 실행 결과를 “메모리 덤프값”으로 증명하는 방식이다.


2) Signature base address

#define SIG_ADDR 0x00000200u
volatile uint32_t * const sig = (uint32_t*) SIG_ADDR;


SIG_ADDR = 0x200

word addressing 기준으로 sig[i]는 RAM[128 + i]에 저장된다.

이유: 0x200 / 4 = 128

즉, 아래처럼 매핑된다.

sig[0] → RAM[128]

sig[1] → RAM[129]

…

sig[36] → RAM[164]

3) 테스트 코드 전체

#include <stdint.h>

#define SIG_ADDR 0x00000200u

volatile uint32_t * const sig = (uint32_t*) SIG_ADDR;

uint32_t add1(uint32_t x);// jalr 감지 위한 함수
uint32_t fib();

uint32_t main(void){
    int32_t A=6, B=-10, D=8, F=2;
    uint32_t C=-10;

    uint32_t (*fp)(uint32_t) = add1;

    //R-type 확인
    sig[0]=A+B; // -4
    sig[1]=A-B; // 16
    sig[2]=A<<D; //1536
    sig[3]=C>>F; //3FFFFFFD
    sig[4]=B>>F; //FFFFFFFD

    A=sig[4]; B=sig[2];

    sig[5]=A<B; //1
    sig[6]=(uint32_t)A<(uint32_t)B; // 0
    sig[7]= A^B; // FFFFF9FD
    sig[8]= A|B; // FFFFFFFD
    sig[9]= A&B; // 00000600

    //I-type 확인
    A=6; B=-10;
    sig[10]=A-10;
    sig[11]=A<<8;
    sig[12]=B>>2;
    sig[13]=(uint32_t)B>>2;
    sig[14]=B<8; 
    sig[15]=(uint32_t)B<8;
    sig[16]=A^8;
    sig[17]=A|8;
    sig[18]=A&8;

    A=fp(A); //jalr 확인
    sig[19]=A; // 7

    //branch 확인  A=7, B=-10
    if(A==B) sig[20]=0;
    else sig[20]=1;

    if(A<B) sig[21]=0;
    else sig[21]=1;

    if((uint32_t)A<(uint32_t)B) sig[22]=0;
    else sig[22]=1;

    //LUI 큰 상수 강제로 확인
    unsigned x = 0x123456;
    sig[23]=x;   

    // Fibonacci + jal 검증
    sig[34]=fib()+2; // 만약 jal로 함수 호출 후 정상 복귀했다면 10이 저장됨

    // AUIPC 검증 (PC 얻기)
    uint32_t pc;
    asm volatile(
        "auipc %0, 0\n"
        : "=r"(pc)
    );
    sig[35] = pc;

    // end signature
    sig[36]=0x7FFFFFFF;

    return 0;
}

uint32_t add1(uint32_t x){return x+1;}

uint32_t fib(){
    uint32_t i;
    sig[24] =1;
    sig[25] =1;

    for(i=26;i<34;i++){ 
        sig[i]=sig[i-1]+sig[i-2];
    }
    return 8;
}

4) 검증 항목 상세 설명

(1) R-type 검증 (sig[0] ~ sig[9])

sig[0]=A+B;      // add
sig[1]=A-B;      // sub
sig[2]=A<<D;     // sll
sig[3]=C>>F;     // srl (logical)
sig[4]=B>>F;     // sra (arithmetic)


A=6, B=-10, D=8, F=2, C=(uint32_t)-10

signed/unsigned shift 차이를 같이 검증 가능

추가로 비교 + bitwise 연산까지 포함한다.

sig[5]=A<B;                          // slt
sig[6]=(uint32_t)A<(uint32_t)B;      // sltu
sig[7]=A^B;                          // xor
sig[8]=A|B;                          // or
sig[9]=A&B;                          // and

(2) I-type 검증 (sig[10] ~ sig[18])

I-type ALU 연산도 동일하게 확인한다.

sig[10]=A-10;             // addi
sig[11]=A<<8;             // slli
sig[12]=B>>2;             // srai (signed)
sig[13]=(uint32_t)B>>2;   // srli (unsigned)
sig[14]=B<8;              // slti
sig[15]=(uint32_t)B<8;    // sltiu
sig[16]=A^8;              // xori
sig[17]=A|8;              // ori
sig[18]=A&8;              // andi

(3) JALR 검증 (sig[19])

uint32_t (*fp)(uint32_t) = add1;
A = fp(A);
sig[19] = A;

여기서 중요한 포인트는 함수 포인터 호출이 jalr을 유도한다는 점이다.

fp(A) 실행 시 jalr로 점프 + 복귀 주소 처리 확인 가능

add1(6) 결과는 7이므로 sig[19]=7이 되어야 한다.

(4) Branch 검증 (sig[20] ~ sig[22])

if(A==B) sig[20]=0;
else sig[20]=1;

if(A<B) sig[21]=0;
else sig[21]=1;

if((uint32_t)A<(uint32_t)B) sig[22]=0;
else sig[22]=1;


A=7, B=-10일 때

예상되는 결과:

A==B → false → sig[20]=1

A<B (signed) → false → sig[21]=1

A<B (unsigned) → true/false가 signed와 달라질 수 있음 → sig[22]로 검증

(5) LUI 검증 (sig[23])

unsigned x = 0x123456;
sig[23] = x;

컴파일러는 큰 상수를 레지스터에 로드할 때 대개:

lui + addi(ori) 형태를 사용한다.

즉, 이 값이 메모리에 제대로 저장되면 LUI 경로도 정상임을 확인할 수 있다.

(6) Fibonacci loop + JAL 검증 (sig[24] ~ sig[34])

함수 fib()는 피보나치 수열을 메모리에 저장한다.

sig[24] = 1;
sig[25] = 1;

for(i=26;i<34;i++){
    sig[i] = sig[i-1] + sig[i-2];
}


따라서 저장되는 값은:

index	value
24	1
25	1
26	2
27	3
28	5
29	8
30	13
31	21
32	34
33	55

그리고 fib()8return 한다.

JAL 검증 포인트 (sig[34])

sig[34] = fib() + 2;

fib()가 정상적으로 호출되고 return되었다면

8 + 2 = 10이 저장된다.

즉 jal로 함수 호출 → 복귀까지 정상 동작해야 sig[34] == 10이 된다.

(7) AUIPC 검증 (sig[35])

uint32_t pc;
asm volatile("auipc %0, 0\n" : "=r"(pc));
sig[35] = pc;

sig[35]에는 AUIPC가 실행된 위치 근처의 PC 값이 저장된다.

이 값은 코드 위치에 따라 달라질 수 있으므로,
TB에서는 “정확한 상수”보다는 정렬/범위 조건으로 검증하는 방식이 안전하지만 컴파일과 코드가 고정되어 있어 이 방식을 사용했다.

(8) End signature (sig[36])

sig[36] = 0x7FFFFFFF;

마지막에 end sign을 찍어주면 TB에서

프로그램이 정상 종료 경로까지 도달했는지

혹은 중간에서 루프/버그로 멈췄는지

를 쉽게 판단할 수 있다.


Summary

이 테스트 프로그램은 다음을 커버한다.

1. R-type: add/sub/shift/slt/xor/or/and

2. I-type: addi/shift immediate/slti/xori/ori/andi

3. JALR: 함수 포인터 호출 기반 jalr 검증

4. Branch: signed/unsigned 비교 기반 분기 검증

5. LUI: 큰 상수 로딩 검증

6. Loop 기반 검증: Fibonacci 수열 생성

7. JAL 검증: fib() 호출 후 복귀 확인 (sig[34]=10)

8. AUIPC 검증: auipc rd,0로 PC 확보 (sig[35])

9. End sign: 정상 종료 확인 (sig[36]=0x7FFFFFFF)




RV32I Single-Cycle CPU Testbench (Signature 기반 자동 검증)

Goal : RV32I single-cycle CPU의 기능을 C 테스트 프로그램 + Testbench 자동 채점 방식으로 검증한다.

결과는 Data Memory Signature 영역(0x200) 에 저장되며, TB가 RAM 값을 읽어 PASS/FAIL을 출력한다.



1. 테스트 검증 구조 개요

  • 이 프로젝트는 RISC-V에서 자주 쓰는 Signature 방식으로 검증한다.

  • C 프로그램이 결과를 특정 주소(0x200)에 저장

  • Testbench가 해당 메모리를 읽고 기대값과 비교 후 최종 PASS/FAIL summary 출력해 Verification_result에 저장

  • Signature base address

#define SIG_ADDR 0x00000200u
volatile uint32_t * const sig = (uint32_t*) SIG_ADDR;


SIG_ADDR = 0x200
  • word addressing 기준으로 0x200 / 4 = 128

  • 즉 sig[0]은 TB에서 RAM[128] 로 매칭된다.



2. Testbench 주요 기능

(1) Waveform dump 설정

initial begin
    $dumpfile("waves_cpu.vcd");
    $dumpvars(0,tb_cpu.DUT.datapath_inst.pc_inst);
    $dumpvars(0,tb_cpu.DUT.datapath_inst);
    $dumpvars(0,tb_cpu.DUT.datapath_inst.pc_next_inst);
    $dumpvars(0,tb_cpu.DUT.datapath_inst.reg_inst);
    $dumpvars(0,tb_cpu.DUT.datapath_inst.alu_inst);   
    $dumpvars(0,tb_cpu.DUT.datapath_inst.immgen_inst);      
end
  • waves_cpu.vcd 생성

  • datapath 내부 신호(PC, ALU, RegFile, ImmGen) 확인 가능

(2) clock/reset 및 timeout 기반 종료

initial clk=0;
initial reset=0;
initial cycles=0;

initial #2 reset=1;
initial #4 reset=0;
always #1 clk = ~clk;
  • reset pulse를 짧게 주고 실행 시작

  • MAX_CYCLES = 2000 넘어가면 자동 채점 후 종료



3. 실행 trace 출력 (디버깅 로그)

  • TB는 실행 중 Register write / Memory write 발생 시 로그를 남긴다.
if (`REG_SIG) $fdisplay(fd,
  "REG :: pc= %h \treg_indx=%8d  \t wdata=%8d",
  `PC_SIG, `INSTR[11:7], `WDATA);

if (`MEM_SIG) $fdisplay(fd,
  "MEM :: pc= %h \tmem_addr=%08h \t wdata=%8d",
  `PC_SIG, `ALU_OUT, `MEM_DATA);

로그 예시

  • 어떤 PC에서 어떤 레지스터가 쓰였는지 (reg_indx, wdata)
  • 어떤 PC에서 어떤 메모리 주소에 store 했는지 (mem_addr, wdata)
  • 위 로그들은 trace.log에 기록된다.


4. 자동 채점 함수 (CHECK_RAM)

Signature memory를 읽어서 기대값과 비교한다.

task automatic CHECK_RAM;
    input integer fd;
    input integer idx;
    input [31:0] expect;
    input [200:0] name;
    reg [31:0] actual;
begin
    actual = `RAM[idx];
    if (actual === expect) begin
        $fdisplay(fd, "%s : PASS  (RAM[%0d]=0x%08h)", name, idx, actual);
        pass_cnt = pass_cnt + 1;
    end else begin
        $fdisplay(fd, "%s : FAIL  (RAM[%0d]=0x%08h, expect=0x%08h)",
                  name, idx, actual, expect);
        fail_cnt = fail_cnt + 1;
    end
end
endtask
  • === 사용 → X/Z 상태까지 포함해서 정확히 비교 가능

  • PASS/FAIL 카운트 자동 집계



5. 검증 항목 리스트

(0) Load / Store

CHECK_RAM(result, 128, 32'hFFFF_FFFC, "I-type : lw"); 
CHECK_RAM(result, 128, 32'hFFFF_FFFC, "S-type : sw"); 
  • sig[0] 결과가 RAM[128]

  • store→load가 정상적으로 동작했는지 확인

(1) R-type 검증

CHECK_RAM(result, 128, 32'hFFFF_FFFC, "R-type : add");
CHECK_RAM(result, 129, 32'd16,        "R-type : sub");
CHECK_RAM(result, 130, 32'd1536,      "R-type : sll");
CHECK_RAM(result, 131, 32'h3FFF_FFFD, "R-type : srl");
CHECK_RAM(result, 132, 32'hFFFF_FFFD, "R-type : sra");
CHECK_RAM(result, 133, 32'd1,         "R-type : slt");
CHECK_RAM(result, 134, 32'd0,         "R-type : sltu");
CHECK_RAM(result, 135, 32'hFFFF_F9FD, "R-type : xor");
CHECK_RAM(result, 136, 32'hFFFF_FFFD, "R-type : or");
CHECK_RAM(result, 137, 32'h0000_0600, "R-type : and");
  • RV32I R-type 산술/논리/시프트/비교 연산 전부 포함

(2) I-type 검증

CHECK_RAM(result, 138, 32'hFFFF_FFFC, "I-type : addi");
CHECK_RAM(result, 139, 32'd1536,      "I-type : slli");
CHECK_RAM(result, 140, 32'hFFFF_FFFD, "I-type : srli");
CHECK_RAM(result, 141, 32'h3FFF_FFFD, "I-type : srai");
CHECK_RAM(result, 142, 32'd1,         "I-type : slti");
CHECK_RAM(result, 143, 32'd0,         "I-type : sltui");
CHECK_RAM(result, 144, 32'd14,        "I-type : xori");
CHECK_RAM(result, 145, 32'd14,        "I-type : ori");
CHECK_RAM(result, 146, 32'd0,         "I-type : andi");
CHECK_RAM(result, 147, 32'd7,         "I-type : jalr");

(3) Branch / LUI

CHECK_RAM(result, 148, 32'd1,         "B-type : beq and bne");
CHECK_RAM(result, 149, 32'd1,         "B-type : blt and bge");
CHECK_RAM(result, 150, 32'd0,         "B-type : bltu and bgeu");
CHECK_RAM(result, 151, 32'h0012_3456, "U-type : lui");
  • signed/unsigned branch 비교 모두 포함

  • LUI로 큰 상수 로딩 검증

(4) Fibonacci sequence (loop 기반 추가 검증)

CHECK_RAM(result, 152, 32'd1, "Fibo : a[0]=1");
CHECK_RAM(result, 153, 32'd1, "Fibo : a[1]=1");
CHECK_RAM(result, 154, 32'd2, "Fibo : a[2]=2");
CHECK_RAM(result, 155, 32'd3, "Fibo : a[3]=3");
CHECK_RAM(result, 156, 32'd5, "Fibo : a[4]=5");
CHECK_RAM(result, 157, 32'd8, "Fibo : a[5]=8");
CHECK_RAM(result, 158, 32'd13,"Fibo : a[6]=13");
CHECK_RAM(result, 159, 32'd21,"Fibo : a[7]=21");
CHECK_RAM(result, 160, 32'd34,"Fibo : a[8]=34");
CHECK_RAM(result, 161, 32'd55,"Fibo : a[9]=55");
  • 단순 연산만이 아니라 반복문 기반 흐름 제어까지 검증 가능

  • 루프가 제대로 돈다는 것 자체가 CPU 안정성 확인에 중요함

(5) JAL / AUIPC / End Sign

CHECK_RAM(result, 162, 32'd10,   "J-type : JAL");
CHECK_RAM(result, 163, 32'h300,  "U-type : AUIPC");
CHECK_RAM(result, 164, 32'h7FFF_FFFF, "end sign : pass");
  • jal / auipc 결과를 signature에 저장해서 검증

  • 마지막에 end signature 확인 후 테스트 종료



6. 결과 출력 형식

$fdisplay(result, "TOTAL PASS = %0d", pass_cnt);
$fdisplay(result, "TOTAL FAIL = %0d", fail_cnt);

if (fail_cnt == 0) $display("ALL TEST PASSED ✅");
else               $display("TEST FAILED ❌ fail=%0d", fail_cnt);
  • Verification_result 파일에 PASS/FAIL 로그가 저장된다.


7. Summary

이 Testbench는 다음을 자동 검증한다.

1. RV32I R-type / I-type 산술 및 논리 연산

2. Shift 연산 (logical / arithmetic)

3. Branch (signed / unsigned)

4. jalr, jal

5. lui, auipc

6. loop 기반 Fibonacci sequence

7. Signature 기반 PASS/FAIL 자동 채점

힘들다.. 이걸로 끝

profile
1렙 대학생

0개의 댓글