主机与S25FL256S 存储器设备之间的所有通信都是以称为命令(command) 的单元形式进行的。所有命令都以一个指令(instruction) 开始,该指令选择要执行的信息传输类型或设备操作。命令还可能包含地址、指令修饰符、延迟周期、向存储器的数据传输或从存储器的数据传输。所有指令、地址和数据信息在主系统与存储器设备之间以串行方式 传输,所有指令通过 SI 信号作为单比特串行序列从主机传输到存储器。**单比特命令可能仅在 SI 信号上传输地址或数据。数据可以通过 SO 信号以串行方式返回主机。**命令的结构如下:
- 每个命令以 CS拉低(LOW)开始,以 CS返回高电平(HIGH)结束。主机通过将芯片选择(CS)信号在整个命令期间拉低来选择存储设备。
- 串行时钟(SCK)标志着主机与存储器之间每个比特或比特组的传输。
- 每条命令以八位(1字节)指令开始。指令总是以单比特串行序列的形式在串行输入(SI)信号上呈现,每个 SCK 上升沿向存储器设备传输一个比特。指令选择要执行的信息传输类型或设备操作。
- 指令可以是独立的,也可以后跟地址位,以选择设备中多个地址空间之一的某个位置。指令决定使用的地址空间。地址可以是 24 位或 32 位的字节边界地址。地址传输在 SDR 命令中发生在 SCK 上升沿,在 DDR 命令中在每个 SCK 边沿传输。
- 紧随指令之后的所有传输宽度由发送的指令决定。后续传输可以继续仅在 SI 或串行输出(SO)信号上进行单比特串行传输,也可以在 IO0 和 IO1 信号上以每次传输两比特(双模式)进行,或者在 IO0-IO3 信号上以每次传输四比特(四模式)进行。
- 一些指令在地址之后会发送一个称为模式位的指令修饰符,以指示下一个命令将是相同类型的,且为隐式而非显式指令。因此,下一个命令不提供指令字节,仅提供新的地址和模式位。当在一系列命令中重复使用相同类型命令时,这可以减少发送每个命令所需的时间。模式位传输发生在SCK上升沿(对于SDR命令),或者发生在每个SCK边沿(对于DDR命令)。
- 地址或模式位之后可能跟随写入数据以存储到存储设备中,或者在返回读取数据之前经历读取延迟周期。
- 写入数据位传输发生在SCK上升沿(对于SDR命令),或者发生在每个SCK边沿(对于DDR命令)。
- 在任何读取访问延迟期间,SCK继续切换。延迟周期可以为'0'到几个SCK周期(也称为虚拟周期)。在读取延迟周期结束时,第一批读取数据位将在最后一个读取延迟周期结束的SCK下降沿从输出引出。第一批读取数据位在随后的SCK上升沿被视为传输给主机。每次随后的传输发生在下一个SCK上升沿(对于SDR命令),或者发生在每个SCK边沿(对于DDR命令)。
- 如果命令向主机返回读取数据,设备将继续发送数据传输,直到主机将CS信号拉高。在读取数据序列中的任何传输之后,CS信号都可以被拉高,这将终止该命令。
- 在不返回数据的命令结束时,主机将 CS 输入拉高。CS信号必须在独立指令的第八位之后,或者在传输的最后一个写入数据字节之后变为高电平。也就是说,当 CS信号被拉低后的时钟周期数是八的整数倍时,CS信号必须被拉高。如果 CS信号没有在指令或写入数据的第八个 SCK 时钟周期边界准确变高,则该命令将被拒绝,并且不会执行。
- 所有指令、地址和模式位都以最高有效位(MSb)优先的方式移入设备。数据位在移入和移出设备时也以 MSb 优先。所有数据以字节为单位进行传输,先发送最低地址字节。随后字节的数据按从低到高字节地址顺序发送,即字节地址递增。
- 在编程、擦除或写入周期(嵌入操作)期间,尝试读取闪存阵列的操作会被忽略。嵌入操作将继续执行而不会受到任何影响。在嵌入操作期间,仅允许极少数命令。具体命令将在各个命令描述中讨论。
- 根据命令的不同,执行所需时间不同。用于从正在执行的命令中读取状态信息的命令可以用来确定命令何时执行完成,以及命令是否成功。
主闪存阵列被划分为称为扇区的擦除单元。扇区的组织形式可采用4KB与64KB扇区的混合组合,或采用统一的256KB扇区。

配置寄存器:

**一、Status Register 1 (SR1)**相关命令:
读取状态寄存器(RDSR1 05h)、写入寄存器(WRR 01h)、写入使能(WREN 06h)、写入禁用(WRDI 04h)、清除状态寄存器(CLSR 30h)。

写使能锁存器(WEL)SR1[1]: WEL 位必须设置为1才能启用编程、写入或擦除操作,以防止内存或寄存器值被意外更改。执行写使能(WREN)命令会将写使能锁存器设置为 1,允许后续执行任何编程、擦除或写入命令。使用写禁用(WRDI)命令可以将写使能锁存器设置为 0,阻止所有编程、擦除和写入命令的执行。在任何成功的编程、写入或擦除操作结束后, WEL 位会被清零。如果操作失败, WEL 位可能保持设置,应在执行 CLSR 命令后使用 WRDI 命令将其清零。在电源关闭/重新启动、硬件复位或软件复位后,写使能锁存器会被设置为 0。 WRR 命令不会影响此位。
**1. 写使能(WREN 06h)**命令将状态寄存器1(SR1[1])的写入使能锁存器(WEL)位设置为1。必须通过发出写入使能(WREN)命令将写入使能锁存器(WEL)位设置为1,以启用写入、编程和擦除命令。
在指令字节的第八位被锁存在SI后,CS必须驱动到逻辑高电平状态。如果在指令字节的第八位被锁存在SI后,CS没有被驱动到逻辑高电平状态,写入使能操作将不会执行。

IDLE → SEND_CMD → IDLE
**2. 写禁用(WRDI 04h)**命令将状态寄存器(SR1[1])的写入启用锁存器(WEL)位设置为0。通过发出写入禁用(WRDI)命令,可以将写入启用锁存器(WEL)位设置为0,从而禁用页编程(PP)、扇区擦除(SE)、批量擦除(BE)、写入寄存器(WRR)、一次性编程(OTPP)以及其他需要 WEL 位置为1才能执行的命令。用户可以使用 WRDI 命令来保护内存区域,防止意外写入可能损坏内存内容。在嵌入式操作期间,当WIP位=1时, WRDI 命令会被忽略。
CS信号必须在SI信号锁存指令字节的第八位后,被驱动至逻辑高电平。若未在SI锁存第八位后将CS驱动至逻辑高电平,则写入禁用操作将无法执行。
**3. 读取状态寄存器1(RDSR1 05h)**指令可从SO读取状态寄存器1的内容。该寄存器内容可随时读取,即使在程序运行、擦除或写入操作进行时亦可。通过提供八倍时钟周期的倍数,可实现状态寄存器1的连续读取。每读取八个周期,状态信息即更新一次。

**4. 写寄存器(WRR 01h)**允许将新值写入状态寄存器1和配置寄存器。在设备接受写寄存器(WRR)命令之前,必须先接收到写使能(WREN)命令。成功译码写使能(WREN)命令后,设备将在状态寄存器中设置写使能锁存器(WEL),以启用任何写操作。
CS在数据的第八或第十六位锁存后必须驱动到高电平。如果不这样做,写寄存器(WRR)命令将不会执行。如果CS在第八周期后被驱动到高电平,则仅写入状态寄存器1;否则,在第十六周期后,状态寄存器和配置寄存器都将被写入。
- 清除状态寄存器(CLSR 30h)命令会重置位SR1[5](擦除失败标志)和位SR1[6](编程失败标志)。执行清除SR命令前无需设置 WEL 位。
二、页编程(PP 02h)指令:
允许对内存中的字节进行编程(将位从1变为0)。在设备接收 PP 指令之前,必须先发出写使能(WREN)指令并由设备译码。在写使能(WREN)指令成功译码后,设备在状态寄存器中设置写入使能锁存器(WEL),以启用任何写入操作。02h (ExtAdd=0) 允许24位地址(A23-A0)。

IDLE → WR_ENABLE → SEND_CMD → SEND_ADDR → SEND_WDATA → WAIT_BUSY
三、读指令(Read 03h):
03h (ExtAdd=0) 允许24位地址(A23-A0) ,随后,系统会通过SO总线将指定地址的存储器内容移出。READ命令的最大工作时钟频率为50MHz。该地址可从存储器阵列的任意字节位置开始。每当移出一个字节数据后,地址会自动按顺序递增到下一个更高地址。因此,只要提供地址000000h,就能通过单条读取指令读取整个存储器。当达到最高地址时,地址计数器会自动回绕并重置为000000h,从而允许读取序列无限次循环执行。

IDLE → SEND_CMD → SEND_ADDR → REC_RDATA → IDLE
四、快速读指令(FAST_READ 0Bh):
0Bh (ExtAdd=0) 允许24位地址(A23-A0)。地址后会跟随零个或八个虚拟周期,具体取决于配置寄存器中设置的延迟码。这些虚拟周期为设备内部电路提供了额外时间来访问初始地址位置。在虚拟周期期间,SO端的数据值为"不关心"状态,可能呈现高阻抗。随后,指定地址处的存储器内容会通过SO端移出。FAST READ命令的最大工作时钟频率为133 MHz。该地址可从存储器阵列的任意字节位置开始。每次移出一个字节数据后,地址会自动按顺序递增到下一个更高地址。因此,只要提供地址000000h,整个存储器就能通过单次读取指令完成读取。当达到最高地址时,地址计数器会回滚至000000h,从而允许读取序列无限次循环。

IDLE → SEND_CMD → SEND_ADDR →SEND_DUMMY_BYTE → REC_RDATA → IDLE
五、4KB扇区擦除指令( P4E 20h);
4KB扇区擦除(P4E)命令会将4KB参数扇区的所有位设置为1(所有字节均为FFh)。在设备接受P4E命令之前,必须先发出并译码写使能(WREN)命令,该命令通过在状态寄存器中设置写入使能锁存器(WEL)来启用任何写入操作。指令20h(扩展地址=0)后跟一个3字节地址(A23-A0),或 20h(扩展地址=1)后跟一个4字节地址(A31-A0),或 21 h后跟一个4字节地址(A31-A0)。

IDLE → WR_ENABLE → SEND_CMD → SEND_ADDR → WAIT_BUSY → IDLE
地址的第24位或第32位锁存后,CS必须被驱动到逻辑高电平。这将启动内部擦除周期的开始,该周期涉及闪存阵列所选扇区的预编程和擦除。如果地址的最后一位后CS未被驱动到高电平,则扇区擦除操作不会执行。一旦CS被驱动到高电平,内部擦除周期将立即启动。在内部擦除周期进行期间,用户可以通过读取写入进行中(WIP)位的值来确定操作何时完成。WIP位在擦除周期进行时显示为1,在擦除周期完成后显示为0。
若对通过块保护位(Block Protection bits)或ASP(扇区保护位)设置写保护的扇区执行P4E命令,该命令将无法执行并触发E_ERR状态;若对超过4千字节(4 kbytes)的扇区执行P4E命令,同样无法执行且不会触发E_ERR状态。
六、64KB扇区擦除指令(SE D8h or 4SE DCh)
ASP为每个扇区,包括任何4KB的扇区,提供了一个PPB和一个 DYB 保护位。如果对包含受保护的4KB扇区的64KB范围或包含64KB受保护地址范围的256KB范围应用扇区擦除命令,则不会执行擦除操作,并将设置E_ERR状态。
七、全擦除指令 (BE 60h or C7h)
批量擦除(BE)命令会将整个闪存阵列中的所有位设置为1(所有字节均为FFh)。在设备接受BE命令之前,必须先发出并译码写入使能(WREN)命令,该命令通过在状态寄存器中设置写入使能锁存器(WEL)来启用任何写入操作。
在指令字节的第八位被锁存到SI后,必须将CS驱动至逻辑高电平。这将启动擦除周期,该周期涉及整个闪存阵列的预编程和擦除。如果在指令的最后一位之后CS未被驱动至高电平,BE操作将不会执行。一旦CS被驱动至逻辑高电平,擦除周期将立即启动。在擦除周期进行期间,用户可以通过读取写入进行中(WIP)位的值来确定操作何时完成。当擦除周期进行时,WIP位将显示为1;当擦除周期完成时,WIP位将显示为0。
只有当块保护(BP2、BP1、BP0)位被设置为0时,才能执行BE命令。如果BP位不为0,则BE命令不会执行且E_ERR状态不会被设置。BE命令将跳过由 DYB 或PPB保护的所有扇区,且E_ERR状态不会被设置。

IDLE → PREPARE → WR_ENABLE → SEND_CMD → WAIT_BUSY → IDLE
八、读取设备ID指令(RDID 9Fh)

IDLE → SEND_CMD → REC_DEVICE_ID → IDLE
设备ID如下表所示,S25FL256S的ID号为0x010219:

RDID指令实现代码如下:
① 检测到start上升沿时将spi_cs拉低:

② 将spi_cs延迟一拍,启动cnt_value计数,cnt_value从0到15循环:

③ 对clk进行分频,得到sclk,取反得到spi_sck作为flash的时钟输入:

④ 将spi_cs延迟一拍,启动bit_cnt计数,在sclk下降沿将bit_cnt从0到7递增:

⑤ 将spi_cs延迟一拍,启动byte_cnt计数,在sclk下降沿bit_cnt从0到3递增(8位指令+24位ID):

⑥ 检测到start上升沿时发送RDID命令,在sclk下降沿对写数据进行移位:

在spi_cs_n_d拉低期间,在sclk上升沿将写数据的最高位作为MOSI输出:

⑦在sclk上升沿移位寄存miso:

⑧在指令发送完成后一次8bit数据传输完成时将shift_reg寄存到rd_data:

使用虚拟IO模拟start触发传输:

使用ILA捕获数据如下,观察到主机在sclk上升沿将RDID命令(9Fh)移位输出,flash在spi_sclk上升沿对mosi进行采样:

RDID完整传输过程如下,可以看到主机获得了来自flash的三个读数据01、02、19,RDID指令成功执行。

完整代码:
flash_driver.v:
cpp
`timescale 1ns / 1ps
module SPI_FLASH (
input clk,
(*MARK_DEBUG = "TRUE"*) input reset,
(*MARK_DEBUG = "TRUE"*) output spi_sck,
(*MARK_DEBUG = "TRUE"*) output spi_cs_n,
(*MARK_DEBUG = "TRUE"*) output reg spi_mosi,
(*MARK_DEBUG = "TRUE"*) input spi_miso
);
(*MARK_DEBUG = "TRUE"*)reg [2:0] bit_cnt;
(*MARK_DEBUG = "TRUE"*)reg [7:0] byte_cnt;
(*MARK_DEBUG = "TRUE"*)reg [7:0] wr_data;
(*MARK_DEBUG = "TRUE"*)reg [7:0] rd_data;
(*MARK_DEBUG = "TRUE"*)reg [7:0] shift_reg;
(*MARK_DEBUG = "TRUE"*)reg rd_vld;
(*MARK_DEBUG = "TRUE"*)wire vio_start;
reg vio_start_d0;
reg vio_start_d1;
reg vio_start_d2;
(*MARK_DEBUG = "TRUE"*)reg spi_cs;
(*MARK_DEBUG = "TRUE"*)reg spi_cs_d;
(*MARK_DEBUG = "TRUE"*)reg spi_cs_d1;
(*MARK_DEBUG = "TRUE"*)reg spi_cs_d2;
(*MARK_DEBUG = "TRUE"*)reg sclk;
(*MARK_DEBUG = "TRUE"*)reg [3:0] cnt_value;
(*MARK_DEBUG = "TRUE"*)reg rd_vld_d;
(*MARK_DEBUG = "TRUE"*)reg [7:0] byte_cnt_d;
assign spi_cs_n = spi_cs_d & spi_cs_d1 & spi_cs_d2;
always @(posedge clk) begin
if (reset) sclk <= 0;
else if (!(spi_cs_d && spi_cs_d1)) begin
if (cnt_value[0]) sclk <= 0;
else sclk <= 1;
end else sclk <= 0;
end
assign spi_sck = !sclk;
always @(posedge clk) begin
if (reset) spi_cs_d <= 1;
else spi_cs_d <= spi_cs;
end
always @(posedge clk) begin
if (reset) spi_cs_d1 <= 1;
else spi_cs_d1 <= spi_cs_d;
end
always @(posedge clk) begin
if (reset) spi_cs_d2 <= 1;
else spi_cs_d2 <= spi_cs_d1;
end
always @(posedge clk) begin
if (reset) cnt_value <= 0;
else if (!spi_cs_d) cnt_value <= cnt_value + 1;
else cnt_value <= 0;
end
always @(posedge clk) begin
vio_start_d0 <= vio_start;
vio_start_d1 <= vio_start_d0;
vio_start_d2 <= vio_start_d1;
end
always @(posedge clk) begin
if (reset) spi_cs <= 1;
else if (vio_start_d1 && !vio_start_d2) spi_cs <= 0;
else if (!spi_cs_d && bit_cnt == 7 && byte_cnt_d == 3) spi_cs <= 1;
end
always @(posedge clk) begin
if (reset) bit_cnt <= 0;
else if (!spi_cs_d && bit_cnt == 7 && cnt_value[0]) bit_cnt <= 0;
else if (!spi_cs_d && cnt_value[0]) bit_cnt <= bit_cnt + 1;
end
always @(posedge clk) begin
if (reset) byte_cnt <= 0;
else if (!spi_cs_d && bit_cnt == 7 && byte_cnt == 3 && cnt_value[0]) byte_cnt <= 0;
else if (!spi_cs_d && bit_cnt == 7 && cnt_value[0]) byte_cnt <= byte_cnt + 1;
end
always @(posedge clk) begin
if (reset) wr_data <= 0;
else if (vio_start_d1 && !vio_start_d2) wr_data <= 8'h9f;
else if (!spi_cs_d && cnt_value[0]) wr_data <= wr_data << 1; //sclk上升沿移位wr_data
end
always @(posedge clk) begin
if (reset) spi_mosi <= 0;
else if (!spi_cs_d)
spi_mosi <= wr_data[7]; //sclk上升沿输出mosi spi_clk上升沿采样mosi
end
always @(posedge clk) begin
if (reset) shift_reg <= 0;
if (!(spi_cs_d && spi_cs_d1) && ((byte_cnt > 0) || (byte_cnt_d > 0)) && !cnt_value[0])
shift_reg <= {shift_reg[6:0], spi_miso}; //sclk上升沿采样寄存miso spi_clk上升沿输出miso
end
always @(posedge clk) begin
if (reset) rd_vld <= 0;
if (!spi_cs_d && bit_cnt == 7 && byte_cnt > 0 && cnt_value[0]) rd_vld <= 1'b1;
else rd_vld <= 0;
end
always @(posedge clk) begin
if (reset) rd_data <= 0;
else if (rd_vld_d) rd_data <= shift_reg;
end
always @(posedge clk) begin
if (reset) rd_vld_d <= 0;
else rd_vld_d <= rd_vld;
end
always @(posedge clk) begin
if (reset) byte_cnt_d <= 0;
else byte_cnt_d <= byte_cnt;
end
always @(posedge clk) begin
if (reset) rd_data <= 0;
else if (rd_vld_d) rd_data <= shift_reg;
end
vio_0 vio_o (
.clk (clk), // input wire clk
.probe_out0(vio_start) // output wire [0 : 0] probe_out0
);
endmodule
top.v
cpp
`timescale 1ns / 1ps
module top (
input sys_clk_p,
input sys_clk_n,
input rst_n,
output spi_cs_n,
output spi_mosi,
input spi_miso
);
wire clk;
wire spi_sck; //50MHz
clock_and_reset inst_clock_and_reset (
.sys_clk_p (sys_clk_p),
.sys_clk_n (sys_clk_n),
.clkout_100M(clk)
);
SPI_FLASH inst_SPI_FLASH (
.clk (clk),
.reset (!rst_n),
.spi_sck (spi_sck),
.spi_cs_n(spi_cs_n),
.spi_mosi(spi_mosi),
.spi_miso(spi_miso)
);
STARTUPE2 #(
.PROG_USR("FALSE"), // Activate program event security feature. Requires encrypted bitstreams.
.SIM_CCLK_FREQ(0.0) // Set the Configuration Clock Frequency(ns) for simulation
) STARTUPE2_inst (
.CFGCLK(), // 1-bit output: Configuration main clock output
.CFGMCLK(), // 1-bit output: Configuration internal oscillator clock output
.EOS(), // 1-bit output: Active high output signal indicating the End Of Startup.
.PREQ(), // 1-bit output: PROGRAM request to fabric output
.CLK(0), // 1-bit input: User start-up clock input
.GSR(0), // 1-bit input: Global Set/Reset input (GSR cannot be used for the port name)
.GTS(0), // 1-bit input: Global 3-state input (GTS cannot be used for the port name)
.KEYCLEARB(1), // 1-bit input: Clear AES Decrypter Key input from Battery-Backed RAM (BBRAM)
.PACK(1), // 1-bit input: PROGRAM acknowledge input
.USRCCLKO(spi_sck), // 1-bit input: User CCLK input**SPI CLK**
.USRCCLKTS(0), // 1-bit input: User CCLK 3-state enable input
.USRDONEO(1), // 1-bit input: User DONE pin output control
.USRDONETS(1) // 1-bit input: User DONE 3-state enable outpu
);
约束文件(Genesys2)
cpp
# clk
set_property -dict {PACKAGE_PIN AD11 IOSTANDARD LVDS} [get_ports sys_clk_n]
set_property -dict {PACKAGE_PIN AD12 IOSTANDARD LVDS} [get_ports sys_clk_p]
set_property -dict {PACKAGE_PIN R19 IOSTANDARD LVCMOS33} [get_ports rst_n]
# flash
set_property -dict {PACKAGE_PIN U19 IOSTANDARD LVCMOS33} [get_ports spi_cs_n]
set_property -dict {PACKAGE_PIN R25 IOSTANDARD LVCMOS33} [get_ports spi_miso]
set_property -dict {PACKAGE_PIN P24 IOSTANDARD LVCMOS33} [get_ports spi_mosi]