软件版本:Anlogic -TD5.9.1-DR1_ES1.1
操作系统:WIN10 64bit
硬件平台:适用安路(Anlogic)FPGA
实验平台:米联客-MLKPAI-SF01-DR1M90M开发板
板卡获取平台: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通信,主机必须发送时钟信号,系统时钟一般运行于较高速度,而SPI的SCLK需要基于系统时钟分频后产生,所以首先需要设计一个分频器,并设置CPOL信号控制SCLK的空闲状态。并行数据需要通过MOSI总线发送出去,因此需要一个并串移位模块,将并行数据转成串行数据一位一位发送出去,并设置CPHA信号控制数据的采样时刻。 为了方便SPI Master主控制器可以方便使用该驱动程序,设计数据控制器模块,用来保存要发送的数据。使用I_spi_tx_req以及O_spi_busy用于信号的握手,在以后米联客的代码中,接口之间的握手也会采用类似信号和时序。用户程序通过设置I_spi_tx_req为高,请求发送驱动器发送数据;设置O_spi_busy为1,表示发送总线正忙,这时用户程序需要等待非忙的时候,请求发送数据。
根据以上分析,发送驱动程序包含基本的时钟分频器、数据控制器、并串移位模块、CPOL控制、CPHA控制。
时钟分频器模块: 系统时钟一般运行于较高速度,而SPI的SCLK需要基于系统时钟分频后产生,所以首先需要设计一个分频器,用于对SCLK分频,当spi_en拉高代表启动传输,clk_div开始计数,计满清0。 - localparam [9:0] SPI_DIV = CLK_DIV; //第二时钟边沿计数器
- localparam [9:0] SPI_DIV1 = SPI_DIV/2; //第一时钟边沿计数器
- always@(posedge I_clk)begin //时钟分频器
- if(spi_en == 1'b0)
- clk_div <= 10'd0;
- else if(clk_div < SPI_DIV)
- clk_div <= clk_div + 1'b1;
- else
- clk_div <= 0;
- end
复制代码SCLK模块: SCLK可以支持CPOL=0(空闲状态输出低电平)和CPOL=1(空闲状态输出高电平) 首先我们可以设置一个内部参考时钟,这个时钟默认的时钟极性为CPOL=0的情况,当我们设置CPOL=0或者CPOL=1的时候我们只要对时钟采取不取反或者取反操作,最后赋值给O_spi_sclk。 内部的SCLK通过clk_en1和clk_en2的触发时刻来实现电平的输出和切换。
- assign clk_en1 = (clk_div == SPI_DIV1); //第一内部时钟边沿使能
- assign clk_en2 = (clk_div == SPI_DIV); //第二内部时钟边沿使能
- assign O_spi_sclk = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平
- always@(posedge I_clk)begin //生成spi内部时钟
- if(spi_en == 1'b0)
- spi_clk <= 1'b0;
- else if(clk_en2)
- spi_clk <= 1'b0; //第二时钟边沿
- else if(clk_en1&&(tx_cnt<4'd8)) //第一时钟边沿
- spi_clk <= 1'b1;
- else
- spi_clk <= spi_clk;
- end
复制代码数据控制器设计:
数据控制器是SPI-Master发送驱动器设计中最关键的部分,数据控制器包括驱动控制接口,也包含了SPI数据部分的并串移位模块。 当SPI的控制器部分发送了I_spi_tx_req为高电平后,下一个系统时钟周期数据会被寄存到spi_tx_data_r并且设置spi_en为高电平,之后时钟分频模块、SCLK模块等开始工作。同时设置spi_busy信号为高电平,通知SPI控制器SPI驱动器已经处于工作状态。 - assign clk_end = (clk_div == SPI_DIV1)&&(tx_cnt==4'd8);
- assign O_spi_mosi = spi_tx_data_r[7];
- assign O_spi_busy = spi_en;
- ……
- ……
- //spi发送模块
- always@(posedge I_clk)begin //spi发送模块
- if(I_rstn == 1'b0 || clk_end)begin
- spi_en <= 1'b0;
- spi_tx_data_r <= 8'h00;
- end
- else if(I_spi_tx_req&&(spi_en == 1'b0)) begin //启用传输
- spi_en <= 1'b1;
- spi_tx_data_r <= I_spi_tx_data;
- end
- else if(spi_en)begin
- spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
- end
-
- end
复制代码移位数据的更新通过spi_stroble控制,spi_stroble根据CPHA的设置决定是clk_en1更新数据还是clk_en2更新数据。clk_en1和SCLK的第1个跳变沿同步,clk_en2和SCLK的第2个跳变沿同步。 //当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上//当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上assign
- //当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
- //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
- assign spi_strobe = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;
- always@(posedge I_clk)begin
- if(I_rstn == 1'b0)
- spi_strobe_en <= 1'b0;
- else if(tx_cnt < 4'd8)
- begin
- if(clk_en1)
- spi_strobe_en <= 1'b1;
- else
- spi_strobe_en <= spi_strobe_en;
- end
- else
- spi_strobe_en <= 1'b0;
- end
- always@(posedge I_clk)begin
- if((I_rstn == 1'b0)||(spi_en == 1'b0))
- tx_cnt <= 4'd0;
- else if(clk_en1)
- tx_cnt <= tx_cnt + 1'b1;
- 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,开始传输后进入状态1,spi_busy为高电平时代表正在传输,设置spi_tx_req=0,并且等待spi_busy变为低电平,之后可以进行下一次的数据传输。
2.2驱动源码
- `timescale 1ns / 1ps //定义仿真时间刻度/精度
- module ui_mspi_tx#
- (
- parameter CLK_DIV = 100,
- parameter CPOL = 1'b0, //时钟极性参数设置
- parameter CPHA = 1'b0 //时钟相位参数设置
- )
- (
- input I_clk, //系统时钟输入
- input I_rstn, //系统复位输入
- output O_spi_mosi, //发送SPI数据
- output O_spi_sclk, //发送SPI时钟
- input I_spi_tx_req, //发送数据请求
- input [7:0] I_spi_tx_data, //发送数据
- output O_spi_busy //发送状态忙,代表正在发送数据
- );
- localparam [9:0] SPI_DIV = CLK_DIV; //第二时钟边沿计数器
- localparam [9:0] SPI_DIV1 = SPI_DIV/2; //第一时钟边沿计数器
- reg [9:0] clk_div = 10'd0;
- reg spi_en = 1'b0;
- reg spi_clk = 1'b0;
- reg [3:0] tx_cnt = 4'd0;
- reg [7:0] spi_tx_data_r=8'd0;
- wire clk_end;
- wire clk_en1; //第一内部时钟边沿使能
- wire clk_en2; //第二内部时钟边沿使能
- reg spi_strobe_en;
- wire spi_strobe; //CPHA=0数据在第一时钟边沿上传输,CPHA=1数据在第二时钟边沿上发送
- assign clk_en1 = (clk_div == SPI_DIV1);//第一内部时钟边沿使能
- assign clk_en2 = (clk_div == SPI_DIV);//第二内部时钟边沿使能
- assign clk_end = (clk_div == SPI_DIV1)&&(tx_cnt==4'd8);
- //计数器发送第一个内部时钟0到7次,当计数达到最后8时,不发送时钟//当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
- //当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
- assign spi_strobe = CPHA ? clk_en1&spi_strobe_en : clk_en2&spi_strobe_en ;
- assign O_spi_sclk = (CPOL == 1'b1) ? ~spi_clk : spi_clk;//设置SPI时钟的初始电平
- assign O_spi_mosi = spi_tx_data_r[7];
- assign O_spi_busy = spi_en;
- always@(posedge I_clk)begin //时钟分频器
- if(spi_en == 1'b0)
- clk_div <= 10'd0;
- else if(clk_div < SPI_DIV)
- clk_div <= clk_div + 1'b1;
- else
- clk_div <= 0;
- end
- always@(posedge I_clk)begin //生成spi内部时钟
- if(spi_en == 1'b0)
- spi_clk <= 1'b0;
- else if(clk_en2)
- spi_clk <= 1'b0; //第二时钟边沿
- else if(clk_en1&&(tx_cnt<4'd8)) //第一时钟边沿
- spi_clk <= 1'b1;
- else
- spi_clk <= spi_clk;
- end
- always@(posedge I_clk)begin
- if(I_rstn == 1'b0)
- spi_strobe_en <= 1'b0;
- else if(tx_cnt < 4'd8)begin
- if(clk_en1) spi_strobe_en <= 1'b1;
- end
- else
- spi_strobe_en <= 1'b0;
- end
- always@(posedge I_clk)begin
- if((I_rstn == 1'b0)||(spi_en == 1'b0))
- tx_cnt <= 4'd0;
- else if(clk_en1)
- tx_cnt <= tx_cnt + 1'b1;
- end
- always@(posedge I_clk)begin //spi发送模块
- if(I_rstn == 1'b0 || clk_end)begin
- spi_en <= 1'b0;
- spi_tx_data_r <= 8'h00;
- end
- else if(I_spi_tx_req&&(spi_en == 1'b0)) begin //启用传输
- spi_en <= 1'b1;
- spi_tx_data_r <= I_spi_tx_data;
- end
- else if(spi_en)begin
- spi_tx_data_r[7:0] <= (spi_strobe) ? {spi_tx_data_r[6:0],1'b1} : spi_tx_data_r;
- end
-
- end
- endmodule
复制代码SPI Master发送控制器源码 SPI Master的发送控制器根据不同的实际应用需要一次或者多出把一个或者多个数据发送出去,在本实验中,演示了发送连续的加计数器数据的方法。 - `timescale 1ns / 1ps
- module spi_master_tx#
- (
- parameter CLK_DIV = 100
- )
- (
- input I_clk, //输入时钟
- input I_rstn, //系统复位
- output O_spi_sclk, //SPI发送时钟
- output O_spi_mosi //SPI发送数据
- );
- wire spi_busy; //SPI忙信号
- reg spi_tx_req; //SPI发送req信号,有发送需求时拉高
- reg [7:0] spi_tx_data; //待发送数据存储
- reg [1:0] M_S; //状态机
- //spi send state machine
- always @(posedge I_clk) begin
- if(!I_rstn) begin //拉低复位
- spi_tx_req <= 1'b0;
- spi_tx_data <= 8'd0;
- M_S <= 2'd0;
- end
- else begin
- case(M_S)
- 0:if(!spi_busy)begin //总线不忙启动传输
- spi_tx_req <= 1'b1; //req信号拉高,开始传输
- spi_tx_data <= spi_tx_data + 1'b1; //测试数据
- M_S <= 2'd1;
- end
- 1:if(spi_busy)begin //如果spi总线忙,清除spi_tx_req
- spi_tx_req <= 1'b0;
- M_S <= 2'd0;
- end
- default:M_S <= 2'd0;
- endcase
- end
- end
- //例化SPI Master发送驱动器
- ui_mspi_tx#
- (
- .CLK_DIV(CLK_DIV),
- .CPOL(1'b0), //CPOL参数设置,可调整
- .CPHA(1'b0) //CPHA参数设置,可调整
- )
- ui_mspi_tx_inst(
- .I_clk(I_clk), //系统时钟输入
- .I_rstn(I_rstn), //系统复位输入
- .O_spi_mosi(O_spi_mosi), //SPI发送数据串行总线
- .O_spi_sclk(O_spi_sclk), //SPI发送时钟总线
- .I_spi_tx_req(spi_tx_req), //SPI发送(写)数据请求
- .I_spi_tx_data(spi_tx_data), //SPI发送(写)数据
- .O_spi_busy(spi_busy) //SPI发送驱动器忙
- );
- endmodule
复制代码M_S状态机只有2个状态,M_S==0状态等待SPI-Master驱动器非忙的情况下,发送数据发送请求信号,并且在M_S==1状态等待数据确认进入忙状态后,再次回到状态0等待空闲,如果总线空闲发送下一个测试数据。 3RTL仿真
3.1仿真激励文件Modelsim仿真的创建过程不再重复,如有不清楚的请看前面实验 本实验以仿真的方式演示,仿真激励信号提供一个系统时钟即可 - `timescale 1ns / 1ps
- module master_spi_tb;
- localparam SYS_TIME = 'd20;//时钟周期,以ns为单位
- reg I_sysclk; //系统时钟
- reg rstn_i;
- wire spi_sclk_o;
- wire spi_mosi_o;
- spi_master_tx#
- (
- .CLK_DIV(100) //设置时钟参数,可以减少仿真时间
- )
- spi_master_tx_inst(
- .I_clk(I_sysclk),
- .I_rstn(rstn_i),
- .O_spi_sclk(spi_sclk_o),
- .O_spi_mosi(spi_mosi_o)
- );
- initial begin
- I_sysclk = 1'b0; //设置时钟基础值
- rstn_i = 1'b0; //低电平复位
- #100;
- rstn_i = 1'b1; //复位释放
- #2000000 $finish;
- end
-
- always #(SYS_TIME/2) I_sysclk = ~I_sysclk; //产生主时钟
- endmodule
复制代码以下启动modelsim仿真 3.2SPI发送驱动代码仿真CPHA=0 CPOL=0如下图所示,当CPHA=0 CPOL=0,代表SPI的SCLK默认是低电平,SPI接收器在SCLK第1个时钟沿采样。SPI发送驱动器数据在SCLK的第2个时钟沿更新,确保SPI下一个SCLK的第1个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。
3.3SPI发送驱动代码仿真CPHA=1 CPOL=0如下图所示,当CPHA=1 CPOL=0,代表SPI的SCLK默认是低电平,SPI接收器在SCLK第2个时钟沿采样。SPI发送驱动器数据在下一个SCLK的第1个时钟沿更新,确保SPI下一个SCLK的第2个时钟沿数据有足够的建立和保持时间。下图以发送8’h02为例。
3.4SPI发送驱动代码仿真CPHA=0 CPOL=1和CPHA=0 CPOL=0这种设置相比,时钟SCLK取反
3.5SPI发送驱动代码仿真CPHA=1 CPOL=1和CPHA=1 CPOL=0这种设置相比,时钟SCLK取反
|