
不少朋友第一次接触FPGA都会卡在“怎么把一个芯片的手册变成能跑的代码”这道坎上。尤其是SPI Flash这种外设命令发出去没反应读回来的数据全是FF或者前几拍还好好的、后面莫名其妙错位……我最早给项目调N25Q128也折腾了好几天。后来回头看问题根源往往不是Verilog语法没过关而是没把Datasheet里的命令格式图和时序要求翻译成状态机的结构。这篇文章就用N25Q128这颗很常见的128Mb SPI NOR Flash为例从Datasheet的关键页讲起一步一步把它变成可用的Verilog状态机驱动。整个思路不局限于这一颗芯片读完你至少能看懂命令表、自己画出状态转移图、写出能仿真也能上板的SPI读写模块并且知道常见的坑都埋在哪。适合刚学完Verilog基础、想把外设真正调起来的人也适合在项目里被Flash读写时序折磨到怀疑人生的朋友。如果你写过I2C EEPROM驱动会发现SPI Flash的状态机更直白至少不用处理ACK那套握手逻辑。1. 读Datasheet的“姿势”N25Q128到底需要哪些关键信息有人拿到Datasheet习惯从头翻到尾几百页看完记不住几个字。其实驱动一个SPI Flash你真正需要的信息集中在几块地方不用全看但该看的必须看透。1.1 容量、接口和命令集先画一张“命令地图”N25Q128是Micron的128Mb串行NOR Flash换算过来是16MB。供电3VSPI接口支持标准SPI、Dual SPI、Quad SPI。我们先只用标准SPI四根线SCK、SI、SO、/CS剩下那些WP、HOLD引脚如果板子上没用到必须上拉到高电平否则引脚浮空时芯片可能莫名其妙进入保护模式或HOLD模式这是上板后第一个隐蔽坑。芯片支持的命令不少但驱动里真正常用的也就这几个功能命令码命令序列说明Read Data0x03发命令 24位地址随后读数据标准读速度相对慢Fast Read0x0B发命令 24位地址 8个dummy时钟随后读数据适合高频时钟场景Page Program0x02发命令 24位地址 1~256字节数据页编程不能跨页Sector Erase0xD8发命令 24位地址64KB扇区擦除SubSector Erase0x20发命令 24位地址4KB子扇区擦除Bulk Erase0xC7发命令即可整片擦除慎用Write Enable0x06发命令即可写/擦之前必须发Read Status0x05发命令随后读状态字节看WIP位轮询用Read ID0x9F发命令随后读数据N25Q128前3字节是20 BB 18拿到Datasheet先把这个命令表抄下来在旁边标注每个命令“CS拉低之后需要发几个字节、地址占几个字节、有没有dummy周期、数据方向是读还是写”。这张表就是你后面写状态机的图纸。我当时是在纸上把READ(0x03)的时序行画成一串方框命令框、地址框、数据接收框然后再对照状态机代码基本就不会画错状态。1.2 别把时间花在整篇Datasheet上重点只看这几页Datasheet虽然厚但对你现阶段有用的就几块内容。我自己的习惯是打印出来直接跳着看第一块是“Command Set”章节。所有命令码、命令字节序列、响应时序都在这里。这是写状态机的直接依据。第二块是“AC Characteristics”时序参数。重点看时钟最高频率、CS下降沿到SCK上升沿的最小建立时间、命令结束后CS拉高的最小释放时间。这些参数会决定你的状态机里是否需要插入延时周期。第三块是“Status Register”定义。主要看bit0是不是WIPbit1是不是WEL。轮询擦写完成的逻辑全靠这两个位。第四块是“Memory Organization”搞清楚页大小、子扇区大小、扇区大小。N25Q128页大小是256字节子扇区4KB扇区64KB。跨页编程的问题就是从这里引出来的。“AC Characteristics”这章最容易被忽略。比如CS拉高之后到下一次CS拉低中间有个最小释放时间tSHSL。如果你上一个命令刚把CS拉高下一个时钟周期立刻又拉低芯片可能还没准备好命令就被丢了。状态机里加一个几拍的延时状态就是为了满足这类时序要求。2. 驱动架构设计从命令序列到状态机硬件芯片的时序本质就是“CS拉低、发命令、发地址、读写数据、CS拉高”这一串动作的组合。写Verilog驱动不是上来就敲代码而是先想清楚架构否则后面每加一个命令都要重写一遍状态机。2.1 顶层接口怎么定才能兼容读、写、擦除驱动模块对外的接口我建议设计成类似下面这种形式module n25q128_ctrl #( parameter CLK_DIV 5 // 系统时钟分频系数默认50MHz - 10MHz SPI时钟 )( input wire clk, input wire rst_n, // 用户请求接口 input wire start, input wire [ 2:0] op, // 操作类型读ID/读数据/页编程/擦除/读状态 input wire [23:0] addr, // 24位地址 input wire [ 7:0] din, // 写数据输入 input wire din_valid,// 写数据有效 output reg [ 7:0] dout, // 读数据输出 output reg dout_valid, // 读数据有效 output reg busy, // 忙状态高有效 output reg done, // 本次操作完成 // SPI接口 output reg sclk, output reg cs_n, output reg mosi, input wire miso );为什么接口要分这么细因为上层使用场景不一样。读ID时不需要地址页编程需要地址还要连续输入数据读数据需要地址然后连续接收数据擦除只需要地址。把start、addr、din、din_valid独立出来操作类型用op区分状态机内部再根据op走不同的分支这样一个模块就能覆盖所有常用操作。busy和done这两个信号最好同时留。busy供上层做阻塞等待done供上层做中断触发。很多新手只做一个done结果上层逻辑不知道什么时候才能发起下一次操作只能靠猜最后把自己绕晕。2.2 三段式状态机到底怎么划分状态机风格有“一段式”“二段式”“三段式”的争论。我的建议是写SPI这种时序敏感的驱动老老实实三段式。一段式把所有逻辑揉进一个always块代码短但状态转移、次态判断、输出寄存全混在一起组合逻辑毛刺也容易被带进输出。二段式比一段式清晰但输出仍是组合逻辑在SPI这种需要精确延时的场景下容易出问题。三段式则是把状态转移、次态判断、输出寄存分开第一段时序逻辑负责state寄存器更新。第二段组合逻辑根据当前状态和输入计算next状态。第三段时序逻辑根据当前状态或次态锁存输出。用三段式写驱动出问题了拉起波形就能快速判断是“状态跳错了”还是“输出没锁住”。这个在调试SPI时序时特别有用因为SPI出问题往往不是整体跑飞而是某一位的跳变沿不对你需要能把问题定位到具体状态。2.3 表驱动思路把“操作类型”变成“命令属性表”有人会说每个命令的序列不一样状态机里是不是得写很多if else确实可以写但那样每加一个操作类型就要大改状态机。更好的做法是“表驱动”。所谓表驱动不是说你必须在FPGA里放一块RAM查表而是用case语句或者参数化常量把每个操作的属性描述出来状态机只当一个“搬运工”。比如读数据操作命令码是0x03地址长度是3字节dummy周期是0数据方向是读数据长度由外部控制页编程操作则是命令码是0x02地址长度是3字节dummy周期是0数据方向是写数据长度不超过256字节把这些属性定义成参数或者case分支状态机的“发命令、发地址、发数据/收数据”主流程就完全复用。以后要加Fast Read只改“命令码是否加dummy周期”这两个属性不用动状态机主干。这也是为什么现在很多人把这类模块做成可配置、可扩展结构的原因。3. SPI时序实现模式0的发送与采样细节SPI时序是整个驱动的心脏。命令发不对、数据采不对其余都白搭。这一节重点说清楚SPI模式0下的数据变化沿和采样沿然后给出可直接用的核心代码结构。3.1 为什么我选SPI Mode 0而不是Mode 3N25Q128支持SPI Mode 0和Mode 3。两种模式协议上都能正常通信但我习惯用Mode 0。Mode 0对应CPOL0、CPHA0含义是SCK空闲时为低电平数据在SCK上升沿被采样在SCK下降沿发生变化。对主机来说发送时序是“在下降沿之后把要发送的bit放到MOSI上”接收时序是“在上升沿附近锁存MISO上的数据”。我选Mode 0还有个很实际的原因FPGA里寄存器复位后默认值通常是0SCK空闲为低正好和复位默认值一致。万一初始化代码有遗漏SCK虽然一直不给时钟但至少电平状态不会让对方芯片误判。3.2 SCK分频和bit搬运代码把SCK频率先降下来N25Q128支持的标准读时钟频率上限不低Fast Read甚至可以跑到上百MHz。但工程上不是频率越高越好尤其PCB布线一般、Flash引脚和FPGA引脚走线较长时SCK频率太高会出现采样不稳定。我的经验是先把SCK降到10MHz左右等整个链路调通再往上提。假设系统时钟50MHz想得到10MHz的SCK需要每5个系统时钟翻转一次SCK。代码结构可以这样写// SPI主时钟分频产生SCK并使能采样/更新 reg [3:0] div_cnt; reg sclk; wire spi_tick; assign spi_tick (div_cnt CLK_DIV - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 0; sclk 0; // Mode 0: 空闲SCK为低 end else if (spi_active) begin if (spi_tick) begin div_cnt 0; sclk ~sclk; end else begin div_cnt div_cnt 1; end end else begin div_cnt 0; sclk 0; end endspi_active信号由状态机控制只有在发命令/地址/数据以及接收数据期间才让SCK运行。这样做的好处是CS拉低之前和CS拉高之后SCK都能保持确定的空闲电平避免多出多余的时钟沿让Flash状态错乱。发送和接收的“位搬运”核心逻辑可以用一个bit_cnt计数器和两段时序逻辑来实现。Mode 0下在SCK下降沿来到的时刻更新MOSI在SCK上升沿来到的时刻采样MISO// Mode 0: 下降沿输出上升沿采样 always (posedge clk or negedge rst_n) begin if (!rst_n) begin mosi 1b1; end else if (spi_tick !sclk) begin // 处于SCK低电平阶段MOSI更新为当前发送位 mosi tx_data[7 - bit_cnt]; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_shift 8h0; end else if (spi_tick sclk) begin // 处于SCK高电平阶段锁存MISO rx_shift {rx_shift[6:0], miso}; end end注意rx_shift这里用的是移位寄存器方式每采样一个bit就把MISO装进去采样8次后rx_shift就是完整一字节。代码里用spi_tick !sclk和spi_tick sclk区分“当前系统时钟周期里SCK处于低还是高”实际工程上可能要根据你的分频相位微调但思路就是这样。3.3 MISO采样时机这里最容易出问题SPI调试最大的坑不在发送在接收。发送是你主动控制MOSI电平逻辑清楚了基本不会错接收是要在一个正确的时间点把MISO采进来而这个时间点如果选得不对读回来的数据就全是乱的。我已经不止一次看到有人把MISO采样点放在SCK下降沿。这是反的。Mode 0下从机在SCK下降沿更新MISO也就是说下降沿之后MISO才刚变成新的bit此刻的电平可能还在跳变、还没稳定。如果你在这个点去采样轻则采到上一bit重则采到中间电平仿真里看是好的上板就错。正确做法是等MISO稳定了再采。放在SCK上升沿附近是标准做法但严格说是上升沿之后的某个稳定窗口内。我自己的习惯是放在SCK高电平的中间时刻也就是上升沿之后过了大概半个SPI时钟周期。这样从机改完数据之后已经过了很长时间MISO早就稳定了。这个“在稳定窗口内取点”的思路其实和滑动窗口滤波的思想很像——不是在任何位置都去抓数据而是等信号进入稳定的“窗口”再行动。你抓住这个核心原则SPI接收就不会再出那种“波形看着对数据读不对”的诡异问题。多字节收发也是同样的道理。SPI没有字节边界CS拉低后就是一条bit流全靠发送方一位一位往外吐、接收方一位一位往里收。状态机里必须有两个计数器配合bit_cnt负责当前字节内的8个bitbyte_cnt负责连续收发的第几个字节。只做bit_cnt不做byte_cnt是很多新手连续读数据时读到第二个字节就错位的根源。4. 核心功能流程写使能、页编程、擦除与状态轮询命令发送的“骨架”搭好之后剩下的就是按不同操作填入具体内容。这里把读、写、擦三条主流程拆开讲顺便把最容易出错的几个细节点透。4.1 写使能为什么必须在每次写/擦之前发很多人第一次做页编程代码看着没问题命令也发了地址也对可Flash就是不写入。最后查了半天发现少发了Write Enable命令。N25Q128内部有个Write Enable LatchWEL保护机制。任何编程和擦除操作芯片要求WEL必须为1才会真正执行。WEL只能通过发0x06命令置1并且在一次写/擦操作完成之后自动清0。也就是说每一次写命令、每一次擦除命令之前都必须重新发一次0x06。不要觉得这个动作多余这是Flash防止误写的重要机制。一次完整的页编程时序是CS拉低发0x06CS拉高CS拉低发0x02发24位地址连续发送数据CS拉高轮询状态寄存器等待WIP变成0为什么要分两次CS低因为0x06和0x02之间必须先给一个CS高电平芯片才能正确处理“写使能”这个状态。如果你把0x06和0x02连在同一个CS低电平窗口里发芯片不会认。4.2 页编程注意事项256字节边界N25Q128的页大小是256字节。Page Program命令一次最多写256字节而且不允许跨页边界写入。如果你要从地址0x100开始连续写200字节没问题如果从地址0x100开始写300字节那就不行因为后半部分会跑到下一页去。这里的“页边界”指的是起始地址所在页的页内偏移加上写入长度不能超过256。判断方法很简单看addr的低8位如果addr[7:0] write_len 256就必须拆成多次页编程操作。这个拆分逻辑放在驱动里处理上层就不用关心页边界问题了。页编程的时候还有一点值得注意地址必须是写入起始地址数据是按地址连续递增的。比如你写命令0x02地址0x000100然后连续发256个字节芯片就自动把第一个字节写到0x000100第二个写到0x000101一直到0x0001FF。你不用在每个字节前重复发地址但你的驱动必须保证数据是按顺序给的。4.3 擦除后轮询WIP的实现逻辑擦除比页编程费时得多。N25Q128的4KB子扇区擦除典型时间大约0.3秒最大可能到几秒64KB扇区擦除耗时更长。你不能发完擦除命令就立刻去读数据芯片还在忙此时任何操作都是无效的。所以擦除之后必须轮询状态寄存器里的WIP位。WIP全称是Write In Progressbit0为1表示Flash内部正在执行写/擦操作为0表示空闲。轮询的做法是CS拉低发0x05命令读一个字节取bit0CS拉高如果bit0还是1隔一段时间重复上述步骤状态机里实现轮询时一定要加一个超时计数器。就算Flash再怎么慢擦除命令几秒内也肯定完成了。万一芯片异常、状态机死循环超时计数器能帮你跳出轮询把错误状态报给上层。我在驱动里会固定放一个10秒超时虽然正常流程用不到但板子出问题时就知道是Flash没响应而不是驱动卡死。5. 仿真验证用行为模型代替真芯片写完代码别急着上板先仿真。仿真环境里你可以完全掌控Flash的响应行为定位问题比示波器抓波形快得多。而且现在很多FPGA仿真工具都是免费的没必要一上来就开重型IDE。5.1 最简单的Flash行为模型怎么写仿真时需要写一个Flash行为模型模拟N25Q128对外响应。这个模型不需要把整个16MB都建模出来按片内SRAM方式开一个寄存器数组就行关键是命令解析逻辑要写对。一个最简模型的核心思路监听CS和SCK在CS拉低期间按bit接收指令解析出命令码后决定是写入还是读取。对跳过的细节如Dual/Quad不用管标准SPI协议就够用。// 极简N25Q128行为模型片段仅用于仿真 reg [7:0] mem [0:255]; // 只模拟256字节够用 reg [7:0] cmd, addr_buf; always (posedge sclk or negedge cs_n) begin if (!cs_n) begin // 在每个SCK上升沿采样SI拼接命令和地址 cmd_buf {cmd_buf[6:0], si}; end end实际写模型的时候我建议只实现Read ID、Read Data、Page Program、Write Enable、Read Status这5条命令足以覆盖驱动的主流程。模型里用一个reg变量模拟WREN锁存状态收到0x06就置1收到Page Program且锁存为1就把数据写入内存写完后把WIP拉高一会儿再拉低模拟擦写耗时。5.2 测试激励先测读ID再测Page Program写测试激励不要一口气把所有功能都测了容易一起报错看不出主因。我的顺序是第一步先测Read ID。这是最简单的命令不需要地址不需要写使能只要把0x9F发出去再读3个字节期望得到0x20、0xBB、0x18。这一关过了说明CS控制、命令发送、MISO采样这条链路整体是通的。第二步再测页编程。先发0x06再发0x02和地址再连续发一串数据然后轮询WIP最后用0x03读回来和写入数据比对。如果读到不一致优先怀疑采样时序和跨字节边界处理。第三步测擦除。先用0x02写入一批数据再用0x20擦除接着读回来确认数据变成0xFF。擦除命令一般不破坏整片只影响指定4KB范围这个现象也可以顺便验证地址解析对不对。5.3 常见仿真报错license问题从哪儿查起仿真会报一些让人头大的环境问题典型的有类似“17.1 error: failure to obtain a verilog simulation license”这种报错。这不是你的RTL代码写错了是仿真工具的license没有正确获取。遇到这种报错先别急着改代码按顺序检查检查仿真器安装时license文件路径是否写进了环境变量检查当前用户对安装目录有没有读写权限尝试从命令行直接启动仿真器看完整报错信息图形界面经常会吞掉关键细节确认你是不是调错了仿真器版本有的工程默认调用64位版本但license只认32位这些排查路径比你在工程里反复折腾HDL代码有效得多。如果你用的工具一直没有可用的license建议干脆装个开源仿真器比如Icarus Verilog配GTKWave看波形轻量又免费仿真SPI时序完全够用。6. 调试现场实录常见问题与排查技巧仿真过了不代表上板就能跑。真实板子上的SPI调试问题往往更隐蔽波形不是没有而是看起来“差不多对”。这一节整理几个我自己踩过、也帮别人排查过的高频问题以及对应的排查思路。6.1 读出来全是FF到底哪一环断了Flash空片本身读出来是0xFF但如果读的是特定地址而不是空数据读出来一长串FF就有问题了。排查顺序从物理层开始第一步用示波器或FPGA内部ILA看CS、SCK、MOSI三根线。CS有没有正常拉低低电平期间SCK有没有时钟MOSI上发的命令码是不是0x03很多“读出来全FF”其实是CS压根没拉低或者SCK根本没跑。第二步看地址线。0x03命令后必须跟3字节地址地址字节顺序是高字节在前。如果你地址发反了芯片就把命令解析成别的东西了。第三步再看MISO。确认读数据阶段SCK的边沿是否正常以及采样点是否落在MISO稳定窗口内。如果前面都对但数据还是FF大概率是Flash根本没接收到正确命令。6.2 写进去马上读出来是旧数据这个问题比全FF更折磨人因为波形看起来命令都发了Flash好像也响应了但数据没变。十有八九是写使能没到位。检查你的流程里有没有发0x06而且是在CS低窗口发完之后确实有CS高电平的释放。如果0x06和0x02是在同一个CS低窗口内连续发的Flash不会执行写操作。我见过的最难查的情况是0x06发了、CS也释放了但0x02后发完数据忘了轮询WIP紧接着就去读读到的一瞬间Flash还在内部擦写数据当然还是旧值。6.3 排查清单现象可能性排查思路读ID全FFCS/SCK/MOSI链路物理问题先抓CS再抓SCK再确认命令字节读ID返回值不对命令码错误或采样点错误核对0x9F是否准确发出MISO采样是否在稳定窗口写后读取旧数据没发写使能确认0x06已发送且CS有正确释放连续读错位字节边界没数对检查bit_cnt和byte_cnt是否同时有效擦除后数据没变擦除地址错了或没轮询WIP确认地址覆盖范围确认擦除已完成SCK频率太高导致不稳定信号质量差降低SCK频率比如先降到1MHz验证6.4 关于MISO采样、HOLD引脚的几个隐藏坑最后补三个隐藏坑。第一个是MISO没有做同步处理。如果MISO和主时钟不是严格同源进入FPGA后最好打两拍再使用否则采样时可能采到亚稳态电平。SPI时钟频率不高时这个问题不容易暴露但一旦频率提上去就出来了。第二个是HOLD引脚没处理。N25Q128部分封装的HOLD引脚如果悬空板子上的噪声可能让芯片进入HOLD模式此时SCK和SI都不生效表现出来就是“偶尔不响应”。把HOLD和WP引脚都上拉就能解决。第三个是时钟频率。如果你的SPI主频已经跑到30MHz以上而Flash到FPGA的走线又比较长波形会严重变形。这时候与其去调终端匹配不如先把SCK降到10MHz验证功能再把频率一点点往上抬。调试的时候用逻辑分析仪抓SPI也不是不行但很多桌面逻辑分析仪的采样率不够抓20MHz以上的SPI会引入新的“假波形”。有条件还是用示波器看或者直接用FPGA内部的ILA抓内部采到的信号反而能看到真实数据。我个人在实际操作中的体会是SPI Flash驱动真不是个“难”东西困难在于眼高手低——总想着一次把代码写完却不肯先花半小时把Datasheet里的命令时序图翻译成状态图。后来我养成一个习惯再做任何外设驱动第一步永远是拿着笔在纸上画命令时序的方框图画完再写Verilog。看起来是多花了时间其实省的是后面几天对着波形发呆的时间。最后再分享一个小技巧驱动里把超时计数器和错误码这两个东西留好哪怕当前功能用不到。等哪天你在现场遇到Flash异常就知道这两个信号能帮你省多少事。这个模块后续想扩展Quad SPI读也不难状态机主流程不用大改只需要修改命令属性和数据位宽——表驱动的好处就在这里。