SPI 기반 Flash 메모리 제어 구현

  1. SPI 통신 프로토콜 개요

SPI(Serial Peripheral Interface)는 고속, 전이방식, 동기식 시리얼 통신 인터페이스로, 모토롤라에서 개발한 표준이다. 이 프로토콜은 단순한 하드웨어 설계와 높은 데이터 전송 속도를 제공하지만, 데이터 수신 확인 메커니즘이 없어 신뢰성에 약점이 있다.

주요 핀 정의

  • SCK (Serial Clock): 마스터 장치가 생성하는 클럭 신호로, 데이터 전송을 동기화한다.
  • MOSI (Master Output Slave Input): 마스터에서 슬레이브로의 데이터 출력선.
  • MISO (Master Input Slave Output): 마스터에서 슬레이브로의 데이터 입력선.
  • CS# (Chip Select): 슬레이브 선택 신호로, 활성 낮음(0) 상태일 때 해당 장치가 작동한다. 각 슬레이브마다 독립된 CS 신호가 필요하다.

통신 모드 설정

SPI는 총 4가지 모드로 구성되며, 다음 두 파라미터에 따라 결정된다:

  • CPOL (Clock Polarity): 대기 상태에서 클럭의 전압 수준.
  • CPHA (Clock Phase): 데이터 샘플링 시점.
  • CPHA=0: 첫 번째 클럭 에지에서 샘플링.
  • CPHA=1: 두 번째 클럭 에지에서 샘플링.

데이터 전송 순서

데이터는 최상위 비트부터 전송되며, 클럭에 맞춰 일정한 주기로 전달된다.


  1. M25P16 Flash 메모리 특성

기본 사양

  • 용량: 16Mbit (2MB)
  • 섹터 구조: 32개 섹터, 각 섹터당 256페이지
  • 페이지 크기: 256바이트
  • 비휘발성 저장: 전원 차단 후 데이터 유지

지원 모드

  • 모드 0 (CPOL=0, CPHA=0)
  • 모드 3 (CPOL=1, CPHA=1)

핵심 명령어 설명

2.1 쓰기 허용 (WREN)

쓰기 작업(페이지 프로그래밍, 섹터 지우기 등) 전 반드시 실행해야 하는 명령어. 내부 쓰기 허용 레지스터를 활성화한다.

2.2 섹터 지우기 (SE)

선택한 섹터 내 모든 비트를 FFh로 초기화. 지우기 시작 후에는 상태 레지스터의 WIP 비트가 1로 설정되며, 완료 시 0으로 변경된다.

2.3 전체 지우기 (BE)

전체 플래시 메모리의 모든 데이터를 FFh로 초기화한다.

2.4 상태 레지스터 읽기 (RDSR)
  • WIP (Write In Progress): 현재 쓰기/지우기 작업 중인지 여부. 1 = 진행 중.
  • WEL (Write Enable Latch): 쓰기 허용 상태. 1 = 쓰기 가능.
  • BP2/BP1/BP0: 보호 영역 설정 비트. 1로 설정 시 해당 영역은 프로그램 및 지우기 금지.
  • SRWD (Status Register Write Disable): 상태 레지스터 쓰기 방지 비트. 하드웨어 보호 모드 진입 시 사용.
2.5 페이지 프로그래밍 (PP)
  • 한 페이지(256바이트)에 데이터를 쓸 수 있으며, 초과 시 오버랩 발생.
  • 주소가 0이 아닌 경우, 페이지 경계를 넘어 다시 처음부터 반복 쓰기 가능.
2.6 데이터 읽기 (READ)

임의의 주소에서 데이터를 읽을 수 있으며, 자동으로 주소 증가. 쓰기 또는 지우기 중에는 읽기 명령이 무시된다.

2.7 디바이스 식별 (RDID)
  • CS# 하강 → RDID 명령 (10011111) → 3바이트 디바이스 정보 → CS# 상승
  • 지우기/프로그램 중에는 이 명령이 무시됨.

  1. 하드웨어 설계 및 모듈 구현

3.1 SPI 마스터 모듈 설계

module spi_master (
    input clk,
    input rst_n,
    input [7:0] din,
    input req,
    output reg [7:0] dout,
    output reg done,
    output sclk,
    output cs_n,
    output mosi,
    input miso
);

parameter SCLK_PERIOD = 16; // 3.125MHz, 320ns
parameter BIT_COUNT = 8;

// 상태 정의
localparam IDLE = 4'b0001,
           READY = 4'b0010,
           DATA = 4'b0100,
           DONE = 4'b1000;

reg [3:0] state;
reg [3:0] cnt_sclk;
reg [2:0] cnt_bit;
reg spi_sclk, spi_mosi;

// 상태 머신
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

// 상태 전환 조건
assign idle2ready = (state == IDLE) && req;
assign ready2data = (state == READY);
assign data2done = (state == DATA) && (cnt_bit == 7);
assign done2idle = (state == DONE);

// 클럭 생성 (모드 3: 클럭 활성 높음, 데이터 하강 시 샘플링)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_sclk <= 1'b1;
        cnt_sclk <= 0;
    end else if (cnt_sclk == 4 - 1) begin
        spi_sclk <= 1'b0;
    end else if (cnt_sclk == 12 - 1) begin
        spi_sclk <= 1'b1;
    end else if (req && (state == DATA)) begin
        cnt_sclk <= cnt_sclk + 1;
    end
end

// MOSI 출력 (최상위 비트 우선)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        spi_mosi <= 0;
    else if (cnt_sclk == 4 - 1)
        spi_mosi <= din[7 - cnt_bit];
end

// MISO 입력 (클럭 상승 시 샘플링)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        dout <= 0;
    else if (cnt_sclk == 12 - 1)
        dout[7 - cnt_bit] <= miso;
end

// 비트 카운터
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt_bit <= 0;
    else if (state == DATA && cnt_sclk == 12 - 1)
        cnt_bit <= cnt_bit + 1;
end

// 출력 선언
assign sclk = spi_sclk;
assign cs_n = ~req;
assign mosi = spi_mosi;
assign done = (state == DONE) && (cnt_bit == 7);

endmodule

3.2 Flash 제어 모듈

module flash_ctrl #(
    parameter READ_MAX = 4,
    parameter WRITE_MAX = 4,
    parameter WAIT_MAX = 150_000_000,
    parameter DONE_MAX = 250_000
)(
    input clk,
    input rst_n,
    input wr_en,
    input rd_en,
    input done,
    input busy,
    input [7:0] rx_data,
    input rx_data_vld,
    input [7:0] din_data,
    output reg [7:0] tx_data,
    output reg tx_data_vld,
    output reg [7:0] dout_data,
    output reg req
);

// 상태 정의
localparam IDLE = 0,
           WREN1 = 1,
           SECTOR_ERASE = 2,
           WAITING = 3,
           WREN2 = 4,
           PAGE_WRITE = 5,
           READ_OP = 6,
           DONE_STATE = 7;

reg [3:0] state;
reg [30:0] delay_cnt;
reg delay_flag;

// 명령 코드 정의
localparam RD_CMD = 8'h03,
           RD_ID = 8'h9F,
           WREN = 8'h06,
           SE = 8'hD8,
           PP = 8'h02;

// 상태 전이 조건
assign idle2wren1 = (state == IDLE) && wr_en;
assign wren12se = (state == WREN1) && delay_flag;
assign se2wait = (state == SECTOR_ERASE) && delay_flag;
assign wait2wren2 = (state == WAITING) && delay_flag;
assign wren22pp = (state == WREN2) && delay_flag;
assign pp2done = (state == PAGE_WRITE) && (byte_cnt == WRITE_MAX - 1);
assign read2done = (state == READ_OP) && (byte_cnt == READ_MAX - 1);
assign done2idle = (state == DONE_STATE) && delay_flag;

// 지연 카운터
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        delay_cnt <= 0;
        delay_flag <= 0;
    end else if (delay_flag) begin
        if (delay_cnt == ((state == WAITING) ? WAIT_MAX - 1 : 
                         (state == DONE_STATE) ? DONE_MAX - 1 : 5) - 1)
            delay_cnt <= 0;
        else
            delay_cnt <= delay_cnt + 1;
    end else begin
        if (state == WREN1 || state == WREN2 || state == SECTOR_ERASE)
            delay_flag <= 1;
        else if (state == WAITING || state == DONE_STATE)
            delay_flag <= 1;
    end
end

// 바이트 카운터
reg [9:0] byte_cnt;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        byte_cnt <= 0;
    else if (done)
        byte_cnt <= byte_cnt + 1;
end

// 출력 데이터 제어
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        dout_data <= 0;
    else case(state)
        WREN1: dout_data <= WREN;
        SECTOR_ERASE: dout_data <= SE;
        PAGE_WRITE: begin
            if (byte_cnt < 4)
                dout_data <= PP;
            else
                dout_data <= rx_data;
        end
        READ_OP: begin
            if (byte_cnt < 4)
                dout_data <= RD_CMD;
            else
                dout_data <= din_data;
        end
        default: dout_data <= 0;
    endcase
end

// req 신호 제어
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        req <= 0;
    else if (state == IDLE)
        req <= 0;
    else if (done || delay_flag)
        req <= 0;
    else
        req <= 1;
end

// FIFO 연결 (데이터 버퍼)
fifo_8x256 u_fifo_wr (
    .clock(clk),
    .aclr(~rst_n),
    .data(rx_data),
    .wrreq(~fifo_full && rx_data_vld),
    .rdreq(fifo_rd),
    .q(fifo_qout),
    .empty(fifo_empty),
    .full(fifo_full),
    .usedw(fifo_used)
);

// 결과 데이터 처리
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        tx_data <= 0;
    else if (read2done)
        tx_data <= din_data;
end

assign tx_data_vld = (state == READ_OP) && (byte_cnt == READ_MAX - 1);

endmodule

3.3 최상위 모듈 구성

module top (
    input clk,
    input rst_n,
    input rx,
    output tx,
    input [1:0] key_in,
    output sclk,
    output cs_n,
    output mosi,
    input miso
);

wire [7:0] rx_data;
wire rx_valid;
wire [7:0] tx_data;
wire tx_valid;
wire [7:0] flash_dout;
wire flash_req;
wire flash_done;

flash_ctrl #(.READ_MAX(20), .WRITE_MAX(20)) u_flash_ctrl (
    .clk(clk),
    .rst_n(rst_n),
    .wr_en(key_in[0]),
    .rd_en(key_in[1]),
    .done(flash_done),
    .busy(tx_valid),
    .rx_data(rx_data),
    .rx_data_vld(rx_valid),
    .din_data(flash_dout),
    .tx_data(tx_data),
    .tx_data_vld(tx_valid),
    .dout_data(flash_dout),
    .req(flash_req)
);

spi_master u_spi (
    .clk(clk),
    .rst_n(rst_n),
    .din(flash_dout),
    .req(flash_req),
    .dout(flash_dout),
    .done(flash_done),
    .sclk(sclk),
    .cs_n(cs_n),
    .mosi(mosi),
    .miso(miso)
);

key_filter u_key_filter (
    .clk(clk),
    .rst_n(rst_n),
    .key_in(key_in),
    .key_out(key_out)
);

uart_rx u_uart_rx (
    .clk(clk),
    .rst_n(rst_n),
    .rx_din(rx),
    .rx_dout(rx_data),
    .rx_dout_vld(rx_valid)
);

uart_tx u_uart_tx (
    .clk(clk),
    .rst_n(rst_n),
    .tx_din(tx_data),
    .tx_din_vld(tx_valid),
    .tx_dout(tx),
    .tx_done(tx_valid)
);

endmodule

  1. 시뮬레이션 및 하드웨어 검증

4.1 시뮬레이션 코드 예제

`timescale 1ns/1ps
module tb_top;

reg clk, rst_n;
reg rx;
reg [1:0] key_in;
wire tx;

top u_top (
    .clk(clk),
    .rst_n(rst_n),
    .rx(rx),
    .tx(tx),
    .key_in(key_in),
    .sclk(sclk),
    .cs_n(cs_n),
    .mosi(mosi),
    .miso(miso)
);

initial begin
    clk = 1;
    rst_n = 0;
    #100;
    rst_n = 1;
    #100;
    key_in = 2'b10;
    #200;
    key_in = 2'b11;
    #10000;
    $stop;
end

always #10 clk = ~clk;

endmodule

4.2 결과 검증

시뮬레이션 및 실제 보드 테스트를 통해, 주어진 명령어 시퀀스에 따라 플래시 메모리의 쓰기 및 읽기 성공 여부를 확인. 입력 데이터와 출력 데이터가 일치함을 확인했으며, 전체 제어 로직이 안정적으로 동작함을 입증하였다.

태그: SPI Flash M25P16 FPGA Verilog

8월 7일 15:17에 게시됨