接口/总线/驱动
上一篇文章非常简单的介绍了我们配置芯片寄存器的SPI协议的基础内容,接下来我们就需要开始写verilog描述出相关的电路。
写verilog第一步肯定需要将输入输出端口,常量等信息补齐全;
设置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个状态,
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值;
所以我们需要判断,
当SPI_CLK为低电平并且传输还没有结束时,我们需要将SPI_CLK拉高,将需要的发送数据串行数据更新到SPI_DI,更新计数器值;
而当SPI_CLK为高电平并且传输还没有结束时,我们仅需要将SPI_CLK拉低,保持SPI_DI不变。
READ状态里,我们仅需要在SPI_CLK时钟的下降沿采样SPI_DO值;即
当SPI_CLK为高电平并且传输还没有结束时,我们需要将SPI_CLK拉低,将SPI_DO采样并保存至read_data,更新计数器值;
而当SPI_CLK为低电平并且传输还没有结束时,我们仅需要将SPI_CLK拉高。整个电路的流程就已经被我们用verilog描述出来了。
使用VCS仿真之后的结果,可以见下图:
希望通过这一版简单的讲解,能让大家对SPI的verilog描述有更加清晰的认识。
这一版本的SPI最终上板测试没有问题,但确实还存在一些问题,不推荐使用生成时钟做SPI_CLK, 如果还有更好的建议可以提出来一起讨论。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !