欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-18 13:46:22
...

一、前言

      最近在使用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所示(由于电脑没有装啥画图软件,就用平板手画了一下,各位将就看下吧,能看懂就行)。

Verilog实现的SPI通信协议(主机模式)
图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所示:

Verilog实现的SPI通信协议(主机模式)
图2 状态时序图

3、状态跳转分析

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

Verilog实现的SPI通信协议(主机模式)
图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;

/* 时钟分频计数器控制块 */
aaa@qq.com(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控制块 */
aaa@qq.com(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上升沿检测块 */
aaa@qq.com(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下降沿检测块 */
aaa@qq.com(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)状态机部分

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

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

aaa@qq.com(*) 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模式下输出数据的时刻不同,我这里都有进行处理。

/* 发送数据控制块 */
aaa@qq.com(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)数据接收部分

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

/* 接收数据控制块 */
aaa@qq.com(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通信协议(主机模式)
图4 8位SPI仿真结果

 

Verilog实现的SPI通信协议(主机模式)
图5 16位SPI仿真结果
Verilog实现的SPI通信协议(主机模式)
图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);
aaa@qq.com(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

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

aaa@qq.com(*) 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


/* 发送数据控制块 */
aaa@qq.com(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

/* 接收数据控制块 */
aaa@qq.com(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

/* 时钟边沿计数块 */
aaa@qq.com(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