[X]关闭

[米联客-XILINX-H3_CZ08_7100] FPGA基础篇连载-14 SPI MASET发送程序设计

文档创建者:FPGA课程
浏览次数:229
最后更新:2024-08-26
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-FPGA部分 » 2_FPGA实验篇(仅旗舰) » 1-FPGA基础入门实验
本帖最后由 FPGA课程 于 2024-8-27 17:16 编辑

​ 软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用 XILINX A7/K7/Z7/ZU/KU 系列 FPGA
实验平台:米联客-MLK-H3-CZ08-7100开发板
板卡获取平台:https://milianke.tmall.com/
登录“米联客”FPGA社区 http://www.uisrc.com 视频课程、答疑解惑!



1概述
SPI的发送器驱动程序主要围绕SPI_MOSI以及SPI_SCLK来设计。通过前面的SPI协议学习,我们这里设计的SPI驱动程序需要支持CPHA=0 CPOL=0; CPHA=1 CPOL=0; CPHA=0 CPOL=1;CPHA=1 CPOL=1四种情况。CPHA用于控制SPI接收器的采样时钟位置,CPOL用于设置SPI_SCLK的初始电平是高电平还是低电平。
SPI发送器的SPI_MOSI的设计:
可以通过设置一个stroble信号用于控制数据是在哪一个SPI_SCLK更新。
当CPHA=0的时候,当stroble有效,在SPI_SCK的第2个时钟沿更新数据;当CPHA=1的时候,当stroble有效,在SPI_SCK的第1个时钟沿更新数据。
SPI发送器的SPI_SCLK的设计:
SPI_SCLK通过CPOL设置,可以设置一个内部参考时钟,这个时钟默认的时钟极性为CPOL=0的情况,如果设置CPOL=1,只需要对该时钟取反。
SPI发送器的分频器设计:
SPI的SPI_SCLK可以通过分频系数设置分频参数,这样可以调整SPI发送驱动的数据发送速度。
2SPI Master发送驱动器设计
SPI Master驱动程序包含基本的时钟分频器、数据控制器、并串移位模块、CPOL控制、CPHA控制。
456b2046ce0d4ec5a3dc2d92667f846b.jpg
为了方便SPI Master主控制器可以方便使用该驱动程序,设计了spi_tx_req以及spi_busy用于信号的握手。
-设置spi_tx_req为1,通知SPI发送驱动程序种的数据控制器,请求数据发送。
-SPI发送驱动的数据控制器保存spi_tx_data数据,并且设置O_spi_busy代表开始发送数据,spi总线忙;
以下给出握手时序。在以后米联客的代码种,接口之间的握手也会采用类似信号和时序。
3bd70abc8d304a43874c8b363300d758.jpg

3SPI Master发送控制器设计
da37302ea1b74850ad71dfbd3c5c75e7.jpg
发送控制器设计核心部分在于状态机的设计。我们米联客设计的驱动接口,一般将接口驱动程序和驱动控制程序分开编写,这样的好处可以让代码层次更加清晰,实用维护更加方便。
在SPI Master发送控制器的设计种,核心状态机部分首先设置spi_tx_req=1启动一次SPI传输,之后等待spi_busy为高电平,之后设置spi_tx_req=0,并且等待spi_busy变为低电平。之后可以进行下一次的数据传输。
4SPI Master发送驱动程序设计
我们先给出spi-master的驱动程序源码,然后对源码的设计做一些分析。
  1. /*******************************SPI Master发送驱动器*********************
  2. --以下是米联客设计的SPI Master发送驱动器
  3. --1.代码简洁,占用极少逻辑资源,代码结构清晰,逻辑设计严谨
  4. --2.I_spi_tx_req,用户程序通过设置I_spi_tx_req为高,请求SPI Master发送驱动器,发送数据,SPI Master发送驱动器把I_spi_tx_data数据通过 O_spi_mosi总线发送出去
  5. --3.通过CPOL以及CPHA自由的控制SPI Master发送驱动器的时钟极性一级时钟相位,并且在时序上做出相应的匹配。
  6. --4.O_spi_busy,表示当前*SPI Master发送总线整忙,这时用户程序需要等待非忙的时候,请求发送数据。
  7. *********************************************************************/
  8. `timescale 1ns / 1ps                                            //定义仿真时间刻度/精度

  9. module ui_mspi_tx#
  10. (
  11. parameter CLK_DIV = 100,
  12. parameter CPOL = 1'b0,                                            //时钟极性参数设置
  13. parameter CPHA = 1'b1                                             //时钟相位参数设置
  14. )
  15. (
  16. input       I_clk,                                                 //系统时钟输入
  17. input       I_rstn,                                                //系统复位输入
  18. output      O_spi_mosi,                                            //发送SPI数据
  19. output      O_spi_sclk,                                            //发送SPI时钟
  20. input       I_spi_tx_req,                                         //发送数据请求
  21. input [7:0] I_spi_tx_data,                                        //发送数据  
  22. output      O_spi_busy                                            //发送状态忙,代表正在发送数据
  23. );

  24. localparam [9:0] SPI_DIV     = CLK_DIV;                         //第二时钟边沿计数器
  25. localparam [9:0] SPI_DIV1    = SPI_DIV/2;                       //第一时钟边沿计数器

  26. reg [9:0]   clk_div  = 10'd0;   
  27. reg         spi_en   = 1'b0;
  28. reg         spi_clk  = 1'b0;
  29. reg [3:0]   tx_cnt   = 4'd0;
  30. reg [7:0]   spi_tx_data_r=8'd0;
  31. wire        clk_end;
  32. wire        clk_en1;                                           //第一内部时钟边沿使能
  33. wire        clk_en2;                                           //第二内部时钟边沿使能
  34. reg         spi_strobe_en;
  35. wire        spi_strobe;        //CPHA=0数据在第一时钟边沿上传输,CPHA=1数据在第二时钟边沿上发送

  36. assign      clk_en1     = (clk_div == SPI_DIV1);//第一内部时钟边沿使能
  37. assign      clk_en2     = (clk_div == SPI_DIV);//第二内部时钟边沿使能
  38. assign      clk_end     = (clk_div == SPI_DIV1)&&(tx_cnt==4'd8);
  39. //计数器发送第一个内部时钟0到7次,当计数达到最后8时,不发送时钟//当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
  40. //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
  41. assign      spi_strobe  = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;
  42. assign      O_spi_sclk  = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平
  43. assign      O_spi_mosi  = spi_tx_data_r[7];
  44. assign      O_spi_busy  = spi_en;

  45. always@(posedge I_clk)begin                                   //时钟分频器
  46.     if(spi_en == 1'b0)
  47.         clk_div <= 10'd0;
  48.     else if(clk_div < SPI_DIV)
  49.         clk_div <= clk_div + 1'b1;
  50.     else
  51.         clk_div <= 0;
  52. end
  53. always@(posedge I_clk)begin                                   //生成spi内部时钟
  54.         if(spi_en == 1'b0)
  55.             spi_clk <= 1'b0;
  56.     else if(clk_en2)
  57.             spi_clk <= 1'b0;                                   //第二时钟边沿
  58.         else if(clk_en1&&(tx_cnt<4'd8))                       //第一时钟边沿
  59.             spi_clk <= 1'b1;
  60.     else
  61.         spi_clk <= spi_clk;
  62. end

  63. always@(posedge I_clk)begin  
  64.           if(I_rstn == 1'b0)
  65.              spi_strobe_en <= 1'b0;
  66.           else if(tx_cnt < 4'd8)begin
  67.                if(clk_en1) spi_strobe_en <= 1'b1;   
  68.           end
  69.           else
  70.                spi_strobe_en <= 1'b0;         
  71. end

  72. always@(posedge I_clk)begin  
  73.           if((I_rstn == 1'b0)||(spi_en == 1'b0))
  74.              tx_cnt <= 4'd0;
  75.           else if(clk_en1)
  76.              tx_cnt <= tx_cnt + 1'b1;      
  77. end

  78. always@(posedge I_clk)begin                                           //spi发送模块
  79.     if(I_rstn == 1'b0 || clk_end)begin
  80.         spi_en <= 1'b0;
  81.         spi_tx_data_r <= 8'h00;
  82.     end
  83.     else if(I_spi_tx_req&&(spi_en == 1'b0)) begin                    //启用传输
  84.             spi_en <= 1'b1;
  85.             spi_tx_data_r <= I_spi_tx_data;
  86.     end
  87.     else if(spi_en)begin
  88.          spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
  89.     end
  90. end   

  91. endmodule
复制代码

我们看下驱动器的驱动程序部分核心模块设计分析
4.1分频器设计
系统时钟一般运行于较高速度,而SPI的SCLK需要基于系统时钟分频后产生,所以首先需要设计一个分频器,用于对SCLK分频
  1. localparam [9:0] SPI_DIV     = CLK_DIV;                             //第二时钟边沿计数器
  2. localparam [9:0] SPI_DIV1    = SPI_DIV/2;                           //第一时钟边沿计数器

  3. always@(posedge I_clk)begin                                   //时钟分频器
  4.     if(spi_en == 1'b0)
  5.         clk_div <= 10'd0;
  6.     else if(clk_div < SPI_DIV)
  7.         clk_div <= clk_div + 1'b1;
  8.     else
  9.         clk_div <= 0;
  10. end
复制代码

4.2SCLK时钟设计
SCLK可以支持CPOL=0(空闲状态输出低电平)和CPOL=1(空闲状态输出高电平)
首先我们设计内部SCLK,这个SCLK的周期和我们实际输出的SCLK周期一致,当我们设置CPOL=0或者CPOL=1的时候我们只要对时钟采取取反或者不取反操作。
内部的SCLK通过clk_en1和clk_en2的触发时刻来实现电平的输出和切换。
  1. assign      clk_en1     = (clk_div == SPI_DIV1);               //第一内部时钟边沿使能
  2. assign      clk_en2     = (clk_div == SPI_DIV);                 //第二内部时钟边沿使能
  3. assign      spi_sclk_o  = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平

  4. always@(posedge I_clk)begin                                   //生成spi内部时钟
  5.         if(spi_en == 1'b0)
  6.             spi_clk <= 1'b0;
  7.     else if(clk_en2)
  8.             spi_clk <= 1'b0;                                   //第二时钟边沿
  9.         else if(clk_en1&&(tx_cnt<4'd8))                       //第一时钟边沿
  10.             spi_clk <= 1'b1;
  11.     else
  12.         spi_clk <= spi_clk;
  13. end
复制代码

4.3数据控制器设计
数据控制器是SPI-Master驱动器设计中最关键的部分,数据控制器包括驱动控制接口,也包含了SPI数据部分的并串移位模块。当SPI的控制器部分发送了spi_tx_req为高电平后,下一个系统时钟周期数据会被寄存到spi_tx_data_r并且设置spi_en为高电平,之后时钟分频模块、spi_clk模块等开始工作。同时设置spi_busy信号为高电平,通知SPI控制器SPI驱动器已经处于工作状态。
移位数据的更新通过spi_stroble控制,spi_stroble根据CPHA的设置决定是clk_en1更新数据还是clk_en2更新数据。clk_en1和SCLK的第1个跳变沿同步,clk_en2和SCLK的第2个跳变沿同步。
  1. //当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
  2. //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
  3. assign      spi_strobe  = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;

  4. assign      O_spi_mosi  = spi_tx_data_r[7];
  5. assign      O_spi_busy  = spi_en;
  6. always@(posedge I_clk)begin                                           //spi发送模块
  7.     if(I_rstn == 1'b0 || clk_end)begin
  8.         spi_en <= 1'b0;
  9.         spi_tx_data_r <= 8'h00;
  10.     end
  11.     else if(I_spi_tx_req&&(spi_en == 1'b0)) begin                    //启用传输
  12.             spi_en <= 1'b1;
  13.             spi_tx_data_r <= I_spi_tx_data;
  14.     end
  15.     else if(spi_en)begin
  16.          spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
  17.     end

  18. end   
复制代码

5SPI Master发送控制器设计
SPI Master的发送控制器根据不同的实际应用需要一次或者多次把一个或者多个数据发送出去,在本实验中,演示了发送连续的加计数器数据的方法
  1. /************************ SPI Master发送控制器*********************

  2. --以下是米联客设计的SPI Master发送控制器
  3. --1.spi_tx_req,用户程序通过设置spi_tx_req为高,请求SPI Master发送驱动器,发送数据,SPI Master发送驱动器把spi_tx_data测试数据通过spi_mosi_o总线发送出去
  4. --2.spi_busy_o,表示当前SPI发送总线整忙,这时用户程序需要等待非忙的时候,请求发送数据。
  5. --3.演示发送spi_tx_data测试数据,通过ila观察发送数据时许
  6. *********************************************************************/
  7. `timescale 1ns / 1ps

  8. module spi_master_tx#
  9. (
  10. parameter CLK_DIV = 100        
  11. )
  12. (
  13. //input  clk_i,
  14. input  I_sysclk_p,
  15. input  I_sysclk_n,                                           //输入时钟
  16. input  I_rstn,                                         //系统复位
  17. output O_spi_sclk,                                     //SPI发送时钟
  18. output O_spi_mosi                                      //SPI发送数据
  19. );

  20. wire I_clk;
  21. IBUFGDS CLK_U(
  22. .I(I_sysclk_p),
  23. .IB(I_sysclk_n),
  24. .O(I_clk)
  25. );

  26. wire        spi_busy;                                     //SPI忙信号
  27. reg         spi_tx_req;                                   //SPI发送req信号,有发送需求时拉高
  28. reg [7:0]   spi_tx_data;                                  //待发送数据存储
  29. reg [1:0]   M_S;                                           //状态机

  30. //spi send state machine
  31. always @(posedge I_clk) begin
  32.     if(!I_rstn) begin                                      //拉低复位
  33.         spi_tx_req  <= 1'b0;
  34.         spi_tx_data <= 8'd0;
  35.         M_S <= 2'd0;
  36.     end
  37.     else begin
  38.         case(M_S)
  39.         0:if(!spi_busy)begin                            //总线不忙启动传输
  40.            spi_tx_req  <= 1'b1;                         //req信号拉高,开始传输
  41.            spi_tx_data <= spi_tx_data + 1'b1;          //测试数据
  42.            M_S <= 2'd1;
  43.         end
  44.         1:if(spi_busy)begin                             //如果spi总线忙,清除spi_tx_req
  45.            spi_tx_req  <= 1'b0;
  46.            M_S <= 2'd0;
  47.         end
  48.         default:M_S <= 2'd0;
  49.         endcase
  50.     end
  51. end  

  52. //例化SPI Master发送驱动器
  53. ui_mspi_tx#
  54. (
  55. .CLK_DIV(CLK_DIV),
  56. .CPOL(1'b1),                                  //CPOL参数设置,可调整
  57. .CPHA(1'b1)                                   //CPHA参数设置,可调整
  58. )
  59. ui_mspi_tx_inst(
  60. .I_clk(I_clk),                              //系统时钟输入
  61. .I_rstn(I_rstn),                            //系统复位输入
  62. .O_spi_mosi(O_spi_mosi),                   //SPI发送数据串行总线
  63. .O_spi_sclk(O_spi_sclk),                   //SPI发送时钟总线
  64. .I_spi_tx_req(spi_tx_req),                  //SPI发送(写)数据请求
  65. .I_spi_tx_data(spi_tx_data),                //SPI发送(写)数据
  66. .O_spi_busy(spi_busy)                        //SPI发送驱动器忙
  67. );
  68. endmodule
复制代码

6RTL仿真6.1仿真激励文件
本实验以仿真的方式演示,仿真激励信号提供一个系统时钟即可
  1. `timescale 1ns / 1ps
  2. module master_spi_tb;
  3. localparam      SYS_TIME   =  'd10;//时钟周期,以ns为单位
  4. reg             I_sysclk_p;
  5. reg             I_sysclk_n;               //系统时钟
  6. reg I_rstn;  
  7. wire O_spi_sclk;
  8. wire O_spi_mosi;

  9. spi_master_tx#
  10. (
  11. .CLK_DIV(100)                                    //设置时钟参数,可以减少仿真时间
  12. )
  13. spi_master_tx_inst(
  14. //.clk_i(sysclk_i),
  15. .I_sysclk_p(I_sysclk_p),
  16. .I_sysclk_n(I_sysclk_n),
  17. .I_rstn(I_rstn),
  18. .O_spi_sclk(O_spi_sclk),
  19. .O_spi_mosi(O_spi_mosi)
  20. );
  21. initial begin
  22.     I_sysclk_p  = 1'b0;  
  23.     I_sysclk_n  = 1'b1;                            //设置时钟基础值
  24.     I_rstn = 1'b0;                              //低电平复位
  25.     #100;
  26.     I_rstn = 1'b1;                             //复位释放
  27. #2000000 $finish;
  28. end
  29. always #(SYS_TIME/2) I_sysclk_p = ~I_sysclk_p;
  30. always #(SYS_TIME/2) I_sysclk_n = ~I_sysclk_n;     //产生主时钟
  31. endmodule
复制代码

下面展示不同CPHA、CPOL参数显示的不同的仿真结果,结果可能从图片中观察的不是很直观,各位可以自行前往工程仿真一遍。
6.2SPI发送驱动代码仿真CPHA=0 CPOL=0
如下图所示,当CPHA=0 CPOL=0,代表SPI的SCLK默认是低电平,SPI接收器在SCLK第1个时钟沿采样。SPI发送驱动器数据在SCLK的第2个时钟沿更新,确保SPI下一个SCLK的第1个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。
cb7f4fc483cc44c69edb4e8b1c702431.jpg
6.3SPI发送驱动代码仿真CPHA=1 CPOL=0
如下图所示,当CPHA=1 CPOL=0,代表SPI的SCLK默认是低电平,SPI接收器在SCLK第2个时钟沿采样。SPI发送驱动器数据在下一个SCLK的第1个时钟沿更新,确保SPI下一个SCLK的第2个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。
0ac3e5c4cb7d47fbbe04867093756188.jpg
6.4SPI发送驱动代码仿真CPHA=0 CPOL=1
和CPHA=0 CPOL=0这种设置相比,时钟SCLK取反
ac1689e1729c4f0a9850f3372658ae05.jpg
6.5SPI发送驱动代码仿真CPHA=1 CPOL=1
和CPHA=1 CPOL=0这种设置相比,时钟SCLK取反
9806c89edd8e440ba2d1492557ce129b.jpg



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则