SPI通信——FPGA学习笔记14

一、简介

SPI(Serial Periphera Interface,串行外围设备接口)通讯协议,是 Motorola 公司提出的一种同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输,广泛用于 EEPROM、FIash、RTC(实时时钟)、ADC(数模转换器)、DSP(数字信号处理器)以及数字信号解码器上,是常用的、重要的低速通讯协议之一。

多从机

常规寻址:

通信过程:

SPI协议

时钟极性(CPOL):空闲时时钟的电平极性

时钟相位(CPHA):控制数据更新时刻和采样时刻

CPOL=0,CPHA=0

Sendstrobe:内部发送模块发送的触发信号;Capstrobe:内部接收数据模块采样的触发信号

CPOL=0,CPHA=1

CPOL=1,CPHA=0

CPOL=1,CPHA=1

驱动基本模块

FPGA时序要求

二、程序设计

1、本实验设计一个SPI发送驱动,包含SPI四种工作模式

2、握手信号

只有当req请求信号拉高并低电平时才会开启一次数据传输,数据传输过程中busy信号拉高表示设备忙。

3、SPI发送状态机控制器设计

Haskell 复制代码
`timescale 1ns / 1ps

module spi_tx#(
    parameter CLK_DIV   = 'd100 ,   //     
    parameter CPOL      = 1'b0  ,   //时钟极性
    parameter CPHA      = 1'b0      //时钟相位
)
(   
    input           I_clk           ,   //系统时钟
    input           I_rstn          ,   //系统复位
    input           I_spi_tx_req    ,   //发送数据请求
    input   [7:0]   I_spi_tx_data   ,   //发送数据

    output          O_spi_mosi      ,   //输出SPI数据
    output          O_spi_sclk      ,   //输出SPI时钟
    output          O_spi_busy          //输出忙信号
    
);

localparam  [9:0]   SPI_DIV     =   CLK_DIV     ;   //第二时钟边沿计数器
localparam  [9:0]   SPI_DIV1    =   SPI_DIV / 2 ;   //第一时钟边沿计数器    //分频系数一半

reg [9:0]   clk_div         ;
reg         spi_en          ;       //发送使能
reg [3:0]   tx_cnt          ;
reg         spi_clk         ;
reg [7:0]   I_spi_tx_data_r ;  
reg         spi_strobe_en   ; 


wire        clk_end         ;   //
wire        clk_en1         ;   //第一内部时钟使能
wire        clk_en2         ;   //第二内部时钟使能
wire        spi_strobe      ;



//计数器发送第一个时钟0-7次,当计数达到8时,不发送时钟
assign      clk_en1     = (clk_div == SPI_DIV1);    //第一内部时钟边沿使能      一半
assign      clk_en2     = (clk_div == SPI_DIV );    //第二内部时钟边沿使能      记满
assign      clk_end     = ((clk_div == SPI_DIV1)&&(tx_cnt == 4'd8));    //时钟结束信号    计数半个周期


//当CPHA=0时,数据的第一个SCLK转换边缘被采样,因此数据更新在第二个转换边缘上
//当CPHA=1时,数据的第二个SCLK转换边缘被采样,因此数据更新在第一个转换边缘上
assign      spi_strobe = CPHA ?  clk_en1 & spi_strobe_en : clk_en2 & spi_strobe_en;
assign      O_spi_sclk  = (CPOL == 1'b1) ? !spi_clk : spi_clk ;         //设置SPI初始时钟
assign      O_spi_mosi  = I_spi_tx_data_r[7] ;                          //SPI输出数据
assign      O_spi_busy  = spi_en;



//SPI时钟计数器
always @(posedge I_clk ) begin
    if (spi_en == 1'b0) begin
        clk_div <= 10'd0;
    end 
    else if(clk_div < SPI_DIV)begin
        clk_div <= clk_div + 1'b1;
    end
    else begin
        clk_div <= 10'd0;
    end
end

//SPI时钟生成
always @(posedge I_clk ) begin
    if (spi_en == 1'b0) begin
        spi_clk <= 1'b0;    
    end 
    else if(clk_en2 == 1'b1)begin
        spi_clk <= 1'b0;
    end
    else if ((clk_en1 == 1'b1)&&(tx_cnt < 4'd8)) begin
        spi_clk <= 1'b1;
    end 
    else begin
        spi_clk <= spi_clk;    
    end
end

//SPI bit计数器
always @(posedge I_clk ) begin
    if ((!I_rstn)||(spi_en == 1'b0)) begin
        tx_cnt <= 4'd0;
    end 
    else if(clk_en1 == 1'b1)begin
        tx_cnt <= tx_cnt + 1'b1;
    end
end

//
always @(posedge I_clk ) begin
    if (!I_rstn) begin
        spi_strobe_en <= 1'b0;
    end 
    else if(tx_cnt < 4'd8)begin
        if (clk_en1 == 1'b1) begin
            spi_strobe_en <= 1'b1;
        end 
        else begin
            spi_strobe_en <= spi_strobe_en; 
        end    
    end
    else begin
        spi_strobe_en <= 1'b0;
    end
end

//SPI发送模块
always @(posedge I_clk ) begin
    if ((!I_rstn)||(clk_end == 1'b1)) begin
        spi_en <= 1'b0;
        I_spi_tx_data_r <= 8'b0;
    end 
    else if((I_spi_tx_req == 1'b1)&&(spi_en == 1'b0))begin      //启动传输
        spi_en <= 1'b1;
        I_spi_tx_data_r <=  I_spi_tx_data;
    end
    else if (spi_en == 1'b1) begin
        I_spi_tx_data_r[7:0] <= (spi_strobe)?{I_spi_tx_data_r[6:0],1'b1} : I_spi_tx_data_r;
    end
end


endmodule
Haskell 复制代码
`timescale 1ns / 1ps

module spi_master_tx#(
    parameter   CLK_DIV = 100
)
(
    input   I_clk       ,   //
    input   I_rstn      ,   //
    output  O_spi_sclk  ,   //
    output  O_spi_mosi      //
);

wire        spi_busy    ;   //spi忙的标志
reg         spi_tx_req  ;   //spi请求发送
reg [7:0]   spi_tx_data ;   //spi待发送数据
reg [1:0]   M_S         ;   //状态机


always @(posedge I_clk ) begin
    if (!I_rstn) begin
        spi_tx_req  <=  1'b0;
        spi_tx_data <=  8'd0;
        M_S         <=  2'd0;
    end 
    else begin
        case (M_S)
            0:begin
                if (spi_busy == 1'b0) begin
                    spi_tx_req   <= 1'b1;       //请求发送
                    spi_tx_data  <= spi_tx_data + 1'b1;     //测试累加数据
                    M_S          <= 2'd1; 
                end 
            end 
            1:begin
                if (spi_busy == 1'b1) begin     //清楚请求信号
                    spi_tx_req <= 1'b0;
                    M_S        <= 2'd0;
                end
            end
            default:M_S        <= 2'd0;
        endcase    
    end
end


spi_tx#(
    .CLK_DIV   (CLK_DIV),   //     
    .CPOL      (1'b0   ),   //时钟极性
    .CPHA      (1'b0   )    //时钟相位
)
u_spi_tx(   
    .I_clk           (I_clk         ),   //系统时钟
    .I_rstn          (I_rstn        ),   //系统复位
    .I_spi_tx_req    (spi_tx_req    ),   //发送数据请求
    .I_spi_tx_data   (spi_tx_data   ),   //发送数据
    .O_spi_mosi      (O_spi_mosi    ),   //输出SPI数据
    .O_spi_sclk      (O_spi_sclk    ),   //输出SPI时钟
    .O_spi_busy      (spi_busy      )    //输出忙信号
);


endmodule
Haskell 复制代码
`timescale 1ns / 1ps


module tb( );

localparam  SYS_TIME = 4'd10;

reg I_clk;
reg I_rstn;

wire    O_spi_mosi;
wire    O_spi_sclk;

spi_master_tx#(
    .CLK_DIV (100)
)
u_spi_master_tx(
    .I_clk       (I_clk         ),   //
    .I_rstn      (I_rstn        ),   //
    .O_spi_sclk  (O_spi_sclk    ),   //
    .O_spi_mosi  (O_spi_mosi    )    //
);

initial begin
    I_clk = 0;
    I_rstn = 0;
    #100;
    I_rstn = 1;
end

always #(SYS_TIME / 2) I_clk = !I_clk;

endmodule

00

4、SPI接收模块

Haskell 复制代码
`timescale 1ns / 1ps

module spi_rx#
(
    parameter   BITS_LEM    =   8       ,//bit数量
    parameter   CPOL        =   1'b0    ,
    parameter   CPHA        =   1'b0     
)
(   
    input                       I_clk       ,   //系统时钟
    input                       I_rstn      ,   //系统复位
    input                       I_spi_clk   ,   //SPI时钟
    input                       I_spi_rx    ,   //SPI rx数据总线
    input                       I_spi_ss    ,   //SPI片选信号

    output                      O_spi_rvalid,   //SPI rx接收数据有效信号  1:rdata有效     0:无效
    output [BITS_LEM - 1'b1:0]  O_spi_rdata     //SPI rx接收到的数据输出

);

reg [3:0]               spi_clk_r   ;       //时钟打拍
reg [3:0]               spi_ss_r    ;       //片选打拍
reg                     spi_cap     ;       //采样信号
reg [3:0]               spi_bit_cnt ;       //bit计数器
reg [BITS_LEM - 1'b1:0] spi_rx_r1   ;       //接收缓存

wire                    spi_rx_en   ;       //接收使能
wire                    spi_clkp    ;       //spi上升沿
wire                    spi_clkn    ;       //spi下升沿


assign  spi_clkp        =   (spi_clk_r[3:2] == 2'b01)   ;        //spi上升沿
assign  spi_clkn        =   (spi_clk_r[3:2] == 2'b10)   ;        //spi下升沿
assign  spi_rx_en       =   (!spi_ss_r[3])              ;

assign  O_spi_rdata     =   spi_rx_r1                   ;
assign  O_spi_rvalid    =   (spi_bit_cnt == BITS_LEM)   ;


//I_spi_clk去毛刺
always @(posedge I_clk or negedge I_rstn ) begin
    if (!I_rstn) begin
        spi_clk_r <= 1'b0;
    end 
    else begin
       spi_clk_r <= {spi_clk_r[2:0],I_spi_clk};     
    end
end

//I_spi_ss去毛刺
always @(posedge I_clk or negedge I_rstn ) begin
    if (!I_rstn) begin
        spi_ss_r <= 1'b0;
    end 
    else begin
       spi_ss_r <= {spi_ss_r[2:0],I_spi_ss};     
    end
end

//cap信号生成    何时采样
always @( * ) begin
    if (CPHA == 1'b1) begin
        if (CPOL == 1'b1) begin
            spi_cap = spi_clkn;        //CPHA = 1   CPOL = 1
        end 
        else begin
            spi_cap = spi_clkp;        //CPHA = 1   CPOL = 0   
        end
    end 
    else begin
        if (CPOL == 1'b1) begin
            spi_cap = spi_clkn;         //CPHA = 0   CPOL = 1 
        end 
        else begin
            spi_cap = spi_clkp;         //CPHA = 0   CPOL = 0 
        end
    end
end

//bit计数器
always @(posedge I_clk ) begin
    if ((spi_rx_en == 1'b1)&&(spi_bit_cnt < BITS_LEM)&&(spi_cap == 1'b1)) begin    //开启接收,并且未达到计数最大值
        spi_bit_cnt <= spi_bit_cnt + 1'b1;
    end 
    else if((spi_rx_en == 1'b0)||(spi_bit_cnt == BITS_LEM))begin    //未开启接收    或计数到最大追
        spi_bit_cnt <= 4'd0;
    end
end

//bit移位  串转并
always @(posedge I_clk ) begin
    if ((spi_rx_en == 1'b1)&&(spi_cap == 1'b1)) begin
        spi_rx_r1 <= {spi_rx_r1[BITS_LEM - 2:0],I_spi_rx};          //移位寄存   高位在前
    end 
    else if(spi_rx_en == 1'b0)begin
        spi_rx_r1 <= 'd0;
    end
end







endmodule
Haskell 复制代码
`timescale 1ns / 1ps


module tb( );

localparam   BYTES       =   8       ;
localparam   CPOL        =   1'b0    ;
localparam   CPHA        =   1'b0    ;

localparam   TCNT        =   BYTES * 8 * 2 - 1;

reg             I_clk               ;       //系统时钟
reg     [7:0]   i                   ;       //计数器,产生SPI时钟           
reg             I_rstn              ;       //系统复位
reg             I_spi_clk           ;       //SPI时钟
reg             I_spi_ss            ;       //SPI片选
reg     [3:0]   bit_cnt             ;       //bit计数器           
reg     [7:0]   spi_tx_buf          ;       //发送缓冲(移位寄存器)
reg     [7:0]   spi_tx_buf_r        ;       //发送化缓冲,用于生成测试文件
reg             first_data_flag     ;       //是否是一个时钟该变数据           

wire            O_spi_rvalid        ;       //SPI数据接收有效
wire    [7:0]   O_spi_rdata         ;       
wire            I_spi_rx            ;  


assign  I_spi_rx = spi_tx_buf[7];




spi_rx#
(
    .BITS_LEM    (8      ) ,//bit数量
    .CPOL        (1'b0   ) ,
    .CPHA        (1'b0   )  
)
u_spi_rx(   
    .I_clk       (I_clk         ),   //系统时钟
    .I_rstn      (I_rstn        ),   //系统复位
    .I_spi_clk   (I_spi_clk     ),   //SPI时钟
    .I_spi_rx    (I_spi_rx      ),   //SPI rx数据总线
    .I_spi_ss    (I_spi_ss      ),   //SPI片选信号

    .O_spi_rvalid(O_spi_rvalid  ),   //SPI rx接收数据有效信号  1:rdata有效     0:无效
    .O_spi_rdata (O_spi_rdata   )    //SPI rx接收到的数据输出
);

initial begin
    I_clk = 0;
    I_rstn = 0;
    #100;
    I_rstn = 1;
end

always #5 I_clk = !I_clk;

initial begin
    #100
    i = 0;

    forever begin
        I_spi_clk = CPOL;
        I_spi_ss = 1;
        #2000
        I_spi_ss = 0;
        for (i = 0;i<TCNT ;i = i + 1 ) begin
            #1000
            I_spi_clk = !I_spi_clk;
        end
        #2000
        I_spi_ss = 1;
    end
end

initial begin
    #100
    bit_cnt = 0;
    first_data_flag = 0;
    spi_tx_buf[7:0] = 8'ha0;
    spi_tx_buf_r[7:0] = 5'ha0;
    forever begin
        wait(I_spi_ss);
        bit_cnt = 0;
        spi_tx_buf[7:0] =   8'ha0;
        spi_tx_buf_r[7:0] = 8'ha0; 
        if ((CPHA == 1 && CPOL == 0)||(CPHA == 1 && CPOL == 1)) begin
            first_data_flag = 1;
        end

        wait(!I_spi_ss);
        
        while (!I_spi_ss) begin
            if (CPHA == 0 && CPOL == 0) begin
                @(negedge I_spi_clk)begin
                    if (bit_cnt == 7) begin
                        bit_cnt = 0;
                        spi_tx_buf_r = spi_tx_buf_r + 1'b1;
                        spi_tx_buf = spi_tx_buf_r;
                    end 
                    else begin
                        spi_tx_buf = {spi_tx_buf[6:0],1'b0};
                        bit_cnt = bit_cnt + 1'b1;
                    end
                end
            end 

            if (CPHA == 0 && CPOL == 1) begin
                @(posedge I_spi_clk)begin
                    if (bit_cnt == 7) begin
                        bit_cnt = 0;
                        spi_tx_buf_r = spi_tx_buf_r + 1'b1;
                        spi_tx_buf = spi_tx_buf_r;
                    end 
                    else begin
                        spi_tx_buf = {spi_tx_buf[6:0],1'b0};
                        bit_cnt = bit_cnt + 1'b1;
                    end
                end
            end

            if (CPHA == 1 && CPOL == 0) begin
                @(posedge I_spi_clk)begin
                    if (first_data_flag == 1'b1) begin
                        first_data_flag = 0;
                    end
                    else begin
                        if (bit_cnt == 7) begin
                            bit_cnt = 0;
                            spi_tx_buf_r = spi_tx_buf_r + 1'b1;
                            spi_tx_buf = spi_tx_buf_r;
                        end 
                        else begin
                            spi_tx_buf = {spi_tx_buf[6:0],1'b0};
                            bit_cnt = bit_cnt + 1'b1;
                        end
                    end
                end
            end  
            
            if (CPHA == 1 && CPOL == 1) begin
                @(negedge I_spi_clk)begin
                    if (first_data_flag == 1'b1) begin
                        first_data_flag = 0;
                    end
                    else begin
                        if (bit_cnt == 7) begin
                            bit_cnt = 0;
                            spi_tx_buf_r = spi_tx_buf_r + 1'b1;
                            spi_tx_buf = spi_tx_buf_r;
                        end 
                        else begin
                            spi_tx_buf = {spi_tx_buf[6:0],1'b0};
                            bit_cnt = bit_cnt + 1'b1;
                        end
                    end
                end
            end  
        end
    end
end





endmodule

三、仿真验证

相关推荐
聪明的笨猪猪12 分钟前
Java JVM “内存(1)”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
_dindong24 分钟前
Linux网络编程:Socket编程TCP
linux·服务器·网络·笔记·学习·tcp/ip
金士顿28 分钟前
ethercat网络拓扑详细学习
学习
知识分享小能手1 小时前
uni-app 入门学习教程,从入门到精通,uni-app组件 —— 知识点详解与实战案例(4)
前端·javascript·学习·微信小程序·小程序·前端框架·uni-app
wahkim1 小时前
Flutter 学习资源及视频
学习
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 属性优先级 行内写法 变量选择 笔记42
java·spring boot·笔记
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 总结 热部署 常用配置 笔记44
java·spring boot·笔记
小白要努力sgy2 小时前
待学习--中间件
学习·中间件
rechol2 小时前
汇编与底层编程笔记
汇编·arm开发·笔记
~无忧花开~2 小时前
CSS学习笔记(五):CSS媒体查询入门指南
开发语言·前端·css·学习·媒体