Verilog实现SPI主机通信

前言

在今年二月份的时候我写了一篇关于SPI模式的Verilog代码实现的博客(原文 ),当时由于时间关系,我只测试了SPI的一种通信模式(CPOL = 0, CPHA = 0),在该模式下通信正常,但是其它模式没有进行仔细测试,经网友提醒发现该代码在其他模式下会出现数据错位的情况,于是我花了一点时间仔细分析了SPI的工作模式,并重写了SPI主机代码,相对于以前的程序,该版本更为简洁、易懂,同时,在新的程序中,我使用了输入信号来告诉模块需要发送的数据位宽,实现动态的改变需要发送的数据位数。

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时钟模块

对于SPI时钟模块,主要的功能为产生指定频率的SCK时钟,以及输出控制信号给SPI控制模块使用,该模块代码比较简单,模块输入输出信号及功能由下表所示:

信号名称方向位宽功能
Clk_I输入1模块时钟
RstP_I输入1模块复位信号,高电平有效
SPI_InCtrl_O输出1SPI主机数据采样脉冲
SPI_OutCtrl_O输出1SPI主机数据输出脉冲
SPI_SCK_O输出1SPI主机SCK时钟

同时有四个入口参数,由下表所示:

参数名称参数功能
CLK_FREQ输入时钟频率,单位为MHz
CPOLSPI参数,时钟极性
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
end

SPI主机控制模块

SPI主机控制模块负责数据的采样、输出控制,模块输入输出信号由下表所示:

信号名称方向位宽功能
Clk_I输入1模块时钟
RstP_I输入1模块复位信号,高电平有效
WrRdReq_I输入1数据发送、读取请求,高电平有效
WrRdReqAck_O输出1模块对发送、读取请求的ACK
WrRdFinish_O输出1数据读取、发送完成信号
WrRdDataBits_I输入7要发送、读取的数据位宽
WrData_I输入32要发送的数据
RdData_O输出32读取到的数据
SPI_InCtrl_I输入1数据采样脉冲
SPI_OutCtrl_I输出1数据输出脉冲
MOSI_O输出1MOSI
MISO_I输入1MISO
CS_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
end

SPI Master顶层模块实现

SPI Master模块主要完成SPI_Master_Clock模块与SPI_Master_Ctrl模块的连线,其整体的RTL级视图如图所示:在这里插入图片描述
在这里我直接将CS信号作为SPI_Master_Colck模块的复位信号,这样SPI_Master_Ctrl模块里面就不需要额外的处理对SCK时钟的使能信号。

模块仿真

CPOL = 0,CPHA = 0

发送数据位数为32位,SPI频率为8MHz,可以看到SPI主机发送的数据SPI从机正确接收,SPI从机发送的数据主机也正确接收。
在这里插入图片描述

CPOL = 0,CPHA = 1

在这里插入图片描述

CPOL = 1,CPHA = 0

发送32位数据
在这里插入图片描述
发送8位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位
在这里插入图片描述

CPOL = 1,CPHA = 1

发送32位数据
在这里插入图片描述
发送24位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位
在这里插入图片描述
发送8位数据,注意,发送的有效数据放在高位,接收到的有效数据放在低位
在这里插入图片描述
由以上仿真结果可以看出,SPI主机模块对于四种通信模式都能正常工作,同时对于动态改变位宽也能正常功能。

总线

这篇博客是我上一篇关于SPI主机模块的更新文章,间隔了有半年时间,这半年间也对FPGA的代码理解也更为深入,因此也觉得曾经写的主机代码不是很好,也就没有去更新那篇博文,而是另外写了这一篇,在这份代码里也肯定有我没有考虑到的地方,也希望有发现的网友能够指出错误,共同学习。相关代码我已上传论坛,有需要的可以去下载

  • 27
    点赞
  • 176
    收藏
    觉得还不错? 一键收藏
  • 35
    评论
以下是一个简单的Verilog代码实现SPI通信的例子: ``` module spi_master( input clk, //时钟信号 input rst, //复位信号 output reg mosi, //主机输出数据线 input miso, //从机输出数据线 output reg sclk, //串行时钟信号 output reg cs //从机芯片选择信号 ); //定义状态机状态 parameter IDLE = 2'b00; parameter SEND = 2'b01; parameter RECV = 2'b10; parameter DONE = 2'b11; reg [1:0] state; //状态寄存器 reg [7:0] tx_data; //传输数据寄存器 reg [7:0] rx_data; //接收数据寄存器 //初始状态为IDLE initial state = IDLE; always @(posedge clk) begin if (rst) begin state <= IDLE; mosi <= 0; sclk <= 0; cs <= 1; tx_data <= 0; rx_data <= 0; end else begin case(state) IDLE: begin mosi <= 0; sclk <= 0; cs <= 1; if (tx_data != 0) begin state <= SEND; end else begin state <= RECV; end end SEND: begin mosi <= tx_data[7]; tx_data <= {tx_data[6:0], 1'b0}; sclk <= ~sclk; if (sclk == 1) begin if (tx_data == 0) begin state <= RECV; end end end RECV: begin sclk <= ~sclk; if (sclk == 1) begin rx_data <= {rx_data[6:0], miso}; if (tx_data == 0) begin state <= DONE; end else begin state <= SEND; end end end DONE: begin mosi <= 0; sclk <= 0; cs <= 1; state <= IDLE; end endcase end end endmodule ``` 上述代码实现了一个简单的SPI主机,可以通过调用tx_data和读取rx_data来进行数据传输。在时钟信号的上升沿或下降沿,根据状态机的状态来控制输出数据、接收数据和时钟信号的变化。需要注意的是,这个代码只能进行简单的8位数据传输,如果需要传输更多的数据,需要进行相应的修改。
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值