SPI 통신은 거리가 짧은 장치들끼리 통신할때 많이 쓰이는 프로토콜입니다.
최근에 Xilinx社의 FPGA에서 SPI 통신을 구현하여 동작시킬 일이 있어서 코드를 짜봤습니다.
FPGA의 경우엔 구글에서 좀 뒤져봤는데 프로토콜을 잘못 이해하고 짠 것들이 많아 힘들었는데, [여기]가 기반코드를 잘 짜놔서 여기에서부터 시작했습니다.
참고로 Xlinx社 제품들의 디지털 신호는 LVCMOS33(3.3V)이 대부분인데 아두이노는 5V입니다. 따라서 아두이노에서 FPGA로 신호를 보낼때는 전압 강하를 시키던가 하지 않으면 보드가 죽을 수 있습니다. 저는 [여기]를 참고(2N3094, 2N3906이 필요)해서 강하 회로를 만들었습니다만, Level Shifter 칩을 구해서 써도 됩니다. 정 아니면 저항만 써도 되긴 하다만 안전한 방법은 아닙니다.
아래는 코드 주요 사항 설명입니다.
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 |
#include <SPI.h> SPISettings spiSettings(200000, MSBFIRST, SPI_MODE3); #define MYSPI_CMD_WRITE 1 #define MYSPI_CMD_READ 2 #define MYSPI_CMD_TEST 3 void setup (void) { SPI.begin (); // SPI 통신 초기화 digitalWrite(SS, HIGH); // 슬레이브가 선택되지 않은 상태로 유지 Serial.begin(9600); } void loop (void) { SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. char received = SPI.transfer(MYSPI_CMD_TEST); digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(0); // digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(0); digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(0); digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(0); digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); /* SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(MYSPI_CMD_WRITE); // digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); SPI.beginTransaction(spiSettings); digitalWrite(SS, LOW); // 슬레이브를 선택한다. received = SPI.transfer(0); // digitalWrite(SS, HIGH); // 슬레이브 선택을 해제한다. SPI.endTransaction(); Serial.println(received); */ delay(100); } |
Slave.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 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 |
`timescale 1ns / 1ps // SPI Slave code (MSB FIRST,SPI_MODE3/CPOL=CPHA=1) // by TAE-IN, KIM ([email protected]) // since 2019.01.25 module SPI_Slave( input clk, input sclk, output reg miso, input mosi, input ss, input reset, output wire sclk_org, output wire mosi_org, output wire ss_org, output reg debug ); localparam DATA_WIDTH = 8; reg [2:0] bit_count; // sizeof(bit_count) == DATA_WIDTH localparam DATA_TEST = 8'b01000001; localparam DATA_TEST2 = 16'b0100001101000010; localparam DATA_TEST3 = 32'b01000101010001000100001101000010; reg [7:0] last_command; reg [2:0] param_count; // 2:0 => can count 8 localparam CMD_WRITE = 8'b00000001; localparam CMD_WRITE_PARAM_SIZE = 1; localparam CMD_READ = 8'b00000010; localparam CMD_READ_PARAM_SIZE = 2; localparam CMD_TEST = 8'b00000011; localparam CMD_TEST_PARAM_SIZE = 4; // reg [7:0]memory; reg [7:0]cache; reg [7:0]last_data; reg send_data; reg finished; reg [2:0] SCLKr; wire SCLK_posedge = (SCLKr[2:1]==2'b01); wire SCLK_negedge = (SCLKr[2:1]==2'b10); reg [2:0] SSr; wire SS_active = ~SSr[1]; wire SS_startmessage = (SSr[2:1]==2'b10); wire SS_endmessage = (SSr[2:1]==2'b01); assign sclk_org = sclk; assign mosi_org = mosi; assign ss_org = ss; reg clear; always @(posedge reset) begin clear <= 0; end always @(posedge clk)begin SCLKr <= {SCLKr[1:0], sclk}; SSr <= {SSr[1:0], ss}; debug <= finished; // Task during slave is not selected (SS == 1) if(~SS_active) begin if(last_data == CMD_WRITE) begin last_command <= CMD_WRITE; param_count <= CMD_WRITE_PARAM_SIZE; send_data <= 1; end else if(last_data == CMD_READ) begin last_command <= CMD_READ; param_count <= CMD_READ_PARAM_SIZE; send_data <= 1; end else if(last_data == CMD_TEST) begin last_command <= CMD_TEST; param_count <= CMD_TEST_PARAM_SIZE; send_data <= 1; end else begin if(param_count == 0) begin send_data <= 0; finished <= 1; end else begin finished <= 0; end end if(last_command == CMD_WRITE) begin cache <= DATA_TEST; end else if(last_command == CMD_READ) begin if(param_count == 2) begin cache <= DATA_TEST2[15:8]; end else if(param_count == 1) begin cache <= DATA_TEST2[7:0]; end end else if(last_command == CMD_TEST) begin if(param_count == 4) begin cache <= DATA_TEST3[31:24]; end else if(param_count == 3) begin cache <= DATA_TEST3[23:16]; end else if(param_count == 2) begin cache <= DATA_TEST3[15:8]; end else if(param_count == 1) begin cache <= DATA_TEST3[7:0]; end end bit_count <= 3'b000; miso <= 0; // reset MISO signal end // Task during slave is selected (SS == 0) else begin if(SCLK_posedge) begin last_data[7-bit_count] <= mosi; bit_count <= bit_count + 1; end if(SCLK_negedge) begin if(send_data == 1) begin if(bit_count == 7) begin param_count <= param_count - 1; end miso <= cache[7-bit_count]; end else begin // miso <= mosi; end end end end endmodule |
아래는 위 코드를 실행했을 때 나오는 파형입니다. 마스터가 CMD_TEST3을 보내면 아스키코드로 “EDCB”에 해당하는 비트를 슬레이브가 보냅니다.
FPGA 프로그래밍은 뭐랄까… 디버깅이 물리적이라는 점도 진입장벽이긴 하지만 Verilog 특유의 시스템(always, Blocking/Non-Blocking Assignment 등)이 참 새롭네요.
시뮬레이션에서 원하는대로 된다고 해도 합성&구현하고나서 안되는 경우도 있고.. Vivado에서 합성했을때 나오는 Critical Warning 메시지는 꼭 읽고 해결해야합니다. Warning은 가능하면 해결해야하구요. 안그럼 Bitstream이 안 만들어지는 경우가 대다수고 만들어진다해도 생각하던대로 동작하지 않을겁니다.
특히 논리회로에서 Finite State Machine(FSM) 공부를 애매하게 해놓으면 C++, Java 등으로 소프트웨어 짜듯이 Verilog 코드를 짜게되어서 힘들어지네요. ㅠㅠ