진행년월: 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
'RTL, Synthesis, P&R' 카테고리의 다른 글
UART 통신 - (2) Arm보드로 FPGA LCD 제어 (0) | 2024.09.03 |
---|---|
UART 통신 - (1) RTL (0) | 2024.09.03 |
32-bit SPI Interface - (3) Arduino로 FPGA Motor 제어 (0) | 2024.09.03 |
32-bit SPI Interface - (2) Synthesis, P&R (0) | 2024.09.03 |
4-digit Counter with FPGA (0) | 2024.08.30 |