用Verilog实现一个SPI从机

/ 0评 / 0

网上很多教程都是怎么做SPI主机,怎么控制外部的东西,但是有时候FPGA是作为接受上游信息的存在的,有必要实现一个从机协议.我这里就一步一步的描述我是怎么思考然后最终实现一个从机的.

首先我这里确定一下我需要的信号.

module top(
    input wire clk_i,
    input wire rst_i,

    input wire sdi_csn_i,
    input wire sdi_clk_i,
    input wire sdi_dat_i,
    output wire sdi_dat_o,
    
    output wire [5:0] led
);

因为我的开发板只有6个LED,所以就暂时让收到的8B数据中的6B显示在LED,其余的都是SPI本身的信号,以及系统主时钟,也可以理解成采样时钟,并且采样时钟是快于SPI时钟的.

然后跨时钟域采集他们,并且要捕获SCK的上升沿.

reg [2:0] sync_sck_ff;
reg [1:0] sync_csn_ff;
reg [1:0] sync_sdi_ff; 

// SCK 边缘采集
assign sync_sck = sync_sck_ff[1] ^ sync_sck_ff[2];
assign sync_csn = sync_csn_ff[1];
assign sync_sdi = sync_sdi_ff[1];

// 跨时钟域处理
always @(posedge clk_i or negedge rstn_i) begin    
    if(!rstn_i) begin
        sync_sck_ff <= 1'b0;
        sync_csn_ff <= 1'b0;
        sync_sdi_ff <= 1'b0;
    end else begin
        sync_sck_ff <= {sync_sck_ff[1:0],sdi_clk_i};
        sync_csn_ff <= {sync_csn_ff[0],sdi_csn_i};
        sync_sdi_ff <= {sync_sdi_ff[0],sdi_dat_i};
    end
end

其中sync_sck指示目前时钟跳变了,sync_csn是传递到clk_i时钟域的CS,sync_sdi是传递到clk_i时钟域的DAT,sdi_dat_o为什么没在这里呢,因为我们clk_i是快时钟,我们只要保证快时钟有足够的周期数,慢时钟的sdi_clk_i就能采集,而因为我们一定会保持到下一个sdi_clk_i来到,所以肯定是足够的.

接下来到最难的状态机部分了,我这里定义了三个状态,用serial_state来记录.

reg [1:0] serial_state;
reg [3:0] serial_cnt;
reg [7:0] serial_sreg;
reg serial_sdi_ff;
reg serial_start;
reg serial_done;

assign sdi_dat_o = serial_sreg[7];

always @(posedge clk_i or negedge rstn_i) begin
    if(!rstn_i) begin
        serial_start  <= 1'b0;
        serial_done   <= 1'b0;
        serial_state  <= 3'b0;
        serial_cnt    <= 4'b0;
        serial_sreg   <= 8'b0;
        serial_sdi_ff <= 1'b0;
    end
    else begin
        serial_start <= 1'b0;
        serial_done <= 1'b0;

        case (serial_state) 
            2'b00:begin
                serial_cnt <= 4'b0;
                serial_sreg <= 8'h00;
                if(!sync_csn) begin
                    serial_start <= 1'b1;
                    serial_state[1:0] <= 2'b10;
                end 
            end
            2'b10:begin
                serial_sdi_ff <= sync_sdi;
                if(sync_csn) begin
                    serial_state[1:0] <= 2'b00;
                end
                else if (sync_sck) begin
                    serial_cnt <= serial_cnt + 1'b1;
                    serial_state[1:0] <= 2'b11;
                end
            end
            2'b11:begin
                if(sync_csn) begin
                    serial_state[1:0] <= 2'b00;
                end
                else if(sync_sck) begin
                    serial_sreg <= {serial_sreg[6:0],serial_sdi_ff};
                    if(serial_cnt[3] == 1'b1) begin
                        serial_done <= 1'b1;
                        serial_state[1:0] <= 2'b00;
                    end
                    else begin
                        serial_state[1:0] <= 2'b10;
                    end
                end
            end
            default begin
                serial_state[1:0] <= 2'b00;
            end
        endcase
    end
end

把数值附给LED.

reg [7:0] led_reg;
assign led = ~led_reg[5:0];

always @(posedge clk_i or negedge rstn_i) begin
    if(!rstn_i) begin
        led_reg[7:0] = 8'b0;
    end
    else if(serial_done)
    begin
        led_reg[7:0] = serial_sreg[7:0];
    end
end

但是在实测中serial_sreg仍有低概率是错误的,这时候可以再前置一个滤波采集,完整代码如下.

https://gist.github.com/nickfox-taterli/fe3713455b0ba55c73b63d45512f2bd9

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注