FPGA零基础学习:SPI 协议驱动设计(上)

2021-03-23 09:54:18 浏览数 (1)

FPGA零基础学习:SPI 协议驱动设计

本系列将带来FPGA的系统性学习,从最基本的数字电路基础开始,最详细操作步骤,最直白的言语描述,手把手的“傻瓜式”讲解,让电子、信息、通信类专业学生、初入职场小白及打算进阶提升的职业开发者都可以有系统性学习的机会。

系统性的掌握技术开发以及相关要求,对个人就业以及职业发展都有着潜在的帮助,希望对大家有所帮助。后续会陆续更新 Xilinx 的 Vivado、ISE 及相关操作软件的开发的相关内容,学习FPGA设计方法及设计思想的同时,实操结合各类操作软件,会让你在技术学习道路上无比的顺畅,告别技术学习小BUG卡破脑壳,告别目前忽悠性的培训诱导,真正的去学习去实战应用。话不多说,上货。

SPI 协议驱动设计

作者:郝旭帅 校对:陆辉

本篇实现基于叁芯智能科技的SANXIN -B01 FPGA开发板,以下为配套的教程,如有入手开发板,可以登录官方淘宝店购买,还有配套的学习视频。

SPI是串行外设接口(Serial Peripheral Interface)的缩写。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议。

SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,中间靠三线或者四线连接(三线时为单向传输或者数据线双向传输)。所有基于SPI的设备共有的,它们是MISO、MOSI、SCLK、CS。

MISO– Master Input Slave Output,主设备数据输入,从设备数据输出。

MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入。

SCLK – Serial Clock,时钟信号,由主设备产生。

CS – Chip Select,从设备使能信号,由主设备控制。

cs是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个spi设备成为可能。

通讯是通过数据交换完成的,由sclk提供时钟脉冲,mosi、miso则基于此脉冲完成数据传输。数据输出通过 mosi线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要N次时钟信号的改变(上沿和下沿为一次),才能完成N位数据的传输。

spi通信有四种不同的模式,不同的从设备可能在出厂时就已经配置为某种模式。通信的双方必须是工作在同一模式下,所以我们可以对主设备的spi模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来控制我们主设备的通信模式。

mode0:CPOL=0,CPHA=0;

mode1:CPOL=0,CPHA=1;

mode2:CPOL=1,CPHA=0;

mode3:CPOL=1,CPHA=1;

时钟极性CPOL是用来配置SCLK在空闲时,应该处于的状态;时钟相位CPHA用来配置在第几个边沿进行采样。

CPOL=0,表示在空闲状态时,时钟SCLK为低电平。

CPOL=1,表示在空闲状态时,时钟SCLK为高电平。

CPHA=0,表示数据采样是在第1个边沿。

CPHA=1,表示数据采样是在第2个边沿。

即:

CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。

CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。

CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。

CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

硬件简介

FLASH闪存 的英文名称是"Flash Memory",一般简称为"Flash",它属于内存器件的一种,是一种非易失性( Non-Volatile )内存。

在开发板上有一块flash(M25P16),用来保存FPGA的硬件配置信息,也可以用来存储用户的应用程序或数据。

M25P16是一款带有写保护机制和高速SPI总线访问的2M字节串行Flash存储器,该存储器主要特点:2M字节的存储空间,分32个扇区,每个扇区256页,每页256字节;能单个扇区擦除和整片擦除;每扇区擦写次数保证10万次、数据保存期限至少20年。

C(serial clock:串行时钟)为D和Q提供了数据输入或者输出的时序。D的数据总是在C的上升沿被采样。Q的数据 在C的下降沿被输出。

(Chip Select:芯片选择端),当输入为低时,该芯片被选中,可以允许进行读写操作。当输入为高时,该芯片被释放,不能够进行操作。

对于H——o——l——d——和W——, 为保持功能和硬件写保护功能,在本设计中不使用此管脚,在硬件设计时,这两个管脚全部被拉高了,即全部失效。

flash采用spi的通信协议,flash当做从机。serial clcok等效于spi中的sclk,chip select等效于spi中的cs,D等效于spi中的mosi,Q等效于spi中的miso。

flash可以支持mode0和mode3,这两种模式中,都是在时钟的上升沿采样,在时钟的下降沿发送数据。

flash的每一页都可以被写入,但是写入只能是把1改变为0。擦除可以把0改变为1。所以在正常写入数据之前,都要将flash进行擦除。

flash的命令表如下:

下面介绍几个常用的命令。

RDID(Read Identification :读ID):发送命令RDID(9F),然后接收第1个字节的memory type(20H),第二个字节的memory capacity(15H)。后续的字节暂不关心。

WREN(Write Enable :写使能):在任何写或者擦除的命令之前,都必须首先打开写使能。打开写使能为发送命令WREN(06h)。

RDSR(Read Status Register:读状态寄存器):发送命令RDSR(05h),然后返回一个字节的状态值。

状态寄存器的格式如下:

WIP(Write In Progress bit)表示flash内部是否正在进行内部操作,写和擦除都会导致flash内部进行一段时间的工作,在内部工作期间,外部的命令会被忽略,所以在进行任何命令之前,都需要查看flash内部是否正在工作。WIP为1时,表示flash内部正在工作;WIP为0时,表示flash内部没有在工作。

READ(Read DATA Bytes:读数据):发送命令READ(03H),后续发送3个字节的地址,然后就可以接收数据,内部的地址会不断递增。一个读命令就可以把整个flash全部读完。

PP(Page Program :页编写):发送命令PP(02H),接着发送3个字节的地址,然后发送数据即可。切记所写的数据不能超过本页的地址范围。

SE(Sector Erase :扇区擦除):发送命令SE(D8H),接着发送3个字节的地址。

BE(Bulk Erase:整片擦除):发送命令BE(C7H)。

关于flash的其他的介绍,可以参考03_芯片手册->FLASH->M25P16.pdf。

设计要求

设计flash(M25P16)控制器。

设计分析

根据M25P16的数据手册得知,其接口为spi接口,且支持模式0和模式3,本设计中选择模式0。

输入时序图如下:

输出时序如下:

时序图中所对应的符号说明:

根据输入和输出的时序图以及参数表,将SPI的时钟的频率定为10MHz。

在设计中,FPGA作为主机,M25P16作为从机。

架构设计和信号说明

此模块命名为m25p16_drive。

二级模块(分模块)(第一页)

二级模块(分模块)(第二页)

设计中,各个命令单独写出控制器,通过多路选择器选择出对应的命令,然后控制spi_8bit_drive将数据按照spi的协议发送出去。各个命令的脉冲通过ctrl模块进行控制各个命令控制器,写入的数据首先写入到写缓冲区,读出的数据读出后写入到读缓冲区。

暂不分配的端口,在应用时都是由上游模块进行控制,本设计测试时,编写上游模块进行测试。

各个模块的功能,和连接线的功能在各个模块设计中说明。

spi_8bit_drive设计实现

本模块负责将8bit的并行数据按照spi协议发送出去,以及负责按照spi协议接收数据,将接收的数据(8bit)并行传输给各个模块。

spi_send_en为发送数据使能信号(脉冲信号),spi_send_data为所要发送数据,spi_send_done为发送完成信号(脉冲信号)。

spi_read_en为接收数据使能信号(脉冲信号),spi_read_data为所接收的数据,spi_read_done为接收完成信号(脉冲信号)。

代码语言:javascript复制
module spi_8bit_drive (

  input   wire              clk,
  input   wire              rst_n,
  
  input   wire              spi_send_en,
  input   wire  [7:0]       spi_send_data,
  output  reg               spi_send_done,
  
  input   wire              spi_read_en,
  output  reg   [7:0]       spi_read_data,
  output  reg               spi_read_done,
  
  output  wire              spi_sclk,
  output  wire              spi_mosi,
  input   wire              spi_miso
);

  reg           [8:0]       send_data_buf;
  reg           [3:0]       send_cnt;
  reg                       rec_en;
  reg                       rec_en_n;
  reg           [3:0]       rec_cnt;
  reg           [7:0]       rec_data_buf;
  
  always @ (negedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      send_data_buf <= 9'd0;
    else
      if (spi_send_en == 1'b1)
        send_data_buf <= {spi_send_data, 1'b0};
      else
        send_data_buf <= send_data_buf;
  end
  
  always @ (negedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      send_cnt <= 4'd8;
    else
      if (spi_send_en == 1'b1)
        send_cnt <= 4'd0;
      else
        if (send_cnt < 4'd8)
          send_cnt <= send_cnt   1'b1;
        else
          send_cnt <= send_cnt;
  end
  
  assign spi_mosi = send_data_buf[8 - send_cnt];
  assign spi_sclk = (send_cnt < 4'd8 || rec_en_n == 1'b1) ? clk : 1'b0;
  
  always @ (negedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      spi_send_done <= 1'b0;
    else
      if (send_cnt == 4'd7)
        spi_send_done <= 1'b1;
      else
        spi_send_done <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rec_en <= 1'b0;
    else
      if (spi_read_en == 1'b1)
        rec_en <= 1'b1;
      else
        if (rec_cnt ==  4'd7)
          rec_en <= 1'b0;
        else
          rec_en <= rec_en;
  end
  
  always @ (negedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rec_en_n <= 1'b0;
    else
      rec_en_n <= rec_en;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rec_data_buf <= 8'd0;
    else
      if (rec_en == 1'b1)
        rec_data_buf <= {rec_data_buf[6:0], spi_miso};
      else
        rec_data_buf <=rec_data_buf;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rec_cnt <= 4'd0;
    else
      if (rec_en == 1'b1)
        rec_cnt <= rec_cnt   1'b1;
      else
        rec_cnt <= 4'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      spi_read_done <= 1'b0;
    else
      if (rec_cnt == 4'd8)
        spi_read_done <= 1'b1;
      else
        spi_read_done <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      spi_read_data <= 8'd0;
    else
      if (rec_cnt == 4'd8)
        spi_read_data <= rec_data_buf;
      else
        spi_read_data <= spi_read_data;
  end
  
endmodule

在发送逻辑控制中,全部的信号采用下降沿驱动。利用外部给予的spi_send_en作为启动信号,启动send_cnt。send_cnt在不发送数据时为8,发送数据时,从0到7。

在接收逻辑中,全部的信号采用上升沿驱动。利用外部给予的spi_read_en作为启动信号,启动rec_en,经过移位接收数据。

在spi_sclk输出时,采用组合逻辑。由于设计采用spi的模式0,故而spi_sclk不发送或者接收数据时为0,接收数据时为时钟信号。因为要求为模式0,所以在接收数据时,spi_sclk的输出不能够先有下降沿,即要求spi_sclk的控制信号不能由上升沿信号驱动,所以将rec_en同步到下降沿的rec_en_n。

仿真代码为:

代码语言:javascript复制
`timescale 1ns/1ps

module spi_8bit_drive_tb;

  reg               clk;
  reg               rst_n;
  
  reg               spi_send_en;
  reg     [7:0]     spi_send_data;
  wire              spi_send_done;
  
  reg               spi_read_en;
  wire    [7:0]     spi_read_data;
  wire              spi_read_done;
  
  wire              spi_sclk;
  wire              spi_mosi;
  reg               spi_miso;

  spi_8bit_drive spi_8bit_drive_inst(

      .clk              (clk),
      .rst_n            (rst_n),
      
      .spi_send_en      (spi_send_en),
      .spi_send_data    (spi_send_data),
      .spi_send_done    (spi_send_done),
      
      .spi_read_en      (spi_read_en),
      .spi_read_data    (spi_read_data),
      .spi_read_done    (spi_read_done),
      
      .spi_sclk         (spi_sclk),
      .spi_mosi         (spi_mosi),
      .spi_miso         (spi_miso)
  );
  
  initial clk = 1'b0;
  always # 50 clk = ~clk;
  
  initial begin
    rst_n = 1'b0;
    spi_send_en = 1'b0;
    spi_send_data = 8'd0;
    spi_read_en = 1'b0;
    spi_miso = 1'b0;
    # 201
    rst_n = 1'b1;
    # 200
    @ (posedge clk);
    # 2;
    spi_send_en = 1'b1;
    spi_send_data = {$random} % 256;
    @ (posedge clk);
    # 2;
    spi_send_en = 1'b0;
    spi_send_data = 8'd0;
    
    @ (posedge spi_send_done);
    # 2000
    @ (posedge clk);
    # 2;
    spi_read_en = 1'b1;
    @ (posedge clk);
    # 2;
    spi_read_en = 1'b0;
    
    @ (posedge spi_read_done);
    # 200
    $stop;
  end
  
  always @ (negedge clk) spi_miso <= {$random} % 2;

endmodule

在仿真中,将时钟设置为10MHz。

所有的信号采用上升沿驱动。发送一个8bit的随机数值,接收一个8bit的随机数值。

spi_miso信号为从机下降沿驱动信号。

通过RTL仿真,可以看出发送和接收全部正常。

mux7_1设计实现

本模块负责将7个命令模块发出的命令(写使能、写数据和读使能)经过选择发送给spi_8bit_drive模块。

代码语言:javascript复制
module mux7_1 (

  input     wire          rdsr_send_en,
  input     wire    [7:0] rdsr_send_data,
  input     wire          rdsr_read_en,
  
  input     wire          pp_send_en,
  input     wire    [7:0] pp_send_data,
  
  input     wire          wren_send_en,
  input     wire    [7:0] wren_send_data,
  
  input     wire          be_send_en,
  input     wire    [7:0] be_send_data,
  
  input     wire          se_send_en,
  input     wire    [7:0] se_send_data,
  
  input     wire          rdid_send_en,
  input     wire    [7:0] rdid_send_data,
  input     wire          rdid_read_en,
  
  input     wire          read_send_en,
  input     wire    [7:0] read_send_data,
  input     wire          read_read_en,
  
  input     wire    [2:0] mux_sel,                 
  
  output    reg           spi_send_en,
  output    reg     [7:0] spi_send_data,
  output    reg           spi_read_en
);

  always @ * begin
    case (mux_sel)
      3'd0    : begin 
        spi_send_en = rdsr_send_en; 
        spi_send_data = rdsr_send_data; 
        spi_read_en = rdsr_read_en; 
      end
      3'd1    : begin 
        spi_send_en = pp_send_en; 
        spi_send_data = pp_send_data; 
        spi_read_en = 1'b0; 
      end
      3'd2    : begin 
        spi_send_en = wren_send_en; 
        spi_send_data = wren_send_data; 
        spi_read_en = 1'b0; 
      end
      3'd3    : begin 
        spi_send_en = be_send_en; 
        spi_send_data = be_send_data; 
        spi_read_en = 1'b0; 
      end
      3'd4    : begin 
        spi_send_en = se_send_en; 
        spi_send_data = se_send_data; 
        spi_read_en = 1'b0; 
      end
      3'd5    : begin 
        spi_send_en = rdid_send_en; 
        spi_send_data = rdid_send_data; 
        spi_read_en = rdid_read_en; 
      end
      3'd6    : begin 
        spi_send_en = read_send_en; 
        spi_send_data = read_send_data; 
        spi_read_en = read_read_en; 
      end
      default : begin 
        spi_send_en = 1'b0; 
        spi_send_data = 8'd0; 
        spi_read_en = 1'b0; 
      end
    endcase
  end

endmodule

在设计中,有的命令模块不需要进行读取(pp和se等等),此时将输出的读使能信号输出为低电平。

be设计实现

该模块接收到be_en(整片擦除的脉冲信号)信号后,发送对应的使能和数据,等待发送完成脉冲。发送完成后,输出擦除完成的脉冲。

代码语言:javascript复制
module be (

  input     wire            clk,
  input     wire            rst_n,
  
  input     wire            be_en,
  output    reg             be_done,
  
  output    reg             be_send_en,
  output    wire    [7:0]   be_send_data,
  input     wire            spi_send_done
);

  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      be_send_en <= 1'b0;
    else
      be_send_en <= be_en;
  end
  
  assign be_send_data = 8'hc7;

  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      be_done <= 1'b0;
    else
      be_done <= spi_send_done;
  end
  
endmodule

整片擦除的命令为8’hc7。

wren设计实现

该模块接收到wren_en(打开flash内部的写使能的脉冲信号)信号后,发送对应的使能和数据,等待发送完成脉冲。发送完成后,输出擦除完成的脉冲。

代码语言:javascript复制
module wren (

  input     wire            clk,
  input     wire            rst_n,
  
  input     wire            wren_en,
  output    reg             wren_done,
  
  output    reg             wren_send_en,
  output    wire    [7:0]   wren_send_data,
  input     wire            spi_send_done
);

  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      wren_send_en <= 1'b0;
    else
      wren_send_en <= wren_en;
  end
  
  assign wren_send_data = 8'h06;

  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      wren_done <= 1'b0;
    else
      wren_done <= spi_send_done;
  end
  
endmodule

打开flash内部写使能的命令码为8’h06。

se设计实现

该模块接收到se_en(擦除扇区的写使能的脉冲信号)信号后,发送对应的使能和数据,等待发送完成脉冲。发送完成后,接着发送高八位地址,中间八位地址和低八位地址。全部发送完成后,发送se_done信号。

该模块采用状态机实现。SE_STATE(扇区擦除命令发送)、H_ADDR(高八位地址发送)、M_ADDR(中间八位地址发送)、L_ADDR(低八位地址发送)、SE_DONE(扇区擦除完成)。所有的脉冲信号在未标注的时刻,输出全部为0。

设计代码为:

代码语言:javascript复制
module se (

  input   wire                clk,
  input   wire                rst_n,
  
  input   wire                se_en,
  input   wire      [23:0]    se_addr,
  output  reg                 se_done,
  
  output  reg                 se_send_en,
  output  reg       [7:0]     se_send_data,
  input   wire                spi_send_done
);

  localparam      SE_STATE    = 5'b00001;
  localparam      H_ADDR      = 5'b00010;
  localparam      M_ADDR      = 5'b00100;
  localparam      L_ADDR      = 5'b01000;
  localparam      SE_DONE     = 5'b10000;
  
  reg               [4:0]     c_state;
  reg               [4:0]     n_state;
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      c_state <= SE_STATE;
    else
      c_state <= n_state;
  end
  
  always @ * begin
    case (c_state)
      SE_STATE    :   begin
        if (se_en == 1'b0)
          n_state = SE_STATE;
        else
          n_state = H_ADDR;
      end
      
      H_ADDR      :   begin
        if (spi_send_done == 1'b0)
          n_state = H_ADDR;
        else
          n_state = M_ADDR;
      end
      
      M_ADDR      :   begin
        if (spi_send_done == 1'b0)
          n_state = M_ADDR;
        else
          n_state = L_ADDR;
      end
      
      L_ADDR      :   begin
        if (spi_send_done == 1'b0)
          n_state = L_ADDR;
        else
          n_state = SE_DONE;
      end
      
      SE_DONE     :   begin
        if (spi_send_done == 1'b0)
          n_state = SE_DONE;
        else
          n_state = SE_STATE;
      end
      
      default     :     n_state = SE_STATE;
    endcase
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      se_send_en <= 1'b0;
    else
      case (c_state)
        SE_STATE    :   begin
          if (se_en == 1'b1)
            se_send_en <= 1'b1;
          else
            se_send_en <= 1'b0;
        end
        
        H_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_en <= 1'b1;
          else
            se_send_en <= 1'b0;
        end
        
        M_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_en <= 1'b1;
          else
            se_send_en <= 1'b0;
        end
        
        L_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_en <= 1'b1;
          else
            se_send_en <= 1'b0;
        end
        
        SE_DONE     :   begin
          se_send_en <= 1'b0;
        end
        
        default     :   se_send_en <= 1'b0;
      endcase
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      se_send_data <= 8'd0;
    else
      case (c_state)
        SE_STATE    :   begin
          if (se_en == 1'b1)
            se_send_data <= 8'hd8;
          else
            se_send_data <= 8'd0;
        end
      
        H_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_data <= se_addr[23:16];
          else
            se_send_data <= 8'd0;
        end
        
        M_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_data <= se_addr[15:8];
          else
            se_send_data <= 8'd0;
        end
        
        L_ADDR      :   begin
          if (spi_send_done == 1'b1)
            se_send_data <= se_addr[7:0];
          else
            se_send_data <= 8'd0;
        end
        
        SE_DONE     :   begin
          se_send_data <= 8'd0;
        end
        
        default     :   se_send_data <= 8'd0;
      endcase
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      se_done <= 1'b0;
    else
      if (c_state == SE_DONE && spi_send_done == 1'b1)
        se_done <= 1'b1;
      else
        se_done <= 1'b0;
  end
  
endmodule

在发送过程中,由于是每8bit发送一次,所以在时序上将看到发送时,每8个脉冲一组,中间会有明显的间隔。

余下内容见下篇

0 人点赞