前几篇的原理与代码写得不够详细并且有疏漏得地方,在本篇中会进行纠正
首先,我们先认识到我们需要在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表示为读,W0R1
reg [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;
end
end
always @ (*) 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;
end
always @ (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 ;
end
wire 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 buf
always @ (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 data
end
end
assign O_DOUT = R_out_buf;
// load parallel input buff
// shift parallel input to SDOUT
always @ (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};
end
assign 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)begin
R_wa <= I_WA;
R1_I_WD <= I_WD ;
R1_I_WDE <= I_WDE;//缓冲
// read address just inc 1 every clk period , for read test
R_ra <= R_ra + 1;
end
end
dpram_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 param
defparam 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 param
parameter 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];
end
endmodule // 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 < 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;
// 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 < 128; i ++){
wa = i&(128-1) ; wa_H = (wa >> 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;
}
目前最后一部还未完全验证。