在今年二月份的时候我写了一篇关于SPI模式的Verilog代码实现的博客(原文 ),当时由于时间关系,我只测试了SPI的一种通信模式(CPOL = 0, CPHA = 0),在该模式下通信正常,但是其它模式没有进行仔细测试,经网友提醒发现该代码在其他模式下会出现数据错位的情况,于是我花了一点时间仔细分析了SPI的工作模式,并重写了SPI主机代码,相对于以前的程序,该版本更为简洁、易懂,同时,在新的程序中,我使用了输入信号来告诉模块需要发送的数据位宽,实现动态的改变需要发送的数据位数。
关于SPI的通信时序,我也是参考其他博主的博文进行学习,这里我贴上一个我认为写的比较好的博文,供需要的网友学习,博文地址:SPI总线协议及SPI时序图详解。 在这里我主要分析一下怎么在四种SPI通信模式的时序中提取出共同特点,以方便我们编写出通用性高、简洁的代码。
在上图中,我画出了CPOL、CPHA取不同值时的时钟、数据波形图,当CPOL、CPHA取不同值时,一共有四种组合,也即组成了SPI的四种通信模式。 在分析之前,我们先规定以数据变化的时钟边沿为0时刻,参考上图,当CPHA = 0时,以红色竖线所对应的时钟边沿为0时刻,当CPHA = 1时,以蓝色竖线所对应的时钟边沿为0时刻。 1.时钟0时刻电平与CPOL、CPHA的关系 对于SPI通信而言,SCK时钟线并不是一直有时钟,而是只有在通信的时候才有时钟产生,因此,我们就需要确定当SPI总线空闲的时候,也就是0时刻的时候,SCK时钟线处于什么电平,我们分析上图可以得出以下四条结论:
CPOL = 0,CPHA = 0时,0时刻SCK电平为低电平;
CPOL = 0,CPHA = 1时,0时刻SCK电平为高电平;
CPOL = 1,CPHA = 0时,0时刻SCK电平为高电平;
CPOL = 1,CPHA = 1时,0时刻SCK电平为低电平。 由以上四条结论我们又可以总结出SCK的空闲电平与CPOL、CPHA的关系为: SCKIDLE = CPOL^CPHA 2.数据采样、输出与时钟边沿的关系 要使代码具有通用性,就需要知道不同模式下,数据的采样、输出与时钟边沿的关系,以及有什么共同的特点,我们分析上图同样可以得出四条结论(以0时刻开始分析):
CPOL = 0,CPHA = 0,数据在SCK的上升沿采样,下降沿输出;
CPOL = 0,CPHA = 1,数据在SCK的上升沿输出,下降沿采样;
CPOL = 1,CPHA = 0,数据在SCK的上升沿输出,下降沿采样;
CPOL = 1,CPHA = 1,数据在SCK的上升沿采样,下降沿输出。 由以上四条结论我们可以总结出:CPOL^CPHA为真时,数据在SCK的上升沿输出,下降沿采样,CPOL ^CPHA为假时,数据在SCK的上升沿采样,下降沿输出。但是实际我们在编写SPI主机代码的时候,SCK的时钟往往都是由分频器产生的,由于前面我们分析的不同模式SCK的空闲电平不一样,如果以SCK的上升沿、下降沿来确定数据的采样以及输出,就会带来额外的复杂度,因为空闲电平不一样,产生上升沿及下降沿的时刻也不一样。因此,我们需要进一步提取出共同点,仔细分析通信时序,我们可以得到一个非常简洁的结论:对于任意的CPOL、CPHA组合,数据采样都在第一个时钟边沿进行,数据输出都在第二个时钟边沿进行!! 程序中我们更容易知道什么时候SCK进行第一次变化,什么时候进行第二次变化,这就给我们程序的编写带来了极大的方便。
对于SPI时钟模块,主要的功能为产生指定频率的SCK时钟,以及输出控制信号给SPI控制模块使用,该模块代码比较简单,模块输入输出信号及功能由下表所示:
信号名称方向位宽功能Clk_I输入1模块时钟RstP_I输入1模块复位信号,高电平有效SPI_InCtrl_O输出1SPI主机数据采样脉冲SPI_OutCtrl_O输出1SPI主机数据输出脉冲SPI_SCK_O输出1SPI主机SCK时钟同时有四个入口参数,由下表所示:
参数名称参数功能CLK_FREQ输入时钟频率,单位为MHzCPOLSPI参数,时钟极性CPHASPI参数,时钟相位SPI_CLK_FREQSPI时钟频率,单位为Hz在该模块中,我们使用计数的方式来产生指定频率的时钟,首先使用入口参数来确定我们计数器的归零值,计算方式如下:
/* 时钟分频计数器 */ localparam CLK_DIV_CNT = (CLK_FREQ * 1000) / SPI_CLK_FREQ;使用位宽计算函数来确定计数器所需要的位宽:
/* 位宽计算函数 */ function integer clogb2(input integer depth); begin for(clogb2 = 0; depth > 0; clogb2 = clogb2 + 1) depth = depth >> 1; end endfunction reg[clogb2(CLK_DIV_CNT) - 1 : 0] ClkDivCnt;最重要的,使用入口参数CPOL、CPHA来确定时钟的空闲电平:
/* 根据SPI模式来决定SCK空闲的极性 ___ ___ ___ ___ ___ CPOL = 0: ___| |___| |___| |___| |___| ___ ___ ___ ___ ___ CPOL = 1: |___| |___| |___| |___| |___ _______ _______ _______ _______ _______ CPHA = 0: X_______X_______X_______X_______X_______X ___ _______ _______ _______ _______ ___ CPHA = 1: ___X_______X_______X_______X_______X___ 分析SPI模式对应的波形图,由数据线变化的时钟沿为起始时钟,可以得到以下分析结果: 1、当CPOL = 0,CPHA = 1以及CPOL = 1,CPHA = 0时,SCK的起始电平为高电平。 2、当CPOL = 0,CPHA = 0以及CPOL = 1,CPHA = 1时,SCK的起始电平的低电平。 3、主机及从机都在时钟的奇数边沿对数据进行采样,偶数边沿进行数据的变化。 由以上分析结果我们可以知道: 1、SCK的起始电平,也即空闲电平为CPOL^CPHA; 2、SPI_InCtrl(数据采样信号)为时钟的奇数边沿,SPI_OutCtrl(数据变化信号)为时钟的偶数边沿。 */ localparam SCK_IDLE = CPHA ^ CPOL;至此,我们就确定的模块的主要参数,由我们之前分析的结果,SPI主机的数据采样边沿为第一个时钟边沿,输出边沿为第二个时钟边沿,同时我们所计算的计数器归零值为一个SCK周期的计数值,因此,我们在计数器计数到CLK_DIV_CNT/2的时候输出第一个边沿,也就是主机的采样边沿,计数到CLK_DIV_CNT的时候输出第二个时钟边沿,也就是主机的输出边沿。这样,我们就不需要额外的判断SCK的上升沿或者下降沿。
/* 时钟分频计数器控制块 */ always@(posedge Clk_I) begin if(RstP_I) begin ClkDivCnt <= 0; end else if(ClkDivCnt == CLK_DIV_CNT - 1) begin ClkDivCnt <= 0; end else begin ClkDivCnt <= ClkDivCnt + 1'b1; end end /* SCK控制块 */ always@(posedge Clk_I) begin if(RstP_I) begin SCK <= SCK_IDLE; end else if(ClkDivCnt == CLK_DIV_CNT - 1 || (ClkDivCnt == (CLK_DIV_CNT >> 1) - 1)) begin SCK <= ~SCK; end else begin SCK <= SCK; end end /* 数据输出信号 */ always@(posedge Clk_I) begin if(RstP_I) begin SPI_OutCtrl <= 1'b0; end else begin SPI_OutCtrl <= (ClkDivCnt == CLK_DIV_CNT - 2) ? 1'b1 : 1'b0; end end /* 数据采样信号 */ always@(posedge Clk_I) begin if(RstP_I) begin SPI_InCtrl <= 1'b0; end else begin SPI_InCtrl <= (ClkDivCnt == (CLK_DIV_CNT >> 1) - 2) ? 1'b1 : 1'b0; end endSPI主机控制模块负责数据的采样、输出控制,模块输入输出信号由下表所示:
信号名称方向位宽功能Clk_I输入1模块时钟RstP_I输入1模块复位信号,高电平有效WrRdReq_I输入1数据发送、读取请求,高电平有效WrRdReqAck_O输出1模块对发送、读取请求的ACKWrRdFinish_O输出1数据读取、发送完成信号WrRdDataBits_I输入7要发送、读取的数据位宽WrData_I输入32要发送的数据RdData_O输出32读取到的数据SPI_InCtrl_I输入1数据采样脉冲SPI_OutCtrl_I输出1数据输出脉冲MOSI_O输出1MOSIMISO_I输入1MISOCS_O输出1CS该模块由状态机驱动,状态定义及转移请看代码:
/* 主状态机 */ always@(posedge Clk_I) begin if(RstP_I) begin CurrentState <= S_RST; end else begin CurrentState <= NextState; end end always@(*) begin NextState = S_RST; case(CurrentState) S_RST: NextState = S_IDLE; /* 复位状态,在该状态对所有寄存器进行复位 */ /* 检测到数据读写请求,则跳转到数据锁存状态 */ S_IDLE: NextState = (WrRdReq_I) ? S_ACK: S_IDLE; /* 拉高信号忙,并等待外部请求拉低 */ S_ACK: NextState = (WrRdReq_I) ? S_ACK : S_RUN; /* 等待数据发送完成 */ S_RUN: NextState = (WrRdBitsCnt == WrRdBitsLatch) ? S_END : S_RUN; /* 在该状态对外发送一个时钟周期宽度的脉冲信号,表示读取数据完成 */ S_END: NextState = S_IDLE; default: NextState = S_RST; endcase end数据输出:
/* 发送数据控制块 */ always@(posedge Clk_I) begin case(CurrentState) S_RST, S_IDLE: WrDataLatch <= 32'd0; S_ACK: WrDataLatch <= WrData_I; /* 先保存需要发送的数据 */ S_RUN: begin if(SPI_OutCtrl_I) begin WrDataLatch <= {WrDataLatch[30:0], 1'b0}; end else begin WrDataLatch <= WrDataLatch; end end default: WrDataLatch <= 32'd0; endcase end数据采样:
/* 接收数据控制块 */ always@(posedge Clk_I) begin case(CurrentState) S_RST, S_ACK: RdDataLatch <= 32'd0; S_RUN: begin if(SPI_InCtrl_I) begin RdDataLatch <= {RdDataLatch[30:0], MISO_I}; end else begin RdDataLatch <= RdDataLatch; end end default: RdDataLatch <= RdDataLatch; endcase endSPI Master模块主要完成SPI_Master_Clock模块与SPI_Master_Ctrl模块的连线,其整体的RTL级视图如图所示: 在这里我直接将CS信号作为SPI_Master_Colck模块的复位信号,这样SPI_Master_Ctrl模块里面就不需要额外的处理对SCK时钟的使能信号。
发送数据位数为32位,SPI频率为8MHz,可以看到SPI主机发送的数据SPI从机正确接收,SPI从机发送的数据主机也正确接收。
发送32位数据 发送8位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位
发送32位数据 发送24位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位 发送8位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位 由以上仿真结果可以看出,SPI主机模块对于四种通信模式都能正常工作,同时对于动态改变位宽也能正常功能。
这篇博客是我上一篇关于SPI主机模块的更新文章,间隔了有半年时间,这半年间也对FPGA的代码理解也更为深入,因此也觉得曾经写的主机代码不是很好,也就没有去更新那篇博文,而是另外写了这一篇,在这份代码里也肯定有我没有考虑到的地方,也希望有发现的网友能够指出错误,共同学习。相关代码我已上传论坛,有需要的可以去下载。