Verification 방법을 설명한다.
구현한 모든 명령어 타입을 검증한다.
코드 링크
https://github.com/liquetxnx/RV32I_single_cycle_processor
이번 테스트 프로그램은 RV32I single-cycle CPU를 검증하기 위해 작성한 signature 기반 C 코드이다.
CPU가 실행한 연산 결과를 고정된 MMIO 주소(0x200)에 저장하고, testbench가 해당 메모리 값을 읽어 PASS/FAIL을 판단하는 방식이다.
RISC-V CPU 검증에서 자주 쓰는 방식으로,
프로그램이 결과를 sig[] 배열에 저장
testbench가 메모리의 특정 위치를 읽어서 기대값과 비교
즉, 실행 결과를 “메모리 덤프값”으로 증명하는 방식이다.
#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]
#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;
}
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
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
uint32_t (*fp)(uint32_t) = add1;
A = fp(A);
sig[19] = A;
여기서 중요한 포인트는 함수 포인터 호출이 jalr을 유도한다는 점이다.
fp(A) 실행 시 jalr로 점프 + 복귀 주소 처리 확인 가능
add1(6) 결과는 7이므로 sig[19]=7이 되어야 한다.
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]로 검증
unsigned x = 0x123456;
sig[23] = x;
컴파일러는 큰 상수를 레지스터에 로드할 때 대개:
lui + addi(ori) 형태를 사용한다.
즉, 이 값이 메모리에 제대로 저장되면 LUI 경로도 정상임을 확인할 수 있다.
함수 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()는 8을 return 한다.
JAL 검증 포인트 (sig[34])
sig[34] = fib() + 2;
fib()가 정상적으로 호출되고 return되었다면
8 + 2 = 10이 저장된다.
즉 jal로 함수 호출 → 복귀까지 정상 동작해야 sig[34] == 10이 된다.
uint32_t pc;
asm volatile("auipc %0, 0\n" : "=r"(pc));
sig[35] = pc;
sig[35]에는 AUIPC가 실행된 위치 근처의 PC 값이 저장된다.
이 값은 코드 위치에 따라 달라질 수 있으므로,
TB에서는 “정확한 상수”보다는 정렬/범위 조건으로 검증하는 방식이 안전하지만 컴파일과 코드가 고정되어 있어 이 방식을 사용했다.
sig[36] = 0x7FFFFFFF;
마지막에 end sign을 찍어주면 TB에서
프로그램이 정상 종료 경로까지 도달했는지
혹은 중간에서 루프/버그로 멈췄는지
를 쉽게 판단할 수 있다.
이 테스트 프로그램은 다음을 커버한다.
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)
Goal : RV32I single-cycle CPU의 기능을 C 테스트 프로그램 + Testbench 자동 채점 방식으로 검증한다.
결과는 Data Memory Signature 영역(0x200) 에 저장되며, TB가 RAM 값을 읽어 PASS/FAIL을 출력한다.
이 프로젝트는 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] 로 매칭된다.
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) 확인 가능
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 넘어가면 자동 채점 후 종료
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에 기록된다.
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 카운트 자동 집계
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가 정상적으로 동작했는지 확인
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");
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");
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로 큰 상수 로딩 검증
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 안정성 확인에 중요함
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 확인 후 테스트 종료
$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);
이 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 자동 채점
힘들다.. 이걸로 끝