问答 店铺
热搜: ZYNQ FPGA discuz

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

微信扫一扫 分享朋友圈

已有 98 人浏览分享

开启左侧

[MILIANPAI-F01-EG4D]FPGA程序设计基础实验连载-14 SPI MASTER发送程序设计

[复制链接]
98 0
安路-FPGA课程
安路课程: 基础入门 » 新手入门实验
安路系列: EG4
本帖最后由 UT发布 于 2025-4-10 16:42 编辑

软件版本:TD_5.6.4_Release_97693
操作系统:WIN11 64bit
硬件平台:适用安路(Anlogic)FPGA
登录米联客”FPGA社区-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的初始电平是高电平还是低电平。

2 程序设计
2.1系统框图

本次实验设计一个SPI Master(SPI_MOSI)发送驱动,包含SPI的四种工作模式。SPI Master(SPI_MOSI)共有两个模块,分别为顶层模块spi_master_tx和发送驱动模块ui_mspi_tx我们米联客设计的驱动接口,一般将接口驱动程序和驱动控制程序分开编写,这样的好处可以让代码层次更加清晰,实用维护更加方便。

SPI Master发送驱动器模块:

根据上一节课关于SPI通信原理的学习,我们知道要开始SPI通信,主机必须发送时钟信号,系统时钟一般运行于较高速度,而SPISCLK需要基于系统时钟分频后产生,所以首先需要设计一个分频器,并设置CPOL信号控制SCLK的空闲状态。并行数据需要通过MOSI总线发送出去,因此需要一个并串移位模块,将并行数据转成串行数据一位一位发送出去,并设置CPHA信号控制数据的采样时刻。

为了方便SPI Master主控制器可以方便使用该驱动程序,设计数据控制器模块,用来保存要发送的数据。使用I_spi_tx_req以及O_spi_busy用于信号的握手,在以后米联客的代码中,接口之间的握手也会采用类似信号和时序。用户程序通过设置I_spi_tx_req为高,请求发送驱动器发送数据;设置O_spi_busy1表示发送总线正忙,这时用户程序需要等待非忙的时候,请求发送数据。

image.jpg

根据以上分析,发送驱动程序包含基本的时钟分频器、数据控制器、并串移位模块、CPOL控制、CPHA控制。

image.jpg
时钟分频器模块

系统时钟一般运行于较高速度,而SPISCLK需要基于系统时钟分频后产生,所以首先需要设计一个分频器,用于对SCLK分频,spi_en拉高代表启动传输,clk_div开始计数,计满清0

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
复制代码

SCLK可以支持CPOL=0(空闲状态输出低电平)CPOL=1(空闲状态输出高电平)

首先我们可以设置一个内部参考时钟,这个时钟默认的时钟极性为CPOL=0的情况,当我们设置CPOL=0或者CPOL=1的时候我们只要对时钟采取不取反或者取反操作,最后赋值给O_spi_sclk

内部的SCLK通过clk_en1clk_en2的触发时刻来实现电平的输出和切换。

image.jpg
  1. assign      clk_en1     = (clk_div == SPI_DIV1);               //第一内部时钟边沿使能
  2. assign      clk_en2     = (clk_div == SPI_DIV);                 //第二内部时钟边沿使能
  3. assign      O_spi_sclk  = (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
复制代码
数据控制器设计
image.jpg

数据控制器是SPI-Master发送驱动器设计中最关键的部分,数据控制器包括驱动控制接口,也包含了SPI数据部分的并串移位模块。

SPI的控制器部分发送了I_spi_tx_req为高电平后,下一个系统时钟周期数据会被寄存到spi_tx_data_r并且设置spi_en为高电平,之后时钟分频模块、SCLK模块等开始工作。同时设置spi_busy信号为高电平,通知SPI控制器SPI驱动器已经处于工作状态。

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

移位数据的更新通过spi_stroble控制,spi_stroble根据CPHA的设置决定是clk_en1更新数据还是clk_en2更新数据。clk_en1SCLK的第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. always@(posedge I_clk)begin  
  5.           if(I_rstn == 1'b0)
  6.              spi_strobe_en <= 1'b0;
  7.           else if(tx_cnt < 4'd8)begin
  8.                if(clk_en1) spi_strobe_en <= 1'b1;   
  9.           end
  10.           else
  11.                spi_strobe_en <= 1'b0;         
  12. end
  13. always@(posedge I_clk)begin  
  14.           if((I_rstn == 1'b0)||(spi_en == 1'b0))
  15.              tx_cnt <= 4'd0;
  16.           else if(clk_en1)
  17.              tx_cnt <= tx_cnt + 1'b1;      
  18. end
复制代码
SPI Master发送控制器设计

发送控制器设计核心部分在于状态机的设计。M_S状态机只有2个状态,M_S==0状态等待SPI-Master驱动器非忙的情况下,发送数据发送请求信号,并且在M_S==1状态等待数据确认进入忙状态后,再次回到状态0等待空闲,如果总线空闲发送下一个测试数据。

SPI Master发送控制器的设计中,核心状态机部分首先设置spi_tx_req=1启动一次SPI传输(状态0)发送一次数据,下一个待发送数据计数加1并存储在spi_tx_data,开始传输后进入状态1spi_busy为高电平时代表正在传输,设置spi_tx_req=0,并且等待spi_busy变为低电平,之后可以进行下一次的数据传输。

image.jpg

  1. module ui_mspi_tx#
  2. (
  3. parameter CLK_DIV = 100,
  4. parameter CPOL = 1'b0,                                            //时钟极性参数设置
  5. parameter CPHA = 1'b0                                             //时钟相位参数设置
  6. )
  7. (
  8. input       I_clk,                                                 //系统时钟输入
  9. input       I_rstn,                                                //系统复位输入
  10. output      O_spi_mosi,                                            //发送SPI数据
  11. output      O_spi_sclk,                                            //发送SPI时钟
  12. input       I_spi_tx_req,                                         //发送数据请求
  13. input [7:0] I_spi_tx_data,                                        //发送数据  
  14. output      O_spi_busy                                            //发送状态忙,代表正在发送数据
  15. );
  16. localparam [9:0] SPI_DIV     = CLK_DIV;                         //第二时钟边沿计数器
  17. localparam [9:0] SPI_DIV1    = SPI_DIV/2;                       //第一时钟边沿计数器
  18. reg [9:0]   clk_div  = 10'd0;   
  19. reg         spi_en   = 1'b0;
  20. reg         spi_clk  = 1'b0;
  21. reg [3:0]   tx_cnt   = 4'd0;
  22. reg [7:0]   spi_tx_data_r=8'd0;
  23. wire        clk_end;
  24. wire        clk_en1;                                           //第一内部时钟边沿使能
  25. wire        clk_en2;                                           //第二内部时钟边沿使能
  26. reg         spi_strobe_en;
  27. wire        spi_strobe;                                        //CPHA=0数据在第一时钟边沿上传输,CPHA=1数据在第二时钟边沿上发送
  28. assign      clk_en1     = (clk_div == SPI_DIV1);//第一内部时钟边沿使能
  29. assign      clk_en2     = (clk_div == SPI_DIV);//第二内部时钟边沿使能
  30. assign      clk_end     = (clk_div == SPI_DIV1)&&(tx_cnt==4'd8);
  31. //计数器发送第一个内部时钟0到7次,当计数达到最后8时,不发送时钟
  32. //当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
  33. //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
  34. assign      spi_strobe  = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;
  35. assign      O_spi_sclk  = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平
  36. assign      O_spi_mosi  = spi_tx_data_r[7];
  37. assign      O_spi_busy  = spi_en;
  38. always@(posedge I_clk)begin                                   //时钟分频器
  39.     if(spi_en == 1'b0)
  40.         clk_div <= 10'd0;
  41.     else if(clk_div < SPI_DIV)
  42.         clk_div <= clk_div + 1'b1;
  43.     else
  44.         clk_div <= 0;
  45. end
  46. always@(posedge I_clk)begin                                   //生成spi内部时钟
  47.         if(spi_en == 1'b0)
  48.             spi_clk <= 1'b0;
  49.     else if(clk_en2)
  50.             spi_clk <= 1'b0;                                   //第二时钟边沿
  51.         else if(clk_en1&&(tx_cnt<4'd8))                       //第一时钟边沿
  52.             spi_clk <= 1'b1;
  53.     else
  54.         spi_clk <= spi_clk;
  55. end
  56. always@(posedge I_clk)begin  
  57.           if(I_rstn == 1'b0)
  58.              spi_strobe_en <= 1'b0;
  59.           else if(tx_cnt < 4'd8)begin
  60.                if(clk_en1) spi_strobe_en <= 1'b1;   
  61.           end
  62.           else
  63.                spi_strobe_en <= 1'b0;         
  64. end
  65. always@(posedge I_clk)begin  
  66.           if((I_rstn == 1'b0)||(spi_en == 1'b0))
  67.              tx_cnt <= 4'd0;
  68.           else if(clk_en1)
  69.              tx_cnt <= tx_cnt + 1'b1;      
  70. end
  71. always@(posedge I_clk)begin                                           //spi发送模块
  72.     if(I_rstn == 1'b0 || clk_end)begin
  73.         spi_en <= 1'b0;
  74.         spi_tx_data_r <= 8'h00;
  75.     end
  76.     else if(I_spi_tx_req&&(spi_en == 1'b0)) begin                    //启用传输
  77.             spi_en <= 1'b1;
  78.             spi_tx_data_r <= I_spi_tx_data;
  79.     end
  80.     else if(spi_en)begin
  81.          spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
  82.     end
  83. end   
  84. endmodule
复制代码
2.2 驱动源码

SPI Master发送控制器源码

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

  1. module ui_mspi_tx#
  2. (
  3. parameter CLK_DIV = 100,
  4. parameter CPOL = 1'b0,                                            //时钟极性参数设置
  5. parameter CPHA = 1'b0                                             //时钟相位参数设置
  6. )
  7. (
  8. input       I_clk,                                                 //系统时钟输入
  9. input       I_rstn,                                                //系统复位输入
  10. output      O_spi_mosi,                                            //发送SPI数据
  11. output      O_spi_sclk,                                            //发送SPI时钟
  12. input       I_spi_tx_req,                                         //发送数据请求
  13. input [7:0] I_spi_tx_data,                                        //发送数据  
  14. output      O_spi_busy                                            //发送状态忙,代表正在发送数据
  15. );
  16. localparam [9:0] SPI_DIV     = CLK_DIV;                         //第二时钟边沿计数器
  17. localparam [9:0] SPI_DIV1    = SPI_DIV/2;                       //第一时钟边沿计数器
  18. reg [9:0]   clk_div  = 10'd0;   
  19. reg         spi_en   = 1'b0;
  20. reg         spi_clk  = 1'b0;
  21. reg [3:0]   tx_cnt   = 4'd0;
  22. reg [7:0]   spi_tx_data_r=8'd0;
  23. wire        clk_end;
  24. wire        clk_en1;                                           //第一内部时钟边沿使能
  25. wire        clk_en2;                                           //第二内部时钟边沿使能
  26. reg         spi_strobe_en;
  27. wire        spi_strobe;                                        //CPHA=0数据在第一时钟边沿上传输,CPHA=1数据在第二时钟边沿上发送
  28. assign      clk_en1     = (clk_div == SPI_DIV1);//第一内部时钟边沿使能
  29. assign      clk_en2     = (clk_div == SPI_DIV);//第二内部时钟边沿使能
  30. assign      clk_end     = (clk_div == SPI_DIV1)&&(tx_cnt==4'd8);
  31. //计数器发送第一个内部时钟0到7次,当计数达到最后8时,不发送时钟
  32. //当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
  33. //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
  34. assign      spi_strobe  = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;
  35. assign      O_spi_sclk  = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平
  36. assign      O_spi_mosi  = spi_tx_data_r[7];
  37. assign      O_spi_busy  = spi_en;
  38. always@(posedge I_clk)begin                                   //时钟分频器
  39.     if(spi_en == 1'b0)
  40.         clk_div <= 10'd0;
  41.     else if(clk_div < SPI_DIV)
  42.         clk_div <= clk_div + 1'b1;
  43.     else
  44.         clk_div <= 0;
  45. end
  46. always@(posedge I_clk)begin                                   //生成spi内部时钟
  47.         if(spi_en == 1'b0)
  48.             spi_clk <= 1'b0;
  49.     else if(clk_en2)
  50.             spi_clk <= 1'b0;                                   //第二时钟边沿
  51.         else if(clk_en1&&(tx_cnt<4'd8))                       //第一时钟边沿
  52.             spi_clk <= 1'b1;
  53.     else
  54.         spi_clk <= spi_clk;
  55. end
  56. always@(posedge I_clk)begin  
  57.           if(I_rstn == 1'b0)
  58.              spi_strobe_en <= 1'b0;
  59.           else if(tx_cnt < 4'd8)begin
  60.                if(clk_en1) spi_strobe_en <= 1'b1;   
  61.           end
  62.           else
  63.                spi_strobe_en <= 1'b0;         
  64. end
  65. always@(posedge I_clk)begin  
  66.           if((I_rstn == 1'b0)||(spi_en == 1'b0))
  67.              tx_cnt <= 4'd0;
  68.           else if(clk_en1)
  69.              tx_cnt <= tx_cnt + 1'b1;      
  70. end
  71. always@(posedge I_clk)begin                                           //spi发送模块
  72.     if(I_rstn == 1'b0 || clk_end)begin
  73.         spi_en <= 1'b0;
  74.         spi_tx_data_r <= 8'h00;
  75.     end
  76.     else if(I_spi_tx_req&&(spi_en == 1'b0)) begin                    //启用传输
  77.             spi_en <= 1'b1;
  78.             spi_tx_data_r <= I_spi_tx_data;
  79.     end
  80.     else if(spi_en)begin
  81.          spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
  82.     end
  83. end   
  84. endmodule
复制代码

M_S状态机只有2个状态,M_S==0状态等待SPI-Master驱动器非忙的情况下,发送数据发送请求信号,并且在M_S==1状态等待数据确认进入忙状态后,再次回到状态0等待空闲,如果总线空闲发送下一个测试数据。

3 RTL仿真
3.1 仿真激励文件

Modelsim仿真的创建过程不再重复,如有不清楚的请看前面实验

本实验以仿真的方式演示,仿真激励信号提供一个系统时钟即可

  1. module master_spi_tb;
  2. localparam      SYS_TIME   =  'd20;//时钟周期,以ns为单位
  3. reg             I_sysclk;               //系统时钟
  4. reg I_rstn;  
  5. wire O_spi_sclk;
  6. wire O_spi_mosi;
  7. spi_master_tx#
  8. (
  9. .CLK_DIV(100)                                    //设置时钟参数,可以减少仿真时间
  10. )
  11. spi_master_tx_inst(
  12. .I_clk(I_sysclk),
  13. .I_rstn(I_rstn),
  14. .O_spi_sclk(O_spi_sclk),
  15. .O_spi_mosi(O_spi_mosi)
  16. );
  17. initial begin
  18.     I_sysclk  = 1'b0;                              //设置时钟基础值
  19.     I_rstn = 1'b0;                              //低电平复位
  20.     #100;
  21.     I_rstn = 1'b1;                             //复位释放
  22. #2000000 $finish;
  23. end
  24. always #(SYS_TIME/2) I_sysclk = ~I_sysclk;     //产生主时钟
  25. endmodule
复制代码

以下启动modelsim仿真设置仿真时间2ms

3.2 SPI发送驱动代码仿真CPHA=0 CPOL=0

如下图所示,当CPHA=0 CPOL=0,代表SPISCLK默认是低电平,SPI接收器在SCLK1个时钟沿采样。SPI发送驱动器数据在SCLK的第2个时钟沿更新,确保SPI下一个SCLK的第1个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。

image.jpg
3.3 SPI发送驱动代码仿真CPHA=1 CPOL=0

如下图所示,当CPHA=1 CPOL=0,代表SPISCLK默认是低电平,SPI接收器在SCLK2个时钟沿采样。SPI发送驱动器数据在下一个SCLK的第1个时钟沿更新,确保SPI下一个SCLK的第2个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。

image.jpg
3.4 SPI发送驱动代码仿真CPHA=0 CPOL=1

CPHA=0 CPOL=0这种设置相比,时钟SCLK取反

image.jpg
3.5 SPI发送驱动代码仿真CPHA=1 CPOL=1

CPHA=1 CPOL=0这种设置相比,时钟SCLK取反

image.jpg


















































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

本版积分规则

0

关注

0

粉丝

295

主题
精彩推荐
热门资讯
网友晒图
图文推荐

  • 微信公众平台

  • 扫描访问手机版