[Minit-NPU RTL] Design Processing Element
"Processing Element"
pe_mac.v
8-bit 입력 2개를 받아 곱셈을 하고, "en" 신호가 "1"이면 해당 곱셈 결과 값을 누적하는 모듈
"clear" 신호가 "1"이면 누적된 값을 초기화
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | `timescale 1ns / 1ps module pe_mac #( parameter data_width = 8, // 데이터 크기 = 8-bit parameter mul_width = 2 * data_width, // Multiplication (곱한 값은 최대 2 * data_width) parameter acc_width = 2 * mul_width // Accumulation (곱한 결과 값 누적은 최대 2 * mul_width) ) ( input clk, input rst, input clear, // Accumulator 초기화 input en, // 연산 가능 신호 input [data_width-1:0] a_in, input [data_width-1:0] b_in, output [mul_width-1:0] mul_out, output reg [acc_width-1:0] acc_sum_out ); assign mul_out = a_in* b_in; // 곱셈 연산 always @ (posedge clk or negedge rst) begin if (!rst) begin // reset 신호 acc_sum_out <= 0; // Accumulator 초기화 end else if (clear) begin // Accumulator clear 신호 acc_sum_out <= 0; // Accumulator 초기화 end else if (en) begin // 누적 연산 가능 신호 acc_sum_out <= acc_sum_out + mul_out; // 누적 연산 end end endmodule | cs |
tb_pe_mac.v
"pe_mac.v" 코드를 Instance
테스트 벤치 코드에서도 pe_mac을 구현하여 DUT에서 계산한 결과와 TB에서 계산한 결과가 동일한지 확인
서로 맞지 않다면 Count를 늘려가며 통계에 사용
입력 값 "a_in", "b_in"은 0~255 범위 중 랜덤한 값을 사용
누적 신호 값 "en"은 0/1 중 랜덤한 값을 사용, "1"일 때 누적 진행
누적 값 초기화 "clear"는 5% 확률로 "1"이 되어 초기화
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | `timescale 1ns / 1ps module tb_pe_mac; localparam data_width = 8; // TB 파일에서만 적용 localparam mul_width = 2 * data_width; // TB 파일에서만 적용 localparam acc_width = 2 * mul_width; // TB 파일에서만 적용 reg clk; reg rst; reg clear; reg en; reg [data_width-1:0] a_in; reg [data_width-1:0] b_in; wire [mul_width-1:0] mul_out; wire [acc_width-1:0] acc_sum_out; // 통계를 위한 Count 변수 integer err_mul_cnt; integer err_acc_cnt; integer total_cnt; // ================================================================ // 1) Clock & Reset initial begin clk <= 0; forever #5 clk <= ~clk; // 주기 10ns end initial begin rst <= 0; // 초기화 a_in <= 0; // 초기화 b_in <= 0; // 초기화 en <= 0; // 초기화 clear <= 0; // 초기화 #30 // 30ns 이후 rst <= 1; // reset 신호 인가 #2000 // 2000ns동안 랜덤 입력에 대한 계산 진행 // 통계 출력 $display("==============================="); $display("[Total Cycles] : %d", total_cnt); $display("[MUL Errors] : %d", err_mul_cnt); $display("[ACC Errors] : %d", err_acc_cnt); $finish; // 종료 end // ================================================================ // 2) DUT와 TB값을 비교하기 위해 DUT와 똑같은 Logic을 TB 내부에 구현 wire [mul_width-1:0] tb_mul_out; reg [acc_width-1:0] tb_acc_sum_out; assign tb_mul_out = a_in * b_in; always @ (posedge clk or negedge rst) begin if (!rst) begin tb_acc_sum_out <= 0; end else if (clear) begin tb_acc_sum_out <= 0; end else if (en) begin tb_acc_sum_out <= tb_acc_sum_out + tb_mul_out; end end // ================================================================ // 3) 매 clock마다 DUT의 결과와 TB 결과를 비교 always @ (posedge clk) begin if (!rst) begin err_mul_cnt <= 0; err_acc_cnt <= 0; total_cnt <= 0; end else begin total_cnt <= total_cnt + 1; if (mul_out !== tb_mul_out) begin $display("[MUL_ERROR] Time=%0,t | DUT=%d, TB=%d", $time mul_out, tb_mul_out); err_mul_cnt = err_mul_cnt + 1; end else begin $display("[MUL_PASS] Time=%0t | DUT=%d, TB=%d", $time, mul_out, tb_mul_out); end if (acc_sum_out !== tb_acc_sum_out) begin $display("[ACC_ERROR] Time=%0t | DUT=%d, TB=%d", $time, acc_sum_out, tb_acc_sum_out); err_acc_cnt = err_acc_cnt + 1; end else begin $display("[ACC_PASS] Time=%0t | DUT=%d, TB=%d", $time, acc_sum_out, tb_acc_sum_out); end end end // ================================================================ // 4) Instance를 통한 port 연결 pe_mac #( // TB 파일에서 설정한 값을 DUT에도 적용하기 위함 .data_width(data_width), .mul_width(mul_width), .acc_width(acc_width) ) dut ( // dut 이름으로 instance .clk(clk), .rst(rst), .clear(clear), .en(en), .a_in(a_in), .b_in(b_in), .mul_out(mul_out), .acc_sum_out(acc_sum_out) ); // ================================================================ // 5) 입력 생성 always @ (posedge clk or negedge rst) begin if (!rst) begin a_in <= 0; b_in <= 0; end else begin a_in <= $urandom_range(0, (1<<data_width)-1); // 1을 왼쪽으로 8칸 shift = 256(1_0000_0000) b_in <= $urandom_range(0, (1<<data_width)-1); // 1을 왼쪽으로 8칸 shift = 256(1_0000_0000) en <= $urandom_range(0, 1); // 0/1 중 랜덤, 1일 때 mul 값을 누적 if ($urandom_range(0, 99) < 5) begin // 5% 확률로 clear 신호 켜서 acc 값 초기화 clear <= 1; end else begin clear <= 0; end end end endmodule | cs |
Result
"#30"을 통해 30ns 이후부터 "rst=1"이 되어 연산을 시작
DUT의 mul 값인 "mul_out"과 TB의 mul 값인 "tb_mul_out"의 값이 같아 Console 창에서 "[MUL_Pass]"가 출력
DUT의 mul 값인 "mul_out"과 TB의 mul 값인 "tb_mul_out"의 값이 같아 Console 창에서 "[MUL_Pass]"가 출력
"en" 신호가 0이므로 곱셈 결과 값이 누적되지 않아 ACC값은 "0"으로 출력
"en" 신호가 1이 된 경우 해당 곱셈 값은 누적되는 것을 "acc_sum_in"과 "tb_acc_sum_in"을 통해 확인 가능
"#30" 이후 rst 값을 줬기 때문에 30ns에 "#2000"인 2030ns에 종료되며 최종 연산 횟수와 Error 개수를 출력
"clear" 신호가 1이 되어 누적 값인 "acc_sum_in"과 "tb_acc_sum_in"이 "0"으로 초기화 되는 것을 확인 가능
"#30" 이후 rst 값을 줬기 때문에 30ns에 "#2000"인 2030ns에 종료되며 최종 연산 횟수와 Error 개수를 출력
Reference
▶ Design 1D PE Chain
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-1d-pe-chain.html
▶ Design 2D Systolic Array
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-2d-systolic-array.html
▶ Design Systolic Array + Controller
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-systolic-array.html
▶ Design Systolic Array + Controller + Activation(ReLU)
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-systolic-array_27.html
댓글
댓글 쓰기