진행년월: 24.08

 

- 이전 글

(1) 2차원 Filter : https://chonh0531.tistory.com/20

 

Filter - (2) FPGA에서 CNN구현

진행년월: 24.08 목차1. 배경2. 과제 정의 및 개요3. 소스코드4. 시뮬레이션 결과  1. 배경CNN 모델 중 가장 기본적인 Lenet-5 모델을 구현해보려고 합니다.메모리 등을 고려해야 하기 때문에,성능 자

chonh0531.tistory.com

 

목차

1. 배경

2. 과제 정의 및 개요

3. 소스코드

4. 시뮬레이션 결과

 

 

1. 배경

CNN 모델 중 가장 기본적인 Lenet-5 모델을 구현해보려고 합니다.

메모리 등을 고려해야 하기 때문에,

성능 자체를 끌어올리기보다는 구현 자체에 초점을 맞춥니다.

 

 

2. 과제 정의 및 개요

 

 

구현할 모델은 위와 같습니다.

 

1. CNN Python코드 작성
2. Verilog 구현용 C코드 작성
3. Verilog 코드 구현
4. 이미지 예측 정확도 평가

 

3. 소스코드

- Python CNN

import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
import struct

# **1. LeNet-5 모델 정의**
def build_lenet5():
    model = models.Sequential([
        layers.Conv2D(6, (5, 5), activation='relu', input_shape=(28, 28, 1)),
        layers.AveragePooling2D((2, 2)),
        layers.Conv2D(16, (5, 5), activation='relu'),
        layers.AveragePooling2D((2, 2)),
        layers.Flatten(),
        layers.Dense(120, activation='relu'),
        layers.Dense(84, activation='relu'),
        layers.Dense(10, activation='softmax')
    ])
    return model

# **2. 데이터셋 로드 및 전처리**
def load_data():
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    x_train = x_train / 255.0  # Normalize to [0, 1]
    x_test = x_test / 255.0
    x_train = np.expand_dims(x_train, axis=-1)  # Add channel dimension
    x_test = np.expand_dims(x_test, axis=-1)
    return (x_train, y_train), (x_test, y_test)

# **3. 양자화 함수: 32비트를 8/11비트로 변환**
def quantize(value, bits=8, frac_bits=4):
    scale = 2 ** frac_bits
    quantized = np.round(value * scale)
    quantized = np.clip(quantized, -(2 ** (bits - 1)), 2 ** (bits - 1) - 1)
    return quantized.astype(np.int16)

# **4. 학습과 가중치 저장**
def train_and_save_weights(model, x_train, y_train, x_test, y_test, quant_bits=8, frac_bits=4):
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    model.fit(x_train, y_train, epochs=5, validation_data=(x_test, y_test))
    test_loss, test_acc = model.evaluate(x_test, y_test)
    print(f"Test Accuracy (Python): {test_acc:.4f}")

    # Save weights for FPGA implementation
    for layer in model.layers:
        if isinstance(layer, (layers.Conv2D, layers.Dense)):
            layer_weights, layer_biases = layer.get_weights()

            # 양자화된 가중치 저장
            quantized_weights = quantize(layer_weights, quant_bits, frac_bits)
            quantized_biases = quantize(layer_biases, quant_bits, frac_bits)

            # 파일 저장
            np.savetxt(f"{layer.name}_weights.txt", quantized_weights.flatten(), fmt='%d')
            np.savetxt(f"{layer.name}_biases.txt", quantized_biases, fmt='%d')

            print(f"Weights and biases for layer '{layer.name}' saved.")
    
    print("All layer weights and biases saved.")
    return test_acc

# **5. 테스트 데이터 저장 (Verilog 테스트벤치용)**
def save_test_data(x_test, y_test):
    (x_test, y_test) = (x_test[:1000], y_test[:1000])  # 제한된 샘플 수로 간단히 처리
    x_test_flat = x_test.reshape(x_test.shape[0], -1)  # Flatten 28x28 images to 784

    # Save test data
    np.savetxt('test_images.txt', x_test_flat, fmt='%d')  # Save as integer values (0-255)
    np.savetxt('test_labels.txt', y_test, fmt='%d')      # Save labels

    print("Test images and labels saved.")

# **Main Script**
if __name__ == "__main__":
    # Load data
    (x_train, y_train), (x_test, y_test) = load_data()
    
    # Build model
    model = build_lenet5()
    
    # Train model and save weights
    python_accuracy = train_and_save_weights(model, x_train, y_train, x_test, y_test)
    
    # Save test data
    save_test_data(x_test, y_test)

 

python으로 모델을 구성한 뒤, 학습을 진행합니다.

테스트 데이터는 MNIST 데이터를 사용하였습니다.

학습된 모델의 Weight와 Bias를 Vivado에서 Block 메모리 초기화 파일로 사용하기 위해

COE 파일로 저장합니다.

 

 

- Verilog CNN 이전 Convolution test

module cnn (
    input clk,
    input reset,
    input [7:0] t_rdata,
    output reg [9:0] t_addr,
    output reg [11:0] output_address, // Output memory address
    output reg signed [10:0] output_data, // Output write data
    output reg output_write_enable // Output write enable
);

// RAM Signals
reg [9:0] image_address;
reg [7:0] image_write_data;
wire [7:0] image_read_data;
reg image_write_enable;
wire signed [10:0] output_read_data;

// ROM Signals
reg [7:0] weight_address;
wire signed [7:0] weight_read_data;
reg [2:0] bias_address;
wire signed [7:0] bias_read_data;

// Loop Index Registers
reg [4:0] row, next_row, col, next_col;
reg [2:0] filter, next_filter, kernel_row, next_kernel_row, kernel_col, next_kernel_col;

// Additional Registers
reg [9:0] current_image_address, next_image_address;
(* use_dsp = "yes" *) reg signed [18:0] conv_sum, next_conv_sum;
reg [7:0] current_weight_address, next_weight_address;
reg [11:0] current_output_address, next_output_address;
reg signed [10:0] temp_output, next_temp_output;

// State Definitions
localparam s0 = 0, s1 = 1, s2 = 2, s3 = 3, s4 = 4, s5 = 5, s6 = 6;
reg [3:0] state, next_state;

// File Descriptor
integer fd;

// RAM Instantiation
i_ram image_ram (
    .clka(clk),
    .wea(image_write_enable),
    .addra(image_address),
    .dina(image_write_data),
    .douta(image_read_data)
);

a_ram output_ram (
    .clka(clk),
    .wea(output_write_enable),
    .addra(output_address),
    .dina(output_data),
    .douta(output_read_data)
);

// ROM Instantiation
w0_rom weight_rom (
    .clka(clk),
    .addra(weight_address),
    .douta(weight_read_data)
);

b0_rom bias_rom (
    .clka(clk),
    .addra(bias_address),
    .douta(bias_read_data)
);

// Finite State Machine (FSM)
always @ (reset, state, t_rdata, image_read_data, output_read_data, weight_read_data, bias_read_data, row, col, filter, kernel_row, kernel_col, current_image_address, conv_sum, current_weight_address, current_output_address, temp_output) begin
    // Default memory settings
    t_addr = 0;
    image_address = 0;
    image_write_data = 0;
    image_write_enable = 0;
    output_address = 0;
    output_data = 0;
    output_write_enable = 0;
    weight_address = 0;
    bias_address = 0;

    // Default register settings
    next_state = state;
    next_row = row;
    next_col = col;
    next_filter = filter;
    next_kernel_row = kernel_row;
    next_kernel_col = kernel_col;
    next_image_address = current_image_address;
    next_conv_sum = conv_sum;
    next_weight_address = current_weight_address;
    next_output_address = current_output_address;
    next_temp_output = temp_output;

    case (state)
        s0: begin
            next_state = s1;
            next_image_address = 0;
            t_addr = 0;
            next_row = 0;
            next_col = 0;
        end

        s1: begin
            image_write_data = t_rdata;
            image_address = current_image_address;
            image_write_enable = 1'b1;
            next_state = s1;
            next_col = col + 1;
            if (next_col == 28) begin
                next_col = 0;
                next_row = row + 1;
                if (next_row == 28) begin
                    next_row = 0;
                    next_state = s2;
                end
            end
            next_image_address = next_row * 28 + next_col;
            t_addr = next_image_address;
        end

        s2: begin
            next_filter = 0;
            next_row = 0;
            next_col = 0;
            next_conv_sum = 0;
            next_kernel_row = 0;
            next_kernel_col = 0;
            image_address = 0;
            weight_address = 0;
            next_state = s3;
        end

        s3: begin
            next_conv_sum = conv_sum + $signed({1'b0, image_read_data}) * weight_read_data;
            next_state = s3;
            next_kernel_col = kernel_col + 1;
            if (next_kernel_col == 5) begin
                next_kernel_col = 0;
                next_kernel_row = kernel_row + 1;
                if (next_kernel_row == 5) begin
                    next_kernel_row = 0;
                    next_state = s4;
                    bias_address = filter;
                end
            end
            next_image_address = (row + next_kernel_row) * 28 + col + next_kernel_col;
            next_weight_address = filter * 25 + next_kernel_row * 5 + next_kernel_col;
            image_address = next_image_address;
            weight_address = next_weight_address;
        end

        s4: begin
            next_temp_output = conv_sum[18:8];
            if (next_temp_output[10] == 1'b1) begin
                next_temp_output = next_temp_output + (|conv_sum[7:0]); // Round towards zero (RTZ)
            end
            next_temp_output = next_temp_output + bias_read_data;
            if (next_temp_output[10] == 1'b1) begin
                next_temp_output = 0;
            end
            next_output_address = filter * 576 + row * 24 + col;
            output_address = next_output_address;
            output_data = next_temp_output;
            output_write_enable = 1'b1;
            next_state = s3;
            next_col = col + 1;
            if (next_col == 24) begin
                next_col = 0;
                next_row = row + 1;
                if (next_row == 24) begin
                    next_row = 0;
                    next_filter = filter + 1;
                    if (next_filter == 6) begin
                        next_filter = 0;
                        next_state = s5;
                    end
                end
            end
            next_conv_sum = 0;
            next_image_address = next_row * 28 + next_col;
            next_weight_address = next_filter * 25;
            image_address = next_image_address;
            weight_address = next_weight_address;
        end

        s5: begin
            next_state = s5;
        end

        default: begin
            next_state = s0;
        end
    endcase

    if (reset == 1) begin
        next_state = s0;
        next_row = 0;
        next_col = 0;
        next_filter = 0;
        next_kernel_row = 0;
        next_kernel_col = 0;
        next_image_address = 0;
        next_conv_sum = 0;
        next_weight_address = 0;
        next_output_address = 0;
        next_temp_output = 0;
    end
end

// State update on clock edge
always @ (posedge clk) begin
    state <= next_state;
    row <= next_row;
    col <= next_col;
    filter <= next_filter;
    kernel_row <= next_kernel_row;
    kernel_col <= next_kernel_col;
    current_image_address <= next_image_address;
    conv_sum <= next_conv_sum;
    current_weight_address <= next_weight_address;
    current_output_address <= next_output_address;
    temp_output <= next_temp_output;
end

// Synthesis directives and file writing
// synthesis translate_off
always @ (posedge clk) begin
    fd = $fopen("test.txt", "a");
    if (state == s4 && output_write_enable == 1'b1) begin
        $fwrite(fd, "A[%0d][%0d][%0d] = %0d\n", filter, row, col, output_data);
    end
    if (reset == 1) begin
        fd = $fopen("test.txt", "w");
    end
    $fclose(fd);
end
// synthesis translate_on

endmodule

 

module tb;
    reg clk;
    reg reset;
    wire [9:0] t_addr;
    wire [7:0] t_rdata;
    wire [11:0] output_address;
    wire [10:0] output_data;
    wire output_write_enable;

    //
    // ROM instantiation
    //
    t_rom t_test (
        .clka(clk),
        .addra(t_addr),
        .douta(t_rdata)
    );

    //
    // CNN instantiation
    //
    cnn uut (
        .clk(clk),
        .reset(reset),
        .t_addr(t_addr),
        .t_rdata(t_rdata),
        .output_address(output_address),
        .output_data(output_data),
        .output_write_enable(output_write_enable)
    );

    // 100 MHz clock
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    // reset
    initial begin
        reset = 0;
        #100 reset = 1;
        #100 reset = 0;
    end
endmodule

 

Verilog로 구현하기 전에 가장 중요한 Convolution 층을 먼저 구현하여 테스트 하였습니다.

 

 

- Verilog CNN

module lenet5_fpga (
    input clk,
    input reset,
    input [7:0] input_data,        // 8-bit quantized input pixel data
    output reg [3:0] output_class, // Predicted class (0-9)
    output reg done                // Processing done signal
);

    // Parameters
    parameter IMG_SIZE = 28;
    parameter NUM_CLASSES = 10;

    // State Machine States
    localparam IDLE = 0, LOAD_IMAGE = 1, 
               CONV1 = 2, POOL1 = 3, 
               CONV2 = 4, POOL2 = 5,
               FLATTEN = 6, FC1 = 7, FC2 = 8, FC3 = 9, DONE = 10;
    reg [3:0] state;

    // Layer Buffers
    reg [7:0] image [0:IMG_SIZE-1][0:IMG_SIZE-1]; // 8-bit Image Data
    reg signed [10:0] conv1_out [0:23][0:23][0:5];  // 11-bit Conv1 output
    reg signed [10:0] pool1_out [0:11][0:11][0:5];  // 11-bit Pool1 output
    reg signed [10:0] conv2_out [0:7][0:7][0:15];   // 11-bit Conv2 output
    reg signed [10:0] pool2_out [0:3][0:3][0:15];   // 11-bit Pool2 output
    reg signed [10:0] fc1_out [0:119];              // FC1 output
    reg signed [10:0] fc2_out [0:83];               // FC2 output
    reg signed [10:0] fc3_out [0:NUM_CLASSES-1];    // FC3 output

    // Flatten Buffer
    reg signed [10:0] pool2_flat [0:399]; // Flattened Pool2 Output

    // Temporary Registers
    reg signed [18:0] conv_sum; // For convolution accumulation
    reg signed [18:0] fc_sum;   // For FC accumulation
    reg signed [10:0] max_val;
    reg [3:0] max_index;
    integer i, j, k, kr, kc, c, row, col;

    // Block Memory Generators for weights and biases
    wire [7:0] conv1_weight_data;
    wire [7:0] conv2_weight_data;
    wire [7:0] fc1_weight_data;
    wire [7:0] fc2_weight_data;
    wire [7:0] fc3_weight_data;
    wire [7:0] bias_data;

    reg [15:0] conv1_weight_addr;
    reg [15:0] conv2_weight_addr;
    reg [15:0] fc1_weight_addr;
    reg [15:0] fc2_weight_addr;
    reg [15:0] fc3_weight_addr;
    reg [15:0] bias_addr;

    // Instantiate Block RAMs for weights and biases
    blk_mem_gen_conv1_weights conv1_weights_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(conv1_weight_addr),
        .dina(8'b0),
        .douta(conv1_weight_data)
    );

    blk_mem_gen_conv2_weights conv2_weights_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(conv2_weight_addr),
        .dina(8'b0),
        .douta(conv2_weight_data)
    );

    blk_mem_gen_fc1_weights fc1_weights_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(fc1_weight_addr),
        .dina(8'b0),
        .douta(fc1_weight_data)
    );

    blk_mem_gen_fc2_weights fc2_weights_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(fc2_weight_addr),
        .dina(8'b0),
        .douta(fc2_weight_data)
    );

    blk_mem_gen_fc3_weights fc3_weights_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(fc3_weight_addr),
        .dina(8'b0),
        .douta(fc3_weight_data)
    );

    blk_mem_gen_bias bias_bram (
        .clka(clk),
        .wea(1'b0),
        .addra(bias_addr),
        .dina(8'b0),
        .douta(bias_data)
    );

    // FSM Logic
    always @(posedge clk or posedge reset) begin
        if (reset) begin
            state <= IDLE;
            done <= 0;
        end else begin
            case (state)
                IDLE: begin
                    done <= 0;
                    state <= LOAD_IMAGE;
                end

                LOAD_IMAGE: begin
                    row = 0;
                    col = 0;
                    
                    // Sequentially load the image into the buffer
                    if (row < IMG_SIZE) begin
                        image[row][col] <= input_data;
                        col = col + 1;
                        if (col == IMG_SIZE) begin
                            col = 0;
                            row = row + 1;
                        end
                    end
        
                    // Move to the next state after loading the image
                    if (row == IMG_SIZE && col == 0) begin
                        state <= CONV1;
                    end
                end

                CONV1: begin
                    // Convolution Layer 1
                    for (i = 0; i < 24; i = i + 1) begin
                        for (j = 0; j < 24; j = j + 1) begin
                            for (k = 0; k < 6; k = k + 1) begin
                                conv1_weight_addr = k * 25; // Example address calculation
                                conv_sum = 0;
                                for (kr = 0; kr < 5; kr = kr + 1) begin
                                    for (kc = 0; kc < 5; kc = kc + 1) begin
                                        conv_sum = conv_sum + 
                                                   image[i+kr][j+kc] * conv1_weight_data;
                                    end
                                end
                                conv1_out[i][j][k] <= (conv_sum + bias_data) >>> 8;
                            end
                        end
                    end
                    state <= POOL1;
                end

                POOL1: begin
                    for (i = 0; i < 12; i = i + 1) begin
                        for (j = 0; j < 12; j = j + 1) begin
                            for (k = 0; k < 6; k = k + 1) begin
                                pool1_out[i][j][k] <= 
                                    (conv1_out[2*i][2*j][k] + conv1_out[2*i+1][2*j][k] +
                                     conv1_out[2*i][2*j+1][k] + conv1_out[2*i+1][2*j+1][k]) >> 2;
                            end
                        end
                    end
                    state <= CONV2;
                end

                CONV2: begin
                    for (i = 0; i < 8; i = i + 1) begin
                        for (j = 0; j < 8; j = j + 1) begin
                            for (k = 0; k < 16; k = k + 1) begin
                                conv_sum = 0;
                                for (c = 0; c < 6; c = c + 1) begin
                                    for (kr = 0; kr < 5; kr = kr + 1) begin
                                        for (kc = 0; kc < 5; kc = kc + 1) begin
                                            conv_sum = conv_sum + 
                                                       (pool1_out[i+kr][j+kc][c] * conv2_weight_data);
                                        end
                                    end
                                end
                                conv2_out[i][j][k] <= (conv_sum + bias_data) >>> 8;
                            end
                        end
                    end
                    state <= POOL2;
                end

                POOL2: begin
                    for (i = 0; i < 4; i = i + 1) begin
                        for (j = 0; j < 4; j = j + 1) begin
                            for (k = 0; k < 16; k = k + 1) begin
                                pool2_out[i][j][k] <= 
                                    (conv2_out[2*i][2*j][k] + conv2_out[2*i+1][2*j][k] +
                                     conv2_out[2*i][2*j+1][k] + conv2_out[2*i+1][2*j+1][k]) >> 2;
                            end
                        end
                    end
                    state <= FLATTEN;
                end

                FLATTEN: begin
                    for (i = 0; i < 400; i = i + 1) begin
                        pool2_flat[i] <= pool2_out[i/100][(i%100)/25][i%25];
                    end
                    state <= FC1;
                end

                FC1: begin
                    for (i = 0; i < 120; i = i + 1) begin
                        fc_sum = 0;
                        for (j = 0; j < 400; j = j + 1) begin
                            fc1_weight_addr = i * 400 + j;
                            fc_sum = fc_sum + pool2_flat[j] * fc1_weight_data;
                        end
                        fc1_out[i] <= (fc_sum + bias_data) >>> 8;
                    end
                    state <= FC2;
                end

                FC2: begin
                    for (i = 0; i < 84; i = i + 1) begin
                        fc_sum = 0;
                        for (j = 0; j < 120; j = j + 1) begin
                            fc2_weight_addr = i * 120 + j;
                            fc_sum = fc_sum + fc1_out[j] * fc2_weight_data;
                        end
                        fc2_out[i] <= (fc_sum + bias_data) >>> 8;
                    end
                    state <= FC3;
                end

                FC3: begin
                    max_val = -32768;
                    for (i = 0; i < NUM_CLASSES; i = i + 1) begin
                        fc_sum = 0;
                        for (j = 0; j < 84; j = j + 1) begin
                            fc3_weight_addr = i * 84 + j;
                            fc_sum = fc_sum + fc2_out[j] * fc3_weight_data;
                        end
                        fc3_out[i] <= (fc_sum + bias_data) >>> 8;
                        if (fc3_out[i] > max_val) begin
                            max_val = fc3_out[i];
                            max_index = i;
                        end
                    end
                    output_class <= max_index;
                    state <= DONE;
                end

                DONE: begin
                    done <= 1;
                end
            endcase
        end
    end
endmodule

 

이제 모든 층을 구현하여 코드를 구성합니다.

장비의 Memory가 충분치 않아 성능의 타협을 보고

32비트 floating point를 8비트 fixed point로 구현하였습니다.

 

 

- Verilog CNN Testbench

 

`timescale 1ns / 1ps

module tb_lenet5_fpga;

    // Signals
    reg clk;
    reg reset;
    reg [7:0] input_data;
    wire [3:0] output_class;
    wire done;

    // Parameters
    parameter CLK_PERIOD = 10;        // Clock period
    parameter NUM_IMAGES = 1000;     // Number of images to process
    parameter IMG_SIZE = 28;         // Image size (28x28)

    // File I/O
    integer input_file, output_file;
    integer i, j, k;
    reg [7:0] image_buffer [0:IMG_SIZE*IMG_SIZE-1]; // Buffer to hold one image

    // Instantiate DUT (Device Under Test)
    lenet5_fpga dut (
        .clk(clk),
        .reset(reset),
        .input_data(input_data),
        .output_class(output_class),
        .done(done)
    );

    // Clock generation
    always #(CLK_PERIOD / 2) clk = ~clk;

    // Testbench Logic
    initial begin
        // Initialize
        clk = 0;
        reset = 1;
        input_data = 0;

        // Open input image file
        input_file = $fopen("test_image.txt", "r");
        if (input_file == 0) begin
            $display("Error: Input image file not found!");
            $finish;
        end

        // Open output file
        output_file = $fopen("verilog_predietions.txt", "w");
        if (output_file == 0) begin
            $display("Error: Could not open output file!");
            $finish;
        end

        // Deassert reset after a few cycles
        #20 reset = 0;

        // Process NUM_IMAGES images
        for (k = 0; k < NUM_IMAGES; k = k + 1) begin
            $display("Processing image %d/%d...", k + 1, NUM_IMAGES);

            // Load one image into the buffer
            for (i = 0; i < IMG_SIZE * IMG_SIZE; i = i + 1) begin
                if (!$feof(input_file)) begin
                    $fscanf(input_file, "%d ", image_buffer[i]);
                end else begin
                    $display("Error: Unexpected end of input file!");
                    $finish;
                end
            end

            // Feed image buffer to DUT
            for (i = 0; i < IMG_SIZE; i = i + 1) begin
                for (j = 0; j < IMG_SIZE; j = j + 1) begin
                    input_data = image_buffer[i * IMG_SIZE + j];
                    #CLK_PERIOD;
                end
            end

            // Wait for done signal
            wait (done);

            // Write output class to file
            $fwrite(output_file, "%d\n", output_class);
        end

        $display("All images processed. Results saved to verilog_predictions.txt.");

        // Close files and finish simulation
        $fclose(input_file);
        $fclose(output_file);
        $finish;
    end
endmodule

 

이제 미리 준비한 test 데이터를 input으로 해서,

모델로 예측한 결과값들을 txt 파일로 저장합니다.

 

4. 시뮬레이션 결과

- Python 정확도

 

Python에서 확인한 정확도는 99%로,

실제 다른 경우에서는 이정도 정확도를 가지기 어렵지만

학습용 데이터를 사용하였기 때문에 가능합니다.

 

 

- FPGA 정확도

 

FPGA에서 나온 예측값들의 정확도는 96%로,

앞서 8bit fixed point로 구현하면서 타협봤었죠.

모델의 성능 차이는 3% 내외로 준수한 성능을 보입니다.

 

다른 경우에서는 Domain에 따라 어느 정도까지 타협을 볼지 정해야겠지만,

장비의 memory를 생각하며 먼저 python으로 성능을 테스트 한 뒤에

FPGA에 그 모델을 구현해서 칩에 AI를 구성할 수 있습니다.

- 이전 글

(1) RTL : https://chonh0531.tistory.com/5

 

UART 통신 - (1) RTL

목차1. 배경2. 과제 정의 및 개요3. 소스코드4. 시뮬레이션 결과  1. 배경지난 프로젝트에서 컴퓨터와 신호를 주고받기 위해 UART 통신을 사용하였는데요,UART 통신도 이해하고 Verilog로 구현해보고

chonh0531.tistory.com

 

목차

1. 과제 개요

2. 소스코드

3. 실습 결과

 

 

1. 과제 개요

이전에 구현해 본 UART_RX를 테스트해 보기로 합니다.

UART 신호 생성은 ARM Cortex-M4 Core가 있는 NUCLEO-F429ZI 보드를 이용하여

FPGA에서 LCD를 제어해 보도록 합니다.

 

1. 컴퓨터에서 UART 통신으로 Nucleo 보드에 명령을 전달
2. Nucleo 보드는 FPGA에 다시 UART 통신으로 명령을 전달
3. FPGA는 data 부분의 마지막 4비트를 모터 속도제어에 이용

 

 

 

2. 소스코드

- Top 모듈 Uart_lcd

`timescale 1ns / 1ps

module uart_lcd(
input clk,
input rst,
input rx,
output [7:0]data,
output lcd_e,
output lcd_rs
    );
 wire rx_clk;
     
 wire [7:0]rx_register;
 wire [4:0]clk_cnt;
 wire [3:0]rx_cnt;    
 wire [7:0]lcd_register;
 wire [6:0]lcd_cnt;
 
uart_clock #(.max(163)) C0 (.clk_in(clk),.rst(rst),.clk_out(rx_clk));    
    
uart_rx R0 (.clk(rx_clk),.rst(rst),.rx(rx),.rx_register(rx_register),.clk_cnt(clk_cnt),.rx_cnt(rx_cnt));

lcd L0 (.clk(clk),.rx_clk(rx_clk),.rst(rst),.rx_register(rx_register),.rx_cnt(rx_cnt),.clk_cnt(clk_cnt),.lcd_e(lcd_e),.lcd_rs(lcd_rs),.data(data),.lcd_cnt(lcd_cnt),.lcd_register(lcd_register));

ila_0 ila_0 (.clk(clk),.probe0(lcd_cnt),.probe1(rx),.probe2(data),.probe3(lcd_register),.probe4(rx_cnt));
    
endmodule

 

 

- UART_RX

module uart_rx(
input clk,
input rx,
input rst,
output reg [7:0]rx_register,
output reg rx_end,
output reg [4:0]clk_cnt,
output reg [3:0]rx_cnt
    );  

localparam RX_IDLE = 2'b00;
localparam RX_START = 2'b01;
localparam RX_STOP = 2'b10;

reg [1:0]current_state ;
reg [1:0]next_state;
    
reg [15:0]rx_check;
    
always @(posedge clk or posedge rst) begin
    if (rst)
        clk_cnt <= 0;   
    else if (clk_cnt == 16 && rx_cnt != 11)
        clk_cnt <= 1;
    else if (clk_cnt == 16 && rx_cnt == 11)
        clk_cnt <= 0;    
    else 
        case (next_state)
        RX_IDLE : clk_cnt <= 0;
        RX_START : clk_cnt <= clk_cnt+1;
        RX_STOP : clk_cnt <= clk_cnt+1;
        default : clk_cnt <= clk_cnt;
        endcase                
    end

always @(posedge clk or posedge rst) begin
    if (rst)
        rx_cnt <= 0;
    else if (rx_cnt == 11 && clk_cnt == 16)
        rx_cnt <= 0;
    else if (current_state == RX_IDLE && rx == 0)
        rx_cnt <= 1;
    else if (clk_cnt == 16)
        rx_cnt <= rx_cnt+1;
    else
        rx_cnt <= rx_cnt;
    end                            
    
always @(posedge clk or posedge rst) begin
    if (rst)
        current_state <= RX_IDLE;
    else
        current_state <= next_state;
    end   

always @(negedge clk or posedge rst) begin
    if (rst)
        next_state <= RX_IDLE;
    else
        case(current_state)
        RX_IDLE : if (rx == 0)
                    next_state <= RX_START;
               else       
                    next_state <= next_state;                    
        RX_START : if (rx_cnt == 11)
                    next_state <= RX_STOP;
                else 
                    next_state <= next_state;                              
        RX_STOP : if (rx_cnt == 0)
                    next_state <= RX_IDLE;      
               else
                    next_state <= next_state;
        default : next_state <= next_state;                           
        endcase
    end    
    
 reg [4:0]rx_score;
    
always @(posedge clk or posedge rst) begin
    if (rst)
        rx_check <= 16'b1111_1111_1111_1111;          
    else 
        rx_check <= {rx_check[15:0], rx};                   
    end

always @(posedge clk or posedge rst) begin
    if (rst)
        rx_score <= 0;
    else if (rx_cnt == 0)
        rx_score <= 0;    
    else if (clk_cnt ==16)
        rx_score <= 0;
    else if (rx_cnt == 11)
        rx_score <= 0;        
    else if (rx == 1)
        rx_score <= rx_score+1;       
    else 
        rx_score <= rx_score;
    end             

reg rx_sampling;

always @(posedge clk or posedge rst) begin
    if (rst)
        rx_sampling <= 1;
    else if (rx_cnt == 0)
        rx_sampling <= 1;    
    else if (clk_cnt == 16 && rx_score > 8 && rx_cnt != 11)        
        rx_sampling <= 1;
    else if (clk_cnt == 16 && rx_score < 8 && rx_cnt != 11)        
        rx_sampling <= 0;    
    else 
        rx_sampling <= rx_sampling;
    end

reg [9:0]rx_received;    
    
always @(posedge clk or posedge rst) begin
    if (rst)
        rx_received <= 10'b00_0000_0000;
    else if (clk_cnt == 1 && rx_cnt >1) 
        rx_received <= {rx_received[8:0], rx_sampling};
    else 
        rx_received <= rx_received;
    end    

always @(posedge clk or posedge rst) begin
    if (rst)
        rx_register <= 8'b0000_0000;
    else if (rx_cnt == 11 && clk_cnt == 2)
        rx_register <= rx_received[8:1];
    else 
        rx_register <= rx_register;
    end                                              

always @(posedge clk or posedge rst) begin
    if (rst)
        rx_end <= 1;
    else 
        case (next_state)
        RX_IDLE : rx_end <= 1;
        RX_START : rx_end <= 0;
        RX_STOP : rx_end <= 0;
        default : rx_end <= rx_end;
        endcase
    end    
       
endmodule

 

이전 포스팅에서 언급하였듯이,

코드를 최적화하지 않고 직관적으로 코딩한 후 그대로 두었습니다.

 

- UART_Clock

`timescale 1ns / 1ps

module uart_clock(
input clk_in,
input rst,
output reg clk_out
    );
    
reg [10:0]clk_cnt;

parameter max = 1;
    
always @(posedge clk_in or posedge rst) begin
    if (rst) begin
        clk_cnt <= 0;
        end
    else if (clk_cnt == max) begin        
        clk_cnt <= 0;
        end
    else 
        clk_cnt <= clk_cnt + 1;
    end

always @(posedge clk_in or posedge rst) begin
    if (rst) begin
        clk_out <= 0;
        end
    else if (clk_cnt == max) begin        
        clk_out <= !clk_out;
        end
    else 
        clk_out <= clk_out;
    end    
            
endmodule

 

 

- LCD

`timescale 1ns / 1ps

module lcd(
input clk,
input rx_clk,
input rst,
input [7:0]rx_register,
input [3:0]rx_cnt,
input [4:0]clk_cnt,
output reg lcd_e,
output reg lcd_rs,
output reg [7:0]data,
output reg [6:0]lcd_cnt,
output reg [7:0]lcd_register
    );
    
 reg [7:0] dspdata [0:37];
 integer i = 0;
 integer j = 0;
 
    always @(posedge rx_clk or posedge rst) begin
        if (rst)
            lcd_register <= 8'h80;
        else if (rx_cnt == 11 && clk_cnt == 16) begin
            lcd_register[0] <= rx_register[7];
            lcd_register[1] <= rx_register[6];
            lcd_register[2] <= rx_register[5];
            lcd_register[3] <= rx_register[4];
            lcd_register[4] <= rx_register[3];
            lcd_register[5] <= rx_register[2];
            lcd_register[6] <= rx_register[1];
            lcd_register[7] <= rx_register[0];
            end
        else
            lcd_register <= lcd_register;
        end            
    
    always @(posedge rx_clk or posedge rst) begin
        if (rst)
            lcd_cnt <= 4;
        else if (lcd_cnt == 20 && rx_cnt == 11 && clk_cnt == 16)
            lcd_cnt <= 22;
        else if (lcd_cnt == 37 && rx_cnt == 11 && clk_cnt == 16)
            lcd_cnt <= 5;       
        else if (rx_cnt== 11 && clk_cnt == 16)
            lcd_cnt <= lcd_cnt+1;             
        else
            lcd_cnt <= lcd_cnt;
        end                    
    
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            dspdata[0] <= 8'b00111000;    // function set 8bit, 2line, 5x7 dot
            dspdata[1] <= 8'b00001100;    // display on/off , display on, cursor off, cursor blink off 
            dspdata[2] <= 8'b00000110;    // entry mode set increment cursor position, no display shift
            dspdata[3] <= 8'b00000001;    // clear display
            dspdata[4] <= 8'h80;    // set cg ram address 1000 0000   1번라인 첫번째부터
            dspdata[5] <= 8'b00110000;    // 0
            dspdata[6] <= 8'b00110000;    // 0
            dspdata[7] <= 8'b00110000;    // 0
            dspdata[8] <= 8'b00110000;    // 0
            dspdata[9] <= 8'b00110000;    // 0
            dspdata[10] <= 8'b00110000;    // 0
            dspdata[11] <= 8'b00110000;    // 0
            dspdata[12] <= 8'b00110000;    // 0
            dspdata[13] <= 8'b00110000;    // 0
            dspdata[14] <= 8'b00110000;    // 0
            dspdata[15] <= 8'b00110000;    // 0
            dspdata[16] <= 8'b00110000;    // 0
            dspdata[17] <= 8'b00110000;    // 0
            dspdata[18] <= 8'b00110000;    // 0
            dspdata[19] <= 8'b00110000;    // 0
            dspdata[20] <= 8'b00110000;    // 0           
            dspdata[21] <= 8'hC0;   // set cg ram address 1100 0000   2번라인 첫번째부터
            dspdata[22] <= 8'b00110000;    // 0
            dspdata[23] <= 8'b00110000;    // 0
            dspdata[24] <= 8'b00110000;    // 0
            dspdata[25] <= 8'b00110000;    // 0
            dspdata[26] <= 8'b00110000;    // 0
            dspdata[27] <= 8'b00110000;    // 0
            dspdata[28] <= 8'b00110000;    // 0
            dspdata[29] <= 8'b00110000;    // 0
            dspdata[30] <= 8'b00110000;    // 0
            dspdata[31] <= 8'b00110000;    // 0
            dspdata[32] <= 8'b00110000;    // 0
            dspdata[33] <= 8'b00110000;    // 0
            dspdata[34] <= 8'b00110000;    // 0
            dspdata[35] <= 8'b00110000;    // 0
            dspdata[36] <= 8'b00110000;    // 0
            dspdata[37] <= 8'b00110000;    // 0
            end
        else if (rx_cnt == 0)
            dspdata[lcd_cnt] <= lcd_register;    
        else begin
            dspdata[0] <= dspdata[0];
            dspdata[1] <= dspdata[1];
            dspdata[2] <= dspdata[2];
            dspdata[3] <= dspdata[3];            
            dspdata[4] <= dspdata[4];
            dspdata[5] <= dspdata[5];
            dspdata[6] <= dspdata[6];
            dspdata[7] <= dspdata[7];
            dspdata[8] <= dspdata[8];
            dspdata[9] <= dspdata[9];
            dspdata[10] <= dspdata[10];
            dspdata[11] <= dspdata[11];
            dspdata[12] <= dspdata[12];
            dspdata[13] <= dspdata[13];
            dspdata[14] <= dspdata[14];
            dspdata[15] <= dspdata[15];
            dspdata[16] <= dspdata[16];
            dspdata[17] <= dspdata[17];
            dspdata[18] <= dspdata[18];
            dspdata[19] <= dspdata[19];
            dspdata[20] <= dspdata[20];            
            dspdata[21] <= dspdata[21];
            dspdata[22] <= dspdata[22];
            dspdata[23] <= dspdata[23];
            dspdata[24] <= dspdata[24];
            dspdata[25] <= dspdata[25];
            dspdata[26] <= dspdata[26];
            dspdata[27] <= dspdata[27];
            dspdata[28] <= dspdata[28];
            dspdata[29] <= dspdata[29];
            dspdata[30] <= dspdata[30];
            dspdata[31] <= dspdata[31];
            dspdata[32] <= dspdata[32];
            dspdata[33] <= dspdata[33];
            dspdata[34] <= dspdata[34];
            dspdata[35] <= dspdata[35];
            dspdata[36] <= dspdata[36];
            dspdata[37] <= dspdata[37];
            end
    end                                
    
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            i <= 0;            
            lcd_e <= 0;            
            data <= 8'b0;
            end
        else begin
            if(i <= 1000000) begin
                i <= i + 1;
                lcd_e <= 1;
                data <= dspdata[j];
                end 
            else if ((i > 1000000) && (i < 2000000)) begin
                i <= i + 1;
                lcd_e <= 0;
                end 
            else if (i == 2000000) begin              
                i <= 1'b0;
                end 
                                      
            else begin
                i <= i;
                lcd_e <= lcd_e;
                data <= data;
                end
        end
   end     
   
   always @(posedge clk or posedge rst) begin
       if (rst)
        j <= 0;    
    else if (j == 38)   
        j <= 4;
    else if (i == 2000000)
        j <= j+1;    
    else 
        j <= j;    
    end
    
    always @(posedge clk or posedge rst) begin
        if (rst)
            lcd_rs <= 0;            
        else if (j <= 4)
            lcd_rs <= 0;
        else if (j > 4 && j < 21)
            lcd_rs <= 1;
        else if (j == 21)
            lcd_rs <= 0;
        else if (j > 21 && j < 38)
            lcd_rs <= 1;
        else 
            lcd_rs <= lcd_rs;
        end                               
endmodule

 

 

- Nucleo Board 제어용 코드

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
  uint8_t rx_data;
  int led_mode = 0;

/* USER CODE END PV */


int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART3_UART_Init();

  /* Initialize interrupts */
  MX_NVIC_Init();
  /* USER CODE BEGIN 2 */

  HAL_UART_Receive_IT(&huart3, (uint8_t *)&rx_data, 1);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  switch (led_mode)
	  {
	  	  case 1 : GPIOB -> ODR = 0x0001; break;
	  	  case 2 : GPIOB -> ODR = 0x0080; break;
	  	  case 3 : GPIOB -> ODR = 0x4000; break;
	  	  case 0 : GPIOB -> ODR = 0x0000; break;
	  }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart -> Instance == USART3)
  {
	  HAL_UART_Receive_IT(&huart3, (uint8_t *)&rx_data, 1);
	  HAL_UART_Transmit(&huart3, (uint8_t *)&rx_data, 1, 500);
	  switch (rx_data)
	  {
	  case '1': led_mode = 1; break;
	  case '2': led_mode = 2; break;
	  case '3': led_mode = 3; break;
	  default: led_mode = 0;
	  }

  }

}
/* USER CODE END 4 */

 

CUBE IDE를 다루는 법은 본 글의 목적에 맞지 않으므로

자세히 설명하지 않고, 필요한 부분만 적어두었습니다.

led 부분의 경우 uart 통신이 잘 이루어지는지 확인하기 위해 넣었습니다.

 

3. 실습결과

FPGA는 ZYNQ-7000의 것을 사용하였고,

Tool은 Vivado를 사용하였습니다.

Nucleo 보드는 CUBE IDE에서 제어하였습니다.

 

 

 

 

키보드를 통해 입력한 문자들이

Nucleo 보드를 거쳐 LCD에 잘 출력되는 것을 볼 수 있습니다.

 

이번에는 합성 및 P&R 단계를 생략하고 테스트를 먼저 진행하였습니다.

이후 시간이 된다면 해당 부분도 포스팅하도록 하겠습니다.

 

- 이전 글

(1) RTL : https://chonh0531.tistory.com/2

(2) Synthesis, P&R : https://chonh0531.tistory.com/3

목차

1. 과제 개요

2. 소스코드

3. 실습 결과

 

 

1. 과제 개요

이전에 구현해 본 SPI_Slave를 테스트해 보기로 하였고,

Master 역할로 Arduino Uno 보드를 이용하여

FPGA에서 Motor를 제어해보도록 합니다.

 

1. 컴퓨터에서 UART 통신으로 Arduino Uno 보드에 명령어를 전달
2. Arduino Uno는 FPGA에 SPI 통신으로 명령을 전달
3. FPGA는 data 부분의 마지막 4비트를 모터 속도제어에 이용

 

 

 

2. 소스코드

- SPI_Slave

`timescale 1ns / 1ps

module top(
    input i_clk, i_SCLK, i_CS, i_rst_H, i_MOSI,
    output o_out, o_nsleep, o_MISO 
    );
    
    wire [3:0] w_duty;
    wire [4:0] w_bitcnt;
                                          
    SPI_Slave U0 (.i_clk(i_clk), .i_SCLK(i_SCLK), .i_CS(i_CS), .i_rst(i_rst_H), .i_MOSI(i_MOSI), .o_MISO(o_MISO), .o_duty(w_duty), .o_bitcnt(w_bitcnt));
    motor U1 (.i_clk(i_clk), .i_rst(i_rst), .i_duty(w_duty), .o_out(o_out), .o_nsleep(o_nsleep));

endmodule

 

module motor(
    input i_clk,
    input i_rst_H,
    input [3:0] i_duty,
    output reg o_out,
    output reg o_nsleep
    );

    reg [18:0] r_cnt;
    reg [4:0] r_duty;
    reg r_clk2;

    always @(posedge i_clk or posedge i_rst_H) begin
        if (i_rst_H)
            o_nsleep <= 0;
        else 
            o_nsleep <= 1;
    end

    always @(posedge i_clk or posedge i_rst_H) begin
        if (i_rst_H) begin
            r_cnt <= 0;
            r_clk2 <= 0;
        end
        else if (r_cnt == 500_000) begin
            r_clk2 <= ~r_clk2;
            r_cnt <= 0;
        end
        else
            r_cnt <= r_cnt + 1;
    end
    
    always @(posedge r_clk2 or posedge i_rst_H) begin
        if (i_rst_H)
            r_duty <= 0;
        else if (r_duty >= 16)
            r_duty <= 0;
        else
            r_duty <= r_duty + 1;
    end
    
    always @(posedge i_clk or posedge i_rst_H) begin
        if (i_rst_H)
            o_out <= 0;
        else if (r_duty < i_duty)
            o_out <= 1;
        else
            o_out <= 0;
    end
endmodule

 

`timescale 1ns / 1ps

`define IDLE 2'b00
`define READ 2'b10
`define WRITE 2'b11

module SPI_Slave(
    // Internal input
    input i_clk,
    input i_rst_H,
    input [1:0] i_ID,

    // SPI Interface
    input i_SCLK,
    input i_CS,
    input i_MOSI,
    output reg o_MISO,

    // Additional output for duty cycle
    output reg [3:0] o_duty
    );

    reg [5:0] bitcnt = 6'd0;
    reg [1:0] current_state = 2'b00;
    reg [1:0] next_state = 2'b00;
    
    reg [15:0] D0;
    reg [15:0] D1;

    reg addr;    
    reg [31:0] data_RX;
    reg id_ok;

// FSM part
    always @(negedge i_SCLK or posedge i_CS) begin
        if (i_CS) begin
            current_state <= `IDLE;
            next_state <= `IDLE;
        end
        else begin
            current_state <= next_state;
            case(current_state)
                `IDLE : begin
                    if (bitcnt == 6'd3 && id_ok == 1 && data_RX[0] == 1)
                        next_state <= `READ;
                    else if (bitcnt == 6'd3 && id_ok == 1 && data_RX[0] == 0)
                        next_state <= `WRITE;
                    else
                        next_state <= next_state;
                end
                `READ : begin
                    if (bitcnt == 6'd0)
                        next_state <= `IDLE;
                    else
                        next_state <= next_state;
                end
                `WRITE : begin
                    if (bitcnt == 6'd0)
                        next_state <= `IDLE;
                    else
                        next_state <= next_state;
                end
                default : next_state <= next_state; // no cases. just for synthesis
            endcase
        end
    end

    always @(posedge i_SCLK or posedge i_CS) begin
        if(i_CS) begin
            id_ok <= 0;
        end
        else if (bitcnt == 6'd2 && data_RX[1:0] == i_ID) begin
            id_ok <= 1;
        end
        else
            id_ok <= id_ok;
    end

// Receive data
    always @(negedge i_SCLK or posedge i_CS) begin
        if(i_CS) begin
            bitcnt <= 6'd0;
            data_RX <= 32'd0;
        end
        else begin
            bitcnt <= bitcnt + 6'd1;
            data_RX <= {data_RX[30:0], i_MOSI};
        end
    end

// Read address
    always @(negedge i_SCLK or posedge i_CS) begin
        if(i_CS) begin
            addr <= 1'd0;
        end
        else if (current_state == `IDLE && bitcnt == 6'd4) begin
            addr <= data_RX[0];
        end
        else
            addr <= addr;
    end

// Write data
    always @(posedge i_SCLK or posedge i_rst_H) begin
        if(i_rst_H) begin
            D0 <= 16'd0;
            D1 <= 16'd0;
        end
        else if(current_state == `WRITE && addr == 1'b0 && bitcnt == 6'd32) begin
            D0 <= data_RX[15:0];
        end
        else if(current_state == `WRITE && addr == 1'b1 && bitcnt == 6'd32) begin
            D1 <= data_RX[15:0];
        end
        else begin
            D0 <= D0;
            D1 <= D1;
        end
    end

// Update o_duty from D0 or D1
always @(posedge i_SCLK or posedge i_rst_H) begin
    if (i_rst_H)
        o_duty <= 4'd0;
    else if (current_state == `WRITE && bitcnt == 6'd32) begin
        if (addr == 1'b0)
            o_duty <= D0[3:0]; // Update duty cycle from the lower 4 bits of D0
        else
            o_duty <= D1[3:0]; // Update duty cycle from the lower 4 bits of D1
    end
    else
        o_duty <= o_duty;
end

// READ Mode: data transfer
    always @(posedge i_SCLK) begin
        if(current_state == `READ && addr == 1'b0) begin
            case(bitcnt)
                6'd16 : o_MISO <= D0[15];
                6'd17 : o_MISO <= D0[14];
                6'd18 : o_MISO <= D0[13];
                6'd19 : o_MISO <= D0[12];
                6'd20 : o_MISO <= D0[11];
                6'd21 : o_MISO <= D0[10];
                6'd22 : o_MISO <= D0[9];
                6'd23 : o_MISO <= D0[8];
                6'd24 : o_MISO <= D0[7];
                6'd25 : o_MISO <= D0[6];
                6'd26 : o_MISO <= D0[5];
                6'd27 : o_MISO <= D0[4];
                6'd28 : o_MISO <= D0[3];
                6'd29 : o_MISO <= D0[2];
                6'd30 : o_MISO <= D0[1];
                6'd31 : o_MISO <= D0[0];
                default : o_MISO <= 0;
            endcase
        end else if(current_state == `READ && addr == 1'b1) begin
            case(bitcnt)
                6'd16 : o_MISO <= D1[15];
                6'd17 : o_MISO <= D1[14];
                6'd18 : o_MISO <= D1[13];
                6'd19 : o_MISO <= D1[12];
                6'd20 : o_MISO <= D1[11];
                6'd21 : o_MISO <= D1[10];
                6'd22 : o_MISO <= D1[9];
                6'd23 : o_MISO <= D1[8];
                6'd24 : o_MISO <= D1[7];
                6'd25 : o_MISO <= D1[6];
                6'd26 : o_MISO <= D1[5];
                6'd27 : o_MISO <= D1[4];
                6'd28 : o_MISO <= D1[3];
                6'd29 : o_MISO <= D1[2];
                6'd30 : o_MISO <= D1[1];
                6'd31 : o_MISO <= D1[0];
                default : o_MISO <= 0;
            endcase
        end else
            o_MISO <= 1'b0;
    end

endmodule

 

기존 코드에서, 입력으로 받은 데이터중 하위 4비트 데이터를 duty로 전달하였습니다.

motor 제어 부분은 간단하므로, 추가로 설명하지 않았습니다.

 

 

- SPI_Master에 사용할 Arduino 코드

#include <SPI.h>


enum InputState {
  WAITING_FOR_MODE,
  WAITING_FOR_ID,  // ID 입력 대기 상태 추가
  WAITING_FOR_UPPER_LOWER,
  WAITING_FOR_ADDRESS,
  WAITING_FOR_DATA
};

InputState currentState = WAITING_FOR_MODE;

bool writeMode;
bool upperData; // 4번째 비트 (Upper/Lower)
bool address;
uint16_t addr; // 주소
uint16_t data; // 데이터
uint8_t id; // ID 값 (2비트)

const int CS_PIN = 10; // SS 핀 (CS 핀) 고정값 변경 못함
const int SCLK_PIN = 13; // SCLK 핀
const int ID_PIN_1 = 40; // ID 핀 1
const int ID_PIN_2 = 42; // ID 핀 2

void setup() {
  Serial.begin(9600); // 통신속도
  SPI.begin(); // SPI 초기화
  SPI.setClockDivider(SPI_CLOCK_DIV2); // 클럭 속도 설정
  SPI.setDataMode(SPI_MODE2); // SPI 모드 설정 (모드 2)

  // SPI 핀 모드 설정
  pinMode(CS_PIN, OUTPUT); // SS 핀을 출력 모드로 설정
  digitalWrite(CS_PIN, HIGH); // 슬레이브 비선택 상태
  pinMode(SCLK_PIN, OUTPUT); // SCLK 핀을 출력 모드로 설정
  digitalWrite(SCLK_PIN, LOW); // 초기 상태 LOW

  // ID 핀 모드 설정
  pinMode(ID_PIN_1, OUTPUT);
  pinMode(ID_PIN_2, OUTPUT);
  digitalWrite(ID_PIN_1, LOW);
  digitalWrite(ID_PIN_2, LOW);

  // 시작 프롬프트 출력
  Serial.println("Enter read/write mode (1 for read, 0 for write):"); // 콘솔에 값 출력
}

void loop() { // 반복문 
  if (Serial.available() > 0) { //시리얼 통신에 값이 있으면
    String input = Serial.readStringUntil('\n'); // 엔터까지 읽기
    input.trim(); // 공백 제거

    if (currentState == WAITING_FOR_MODE) {
      // 모드 입력 받기
      if (input.equals("1") || input.equals("0")) {
        writeMode = input.equals("0"); // 수정: 0을 입력하면 true(0), 1을 입력하면 false(1) 0이 WRITE니까 입력해야 하니까 상태 변화
        currentState = WAITING_FOR_ID; // ID 입력 대기 상태로 전환
        Serial.println("Enter ID value (0 to 3):");  // WRITE일 경우 ID 값 받기
      } else {
        Serial.println("Invalid input. Enter 1 for read, 0 for write:"); //READ는 그냥 넘어가고 0, 1 이외의 값은 그냥 끝내기
      }
    } else if (currentState == WAITING_FOR_ID) { // WRITE 입력하면 상태가 얘로 바뀌니까 반복문에 의해서 얘가 실행
      // ID 입력 받기
      int idValue = input.toInt();
      if (idValue >= 0 && idValue <= 3) {
        id = idValue;
        digitalWrite(ID_PIN_1, id & 0x01); // id가 입력한 값으로 들어온 만약 id로 3을 입력하면 0000 0011이랑 0000 0001이란 &연산 = 0000 0001
        digitalWrite(ID_PIN_2, (id >> 1) & 0x01); // 0000 0011 >> 1 => 0000 0001 & 0000 00001
        currentState = WAITING_FOR_UPPER_LOWER;
        Serial.println("Enter upper or lower (1 for upper, 0 for lower):");
      } else {
        Serial.println("Invalid input. Enter ID value (0 to 3):");
      }
    } else if (currentState == WAITING_FOR_UPPER_LOWER) {
      // upper/lower 입력 받기
      upperData = input.toInt();
      // if (input.equals("1") || input.equals("0")) {
        //upperData = input.equals("1");
      if(upperData == 1 || upperData == 0) {
        currentState = WAITING_FOR_ADDRESS;
        Serial.println("Enter address (in hex):");
      } else {
        Serial.println("Invalid input. Enter 1 for upper, 0 for lower:");
      }
    } else if (currentState == WAITING_FOR_ADDRESS) { // ADDRESS인데 그냥 그 빈공간임
      // 주소 입력 받기
      addr = strtol(input.c_str(), NULL, 16);
      currentState = WAITING_FOR_DATA;
      Serial.println("Enter data (in hex):");
    } else if (currentState == WAITING_FOR_DATA) {
      // 데이터 입력 받기
      data = strtol(input.c_str(), NULL, 16);

      // 32비트 데이터 구성
      uint32_t dataToSend = ((uint32_t)id << 30) | ((uint32_t)(!writeMode) << 29) | ((uint32_t)upperData << 28) | ((uint32_t)addr << 16) | data;

      // SPI 데이터 전송
      SPI.beginTransaction(SPISettings(800, MSBFIRST, SPI_MODE2)); // SPI 설정 (모드 2)
      digitalWrite(CS_PIN, LOW); // 슬레이브 선택

      byte bytesToSend[4];
      bytesToSend[0] = (dataToSend >> 24) & 0xFF;
      bytesToSend[1] = (dataToSend >> 16) & 0xFF;
      bytesToSend[2] = (dataToSend >> 8) & 0xFF;
      bytesToSend[3] = dataToSend & 0xFF;
      
      uint32_t receivedData = 0;
      for (int i = 0; i < 4; ++i) {
        receivedData = (receivedData << 8) | SPI.transfer(bytesToSend[i]);
      }

      digitalWrite(CS_PIN, HIGH); // 슬레이브 비선택
      SPI.endTransaction(); // SPI 트랜잭션 종료

      // 결과 출력
      Serial.print("Sent: ID(");
      Serial.print(id, BIN); // ID 값을 이진수로 출력
      Serial.print("), ");
      Serial.print(writeMode ? "Write" : "Read");
      Serial.print(", ");
      Serial.print(upperData ? "Upper" : "Lower");
      Serial.print(", Address: 16'b");
      Serial.print(addr, BIN);
      Serial.print(", Data: 16'b");
      Serial.print(data, BIN);
      Serial.println();

      // 읽기 모드일 때 수신된 데이터 출력
      if (!writeMode) {
        Serial.print("Received: 0x");
        Serial.println(receivedData, HEX);
      }

      // 다시 모드 입력 상태로 전환
      currentState = WAITING_FOR_MODE;
      Serial.println("Enter read/write mode (1 for read, 0 for write):");
    }
  }
}

 

upper / lower는 각각 D1, D0 데이터에 넣는다는 의미입니다.

 

3. 실습결과

FPGA는 ZYNQ-7000의 것을 사용하였고,

Tool은 Vivado를 사용하였습니다.

Arduino의 제어 Tool은 Arduino IDE를 사용하였습니다.

 

 

데이터 "1000_0000_0000_0000_0000_0000_1001_1001" 이 전송되며,

10은 ID, 0은 writemode, 0이므로 D0에 0000_0000_1001_1001이 저장됩니다.

또한 최하위 1001은 duty로 전달되며, 모터 동작 속도를 결정하게 됩니다.

 

결과적으로, SPI_Slave 설계 프로젝트는 약식이지만 모든 테스트를 통과하였고,

Tape-out 단계 전까지 완료되었습니다.

 

'RTL, Synthesis, P&R' 카테고리의 다른 글

UART 통신 - (2) Arm보드로 FPGA LCD 제어  (0) 2024.09.03
UART 통신 - (1) RTL  (0) 2024.09.03
32-bit SPI Interface - (2) Synthesis, P&R  (0) 2024.09.03
32-bit SPI Interface - (1) RTL  (0) 2024.09.03
4-digit Counter with FPGA  (0) 2024.08.30

진행년월: 24.04

목차

1. 배경

2. 과제 정의 및 개요

3. 소스코드

4. 실습결과

 

1. 배경

Counter는 굉장히 기본적인 예제이지만, 보통의 예제에서는 보드 상에 FND가 최소 2개 이상, 제가 사용한 Xilinx(현 AMD) 사의 ZYNQ-7000의 경우에는 4개가 있는데, 이를 다 활용하는 것이 아니라 일의 자릿수만 하거나 복수의 경우도 단순히 복제해서 작동시키는 경우가 대부분이었습니다.

저는 FPGA 구조를 이해하고 4자리수로 동작시킬 방법에 대해 생각해보려고 합니다.

7-Segment나 Counter 등의 자료는 다른 곳에서도 많이 찾아볼 수 있기 때문에 해당 내용은 생략하도록 하겠습니다.

 

 

2. 과제 정의 및 개요

먼저, 구성이 어떻게 되어 있는지를 알 필요가 있습니다.

 

보통 Pin의 개수 이득을 위해서인지 위와 같이 Digit Pin으로 어떤 FND를 활성화할 것인지를 결정하고,

그 칸에서의 dot를 포함한 8개의 Segment를 제어하도록 구성되어 있습니다.

 

예를들어, Digit0 (T11) 핀을 활성화하고 나머지(T10, T12, U12) 핀을 비활성화하면,

Segment들의 작동은 일의 자릿수인 첫 번째 FND에서만 보여주게 됩니다.

 

그러면, Digit0부터 Digit3까지 Counter의 숫자에 따라 적절하게 변화를 줄 수 있도록 설계해야 합니다.

다양한 방법이 있겠지만, 저는 다음과 같은 아이디어로 스펙을 정의하도록 하겠습니다.

 

스펙 정의

1. Counter는 사람이 인지할 수 있는 속도로 증가합니다.(Slow Clock)

2. Shift Registor를 활용해 Digit0부터 Digit3까지 사람 눈에 인식되지 않는 빠른 속도로 (Fast Clock) 1을 순환시킵니다. 이는 모든 칸이 계속 불이 들어오는 것으로 인지됩니다.
(ex: 0001 -> 0010 -> 0100 -> 1000 -> 0001)

3. Counter 값에서 자릿수 숫자를 분리하여 각 자릿수에 해당하는 칸이 활성화될 때, 해당 자릿수 숫자를 할당합니다.
(ex: Counter가 396일 때각 자릿수는 0396이며,  Digit이 0100일 순간에는 0300을 보여주도록 함)

4. 할당된 숫자는 Segment로 디코딩되며, 디지털 숫자로 보여주도록 합니다.

 

 

3. 소스코드

// 메인 모듈
module counter(clk, rst, seg_out, mode_out);
    input clk, rst;                                         // 클록, 리셋 포트
    output [7:0] seg_out;                                   // 7-세그먼트 포트 a ~ dp
    output [3:0] mode_out;                                  // digit_0 ~ digit_3 포트
    reg [3:0] thous, hunds, tens, units;                    // 자릿수에 사용될 십진수
    wire [7:0] seg_thous, seg_hunds, seg_tens, seg_units;  // 7-세그먼트로 변환된 자릿수
    wire clk_wiz, clk_div;                                 // 분주기를 통해 나온 두가지 클록 신호. clk_wiz는 clocking wizard를 통해 만들어짐.
    integer tmp, cnt;                                      // 자릿수 계산에 사용될 임시 변수, 카운터에 사용될 변수
    reg [3:0] mode_tmp;                                    // 디지털 숫자판 digit_0 ~ digit_3중 어디에 신호를 넣을지 결정하는 배열
    reg [7:0] seg_tmp;                                     // 모드가 결정되면 그에 따른 7-세그먼트를 선언해 주기 위한 변수
    
    assign mode_out = mode_tmp;                            // 어떤 칸을 켤지에 대한 포트. 0010 이면 오른쪽에서 두번째 칸을 킨다.
    assign seg_out = seg_tmp;                              // 결정된 모드에 따라 알맞은 자릿수를 전달하기 위함

// 카운터 모듈 동작부. 분주기 모듈 gen_clk에 의해 동작한다.
    always @(posedge clk_div or posedge rst) begin
        if (rst) begin
            cnt <= 0; units <= 0; tens <= 0; hunds <= 0; thous <= 0;
        end
        else if (cnt == 9999) begin
            cnt <= 0; units <= 0; tens <= 0; hunds <= 0; thous <= 0;
        end
        else begin
            cnt = cnt + 1;                                          // 자릿수 계산. 중간에 tmp 변수를 활용. 
            thous = (cnt - (cnt % 1000)) / 1000; tmp = cnt % 1000;  // 직관적으로 이해할 수 있도록 이것과 같이 코드를 작성하였음.
            hunds = (tmp - (tmp % 100)) / 100; tmp = tmp % 100; 
            tens = (tmp - (tmp % 10)) / 10;
            units = tmp % 10;
        end
    end

// Shift-Resister 방식으로 mode의 값을 순차적으로 왼쪽으로 밀어줌.
// 이것은 fnd의 켜지는 칸을 계속 바꿔주는 작동을 함.
    always @(posedge clk_wiz or posedge rst) begin
        if (rst) begin
            mode_tmp <= 4'b0001; seg_tmp <= 8'b01111111;
        end
        else begin
            mode_tmp[0] <= mode_tmp[3];
            mode_tmp[3:1] <= mode_tmp[2:0];            
        end
        case(mode_out)
            4'b0001 : seg_tmp <= seg_tens; // 위에서 한칸씩 왼쪽으로 이동되므로 하나씩 왼쪽 것을 할당한다.
            4'b0010 : seg_tmp <= seg_hunds;
            4'b0100 : seg_tmp <= seg_thous;
            4'b1000 : seg_tmp <= seg_units;
            default: seg_tmp <= 8'b11000000; 
        endcase
    end

clk_wiz_0 wclk(clk_wiz, clk);                             // Clocking Wizard에 의한 클록 신호. 5 Mhz 보다 낮게 설정 불가
gen_clk gclk(clk_wiz, rst, clk_div);                      // 사용자의 임의대로 만들어질 새로운 클록 신호.
seven_segment thous_seg(.digit(thous), .seg(seg_thous));  // 천의 자리 세그먼트
seven_segment hunds_seg(.digit(hunds), .seg(seg_hunds));  // 백의 자리 세그먼트
seven_segment tens_seg(.digit(tens), .seg(seg_tens));     // 십의 자리 세그먼트
seven_segment units_seg(.digit(units), .seg(seg_units));  // 일의 자리 세그먼트
endmodule

 

제가 사용한 보드에서는 50 MHz 클럭이고, 분주 방법으로 Vivado의 IP Catalog 중 Clocking Wizard를 사용하였습니다.

이 방법으로는 5 MHz보다 느리게 만들 수 없지만,

이것은 사람이 인지보다 훨씬 빠르기 때문에 Fast Clock으로 사용하였습니다.

 

 

Counter를 위한 Slow Clock을 만들기 위해 분주기를 따로 사용한 코드는 다음과 같습니다.

// 카운터를 응용하여 분주기 모듈을 만들어봄. 더 효율적인 것이 있을 수 있음.
module gen_clk(input clk, input rst, output reg clk_div);
    integer i;
    always @(posedge clk) begin
        if (rst) begin
            i = 0; clk_div = 0;
        end
        else begin
            i = i+1;
            if (i == 249999) begin           // 2,499,999 를 사용하면 1초에 한번 클락으로 가능.
                i = 0; clk_div = ~clk_div;   // 네자리수까지 보여줘야 하므로 0.1초 클락으로 진행.
            end
        end
    end
endmodule

 

 

 

4개의 자릿수마다 segment를 따로 Decoding 해주어야 하므로 인스턴스화해서 사용하였고, 코드는 다음과 같습니다.

// 7-세그먼트 표현을 위한 모듈 
// 1은 꺼진 것, 0이 켜진 것으로 설계.
// 8번째 칸은 디지털 숫자 우하단 점(dp)를 의미하는데 사용하지 않을 것이므로 모두 1로 한다.
module seven_segment(input [3:0] digit, output reg [7:0] seg);
    always @(*) begin
        case (digit)
            4'b0000: seg = 8'b11000000; // 0
            4'b0001: seg = 8'b11111001; // 1
            4'b0010: seg = 8'b10100100; // 2
            4'b0011: seg = 8'b10110000; // 3
            4'b0100: seg = 8'b10011001; // 4
            4'b0101: seg = 8'b10010010; // 5
            4'b0110: seg = 8'b10000010; // 6
            4'b0111: seg = 8'b11111000; // 7
            4'b1000: seg = 8'b10000000; // 8
            4'b1001: seg = 8'b10010000; // 9
            default: seg = 8'b11111111; // 이외에 다른 숫자가 입력으로 들어올 시 모두 off
        endcase
    end
endmodule

 

 

4. 실습결과

 

 

 

촬영 환경이 좋지 않은 점 양해 부탁드립니다.

+ Recent posts