FPGA读写SD卡实战:SPI模式初始化时序与Verilog控制器避免踩坑
简介FPGA与SD卡交互的完整设计资料包面向数字逻辑工程师、电子竞赛备赛者及存储接口学习者聚焦SD卡初始化、块读写、CRC校验和错误处理等工程实际问题系统性地给出可直接参照的工程模板。包内共71个文件以Verilog源码为核心配合ISE工程配置、综合布线报告、比特流文件与日志摘要文档可对照目录结构还原设计全流程。压缩包大小约2.09MB体量轻、层次清晰便于快速查阅。内容覆盖SPI与1-bit SD模式下的命令应答机制、数据块传输流程以及初始化序列中的版本检测、地址协商等关键步骤状态机设计、CRC计算与错误恢复策略在源码中有模块级体现配置和测试说明附带在工程报告中。当前已有617人学习下载适合在校学生和在职工程师作为SD存储接口开发的入门参考或复用模板。1. FPGA 读写 SD 卡到底难在哪先绕开三个最蠢的坑SD 卡读写是 FPGA 项目里特别常见的需求做数据采集、图像处理、高速 ADC 采样数据量一大总得找个地方落盘。你以为就是发几条命令、收几串数据真正上板才发现读回来全是 0xFF或者卡直接不响应。问题几乎不出在协议规范本身而出在上电时序、命令 CRC、数据起始令牌和忙检测这些细节。这篇笔记我把从初始化到单块读写的完整套路整理出来附带能直接用的 Verilog 框架和避坑记录适合已经写过基础状态机的 FPGA 开发者也适合刚打算把 SD 卡加进项目的新手。你不需要完全背下 SD 规范按这里的顺序走一遍能少走好几天的弯路。2. 从 SD 卡协议说起SPI 模式与 SD 模式怎么选以及初始化时序2.1 两种访问模式接口占用、速度、复杂度对比SD 卡物理层支持 SD 模式默认和 SPI 模式两种访问方式。SD 模式用 6 根线其中 DAT0~DAT3 是 4bit 并行数据线CLK 独立CMD 单独一根整个链路是半双工近距离同步传输最高速率可以做到每秒几十 MB。SPI 模式则是把 SD 卡当作一颗 SPI 从设备只有 SCLK、MOSI、MISO、CS 四根线数据一位一位串行走最高规范速率是 25MHz实际很多卡能跑更高。对 FPGA 开发者来说SD 模式的 4bit 总线协议实现并不难难的是时序收敛命令响应是可变长度数据窗口只有几个纳秒上板稍有不稳就出错。而 SPI 模式命令格式固定响应也是固定字节状态机写出来非常直白。我一般优先选 SPI 模式理由很实在IO 占用少逻辑简单调试方便。比如在常见的 FPGA 开发板上SD 卡座通常已经引出了 SPI 引脚直接把 CS、SCLK、MOSI、MISO 接到普通 IO 就能用不占用专用高速 bank也不必处理 LVDS 或 DDR 对齐。如果你的 FPGA 项目已经塞满了图像处理或高速 ADC 采样的逻辑留给 SD 卡控制器的资源本来就不多SPI 模式更稳妥。SD 模式适合那些确实需要持续写入数十 MB/s 的场景比如无压缩视频录制但那样一般会外接 SD 卡控制器芯片或使用 SoC FPGA 的硬核外设而不是用纯可编程逻辑去肉搏。为了方便你决策我把两种模式的关键差异列在下面。| 项目 | SPI 模式 | SD 模式 | | 线数 | 4SCLK/MOSI/MISO/CS | 6CLK/CMD/DAT0~DAT3 | | 最大速率 | 25MHz规范 | 100MHz高速模式 | | 命令帧 | 6 字节响应 1/5 字节 | 48bit 串行响应可变长度 | | 数据线宽度 | 1bit | 4bit | | 实现复杂度 | 低状态机直观 | 高要处理总线切换和时序对齐 | | 典型应用 | 数据记录、文件存储、小系统 | 视频码流、高速采集回放 |选型结论先确认项目的真实写入速度需求。FPGA 内部逻辑处理完数据后如果只是每秒写几百 KB 到几 MBSPI 模式完全够用如果持续吞吐要求超过 10MB/sSPI 模式的 25MHz 时钟有效吞吐约 2.5MB/s就顶不住了这时候再考虑 SD 模式。注意即便是 SD 模式卡的实际写入速度还受内部闪存擦写限制不是所有卡都能持续高速写。2.2 初始化时序上电、CMD0、CMD8、ACMD41 的命令序列不管哪种模式SD 卡上电后都要先进入 IDLE 状态。SPI 模式下主机通过发送命令序列让卡从 SD 模式切换到 SPI 模式。标准顺序是上电后 CS 置高时钟线给至少 74 个周期然后拉低 CS发送 CMD0参数 0x00000000CRC 0x95卡返回 0x01 表示进入 IDLE。接着发送 CMD8参数 0x000001AACRC 0x87用于区分 V1.0 和 V2.0 卡V2.0 卡会返回 R7 响应老卡不支持 CMD8会响应 0x05 或完全不响应。然后循环发送 ACMD41CMD55 CMD41每次 CMD55 返回 0x01CMD41 返回 0x01 表示还在初始化直到返回 0x00 则初始化完成。为什么 CMD0 必须带 CRC因为卡在上电时默认处于 SD 模式SD 模式的命令帧带 CRC7。只有在卡识别到主机要切到 SPI 模式后后续命令才能关闭 CRC。所以 CMD0 和 CMD8 的 CRC 必须算对否则卡根本不回你。好在 CRC 是固定值CMD0 是 0x95CMD8 是 0x87。如果你换了参数就得重新算。CRC7 的算法是多项式 x^7x^31初始值 0按位移位计算Verilog 实现如下。// CRC7 计算模块输入 32bit 参数输出 7bit CRC module crc7_calc( input [31:0] din, output [6:0] crc_out ); integer i; reg [6:0] crc_reg; always (din) begin crc_reg 7b0; for (i 31; i 0; i i - 1) begin crc_reg[6] crc_reg[5]; crc_reg[5] crc_reg[4]; crc_reg[4] crc_reg[3]; if (crc_reg[6] ^ din[i]) begin crc_reg[3] crc_reg[2] ^ 1b1; end else begin crc_reg[3] crc_reg[2]; end crc_reg[2] crc_reg[1]; crc_reg[1] crc_reg[0]; crc_reg[0] crc_reg[6] ^ din[i]; end end assign crc_out crc_reg; endmodule这段代码用了组合逻辑循环在 FPGA 里综合成约 30 个 LUT可接受。真正发送命令时需要把 CRC 左移一位最低位补 1组成 8bit 校验字节。例如 CMD0 的 48bit 帧是 0x40 00 00 00 00 00加上 CRC 字节 0x95总共 6 字节。发送顺序是最高位先出。很多模板代码喜欢把 CRC 计算和发送放在一起但我觉得拆分更清晰计算模块只负责算发送模块把 6 字节按位移出。初始化状态机的完整控制流可以这样描述复位后进入 POWER_ON 状态延时 10ms进入 IDLE_CLK 状态CS 拉高给 80 个时钟然后拉低 CS依次发出 CMD0、CMD8、ACMD41。每次发完命令后等待响应。注意每个命令之间要留 8 个空时钟周期卡需要时间处理。ACMD41 需要循环循环次数做上限比如 100 次超过就报错。我通常把这部分写成一个大 state machine状态包括 CMD0、CMD8、CMD55、CMD41、DONE、ERROR。上板后用状态指示 LED 或串口打印状态值能快速判断卡到哪一步不走了。2.3 用逻辑分析仪验证初始化波形一个实用 checklist写完初始化代码别急着读数据先抓一下初始化波形因为初始化过不了后面全是无用功。我常用的做法是把逻辑分析仪接到 CS、SCLK、MOSI 三根线上触发条件设为 CS 下降沿然后采样率至少是 SCLK 的 4 倍。检查清单如下。上电后 CS 为高电平至少持续几毫秒期间 SCLK 有 74 个以上空闲周期。CMD00x40 00 00 00 00 95发出后MISO 上能出现 0x01IDLE 状态。紧接着 CMD80x48 00 00 01 AA 87发出如果卡支持 V2.0返回 R7 响应0x01 00 00 01 AA老卡不响应跳过 CMD8 也可。循环发送 CMD55CMD41直到响应变为 0x00。如果一直停在 0x01多半是时钟频率太高或供电不稳。初始化完成后发一个 CMD59 关闭 CRC 校验或者直接发 CMD16 设置块长度保持默认 512。如果你发现 MISO 一直为高或全 0xFF先查 CS 是否真的拉低再查上电延时是否够长。很多开发板的 SD 卡插座是 3.3V 供电而 FPGA bank 可能是 1.8V/2.5V信号电平不匹配也会导致卡不响应。我见过一块板子SD 卡座自带电平转换但 FPGA 约束文件里 IO 标准没设成 LVCMOS33初始化直接失败把一行约束改对就好。这种问题属于硬件细节的玄学但确实最容易让人卡一整天。逻辑分析仪不只是看响应值还要看命令之间的间隔。有些卡对背靠背命令很敏感间隔不足会吞掉命令。我的经验是每条命令后至少留 16 个空闲时钟不要省。3. 用 Verilog 写 SD 卡控制器单块读写的命令与数据通路3.1 命令层实现CMD17/CMD24 与 CRC 处理初始化完成后数据传输模式的命令格式和初始化阶段一样6 字节帧起始位 0传输位 16bit 命令索引4 字节参数。读一块的命令是 CMD17参数是字节地址写一块是 CMD24参数也是字节地址。地址必须按 512 对齐比如逻辑块号 0 对应地址 0x00000000逻辑块号 1 对应 0x00000200。命令层我建议设计一个可复用的发送模块输入命令索引和参数输出 done 和响应。它在 SPI 时钟下工作内部有一个 48bit 移位寄存器。因为我们已经关闭了 CRC 校验发送帧可以直接发 0x40cmd_index参数照发最后补一个字节 0x00 就行。但如果你没有关闭 CRC就需要实时计算 CRC7。我下面给出一段发送 CMD17 的状态机代码参数 addr 由外部给出。module sd_cmd17 ( input clk, // 系统时钟 input rst_n, input start, input [31:0] addr, output reg busy, output reg mosi, input miso, output reg done ); localparam S_IDLE3d0, S_SEND3d1, S_WAIT_RESP3d2, S_DONE3d3; reg [2:0] state; reg [47:0] frame; reg [5:0] bit_cnt; reg [7:0] resp; // 命令帧组装起始0, 传输位1, CMD17 参数 CRC(0, 已关闭) wire [47:0] cmd17_frame {2b01, 6b010001, addr, 8h00}; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; busy 1b0; done 1b0; end else begin case (state) S_IDLE: begin done 1b0; if (start) begin frame cmd17_frame; bit_cnt 6d47; busy 1b1; state S_SEND; end end S_SEND: begin if (bit_cnt 6d0) begin state S_WAIT_RESP; end else begin mosi frame[47]; frame {frame[46:0], 1b0}; bit_cnt bit_cnt - 1; end end S_WAIT_RESP: begin // 等待R1响应最高位为1表示卡忙0表示接受 // 简单连续采样8个bit得到resp // 假设resp采样逻辑已实现 if (resp[7] 1b0) begin state S_DONE; busy 1b0; done 1b1; end end S_DONE: state S_IDLE; endcase end end endmodule参数说明cmd17_frame 是 48bit 的帧前两位固定 01接着 6 位命令索引CMD17 即 010001然后 32 位地址最后 8 位 CRC 或占位。如果关闭了 CRC最后字节写 0x00 也没问题如果不关闭需要替换为 CRC 字节。S_SEND 状态里每次把最高位放到 MOSI左移48 个 bit 发完就等响应。R1 响应的最高位为 0 代表卡处于传输状态可以继续。实际中 R1 可能是多字节但第一个字节最关键后面字节可以用来做状态扩展但单块读写不需要。3.2 数据通路SPI 模式下的字节收发与状态机读单块数据的过程发送 CMD17 后卡先返回 R1 响应0x00然后过一段时间MISO 上出现起始令牌 0xFE接着连续输出 512 字节用户数据最后输出 2 字节 CRC。主机需要从令牌开始同步接收每 8 个 bit 拼一个字节。下面是一个读单块状态机的核心代码使用字节计数器每收到一个字节存入 BRAM。// 读单块状态机节选 reg [9:0] byte_cnt; // 计数 0~511 reg [7:0] rx_byte; reg [7:0] rx_shift; reg [2:0] rx_bit_cnt; reg found_token; parameter S_IDLE0, S_WAIT_TOKEN1, S_READ_DATA2, S_DONE3; always (posedge spiclk) begin if (state S_WAIT_TOKEN) begin rx_shift {rx_shift[6:0], miso}; if (rx_bit_cnt 3d7) begin if (rx_shift 8hFE) begin found_token 1b1; state S_READ_DATA; byte_cnt 10d0; end rx_bit_cnt 3d0; end else begin rx_bit_cnt rx_bit_cnt 1; end end else if (state S_READ_DATA) begin rx_shift {rx_shift[6:0], miso}; if (rx_bit_cnt 3d7) begin ram[byte_cnt] rx_shift; // 实际需要 ram_we 信号 if (byte_cnt 10d511) begin state S_DONE; // 下一步跳过 2 字节 CRC end else begin byte_cnt byte_cnt 1; end rx_bit_cnt 3d0; end else begin rx_bit_cnt rx_bit_cnt 1; end end end这个状态机的关键在于采样沿。在 SPI 模式中卡在 SCLK 上升沿改变 MISO所以主机应该在下降沿采样。注意代码里使用的是 posedge spiclk但理想的双沿采样可以用 negedge实际实现时要根据你的时钟生成方式来调整。我习惯于生成一个 50% 占空比的 SPI 时钟然后主逻辑用它的下降沿做读采样上升沿做写发送。如果你把读和写逻辑统一在系统时钟下做那就得把 MISO 打两拍再判沿否则会采到亚稳态。收到起始令牌之前MISO 可能一直是高或者出现几个 0x00 的填充字节。有些卡的填充长度不固定所以必须检测到 0xFE 才认为数据开始。这里有一个容易犯的错只检测一个字节为 0xFE但万一数据头也是 0xFE 呢其实不会因为卡会先在数据前面放起始令牌 0xFE 然后紧接着数据数据内容可以任意但你已经知道令牌已经过了之后都是数据所以无需担心。真正要担心的是如果起始令牌不是 0xFE 而卡直接发数据某些卡在特定模式下可能省略令牌这种情况少见但还是建议强制检查。3.3 测试平台怎么用 ModelSim 模拟一张 SD 卡在没有硬件的情况下用 ModelSim 或 Vivado Simulator 做仿真验证非常有必要。SD 卡的行为级模型写起来并不困难。你可以在 testbench 里用一个 always 块解析 MISO/MOSI 方向上的命令维护一个 reg [7:0] memory[0:65535]响应 CMD0 返回 0x01响应 ACMD41 返回 0x00响应 CMD17 时把内存对应地址的数据按 0xFE 开头输出。这样你的控制器状态机能在仿真里跑通整个读写流程。// 简单 SD 模型节选 reg [7:0] sd_mem [0:4095]; reg [7:0] resp; always (posedge sclk) begin if (cs 1b0) begin case (rx_cmd) 8h40: resp 8h01; // CMD0 8h41: resp 8h00; // ACMD41 8h51: begin // CMD17 // 构造输出0xFE 数据 CRC end endcase end end仿真模型不需要完全符合 SD 规范只需要行为上兼容。它能帮你验证状态机里最关键的握手发完命令有没有等响应、读数据有没有错过起始令牌、跳 CRC 字节有没有漏。但仿真模型不会暴露真实卡的上电时序和忙行为所以最后必须上板验证。我通常是先在仿真里跑通最小系统再拿逻辑分析仪在真实卡上验证同样的波形。一旦上板发现行为不一致先检查时钟极性和采样沿。4. 把读写搬到真实 SD 卡上block 读写参数与时序约束4.1 时钟分频与 SD 卡时钟频率1MHz 初始化的血泪SD 卡规范要求初始化时 SPI 时钟频率必须在 100kHz~400kHz 之间。很多 FPGA 开发板的系统时钟是 50MHz如果你直接拿它驱动 SPI卡根本不响应。我一开始偷懒用 12.5MHz 初始化结果卡偶尔能初始化成功偶尔失败后来才意识到是频率超标。正确做法是初始化阶段分频到 250kHz 左右等 ACMD41 成功后再切换到 12.5MHz 或 25MHz 跑数据。分频切换可以用一个计数器实现。注意切换的瞬间不能有毛刺最好在命令与命令之间的空闲周期切换。下面是一种可综合的时钟生成方式先用系统时钟产生使能脉冲作为 SPI 时钟边沿。reg [7:0] clk_div; reg [7:0] clk_cnt; reg spiclk; wire init_done; always (posedge clk50m or negedge rst_n) begin if (!rst_n) begin clk_cnt 8d0; spiclk 1b0; end else begin clk_cnt clk_cnt 1b1; if (clk_cnt clk_div - 1) begin clk_cnt 8d0; spiclk ~spiclk; end end end always (posedge clk50m) begin if (init_done) clk_div 8d2; // 假设系统时钟 50MHz分频到 25MHz else clk_div 8d100; // 分频到 250kHz50M/(100*2)250k end这里 clk_div 是分频系数的一半因为 spiclk 在每个计数满时翻转一次。比如系统时钟 50MHz要得到 250kHz需要分频系数 200计数上限 100翻转一次所以 clk_div100。参数说明初始化阶段 clk_div 大spiclk 慢传输阶段 clk_div 小spiclk 快。切换发生在 init_done 拉高后的某个空闲期否则 SPI 时钟的中间翻转会让卡在传输中收到额外边沿时序直接崩。实际工程中数据传输阶段如果跑 25MHz 不稳定可以退回 12.5MHz也就是 clk_div 改成 4。4.2 数据读取的等待超时处理防止卡死在无限循环真实硬件的另一个坑是读数据时卡可能长时间不返回起始令牌或者写数据时卡长时间忙。如果状态机无限等待整个 FPGA 控制逻辑就会挂死。所以必须在等待令牌和等待忙释放两个位置加入超时计数器。比如读操作从发出 CMD17 后最多等待 100ms超时直接进错误状态拉高错误标志。状态机设计时给每个等待状态都配一个独立超时参数方便调。超时计数器可以用一个通用模块也可以直接在状态机里写。下面的代码演示了等待令牌的超时处理。reg [23:0] wait_cnt; parameter TIMEOUT 24d5_000_000; // 50MHz clk100ms always (posedge clk) begin if (state S_WAIT_TOKEN) begin if (miso 1b0) begin // 检测到令牌可能的起始暂不复位继续等完整令牌 end else if (wait_cnt TIMEOUT) begin state S_ERROR; error_flag 1b1; end else begin wait_cnt wait_cnt 1; end end end这个代码的细节检测到 miso 为 0 时不清零计数器因为要防止数据流中出现短暂低电平导致超时保护失效。更稳妥的写法是设置一个次状态一旦在超时前收到完整的 0xFE 就进入读数据状态否则超时报错。TIMEOUT 可以根据你实际使用的卡调整一般 100ms 足够。注意超时计数器本身要足够宽防止溢出。如果系统时钟很高TIMEOUT 的位宽要相应加大比如 200MHz 下 100ms 需要 25bit 计数器。4.3 写操作的忙检测与预擦除错过就丢数据写数据CMD24与读类似但数据方向相反。主机发送起始令牌 0xFE然后 512 字节数据最后 2 字节 CRC 或任意填充。卡收到完整数据块后会把 MISO 拉低表示忙写完内部闪存后释放为高。关键是在写操作完成后必须等待 MISO 从低变高才能发下一条命令。很多人的代码写完就发下一条结果新命令被卡忽略因为卡还在忙。忙检测的代码通常在状态机的 S_WRITE_BUSY 状态// 等待 busy 释放超时保护 if (state S_WRITE_BUSY) begin if (miso 1b1) state S_DONE; else if (wait_cnt TIMEOUT_WRITE) state S_ERROR; end注意 MISO 在写数据时主机在每个时钟都要发送 MOSI但在 busy 期间主机可以只给时钟MOSI 保持 1等待卡释放。这又是一个容易踩的坑busy 期间主机乱发命令卡不会处理busy 状态可能持续数百毫秒然后卡释放后你之前发的命令会被当下一条命令响应状态机就乱套了。所以强制遵守等待 busy 释放后再发命令。预擦除是 SD 卡的一个优化功能在写多个块之前发送 CMD35擦除开始地址和 CMD36擦除结束地址再接 CMD38 执行擦除之后写数据会快很多。但不是所有卡都支持执行之前可以读 CSD 寄存器确认。单块写不需要预擦除直接写就是 SD 卡内部自动擦写。如果你做批量连续写预擦除能让写入速度明显提升但代价是状态机更复杂。我通常会先做不预擦除的版本功能稳定后再优化。5. 避坑指南FPGA 与 SD 卡打交道最常见的 5 个翻车现场5.1 现象读回来的数据全是 0xFF原因卡未完成初始化就发读写命令或者 CS 未正确拉低另一种可能是 SPI 时钟极性和相位不对。SD 卡 SPI 模式要求时钟空闲为低数据在上升沿发送、下降沿采样。如果你的 FPGA 初始化了 SPI 控制器的 CPOL1那 MISO 上会出现采错数据的情况。解决先抓初始化波形确认 ACMD41 返回 0x00再把时钟配置改为 CPOL0, CPHA0大多数 SD 卡要求重新采样。5.2 现象CMD0 后 MISO 一直为高根本收不到响应原因上电延时不足或 CMD0 的 CRC 错误。SD 卡上电需要 1ms 以上稳定劣质卡需要 10ms。解决把上电延时加大到 10msCS 拉低前先给 80 个空闲时钟用逻辑分析仪核对 CMD0 帧是否为 0x40 00 00 00 00 95。如果 CRC 错误卡会静默。5.3 现象能读不能写写后数据还是旧值原因写命令发出后没有等 busy 释放或者数据令牌发错。CMD24 使用起始令牌 0xFE而 CMD25多块写使用的是 0xFC。如果你误把 0xFC 当作单块写令牌卡不会接受。解决确认 CMD24 后发送 0xFE写后读 busy直到 MISO 变为 1 后再返回。回读校验可以立刻发现问题。5.4 现象读到的数据只是单个 bit 位翻转或者整体错位原因采样沿与数据变化沿不对齐。在 SPI 模式读数据时卡在 SCLK 上升沿更新 MISO所以主机必须在下降沿采样如果用了上升沿采样会采到上一 bit 或未稳定的值。同时要注意字节序SPI 模式是 MSB first。解决把接收采样的触发沿改为下降沿或者使用 DDR 采样后再对齐。字节序问题可以加一个 bit 顺序反转的转换但在 SPI 模式下规范固定为 MSB first软件上不要自作聪明。5.5 现象FIFO 直接溢出数据部分丢失原因SD 卡写入速度比 FPGA 产生数据慢尤其是在 25MHz SPI 下实际有效写入速率可能只有 2~3MB/s因为命令开销和忙等待。如果数据连续涌入FIFO 瞬间被填满。解决在 FPGA 里加异步 FIFO 做缓冲FIFO 半满时暂停上游数据SD 写逻辑只从 FIFO 里读。这也是很多 FPGA 图像处理项目里标准做法把 SD 卡写入当作一个背压网络而不是无脑往里面灌。这五条是从实际项目里摘出来的尤其是第一条和第四条基本是所有上板者都会遇到的坑。每条解决了SD 卡读写就通了。6. 进阶从裸读写到 FAT 文件系统以及如何验证你写的数据6.1 为什么裸 block 读写不够用FAT16/32 的最小实现思路FPGA 裸读写 SD 卡数据是按地址直接摆放没有文件系统上位机读不到得用 WinHex 之类工具导出。如果你希望板卡上的数据能被 Windows/Mac 直接读就得在 FPGA 里实现 FAT 文件系统。最常见的做法是支持 FAT32需要实现引导区、FAT 表、根目录区、数据区的读取与更新。最小实现只需要三个动作从 DBR 读取 BPB 参数根据文件名在根目录搜索然后读数据区写文件稍麻烦要分配空闲簇并写目录项。FPGA 做文件系统不是不行但资源消耗大逻辑复杂。我实际项目中更常用方法是用软核如 MicroBlaze/RISC-V跑极简 FAT 驱动把 SPI 控制器作为外设这样代码可维护性高。如果你确定只是临时存储不要求 PC 直接读那裸 block 加自编索引完全够用。6.2 用读回校验和 CRC 验证写入正确性完成读写以后上板第一件事是做回读验证。最简单的做法往地址 0 写一个已知 512 字节模式比如递增数再读回来用 FPGA 逻辑或者串口发给上位机比对。如果数据中间错几个 bit多半是时钟沿问题如果大块错位是字节对齐问题。这个方法比用逻辑分析仪看整体效率高。如果做批量写可以每扇区末尾放一个 CRC16回读时校验每一位。下面是一段回读校验的 Verilog 示例用状态机逐字节比对。reg [9:0] cmp_addr; reg cmp_error; always (posedge clk) begin if (state S_CMP) begin if (ram_wdata[cmp_addr] ! ram_rdata[cmp_addr]) cmp_error 1b1; if (cmp_addr 10d511) state S_DONE; else cmp_addr cmp_addr 1; end end实际工程中ram_wdata 和 ram_rdata 可以是两个 BRAM或者同一个 BRAM 在写后读回。注意 addr 需要与 byte_cnt 同步。如果你在仿真中已经验证过上板后只需要对比一次即可。6.3 我自己的习惯上板前先做的三个测试第一拿一张空卡或格式化过的卡做 CMD0/CMD8/ACMD41 初始化打印状态响应。第二用 CMD17 读地址 0看是否能读出 512 字节且不为全 1。第三写一个 random 模式到地址 0x1000然后回读比对。这三关过了才去做带文件系统的复杂逻辑。我自己的血泪经验是跳过初始化波形检查直接调文件系统结果浪费了三天才发现是 SPI 时钟极性错了。所以建议你也把基础三步当成硬性测试别嫌麻烦。希望帮到你。本文还有配套的精品资源点击获取