Verilog实现的SPI通信协议(主机模式)

一、前言

      最近在使用FPGA调试一个MCP2515CAN芯片的时候,需要用到SPI通信协议,也在网上看了许多不同人写的博客,也学习了很多种不同的写法,从结果来看,网上给出的大部分例子都能实现SPI通信协议,但是我也发现了一个共同的问题,就是很多人在实现SPI协议的实现都使用了状态机,而且是一个很长的状态机,每发送一位就有一个状态,这就会导致代码看起来特别长,各个信号的逻辑关系也比较混乱,同时,网上例程实现的大都是8位的SPI协议,然而有些器件通信协议并不一定是8位的,就比如说我使用的这个MCP2515芯片,从器件手册里可以看到该芯片的为32位的SPI(就是在一个CS有效期间,发送的数据位数,一般而言都是一次发送一个字节的数据,对应一次CS有效信号,但是该芯片是一次发送4个字节的数据,对应一次CS有效),如果还使用一位对应一个状态的写法,那代码将会变得更复杂。因此我在这里分享一种使用状态量少、且可配置位宽的SPI协议的写法,相互学习,对于SPI时序的讲解,网上已经有很多人讲得够好了,我就不在这里献丑了。由于本人学习FPGA的时间也不是很长,程序中难免会有错误的地方,如果有人发现有不对地方,希望各位能指出。

二、模块的划分

       通过对SPI的通信时序分析,可以将SPI模块分为两个部分,一个是SPI_Clock模块,负责产生SPI通信所需要的SCK,同时将SCK的两个边沿以脉冲形式输出,以供SPI_Master模块接收及发送数据使用,加一个是SPI_Master模块,负责接收的发送及接收,同时控制SPI_Clock模块SCK时钟信号的输出,其框图如图1所示(由于电脑没有装啥画图软件,就用平板手画了一下,各位将就看下吧,能看懂就行)。

图1 SPI模块框图

其中模块输入输出信号分别为:

  1. CLK:系统时钟信号
  2. Rst:模块复位信号
  3. WrRdReq: 数据读写请求,上升沿有效
  4. WrData: 要发送的数据
  5. RdData: 读取到的数据
  6. DataValid: 读取数据有效信号,脉冲输出,宽度为一个CLK
  7. Busy: 模块忙信号

三、状态的划分

        从我看的FPGA书或者其他人写的代码来看,一个FPGA程序很重要的部分就是状态机,基本上每一个FPGA程序中都会有状态机的身影,由此可以看出状态机对于一个FPGA程序的重要性,好的状态划分可以让程序写起来更轻松、更合理,因此在开始写SPI的代码之前我们需要先进行状态的划分,且不可粗暴的按照一位一个状态来分。

1、SPI_Clock模块

    该模块仅仅将CLK时钟输入进行分频,无需状态机。

2、SPI_Master模块

        对于SPI通信来讲,其实就两种状态,一是空闲状态,没有数据需要发送或者接收,二就是工作状态(SPI在发送数据的同时也在接收数据)。但是在数据接收完成后需要对外输出一个DataValid脉冲信号,因此我们将输出数据有效脉冲单独划分为一个状态,同时需要一个时钟周期将要发送的数据锁存下来,故一共有四个状态,分别为:

  1. IDLE: 空闲状态;
  2. START: 启动状态,进行数据的锁存;
  3. RUNNING: 运行状态,接收(发送)数据中;
  4. DELIVER: 数据转发状态,输出数据有效脉冲信号;

对应到时序图如图2所示:

图2 状态时序图

3、状态跳转分析

        在对状态进行划分之后,就需要清楚的知道各个状态间跳转的条件,对各个状态进行分析后可以知道,在模块没有动作时,即没有数据需要发送、接收时,模块处于IDLE状态;当模块处于IDLE状态时,如果检测到数据发送或接收请求,则跳转到START状态,将要发送的数据锁存下来;当模块处于START状态时,立即跳转到RUNNING状态,进行数据的发送及接收;当模块处于RUNNING状态时,如果检测到数据已经发送(接收)完成,则跳转到DELIVER状态;当模块处于DELIVER状态时,立即跳转到IDLE状态。由此,我们可以画出如图3所示状态跳转图:

图3 状态跳转图

        那如何检测数据发送接收请求呢,我这里使用的是上升沿有效,即检测到WrRdReq有上升沿时,认为有数据需要发送;对于数据数据发送完成的检测,由图2以及SPI发送及接收的原理可知,对于8位SPI而言,当统计到16个SCK时钟边沿时,代表数据已经发送(接收完成),同时对于位宽为DATA_WIDTH为SPI,当统计到DATA_WIDTH*2个SCK时钟边沿时,代表数据已经发送完成,由此,我们需要对SCK的边沿进行计数,已判断数据的发送(接收)状态。

四、Verilog程序的编写

1、SPI_Clock.v

        由于SPI_Clock.v的代码比较简单,就直接贴代码了,不进行分析了。

/**
  *******************************************************************************************************
  * File Name: SPI_Clock.v
  * Author: NUC-何鑫
  * Version: V1.0.0
  * Date: 2019-8-28
  * Brief: SPI时钟发生模块
  *******************************************************************************************************
  * History
  *		1.Author: NUC-何鑫
  *		  Date: 2019-8-28
  *		  Mod: 发布第一版
  *
  *******************************************************************************************************
  */
module SPI_Clock#
(
	parameter	CLK_FREQ        = 50,
    parameter   CPOL            = 1'b0,
	parameter	SPI_CLK_FREQ    = 1000
)
(
	input       Clk_I,
	input       RstP_I,
	input       En_I,
	output      SCK_O,
	output      SCKEdge1_O,		    /* 时钟的第一个跳变沿 */
	output      SCKEdge2_O			/* 时钟的第二个跳变沿 */
);
/* SPI时序说明:1、当CPOL=1时,SCK在空闲时候为低电平,第一个跳变为上升沿
				2、当CPOL=0时,SCK在空闲时为高电平,第一个跳变为下降沿
*/

/* 时钟分频计数器 */
localparam	CLK_DIV_CNT = (CLK_FREQ * 1000)/SPI_CLK_FREQ;

reg         SCK;
reg         SCK_Pdg, SCK_Ndg;
reg[31:0]	ClkDivCnt;

/* 时钟分频计数器控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
	if(RstP_I)
		ClkDivCnt <= 32'd0;
	else if(!En_I)
        ClkDivCnt <= 32'd0;
    else begin
        if(ClkDivCnt == CLK_DIV_CNT - 1)
            ClkDivCnt <= 32'd0;
        else
            ClkDivCnt <= ClkDivCnt + 1'b1;
    end
end

/* SCK控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
	if(RstP_I)
        SCK <= (CPOL) ? 1'b1 : 1'b0;
    else if(!En_I)
        SCK <= (CPOL) ? 1'b1 : 1'b0;
    else begin
        if(ClkDivCnt == CLK_DIV_CNT - 1 || (ClkDivCnt == (CLK_DIV_CNT >> 1) - 1))
            SCK <= ~SCK;
        else
            SCK <= SCK;
    end
end

/* SCK上升沿检测块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        SCK_Pdg <= 1'b0;
    else begin
        if(CPOL)
            SCK_Pdg <= (ClkDivCnt == CLK_DIV_CNT - 1) ? 1'b1 : 1'b0;
        else
            SCK_Pdg <= (ClkDivCnt == (CLK_DIV_CNT >> 1) - 1) ? 1'b1 : 1'b0;
    end
end

/* SCK下降沿检测块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        SCK_Ndg <= 1'b0;
    else begin
        if(CPOL)
            SCK_Ndg <= (ClkDivCnt == (CLK_DIV_CNT >> 1) - 1) ? 1'b1 : 1'b0;
        else
            SCK_Ndg <= (ClkDivCnt == CLK_DIV_CNT - 1) ? 1'b1 : 1'b0;
    end
end


/* 根据CPOL来选择边沿输出 */
assign SCKEdge1_O = (CPOL) ? SCK_Ndg : SCK_Pdg;
assign SCKEdge2_O = (CPOL) ? SCK_Pdg : SCK_Ndg;
assign SCK_O = SCK;
endmodule

2、SPI_Master.v

        该模块的代码分为不同部分进行编写,会让代码写起来逻辑更清晰,不易混乱,同时更易懂。

1)状态机部分

       状态机采用的是三段式状态机写法,至于具体什么是三段式状态机这里不做解释,想了解的朋友请自行百度,同时个人建议写状态时采用该写法,可将控制逻辑与状态机分离,使用代码逻辑性更强。

/* 主状态机 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        MainState <= IDLE;
    else
        MainState <= NxtMainState;
end

always@(*) begin
    NxtMainState = IDLE;
    case(MainState)
        IDLE: NxtMainState = (WrRdReq_Pdg) ? START: IDLE;
        START: NxtMainState = RUNNING;
        RUNNING: NxtMainState = (RecvDoneFlag) ? DELIVER : RUNNING;
        DELIVER: NxtMainState = IDLE;
        default: NxtMainState = IDLE;
    endcase
end

        在这里我没有写出WrRdReq_Pdg以及RecvDoneFlag的具体实现,但是前面已经分析过这两个信号是怎么来的了,需要具体代码的看最后整体代码部分。

2)数据发送部分

        数据发送部分比较简单,使用移位寄存器根据SPI的时钟进行数据移位就行了,同时需要注意的是高位或者低位在前,我这里使用的是高位在前,以及不同的SPI模式下输出数据的时刻不同,我这里都有进行处理。

/* 发送数据控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        WrDataLatch <= 0;
    else begin
        case(MainState)
            START: WrDataLatch <= Data_I;	/* 先保存需要发送的数据 */
            RUNNING: begin
                /* 如果CPHA=1,则在时钟的第一个边沿输出,否则在第二个边沿输出 */
                if(CPHA == 1'b1 && SCKEdge1)
                    WrDataLatch <= {WrDataLatch[DATA_WIDTH - 2:0], 1'b0};
                else if(CPHA == 1'b0 && SCKEdge2)
                    WrDataLatch <= {WrDataLatch[DATA_WIDTH - 2:0], 1'b0};
                else
                    WrDataLatch <= WrDataLatch;
            end
            default: WrDataLatch <= 0;
        endcase
    end
end

3)数据接收部分

        数据接收部分与数据发送数据部分一样,使用移位寄存器即可,注意事项同上。

/* 接收数据控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        RdDataLatch <= 0;
    else begin
        case(MainState)
            START: RdDataLatch <= 0;
            RUNNING: begin
                /* 如果CPHA = 1,则在时钟的每二个边沿对数据进行采样,
                   否则在第一个边沿采样 */
                if(CPHA == 1'b1 && SCKEdge2)	
                    RdDataLatch <= {RdDataLatch[DATA_WIDTH - 2:0], MISO_I};
                else if(CPHA == 1'b0 && SCKEdge1)
                    RdDataLatch <= {RdDataLatch[DATA_WIDTH - 2:0], MISO_I};
                else
                    RdDataLatch <= RdDataLatch;
            end
            default: RdDataLatch <= RdDataLatch;
        endcase
    end
end

4)其余信号部分

/* 接收完成标志 */
assign	RecvDoneFlag = (SCKEdgeCnt == DATA_WIDTH * 2);

/* 数据接收完成时输出一个时钟宽度的脉冲信号 */
assign DataValid_O = (MainState == DELIVER) ? 1'b1 : 1'b0;

/* 读取到的数据 */
assign Data_O = RdDataLatch;

/* 模块忙信号 */
assign Busy_O = (MainState == IDLE) ? 1'b0 : 1'b1;

/* 将要发送的数据发送到MOSI线上 */
assign MOSI_O = (MainState == RUNNING) ? WrDataLatch[DATA_WIDTH - 1] : 1'bz;

/* 片选 */
assign	CS_O = (MainState == RUNNING) ? 1'b0 : 1'b1;

/* SPI时钟使能信号 */
assign	SCKEnable = (MainState == RUNNING) ? 1'b1 : 1'b0;

五、仿真结果

        在这里我分别仿真了8位,16位,32位SPI,结果分别如图4,图5,图6所示。

Verilog实现的SPI通信协议(主机模式)_第1张图片 图4 8位SPI仿真结果

 

Verilog实现的SPI通信协议(主机模式)_第2张图片 图5 16位SPI仿真结果
Verilog实现的SPI通信协议(主机模式)_第3张图片 图6 32位SPI仿真结果

        由仿真结果看,不同位宽的数据,都能正确的进行收发,同时经过我实际使用,代码也是没有问题的。    当然,我这里仅仅仿真了模式0,即CPOL,CPHA都为0,因为我实际中使用的也是这个模式,  但是代码中我对不同的模式都是有进行处理的,但是我没有进行仿真,如果有人发现在其余模式下该代码无法使用,希望可以留言指出,我会对其进行改正。

六、总结

        在这篇博客里我简单的写了一下我所实现的SPI,并没有对SPI的原理进行具体的分析,如果有人想对SPI通信原理进行更基础的了解,需要去查找相关资料。同时我这种写法也不一定好,希望可以起到一个抛砖引玉的作用。

七、整体代码

/**
  *******************************************************************************************************
  * File Name: SPI_Master.v
  * Author: NUC-何鑫
  * Version: V1.0.0
  * Date: 2019-8-28
  * Brief: SPI主机模块代码
  *******************************************************************************************************
  * History
  *		1.Author: NUC-何鑫
  *		  Date: 2019-8-28
  *		  Mod: 发布第一版
  *
  *		2.Author: NUC-何鑫
  *		  Date: 2020-2-7
  *		  Mod: 优化控制逻辑,添加SPI片选信号,增加数据宽度可配置功能
  *
  *******************************************************************************************************
  */
module SPI_Master#
(
	parameter	CLK_FREQ = 50,			/* 模块时钟输入,单位为MHz */
	parameter	SPI_CLK = 1000,		    /* SPI时钟频率,单位为KHz */
	parameter	CPOL = 0,				/* SPI时钟极性控制 */
	parameter	CPHA = 0,				/* SPI时钟相位控制 */
	
	parameter	DATA_WIDTH = 8			/* 数据宽度 */
)
(
	input       Clk_I,			/* 模块时钟输入,应和CLK_FREQ一样 */
	input       RstP_I,			/* 异步复位信号,低电平有效 */
	
	input       WrRdReq_I,		/* 读/写数据请求 */	
	input[DATA_WIDTH - 1:0]		Data_I,		    /* 要写入的数据 */
	output[DATA_WIDTH - 1:0]	Data_O,		    /* 读取到的数据 */
	output	    DataValid_O,	/* 读取数据有效,上升沿有效 */
	output 	    Busy_O,			/* 模块忙信号 */
    
	output	    SCK_O,			/* SPI模块时钟输出 */
	output	    MOSI_O,			/* MOSI_O */
	input	    MISO_I,			/* MISO_I  */
	output		CS_O
);

localparam	    IDLE 	= 0;		/* 模块空闲 */
localparam	    START	= 1;
localparam	    RUNNING	= 2;		/* 模块运行中 */
localparam	    DELIVER	= 3;		/* 数据转发 */


reg[7:0]        MainState, NxtMainState;
wire	        SCKEdge1, SCKEdge2;
wire 	        SCKEnable;
wire			RecvDoneFlag;
reg[7:0]	    SCKEdgeCnt;


reg[DATA_WIDTH - 1:0]	    WrDataLatch;
reg[DATA_WIDTH - 1:0]	    RdDataLatch;

/* 读写信号上升沿检测 */
wire 	        WrRdReq_Pdg;					
reg 	        WrRdReq_D0, WrRdReq_D1;



/* 检测写请求的上升沿 */
assign	WrRdReq_Pdg = (WrRdReq_D0) && (~WrRdReq_D1);
always@(posedge Clk_I or posedge RstP_I) begin
	if(RstP_I) begin	
        WrRdReq_D0 <= 1'b0;
        WrRdReq_D1 <= 1'b0;
	end	else begin
        WrRdReq_D0 <= WrRdReq_I;
        WrRdReq_D1 <= WrRdReq_D0;
	end
end

/* 主状态机 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        MainState <= IDLE;
    else
        MainState <= NxtMainState;
end

always@(*) begin
    NxtMainState = IDLE;
    case(MainState)
        IDLE: NxtMainState = (WrRdReq_Pdg) ? START: IDLE;
        START: NxtMainState = RUNNING;
        RUNNING: NxtMainState = (RecvDoneFlag) ? DELIVER : RUNNING;
        DELIVER: NxtMainState = IDLE;
        default: NxtMainState = IDLE;
    endcase
end


/* 发送数据控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        WrDataLatch <= 0;
    else begin
        case(MainState)
            START: WrDataLatch <= Data_I;	/* 先保存需要发送的数据 */
            RUNNING: begin
                /* 如果CPHA=1,则在时钟的第一个边沿输出,否则在第二个边沿输出 */
                if(CPHA == 1'b1 && SCKEdge1)
                    WrDataLatch <= {WrDataLatch[DATA_WIDTH - 2:0], 1'b0};
                else if(CPHA == 1'b0 && SCKEdge2)
                    WrDataLatch <= {WrDataLatch[DATA_WIDTH - 2:0], 1'b0};
                else
                    WrDataLatch <= WrDataLatch;
            end
            default: WrDataLatch <= 0;
        endcase
    end
end

/* 接收数据控制块 */
always@(posedge Clk_I or posedge RstP_I) begin
    if(RstP_I)
        RdDataLatch <= 0;
    else begin
        case(MainState)
            START: RdDataLatch <= 0;
            RUNNING: begin
                /* 如果CPHA = 1,则在时钟的每二个边沿对数据进行采样,
                   否则在第一个边沿采样 */
                if(CPHA == 1'b1 && SCKEdge2)	
                    RdDataLatch <= {RdDataLatch[DATA_WIDTH - 2:0], MISO_I};
                else if(CPHA == 1'b0 && SCKEdge1)
                    RdDataLatch <= {RdDataLatch[DATA_WIDTH - 2:0], MISO_I};
                else
                    RdDataLatch <= RdDataLatch;
            end
            default: RdDataLatch <= RdDataLatch;
        endcase
    end
end

/* 时钟边沿计数块 */
always@(posedge Clk_I or posedge RstP_I) begin
	if(RstP_I)
		SCKEdgeCnt <= 7'd0;
	else begin
		case(MainState)
			RUNNING: begin
				if(SCKEdge1 || SCKEdge2)		/* 统计两个时钟边沿数量 */
					SCKEdgeCnt <= SCKEdgeCnt + 1'b1;
				else
					SCKEdgeCnt <= SCKEdgeCnt;
			end
			default: SCKEdgeCnt <= 7'd0;
		endcase
	end
end

/* 接收完成标志 */
assign	RecvDoneFlag = (SCKEdgeCnt == DATA_WIDTH * 2);

/* 数据接收完成时输出一个时钟宽度的脉冲信号 */
assign DataValid_O = (MainState == DELIVER) ? 1'b1 : 1'b0;

/* 读取到的数据 */
assign Data_O = RdDataLatch;

/* 模块忙信号 */
assign Busy_O = (MainState == IDLE) ? 1'b0 : 1'b1;

/* 将要发送的数据发送到MOSI线上 */
assign MOSI_O = (MainState == RUNNING) ? WrDataLatch[DATA_WIDTH - 1] : 1'bz;

/* 片选 */
assign	CS_O = (MainState == RUNNING) ? 1'b0 : 1'b1;

/* SPI时钟使能信号 */
assign	SCKEnable = (MainState == RUNNING) ? 1'b1 : 1'b0;

/* 实例化一个SPI时钟模块 */
SPI_Clock#
(
	.CLK_FREQ(CLK_FREQ),
    .CPOL(CPOL),
	.SPI_CLK_FREQ(SPI_CLK)
)
SPI_Clock_Inst 
( 
	.En_I(SCKEnable),
	.Clk_I(Clk_I),
	.SCKEdge1_O(SCKEdge1),
	.SCKEdge2_O(SCKEdge2),
	.RstP_I(RstP_I),
	.SCK_O(SCK_O)
);
endmodule

 

你可能感兴趣的:(程序调试)