基于FPGA的SPI主机数据接收发送控制器

引言:SPI是常用的板级通信协议,在FPGA板级通信中,许多重要的从器件都对SPI协议有所支持。因此,掌握SPI通信的FPGA片上实现对于FPGA工程开发具有重要的意义。本文设计了一个基于SPI模式0的主机通信控制器,系统性阐明了SPI设计的全流程。希望本文的设计能够对更多的人有所帮助。

一、SPI通信协议简介

1.物理层引线

SPI通信的最小结构为一主一从结构,主机向从机提供信号发送接收时钟SCLK。在物理实现中,主机与从机之间存有四根引线,即MOSI(主收从发)、MISO(主发从收)、SCK(通信时钟)、nSS(从机片选信号)。其中,通过在主机上增加片选信号输出端的数目,可以使得主机控制更多的从机,实现一主多从的SPI通信。

2.SPI四种模式

SPI通信协议规定了4钟工作模式,在实际应用中应当保证主机和从机工作在相同的工作模式下。SPI工作模式通过时钟极性CPOL和时钟相位CPHA联合指定。其中没时钟极性CPOL指定SCK在空闲状态时的电平,时钟极性CPHA指定在SCK的何种边缘进行数据采样。其标识如下:

 CPOL=1CPOL=0
CPHA=1MODE3:SCK空闲为高,上升沿采样MODE1:SCK空闲为低,下降沿采样
CPHA-0MODE2:SCK空闲为高,下降沿采样MODE0:SCK空闲为低,上升沿采样

 

其中,SPI中最常用的时模式0和模式2.

二、FPGA实现设计

由于我们设计的目标为SPI模式0主机控制器,因此我需要实现在SCK上升沿时对数据进行采样。由于在时钟上升沿进行数据采样,那么,接收/发送状态机的状态应当早于SCK时钟上升沿提前准备好接收/发送状态。

对于发送状态机,必须在SCK时钟上升沿到来前将需发送的数据在MOSI信号线上准备好。因此其状态的跳变应当较时钟上升沿到来更早。因此,本文设定发送状态机的在SCK时钟的下降沿进行状态变换,在SCK的低电平中心将需发送的数据压至MOSI线上。

对于接收状态机,为了保证状态跳变的一致性,也采用SCK下降沿进行状态跳变时刻。接收状态机在SCK上升沿时将数据采集至接收缓冲口,并在采集完一字节数据后生成信号标志脉冲。这个脉冲将保持一个SCK周期以供FPGA其余模块识别。

对于SPI主机控制器,另一个重要的模块是称之为control的控制模块,它是用来生成SCK时钟与nSS片选信号的模块。由于本文设计只有一个从机,因此只单纯的控制nSS的电平,不对多条nSS进行选择控制。SCK时钟的生成也是依靠状态机,其机理为当状态机处于IDLE空闲状态时,检测到读/写请求,在时钟相位变为下降沿时进入工作状态,并输出8个完整的SCK时钟。此后,状态机转入STOP状态,并在一个周期内检测是否有新的请求信号到来,若无抵达空闲状态,若有,转入工作状态。

本文设计的代码如下,如果要修改SPI工作模式,请根据头文件的相位定义修改代码:

module SPI(
    clk,rst_n,miso,mosi,sclk,ss_n,wr,rd,data_wr,data_rd,wr_req,rd_req
    );
input clk;                                          //时钟信号,设定为50MHz
input rst_n;                                        //异步复位同步释放
//SPI接口信号
input miso;                                         //主收从发信号
output mosi;                                        //主发从收信号
output sclk;                                        //SPI发送接收时钟SCLK
output ss_n;                                        //SPI从设备片选信号
//功能指示与数据传输信号
output wr;                                          //SPI主机发送空闲信号
output rd;                                          //SPI主机接收缓冲区数据可读信号
input[7:0]data_wr;                                  //需通过SPI主机发送的数据
input wr_req;                                       //SPI主机数据发送请求信号
input rd_req;                                       //SPI主机读读从机数据请求信号
output[7:0]data_rd;                                 //SPI主机接收机缓冲区数据
//
wire[8:0]phase;                                     //SCLK时钟相位值
wire[3:0]cnt;                                       //发送/接收字节计数器
//
//控制模块:片选信号及SCLK时钟信号控制
control U1(
    .clk(clk),                                      //时钟信号
    .rst_n(rst_n),                                  //全局复位信号
    .sclk(sclk),                                    //SCLK时钟信号
    .ss_n(ss_n),                                    //从机片选信号
    .phase(phase),                                  //SCLK时钟相位信息
    .wr_req(wr_req),                                //主机写请求
    .rd_req(wr_req),                                //主机读请求
    .cnt(cnt)                                       //字节计数器
    );
//
//发送模块
spi_tx U2(
    .clk(clk),                                      //
    .rst_n(rst_n),                                  //
    .wr_req(wr_req),                                //写请求信号
    .data_wr(data_wr),                              //写数据
    .phase(phase),                                  //SCLK相位
    .wr(wr),                                        //SPI主机发送空闲信号
    .mosi(mosi),                                    //主发从收信号
    .cnt(cnt)                                       //字节计数器
    );
//
//接收模块
spi_rx U3(
    .clk(clk),                                      //
    .rst_n(rst_n),                                  //
    .rd_req(rd_req),                                //读请求信号
    .data_rd(data_rd),                              //读数据
    .phase(phase),                                  //SCLK相位
    .rd(rd),                                        //SPI主机接收缓冲可读信号
    .miso(miso),                                    //主收从发信号
    .cnt(cnt)                                       //字节计数器
    );
//
endmodule
`include "global_definition.v"
module control(
    clk,rst_n,sclk,ss_n,phase,wr_req,rd_req,cnt
    );
input clk;
input rst_n;
output reg sclk;                            //SCLK时钟信号
output reg ss_n;                            //从机片选信号
output reg[8:0]phase;                       //SCLK时钟相位
input wr_req;                               //主机写请求
input rd_req;                               //主机读请求
output cnt;                                 //字节计数器
///
reg[1:0]state;                               //状态机
reg[1:0]next_state;                          //下一状态
//状态机状态变换尽可能贴合BCD码变换顺序,以减少电路成本并降低组合逻辑风险
parameter IDLE = 2'b00;                     //空闲状态
parameter WORK = 2'b01;                     //工作状态
parameter STOP = 2'b11;                     //停止状态
reg[3:0]cnt;                                 //并串转换计数器
///
//相位计数器
always@(posedge clk or negedge rst_n)
    if(!rst_n)  phase <= 9'd0;
    else if(phase == 9'd499) phase <= 9'd0;     //分频数500,SPI时钟速率100KHz
    else phase <= phase + 9'd1;
///
//状态机转移
always@(posedge clk or negedge rst_n)
    if(!rst_n)  state <= IDLE;
    else if(`SCLK_NEG)state <= next_state;      //状态只在SCLK时钟下降沿时改变
    else state <= state;
///
//状态组合判定
always@(wr_req or rd_req or cnt)
    case(state)
    IDLE : if(wr_req||rd_req)  next_state <= WORK;          //空闲状态遭遇读/写请求,下次状态发生改变
            else next_state <= IDLE;
    WORK : if(cnt == 4'd8)  next_state <= STOP;             //发送/接收一字节数据后,状态转移至STOP
            else next_state <= WORK;
    STOP : if(wr_req||rd_req)   next_state <= WORK;         //在STOP状态下,若观测到请求,则继续工作,否则休眠
            else next_state <= IDLE;
    default : next_state <= IDLE;
    endcase
///
//状态机输出
always@(posedge clk or negedge rst_n)
    if(!rst_n)begin
        sclk <= 1'b0;
        ss_n <= 1'b1;
        cnt <= 4'd0;
    end
    else begin
        case(state)
        IDLE : begin
                    sclk <= 1'b0;
                    ss_n <= 1'b1;
                    cnt <= 4'd0;
                end
        WORK : begin
                    ss_n <= 1'b0;
                    if(`SCLK_POS)begin                  //时钟上升沿
                        sclk <= 1'b1;                   //SCLK时钟拉高
                        cnt <= cnt + 4'd1;              //字节计数器加一
                    end
                    else if(`SCLK_NEG)sclk <= 1'b0;     //
                    else sclk <= sclk;         
                end
        STOP : begin
                    cnt <= 4'd0;
                    sclk <= 1'b0;
                    ss_n <= 1'b1;
                end
        default : begin
                        cnt <= 4'd0;
                        sclk <= 1'b0;
                        ss_n <= 1'b1;
                    end
        endcase
    end
        
///
endmodule
`include "global_definition.v"

module spi_tx(
    clk,rst_n,wr_req,data_wr,phase,wr,mosi,cnt
    );
input clk;
input rst_n;
input wr_req;               //主机写请求信号
input[7:0]data_wr;          //写数据
input[8:0]phase;            //SCLK相位
output reg wr;              //写空闲标志
output reg mosi;            //主发从收信号
input[3:0]cnt;              //字节计数器

reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;

//状态机转移
always@(posedge clk or negedge rst_n)
    if(!rst_n) state <= IDLE;
    else if(`SCLK_NEG) state <= next_state;

//状态组合判定
always@(wr_req or cnt)
    case(state)
    IDLE : if(wr_req) next_state <= WORK;
            else next_state <= IDLE;
    WORK : if(cnt == 4'd8)  next_state <= STOP;
            else next_state <= WORK;
    STOP : if(wr_req) next_state <= WORK;
           else next_state <= IDLE;
    default :   state <= IDLE;
    endcase   

//状态机输出
always@(posedge clk or negedge rst_n)
    if(!rst_n)begin
        mosi <= 1'b0;
        wr <= 1'b1;
    end
    else begin
    case(state)
    IDLE :  begin
                mosi <= 1'b0;
                wr <= 1'b1;
            end
    WORK :  begin                               //在SCLK下降沿状态改变后,紧跟
                if(`SCLK_LOW)begin              //SCLK低电平时将数据放置总线上
                    case(cnt)
                    4'd0 : mosi <= data_wr[7];
                    4'd1 : mosi <= data_wr[6];
                    4'd2 : mosi <= data_wr[5];
                    4'd3 : mosi <= data_wr[4];
                    4'd4 : mosi <= data_wr[3];
                    4'd5 : mosi <= data_wr[2];
                    4'd6 : mosi <= data_wr[1];
                    4'd7 : mosi <= data_wr[0];
                    default : mosi <= 1'b0;
                    endcase
                end
                wr <= 1'b0;
            end
    STOP :  begin
                wr <= 1'b1;
                mosi <= 1'b0;
            end
    default :   begin
                    wr <= 1'b1;
                    mosi <= 1'b0;
                end
    endcase
    end

endmodule
`include "global_definition.v"

module spi_rx(
    clk,rst_n,rd_req,data_rd,phase,rd,miso,cnt
    );
input clk;
input rst_n;
input rd_req;               //主机读请求信号
output reg[7:0]data_rd;     //读出数据
input[8:0]phase;            //SCLK相位
output reg rd;              //读数据有效标志
input miso;                 //主收从发信号
input[3:0]cnt;              //字节计数器

reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;

//状态机转移
always@(posedge clk or negedge rst_n)
    if(!rst_n) state <= IDLE;
    else if(`SCLK_NEG) state <= next_state;

//状态组合判定
always@(rd_req or cnt)
    case(state)
    IDLE : if(rd_req) next_state <= WORK;
            else next_state <= IDLE;
    WORK : if(cnt == 4'd8)  next_state <= STOP;
            else next_state <= WORK;
    STOP : if(rd_req) next_state <= WORK;
           else next_state <= IDLE;
    default :   state <= IDLE;
    endcase   

//状态机输出
always@(posedge clk or negedge rst_n)
    if(!rst_n)begin
        rd <= 1'b0;
        data_rd <= 8'd0;
    end
    else begin
    case(state)
    IDLE :  begin
                rd <= 1'b0;
                data_rd <= 8'd0;
            end
    WORK :  begin                               //在SCLK下降沿状态改变后,紧跟
                if(`SCLK_POS)begin              //SCLK上升沿时读取总线上数据
                    case(cnt)
                    4'd0 : data_rd[7] <= miso;
                    4'd1 : data_rd[6] <= miso;
                    4'd2 : data_rd[5] <= miso;
                    4'd3 : data_rd[4] <= miso;
                    4'd4 : data_rd[3] <= miso;
                    4'd5 : data_rd[2] <= miso;
                    4'd6 : data_rd[1] <= miso;
                    4'd7 : data_rd[0] <= miso;
                    default : data_rd <= 8'd0;
                    endcase
                end
                rd <= 1'b0;
            end
    STOP :  begin
                rd <= 1'b1;
            end
    default :   begin
                    rd <= 1'b0;
                    data_rd <= 8'd0;
                end
    endcase
    end

endmodule

头文件如下:

`define SCLK_POS (phase == 9'd499)                 //定义SCLK上升沿
`define SCLK_HIG (phase == 9'd124)                 //定义SCLK高电平
`define SCLK_NEG (phase == 9'd249)                 //定义SCLK下降沿
`define SCLK_LOW (phase == 9'd374)                 //定义SCLK低电平

三、仿真结果

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值