【Day20】SPI的实现

上一篇我们设计了 SPI 的状态机,那麽我们今天要来引用 SPI 状态机模块来实现整个 SPI 的模块,并撰写 testbench 来验证电路的正确性。

先来看看这个 SPI 模块该有哪些输入输出脚吧:

输入:

  • clock_50M
  • rst_n
  • miso(master in, slave out)
  • en(外部给此模组的驱动信号(enable))
  • din(外部输入给此模组要传输的 16 bit 资料)

输出:

  • mosi(master out, slave in)
  • finish(让外部知道资料传输结束了,可以继续传下一笔)
  • SCLK
  • dout(将 miso 的资料蒐集起来并输出观测是否接收能正常)
  • CS(chip select)
module uasge_SPI_4wire(
  en, 
  clock_50M, 
  rst_n, 
  CS, 
  mosi, 
  miso, 
  din, 
  dout, 
  SCLK, 
  finish
);
/*-------ports declaration------*/
input         clock_50M;
input         rst_n;
input         miso;
input         en;
input  [15:0] din;
output [15:0] dout;
output        CS; 
output        mosi;
output        SCLK;
output        finish;
reg    [15:0] dout;
wire          CS; 
wire          mosi;
wire          SCLK;
wire          finish;

宣告变数:
需要用到的变数有:

  • counter(计数用 16 个 bit 用)
  • cnt_SPI(计数用(数 0 ~ 49),要做 tick_SPI)
  • regdata_i(load 进来的资料先暂存在这里)
  • regdata_o(用来存放 miso 的资料,并於传输结束後输出)
  • tick_SPI(50MHZ 的 tick)
  • 其他与状态机模组连接的变数
/*-------variables------*/
reg     [3:0] counter;
reg    [15:0] regdata_i;
reg    [15:0] regdata_o;
reg           tick_SPI;
reg           SCLK_temp;//mux of SCLK_temp or 1'b1
reg     [5:0] cnt_SPI;  //1MHZ counter
wire          countEN;
wire          rstcount;
wire          SHEN;
wire          LDEN;

引用状态机模组

/*-------module instantiate------*/
SPI U0(
  .clk_sys(clock_50M),
  .SCLK_temp(SCLK_temp), 
  .rst_n(rst_n),
  .tick_SPI(tick_SPI),
  .SCLK(SCLK),
  .CS(CS),
  .count(counter),
  .countEN(countEN),
  .rstcount(rstcount),
  .ready(en),
  .finish(finish),
  .SHEN(SHEN),
  .LDEN(LDEN)
);

cnt_SPI

/*-------1M counter------*/
always@(posedge clock_50M or negedge rst_n)begin
  if(!rst_n)cnt_SPI <= 6'd0;
  else begin
    if(cnt_SPI<6'd49)cnt_SPI <= cnt_SPI + 6'd1;
    else             cnt_SPI <= 6'd0;
  end
end

做出 SCLK_temp

/*-------make SCLK------*/
always@(posedge clock_50M or negedge rst_n)begin
  if(!rst_n)begin
    SCLK_temp <= 1'b0;
  end
  else begin
    if(cnt_SPI == 6'd24)     SCLK_temp <= 1'b0;
    else if(cnt_SPI == 6'd49)SCLK_temp <= 1'b1;
    else                     SCLK_temp <= SCLK_temp;
  end
end

为什麽这边是 24 跟 49 呢?因为 0-24 & 25-49 会刚好把 clk 切为一半,这样写就会分别在正缘且 cnt 为 24 & 49 时 SCLK_temp 反向。

tick_SPI

/*-------tick_SPI------*/
always@(posedge clock_50M or negedge rst_n)begin
  if(!rst_n)tick_SPI <= 1'b0;
  else begin
    if(cnt_SPI==6'd48)tick_SPI <= 1'b1;
    else              tick_SPI <= 1'b0;
  end
end

来解释一下位甚麽这边是 48,因为我想让其他动作有在 SCLK 负缘时动作的效果,而 SCLK 会在 cnt = 49 时发生负缘,所以如果我想让其他事情也在 cnt = 49 时动作,那麽我 tick 必须提前一个 clk 升起来才行,所以这里才是 48 而非 49。

靠 rstcount&countEN 来控制 counter

/*-------ctrl counter------*/
always@(posedge clock_50M or negedge rst_n)begin
  if(!rst_n)begin
    counter <= 4'd0;
  end
  else begin
    if(tick_SPI)begin
      if(rstcount)    counter <= 4'd0;
      else if(countEN)counter <= counter + 4'd1;
      else            counter <= 4'd0;
    end
    else counter <= counter;
  end
end

靠 LDEN&SHEN 来控制我们的 regdata_i

/*-------data in------*/
always@(posedge clock_50M or negedge rst_n)begin//SCLK's negedge
  if(!rst_n)begin
    regdata_i <= 16'd0;
  end
  else begin
    if(tick_SPI)begin
      if(LDEN)     regdata_i <= din;
      else if(SHEN)regdata_i <= {regdata_i[14:0], 1'b0};
      else         regdata_i <= regdata_i;
    end
    else regdata_i <= regdata_i;
  end
end

这里使用左移是因为 SPI 为 MSB 先传。

蒐集 miso 的 data

/*-------collect miso data------*/
always@(posedge clock_50M or negedge rst_n)begin//SCLK's posedge
  if(!rst_n)regdata_o <= 16'd0;
  else begin
    if(tick_SPI)begin
      if(SHEN)regdata_o <= {regdata_o[14:0], miso};
      else    regdata_o <= regdata_o;
    end
    else begin
      regdata_o <= regdata_o;
    end 
  end
end

这边来解释一下为什麽用 SCLK 的负缘收资料,因为我设计的模组是预想外部输入会在 SCLK 正缘时改变资料,所以为了确保资料的正确,要在 SCLK 的负缘收比较恰当。

於传输结束後将 dout 送出

/*-------data out------*/
always@(posedge clock_50M or negedge rst_n)begin
  if(!rst_n)begin
    dout <= 16'd0;
  end
  else begin
    if(finish)dout <= regdata_o;
    else      dout <= dout;
  end
end

endmodule

还有最後的 mosi 还没处理

/*-------assign wire------*/
assign mosi = (SHEN)?(regdata_i[15]):(1'b0);

当没有要传资料时其实给什麽值都行,因为此时 SCLK 没有在动作,也意味着 slave 端并不会收取资料。

Quartus 的 State Machine Viewer


TestBench

`timescale 1ns / 1ns

module spi_tb();
reg  sysClk, rst_n, tick_tx, miso;
reg  [7:0]address, txdata;
reg  [15:0]rxdata;
wire [15:0]data_get;
wire sclk, cs_n, mosi;
wire finish;
localparam Freq_i = 50000000;
localparam durTime = 1000;
integer i;

uasge_SPI_4wire UUT(
  .en(tick_tx),
  .clock_50M(sysClk),
  .rst_n(rst_n),
  .CS(cs_n),
  .mosi(mosi),
  .miso(miso),
  .din({address,txdata}),
  .dout(data_get),
  .SCLK(sclk),
  .finish(finish)
);

always@(posedge sclk, negedge rst_n)begin
  if(!rst_n)         miso <= 1'b0;
  else if(i>=0&&i<16)miso <= rxdata[15-i];
  else               miso <= 1'b0;
end

always@(posedge sclk, negedge rst_n)begin
  if(!rst_n)i <= 0;
  else      i <= i + 1;
end

always #10 sysClk = ~sysClk;

initial begin
  sysClk  = 0;
  rst_n   = 0;
  tick_tx = 0;
  address = 8'h7A;
  txdata  = 8'h81;
  rxdata  = 16'h4689;
  rst_n   = 0;
  #1000 rst_n   = 1;
  #1000;
  #1000 tick_tx = 1;
  #1000 tick_tx = 0;
  repeat(20) #durTime;
  $stop;
end

endmodule 

这里的输入资料 16 bit 是由 8 bit 的 address 以及 8 bit 的 txdata 所组成,
而 testbench 会将 rxdata 於 SCLK 的正缘依序由 MSB 传出。

这边可以看到 din 为 7a81,同样为水蓝色信号线也确实有在 SCLK 负缘时改变输出值,而且值为 0111_1010_1000_0001,确实为 7a81。

再来是收的部分,可以看到粉红色信号线是在 SCLK 正缘传送 0100_0110_1000_1001(4689),而在 data_get 也确实在资料传送结束时吐出 4689。

最後也来可以看一下 RTL Viewer

看起来没有合成出过多冗余的部分,还可以~

以上就是我们的 4 线 SPI 教学喽~~~~


<<:  Day20 AR抬头显示器(HUD)与一般的差异 你是5岁就抬头还是3岁才抬头的呢?

>>:  [Day 20] 资料标注 (1/2) — Forget about the price tag ♫

[番外] 来个 Weather App (续)

前置作业-HTML & CSS 使用 Google 查 Weather App 会有很多很多...

Day2:AWS Shared Responsibility Model

只要谈到AWS资安议题绝对不能不提到 AWS Shared Responsibility Model...

Day29 平常如何学习新的知识?

大家好,我是乌木白,今天是倒数最後一天,虽然在最後几天我们没什麽讲技术方面的问题,第一个是我觉得我...

[ 卡卡 DAY 8 ] - React Native 跨平台装置判断

还记得 React Native 可以同时完成 iOS / Android 装置吧? 跨平台装置如...

【Day25】React Class Component 生命周期简单介绍

在写React的时候其实有分为两种写法 Class Component this.state or ...