
简介面向FPGA开发者的SD卡读写设计资源聚焦SD卡初始化、SPI/SD模式通信、数据块读写及CRC校验等关键环节适合电子竞赛备赛、数字系统设计课程实践与入门进阶学习。资源为Xilinx工程文件包共71个文件涵盖Verilog源码v、综合网表ngc/ngd、布线结果ncd、约束文件ucf/pcf、编程比特流bit以及映射/布局布线报告map/par等压缩包约2.09MB便于快速查看工程完整流程。已有617人学习下载。通过该工程可对照学习FPGA与SD卡交互的顶层状态机设计、CMD17/CMD24等命令时序、块传输与CRC校验逻辑并能参考其初始化序列和错误处理思路同时项目自带的仿真与综合报告有助于理解Xilinx ISE工具链使用适合直接复用或在此基础上完成二次开发。1. 把 SD 卡读写跑在 FPGA 上先搞清这份资源能帮你省多少事做 FPGA 开发的人迟早会遇到 SD 卡——不管是存配置参数、存采集数据还是做简易文件系统SD 卡都是性价比最高的存储方案。但 SD 卡写操作在 FPGA 里一直是个“玄学重灾区”SDIO 时序手册上百页命令响应有时候是0xFF有时候是0x00明明仿真全对板上一跑就翻车。这份sd_test.rar资源的核心就是给 FPGA 做 SD 卡读写特别是写方向直接给了一套可仿真的工程和测试代码。它适合两类人一是做 FPGA 入门到项目实战过渡、卡在 SD 卡读写上的人二是已经在用 SPI 或 SDIO 读 SD 卡、想补上写操作和扇区管理这块短板的工程师。资源本身包含的是 Verilog 源码、测试平台和仿真脚本覆盖初始化、单块读、单块写、多块写这几条主链路拿到手可以直接套到自己的工程里改。下面我按自己拆这类工程的顺序从协议选型讲到仿真、上板、排错把这套资源的用法和边界一次说清。2. SD 卡读写在 FPGA 里的实现路径协议选型与工程结构先立住2.1 SD 卡两种工作模式SDIO 和 SPIFPGA 该选哪条路SD 卡在物理层上有两种访问方式SDIO 模式和 SPI 模式。SDIO 模式又分 1-bit 和 4-bit4-bit 数据线需要 4 根数据引脚加时钟加命令一共 6 根线SPI 模式只需要 4 根线但吞吐上限低经典 SPI 跑 25 MHz 时理论写速度约 3 MB/s遇到高容量卡还会被卡内固件瓶颈压在更低的实际值。对于 FPGA 来说选型不是越宽越好。大多数中低端 FPGA 芯片比如黑金、正点原子的 Artix-7 或 Cyclone IV 板卡逻辑资源和引脚都有限SD 卡在项目里往往只是配角不是主数据通路。我一般建议如果实际吞吐需求低于 2 MB/s优先做 SPI 模式代码量小、时序简单、调试手段多逻辑分析仪看两根线就行如果需要在几秒内写入几十 MB 数据那就老老实实上 SDIO 1-bit数据线只有一根但协议复杂度会上升一个台阶。这套资源里默认是 SDIO 1-bit 方向但 SPI 模式的初始化序列也保留着方便对照。判断资源是否适合你先看它初始化序列怎么写的。SDIO 模式的初始化步骤是固定套路上电延时、CMD0 进入 idle、CMD8 检查电压、ACMD41 协商容量、CMD2/CMD3 拿 CID 和 RCA——这套顺序错了任何一步卡都会不理你。而 SPI 初始化和 SDIO 有细节差异CMD0 需要带CS0ACMD41 的响应是 R1 而不是 R3。这份资源里初始化状态机把 SDIO 和 SPI 的分支都写了出来改模式只需要改顶层例化的参数。2.2 扇区读写的基本盘为什么单块写和多块写必须分开封装SD 卡的最小寻址单位是扇区标准 SDSC 是 512 字节一个扇区SDHC/SDXC 则按块寻址。FPGA 读写 SD 卡本质上就是向卡发送CMD24单块写或CMD25多块写然后把数据按 512 字节一个扇区喂给卡。为什么单块写和多块写必须分开封装因为卡对这两种命令的响应和数据流格式不同。单块写主机发 CMD24 地址卡回 R1 响应然后主机发数据起始令牌0xFE加 512 字节数据加 2 字节 CRC卡接收后回写状态令牌最后卡进入 busy 状态用DOUT拉低表示正在写 Flash写完才释放。多块写是 CMD25数据是一次性连续发多个块每块前面都要加0xFC令牌不是0xFE最后发0xFD结束。这套资源里单块写和多块写是两个独立模块驱动层有一个sd_wr_single.v和一个sd_wr_multi.v顶层用状态机选路。这样做的好处是调试时可以先让单块写跑通验证卡和 FPGA 之间的握手没问题再切到多块写测吞吐出了问题也能快速定位是发令牌发错了还是 busy 判断超时了。2.3 工程目录拆解拿到压缩包后先认这些文件按这类工程最常见的组织方式解压后应该看到下面这些部分。资源不同具体命名会有差别但功能对应关系基本一致目录或文件作用使用时机rtl/全部 Verilog 源码含顶层、初始化状态机、读/写驱动、FIFO 接口综合时使用sim/仿真测试平台、sd_card_model.v卡仿真模型、测试用例脚本跑仿真时使用constraints/引脚约束和时钟约束xdc或qsf文件上板综合前必须核对doc/时序说明、命令列表、常见问题记录排查问题时查阅我拿到任何 FPGA 资源的第一步一定是先开sim/目录。如果这个工程连仿真模型都没有那上板调试的代价会非常大——SD 卡写入的时序用逻辑分析仪抓很方便但每次写卡都真实擦写 Flash几百个周期的调试循环会明显拖慢进度。好的做法是先在仿真里把初始化、单块写、多块写全部跑通再上板验证一次真实卡确认跟仿真结论一致。这套资源的好处就是把卡模型和 testbench 都给了省掉自己写卡模型的功夫。2.4 验证手段仿真模型和真实卡要各承担一半信任SD 卡仿真模型通常只会模拟命令响应和基本数据流不会真的模拟 Flash 擦写延迟和磨损均衡。所以仿真过了只能证明你的逻辑状态机没有协议错误不能证明卡在真实环境下仍然稳定。我测试时的顺序是先用仿真模型把 CMD0→CMD8→ACMD41→CMD2→CMD3→CMD7 这套初始化全跑一遍看卡模型的响应是否在自己的状态机预期分支里再跑单块写重点看数据令牌、写入 busy 信号、下一块写入的间隔全绿之后上板用 ILA 抓真实卡的响应。真实卡最容易出问题的环节往往是 ACMD41 的 HCS 位——如果初始化时没有声明自己支持高容量卡SDHC 卡会拒绝进入就绪状态。这个坑在仿真模型里经常被忽略因为很多仿真模型不校验这个位所以你会遇到“仿真全过、上板卡死”的经典翻车场景。3. 上手顺序从解压到跑通仿真一步步把工程转起来3.1 先用脚本和 testbench 跑仿真别急着上板拿到压缩包先别打开 Vivado 或 Quartus 去综合先跑仿真。因为综合一次十几分钟上板一次来回折腾更久而仿真在几秒内就能告诉你协议栈有没有低级错误。常见做法是打开sim/下的仿真脚本可能是.do文件给 ModelSim/QuestaSim 用也可能是.tcl给 Vivado Simulator 用。如果是 ModelSim 跑类似这样vlib work vlog -f filelist.f vsim -c work.sd_test_tb run -all跑完观察波形重点看cmd_sent和cmd_rsp两个信号。初始化阶段每个命令发出后cmd_rsp上要在预期窗口内出现有效响应——SDIO 模式下响应是 48-bit 的 R1/R3SPI 模式下是 8-bit 的 R1。如果响应一直是全1或者超时先查仿真模型的参数配置和时钟频率是否匹配。参数这里多说一句SD 卡初始化阶段时钟频率通常要低于 400 kHz这是 SD 协议规定的最大初始化时钟等卡进入 transfer state 之后才能把时钟升到 25 MHz 或更高。仿真脚本里一般会有一个clk_div_init参数控制这个分频比。如果你仿真时发现初始化老是超时先看看这个参数是不是设得过大把时钟压得太低导致命令间隔超了卡的容忍范围。3.2 修改时钟分频和 FIFO 深度两个必调的参数SD 卡读写的顶层模块一般会暴露几个可调参数这份资源里大概率有两类你必须要改CLK_DIV_INIT和CLK_DIV_DATA以及FIFO_DEPTH。// 从顶层模块中摘出的关键参数 parameter CLK_DIV_INIT 125; // 系统时钟 50MHz初始化时钟 400kHz 以内 parameter CLK_DIV_DATA 4; // 数据阶段时钟 50MHz / 4 12.5MHz保守安全 parameter FIFO_DEPTH 4096; // 写数据 FIFO 深度按 512B 扇区数对齐逻辑说明SD 卡初始化要求时钟不超过 400 kHz如果你的系统时钟是 50 MHzCLK_DIV_INIT至少要设成50MHz / 0.4MHz 125。数据阶段可以放宽但别一上来就拉到 50 MHz 直连——SD 卡数据线和 FPGA 之间没有经过电平转换或阻抗匹配时跑太高容易采到毛刺。FIFO_DEPTH建议按扇区数的整数倍来设比如 8 个扇区就是 4096 字节这样多块写时可以从 FIFO 连续搬数据不用每写一个扇区都等上游供给。这里有一个常见误用很多人图省事把CLK_DIV_DATA设成 1也就是全速跑。系统时钟直连 SD 卡不是不行但前提是板级走线短、卡座质量好、电平标准一致。否则你会看到偶发性的0xFF响应代码逻辑却完全查不出毛病。我一般会先在 12.5 MHz 上把数据链路跑稳再用 ILA 实测时序裕量有余量再升频。资源里默认参数大多偏保守这是好事。3.3 顶层例化要点单块写和多块写怎么接到自己的数据通路这份资源的顶层会暴露一个统一的写接口一般是这样的形式// 顶层写接口示例 module sd_writer_top #( parameter CLK_DIV_INIT 125, parameter CLK_DIV_DATA 4 )( input wire clk_sys, input wire rst_n, output wire sd_clk, output wire sd_cmd, inout wire [3:0] sd_dat, // 用户侧写接口 input wire wr_en, input wire [31:0] wr_sector, input wire [511:0] wr_data, output wire wr_busy, output wire wr_done );接到自己的数据采集逻辑时控制流是先把 512 字节拼到一个wr_data总线或 FIFO 里置wr_en一个周期然后等wr_busy拉低表示这一扇区写完。特别注意wr_sector是 LBA 地址不是字节地址。很多人直接把字节地址传进来结果卡写到了错误的扇区读回来数据错位还以为是卡坏了。4. 核心时序拆解初始化、单块写、多块写的状态机设计4.1 初始化序列状态机CMD0 到 CMD7 的每一步都不能跳SD 卡初始化是整套读写流程里最容易返工的地方。这个状态机一般按下面的顺序排列每个状态发出命令后都需要等待响应和超时判断// 初始化状态机骨架 localparam IDLE 4d0; localparam CMD0 4d1; // 进入 idle 状态 localparam CMD8 4d2; // 电压检查支持 2.7-3.6V localparam ACMD41 4d3; // 卡容量协商发送前先发 CMD55 localparam CMD2 4d4; // 获取 CID localparam CMD3 4d5; // 获取 RCA localparam CMD7 4d6; // 选择卡进入 transfer state localparam DONE 4d7;逻辑说明ACMD41的发送方式比较特殊——必须先发CMD55把卡切换到 APP 命令模式再发ACMD41否则卡直接把命令丢弃。CMD2和CMD3在单卡场景下也不能省略因为卡必须先经过CMD3拿到 RCACMD7才能带着这个 RCA 选中卡。资源里的 state machine 在这几个状态都实现了超时计数比如等待响应超过 200 个时钟周期就跳回 IDLE 重新来。这个超时值是用参数暴露的上板调试时如果卡一直初始化失败可以先用 ILA 抓初始化结束时卡在哪个状态。实际测试中有一个值得注意的现象有的卡初始化对供电斜坡时序敏感上电后需要等待至少 1 ms 再发 CMD0这个延时通常在顶层用计数器实现。如果你用了高速 ADC 或图像采集的 FPGA 板卡电源轨上同时有大电流支路SD 卡供电纹波可能会比较大此时更稳妥的做法是把 CMD0 发送延时长到 5 ms 以上。资源里默认用的是 1 ms如果卡偶尔初始化失败优先改这个值而不是改协议状态机。4.2 单块写时序CMD24 的完整波形和数据令牌细节单块写是验证 SD 卡写通路的最小闭环。主机需要向卡发送 CMD24地址为 32-bit LBA 地址卡回复 R1 响应随后主机把数据块发出去数据块结构是1 字节起始令牌0xFE 512 字节数据 2 字节 CRC。卡在接收到完整数据后会回写一个数据状态令牌然后拉低DAT0表示忙等内部 Flash 写完后释放。// 单块写数据发送逻辑片段 always (posedge clk_sys) begin if (state WR_DATA) begin case (byte_cnt) 8d0: sd_dat 8hFE; // 起始令牌 8d1: sd_dat wr_data[511:504]; // ... 按 8-bit 依次发送 512 字节 8d511: sd_dat crc_hi; // CRC 高字节 8d512: sd_dat crc_lo; // CRC 低字节 endcase end end参数说明byte_cnt从 0 到 512 一共 513 个周期其中索引 0 是令牌1~512 是数据加 CRC。CRC 在 SD 卡协议里用的是 CRC7 多项式x^7 x^3 1发送时先发高位。很多工程图省事会把 CRC 固定成0xFF 0xFF虽然能绕过大多数卡的校验但严格来说是不合规的碰到校验严格的卡会写失败。资源里单块写模块带了 CRC 生成逻辑多块写模块的 CRC 也是同一个子模块复用的这是加分项。写完成后的 busy 判断也是重点卡把DAT0拉低表示忙拉高表示空闲。判断 busy 的窗口要足够宽SD 卡写一个扇区的典型时间是 10~30 ms极端情况可能到 100 ms。如果你的 FPGA 逻辑里 busy 判断超时设得太短比如 5 ms高速卡没问题但低速卡或接近寿命末期的卡会频繁超时。我自己的习惯是给这个超时留到 200 ms代价只是多了一组计数器收益是彻底告别偶发写失败误报。4.3 多块写时序CMD25 和结束令牌的配合多块写是 CMD25数据块之间是连续发送的每块都要带0xFC起始令牌注意不是单块的0xFE。全部数据发送完后主机发送0xFD结束令牌卡才认为事务结束。如果中途因为 FIFO 空导致数据断流超过卡的容忍时间卡会报写错误并中止事务。资源和大多数工程一样多块写的核心思路是FIFO 预存 N 个扇区逻辑按 FIFO 非空信号驱动数据发送全部发完后补结束令牌。逻辑上要注意的是发送完最后一个数据块的数据字节后紧接着要发0xFD中间不能插循环等待——如果你在最后一块的 CRC 之后插了一个判断状态的分支等状态机切过来再发0xFD时间差很容易超过卡的容错窗口。// 多块写结束令牌发送 always (posedge clk_sys) begin if (state WR_END) begin sd_dat 8hFD; // 结束令牌 state WR_IDLE; end end逻辑说明WR_END状态在最后一个数据块的 CRC 发送完成后一拍进入。理解这段代码的关键在于流转条件不是等 FIFO 空了才切WR_END而是自己在内部维护一个扇区计数器发出第 N 个块之后直接跳转。这样才能保证结束令牌是紧跟在数据后面的没有多余的空洞周期。这也是多块写模块比单块写模块难调的地方——单块写每块之间有卡 busy 阶段帮你兜底多块写要求主机侧连续供给任何断流都会导致卡中止写入。资源里多块写模块还暴露了一个MAX_BLOCKS参数用于限制单次事务的块数。这个参数不是 SD 协议要求的而是工程侧的保护机制防止上层逻辑失控时无限发送数据把卡写坏。有些卡对单次多块写长度有限制超过之后也会中止事务所以给这个参数留个可配置口子是对的。4.4 CRC 校验模块CRC7 在 FPGA 里的实现与适配SD 卡命令和数据都带 CRC。命令是 48-bit其中 40-bit 是命令内容后 8-bit 是 CRC7 结束位。数据块是每个扇区 2 字节 CRC16。资源里 CRC 模块一般分两个crc7_gen.v和crc16_gen.v。CRC7 的实现用 LFSR 可以做到每比特周期计算一次。SD 协议规定 CRC7 从命令的第一个比特开始算直到命令的 40-bit 内容结束把生成的多项式结果放在后 8 位的高 7 位。写 Verilog 时常见错误是忘了先清 LFSR 再开始计算导致响应校验一直失败。// CRC7 生成器核心逻辑 reg [6:0] crc; always (posedge clk_sys) begin if (crc_en) begin crc_next {crc[5:0], 1b0} ^ (crc[6] ? 7h09 : 7h00); crc_next[0] crc_next[0] ^ data_in; crc crc_next; end end参数说明7h09是 CRC7 多项式的低位表示。crc_next[0]异或输入数据相当于把数据比特注入 LFSR 最低位。这段代码是组合逻辑生成下一状态时序逻辑拍一拍的写法适合在系统时钟下运行。要注意的是crc_en必须在命令数据的 40 个比特期间保持有效第 41 个周期开始就拉低否则会把后 8 位 CRC 字段也卷进计算里发出去的命令被卡判为 CRC 错误直接丢弃。5. 常见问题与避坑记录从仿真全绿到上板必翻的四个坑5.1 仿真通过但上板初始化失败ACMD41 的 HCS 位没置位现象仿真一切正常上板后逻辑分析仪抓到主机一直在重发 ACMD41卡就是不回0x00。原因仿真模型大多不校验 HCS 位但真实 SDHC 卡会检查。ACMD41 的 bit 30 是 HCSHost Capacity Support置 1 表示主机支持高容量卡卡才会进入 ready 状态如果这个位是 0卡认为自己面对的是一个只认识 SDSC 的老主机SDHC 卡拒绝初始化。解决检查初始化状态下发 ACMD41 时的参数值bit 30 必须为 1。通常写法是acmd41_arg 32h40000000。另外 ACMD41 的响应里 bit 31 是卡侧 CCS 位用于向主机报告卡容量类型可以拿这个位判断初始化是否真正完成。5.2 写操作偶发失败重发一次就成功命令超时参数太激进现象多块写大文件时偶尔出现某个扇区写不进去但重试一次就成功。日志层面可能表现为写速度抖动或校验错误。原因SD 卡内部 Flash 编程的过程中如果遇到坏块管理、磨损均衡或温控降速单块写 busy 时间会显著变长。你的超时窗口设得太短卡还在忙你就开始下一个命令自然失败。解决把 busy 超时从 50 ms 放宽到 200 ms 以上。代价只是一个计数器位宽从 22 位变 24 位FPGA 里几乎不占资源。另外多块写场景下如果检测到 busy 超时不要立即重发整段数据——先等待卡释放 busy再确认是当前块失败还是后续块全部失败逐块重试比整段重发的成功率高得多。5.3 ILA 抓到的命令波形是对的但响应全是 0xFF引脚约束和电平问题现象逻辑分析仪看到主机发出的命令波形和仿真完全一致但DAT0上采到的响应数据全为 1。用万用表量 SD 卡座的引脚电压发现 DAT 线电压只有 1.8V 而不是 3.3V。原因SD 卡座和 FPGA 之间有分压电阻、转换芯片或者板级寄生效应导致信号幅度不足或上升沿变缓。FPGA 内部引脚配置成了默认的输入模式没有使能上拉或施密特触发器。SD 卡规范要求主机侧 DAT/CMD 线上要有上拉电阻很多开发板上靠 FPGA 的内部上拉凑合但内部上拉阻值往往偏大几十 kΩ在 DAT 线被卡驱动时没问题在卡释放总线、主机要采样的瞬间就可能因为阻抗太高采样到毛刺。解决把CMD、DAT0、DAT1-3的引脚约束改成PULLUP使能。如果板上有物理上拉电阻确认阻值在 10kΩ 以内如果只是 FPGA 内部上拉数据速率超过 25 MHz 时大概率出问题建议降速或者改板补焊上拉电阻。5.4 多块写时 FIFO 偶尔下溢读写两侧时钟域没对齐现象多块写前几个扇区正常写到第 5 个左右开始出现数据断流然后卡返回写错误。单块写完全正常。原因数据源侧的时钟域和 SD 卡发送侧时钟域不同步。比如你的采集逻辑时钟是 100 MHzSD 卡发送时钟是 12.5 MHz中间 FIFO 的读侧使能如果只在fifo_empty为假时置位但 SD 侧状态机切换状态需要额外一个周期极端情况下会出现 FIFO 明明还有数据但读使能已经同步到 SD 侧时晚了一拍的情况。解决FIFO 设计上给读侧加一个预取机制——当 FIFO 内数据量大于FIFO_DEPTH/2时才开始发送发送过程中只要数据量不低于一个阈值就持续读。此外多块写的wr_en信号必须来自 SD 时钟域不能直接从源时钟域寄存器接过来否则跨时钟域采样会产生亚稳态导致写进卡的数据字节错位。资源里多块写模块如果直接用了源时钟域信号我通常会在这里加一个同步器或异步 FIFO 适配层这是需要自己补的常见缺口。6. 进阶用法把写卡速度从“能写”变成“按需调速”SD 卡读写跑通只是开始实际项目中真正花时间的是性能调优和稳定性验证。这里给三个进阶方向也都是我调试这类工程时必走的路。第一个是用 ILA 实测 SD 时钟沿和数据线的相对位置确认采样窗口。具体做法是把sd_clk、sd_cmd、sd_dat[0]三个信号同时接到 ILA 上在写某个固定扇区时抓一段波形。重点看数据线上数据变化沿和时钟上升沿的距离——如果数据在时钟上升沿附近翻转说明时序裕量很小要么降速要么调整输出延迟。# Vivado ILA 调试核例化示意在线调试时使用 set_property -dict {list \ [get_nets {sd_clk}] [get_nets {sd_cmd}] [get_nets {sd_dat[0]}] \ } [get_ips ila_0]逻辑说明把这三根线引进 ILA 核触发条件设为sd_cmd 0命令发起的前沿采样深度设成 65536。触发后看 200 个采样点就能看到完整的命令帧和响应帧。如果数据线的翻转时刻落在时钟上升沿 3 ns 以内这个项目在温度变化或电压波动时大概率会偶发失败。第二个是对写入数据做校验写一扇区数据后立即读回比对内容。这个功能资源里如果没直接给我会在 testbench 里加一个回环测试用例将固定图案AA 55 00 FF写入扇区 0~15再通过读模块读回比对不一致就报错。这段测试逻辑既可以放在仿真里验证两个独立模块的一致性也可以搬到上板后的在线测试中——上板跑一遍回环测试基本能确认整个写链路从数据生成到卡内部 Flash 全程无错。卡内部写失败的概率很低大部分校验不过的情况都出在主机侧逻辑回环测试能帮你快速区分是卡的问题还是自己代码的问题。// 回环校验测试片段 for (int i 0; i 16; i) begin wr_sector i; wr_data {16{32hAA55_00FF}}; wait (wr_done); rd_sector i; wait (rd_done); if (rd_data ! {16{32hAA55_00FF}}) begin $display(ERROR: sector %0d mismatch, i); $finish; end end参数说明wr_sector和rd_sector是 LBA 地址wr_data是 512-bit 总线{16{32hAA55_00FF}}表示 16 个 32-bit 重复拼接成 512 位。wr_done在单块写完成信号拉高一个周期rd_done同理。整个回环测试跑完如果无报错说明读写两个方向的时序和逻辑都是对的。第三个方向才是我最想强调的调速。资源和大多数工程会提供速度档位参数可以从初始化完成后的CLK_DIV_DATA4慢慢调到1每档跑一次回环测试测出你这块板子的稳定上限。这个上限不是固定的——同一块卡在不同温度、不同使用时长下会有差异。所以我会在数据链路上保留一个速度寄存器平时用保守速度跑关键数据传输时再动态切换到高速档。从那次被 SD 卡折腾了两天、最后发现只是 CMD 线上拉了 50kΩ 上拉电阻没焊之后我每次做 FPGA 外设调试都强制自己先跑一遍仿真模型加回环测试再碰真实卡拿到任何 SD 卡相关资源包也一定先看有没有仿真模型、有没有 CRC 模块、有没有多块写结束令牌的处理这三个缺一个都会是后面调试的坑。这套思路和这份资源配合使用能让你少走不少弯路希望帮到你。本文还有配套的精品资源点击获取