> 技术文档 > 关于STM32与FPGA的通信(SPI通信方式)(3-实现MCU与FPGA的互相通信)_fpga和stm32通信

关于STM32与FPGA的通信(SPI通信方式)(3-实现MCU与FPGA的互相通信)_fpga和stm32通信

        前几篇的原理与代码写得不够详细并且有疏漏得地方,在本篇中会进行纠正

        首先,我们先认识到我们需要在FPGA中创建什么类型的模块,一个ROM供MCU读取,一个RAM供MCU写入,若干计数器。为了实现这些模块的通信,在这中间我们需要使用一些模块来达到目的。最为重要的模块就是充当MOSI与MISO的处理转化输入输出的模块,我们暂且简称其为SPI_BUS模块,这个模块的一个作用是处理MCU输入的32位数据,将其转化为地址和数据两部分数据发送给各个模块

由上图实例化的模块图可以看出,输出一个16位的数据和一个32位的使能标志,有关使能标志位如何寻址相应寄存器在上一章节讲过,不再赘述。而这边的I_DIN通过一个MUX选择器来选择MCU读取的数据

读取哪一位数据完全由MCU来指示,只有当MUX选择器的使能位W_O_WE[7]接收到来自MCU的指令置1才会启用,即寻址成功。而W_O_WD[15..0]则是选择这16个挂载的附件的哪一个作为输出给MCU。

        这是SPI_BUS的顶层代码,链接各个端口

module spi_bus_32b_A15RD1WR0( I_CLK , I_CS , I_SCLK , I_SDIN , I_DIN , I_DEN , O_SDOUT , O_WD , O_WE , O_TESTOUT1);input I_CLK ;input I_CS ;input I_SCLK ;input I_SDIN ;output O_SDOUT; output [16-1:0] O_WD ;output [32-1:0] O_WE ;output O_TESTOUT1;input[16-1:0] I_DIN ;input I_DEN ;wire[32-1:0] W_U_spi_O_DOUT;wire W_U_spi_O_DOV ;spi_in_to_parallel_data_out_parallel_in_to_spi_out U_spi_s2p( .I_CLK (I_CLK ), .I_CS (I_CS ), .I_SCLK (I_SCLK), .I_SDIN (I_SDIN), .I_DIN (I_DIN ), .I_DEN (I_DEN ), .O_DOUT (W_U_spi_O_DOUT), .O_DOV (W_U_spi_O_DOV ), .O_SDOUT (O_SDOUT ), .O_TESTOUT1());wire [16-1:0] W_U_dec_O_DAT;wire [32 -1:0] W_U_dec_O_DOV; addr_decode U_decode( .I_CLK (I_CLK ), .I_ENA (W_U_spi_O_DOV ), // input enable .I_DIN (W_U_spi_O_DOUT[15: 0]), .I_AIN (W_U_spi_O_DOUT[31:16]), .O_DAT (W_U_dec_O_DAT ), .O_DOV (W_U_dec_O_DOV ), .O_TESTOUT1 ());assign O_WD = W_U_dec_O_DAT;assign O_WE = W_U_dec_O_DOV; endmodule

然后这边的串入并出,并入串出的模块比起上一节多了输出的功能

需要注意:R_sdin_msb实际上是用来存储I_SDIN的最高位的,另一个R_sclk_up_cnt就是用来计数sck上升次数

R_sdout_shift[16:0]用于将并行输入转换为串行数据的移位寄存器

这段代码的核心两个时序逻辑一个是处理MCU向FPGA串行写入数据一个是MCU读取FPGA的数据这时是并入串出的

module spi_in_to_parallel_data_out_parallel_in_to_spi_out( I_CLK , I_CS , I_SCLK , I_SDIN , I_DIN , I_DEN , O_DOUT , O_DOV , O_SDOUT , O_TESTOUT1);output O_TESTOUT1;input I_CLK ;input I_CS ;input I_SCLK ;input I_SDIN ;input [16-1:0] I_DIN;input  I_DEN;reg [16-1:0] R1_I_DIN;reg R1_I_CS ,R2_I_CS ,R3_I_CS ;reg R1_I_SCLK ,R2_I_SCLK,R3_I_SCLK ;reg R1_I_SDIN ,R2_I_SDIN, R3_I_SDIN ;//级联寄存器缓冲减少亚稳态出现概率reg W_I_CS_up , W_I_CS_down, W_I_SCLK_up;reg [32-1:0] R_sdin_shift, R_out_buf;output O_SDOUT;output [32-1:0] O_DOUT ;output O_DOV ; reg O_DOV ; reg [8-1:0] R_sclk_up_cnt;reg R_sdin_msb;//用于存储SDIN的第一位数据,用来决定读写,后续的输出标志位与之有关,为1表示为读,W0R1reg [16:0] R_sdout_shift; // 16+1 bit, one bit(as stuff ) more than data word length// pipeline// | meta stability// | R3_I_CS |R2_I_CS |R1_I_CS | I_CS <-- Input// | R3_I_SCLK |R2_I_SCLK |R1_I_SCLK | I_SCLK// | R3_I_SDIN |R2_I_SDIN |R1_I_SDIN | I_SDIN// |  |W_I_CS_up // |  |W_I_CS_down// |  |W_I_SCLK_up// |R_sclk_up_cnt |//R_sdin_msb |R_sdout_shift//always @ (posedge I_CLK) begin//主要是处理读写位R_sdin_msb和时钟上升沿次数(采样了几次数据) // cs neg edge clear the sclk counter and sdin msb reg to zero if(W_I_CS_down) begin R_sclk_up_cnt <= 0; R_sdin_msb <= 0; end else begin // store the first bit of SDIN if((R_sclk_up_cnt == 0)&& W_I_SCLK_up) R_sdin_msb <= R3_I_SDIN; // sclk counter when CS is enable(low) if(~(R1_I_CS) && W_I_SCLK_up) R_sclk_up_cnt <= R_sclk_up_cnt + 1; endendalways @ (*) begin//捕捉上升沿与下降沿 W_I_CS_up = ((R3_I_CS == 1\'b0)&&( R2_I_CS == 1\'b1)) ? 1\'b1 : 1\'b0; W_I_CS_down = ((R3_I_CS == 1\'b1)&&( R2_I_CS == 1\'b0)) ? 1\'b1 : 1\'b0; W_I_SCLK_up = ((R3_I_SCLK == 1\'b0)&&( R2_I_SCLK == 1\'b1)) ? 1\'b1 : 1\'b0;endalways @ (posedge I_CLK) begin R1_I_CS <= I_CS ; R1_I_SCLK <= I_SCLK ; R1_I_SDIN <= I_SDIN ; R2_I_CS <= R1_I_CS ; R2_I_SCLK <= R1_I_SCLK ; R2_I_SDIN <= R1_I_SDIN ; R3_I_SDIN <= R2_I_SDIN ; R3_I_CS <= R2_I_CS ; R3_I_SCLK <= R2_I_SCLK ;endwire W_spi_is_wr; assign W_spi_is_wr =(~R_sdin_msb); wire W_spi_wr_out_en = W_I_CS_up & (~R_sdin_msb) ;//表明是写操作// shift input SDIN to parallel OUT// load parallel output bufalways @ (posedge I_CLK) begin//处理MCU向FPGA写入数据。并且是串入并出 if(W_I_CS_down) R_sdin_shift <= 0; else if(W_I_SCLK_up) R_sdin_shift <= {R_sdin_shift[30:0],R2_I_SDIN}; // if(W_I_CS_up& (~R_sdin_msb)) // R_out_buf <= R_sdin_shift; // O_DOV <= W_I_CS_up & (~R_sdin_msb); // msb is 1 for read don\'t output data if(W_spi_wr_out_en)begin R_out_buf <= R_sdin_shift; O_DOV <= W_spi_wr_out_en; // msb is 1 for read don\'t output dataendend assign O_DOUT = R_out_buf;// load parallel input buff// shift parallel input to SDOUTalways @ (posedge I_CLK) begin//处理MCU读取FPGA数据,并且是并入串出 if(I_DEN)//数据使能有效 R1_I_DIN <= I_DIN;//I_DIN是数据 if(W_I_CS_down)//捕捉到起始信号 R_sdout_shift <= {1\'b0,R1_I_DIN};//注意,R_sdout_shift是17位的 else if(W_I_SCLK_up && R_sdin_msb) // msb == 1, read mode ,shift out R_sdout_shift <= {R_sdout_shift[15:0],1\'b0};endassign O_SDOUT = R_sdout_shift[16];//串出读数据assign O_TESTOUT1 = W_I_SCLK_up ;endmodule

        再就是写入的RAM模块的代码,

这个双口RAM实现异步读写,其中有个新的语法

实际上就是定义以恶个数组,表示这个数组有

2^ADDRWL个元素。每个元素有ADDRWL的宽度。图中这样写是为了直观的看出多少位多少个元素

module dpram16_spi_wr_test( I_CLK , I_WA , I_WAE , I_WD , I_WDE , O_RD );input  I_CLK ; input [16-1:0] I_WA ; input  I_WAE ;input [16-1:0] I_WD ;input  I_WDE ;output [16-1:0] O_RD ;reg [16-1:0] R_wa ; reg [16-1:0] R1_I_WD ;reg R1_I_WDE ;reg [7-1:0] R_ra ;always @ (posedge I_CLK) begin if(I_WAE)beginR_wa <= I_WA;R1_I_WD <= I_WD ; R1_I_WDE <= I_WDE;//缓冲// read address just inc 1 every clk period , for read testR_ra <= R_ra + 1;endenddpram_spi_wr U_ram_128( .WCLK (I_CLK ), // write clock .RCLK (I_CLK ), // read clock .WA (R_wa ), // write address .WD (R1_I_WD ), // write data .WE (R1_I_WDE), // write enable .RA (R_ra ), // read address .RD (O_RD )); // read data// external set paramdefparam U_ram_128.DATAWL = 16;defparam U_ram_128.ADDRWL = 7;endmodule////////////////////////////////////////////////////////////////////////////////module dpram_spi_wr( WE , // write enable WCLK , // write clock RCLK , // read clock WA , // write address RA , // read address WD , // write data RD ); // read data// external set paramparameter DATAWL = 0;parameter ADDRWL = 0;parameter C2Q = 0; input  WE, WCLK, RCLK; input [ADDRWL -1:0] WA, RA; input [DATAWL -1:0] WD; output [DATAWL -1:0] RD; reg[DATAWL-1:0] RD; reg[DATAWL-1:0] mem [(1 << ADDRWL)-1:0]; always @ (posedge WCLK) begin if(WE) mem[WA] <= #C2Q WD; end always @ (posedge RCLK) begin RD <= #C2Q mem[RA]; endendmodule // module dptbram() 

有关FPGA的部分就大概写做完了,最后的BDF如图所示

        然后就是有关主机操控从机的部分。

        向计数器发送代码的部分泰国简单就不多赘述了

 spi_wr_32bit_MSB_first(0x00, 0x00, 0x02, 0x02); // 16 bit counter 1 spi_wr_32bit_MSB_first(0x00, 0x01, 0x01, 0x01); // 16 bit counter 2 spi_wr_32bit_MSB_first(0x00, 0x00, 0x04, 0x04); // 16 bit counter 1 spi_wr_32bit_MSB_first(0x00, 0x01, 0x05, 0x05); // 16 bit counter 2 spi_wr_32bit_MSB_first(0x00, 0x02, 0x01, 0x02); // 32 bit counter L spi_wr_32bit_MSB_first(0x00, 0x03, 0x03, 0x04); // 32 bit counter H spi_wr_32bit_MSB_first(0x00, 0x04, 0x00, 0x00); // 32 bit counter Update spi_wr_32bit_MSB_first(0x00, 0x02, 0x04, 0x03); // 32 bit counter L spi_wr_32bit_MSB_first(0x00, 0x03, 0x02, 0x01); // 32 bit counter H spi_wr_32bit_MSB_first(0x00, 0x04, 0x00, 0x00); // 32 bit counter Update

然后是向RAM写入的功能,这部分的代码比较简单,就是2个循环从头写到尾和从尾写到头的测试

其中两次传输数据对RAM的写入对应了FPGA中的W_O_WE[5]和W_O_WE[6],这是一个不断对RAM进行写入的过程

这两行用来不断变化地址以及数据同时保证不超出RAM的范围

这段代码将第七位标志位置1,打开mux,从中读取数据

void spi_wr_addr_7b_data_16b_ram_128(){ int i = 0, wa = 0, wd = 0, wa_H, wa_L, wd_H, wd_L; // test case 1 for (i = 0 ; i > 8) & 0xff; wa_L = wa & 0xff; wd = wa*255 ; wd_H = (wd >> 8) & 0xff; wd_L = wd & 0xff; // ADDRESS MUST BE WRITE FIRST //  A_H A_L , D_H , D_L spi_wr_32bit_MSB_first(0x00, 0x05, wa_H, wa_L); // write wa reg spi_wr_32bit_MSB_first(0x00, 0x06, wd_H, wd_L); // write wd reg } // Delay(1000*1000*10); // test case 2 for (i = 0 ; i > 8) & 0xff; wa_L = wa & 0xff; wd = (128-1-wa)*255 ; wd_H = (wd >> 8) & 0xff; wd_L = wd & 0xff; // ADDRESS MUST BE WRITE FIRST //  A_H A_L , D_H , D_L spi_wr_32bit_MSB_first(0x00, 0x05, wa_H, wa_L); // write wa reg spi_wr_32bit_MSB_first(0x00, 0x06, wd_H, wd_L); // write wd reg } }

接下来有关MCU如何从FPGA中读取数据,首先我们要知道MCU读取的FPGA输出的数据是串出的,所以我们首先需要一个读取串出数据的函数,这个函数中我们使用了一个向FPGA写入翻个数据的函数

void spi_wr_sda_out(int val){ GPIO_WriteBit(PORT_MY_SPI_O_SDA,PIN_MY_SPI_O_SDA ,(val&0x1)?Bit_SET:Bit_RESET );}

第一位写1,表示读W0R1,此时的FPGA的SPI模块接收到第一位,R_sdin_msb已经置1,输出时序已准备.

读取数据的循环先写入1是为了保证采样的稳定性

unsigned int spi_wr_1bit_cmd_read_16bit(){ int i = 0; unsigned int rd_val = 0; unsigned char sda_in = 0; unsigned char buf = 0xaa; unsigned char rd16[16] = {0}; spi_wr_cs(1); spi_wr_sck(1); spi_wr_sda_out(1); // drop down cs spi_wr_cs(0); Delay(1); // drop down sck spi_wr_sck(0); Delay(1); // write first data bit as 1 spi_wr_sda_out(1); Delay(1); spi_wr_sck(1); Delay(1); spi_wr_sck(0); // read data for(i = 0; i < 16; i ++){ spi_wr_sda_out(0); buf = buf << 1; Delay(1); spi_wr_sck(1); Delay(1); spi_wr_sck(0); // read sda in ,at sclk neg edge sda_in = spi_rd_sda_in(); // printf(\"sda_in = %x \\r\\n\",sda_in); rd_val = (rd_val << 1)| (sda_in&0x01); rd16[i] = sda_in; } spi_wr_cs(1); spi_wr_sck(1);// printf(\"rd_val = %x \\r\\n\",rd_val);#if 0 // bit print debug for(i = 0; i < 16; i ++){ printf(\"rd16[%d] = %x \\r\\n\",i, rd16[i]); }#endif return rd_val;}

然后就是有关通过MUX读取其负载原件数据的代码,比较简单,就是读取0号位置上挂载的计数器的值,注意开启I_DEN这个使能位,当时我Debug了半天。

unsigned int spi_read_rom(){unsigned int rd_val = 0;spi_wr_32bit_MSB_first(0x00, 0x07, 0x00, 0x00);rd_val = spi_wr_1bit_cmd_read_16bit();return rd_val;}

然后就是进一步读取挂载在1号位的ROM的数据

unsigned short rom_rd[128] = {0};unsigned int spi_rd_addr_7b_data_8_rom_128(){ unsigned int rom_addr = 0, rd_val = 0; //printf(\"#------------------------------------------------------------------------------- \\r\\n\"); //printf(\"# ROM READ TEST \\r\\n\", rom_addr, rd_val); // first write the read select value spi_wr_32bit_MSB_first(0x00, 0x07, 0x00, 0x01); // select the input port 1 for(rom_addr = 0; rom_addr < 128; rom_addr ++){ // write the rom read address spi_wr_32bit_MSB_first(0x00, 0x08, 0x00, rom_addr&0xFF); // select the input port 1 // read rom value rd_val = spi_wr_1bit_cmd_read_16bit(); // save the low 16bit to array rom_rd[rom_addr] = (unsigned short) rd_val & 0xFFFF; } // print the rom value /*for(rom_addr = 0; rom_addr < 128; rom_addr ++){ printf(\"# ROM[%4d] = 0x%-4X \\r\\n\", rom_addr, rom_rd[rom_addr]); }*/return rd_val;}

目前最后一部还未完全验证。