FPGA-SPI

SPI可以实现一对多的多机通信:

本着边学习边运用的角度,想从原来的SPI库函数的使用者,实现完整的传输功能,本篇复刻了xmg的教程。有任何问题请指正!

SPI有四根标准的信号线:

  1. MISO(M-master  I-input S-slave O-output)
  2. MOSI同理,主机输出,同时也是主设备向从设备输出的总线
  3. SCLK时钟
  4. SS(CS)片选信号

SPI必须由机控制从机,因为SCLK信号和SS信号是由主机设备产生的,因此根据主设备产生的时钟的相位和极性的不同,我们可主以将SPI配置成4种模式,当主机和从机配置成相同模式时,SPI的交互就可以完成传输和数据交互。

CPOL 表示时钟的极性:

CPOL=0:时钟空闲为低电平,代表高电平有效

CPOL=1:时钟空闲为高电平,代表低电平有效

CPHA表示时钟的采样相位:

CPHA=0:数据会从第一个边沿开始采样

CPHA=1:数据会从第二个边沿开始采样

此外我们还需要知道SPI的结构可以通过片选CS/SS来分为两种:

1.分片选在主机上(一个主机含有多个片选)

2.主机只有一个片选:这种通常适合在串行转并行的场合

我们有了以上概念就可以照着SPI的时序图 “抄” 了,这个是以CPOL=0,CPHA=0为例说明:

CPOL=0,CPHA=0的意思是什么?

(网上找的一个大佬画的图)

数据采样的意思就是输入,当然输入输出是相对的,以FPGA作为从机为例,

首先cs片选拉低后,根据数据采样的位置开始采样:

在cs拉低后的sck的初始状态是低电平,(CPOL=0)

在sck跳变的第一个时钟沿,进行数据传输,(CPHA=0)

为了追求通用一点的写法,我们需要通过两个极性创造一个自己需要的时钟:

 assign clk_sel = (CPOL ^ CPHA) ? (SPI_SCK) : (~SPI_SCK);

随后我们很容易想到我们也需要一个复位信号,只要有复位按键按下,或者在cs电平被拉低就复位一次,确保单次传输所有寄存器全部清零:

assign  spi_reset = (!rst_n)|spi_cs;

接下来我们就可以写接收和发送的部分了

用两个线性序列机就可以完成的,下面把代码全部附上:

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 2024/09/22 22:59:45
// Design Name: 
// Module Name: spi_slave
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// SPI从机,将需要发送的数据send_data以SPI数据发送,并解析到SPI数据,
// 同时记录从cs低到高的自己输trans_cnt
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


module spi_slave #
(
    parameter integer CPOL = 1'b0,         //空闲时SCK电平:1为高,0为低
    parameter integer CPHA = 1'b0,         //数据捕获边沿:0为第一个边沿,1为第2个边沿
    parameter bits_order = 1'b1
)
(
    input clk,
    input rst_n,
    input send_data_valid,
    input [7:0] send_data,
    input spi_cs,
    input spi_sck,
    input spi_mosi,

    output reg recieve_data_valid,
    output reg [7:0] recieve_data,
    output reg [15:0] trans_cnt,//计数cs在低电平期间,接收到数据的bit
    output reg trans_done,
    output spi_miso,
    output trans_start,
    output trans_end
    );

//先对CS片选信号打一拍,取下降沿
reg cs_r1,cs_r2;

wire sck_sel;   //sck有效信号根据cpol和cpha来选择
wire spi_reset; //cs片选和rst复位共同决定,高电平有效

assign  sck_sel = (CPHA ^ CPOL) ? (spi_sck):(~spi_sck);
assign  spi_reset = (!rst_n)|spi_cs;

reg [7:0] in_cnt ;
reg [7:0] recieve;


reg miso;

//在发送MISO的数据时同样以sck_sel为时钟信号,取send_data[7] 在sck的前7个下降沿将send_data[6:0]发送
assign spi_miso = spi_cs ? 1 : ((out_cnt|CPHA) ? miso : send_data_r[7]);
reg [7:0] send_data_r ;
reg [7:0] out_cnt;


always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        cs_r1 <= 8'h0;
        cs_r2 <= 8'h0;
    end
    else begin
        cs_r1 <= spi_cs;
        cs_r2 <= cs_r1;
    end
end

assign trans_start = (~cs_r1) & cs_r2;
assign trans_end   = cs_r1 & (~cs_r2);
//将spi数据接收
always @(negedge sck_sel or posedge spi_reset) begin
    if(spi_reset)begin
        in_cnt  <= 8'd0;
        recieve <= 0;
        trans_done <= 0;
        trans_cnt <= 0;
    end
    else begin
        case (in_cnt)
            8'd0: begin in_cnt <= in_cnt + 1'b1; recieve[7]<=spi_mosi;trans_done<=1'b0;  end
            8'd1: begin in_cnt <= in_cnt + 1'b1; recieve[6]<=spi_mosi; end
            8'd2: begin in_cnt <= in_cnt + 1'b1; recieve[5]<=spi_mosi; end
            8'd3: begin in_cnt <= in_cnt + 1'b1; recieve[4]<=spi_mosi; end
            8'd4: begin in_cnt <= in_cnt + 1'b1; recieve[3]<=spi_mosi; end
            8'd5: begin in_cnt <= in_cnt + 1'b1; recieve[2]<=spi_mosi; end
            8'd6: begin in_cnt <= in_cnt + 1'b1; recieve[1]<=spi_mosi; end
            8'd7: begin in_cnt <= 8'd0; recieve[0]<=spi_mosi;trans_done<=1'b1;  end
            default: in_cnt <= 8'd0;
        endcase
    end
end

//在完成每一位的字节接收后,根据高位在前还是低位在前给recieve_data赋值
/*
bit_orders
由于这里sck的时钟和clk的时钟是两个时钟域,我们这里首先产生一个done_pos信号用来消除亚稳态
*/
reg done_r1,done_r2;
wire done_pos;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        done_r1 <= 0;
        done_r2 <= 0;
    end
    else begin
        done_r1 <= trans_done;
        done_r2 <= done_r1;
    end
end
assign done_pos = (done_r1)&(~done_r2);

//在done_pos信号为高电平期间,将接收数据有效信号拉高
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        recieve_data_valid <= 1'b0;
    else if (done_pos) 
        recieve_data_valid <= 1'b1;
    else 
        recieve_data_valid <= 1'b0;
end

//将接收的数据清零置位
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
    recieve_data <= 8'h0;
    else if(done_pos)begin
        if(bits_order)
           recieve_data <= recieve;
        else 
           recieve_data <= {recieve[0],recieve[1],recieve[2],recieve[3],recieve[4],recieve[5],recieve[6],recieve[7]};
    end
    else 
        recieve_data <= recieve_data;
end


/*
我们将主机传输过来的数据进行解析,并将解析的数据recieve_data进行输出,现在我们就要将send_data
以spi的数据格式发送————MISO从机输出
这里既要考虑到发送数据的顺序问题,也要考虑到在发送的过程中,数据不要被修改
*/
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        send_data_r <= 8'h00;
    else if(send_data_valid)begin
        if(bits_order)
            send_data_r <= send_data;
        else 
            send_data_r <= {send_data[0],send_data[1],send_data[2],send_data[3],
                            send_data[4],send_data[5],send_data[6],send_data[7]};
    end
    else 
        send_data_r <= send_data_r;
end


/*
保障发送数据的数据正确性后就要考虑其时序正确性了:
同发送一样,接收部分我们也需要一个线性状态机去控制
*/
always @(negedge sck_sel or posedge spi_reset) begin
    if(spi_reset)begin
        out_cnt <= 8'd0;
    end
    else begin
        case (out_cnt)
            8'd0: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[6+CPHA]; end
            8'd1: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[5+CPHA]; end
            8'd2: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[4+CPHA]; end
            8'd3: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[3+CPHA]; end
            8'd4: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[2+CPHA]; end
            8'd5: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[1+CPHA]; end
            8'd6: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[0+CPHA]; end
            8'd0: begin out_cnt <= 8'd0; miso <= send_data_r[0];  end
            default: out_cnt <= 8'd0;
        endcase
    end
end







endmodule

下面是仿真部分的验证:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值