您的位置:首页 > 其它

基于AM335X与FPGA的SPI通讯设计

2017-11-09 16:49 190 查看
在2013年的工作中,涉及到了AM3359与XC7K325T之间的相互通信,其目的是为了获取FPGA设计版本号,该FPGA版本号保存在FPGA的寄存器0xFFFF中,FPGA的版本值随着加载程序发生变化,当时的版本信息为0x1003.

需要说明的是,在本文中的代码风格是刚工作两年的时候的代码风格,现在回看,这些代码风格实在难以阅读。尤其是SPI的verilog程序等。并不代表现在的编程水平与代码风格。

设计框图如下:



本文主要从三个方面介绍AM3359与FPGA的通讯

第一部分:SPI通信的基础定义

第二部分:AM335x的SPI通信编程

第三部分:FPGA从机SPI设计

SPI通信基础定义

通信是怎么发生

SPI接口是一种典型的全双工接口。通过同步时钟SCLK的脉冲将数据一位一位地在主机和从机之间交换。所以在开始通信之前,Master首先需要配置接口时钟,在Master配置时钟的时候,不需要通知从机它所配置的时钟频率具体是多少,设计人员只需要确保通讯频率是从机所支持的即可。

当Master通过片选信号(SS,低电平有效)选定一个Slave的时候,每向Slave发送一个周期的SCLK信号,都会有1bit的数据从MOSI引脚发送给Slave,Slave只需要在对应的引脚接收数据即可;同时,slave每收到一个周期的SCLK信号,都会从MISO想Master发送1bit的数据。



从上段描述中可以分析出一下两点:

1.无论Master还是Slave,都是按照bit来传输的,那么对于需要发送或者接收的数据,必须在Master或Slave中有一个移位寄存器,这些是由硬件来保证的,普通的SPI接口设计者不需要考虑移位寄存器的因素。

2.在通信中,Master发送数据后,一般需要保证Slave收到数据,这样才能确定数据在收发的过程中不发生因为硬件而导致的bit丢失。在SPI中,数据传输以“位交换”的方式传输,这样能根据从机返回的数据来确定从机已经收到数据了。SPI同样与其他基础通信方式(USB,I2C,UART等)一样无法确保传输数据的正确性。

SPI是一个相对比较开放的接口,具体表现在时钟极性/相位、帧大小、传输速度、LSB/MSB等规则没有一个确定的定义,需要根据不同的通信环境由设计开发者进行定义。

SPI的接口时序

在实际开发使用SPI的时候,需要注意使Master和Slave处于相同的Mode下工作。不同mode的定义主要是针对时钟的相关特性。

SCLK极性(CPOL):clock Polarity

SCLK相位(CPHA):clock Phase



CPOL

在解释CPOL之前先要介绍什么是SCLK的空闲时刻。在SPI通讯传输的时候,SCLK并不是时刻都有。在SCLK发送数据之前和发送数据之后,都会回到空闲状态,这个状态下,SCLK要么保持在高电平,要么保持在低电平。这个是需要设计者来指定的,CPOL的作用就是来指定SPI在IDLE状态下的点评状态。

CPOL = 0 :时钟空闲状态(IDLE)的电平为低电平(0)。
CPOL = 1 :时钟空闲状态(IDLE)的点评是高电平(1)。

CPHA

CPHA表示数据采样,数据有效的时刻。对应的数据采样是在第几个边沿进行采样。

CPHA = 0 :在时钟第一个边沿采样。
对于CPOL = 0 :因为IDLE为低电平,那么第一个边沿就是从低电平到高电平,即为上升沿。
对于CPOL = 1 :因为IDLE为高电平,那么第一个边沿就是从高电平到低电平,即为下降沿。

CPHA = 1 :在时钟第二个边沿采样。
对于CPOL = 0 :因为IDLE为低电平,那么第二个边沿就是从高电平到低电平,即为下降沿。
对于CPOL = 1 :因为IDLE为高电平,那么第二个边沿就是从低电平到高电平,即为上升沿。



需要注意的是:采样一定是需要先准备好数据,才用时钟的有效沿将数据打到对应的引脚上。

Mode选择参考

SPI没有一个通用的推荐模式,但是基于工程设计的时候是否有一个推荐的Mode选择呢?在CSDN博友的一篇《SPI接口扫盲 SPI定义/SPI时序(CPHA CPOL)》中,作者从功耗的角度分析,建议应该多选择SCLK在空闲状态下处于低电平,即CPOL保持在IDLE状态下为0。这是一个很好的分析方法。对于CPHA的选择分析,我更赞成根据实际的应用来做设计,而不是根据习惯来设计。

AM335x的SPI通信编程

在本工程中所使用的SPI为AM335x的SPI外设,即在Linux下,只需要对spidev进行文件操作。所用到的文件操作函数有以下四个:

open()

write()

read()

close()

相关的函数说明,请参考网络,在此不赘述。

在编写SPI驱动的时候,还需用到ioctl()函数,下面对ioctl做一些简要介绍。

什么是ioctl

ioctl是设备驱动程序中对设备的IO通道进行管理的函数。即是对设备的一些特性进行控制,例如对串口设备设置波特率,对SPI设备设置字长,通讯速率等。函数的调用方式如下:

int ioctl(int fd,unsigned long cmd,...);
/*
fd:文件描述符
cmd:控制命令
fecc
...:可选参数:插入*argp,具体内容依赖于cmd
*/


其中fd是用户程序打开设备是使用open()函数返回的文件标识符(句柄),cmd是用户程序对设备的控制命令,后面的省略号,表示该函数可能还有其他参数,该参数与cmd相关。

对于ioctl的详细描述,可以参考文末链接1来详细阅读。

AM335x驱动程序源码设计

static uint8_t  mode  = 0;
static uint8_t  bits  = 8;
static uint32_t speed = 16000000;
static uint8_t  cs    = 1;

int fpga_fd = 0;
uint8_t  file_name_buf[FILE_NAME_MAX] = "/dev/spidev2.1";
//==
int fpga_config(uint8_t  *file_name)
{
if (strlen(file_name) > FILE_NAME_MAX)
{
printf("File name length error\r\n");
return 0;
}
strcpy(file_name_buf, file_name);
return 1;
}

int fpga_spi_open(uint8_t  *file_name)
{
int ret = 0;
fpga_fd = open(file_name, O_RDWR);
if (fpga_fd < 0)
{
printf("in fpga_spi_open, can't open device\r\n");
return nQAM_ERROR_CAS_SPI_OPEN;
}
else
{
printf("in fpga_spi_open, open device success\r\n");
}
/*
* spi mode
*/
ret = ioctl(fpga_fd, SPI_IOC_WR_MODE, &mode);
if (ret == -1)
{
printf("in fpga_spi_open, can't set spi mode\r\n");
return nQAM_ERROR_CAS_SPI_CONFIG;
}
else
{
printf("in fpga_spi_open, set spi mode success\r\n");
}
/*
* bits per word
*/
ret = ioctl(fpga_fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret == -1)
{
printf("in fpga_spi_open, can't set bits per word\r\n");
return nQAM_ERROR_CAS_SPI_CONFIG;
}
else
{
printf("in fpga_spi_open, set bits per word success\r\n");
}
/*
* max speed hz
*/
ret = ioctl(fpga_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret == -1)
{
printf("in fpga_spi_open, can't set max speed hz\r\n");
return nQAM_ERROR_CAS_SPI_CONFIG;
}
else
{
printf("in fpga_spi_open, set max speed success\r\n");
}
/*
* chip select
*/
ret = ioctl(fpga_fd,SPI_IOC_WR_CHIP_SELECT,&cs);
if(ret == -1)
{
printf("in fpga_spi_configure,can't set chip  select\r\n");
return nQAM_ERROR_CAS_SPI_CONFIG;
}
return nQAM_ERROR_NOERROR;
}

int fpga_spi_close()
{
return close(fpga_fd);	// close device
}
//数据交换
uint8_t transfer(uint8_t data)
{
uint8_t sbuf = data;
if(write(fpga_fd,&sbuf,1) != -1)
{
if(read(fpga_fd, &sbuf, 1) != -1)
{
printf("In transfer, transfer data over\r\n");
return sbuf;
}
else
{
printf("In transfer, read data from spi device failed!\r\n");
}
}
else
{
printf("In transfer, write data to spi device failed!\r\n");
}
return 0;
}
//通过SPI获取版本号
uint8_t get_fpga_version()
{
uint8_t buf;
if(fpga_spi_open(file_name_buf) != nQAM_ERROR_NOERROR)
{
printf("in get_fpga_vesion, fpga spi open failed\r\n");
return 0;
}
buf = transfer(READ_VESION);
fpga_spi_close();
printf("Read version done.\r\n");
return buf;
}
//测试物理连接状态
uint8_t test_fpga_connect()
{
uint8_t buf;
if(fpga_spi_open(file_name_buf) != nQAM_ERROR_NOERROR)
{
printf("in test_spi_open, fpga spi open failed\r\n");
return 0;
}
buf = transfer(TEST_CONNECT);
fpga_spi_close();
printf("FPGA spi test connect done.\r\n");
return buf;
}


附:spidev的ioctl命令。

SPI_IOC_RD_MODE:               读取spi_device的mode。

SPI_IOC_RD_LSB_FIRST:            如果是SPI_LSB_FIRST的方式则返回1。

SPI_IOC_RD_BITS_PER_WORD:         读取spi_device的bits_per_word.

SPI_IOC_RD_MAX_SPEED_HZ:          读取spi_device的max_speed_hz.

SPI_IOC_WR_MODE:               设置spi_device的mode,并调用spi_setup立即使设置生效。

SPI_IOC_WR_LSB_FIRST:            设置spi使用SPI_LSB_FIRST的传输模式。立即生效。

SPI_IOC_WR_BITS_PER_WORD:         读取字长。

SPI_IOC_WR_MAX_SPEED_HZ:          设置时钟速率。

AM335x应用程序设计

int main(int argc, char *argv )
{
uint8_t connect;
uint8_t version;
connect = test_fpga_connect();
printf("Test Value is %02X\r\n", connect); //测试连接状态

version = get_fpga_version();
printf("the fpga version is %02X\r\n", version);

printf("****fpga app test run over!****\r\n");
return 1;
}


FPGA从机SPI设计

SPI从机的Verilog实现

module spi_slave(clk,rst,data_i,wr_en_i,data_o,tx_valid,valid_o,start_o,end_o,we_ack_o,ss_i,sclk_i,mosi_i,miso_o
);

input                 clk;          // master clock input
input                 rst;          // synchronous active high reset
input   [7:0]         data_i;       // data bus input
input                 wr_en_i;      // write enable input
output  [7:0]         data_o;       // data bus output

output                valid_o;      // request signal output
output                tx_valid;
output                start_o;
output                end_o;
output  reg           we_ack_o;

//spi signals
input                 ss_i;         // slave select
input                 sclk_i;       // serial clock
input                 mosi_i;
output                miso_o;

reg     [7:0]         tx_data;
reg     [7:0]         rx_data;

reg                   tx_tip;       //tx in progress
reg                   rx_tip;       //rx in progress

wire                  rx_negedge;   //miso is sampled on negative edge
wire                  tx_negedge;   //mosi is driven on nesedge edge
wire    [2:0]         len;          //char length
wire                  lsb;          //lsb first on line

wire                  pos_edge;     //recognize posedge of sclk
wire                  neg_edge;     //recognize negdege of sclk

reg                   s_out;
reg                   s_in;
reg     [2:0]         s_sel;
reg     [2:0]         s_clk;

assign    rx_negedge  = 0;            // decided by CPOL and CPHA
assign    tx_negedge  = 1;            // means mode == 00
assign    len         = 7;
assign    lsb         = 0;

assign    miso_o      = s_out;
assign    valid_o     = rst?  1'b0  : (!rx_tip);
assign    data_o      = rst?  8'h00 : rx_data;

//sync SCK to the FPGA clock using a 3-bits shift register
always @ (posedge clk)
begin
s_clk <= {s_clk[1:0], sclk_i};  //sample the sclk_i using clk,when finding the first posedge the value is 001,when finding the first negedge the value is 110 or 010
end

assign pos_edge       =   (s_clk[1:0] == 2'b01);      // posedge when s_clk[1:0] == 2'b01 or s_clk[2:0] = 3'b001
assign neg_edge       =   (s_clk[1:0] == 2'b10);      // negedge when s_clk[1:0] == 2'b10 or s_clk[2:0] = 3'b110

//SSEL
always @ (posedge clk)
begin
s_sel <= {s_sel[1:0], ss_i};    //sample the ss signal
end

wire                  sel_active;                     // from start_o is high to end_o is high sel_active is active
assign sel_active     =   ~s_sel[1];                  // sel[2:0] = 000 001 011 111 110 100 000 when 100 000 001 .0 is high
assign start_o        =   (s_sel[1:0] == 2'b10);      // start_o when s_sel[1:0] = 2'b10 or s_sel[2:0]= 3'b110
assign end_o          =   (s_sel[2:1] == 2'b01);      // end_o when s_sel[2:1] = 2'b01 or s_sel[2:0] = 3'b011

//---------------------receiving bits from line---------------------
wire                  rx_clk;
wire                  rx_lst_bit;
reg   [2:0]           rx_cnt;                         // rx data bit count

assign rx_clk         =   rx_negedge? neg_edge : pos_edge;    // question is the beginning value is "X"
assign rx_lst_bit     = !(|rx_cnt);

always @(posedge clk)
begin
s_in  <=  mosi_i;
end

always @(posedge clk or posedge rst)
begin
if (rst)
rx_cnt <= len;
else begin
if(!rx_tip || end_o || start_o)
rx_cnt  <=  len;
else
rx_cnt  <=  rx_clk ? (rx_cnt - 1'b1) : rx_cnt;        //question is the rx_cnt always is 7?
end
end

//Transfer in process
always @(posedge clk or posedge rst)
begin
if(rst)
rx_tip  <= 1'b0;
else if(!rx_tip)
rx_tip  <= 1'b1;
else if(rx_tip && rx_lst_bit && rx_clk)
rx_tip  <= 1'b0;
end

always @(posedge clk or posedge rst)
begin
if(rst)
rx_data <= {8{1'b0}};
else begin
if(sel_active && rx_clk)
rx_data <= lsb ? {s_in,rx_data[7:1]} : {rx_data[6:0],s_in}; // if lsb = 0 rx_data = {rx_data[6:0],s_in}
// if lsb = 1 rx_data = {s_in,rx_data[7:1]}
end                                           // {s_in,rx_data[7:1]} shift right
end                                             // {rx_data[6:0],s_in} shift left

//---------------------sending bits to line---------------------
wire                  tx_clk;
wire                  tx_lsb_bit;
reg [2:0]             tx_cnt;
assign tx_clk         =   tx_negedge? neg_edge : pos_edge;        // tx_negedge = 1 negedge transfer data
assign tx_lsb_bit     = !(|tx_cnt);
assign tx_valid       = ((s_sel[2:0] == 3'b111) && (tx_tip == 1'b0)) ? 1'b1 : 1'b0;

//  always @(posedge clk or posedge rst)
//  begin
//    if(rst)
//      tx_valid  <=  1'b0;
//    else if((s_sel[2:0] == 3'b111) && (tx_tip == 1'b0))
//      tx_valid  <=  1'b1;
//    else
//      tx_valid  <=  1'b0;
//  end
// character bit counter
always @(posedge clk or posedge rst)
begin
if(rst)
tx_cnt  <= len;
else begin
if(!tx_tip || end_o || start_o)
tx_cnt  <= len;
else
tx_cnt  <= tx_clk? (tx_cnt-1'b1):tx_cnt;
end
end

//transfer in process
always @(posedge clk or posedge rst)
begin
if(rst) begin
tx_tip  <= 1'b0;
end
else if(wr_en_i && (!tx_tip)) begin               //wr_en_i is high when transfer the data
tx_tip  <= 1'b1;
end
else if(tx_tip && tx_lsb_bit && tx_clk) begin
tx_tip  <= 1'b0;
end
end

always @(posedge clk or posedge rst)
begin
if(rst) begin
tx_data <= 8'hff;
we_ack_o  <=  1'b0;
end
else begin
we_ack_o <= 1'b0;
if(wr_en_i && (!tx_tip)) begin
tx_data[7:0]  <=  data_i[7:0];
we_ack_o  <= 1'b1;
end
else begin
if(sel_active && rx_clk) begin
s_out <=  lsb?  tx_data[0] : tx_data[7];
tx_data <= lsb? {1'b1,tx_data[7:1]} : {tx_data[6:0],1'b1};
end
end
end
end
endmodule

FPGA的SPI command verilog设计

`timescale 1 ns / 1 ps
module spi_cmd #(
parameter   BUS_DATA_WIDTH            = 8,
parameter   BUS_ADDR_WIDTH            = 16
)(
input                                 clk,
input                                 rst,
input       [BUS_DATA_WIDTH-1:0]      rx_data,
input                                 rx_valid,
input                                 rx_start,
input                                 rx_end,
input                                 tx_valid,

input                                 tx_ack,
output  reg [BUS_DATA_WIDTH-1:0]      tx_data,
output  reg                           tx_req
);

localparam    CON_WR_REG              = 8'h51;
localparam    CON_RD_REG              = 8'h52;
localparam    RD_DATA_REG             = 8'h96;
localparam    RST_CMD                 = 8'h55;

localparam    ADDR_FPGA_VERSION       = 16'hFFFF;
localparam    ADDR_SPI_TEST           = 16'hF000;
localparam    FPGA_VERSION            = 16'h1003;

localparam    REC_NO_ERR              = 8'h90;
localparam    REC_INS_ERR             = 8'hF1;
localparam    REC_LEN_ERR             = 8'hF2;
localparam    REC_BUSY_ERR            = 8'hF3;
localparam    REC_WR_ERR              = 8'hF4;

localparam    REC_START               = 1;
localparam    REC_TYPE_REG            = 2;
localparam    REC_ADDR_REGH           = 3;
localparam    REC_ADDR_REGL           = 4;
localparam    REC_ADDR_LEN            = 5;
localparam    REC_CON_WRH             = 6;
localparam    REC_CON_WRL             = 7;
localparam    RSP_STATUS              = 8;
localparam    RSP_LEN                 = 9;
localparam    RSP_DIN                 = 10;
localparam    RSP_DOUT                = 11;
localparam    READ_DATAH              = 12;
localparam    READ_DATAL              = 13;
localparam    READ_DONE               = 14;

reg   [7:0]                           rec_type;
reg   [15:0]                          rec_addr;
reg   [7:0]                           addr_len;
reg   [7:0]                           len_count;
reg   [3:0]                           cmd_state;
reg   [7:0]                           err_code;
reg   [15:0]                          rsp_dout;
reg                                   rsp_rd;
reg   [15:0]                          spi_test;
reg   [3:0]                           test_flag;

always@(posedge clk or posedge rst)
begin
if(rst) begin
rec_type          <=  0;
rec_addr          <=  0;
addr_len          <=  0;
len_count         <=  0;
err_code          <=  REC_NO_ERR;
tx_data           <=  0;
tx_req            <=  0;
rsp_rd            <= 1'b0;
cmd_state         <=  REC_START;
test_flag         <=  4'h0;
end
else begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
case(cmd_state)
REC_START : begin
len_count   <=  0;
if(rx_start) begin
cmd_state <=  REC_TYPE_REG;
test_flag <=  4'h1;
end
end
REC_TYPE_REG : begin
if(rx_valid) begin
case(rx_data)
CON_WR_REG, CON_RD_REG : begin
err_code    <=  REC_NO_ERR;
rec_type    <=  rx_data;
cmd_state   <=  REC_ADDR_REGH;
test_flag   <=  4'h2;
end
RD_DATA_REG : begin
cmd_state   <=  RSP_STATUS;
test_flag   <=  4'h6;
end
default : cmd_state <=  REC_START;
endcase
end
end

REC_ADDR_REGH : begin
if(rx_valid) begin
rec_addr[15:8]  <=  rx_data;
cmd_state   <=  REC_ADDR_REGL;
test_flag   <=  4'h3;
end
end
REC_ADDR_REGL : begin
if(rx_valid) begin
rec_addr[7:0] <=  rx_data;
cmd_state   <=  REC_ADDR_LEN;
test_flag   <=  4'h4;
end
end

REC_ADDR_LEN : begin
if(rx_valid) begin
if(rx_data != 0) begin
addr_len    <=  rx_data;
test_flag <=  4'h5;
case(rec_type)
CON_WR_REG : cmd_state  <=  REC_CON_WRH;
CON_RD_REG : cmd_state  <=  REC_START;
default : cmd_state <=  REC_START;
endcase
end
else begin
err_code  <=  REC_LEN_ERR;
cmd_state <=  REC_START;
end
end
end
REC_CON_WRH : begin
if(len_count == addr_len) begin
cmd_state <=  REC_START;
end
else begin
if(rx_valid) begin
if(rec_addr == ADDR_FPGA_VERSION) begin
err_code  <=  REC_WR_ERR;
cmd_state <=  REC_START;
end
else if(rec_addr == ADDR_SPI_TEST) begin
spi_test[15:8]  <=  rx_data;
end
//              else begin
//                rec_buf_data[15:8]  <=   rx_data;
//              end
cmd_state <=  REC_CON_WRL;
end
end
end
REC_CON_WRL : begin
if(rx_valid) begin
if(rec_addr == ADDR_SPI_TEST) begin
spi_test[7:0] <=  rx_data;
end
//            else begin
//              rec_buf_data[7:0] <=  rx_data;
//              rec_addr  <=  rec_addr + 1'b1;
//            end
len_count <=  len_count + 1'b1;
end
end

RSP_STATUS : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
rsp_rd    <=  1'b1;
tx_data   <=  err_code;
cmd_state <=  RSP_LEN;
test_flag <=  4'h7;
end
end

RSP_LEN : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
case(rec_type)
CON_WR_REG : begin
tx_data   <=  0;
cmd_state <=  REC_START;
end
CON_RD_REG : begin
tx_data   <=  addr_len;
cmd_state <=  RSP_DIN;
end
default : begin
tx_data   <=  0;
cmd_state <=  REC_START;
end
endcase
test_flag <=  4'h8;
end
end

RSP_DIN : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
if(len_count == addr_len) begin
cmd_state <=  READ_DONE;
end
else begin
cmd_state <=  RSP_DOUT;
rsp_rd    <=  1'b0;
end
test_flag <=  4'h9;
end
end

RSP_DOUT : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
cmd_state <=  READ_DATAH;
end
end

READ_DATAH : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
tx_data   <=  rsp_dout[15:8];
cmd_state <=  READ_DATAL;
test_flag <=  4'hA;
end
end
READ_DATAL : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
tx_data   <=  rsp_dout[7:0];
len_count <=  len_count + 1'b1;
cmd_state <=  RSP_DIN;
test_flag <=  4'hB;
end
end

READ_DONE : begin
if(tx_valid) begin
if(tx_ack) begin
tx_req  <=  1'b0;
end
else begin
tx_req  <=  1'b1;
end
tx_req    <= 1'b0;
len_count <=  0;
tx_data   <=  0;
rec_type    <=  0;
cmd_state <=  REC_START;
end
end
endcase
end
end

always @(posedge clk or posedge rst)
begin
if(rst == 1'b1)
begin
rsp_dout  <=  {16{1'b0}};
end
else if((rsp_rd == 1'b1) && (test_flag  ==  4'h7)) begin
case(rec_addr)
ADDR_FPGA_VERSION : rsp_dout  <=  FPGA_VERSION;
ADDR_SPI_TEST :   rsp_dout  <=  ~spi_test;
default : ;
endcase
end
end

endmodule


以上项目总结仅作参考,适用于本公司后继者对此工程的修改参考。

链接1 :点击打开链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  AM335x FPGA SPI通信