- 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: 두 번째 클럭 에지에서 샘플링.
데이터 전송 순서
데이터는 최상위 비트부터 전송되며, 클럭에 맞춰 일정한 주기로 전달된다.
- 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#상승- 지우기/프로그램 중에는 이 명령이 무시됨.
- 하드웨어 설계 및 모듈 구현
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
- 시뮬레이션 및 하드웨어 검증
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 결과 검증
시뮬레이션 및 실제 보드 테스트를 통해, 주어진 명령어 시퀀스에 따라 플래시 메모리의 쓰기 및 읽기 성공 여부를 확인. 입력 데이터와 출력 데이터가 일치함을 확인했으며, 전체 제어 로직이 안정적으로 동작함을 입증하였다.