My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI Controller

The SPI interface (Serial Peripheral Interface) is a synchronous serial communication protocol widely used for high-speed data exchange between devices over short distances. Its core features and technical details are as follows:

Communication Modes

  • Uses a master-slave architecture, supporting communication between a single master and one or more slave devices, with the master device managing the slave devices through a chip select signal (CS/SS).
  • Full-duplex synchronous communication, with data transfer rates exceeding 100 MHz.
  • Hardware Structure
  • SCLK: The synchronous clock signal provided by the master device;
  • MOSI: Master output, slave input data line;
  • MISO: Master input, slave output data line;
  • CS/SS: Slave chip select signal line.

My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI ControllerFour modes of the SPI bus: CPOL and CPHA.CPOL indicates the level state of the SPI bus clock when idle, where 0 represents low level and 1 represents high level.CPHA indicates which clock edge is sampled, where 0 indicates the first clock edge and 1 indicates the second clock edge.My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI ControllerObjective of this experiment: Based on the Black Gold platform, implement SPI Master code for FPGA access to M25P16 SPI Flash, including reading the content of the first byte at address 000000, as well as reading the manufacturer ID and device ID of the M25P16.Project Structure:My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI ControllerHardware Structure:My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI Controllertop.v

`timescale 1ns / 1ps
// Create a state machine to input commands to spi_master in sequence
module Top(    input       clk,    input       rst_n,    output      nCS,            output      DCLK,                output      MOSI,                input       MISO,    input       key1,                    // Button triggers transition from IDLE to SEND state
    output      MISO_probe,    output      nCS_ctrl_probe,    output      sys_clk_probe,        output      wr_req_probe,        output      DCLK_probe,        output      MOSI_probe,            output      nCS_probe,            output      wr_ack_probe    );

/* State machine encoding */localparam  S_IDLE = 4'd0;localparam  S_SEND = 4'd1;localparam  S_RECV = 4'd2;
/* Fixed data */localparam  DATA0 = 8'h03;localparam  DATA1 = 8'h00;localparam  DATA2 = 8'h00;localparam  DATA3 = 8'h00;

reg [7:0]  data_in;        wire [7:0]  data_out;//reg     wr_req;wire     wr_ack;
reg     nCS_ctrl;
reg [3:0] state;reg [3:0] next_state;
reg [7:0] send_cnt;reg [7:0] recv_cnt;
reg     wr_ack_d0;reg     wr_ack_d1;wire     wr_ack_pos;        // Detect rising edge
assign wr_ack_pos = wr_ack_d0 & ~wr_ack_d1;
wire      button_negedge;
/*  */


always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)begin        wr_ack_d0 <= 1'b0;        wr_ack_d1 <= 1'b0;end
elsebegin        wr_ack_d0 <= wr_ack;        wr_ack_d1 <= wr_ack_d0;end
end


always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        state <= S_IDLE;    end
    else    begin        state <= next_state;        endend

/* Combinational logic */always @(*) begin    case (state)    S_IDLE:    begin        if(button_negedge == 1'b1)            next_state = S_SEND;else            next_state = S_IDLE;end
    S_SEND:begin        if(send_cnt == 8'd4)            // send_cnt finished sending count        begin            next_state = S_RECV;        end        else        begin            next_state = S_SEND;        end    end
    S_RECV:    begin        if(recv_cnt == 8'd1)            // recv_cnt finished receiving countbegin            next_state = S_IDLE;endelsebegin            next_state = S_RECV;endend
default:        next_state = S_IDLE;    endcase
end

/* Handle send_cnt */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        send_cnt <= 8'd0;end
    else if(state == S_SEND)begin        if(wr_ack == 1'b1)        begin            send_cnt <= send_cnt + 8'd1;endend
    elsebegin        send_cnt <= 8'd0;    end
end

/* Handle recv_cnt */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)begin        recv_cnt <= 8'd0;    end
    else if(state == S_RECV)    begin        if(wr_ack_pos == 1'b1)begin            recv_cnt <= recv_cnt + 8'd1;        end    end
    else    begin        recv_cnt <= 8'd0;end
end

/* Handle nCS_ctrl */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        nCS_ctrl <= 1'd1;end
    else if(state == S_RECV || state == S_SEND)begin        nCS_ctrl <= 1'd0;    end
    else    begin        nCS_ctrl <= 1'd1;end
end

/* Handle data_in */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        data_in <= 8'd0;end
    else if(state == S_SEND)begin        if(send_cnt == 8'd0)            data_in <= DATA0;        else if(send_cnt == 8'd1)            data_in <= DATA1;else if(send_cnt == 8'd2)            data_in <= DATA2;        else if(send_cnt == 8'd3)            data_in <= DATA3;endend

spi_master s0(    .sys_clk        (clk        ),        .rst            (~rst_n        ),        .nCS            (nCS        ),        .DCLK            (DCLK        ),        .MOSI            (MOSI        ),        .MISO            (MISO        ),        .CPOL            (1'b1        ),    .CPHA            (1'b1        ),    .nCS_ctrl        (nCS_ctrl        ),    .clk_div        (16'd0        ),    .wr_req            (~nCS_ctrl        ),    .wr_ack            (wr_ack        ),    .data_in        (data_in        ),    .data_out        (data_out        ),
    /* Output for logic analysis */    .MISO_probe        (MISO_probe        ),    .nCS_ctrl_probe    (nCS_ctrl_probe        ),    .sys_clk_probe    (sys_clk_probe        ),    .wr_req_probe    (wr_req_probe        ),    .DCLK_probe        (DCLK_probe        ),    .MOSI_probe        (MOSI_probe        ),    .nCS_probe        (nCS_probe        ),    .wr_ack_probe    (wr_ack_probe        )
);     

/* Button detection */key_detect ax_debounce_m0(    .clk             (clk),    .rst             (~rst_n),    .button_in       (key1),    .button_posedge  (),    .button_negedge  (button_negedge),    .button_out      ());
endmodule

spi_master.v

module spi_master(    input                       sys_clk,        input                       rst,    output                      nCS,       //chip select (SPI mode)     output                      DCLK,      //spi  clock    output                      MOSI,      //spi  master data output    input                       MISO,      //spi  master input    input                       CPOL,    input                       CPHA,    input                       nCS_ctrl,    input[15:0]                 clk_div,    input                       wr_req,    output                      wr_ack,    input[7:0]                  data_in,    output[7:0]                 data_out,

/* Output for logic analysis */    output                          MISO_probe        ,    output                          nCS_ctrl_probe    ,    output                          sys_clk_probe    ,    output                          wr_req_probe    ,    output                          DCLK_probe        ,    output                          MOSI_probe        ,    output                          nCS_probe        ,    output                          wr_ack_probe
);

/* Output test signals */assign MISO_probe        = MISO;assign nCS_ctrl_probe    = nCS_ctrl;assign sys_clk_probe    = sys_clk;assign wr_req_probe    = wr_req;assign DCLK_probe        = DCLK;assign MOSI_probe        = MOSI;assign nCS_probe        = nCS;assign wr_ack_probe    = wr_ack;

localparam            IDLE            = 0;localparam            DCLK_EDGE       = 1;localparam            DCLK_IDLE       = 2;localparam            ACK             = 3;localparam            LAST_HALF_CYCLE = 4;localparam            ACK_WAIT        = 5;reg                DCLK_reg;reg[7:0]            MOSI_shift;reg[7:0]            MISO_shift;reg[2:0]            state;reg[2:0]            next_state;reg[15:0]            clk_cnt;reg[4:0]            clk_edge_cnt;

assign MOSI = MOSI_shift[7];assign DCLK = DCLK_reg;assign data_out = MISO_shift;assign wr_ack = (state == ACK);assign nCS = nCS_ctrl;

always@(posedge sys_clk or posedge rst)beginif(rst)        state <= IDLE;else        state <= next_state;end

always@(*)begincase(state)        IDLE:            if(wr_req == 1'b1)                next_state <= DCLK_IDLE;else                next_state <= IDLE;        DCLK_IDLE://half a SPI clock cycle produces a clock edgeif(clk_cnt == clk_div)                next_state <= DCLK_EDGE;else                next_state <= DCLK_IDLE;        DCLK_EDGE://a SPI byte with a total of 16 clock edgesif(clk_edge_cnt == 5'd15)                next_state <= LAST_HALF_CYCLE;else                next_state <= DCLK_IDLE;//this is the last data edge                LAST_HALF_CYCLE:if(clk_cnt == clk_div)                next_state <= ACK;else                next_state <= LAST_HALF_CYCLE; //send one byte complete                ACK:            next_state <= ACK_WAIT;//wait for one clock cycle, to ensure that the cancel request signal        ACK_WAIT:            next_state <= IDLE;default:            next_state <= IDLE;    endcaseend

always@(posedge sys_clk or posedge rst)beginif(rst)        DCLK_reg <= 1'b0;else if(state == IDLE)        DCLK_reg <= CPOL;else if(state == DCLK_EDGE)        DCLK_reg <= ~DCLK_reg;//SPI clock edgeend

//SPI clock wait counteralways@(posedge sys_clk or posedge rst)beginif(rst)        clk_cnt <= 16'd0;else if(state == DCLK_IDLE || state == LAST_HALF_CYCLE)         clk_cnt <= clk_cnt + 16'd1;else        clk_cnt <= 16'd0;end
//SPI clock edge counteralways@(posedge sys_clk or posedge rst)beginif(rst)        clk_edge_cnt <= 5'd0;else if(state == DCLK_EDGE)        clk_edge_cnt <= clk_edge_cnt + 5'd1;else if(state == IDLE)        clk_edge_cnt <= 5'd0;end
//SPI data outputalways@(posedge sys_clk or posedge rst)beginif(rst)        MOSI_shift <= 8'd0;else if(state == IDLE && wr_req)        MOSI_shift <= data_in;else if(state == DCLK_EDGE)if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b1)            MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]};else if(CPHA == 1'b1 && (clk_edge_cnt != 5'd0 && clk_edge_cnt[0] == 1'b0))            MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]};end
//SPI data inputalways@(posedge sys_clk or posedge rst)beginif(rst)        MISO_shift <= 8'd0;else if(state == IDLE && wr_req)        MISO_shift <= 8'h00;else if(state == DCLK_EDGE)if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0)            MISO_shift <= {MISO_shift[6:0],MISO};else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1))            MISO_shift <= {MISO_shift[6:0],MISO};end

endmodule 

key_detect.v

`timescale 1 ns / 100 ps
module  key_detect (    input       clk,     input       rst,     input       button_in,    output reg  button_posedge,    output reg  button_negedge,    output reg  button_out);
//// ---------------- internal constants --------------
parameter N = 32 ;           // debounce timer bitwidth
parameter FREQ = 50;         //model clock :Mhz
parameter MAX_TIME = 20;     //ms
localparam TIMER_MAX_VAL =   MAX_TIME * 1000 * FREQ;
////---------------- internal variables ---------------
reg  [N-1 : 0]  q_reg;      // timing regs
reg  [N-1 : 0]  q_next;
reg DFF1, DFF2;             // input flip-flops
wire q_add;                 // control flags
wire q_reset;
reg button_out_d0;
//// ------------------------------------------------------
//// Continuous assignment for counter control
assign q_reset = (DFF1  ^ DFF2);          // xor input flip flops to look for level change to reset counter
assign q_add = ~(q_reg == TIMER_MAX_VAL); // add to counter when q_reg msb is equal to 0
//// Combo counter to manage q_next always @ ( q_reset, q_add, q_reg)begin    case( {q_reset , q_add})        2'b00 :                q_next <= q_reg;        2'b01 :                q_next <= q_reg + 1;        default :                q_next <= { N {1'b0} };    endcase     end
//// Flip flop inputs and q_reg update
always @ ( posedge clk or posedge rst)begin    if(rst == 1'b1)    begin        DFF1 <= 1'b0;        DFF2 <= 1'b0;        q_reg <= { N {1'b0} };    end    else    begin        DFF1 <= button_in;        DFF2 <= DFF1;        q_reg <= q_next;    endend
//// Counter control
always @ ( posedge clk or posedge rst)beginif(rst == 1'b1)        button_out <= 1'b1;    else if(q_reg == TIMER_MAX_VAL)        button_out <= DFF2;    else        button_out <= button_out;end
always @ ( posedge clk or posedge rst)beginif(rst == 1'b1)    begin        button_out_d0 <= 1'b1;        button_posedge <= 1'b0;        button_negedge <= 1'b0;    endelse    begin        button_out_d0 <= button_out;        button_posedge <= ~button_out_d0 & button_out;        button_negedge <= button_out_d0 & ~button_out;    end    endendmodule

Pin Constraint File Top.ucf

NET "clk" IOSTANDARD = LVCMOS33;
NET "DCLK" IOSTANDARD = LVCMOS33;
NET "DCLK_probe" IOSTANDARD = LVCMOS33;
NET "MISO" IOSTANDARD = LVCMOS33;
NET "MISO_probe" IOSTANDARD = LVCMOS33;
NET "MOSI" IOSTANDARD = LVCMOS33;
NET "MOSI_probe" IOSTANDARD = LVCMOS33;
NET "nCS" IOSTANDARD = LVCMOS33;
NET "nCS_ctrl_probe" IOSTANDARD = LVCMOS33;
NET "nCS_probe" IOSTANDARD = LVCMOS33;
NET "sys_clk_probe" IOSTANDARD = LVCMOS33;
NET "wr_ack_probe" IOSTANDARD = LVCMOS33;
NET "wr_req_probe" IOSTANDARD = LVCMOS33;
NET "rst_n" IOSTANDARD = LVCMOS33;

NET "clk" LOC = T8;
NET "DCLK" LOC = R11;
NET "DCLK_probe" LOC = M4;
NET "MISO" LOC = P10;
NET "MISO_probe" LOC = N4;
NET "MOSI" LOC = T10;
NET "MOSI_probe" LOC = P2;
NET "nCS" LOC = T3;
NET "nCS_ctrl_probe" LOC = M5;
NET "nCS_probe" LOC = R2;
NET "rst_n" LOC = L3;
NET "sys_clk_probe" LOC = N6;
NET "wr_ack_probe" LOC = R1;
NET "wr_req_probe" LOC = P6;
# PlanAhead Generated physical constraints 
NET "key1" LOC = C3;
# PlanAhead Generated IO constraints 
NET "key1" IOSTANDARD = LVCMOS33;

Experiment Phenomenon 1: After compiling and programming the FPGA firmware, pressing the key1 button on the development board captured the SPI communication waveform with a logic analyzer, where the FPGA first sent the 03h command and the 000000 address, then read 1 byte of data 0E (the data written by the official Black Gold example).My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI ControllerThe preceding code implements data reading, and below is the code for reading the manufacturer ID and device ID of the M25P16, which only requires modifying the data in top.v, as shown in the modified code below.Top.v

`timescale 1ns / 1ps
// Create a state machine to input commands to spi_master in sequence
module my_spi_top_2(    input       clk,    input       rst_n,    output      nCS,            output      DCLK,                output      MOSI,                input       MISO,    input           key1,                    // Button triggers transition from IDLE to SEND state
/* Connect signals for logic analysis */    output      MISO_probe,    output      nCS_ctrl_probe,    output      sys_clk_probe,        output      wr_req_probe,        output      DCLK_probe,        output      MOSI_probe,            output      nCS_probe,            output      wr_ack_probe    );

/* State machine encoding */localparam  S_IDLE = 4'd0;localparam  S_SEND = 4'd1;localparam  S_RECV = 4'd2;
/* Fixed data, "READ IDENTIFICATION", the ID command for the M25P16 storage chip is 9Fh, 9Eh */localparam  DATA0 = 8'h9f;localparam  DATA1 = 8'h9e;localparam  DATA2 = 8'h00;localparam  DATA3 = 8'h00;

reg [7:0]  data_in;        wire [7:0]  data_out;//reg     wr_req;wire     wr_ack;
reg     nCS_ctrl;
reg [3:0] state;reg [3:0] next_state;
reg [7:0] send_cnt;reg [7:0] recv_cnt;
reg     wr_ack_d0;reg     wr_ack_d1;wire     wr_ack_pos;        // Detect rising edge
assign wr_ack_pos = wr_ack_d0 & ~wr_ack_d1;
wire      button_negedge;
/*  */


always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)begin        wr_ack_d0 <= 1'b0;        wr_ack_d1 <= 1'b0;end
elsebegin        wr_ack_d0 <= wr_ack;        wr_ack_d1 <= wr_ack_d0;end
end


always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        state <= S_IDLE;    end
    else    begin        state <= next_state;        endend

/* Combinational logic */always @(*) begin    case (state)    S_IDLE:    begin        if(button_negedge == 1'b1)            next_state = S_SEND;else            next_state = S_IDLE;end
    S_SEND:begin        if(send_cnt == 8'd2)            // send_cnt finished sending count, 9fh, 9eh commands require sending 2 bytes of data        begin            next_state = S_RECV;        end        else        begin            next_state = S_SEND;        end    end
    S_RECV:    begin        if(recv_cnt == 8'd20)            // recv_cnt finished receiving count, 9eh command reads back 20 bytes of data, including Manufacturer ID (EFh) and Device IDbegin            next_state = S_IDLE;endelsebegin            next_state = S_RECV;endend
default:        next_state = S_IDLE;    endcase
end

/* Handle send_cnt */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        send_cnt <= 8'd0;end
    else if(state == S_SEND)begin        if(wr_ack == 1'b1)        begin            send_cnt <= send_cnt + 8'd1;endend
    elsebegin        send_cnt <= 8'd0;    end
end

/* Handle recv_cnt */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)begin        recv_cnt <= 8'd0;    end
    else if(state == S_RECV)    begin        if(wr_ack_pos == 1'b1)begin            recv_cnt <= recv_cnt + 8'd1;        end    end
    else    begin        recv_cnt <= 8'd0;end
end

/* Handle nCS_ctrl */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        nCS_ctrl <= 1'd1;end
    else if(state == S_RECV || state == S_SEND)begin        nCS_ctrl <= 1'd0;    end
    else    begin        nCS_ctrl <= 1'd1;end
end

/* Handle data_in */always @(posedge clk or negedge rst_n) begin    if(rst_n == 1'b0)    begin        data_in <= 8'd0;end
    else if(state == S_SEND)begin        if(send_cnt == 8'd0)            data_in <= DATA0;        else if(send_cnt == 8'd1)            data_in <= DATA1;else if(send_cnt == 8'd2)            data_in <= DATA2;        else if(send_cnt == 8'd3)            data_in <= DATA3;endend

spi_master s0(    .sys_clk        (clk        ),        .rst            (~rst_n        ),        .nCS            (nCS        ),        .DCLK            (DCLK        ),        .MOSI            (MOSI        ),        .MISO            (MISO        ),        .CPOL            (1'b1        ),    .CPHA            (1'b1        ),    .nCS_ctrl        (nCS_ctrl        ),    .clk_div        (16'd0        ),    .wr_req            (~nCS_ctrl        ),    .wr_ack            (wr_ack        ),    .data_in        (data_in        ),    .data_out        (data_out        ),
    /* Output for logic analysis */    .MISO_probe        (MISO_probe        ),    .nCS_ctrl_probe    (nCS_ctrl_probe        ),    .sys_clk_probe    (sys_clk_probe        ),    .wr_req_probe    (wr_req_probe        ),    .DCLK_probe        (DCLK_probe        ),    .MOSI_probe        (MOSI_probe        ),    .nCS_probe        (nCS_probe        ),    .wr_ack_probe    (wr_ack_probe        )
);     

/* Button detection */key_detect ax_debounce_m0(    .clk             (clk),    .rst             (~rst_n),    .button_in       (key1),    .button_posedge  (),    .button_negedge  (button_negedge),    .button_out      ());
endmodule

Experiment Phenomenon 2: After compiling and programming the FPGA firmware, pressing key1, the FPGA sent the commands specified in the M25P16 datasheet, 9fh and 9eh, and read the corresponding IDs.My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI ControllerM25P16 Datasheet:My 7th Lesson on FPGA: Accessing Flash Memory with a Mature SPI Controller

Leave a Comment