[Mini-NPU RTL] Design Systolic Array + Controller
"Systolic Array + Controller"
pe_mac.v
Design Processing Element의 pe_mac.v와 동일
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 |
pe_systolic_cell.v
Design 2D Systolic Array의 pe_systolic_cell.v와 동일
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 | `timescale 1ns / 1ps module pe_systolic_cell # ( 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, input en, input [data_width-1:0] a_in, input [data_width-1:0] b_in, output [data_width-1:0] a_out, // 전달되는 값 output [data_width-1:0] b_out, // 전달되는 값 output [mul_width-1:0] mul_out, output reg [acc_width-1:0] acc_sum_out ); // a, b 값을 한 번 register에 저장 후 옆/아래로 전달 reg [data_width-1:0] a_reg, b_reg; assign a_out = a_reg; assign b_out = b_reg; always @ (posedge clk or negedge rst) begin if (!rst) begin // reset 신호 a_reg <= 0; // reg값 초기화 b_reg <= 0; // reg값 초기화 end else if (en) begin // en tlsgh a_reg <= a_in; // reg값 update b_reg <= b_in; // reg값 update end end // 내부 MAC 연산 동작은 pe_mac Instance를 통해 수행 pe_mac # ( // 이 파일에서 설정한 값을 pe_mac 파일에도 적용하기 위함 .data_width(data_width), .mul_width(mul_width), .acc_width(acc_width) ) unit_pe_mac ( // unit_pe_mac 이름으로 Instance .clk(clk), .rst(rst), .clear(clear), .en(en), .a_in(a_reg), // a_reg * b_reg 누산 .b_in(b_reg), // a_reg * b_reg 누산 .mul_out(mul_out), .acc_sum_out(acc_sum_out) ); endmodule | cs |
systolic_array_2d.v
Design 2D Systolic Array의 systolic_array_2d.v와 동일
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 | `timescale 1ns / 1ps module systolic_array_2d # ( 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) parameter rows = 2, // 행 개수 parameter cols = 2 // 열 개수 ) ( input clk, input rst, input clear, input en, input [data_width-1:0] a_in_row [0:rows-1], // 왼쪽에서 들어오는 A 행렬 stream (각 행마다 입력 하나) input [data_width-1:0] b_in_col [0:cols-1], // 위쪽에서 들어오는 B 행렬 stream (각 열마다 입력 하나) output [mul_width-1:0] pe_mul_out [0:rows-1][0:cols-1], // 각 PE의 곱셈 결과 output [acc_width-1:0] pe_acc_sum_out [0:rows-1][0:cols-1] // 각 PE의 누적 결과 ); wire [data_width-1:0] a_temp [0:rows-1][0:cols]; // 2개의 열을 외부 입력 - 연결 - 외부 출력 연결 wire [data_width-1:0] b_temp [0:rows][0:cols-1]; // 2개의 행을 외부 입력 - 연결 - 외부 출력 연결 genvar r, c; // 반복문에 사용할 변수 선언 generate for (r = 0; r < rows; r++) begin // 행 반복 assign a_temp[r][0] = a_in_row[r]; // 각 행에 맞는 데이터를 0번째 열에 공급 end for (c = 0; c < cols; c++) begin // 열 반복 assign b_temp[0][c] = b_in_col[c]; // 각 열에 맞는 데이터를 0번째 행에 공급 end endgenerate generate for (r = 0; r < rows; r++) begin // 행 반복 for (c = 0; c < cols; c++) begin // 열 반복 pe_systolic_cell # ( // 이 파일에서 설정한 값을 pe_systolic_cell 파일에도 적용하기 위함 .data_width(data_width), .mul_width(mul_width), .acc_width(acc_width) ) unit_cell ( // unit_cell 이름으로 Instance .clk(clk), .rst(rst), .clear(clear), .en(en), .a_in(a_temp[r][c]), // 현재 위치에서 입력을 받음 .a_out(a_temp[r][c+1]), // 다음 위치로 출력을 보냄 .b_in(b_temp[r][c]), // 현재 위치에서 입력을 받음 .b_out(b_temp[r+1][c]), // 다음 위치로 출력을 보냄 .mul_out(pe_mul_out[r][c]), // 곱셈 결과 .acc_sum_out(pe_acc_sum_out[r][c]) // 누적 결과 ); end end endgenerate endmodule | cs |
systolic_controller.v
FSM을 통해 데이터 공급 타이밍을 조절하여 사용자가 데이터만 입력하면 전체 동작을 제어하는 모듈
① IDLE 상태에서 start 신호가 들어오면 행렬 데이터를 Latch에 저장 & clear 신호를 보내 누적 값 초기화
② 다음 clk에 RUN 상태로 변환
③ RUN 상태에서는 en이 1이 되어 Systolic Array 동작(Latch에 저장된 값을 꺼내어 계산)
④ cnt가 calc_cycles에 도달하면 상태가 DONE으로 바뀌고, 이후 start 신호가 꺼지면 다시 IDLE 상태로 돌아가 다음 명령 대기
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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | `timescale 1ns / 1ps module systolic_controller # ( parameter k_dim = 2, // A, B의 공통 차원 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) parameter rows = 2, // 행 개수 parameter cols = 2 // 열 개수 ) ( input clk, input rst, input start_in, // 연산 시작 신호 input [data_width-1:0] A_in [0:rows-1][0:k_dim-1], // 왼쪽에서 들어오는 A 행렬 stream (각 행마다 입력 하나) input [data_width-1:0] B_in [0:k_dim-1][0:cols-1], // 위쪽에서 들어오는 B 행렬 stream (각 열마다 입력 하나) output [acc_width-1:0] C_out [0:rows-1][0:cols-1], // MAC 연산 결과 output done_out, // 연산 완료 신호 output busy_out // 연산 진행중 신호 ); //========================================================== // 1) 내부 상태와 Buffer 정의 typedef enum logic [1:0] { IDLE, // 대기 RUN, // 연산 진행중 DONE // 연산 완료 } state; state tmp_state, next_state; // 상태 변수 선언 reg [7:0] cnt; // 타이머 // Input Buffer : 입력 데이터를 저장해두는 공간 reg [data_width-1:0] latched_A [0:rows-1][0:k_dim-1]; reg [data_width-1:0] latched_B [0:k_dim-1][0:cols-1]; // Array Interface : 실제 Systolic Array로 1 Cycle마다 들어갈 데이터 reg [data_width-1:0] a_in_row [0:rows-1]; // 왼쪽에서 들어오는 A 행렬 stream (각 행마다 입력 하나) reg [data_width-1:0] b_in_col [0:cols-1]; // 위쪽에서 들어오는 B 행렬 stream (각 열마다 입력 하나) wire en; // 작동 신호 wire clear; // 초기화 신호 // 연산 완료까지 걸리는 시간 계산 localparam calc_cycles = rows + cols + k_dim + 2; //========================================================== // 2) Systolic Array Instance systolic_array_2d # ( // 이 파일에서 설정한 값을 pe_systolic_cell 파일에도 적용하기 위함 .data_width(data_width), .mul_width(mul_width), .acc_width(acc_width), .rows(rows), .cols(cols) ) unit_core_array ( // unit_core_array 이름으로 Instance .clk(clk), .rst(rst), .clear(clear), .en(en), .a_in_row(a_in_row), .b_in_col(b_in_col), .pe_mul_out(), // Unused .pe_acc_sum_out(C_out) // Final Result ); //========================================================== // 3) FSM & Counter always @ (posedge clk or negedge rst) begin if (!rst) begin tmp_state <= IDLE; // 대기 상태로 설정 cnt <= 0; // Counter 초기화 end else begin tmp_state <= next_state; // 다음 상태로 변경 if (tmp_state == RUN) cnt <= cnt + 1; // RUN 상태라면 카운터 증가 else cnt <= 0; // RUN 상태가 아닌 경우 카운터 초기화 end end always @ (*) begin next_state = tmp_state; // 현재 상태를 다음 상태에 저장 case (tmp_state) IDLE: begin // IDLE 상태 if (start_in) next_state = RUN; // start 신호가 오면 다음 상태 = RUN end RUN: begin // RUN 상태 if (cnt >= calc_cycles) next_state = DONE; // 시간이 다 되면 다음 상태 = DONE end DONE: begin // DONE 상태 if (!start_in) next_state = IDLE; // start 신호가 꺼지면 다음 상태 = IDLE end endcase end // Output assign busy_out = (tmp_state == RUN); assign done_out = (tmp_state == DONE); // Array Control Signals assign en = (tmp_state == RUN); // IDLE 상태에서 start 신호가 들어오는 순간 clear 수행 (Accumulator 초기화) → 새로운 연산 시작 assign clear = (tmp_state == IDLE) && start_in; //========================================================== // 4) Input Buffer Latching (연산 도중 입력값이 바뀌어도 내부 연산이 꼬이지 않도록 저장) always @ (posedge clk) begin if (tmp_state == IDLE && start_in) begin // 대기 상태에서 start 신호가 들어온 경우 latched_A <= A_in; // 현재 A_in 값을 저장하여 연산에 사용 latched_B <= B_in; // 현재 B_in 값을 저장하여 연산에 사용 end end //========================================================== // 5) Data Skewing Logic genvar r, c; generate // Row Input Control (Matrix A → Row Input) for (r = 0; r < rows; r++) begin always @ (*) begin a_in_row[r] = 0; // 기본값 0 if (tmp_state == RUN) begin // RUN 상태인 경우 int k; k = cnt - r; // 현재 보내야 할 데이터의 Index if (k >= 0 && k < k_dim) begin // k가 유효한 범위 내에 존재하는 경우에만 데이터 주입 a_in_row[r] = latched_A[r][k]; end end end end // Column Input Control (Matrix B → Col Input) for (c = 0; c < cols; c++) begin always @ (*) begin b_in_col[c] = 0; // 기본값 0 if (tmp_state == RUN) begin // RUN 상태인 경우 int k; k = cnt - c; // 현재 보내야 할 데이터의 Index if (k >= 0 && k < k_dim) begin // k가 유효한 범위 내에 존재하는 경우에만 데이터 주입 b_in_col[c] = latched_B[k][c]; end end end end endgenerate endmodule | cs |
tb_systolic_controller.v
SW로 계산한 결과와 HW로 계산한 결과를 비교
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 | `timescale 1ns / 1ps module tb_systolic_controller; parameter k_dim = 2; // A, B의 공통 차원 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) parameter rows = 2; // 행 개수 parameter cols = 2; // 열 개수 reg clk; reg rst; reg start_in; wire busy_out; wire done_out; reg [data_width-1:0] tb_A_in [0:rows-1][0:k_dim-1]; reg [data_width-1:0] tb_B_in [0:k_dim-1][0:cols-1]; wire [acc_width-1:0] C_out [0:rows-1][0:cols-1]; int tb_C_out [0:rows-1][0:cols-1]; int err_cnt; systolic_controller # ( // 이 파일에서 설정한 값을 pe_systolic_cell 파일에도 적용하기 위함 .data_width(data_width), .k_dim(k_dim), .rows(rows), .cols(cols) ) dut ( // dut 이름으로 Instance .clk(clk), .rst(rst), .start_in(start_in), .done_out(done_out), .busy_out(busy_out), .A_in(tb_A_in), .B_in(tb_B_in), .C_out(C_out) ); initial begin clk <= 0; forever #5 clk = ~clk; // 주기 10ns end initial begin rst <= 0; // 초기화 start_in <= 0; // 초기화 tb_A_in <= '{default:0}; // 초기화 tb_B_in <= '{default:0}; // 초기화 err_cnt <= 0; // 초기화 #30 rst <= 1; // 동작 신호 $display("===== Controller-Based Verification Start ====="); for (int iter = 0; iter < 5; iter++) begin // Random Data Setup for (int r = 0; r < rows; r++) begin for (int k = 0; k < k_dim; k++) begin tb_A_in[r][k] = $urandom_range(0, (1<<data_width)-1); end end // Random Data Setup for (int c = 0; c < cols; c++) begin for (int k = 0; k < k_dim; k++) begin tb_B_in[k][c] = $urandom_range(0, (1<<data_width)-1); end end // SW로 행렬 곱 계산 for (int r = 0; r < rows; r++) begin for (int c = 0; c < cols; c++) begin tb_C_out[r][c] = 0; for (int k = 0; k < k_dim; k++) begin tb_C_out[r][c] += tb_A_in[r][k] * tb_B_in[k][c]; end end end // HW로 행렬 곱 계산 시작 신호 인가 @ (posedge clk); start_in <= 1; @ (posedge clk); start_in <= 0; // DONE 될 때까지 대기 wait(done_out == 1); // 결과 비교 $display("==============================================="); $display("[Iter %0d] Checking Result",iter+1); for (int r = 0; r < rows; r++) begin for (int c = 0; c < cols; c++) begin if (C_out[r][c] !== tb_C_out[r][c]) begin $display("[ERROR] [%0d][%0d] : DUT=%0d, TB=%0d", r, c, C_out[r][c], tb_C_out[r][c]); err_cnt++; end else begin $display("[PASS] [%0d][%0d] : DUT=%0d, TB=%0d", r, c, C_out[r][c], tb_C_out[r][c]); end end end if (err_cnt == 0) $display("[Iter %0d] FINAL RESULT : PASS!", iter+1); else $display("[Iter %0d] FINAL RESULT : FAIL! (# of Error=%0d)", iter+1, err_cnt); // 다음 명령 대기 @ (posedge clk); end $display("===== All Tests Finised ====="); $finish; end endmodule | cs |
Result
"start_in" 신호가 "1"이 된 후 1 Cycle 뒤에 "busy_out"이 "1"이 되는 것을 보아 RUN 상태에 들어간 것을 유추 가능
SW 결과인 "tb_C_out"과 HW 결과인 "C_out"의 결과가 같아 Console 창에 PASS가 출력되는 것을 확인 가능
계산이 완료되어 "done_out"이 "1"이 되고, 이후 "start_in"이 다시 "1"이 될 때까지 대기하다 "1"이 되고 나서 연산 시작
Thinking
Combinational Logic : 값을 순식간에 계산할 때에 사용
Sequential Logic : clk에 반응하여 값을 저장하고 기억해야 할 때 사용
Reference
▶ Design Processing Element
https://hecess.blogspot.com/2026/01/minit-npu-rtl-design-processing-element.html
▶ 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 + Activation(ReLU)
https://hecess.blogspot.com/2026/01/mini-npu-rtl-design-systolic-array_27.html
댓글
댓글 쓰기