目录
SPI介绍
SPI 协议的物理层
SPI引脚功能介绍
SPI 协议层
CPOL/CPHA及通讯模式的配置
SPI 通讯模式时序图:
SPI的波形变化的原因
SPI应用场景
1.FPGA与存储类芯片间通信
2.FPGA与FPGA芯片相互通信
3.FPGA与单片机间通信
面试题
练习1:SPI发送8bit模块
绘制模块框图及波形图
编写模块代码
编写仿真代码
仿真验证
练习2:使用SPI连续发送多Byte
绘制模块框图
编写模块代码
编写仿真代码
仿真验证
SPI介绍
SPI(Serial Peripheral Interface,串行外围设备接口)通讯协议,是一种同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输,广泛用于EEPROM、Flash、RTC(实时时钟)、ADC(数模转换 器)、DSP(数字信号处理器)以及数字信号解码器上,是常用的也是较为重要的通讯协议之一。SPI本质是将数据转化为波形发送出去,其他通信协议也是一样操作。
SPI优缺点:SPI 通讯协议的优点是支持全双工通信,通讯方式较为简单,且相对数据传输速率较 快;缺点是没有指定的流控制,没有应答机制确认数据是否接收,与 IIC 总线通讯协议相 比,在数据可靠性上有一定缺陷。
SPI 协议的物理层
SPI 通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分,根据从机设备的个 数,SPI通讯设备之间的连接方式可分为一主一从和一主多从。
SPI引脚功能介绍
SCK:串行时钟线,由主机产生,同步数据传输,速率受限于主从设备中较慢的一方。
MOSI: 主输出从输入,主机通过此线发送数据给从机。主机发送数据,从机接收数据。
MISO: 主输入从输出,从机通过此线发送数据给主机。主机接收数据,从机发送数据
CS_N: 片选信号,低电平有效,用于选中特定从设备。多个从设备共享SCK、MOSI、MISO,但每个从设备有独立的CS_N引脚,主机通过拉低对应CS_N来寻址,拉高则结束通讯。一个设备对应一个片选信号。
一主一从SPI通讯设备连接图:
一主多从SPI通讯设备连接图:
SPI 协议层
CPOL/CPHA及通讯模式的配置
SPI 通讯协议一共有四种通讯模式,模式 0、模式1、模式2以及模式3,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了空闲状态(CS_N为高电平,设备未被选中)时SCK时钟信号的电平状态(先低后高/先高后低),CPHA规定了数据采样是在SCK时钟的奇数边沿还是偶数边沿。
SPI 通讯模式时序图:
注意事项:主机发送数据,通过SPI通信传输数据,从机接收数据,那么时钟线的配置模式选择,需要查看从机芯片的数据手册进行配置时钟线的模式配置。
SPI的波形变化的原因
1.根据时钟的上升沿还是下降沿进行发送/接收数据,也就是改变时钟线的极性和相位,进行配置时钟线的模式,会改变SPI的波形。
2.根据发送/接收的数据bit位数,一般可能是8bit,16bit,24bit,也可能有其他的非标准的自定义的通信协议,片选信号拉低,一次性发送上百个bit数据。
SPI应用场景
1.FPGA与存储类芯片间通信
利用SPI配置芯片寄存器,非标准SPI
片选拉低,传输16个bit数据。第一bit代表是读操作还是写操作,然后第2个bit到第8个bit是寄存器的地址数据(高位先发),最后是写入寄存器里的值是什么(第9个bit到第16个bit)
2.FPGA与FPGA芯片相互通信
FPGA间可使用SPI通信协议进行相互通信。为什么使用SPI呢?使用串口当然也可以传输数据,但是传输速率太慢,我们的fpga的工作速度是很快的,使用SPI可以使用时钟线快速传输数据。
3.FPGA与单片机间通信
面试题
练习1:SPI发送8bit模块
要求:使用SPI通信协议,将8bit的并行数据转化为单bit的串行数据,SPI的SCK使用模式0,SCK的时钟频率为5Mhz,系统时钟为50Mhz,完成发送8bit数据。
绘制模块框图及波形图
编写模块代码
顶层模块SPI_send_8_top
module SPI_send_8_top( input wire sys_clk_50Mhz, input wire sys_rst_n , input wire [7:0] spi_wr_data , input wire data_valid , output wire CS_N ,//片选信号 output wire SCK ,//串行时钟线SPI模式0 output wire MOSI ,//并行数据转为串行数据发送 output wire done //发送数据完成标志信号 ); wire div_clk ; wire rst_n ; div #( .CNT_MAX ( 4'd9 ) ) div_inst ( .clk (sys_clk_50Mhz), .rst_n (sys_rst_n ), .clk_div10 (div_clk ), .locked (rst_n ) ); SPI_send_8 SPI_send_8_inst( .div_clk (div_clk ), .rst_n (rst_n ), .spi_wr_data (spi_wr_data ), .data_valid (data_valid ), .CS_N (CS_N ), .SCK (SCK ), .MOSI (MOSI ), .done (done ) ); endmodule分频器模块div
module div #( parameter CNT_MAX = 4'd9 ) ( input wire clk , input wire rst_n , output reg clk_div10 , output reg locked ); reg [3:0] cnt; //cnt:0-9循环计数器 always @ (posedge clk or negedge rst_n) if(!rst_n) cnt <= 4'd0; else if(cnt == CNT_MAX) cnt <= 4'd0; else cnt <= cnt + 1'b1; always @ (posedge clk or negedge rst_n) if(!rst_n) clk_div10 <= 1'b0; else if(cnt == 4'd4) clk_div10 <= 1'b1; else if(cnt == CNT_MAX) clk_div10 <= 1'b0; else clk_div10 <= clk_div10; //locked always @ (posedge clk or negedge rst_n) if(!rst_n) locked <= 1'b0; else if(cnt == CNT_MAX && clk_div10) locked <= 1'b1; else locked <= locked; endmodule模块SPI_send_8
module SPI_send_8( input wire div_clk ,//5Mhz input wire rst_n , input wire [7:0] spi_wr_data ,//并行数据 input wire data_valid ,//并行数据有效标志信号 output reg CS_N ,//片选信号 output wire SCK ,//串行时钟线SPI模式0 output reg MOSI ,//并行数据转为串行数据发送 output reg done //发送数据完成标志信号 ); localparam CNT_MAX = 4'd8;//发送8bit localparam idle = 4'd0; localparam s0 = 4'd1; reg [3:0] cnt ; reg [3:0] state ; always@(posedge div_clk or negedge rst_n) begin if(!rst_n) begin CS_N <= 1'b1; MOSI <= 1'b0; done <= 1'b0; cnt <= 4'd0; state <= idle; end else case(state) idle: begin if(data_valid) begin state <= s0; CS_N <= 1'b0; cnt <= 4'd0; MOSI <= spi_wr_data[7]; end else begin CS_N <= 1'b1; MOSI <= 1'b0; done <= 1'b0; cnt <= 4'd0; state <= idle; end end s0 : begin if(cnt == CNT_MAX - 1'b1) begin state <= idle; done <= 1'b1; cnt <= 4'd0; MOSI <= 1'b0; CS_N <= 1'b1; end else begin state <= state; done <= 1'b0; cnt <= cnt + 1'b1; MOSI <= spi_wr_data[4'd7-cnt-1'b1]; CS_N <= 1'b0; end end default: begin CS_N <= 1'b1; MOSI <= 1'b0; done <= 1'b0; cnt <= 4'd0; state <= idle; end endcase end //SCK assign SCK = (!CS_N) ? (~div_clk) : 1'b0; endmodule编写仿真代码
`timescale 1ns/1ps module SPI_send_8_top_tb(); reg sys_clk_50Mhz; reg sys_rst_n ; reg [7:0] spi_wr_data ; reg data_valid ; initial begin sys_clk_50Mhz = 1'b0; sys_rst_n = 1'b0; spi_wr_data = 8'd0; data_valid = 1'b0; #123 sys_rst_n = 1'b1; #1000 spi_wr_data = {$random} % 256; #200 data_valid = 1'b1; #200 data_valid = 1'b0; #2000 spi_wr_data = {$random} % 256; #200 data_valid = 1'b1; #200 data_valid = 1'b0; #2000 spi_wr_data = {$random} % 256; #200 data_valid = 1'b1; #200 data_valid = 1'b0; #2000; end always #10 sys_clk_50Mhz = ~sys_clk_50Mhz; SPI_send_8_top SPI_send_8_top_inst( .sys_clk_50Mhz(sys_clk_50Mhz), .sys_rst_n (sys_rst_n ), .spi_wr_data (spi_wr_data ), .data_valid (data_valid ), .CS_N (),//片选信号 .SCK (),//串行时钟线SPI模式0 .MOSI (),//并行数据转为串行数据发送 .done () //发送数据完成标志信号 ); endmodule仿真验证
可以看出10分频的分频器没有问题。
CS_N,SCK,MOSI,done这四个输出信号也没有问题,说明仿真验证通过。
练习2:使用SPI连续发送多Byte
要求:使用SPI通信协议,将8bit的并行数据转化为单bit的串行数据,SPI的SCK使用模式0,SCK的时钟频率为5Mhz,系统时钟为50Mhz,使用SPI_send_8_top,连续发送多字节,例如128bit。
绘制模块框图
这里利用了串口多byte发送的思想。直接使用之前的代码,但是需要考虑状态机的时钟频率。避免跨时域问题。
编写模块代码
顶层模块SPI_send_duo_byte
module SPI_send_duo_byte( input wire sys_clk_50Mhz, input wire sys_rst_n , output wire CS_N ,//片选信号 output wire SCK ,//串行时钟线SPI模式0 output wire MOSI //并行数据转为串行数据发送 ); wire div_clk; wire rst_n ; wire done ; wire [7:0] pi_data; wire pi_flag; fsm fsm_inst( .clk (div_clk ) ,//5Mhz .rst_n (rst_n ) , .send_done (done ) ,//控制串口发送信号线 .pi_flag (pi_flag ) , .pi_data (pi_data ) ); SPI_send_8_top SPI_send_8_top_inst( .sys_clk_50Mhz(sys_clk_50Mhz), .sys_rst_n (sys_rst_n ), .spi_wr_data (pi_data ), .data_valid (pi_flag ), .CS_N (CS_N ),//片选信号 .SCK (SCK ),//串行时钟线SPI模式0 .MOSI (MOSI ),//并行数据转为串行数据发送 .done (done ),//发送数据完成标志信号 .div_clk (div_clk ), .rst_n (rst_n ) ); endmodule模块fsm
module fsm( input wire clk ,//5Mhz input wire rst_n , input wire send_done ,//控制串口发送信号线 output reg pi_flag , output reg [7:0] pi_data ); //定义一个数组,存放8个字节需要发送的数据 wire [7:0] mem [0:7]; assign mem[0] = 8'h26;//年 assign mem[1] = 8'h07;//月 assign mem[2] = 8'h01; assign mem[3] = 8'h14; assign mem[4] = 8'h15; assign mem[5] = 8'h16; assign mem[6] = 8'h17; assign mem[7] = 8'h18; parameter idle = 8'd0; parameter s0 = 8'd1; parameter s1 = 8'd2; parameter s2 = 8'd3; parameter s3 = 8'd4; reg [7:0] state; //内部变量的定义 reg [31:0] delay_cnt; reg [7:0] send_done_cnt; reg [7:0] index; parameter TIME_1S = 32'd5_000_000; //parameter TIME_1S = 32'd50_000;//用于仿真测试,缩短仿真时间 parameter BYTE_NUM = 32'd8 ; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= idle; pi_flag <= 1'b0; pi_data <= 8'd0; end else begin case(state) idle: begin if(delay_cnt == TIME_1S) begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; pi_flag <= 1'b0; pi_data <= 8'd0; index <= 8'd0; state <= s0 ; end else begin state <= state; delay_cnt <= delay_cnt + 1'b1; end end s0 : begin pi_flag <= 1'b1; pi_data <= mem[index]; state <= s1; end s1 : begin pi_flag <= 1'b0; if(send_done) begin state <= s2; send_done_cnt <= send_done_cnt + 1'b1; index <= index + 1'b1; end else begin state <= state; send_done_cnt <= send_done_cnt; index <= index; end end s2 : begin if(send_done_cnt == BYTE_NUM) state <= s3; else state <= s0; end s3 : begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= state; pi_flag <= 1'b0; pi_data <= 8'd0; end default: begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= state; pi_flag <= 1'b0; pi_data <= 8'd0; end endcase end end endmodule模块SPI_send_8_top
module SPI_send_8_top( input wire sys_clk_50Mhz, input wire sys_rst_n , input wire [7:0] spi_wr_data , input wire data_valid , output wire CS_N ,//片选信号 output wire SCK ,//串行时钟线SPI模式0 output wire MOSI ,//并行数据转为串行数据发送 output wire done ,//发送数据完成标志信号 output wire div_clk , output wire rst_n ); div #( .CNT_MAX ( 4'd9 ) ) div_inst ( .clk (sys_clk_50Mhz), .rst_n (sys_rst_n ), .clk_div10 (div_clk ), .locked (rst_n ) ); SPI_send_8 SPI_send_8_inst( .div_clk (div_clk ), .rst_n (rst_n ), .spi_wr_data (spi_wr_data ), .data_valid (data_valid ), .CS_N (CS_N ), .SCK (SCK ), .MOSI (MOSI ), .done (done ) ); endmodule编写仿真代码
`timescale 1ns/1ps module SPI_send_duo_byte_tb(); reg sys_clk_50Mhz; reg sys_rst_n ; initial begin sys_clk_50Mhz = 1'b0; sys_rst_n = 1'b0; #123 sys_rst_n = 1'b1; end always #10 sys_clk_50Mhz = ~sys_clk_50Mhz; defparam SPI_send_duo_byte_inst.fsm_inst.TIME_1S = 32'd5_000;//1ms SPI_send_duo_byte SPI_send_duo_byte_inst( .sys_clk_50Mhz(sys_clk_50Mhz), .sys_rst_n (sys_rst_n ), .CS_N (),//片选信号 .SCK (),//串行时钟线SPI模式0 .MOSI () //并行数据转为串行数据发送 ); endmodule仿真验证
仿真验证通过。
基于FPGA的配置电路-SPI_FLASH
图中的这几个引脚,连接在FPGA的特殊引脚上,可以让FPGA上电之后,主动通过SPI协议去读取这个芯片,一般用来存储我们的固化程序。
存储芯片W25Q16JV
它的接口类型为SPI,W25Q16JV串行flash存储器的框图
由图可知,该芯片的存储空间32块(Block),每一块大小为64KB。每一块被分为16个扇区(Sector),每一个扇区大小为4KB。
该芯片的全擦除指令介绍:在执行全擦除指令之前,需要先执行写使能指令。
练习3:基于SPI的FLASH驱动控制
实验目标:事先向Flash 芯片中烧录流水灯程序,FPGA上电执行流水灯程序,下载 Flash芯片全 擦除程序到FPGA内部 SRAM并执行,擦除 Flash芯片中烧录的流水灯程序,FPGA重新 上电后,无程序执行。
注意:flash执行全擦除指令(8'hc7),需要先执行写使能指令(8'h06),两指令间隔时间必须大于100ns。所以使用SPI协议先发送8'h06,然后再发送8'hc7,即可以实现使用SPI协议全擦除FLASH。
绘制模块框图
由图可以知晓,按键消抖模块的时钟频率为50Mhz,状态机的时钟频率为5Mhz,所以这里涉及到了单比特跨时域问题,由快时域->慢时域,在慢时域下容易漏采/采不到快时域的单脉冲标志信号,我们可以使用握手机制,将快时域的单脉冲标志信号的持续时间拉长至慢时域时钟上升沿可以采到为止再将其拉底。
编写模块代码
顶层模块key_ctrl_SPI_send_duo_byte
module key_ctrl_SPI_send_duo_byte( input wire sys_clk_50Mhz, input wire sys_rst_n , input wire key_in , output wire CS_N ,//片选信号 output wire SCK ,//串行时钟线SPI模式0 output wire MOSI //并行数据转为串行数据发送 ); wire div_clk ; wire rst_n ; wire po_flag; wire pi_flag; wire [7:0] pi_data; wire key_flag; wire done; div div_inst ( .clk (sys_clk_50Mhz) , .rst_n (sys_rst_n ) , .clk_div10 (div_clk ) , .locked (rst_n ) ); key_XD key_XD_inst ( .clk (sys_clk_50Mhz),//50Mhz .rst_n (sys_rst_n ), .key_in (key_in ), .pi_flag (po_flag ),//用于解决单比特快时域->慢时域的问题(握手机制) .key_flag(key_flag ) ); fsm fsm_inst( .clk (div_clk ) ,//5Mhz .rst_n (rst_n ) , .send_done (done ) ,//控制串口发送信号线 .key_flag (key_flag) , .po_flag (po_flag ) ,//用于解决单比特快时域->慢时域的问题(握手机制) .pi_flag (pi_flag ) , .pi_data (pi_data ) ); SPI_send_8 SPI_send_8_inst( .div_clk (div_clk ),//5Mhz .rst_n (rst_n ), .spi_wr_data(pi_data ),//并行数据 .data_valid (pi_flag ),//并行数据有效标志信号 .CS_N (CS_N ),//片选信号 .SCK (SCK ),//串行时钟线SPI模式0 .MOSI (MOSI ),//并行数据转为串行数据发送 .done (done ) //发送数据完成标志信号 ); endmodule按键消抖模块key_XD
module key_XD #( parameter CNT_MAX = 32'd999_999 ) ( input wire clk ,//50Mhz input wire rst_n , input wire key_in , input wire pi_flag ,//用于解决单比特快时域->慢时域的问题(握手机制) output reg key_flag ); reg [31:0] cnt; //cnt:低电平计数器 always @ (posedge clk or negedge rst_n) if(!rst_n) cnt <= 32'd0; else if(key_in)//高电平 cnt <= 32'd0; else if(cnt == CNT_MAX)//低电平且计数满 cnt <= cnt; else//低电平且计数未满 cnt <= cnt + 1'b1; //key_flag:当计数满20ms后产生按键有效标志位 //且key_flag在999_998时拉高,维持一个时钟的高电平 always @ (posedge clk or negedge rst_n) if(!rst_n) key_flag <= 1'b0; else if(pi_flag) key_flag <= 1'b0; else if(cnt == CNT_MAX - 1'b1) key_flag <= 1'b1; else key_flag <= key_flag; endmodule模块fsm
module fsm( input wire clk ,//5Mhz input wire rst_n , input wire send_done ,//控制串口发送信号线 input wire key_flag , output reg po_flag ,//用于解决单比特快时域->慢时域的问题(握手机制) output reg pi_flag , output reg [7:0] pi_data ); //定义一个数组,存放8个字节需要发送的数据 wire [7:0] mem [0:7]; assign mem[0] = 8'h06;//写使能指令 assign mem[1] = 8'hc7;//全擦除指令 parameter idle = 8'd0; parameter s0 = 8'd1; parameter s1 = 8'd2; parameter s2 = 8'd3; parameter s3 = 8'd4; reg [7:0] state; //内部变量的定义 reg [31:0] delay_cnt; reg [7:0] send_done_cnt; reg [7:0] index; parameter TIME_1S = 32'd5_000_000; //parameter TIME_1S = 32'd50_000;//用于仿真测试,缩短仿真时间 parameter BYTE_NUM = 32'd2 ;//发送字节个数 always@(posedge clk or negedge rst_n) begin if(!rst_n) begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= idle; pi_flag <= 1'b0; pi_data <= 8'd0; end else begin case(state) idle: begin if(key_flag)//delay_cnt == TIME_1S begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; pi_flag <= 1'b0; pi_data <= 8'd0; index <= 8'd0; state <= s0 ; end else begin state <= state; delay_cnt <= delay_cnt + 1'b1; end end s0 : begin pi_flag <= 1'b1; pi_data <= mem[index]; state <= s1; end s1 : begin pi_flag <= 1'b0; if(send_done) begin state <= s2; send_done_cnt <= send_done_cnt + 1'b1; index <= index + 1'b1; end else begin state <= state; send_done_cnt <= send_done_cnt; index <= index; end end s2 : begin if(send_done_cnt == BYTE_NUM) state <= s3; else state <= s0; end s3 : begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= idle; pi_flag <= 1'b0; pi_data <= 8'd0; end default: begin delay_cnt <= 32'd0; send_done_cnt <= 8'd0; index <= 8'd0; state <= idle; pi_flag <= 1'b0; pi_data <= 8'd0; end endcase end end //po_flag always@(posedge clk or negedge rst_n) begin if(!rst_n) po_flag <= 1'b0; else if(key_flag)//检测到key_flag有效拉高 po_flag <= 1'b1; else po_flag <= 1'b0; end endmodule其余两个模块代码和上面的一样。
编写仿真代码
`timescale 1ns/1ps module key_ctrl_SPI_send_duo_byte_tb(); reg sys_clk_50Mhz; reg sys_rst_n ; reg key_in ; initial begin sys_clk_50Mhz = 1'b0; sys_rst_n = 1'b0; key_in = 1'b1; #123 sys_rst_n = 1'b1; #10000 key_in = 1'b0; #50000 key_in = 1'b1; end always #10 sys_clk_50Mhz = ~sys_clk_50Mhz; defparam key_ctrl_SPI_send_duo_byte_inst.fsm_inst.TIME_1S = 32'd5_000;//1ms defparam key_ctrl_SPI_send_duo_byte_inst.key_XD_inst.CNT_MAX = 32'd999;//20us key_ctrl_SPI_send_duo_byte key_ctrl_SPI_send_duo_byte_inst( .sys_clk_50Mhz(sys_clk_50Mhz), .sys_rst_n (sys_rst_n ), .key_in (key_in ), .CS_N (),//片选信号 .SCK (),//串行时钟线SPI模式0 .MOSI () //并行数据转为串行数据发送 ); endmodule仿真验证
仿真验证通过。
上板验证注意事项
绑定引脚