SPI(Serial Peripheral interface,串行外围设备接口):
(1)简介:SPI由一个主设备(Master)和一个或多个从设备(Slave)组成。主设备通过启动与从设备的同步通信来交换数据。SPI的通信原理是以主从方式工作,一般有一个主设备和多个从设备,需要至少四根线,分别是MISO(主设备输入从设备输出)、MOSI(主设备输出从设备输入)、SCLK(时钟)、SS(片选)。当主设备要和某个从设备进行通信时,主设备需要先向对应从设备的片选线上发送使能信号(高电平或低电平)表示选中该从设备,这样通信才能开始。由于SPI使用引脚较少且布线方便,所以越来越多的芯片集成了这种通信协议。
(2)读写机制:主机发送一个数据必然接收一个数据,发送数据忽略接收数据(写din忽略dout),发送空字节接收数据(写0接收dout)
(3)步骤:
1.主设备发起信号,将片选SS拉低,选择从设备,启动通信。
2.主设备通过发送时钟信号,告诉从设备进行写数据或者读数据操作。具体的采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),具体取决于SPI的模式。
3.主设备将要发送的数据写到发送数据缓存区,然后通过MOSI信号线一位一位地将数据移出去传送给从设备。同时,MISO接口接收到的数据经过移位寄存器一位一位地移到接收缓存区。
4.如果需要响应,从设备可以通过MISO线路将数据返回给主设备。
(4)确定采样边沿:
1.时钟极性(CPOL)定义了时钟空闲状态电平(先确认有效状态是高电平还是低电平):
CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平。
CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平。
2.时钟相位(CPHA)定义数据的采集时间(后确认采样边沿和发送边沿):
CPHA=0,在时钟有效态的第一个跳变沿进行数据采样。在第二个边沿改变数据。
CPHA=1,在时钟有效态的第二个跳变沿进行数据采样。在第一个边沿改变数据。
(5)模块:
spi_master模块:生成spi时钟、提取数据位宽、三段式状态机、4-16译码器、移位寄存器
`timescale 1ns/1ns
module spi_master#(
parameter DATA_WIDTH = 8,
parameter SLAVE_COUNT = 15,
parameter CPOL = 0,
parameter CPHA = 0
)(
input i_clk,
input i_rst_n,
input i_miso,
input i_valid,
input i_ready,
input [DATA_WIDTH-1:0] i_din,
input [log2(SLAVE_COUNT-1)-1:0] i_ss_n,
output o_mosi,
output o_finish,
output [DATA_WIDTH-1:0] o_dout,
output [SLAVE_COUNT-1:0] o_ss_n,
output o_run,
output o_spi_clk
);
/*********************************reg*********************************/
reg r_spi_cnt;
reg [log2(DATA_WIDTH-1)-1:0] r_data_cnt;
reg [DATA_WIDTH-1:0] r_din;
reg [DATA_WIDTH-1:0] r_dout;
reg [log2(SLAVE_COUNT-1)-1:0] r_ss_n;
reg r_run;
reg [1:0] cur_state;
reg [1:0] next_state;
reg r_st_done;
reg ro_finish;
reg [SLAVE_COUNT-1:0] ro_ss_n;
reg [DATA_WIDTH-1:0] ro_dout;
/********************************wire********************************/
wire w_start;
/*******************************function*******************************/
//the function to get the width of data
function integer log2(input integer v);
begin
log2=0;
while(v>>log2)
log2=log2+1;
end
endfunction
/******************************parameter******************************/
parameter IDLE = 0;
parameter LOAD = 1;
parameter SHIFT = 2;
parameter DONE = 3;
/*********************************port*********************************/
/*******************************machine*******************************/
always@(posedge o_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
cur_state <= IDLE;
else
cur_state <= next_state;
always@(*)begin
next_state = IDLE;
case(cur_state)
IDLE:
if(w_start)
next_state = LOAD;
else
next_state = IDLE;
LOAD:
next_state = SHIFT;
SHIFT:
if(r_st_done)
next_state = DONE;
else
next_state = SHIFT;
DONE:
next_state = IDLE;
endcase
end
always@(posedge o_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)begin
r_st_done <= 1'b0;
ro_finish <= 1'b0;
r_ss_n <= 'b1;
r_din <= 'b0;
r_run <= 1'b0;
end
else
case(cur_state)
IDLE:begin
r_st_done <= 1'b0;
ro_finish <= 1'b0;
r_ss_n <= 'b1;
r_din <= 'b0;
r_run <= 1'b0;
end
LOAD:begin
r_ss_n <= i_ss_n;
r_din <= i_din;
r_run <= 1'b1;
end
SHIFT:
//计数器为7时还没操作完成,还要再做一次移位操作
if(r_data_cnt==7 && r_run)begin
r_st_done <= 1'b1;
r_run <= 1'b0;
r_din <= {r_din[6:0],i_miso};
end
else if(r_data_cnt!=7 && r_run)begin
r_st_done <= 1'b0;
r_run <= 1'b1;
r_din <= {r_din[6:0],i_miso};
end
else begin
r_st_done <= r_st_done;
r_run <= r_run;
r_din <= r_din;
end
DONE:begin
r_st_done <= 1'b0;
ro_finish <= 1'b1;
r_ss_n <= 'b1;
r_dout <= r_din;
r_run <= 1'b0;
end
default:begin
r_st_done <= 1'b0;
ro_finish <= 1'b0;
r_ss_n <= 'b1;
r_run <= 1'b0;
end
endcase
/*****************************component*****************************/
/*******************************assign*******************************/
assign o_spi_clk = r_spi_cnt ? 1'b1 : 1'b0;
assign w_start = i_valid && i_ready;
assign o_mosi = r_run ? r_din[7]:1'b0;
assign o_dout = ro_dout;
assign o_ss_n = ro_ss_n;
assign o_finish= ro_finish;
assign o_run = r_run;
/*******************************always*******************************/
always@(posedge i_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
r_spi_cnt <= 1'b0;
else
r_spi_cnt <= r_spi_cnt + 1'b1;
//4-16译码器
always@(*)
case(r_ss_n)
4'b0000:ro_ss_n=16'b1111_1111_1111_1110;
4'b0001:ro_ss_n=16'b1111_1111_1111_1101;
4'b0010:ro_ss_n=16'b1111_1111_1111_1011;
4'b0011:ro_ss_n=16'b1111_1111_1111_0111;
4'b0100:ro_ss_n=16'b1111_1111_1110_1111;
4'b0101:ro_ss_n=16'b1111_1111_1101_1111;
4'b0110:ro_ss_n=16'b1111_1111_1011_1111;
4'b0111:ro_ss_n=16'b1111_1111_0111_1111;
4'b1000:ro_ss_n=16'b1111_1110_1111_1111;
4'b1001:ro_ss_n=16'b1111_1101_1111_1111;
4'b1010:ro_ss_n=16'b1111_1011_1111_1111;
4'b1011:ro_ss_n=16'b1111_0111_1111_1111;
4'b1100:ro_ss_n=16'b1110_1111_1111_1111;
4'b1101:ro_ss_n=16'b1101_1111_1111_1111;
4'b1110:ro_ss_n=16'b1011_1111_1111_1111;
4'b1111:ro_ss_n=16'b1111_1111_1111_1111;
default:ro_ss_n=16'b1111_1111_1111_1111;
endcase
always@(posedge o_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
r_data_cnt <= 'b0;
else
if(r_run)
if(r_data_cnt==7)
r_data_cnt <= 'b0;
else
r_data_cnt <= r_data_cnt + 1'b1;
else
r_data_cnt <= 'b0;
always@(posedge o_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
r_dout <= 'b0;
else
if(ro_finish)
ro_dout <= r_dout;
else
ro_dout <= 'b0;
endmodule
spi_slave模块:略
module spi_slave#(
parameter DATA_WIDTH = 8 ,
parameter SLAVE_NUM = 1 ,
parameter CPOL = 0 ,
parameter CPHA = 0
)(
input i_spi_clk ,
input i_rst_n ,
input i_ss_n ,
input i_mosi ,
input i_run ,
output o_miso
);
/*********************************reg*********************************/
reg [log2(DATA_WIDTH-1)-1:0] r_data_cnt ;
reg [DATA_WIDTH-1 : 0] r_dout ;
/********************************wire********************************/
/*******************************function*******************************/
//the function to get the width of data
function integer log2(input integer v);
begin
log2=0;
while(v>>log2)
log2=log2+1;
end
endfunction
/******************************parameter******************************/
/*********************************port*********************************/
/*******************************machine*******************************/
/*****************************component*****************************/
/*******************************assign*******************************/
assign o_miso = r_dout[7];
/*******************************always*******************************/
always@(posedge i_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
r_data_cnt <= 'b0;
else
if(i_run && (!i_ss_n))
if(r_data_cnt==7)
r_data_cnt <= 'b0;
else
r_data_cnt <= r_data_cnt + 1'b1;
else
r_data_cnt <= 'b0;
always@(posedge i_spi_clk or negedge i_rst_n)
if(i_rst_n == 1'b0)
r_dout <= 'b0;
else
if(i_run && (!i_ss_n))
r_dout = {r_dout[6:0],i_mosi};
else
r_dout <= r_dout;
endmodule
(6)学到的知识:
FPGA开发流程:了解项目需求功能→大致框图(模块的作用、模块之间的数据流、visio)→时序图(wavedorm、timegen)→状态图(如果需要、visio)。误区1:信号没定义全、框图不完整、时序图不完整,不敢下手写。把功能了解清楚,就可以开始写了,写到某些模块有思路了再进行补充。误区2:信号随便定义。信号定义的好坏对顶层模块的分析会有很大影响。
SPI协议的优点包括:
全双工串行通信,高速数据传输速率,简单的软件配置,及其灵活的数据传输,不限于8位,它可以是任意大小的字,非常简单的硬件结构,从站不需要唯一地址(与IIC不同),从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同),不需要收发器(与CAN不同)。
SPI协议的缺点包括:
没有硬件从机应答信号(主机可能在不知情的情况下无处发送)。
通常仅支持一个主设备。
与RS-232与CAN总线相比,只能支持非常短的距离。
SPI从机设备没有硬件流控,只能通过主机自主地延迟下个时钟周期到来的时间。