[STM32/FPGA]软件SPI接口模块[STM32主机FPGA从机]
文章目录
SPI系统架构
其中MCU的输出使用单片机程序输出。MOSI和MISO使用FPGA内部电路作为接口模块。其他的为FPGA的读写结构和读写的例子。
STM32:SPI时序与代码实现
基本读写结构
写双字(32位)
写时序:(这里的时序图是FPGA接收到的信号的SignalTap采集结果)
STM32的程序基本按照这个时序来就可以了。
void spi_wr_32bit_MSB_first(unsigned char B3,unsigned char B2,unsigned char B1,unsigned char B0){
int i = 0, j = 0;
unsigned char byte4[4] = {0};
unsigned char buf = 0;
byte4[3] = B3; byte4[2] = B2; byte4[1] = B1; byte4[0] = B0;
spi_wr_cs(1);
spi_wr_sck(1);
spi_wr_sda_out(1);
// drop down cs
spi_wr_cs(0);
delay_us(1);
// drop down sck
spi_wr_sck(0);
delay_us(1);
// put data
for(j = 0; j < 4; j ++){
buf = byte4[3-j];
for(i = 0; i < 8; i ++){
spi_wr_sda_out((buf&0x80) >> 7);
buf = buf << 1;
delay_us(1);
spi_wr_sck(1);
delay_us(1);
spi_wr_sck(0);
}
}
spi_wr_cs(1);
spi_wr_sck(1);
}
读字(16位)
读时序:
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;
}
写数据
写数据同时要写地址,即这个数据将要写入的地址。
本工程的FPGA中的接口模块将写函数spi_wr_32bit_MSB_first函数传送过来的四个字节的数据的前两个作为地址,然后将后两个作为数据。
所以写32位的程序应为:
spi_wr_32bit_MSB_first(0x00, 0x00, 0x02, 0x02);
spi_wr_32bit_MSB_first(0x00, 0x00, 0x04, 0x04);
这两句往地址为0x0000的空间写入了32位的数据。
要写入大量数据(比如写RAM)可以采用循环。
void spi_wr_addr_16b_data_16b_ram_128(){
int i = 0, wa = 0, wd = 0, wa_H, wa_L, wd_H, wd_L;
for (i = 0 ; i < 128; i ++){
wa = i&(128-1); wa_H = (wa >> 8) & 0xff; wa_L = wa & 0xff;
wd = wa*255 ; wd_H = (wd >> 8) & 0xff; wd_L = wd & 0xff;
spi_wr_32bit_MSB_first(wa_H, wa_L, wd_H, wd_L);
}
}
这个函数向一个128位的RAM依次写入一组递增值。
在这里没有用到总线选择器,而是在FPGA中直接写入对应地址。当FPGA中只有这一个RAM需要读写时可以这么做。
而一般FPGA中有多个模块需要读写操作。这时则需要加上总线选择器。
如果将RAM的地址时能连接到总线选择器的0x0000,数据使能连接到总线选择器的0x0001,则上述写RAM可改为:
void spi_wr_addr_16b_data_16b_ram_128(){
int i = 0, wa = 0, wd = 0, wa_H, wa_L, wd_H, wd_L;
for (i = 0 ; i < 128; i ++){
wa = i&(128-1); wa_H = (wa >> 8) & 0xff; wa_L = wa & 0xff;
wd = wa*255 ; wd_H = (wd >> 8) & 0xff; wd_L = wd & 0xff;
spi_wr_32bit_MSB_first(0x00, 0x00, wa_H, wa_L);
spi_wr_32bit_MSB_first(0x00, 0x01, wd_H, wd_L);
}
}
读数据
读数据首先需要写地址。写入地址之后FPGA的总线模块才会将对应的数据送回来给单片机读。
读取16位的程序为:
spi_wr_32bit_MSB_first(0x00, 0x00, 0x00, 0x00);
rd_val = spi_wr_1bit_cmd_read_16bit();
在读数据前的写地址可以将前两个字节数据理解为片选,后两个字节理解为片选后要读取的地址。
读取一片ROM的程序为:
unsigned short rom_rd[128] = {0};
void spi_rd_addr_7b_data_8_rom_128(){
unsigned int rom_addr = 0, rd_val = 0;
spi_wr_32bit_MSB_first(0x00, 0x07, 0x00, 0x01);
for(rom_addr = 0; rom_addr < 128; rom_addr ++){
spi_wr_32bit_MSB_first(0x00, 0x08, 0x00, rom_addr&0xFF);
rd_val = spi_wr_1bit_cmd_read_16bit();
rom_rd[rom_addr] = (unsigned short) rd_val & 0xFFFF;
}
}
在FPGA中对应读总线选择器对应地址为0x0007,要读的ROM连接在读总线选择器的0x01输入口,所以先要向读总线选择器写入0x01,即spi_wr_32bit_MSB_first(0x00, 0x07, 0x00, 0x01)。
要读的ROM地址为0x0008,所以在循环中写地址数据时的片选信号为0x0008。
读取完数据后放入数组rom_rd,等待后续处理。
FPGA:接口模块系统架构
整体功能结构和第一部分的基本相同,在这里将相同或重合功能和接口的模块进行了整合。
其中spi_bus为SPI接口模块,select_in_to为总线选择器,其他的为测试样例。
输入输出时序实现(spi_in_to_parallel_data_out_parallel_in_to_spi_out)
输入需要串入并出寄存器,输出则需要并入串出寄存器。
由于输入的亚稳态问题,还需要加入两级流水线避免这个问题。DOV的延时输出是为了流水线延时匹配。
总线选择器比较简单,另外采用了select_in_to模块实现。
地址译码(addr_decode)
本工程中需要通信的模块较少而设置的地址片选信号较多(片选信号为O_WE),所以采用线选法。选到哪个模块则对应的线置1。
always @ (*) begin
case(R1_I_AIN)
16'h00000 : W_to_DOV = 32'h0000_0001& {32{R2_I_ENA}};
16'h00001 : W_to_DOV = 32'h0000_0002& {32{R2_I_ENA}};
16'h00002 : W_to_DOV = 32'h0000_0004& {32{R2_I_ENA}};
16'h00003 : W_to_DOV = 32'h0000_0008& {32{R2_I_ENA}};
16'h00004 : W_to_DOV = 32'h0000_0010& {32{R2_I_ENA}};
......
default: W_to_DOV = 8'b 0000_0000 & {8{R2_I_ENA}};
endcase
end
可以通过观察每个模块所连接的线选线,在单片机的程序中写入对应片选地址从而实现单片机与FPGA内部不同模块之间的通信。