好了确认完整个框架之后,就可以开始SPI的逻辑设计了,还是先设计好再写代码!!!!
1. baud_clk_gen
这个就单纯的根据波特率分频出一个sck时钟,所以比较容易,不解释
还是注意baud_cnt可能取不刀BAUD_CNT_END这个细节。
1.1. 代码
module baud_clk_gen#(
parameter BAUD_RATE = 12500000,
parameter CPOL = 0,
parameter CLK_FREQ = 50000000
)(
input rstn,
input clk,
output sck
);
localparam BAUD_CNT_END = CLK_FREQ / BAUD_RATE;
localparam BAUD_CNT_WIDTH = $clog2(BAUD_CNT_END);
reg [BAUD_CNT_WIDTH-1:0] baud_cnt;
always@(posedge clk) begin
if(!rstn)
baud_cnt <= 'b0;
else if(baud_cnt < BAUD_CNT_END-1)
baud_cnt <= baud_cnt + 'b1;
else
baud_cnt <= 'b0;
end
generate if(CPOL) begin
always@(posedge clk) begin
if(!rstn)
sck <= 'b0;
else
sck <= !sck;
end
end
else begin
always@(posedge clk) begin
if(!rstn)
sck <= 'b1;
else
sck <= !sck;
end
end
endgenerate
endmodule
2. spi_master
先想好master的功能是啥,无论是什么协议发起事务请求的都是Master,即发起写or读操作的都是Master。
例如异步FIFO,永远是被写or被读的,所以异步FIFO就是一辈子Slave打工人
SPI Master也是,任务是对Slave SPI进行读写。但是一想,SPI的全双工是通过循环移位寄存器实现的,发送数据的同时接受了对方的数据,即写必会读、读必会写。
这样的话,外部写给spi master一个数据,spi master通过循环移位发给spi slave,没毛病。
但是读怎么读呢?spi master得发给spi slave一个没用的数据,传输结束之后,spi master的移位寄存器存储的才是读spi slave的数据。
所以spi_master状态机可以这么设计
这个状态图有两个特点:1. 想让Master读出Slave的数据,必须先写 2. 循环移位之后Master从Slave那得到的数据必须读出,否则不能写入。 第2点可以改一下,就是将WAITREAD和IDLE合并,TRANS完了必须在下一次写来临之前读出,否则不能读出
2.1. IDLE:等待写入
IDLE就是等待用户写入,写入完毕的转到TRANS。可以在IDLE将ready一直拉高等待tx_data_val。但有一个问题:tx_data是在clk快时钟域下,必须同步到sck慢时钟域下
多bit数据跨时钟域数据变化快怎么办?添加一个ack?NONONO,通用好用的方法是什么?异步FIFO!
所以IDLE时,SPI Master从FIFO读数,读到数据了就可以进入TRANS,时序也是常见的握手。
标准握手时序 异步时钟亚稳态 的解决方案——多bit信号
然后就进入TRANS开始移位寄存器循环移位就OK了。但有一个问题,IDLE状态下用户没有指定哪个slave spi被选通怎么办? 即$csn_i == 1'b1
注意决不能assign csn_o = csn_i; 这样用户一选通就直接开始传输了,要是选通了还没从FIFO读出数来咋办?
要是没选通,那就等一等。怎么等?可以为IDLE与TRANS之间添加一个状态等待选通。
2.2. WAIT_CS:等待选通
可以在fifo_rdata_valid==1'b1 时判断(!&csn_i) 是否成立,成立则将csn_o <= csn_i; 并进入TRANS,否则就再加一个状态WAITCS等待选通,如下图
上图TRANS与csn_o 是时序对齐的 当然也可以用1bit reg寄存 是否已经从FIFO读出数来,读出数来了再判断(!&csn_i)
于是状态转换图变成
2.3. TRANS:传输
传输时由于CPOL和CPHA模式导致spi master对mosi驱动和miso采样在sck不同沿进行,所以根据这两种情况分别设计。
(CPOL ^ CPHA == 0)时
即对应{CPOL,CPHA} == 2'b00 || 2'b11; 时,此时是sck上升沿采样miso至移位寄存器、sck下降沿驱动mosi,大端模式
此时移位寄存器内的数据已经准备好,并且是在sck上升沿处驱动csn_o 的某位拉低,所以TRANS状态下第一个沿是下降沿,即先驱动、再采样。
如上图,移位寄存器data_shift 位宽为8,那么一定是negedge sck 时第7位驱动mosi出去、data_shift[6:0] << 1; ,在posedge sck 时miso采样到data_shift[0]
所以说移位寄存器data_shift 的移位操作是在下降沿进行的,data_shift[0] 采样MISO操作时在上升沿进行的,那这个是否违背了触发器单沿触发的设计要求?
就是你不能
always@(posedge clk) begin a <= b; end
always@(negedge clk) begin a <= c; end
违背了,因为IDLE状态下data_shift 被FIFO中的fifo_rdata 赋值时是在posedge sck ,但是TRANS状态下data_shift[7:1] 移位操作是在negedge clk 进行、data_shift[0] 是在posedge sck 进行。
所以说,要在IDLE状态下fifo_rdata_val 有效时的negedge sck 处data_shift[7:1] <= fifo_rdata[7:1]; 、posedge sck 处data_shift[0] <= fifo_rdata[0];
移位的问题完事了,那么怎么判断传输结束呢?
还得是计数器,表示有多少位数据没有驱动出去,但是计数器也得是下降沿触发。但是状态转换是在上升沿啊。所以要在计数到0的第一个上升沿立马退出TRANS,但计数器重置8就得在下一个状态的下降沿重置。
(CPOL ^ CPHA == 1)时
即对应{CPOL,CPHA} == 2'b10 || 2'b01; 时,此时是sck下降沿采样miso至移位寄存器、sck上升沿驱动mosi,大端模式
此时移位寄存器内的数据已经准备好,并且是在sck上升沿处驱动csn_o 的某位拉低,所以TRANS状态下第一个沿是下降沿,即先采样、再驱动。
先采样的话,data_shift 就需要额外1bit存储这个先采样来的值,如下图
如上图,移位寄存器data_shift 位宽为9,那么一定是negedge sck 时miso采样到data_shift[0] ,在posedge sck 时驱动data_shift[8] 至mosi,并data_shift << 1;
但是这种情况下就没有违背单沿触发要求,因为IDLE状态下fifo_rdata_val 有效时data_shift[8:1] <= fifo_rdata; 和TRANS状态下data_shift << 1 移位均在posedge sck 时刻
传输结束的过程也是使用计数器,如下图
2.4. FIFO_WRITE:写入FIFO
这是另一个异步FIFO,用于将Slave SPI读取的数据存入,以用户读出。但这里需要思考一个问题,这个数据一定要被用户读出吗?
例如我就是想给SPI发一个写地址和写数据,写入就行了,那Slave SPI发回的数据没有任何意义,这样的也要写入FIFO 嘛?
我思考的方法还是帧格式,如图UART那样,用1bit表示该数据是否要写入FIFO即可。
时序图的话就是简单的握手。
最终得到状态转换图如下
2.4. 代码
module spi_master#(
parameter CHIP_SEL_NUM = 3,
parameter DATA_WIDTH = 16,
parameter ASYNC_FIFO_WIDTH = 4096,
parameter CPOL = 0,
parameter CPHA = 1
)(
input rstn,
input clk,
input [DATA_WIDTH-1:0] wr_data,
input wr_data_val,
input rd_req,
output [DATA_WIDTH-1:0] rd_data,
output rd_data_val,
output ready,
input sck,
output mosi,
input miso,
input [CHIP_SEL_NUM-1:0] csn_i,
output [CHIP_SEL_NUM-1:0] csn_o
);
async_fifo#(
.DATA_WIDTH (DATA_WIDTH ),
.ASYNC_FIFO_WIDTH (ASYNC_FIFO_WIDTH)
)tx_async_fifo(
.rstn ( ),
.wclk (sck ),
.wdata ( ),
.wr_en ( ),
.full ( ),
.rclk ( ),
.rd_en ( ),
.rdata ( ),
.valid ( ),
.empty ( )
);
async_fifo#(
.DATA_WIDTH (DATA_WIDTH ),
.ASYNC_FIFO_WIDTH (ASYNC_FIFO_WIDTH)
)rx_async_fifo(
.rstn ( ),
.wclk (sck ),
.wdata ( ),
.wr_en ( ),
.full ( ),
.rclk ( ),
.rd_en ( ),
.rdata ( ),
.valid ( ),
.empty ( )
);
endmodule
3. spi_slave
4. spi
5. spi_tb
|