[米联客-安路飞龙DR1-FPSOC] FPGA基础篇连载-14 SPI MASET发送程序设计

软件版本:Anlogic -TD5.9.1-DR1_ES1.1

操作系统:WIN10 64bit

硬件平台:适用安路(Anlogic)FPGA

实验平台:米联客-MLK-L1-CZ06-DR1M90G开发板

板卡获取平台: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的初始电平是高电平还是低电平。

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,表示发送总线正忙,这时用户程序需要等待非忙的时候,请求发送数据。

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

时钟分频器模块:

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

 

 1 localparam [9:0] SPI_DIV     = CLK_DIV;                             //第二时钟边沿计数器
 2 localparam [9:0] SPI_DIV1    = SPI_DIV/2;                           //第一时钟边沿计数器
 3 
 4 always@(posedge I_clk)begin                                          //时钟分频器
 5     if(spi_en == 1'b0)
 6         clk_div <= 10'd0;
 7     else if(clk_div < SPI_DIV)
 8         clk_div <= clk_div + 1'b1;
 9     else 
10         clk_div <= 0;
11 end

 

 

 

 

SCLK模块:

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

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

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

 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 
 5 always@(posedge I_clk)begin                                     //生成spi内部时钟
 6         if(spi_en == 1'b0)
 7             spi_clk <= 1'b0;
 8     else if(clk_en2) 
 9             spi_clk <= 1'b0;                                 //第二时钟边沿
10         else if(clk_en1&&(tx_cnt<4'd8))                     //第一时钟边沿
11             spi_clk <= 1'b1; 
12     else
13         spi_clk <= spi_clk;
14 end

 

 

数据控制器设计:

数据控制器是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 ……
 5 ……
 6 //spi发送模块
 7 always@(posedge I_clk)begin                                           //spi发送模块
 8     if(I_rstn == 1'b0 || clk_end)begin
 9         spi_en <= 1'b0;
10         spi_tx_data_r <= 8'h00;
11     end
12     else if(I_spi_tx_req&&(spi_en == 1'b0)) begin                    //启用传输
13             spi_en <= 1'b1;
14             spi_tx_data_r <= I_spi_tx_data;
15     end
16     else if(spi_en)begin
17          spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
18     end
19  
20 end  

 

 

 

 

移位数据的更新通过spi_stroble控制,spi_stroble根据CPHA的设置决定是clk_en1更新数据还是clk_en2更新数据。clk_en1SCLK的第1个跳变沿同步,clk_en2SCLK的第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 
 5 always@(posedge I_clk)begin  
 6           if(I_rstn == 1'b0) 
 7              spi_strobe_en <= 1'b0;
 8           else if(tx_cnt < 4'd8)
 9             begin
10                if(clk_en1) 
11                  spi_strobe_en <= 1'b1;    
12                else
13                  spi_strobe_en <= spi_strobe_en; 
14             end 
15           else 
16                spi_strobe_en <= 1'b0;         
17 end
18 
19 always@(posedge I_clk)begin  
20           if((I_rstn == 1'b0)||(spi_en == 1'b0)) 
21              tx_cnt <= 4'd0;
22           else if(clk_en1) 
23              tx_cnt <= tx_cnt + 1'b1;      
24 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变为低电平,之后可以进行下一次的数据传输。

2.2 驱动源码

 

 

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

 

SPI Master发送控制器源码

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

 
 1 `timescale 1ns / 1ps
 2 
 3 module spi_master_tx#
 4 (
 5 parameter CLK_DIV = 100        
 6 )
 7 (
 8 input  I_clk,                                          //输入时钟
 9 input  I_rstn,                                         //系统复位
10 output O_spi_sclk,                                     //SPI发送时钟
11 output O_spi_mosi                                      //SPI发送数据
12 );
13 
14 wire        spi_busy;                                     //SPI忙信号
15 reg         spi_tx_req;                                   //SPI发送req信号,有发送需求时拉高
16 reg [7:0]   spi_tx_data;                                  //待发送数据存储
17 reg [1:0]   M_S;                                           //状态机
18 
19 //spi send state machine
20 always @(posedge I_clk) begin
21     if(!I_rstn) begin                                      //拉低复位
22         spi_tx_req  <= 1'b0;
23         spi_tx_data <= 8'd0;
24         M_S <= 2'd0;
25     end
26     else begin
27         case(M_S)
28         0:if(!spi_busy)begin                            //总线不忙启动传输
29            spi_tx_req  <= 1'b1;                         //req信号拉高,开始传输
30            spi_tx_data <= spi_tx_data + 1'b1;          //测试数据
31            M_S <= 2'd1;
32         end
33         1:if(spi_busy)begin                             //如果spi总线忙,清除spi_tx_req
34            spi_tx_req  <= 1'b0;
35            M_S <= 2'd0;
36         end
37         default:M_S <= 2'd0;
38         endcase
39     end
40 end   
41 
42 //例化SPI Master发送驱动器
43 ui_mspi_tx#
44 (
45 .CLK_DIV(CLK_DIV),
46 .CPOL(1'b0),                                  //CPOL参数设置,可调整
47 .CPHA(1'b0)                                   //CPHA参数设置,可调整
48 )
49 ui_mspi_tx_inst(
50 .I_clk(I_clk),                              //系统时钟输入
51 .I_rstn(I_rstn),                            //系统复位输入
52 .O_spi_mosi(O_spi_mosi),                   //SPI发送数据串行总线
53 .O_spi_sclk(O_spi_sclk),                   //SPI发送时钟总线
54 .I_spi_tx_req(spi_tx_req),                  //SPI发送(写)数据请求
55 .I_spi_tx_data(spi_tx_data),                //SPI发送(写)数据
56 .O_spi_busy(spi_busy)                        //SPI发送驱动器忙
57  );
58 endmodule

 

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

3 RTL仿真

3.1仿真激励文件

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

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

 

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

 

 

 

 

以下启动modelsim仿真

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

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

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

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

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

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

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

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

posted @ 2024-07-29 15:51  米联客(milianke)  阅读(21)  评论(0编辑  收藏  举报