Verilog实现讲解

电子说

1.3w人已加入

描述

写verilog第一步肯定需要将输入输出端口,常量等信息补齐全;

module spi_ctrl
#(
parameter	SPI_ADDR_WIDTH = 16,
parameter	SPI_CMD_WIDTH  = 24,
parameter	SPI_IDLE = 0
)
(
	/* System signal */
	input					clk			,
	input					rst			,
	
	/* User Data */
	input		[23:0]			cmd_data		,
	output 	reg	[ 7:0]			read_data		,
	input					en			,
	input					ready			,
	output	reg				sink_vld		,
	
	/* SPI interface */
	output	reg			 	spi_clk			,
	output	reg				spi_enb			,
	output	reg				spi_di			,
	input					spi_do
);

设置SPI_ADDR_WIDTH标记SPI传输数据命令的寄存器地址值宽度,SPI_CMD_WIDTH变量标记SPI传输数据的整体宽度。

输入输出变量中,clk时钟信号和rst复位信号都是必备的系统信号;除去SPI接口的4根数据线外,还有输入的24位cmd_data,将需要发送的数据从这个端口传递给SPI处理;输出的8位read_data,将读取到的寄存器数据输出便于做后续处理;以及控制信号,en控制SPI的工作速率,ready指示SPI发送工作的开始,sink_vld指示SPI发送读取工作的结束。

上一篇文章谈到,我们将整个SPI的发送读取分为5个状态,

VCS

SPI状态机的5种状态

现在我们需要捋顺每个状态跳转的条件;IDLE空闲状态跳转到WRITE_ADDR写地址状态,说明此时需要发送SPI数据,所以ready信号是跳转条件;从上图可以看到,WRITE_ADDR写地址状态可以跳转到WRITE_DATA状态或READ状态,而决定条件是SPI的命令是读取命令还是写入命令,这取决于SPI写入数据的MSB(最高位);WRITE_DATA状态和READ状态跳转回IDLE空闲状态的条件是需要发送的数据已经发送完毕或需要读取的数据已经读取完毕。

另外,在WRITE_ADDR写地址状态,WRITE_DATA状态和READ状态里面需要用到计数器,记录当前已经发送或读取的数据量,作为跳出该状态的判断依据之一。

由于这部分的状态机比较简单,所以第一版我采用了一段式状态机。为了便于理解SPI_CLK的产生,我选择使用分频操作生成SPI_CLK,但其实更推荐的方式是使用MMCM,PLL等方式产生SPI_CLK。

localparam	SPI_DATA_WIDTH  = SPI_CMD_WIDTH - SPI_ADDR_WIDTH;

assign  flag_write_addr_update = (cnt < SPI_ADDR_WIDTH && spi_clk == 1'b0) ? 1'b1 : 1'b0; 

assign	flag_write_addr_hold = (cnt < SPI_ADDR_WIDTH) ? 1'b1 : 1'b0 ;

assign	flag_data_update = (cnt < SPI_DATA_WIDTH && spi_clk == 1'b0) ? 1'b1 : 1'b0 ;

assign	flag_data_hold = (cnt < SPI_DATA_WIDTH) ? 1'b1 : 1'b0 ;

always @ (posedge clk or posedge rst)
begin
	if (rst)
	begin
		spi_clk		<=	SPI_IDLE	;
		spi_enb		<=	1'b1		;
		spi_di		<=	1'b0		;
		read_data	<=	'd0			;
		sink_vld	<=	1'b0		;
		state		<=	IDLE		;
		cmd_data_r	<=	'd0			;
		cnt			<=	'd0			;
		flag_read	<=	1'b0		;
	end
	else if (en)
	begin
		case (state)
		IDLE:
		begin
			if (ready)
			begin
				state		<= WRITE_ADDR		;
				spi_enb		<=	1'b0		;
				cmd_data_r	<=cmd_data		;
				cnt		<=	'd0		;
				flag_read 	<= !cmd_data[23]	;
			end
			sink_vld 	<=	1'b0;
			spi_di		<=	1'b0;
			spi_enb		<=	1'b1;
		end
		
		WRITE_ADDR:
		begin	
			spi_enb		<=	1'b0;
			if (flag_write_addr_update)
			begin
				spi_di		<=	cmd_data_r[23]			;
				cmd_data_r	<=	{cmd_data_r[22:0], 1'b0}	;
				spi_clk		<=	1'b1				;
				cnt			<=	cnt + 8'd1		;
			end	
			else if (flag_write_addr_hold)
			begin
				spi_clk	<=	1'b0;
			end
			else 
			begin
				if (flag_read)
					state	<=	READ		;
				else
					state	<=	WRITE_DATA	;
				cnt		<=	'd0		;
				spi_clk	<=	1'b0			;
			end
		end
		
		WRITE_DATA:
		begin
			if (flag_data_update)
			begin
				spi_di		<=	cmd_data_r[23]			;
				cmd_data_r	<=	{cmd_data_r[22:0], 1'b0}	;
				spi_clk		<=	1'b1				;
				cnt		<=	cnt + 8'd1			;
			end	
			else if (flag_data_hold)
			begin
				spi_clk		<=	1'b0	;
			end
			else 
			begin
				state 	<= 	IDLE	;
				spi_clk <=	1'b0	;
				sink_vld<=  	1'b1    ;
			end
		end
		
		READ:
		begin
			if (flag_data_update)
			begin
				spi_clk	<=	1'b1	;
			end	
			else if (flag_data_hold)
			begin
				spi_clk		<=	1'b0			;
				read_data[0]	<=	spi_do			;
				read_data[7:1]	<=	read_data[6:0]		;
				cnt		<=	cnt + 8'd1		;
			end
			else 
			begin
				state 		<= IDLE;
				sink_vld 	<= 1'b1;
			end
		end
		default : state <= IDLE;
		endcase
	end
end

从上一个文章里面,我们可以看到,在WRITE_ADDR写地址状态,WRITE_DATA状态里,我们需要在SPI_CLK时钟的上升沿更新SPI_DO值,在SPI_CLK下降沿保持SPI_DO值;

所以我们需要判断,

  1. 当SPI_CLK为低电平并且传输还没有结束时,我们需要将SPI_CLK拉高,将需要的发送数据串行数据更新到SPI_DI,更新计数器值;
  2. 而当SPI_CLK为高电平并且传输还没有结束时,我们仅需要将SPI_CLK拉低,保持SPI_DI不变。

READ状态里,我们仅需要在SPI_CLK时钟的下降沿采样SPI_DO值;即

  1. 当SPI_CLK为高电平并且传输还没有结束时,我们需要将SPI_CLK拉低,将SPI_DO采样并保存至read_data,更新计数器值;
  2. 而当SPI_CLK为低电平并且传输还没有结束时,我们仅需要将SPI_CLK拉高。整个电路的流程就已经被我们用verilog描述出来了。

使用VCS仿真之后的结果,可以见下图:

VCS

希望通过这一版简单的讲解,能让大家对SPI的verilog描述有更加清晰的认识。

这一版本的SPI最终上板测试没有问题,但确实还存在一些问题,不推荐使用生成时钟做SPI_CLK, 如果还有更好的建议可以提出来一起讨论。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分