RTL, Synthesis, P&R

32-bit SPI Interface - (1) RTL

MiddleJo 2024. 9. 3. 15:02

진행년월: 24.05

목차

1. 배경

2. 과제 정의 및 개요

3. 소스코드

4. 시뮬레이션 결과

 

 

1. 배경

모든 칩은 통신이 필요하기 때문에 통신 모듈이 들어가있습니다.

그중에서 좀 기본적인 통신 모듈중 하나인 SPI통신을 이해하고 구현해보려고 합니다.

 

 

2. 과제 정의 및 개요

기본적으로 SPI 통신의 표준 프레임인 Motorola의 메뉴얼을 참고하나,

기본적인 구현 자체에 목적이 있기 때문에 불필요한 기능은 빼고 다음과 같이 스펙 정의를 하도록 합니다.

 

1. SPI Mode 2 (CPOL = 1, CPHA = 0)를 채택하여 하강엣지에 데이터를 읽도록 합니다.
2. CS, SCLK, MOSI, MISO 4개의 핀을 사용합니다.
3. 32비트 데이터를 교환하며 구성은 다음과 같습니다.
    - [0,1]비트는 ID
    - [2]비트는 Read / Write 모드 선택
    - [3]비트는 메모리 선택 주소 (D0 or D1)
    - [4~15]비트는 다용도 확장을 위한 빈공간 (주소 확장 등)
    - [16~31]비트는 16비트 데이터

 

Timing Diagram과 State Diagram은 아래와 같습니다.

간단한 구현이므로 엄격하게 많은 상황을 포함하여 그리지는 않았습니다.

 

 

 

3. 소스코드

`timescale 1ns / 1ps

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

module SPI_Slave(
    // Internal input
    // We don't use internal clock now.
    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
    );
    
    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
        
// READ Mode: data transfer
/*  now, there is enough time to prepare the data
	because there is time for empty 12-bit transfers in preparation for expansion(addr or options, maybe).
    
    If necessary, We can also design the data to be read from the next cycle using internal clk, 
    which is about least four times faster than SCLK.
*/
    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

 

주석으로 적어둔 것 처럼, 가운데 Spare 비트들은 확장을 위해 두었으며,

해당 비트 직후 Read 명령을 수행해야 하는 경우,

SCLK보다 최소 4배정도 빠른 내부 clock을 이용해 처리할 수 있습니다.

 

 

module tb_SPI_Slave();

//    reg i_clk;
    reg i_rst_H;
    reg [1:0] i_ID;
    
    reg i_SCLK;
    reg i_CS;
    reg i_MOSI;
    wire o_MISO;
    
    SPI_Slave SPI(
//        .i_clk(i_clk),
        .i_rst_H(i_rst_H),
        .i_SCLK(i_SCLK),
        .i_CS(i_CS),
        .i_MOSI(i_MOSI),
        .o_MISO(o_MISO),
        .i_ID(i_ID)
    );
    
    initial begin
//        i_clk = 1'b0;
        i_rst_H = 1'b0;
        i_SCLK = 1'b0;
        i_MOSI = 1'b0;
        i_CS = 1'b1;
        i_ID = 2'b01;
        
        // Reset sequence
        #40 i_rst_H = 1'b1;
        #40 i_rst_H = 1'b0;
        
        // Test case 1: Write mode "0101_0000_0000_0000_1100_1100_1100_1101"
        #20 i_CS = 1'b0; //if timing problem in pre_sim -> #10
        send_SPI_data(32'b0101_0000_0000_0000_1100_1100_1100_1101);
        #40 i_CS = 1'b1;
        
        // Wait for a while before the next case
        #40;
        
        // Test case 2: Read mode "0111_0000_0000_0000_0000_0000_0000_0000"
        #40 i_CS = 1'b0;
        send_SPI_data(32'b0111_0000_0000_0000_0000_0000_0000_0000);
        #40 i_CS = 1'b1;
        
        // Test case 3: Read mode, ID Error "1011_0000_0000_0000_0000_0000_0000_0000"
        #40 i_CS = 1'b0;
        send_SPI_data(32'b1011_0000_0000_0000_0000_0000_0000_0000);
        #40 i_CS = 1'b1;

        #100 $finish;
    end
    
    // Clock generation
//    always #5 begin i_clk = ~i_clk; end
    always #20 begin i_SCLK = ~i_SCLK; end
    
    // Task to send data
    task send_SPI_data(input [31:0] data);
        integer i;
        begin
            for (i = 31; i >= 0; i = i - 1) begin
                @ (posedge i_SCLK) // For timing
                    i_MOSI <= data[i];
            end
        end
    endtask
    
endmodule

 

 

4. 시뮬레이션 결과

동작을 확인해보기 위해, 또 모범적인 waveform을 마련해놓기 위해 function simulation을 합니다.

Tool은 Xcelium을 사용하였습니다.

 

 

1. Write Mode Test

 

붉은 화살표로 표시한 CS가 0인 구간에 동작을 하고,

결과적으로 D1에 원하는 데이터가 잘 들어간 것을 볼 수 있습니다.

 

 

2. Read Mode Test

 

Read 명령을 받은 후 16비트 타이밍부터 D1에 있는 데이터가

MISO를 통해 잘 출력하고 있는 것을 볼 수 있습니다.

 

 

 

 

- 다음 글

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

 

SPI 통신 - (2) Synthesis, P&R

- 이전 글(1) RTL : https://chonh0531.tistory.com/2 SPI 통신 - (1) RTL목차1. 배경2. 과제 정의 및 개요3. 소스코드4. 시뮬레이션 결과  1. 배경모든 칩은 통신이 필요하기 때문에 통신 모듈이 들어가있습니다.

chonh0531.tistory.com