[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

"#30 rst <= 1"에 의해 30ns에 rst가 1로 올라가는 것을 확인 가능
"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

댓글

이 블로그의 인기 게시물

[Mini-NPU RTL] NN Reference Model

[Mini-NPU RTL] TPU (Study Paper)