基于FPGA的SPI通信-强化篇

📅 2026/7/10 20:53:47
基于FPGA的SPI通信-强化篇
目录SPI介绍SPI 协议的物理层SPI引脚功能介绍SPI 协议层CPOL/CPHA及通讯模式的配置SPI 通讯模式时序图SPI的波形变化的原因SPI应用场景1.FPGA与存储类芯片间通信2.FPGA与FPGA芯片相互通信3.FPGA与单片机间通信面试题练习1SPI发送8bit模块绘制模块框图及波形图编写模块代码编写仿真代码仿真验证练习2使用SPI连续发送多Byte绘制模块框图编写模块代码编写仿真代码仿真验证SPI介绍SPISerial 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种模式分别由时钟极性(CPOLClock Polarity)和时钟相位(CPHAClock Phase)来定义其中CPOL参数规定了空闲状态(CS_N为高电平设备未被选中)时SCK时钟信号的电平状态(先低后高/先高后低)CPHA规定了数据采样是在SCK时钟的奇数边沿还是偶数边沿。SPI 通讯模式时序图注意事项主机发送数据通过SPI通信传输数据从机接收数据那么时钟线的配置模式选择需要查看从机芯片的数据手册进行配置时钟线的模式配置。SPI的波形变化的原因1.根据时钟的上升沿还是下降沿进行发送/接收数据也就是改变时钟线的极性和相位进行配置时钟线的模式会改变SPI的波形。2.根据发送/接收的数据bit位数一般可能是8bit16bit24bit也可能有其他的非标准的自定义的通信协议片选信号拉低一次性发送上百个bit数据。SPI应用场景1.FPGA与存储类芯片间通信利用SPI配置芯片寄存器非标准SPI片选拉低传输16个bit数据。第一bit代表是读操作还是写操作然后第2个bit到第8个bit是寄存器的地址数据(高位先发)最后是写入寄存器里的值是什么第9个bit到第16个bit2.FPGA与FPGA芯片相互通信FPGA间可使用SPI通信协议进行相互通信。为什么使用SPI呢使用串口当然也可以传输数据但是传输速率太慢我们的fpga的工作速度是很快的使用SPI可以使用时钟线快速传输数据。3.FPGA与单片机间通信面试题练习1SPI发送8bit模块要求使用SPI通信协议将8bit的并行数据转化为单bit的串行数据SPI的SCK使用模式0SCK的时钟频率为5Mhz系统时钟为50Mhz完成发送8bit数据。绘制模块框图及波形图编写模块代码顶层模块SPI_send_8_topmodule 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 ( 4d9 ) ) 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分频器模块divmodule div #( parameter CNT_MAX 4d9 ) ( 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 4d0; else if(cnt CNT_MAX) cnt 4d0; else cnt cnt 1b1; always (posedge clk or negedge rst_n) if(!rst_n) clk_div10 1b0; else if(cnt 4d4) clk_div10 1b1; else if(cnt CNT_MAX) clk_div10 1b0; else clk_div10 clk_div10; //locked always (posedge clk or negedge rst_n) if(!rst_n) locked 1b0; else if(cnt CNT_MAX clk_div10) locked 1b1; else locked locked; endmodule模块SPI_send_8module 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 4d8;//发送8bit localparam idle 4d0; localparam s0 4d1; reg [3:0] cnt ; reg [3:0] state ; always(posedge div_clk or negedge rst_n) begin if(!rst_n) begin CS_N 1b1; MOSI 1b0; done 1b0; cnt 4d0; state idle; end else case(state) idle: begin if(data_valid) begin state s0; CS_N 1b0; cnt 4d0; MOSI spi_wr_data[7]; end else begin CS_N 1b1; MOSI 1b0; done 1b0; cnt 4d0; state idle; end end s0 : begin if(cnt CNT_MAX - 1b1) begin state idle; done 1b1; cnt 4d0; MOSI 1b0; CS_N 1b1; end else begin state state; done 1b0; cnt cnt 1b1; MOSI spi_wr_data[4d7-cnt-1b1]; CS_N 1b0; end end default: begin CS_N 1b1; MOSI 1b0; done 1b0; cnt 4d0; state idle; end endcase end //SCK assign SCK (!CS_N) ? (~div_clk) : 1b0; 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 1b0; sys_rst_n 1b0; spi_wr_data 8d0; data_valid 1b0; #123 sys_rst_n 1b1; #1000 spi_wr_data {$random} % 256; #200 data_valid 1b1; #200 data_valid 1b0; #2000 spi_wr_data {$random} % 256; #200 data_valid 1b1; #200 data_valid 1b0; #2000 spi_wr_data {$random} % 256; #200 data_valid 1b1; #200 data_valid 1b0; #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使用模式0SCK的时钟频率为5Mhz系统时钟为50Mhz使用SPI_send_8_top,连续发送多字节例如128bit。绘制模块框图这里利用了串口多byte发送的思想。直接使用之前的代码但是需要考虑状态机的时钟频率。避免跨时域问题。编写模块代码顶层模块SPI_send_duo_bytemodule 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模块fsmmodule 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] 8h26;//年 assign mem[1] 8h07;//月 assign mem[2] 8h01; assign mem[3] 8h14; assign mem[4] 8h15; assign mem[5] 8h16; assign mem[6] 8h17; assign mem[7] 8h18; parameter idle 8d0; parameter s0 8d1; parameter s1 8d2; parameter s2 8d3; parameter s3 8d4; reg [7:0] state; //内部变量的定义 reg [31:0] delay_cnt; reg [7:0] send_done_cnt; reg [7:0] index; parameter TIME_1S 32d5_000_000; //parameter TIME_1S 32d50_000;//用于仿真测试,缩短仿真时间 parameter BYTE_NUM 32d8 ; always(posedge clk or negedge rst_n) begin if(!rst_n) begin delay_cnt 32d0; send_done_cnt 8d0; index 8d0; state idle; pi_flag 1b0; pi_data 8d0; end else begin case(state) idle: begin if(delay_cnt TIME_1S) begin delay_cnt 32d0; send_done_cnt 8d0; pi_flag 1b0; pi_data 8d0; index 8d0; state s0 ; end else begin state state; delay_cnt delay_cnt 1b1; end end s0 : begin pi_flag 1b1; pi_data mem[index]; state s1; end s1 : begin pi_flag 1b0; if(send_done) begin state s2; send_done_cnt send_done_cnt 1b1; index index 1b1; 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 32d0; send_done_cnt 8d0; index 8d0; state state; pi_flag 1b0; pi_data 8d0; end default: begin delay_cnt 32d0; send_done_cnt 8d0; index 8d0; state state; pi_flag 1b0; pi_data 8d0; end endcase end end endmodule模块SPI_send_8_topmodule 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 ( 4d9 ) ) 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 1b0; sys_rst_n 1b0; #123 sys_rst_n 1b1; end always #10 sys_clk_50Mhz ~sys_clk_50Mhz; defparam SPI_send_duo_byte_inst.fsm_inst.TIME_1S 32d5_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它的接口类型为SPIW25Q16JV串行flash存储器的框图由图可知该芯片的存储空间32块(Block)每一块大小为64KB。每一块被分为16个扇区(Sector)每一个扇区大小为4KB。该芯片的全擦除指令介绍在执行全擦除指令之前需要先执行写使能指令。练习3基于SPI的FLASH驱动控制实验目标事先向Flash 芯片中烧录流水灯程序FPGA上电执行流水灯程序下载 Flash芯片全 擦除程序到FPGA内部 SRAM并执行擦除 Flash芯片中烧录的流水灯程序FPGA重新 上电后无程序执行。注意flash执行全擦除指令(8hc7)需要先执行写使能指令(8h06)两指令间隔时间必须大于100ns。所以使用SPI协议先发送8h06然后再发送8hc7即可以实现使用SPI协议全擦除FLASH。绘制模块框图由图可以知晓按键消抖模块的时钟频率为50Mhz状态机的时钟频率为5Mhz所以这里涉及到了单比特跨时域问题由快时域-慢时域在慢时域下容易漏采/采不到快时域的单脉冲标志信号我们可以使用握手机制将快时域的单脉冲标志信号的持续时间拉长至慢时域时钟上升沿可以采到为止再将其拉底。编写模块代码顶层模块key_ctrl_SPI_send_duo_bytemodule 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_XDmodule key_XD #( parameter CNT_MAX 32d999_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 32d0; else if(key_in)//高电平 cnt 32d0; else if(cnt CNT_MAX)//低电平且计数满 cnt cnt; else//低电平且计数未满 cnt cnt 1b1; //key_flag:当计数满20ms后产生按键有效标志位 //且key_flag在999_998时拉高,维持一个时钟的高电平 always (posedge clk or negedge rst_n) if(!rst_n) key_flag 1b0; else if(pi_flag) key_flag 1b0; else if(cnt CNT_MAX - 1b1) key_flag 1b1; else key_flag key_flag; endmodule模块fsmmodule 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] 8h06;//写使能指令 assign mem[1] 8hc7;//全擦除指令 parameter idle 8d0; parameter s0 8d1; parameter s1 8d2; parameter s2 8d3; parameter s3 8d4; reg [7:0] state; //内部变量的定义 reg [31:0] delay_cnt; reg [7:0] send_done_cnt; reg [7:0] index; parameter TIME_1S 32d5_000_000; //parameter TIME_1S 32d50_000;//用于仿真测试,缩短仿真时间 parameter BYTE_NUM 32d2 ;//发送字节个数 always(posedge clk or negedge rst_n) begin if(!rst_n) begin delay_cnt 32d0; send_done_cnt 8d0; index 8d0; state idle; pi_flag 1b0; pi_data 8d0; end else begin case(state) idle: begin if(key_flag)//delay_cnt TIME_1S begin delay_cnt 32d0; send_done_cnt 8d0; pi_flag 1b0; pi_data 8d0; index 8d0; state s0 ; end else begin state state; delay_cnt delay_cnt 1b1; end end s0 : begin pi_flag 1b1; pi_data mem[index]; state s1; end s1 : begin pi_flag 1b0; if(send_done) begin state s2; send_done_cnt send_done_cnt 1b1; index index 1b1; 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 32d0; send_done_cnt 8d0; index 8d0; state idle; pi_flag 1b0; pi_data 8d0; end default: begin delay_cnt 32d0; send_done_cnt 8d0; index 8d0; state idle; pi_flag 1b0; pi_data 8d0; end endcase end end //po_flag always(posedge clk or negedge rst_n) begin if(!rst_n) po_flag 1b0; else if(key_flag)//检测到key_flag有效拉高 po_flag 1b1; else po_flag 1b0; 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 1b0; sys_rst_n 1b0; key_in 1b1; #123 sys_rst_n 1b1; #10000 key_in 1b0; #50000 key_in 1b1; end always #10 sys_clk_50Mhz ~sys_clk_50Mhz; defparam key_ctrl_SPI_send_duo_byte_inst.fsm_inst.TIME_1S 32d5_000;//1ms defparam key_ctrl_SPI_send_duo_byte_inst.key_XD_inst.CNT_MAX 32d999;//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仿真验证仿真验证通过。上板验证注意事项绑定引脚