第一次拿到W25Q16BV这颗FLASH芯片时,我对着数据手册研究了整整一个周末。这颗只有8个引脚的小芯片,内部却藏着2MB的存储空间(16Mbit),相当于可以存储一本《小王子》的完整文本。更妙的是,它通过SPI接口就能完成所有操作,不需要复杂的并行总线。
W25Q16BV内部采用分块管理结构,32个块(Block)组成整个存储空间,每个块64KB。块又细分为16个扇区(Sector),每个扇区4KB。这种层级设计让擦除操作非常灵活——你可以选择擦除整个芯片、单个块或者单个扇区。实际项目中我经常遇到这种情况:只需要修改配置参数时,擦除4KB扇区比整片擦除快得多,还能避免不必要的数据丢失。
芯片的引脚虽然少,但每个都关键:
提示:新手常犯的错误是忽略WP引脚,导致无法写入。记得上拉这个引脚,或者直接接到VCC。
SPI协议就像两个人在打哑谜,需要严格遵守约定的手势规则。在FPGA作为主机的场景下,我们需要特别关注三个关键点:
模式选择是最容易搞混的部分。W25Q16BV支持模式0和模式3,我习惯用模式0(CPOL=0,CPHA=0)。这意味着:
用示波器抓取的波形显示,主机(FPGA)在时钟上升沿改变MOSI数据,从机(FLASH)在下降沿输出MISO数据。这种错开半拍的节奏,保证了数据稳定性。我曾用不同模式测试过,发现模式0的时序余量最大,特别适合初学者。
时钟速率也需要权衡。虽然芯片标称支持80MHz,但实际使用中我建议从10MHz开始。过高的时钟会导致信号完整性问题——有次我为了追求速度用50MHz时钟,结果在长排线上出现了数据错位,后来加了33Ω终端电阻才解决。
完整的SPI传输单元包含:
这是所有修改操作的前置步骤,相当于给芯片"解锁"。有次我调试时发现写入总是失败,后来才发现漏了这条指令。实现时要注意:
verilog复制// 示例代码片段
always @(posedge clk) begin
if(state == WRITE_ENABLE) begin
spi_mosi <= cmd_buffer[7]; // 移位输出06h
cmd_buffer <= {cmd_buffer[6:0], 1'b0}; // 右移
end
end
完成后需要检查状态寄存器的WEL位,确保写使能真正生效。我习惯在发送指令后延时1us再检查,避免竞争条件。
这是最暴力的操作,相当于格式化整个磁盘。实测需要3-5秒完成,期间BUSY位会保持为1。建议在以下场景使用:
注意:擦除期间如果断电,可能导致数据损坏。重要项目建议加备用电池。
FLASH的写入有个特点:只能把1改成0,不能反过来。所以写入前必须先擦除(把整个区域变成全1)。每个页256字节,超过会回卷到页首。我吃过这个亏——连续写入300字节时,前44字节被后续数据覆盖了。
可靠的写入流程应该是:
最常用的指令没有之一。可以任意地址开始读取,芯片会自动递增地址。有个优化技巧:连续读时保持CS为低,可以省去重复发送指令的时间。我在读取1MB数据时,这样操作比单次读取快20倍。
我的最爱指令,4KB的擦除粒度非常实用。地址只需要对齐到扇区起始位置(低12位忽略)。典型操作:
verilog复制task sector_erase;
input [23:0] addr;
begin
write_enable();
send_cmd(8'h20);
send_addr(addr & 24'hFFFF_F000); // 地址对齐
wait_busy();
end
endtask
这是系统的健康检查工具。除了检查BUSY位,还要关注:
我设计的状态机包含7个状态,用独热码(one-hot)编码实现:
verilog复制parameter IDLE = 4'b0001;
parameter WR_EN = 4'b0010;
parameter SECT_ERA = 4'b0100;
parameter PAGE_PROG= 4'b1000;
// 其他状态省略...
状态转移逻辑要处理好几种超时情况:
FPGA的100MHz系统时钟需要分频生成SPI时钟。我的做法是:
verilog复制always @(posedge clk_100m) begin
clk_div <= clk_div + 1;
spi_clk <= clk_div[1]; // 25MHz时钟
end
实测发现,在CLK下降沿采样MISO数据最稳定。为此我专门做了时钟对齐:
verilog复制assign sample_edge = ~spi_clk & clk_div[0]; // 下降沿中间时刻
采用双缓冲设计提高吞吐量:
除了硬件WP引脚,软件上我实现了三重保护:
通过CRC16校验数据完整性。对于256字节数据,校验计算仅增加0.5ms开销,但能避免很多奇怪问题。我的校验模块长这样:
verilog复制crc16 crc_inst (
.clk(spi_clk),
.rst(crc_rst),
.data_in(recv_data),
.crc_out(crc_result)
);
突然断电可能导致正在编程的页损坏。我的解决方案:
连续写入多页数据时,可以保持写使能状态:
在FPGA内实现256字节的写缓存:
双SPI(Dual SPI)模式下,DI/DO引脚可以同时传输数据,速度翻倍。需要修改指令:
verilog复制localparam FAST_READ = 8'h0B; // 快速读指令
第一次调试时,我用逻辑分析仪抓取了完整时序(建议使用Saleae或PulseView)。常见的坑有:
CS信号抖动:导致误判指令开始
verilog复制always @(posedge clk) begin
cs_sync <= {cs_sync[1:0], spi_cs};
if(&cs_sync[2:1]) cs_clean <= 1'b0;
else if(|cs_sync[2:1]) cs_clean <= 1'b1;
end
时钟偏移问题:长走线导致时钟相位偏移
电源噪声干扰:导致写入异常
最有效的调试方法是分阶段验证:
记得在代码中加入调试接口,通过LED或UART输出关键状态。我的调试模块通常会输出: