FPGA实战:基于Verilog的SD卡SPI模式读写与Quartus工程实现

📅 发布时间:2026/9/13 7:34:58
FPGA实战:基于Verilog的SD卡SPI模式读写与Quartus工程实现
简介面向FPGA初、中级开发者的SD卡读写测试工程采用Verilog语言编写目标器件为Cyclone IV E系列的EP4CE6F17C8可在Quartus 17.1中直接打开使用。工程以扇区读写状态机为核心定义了空闲、读、写、结束四个状态并给出读写地址与数据的请求、有效、结束等握手信号同时集成按键消抖和数码管扫描显示模块便于用按键触发读写并在数码管上观察读回数据。压缩包共292个文件以.v源码、Quartus工程文件.qpf/.qsf、.sof位流文件、.sdc约束、.rpt报告和PDF说明为主兼顾功能逻辑、工程配置与使用指导整体仅7.58MB。当前已有1008人学习下载适合在开发板上快速验证SD卡单扇区读写流程。借助该工程可掌握SD卡SPI模式的初始化流程、命令时序、数据读写状态机设计以及Quartus工程的组织方式方便在此基础上扩展FAT文件系统、多扇区传输或自定义存储应用。1. 为什么 FPGA 读写 SD 卡要先跑通 SPI 模式SD 卡是 FPGA 项目里最常见的非易失存储介质但很多人第一步就卡在「初始化不通过」上。这个工程只做一件事用 Verilog 在 SPI 模式下完成 SD 卡的单块读写并把读回来的数据送到板载数码管显示。它的价值不在于功能多而在于把 CMD0/CMD8/ACMD41 这条初始化链路、CMD17/CMD24 的单块读写时序以及 Quartus 工程里从 RTL 到引脚约束的完整流程全部压缩到一个能上板验证的例子里。源码注释保留了 2017 年的 1.0 版本标记工程基于 Cyclone IV E 系列的 EP4CE6F17C8Quartus 版本 17.1。适合刚开始接触 FPGA 存储控制的人也适合想在 Cyclone IV 上快速验证 SD 卡时序的工程师。按键触发一次写、一次读初始化状态和读出数据直接映射到数码管问题出在哪一环板上就能看出来。2. 工程骨架从 Quartus 编译产物到 Verilog 状态机2.1 工程文件里哪些能删哪些必须保留打开压缩包会看到一串后缀很长的文件比如sd_card_test.cmp.ammdb、sd_card_test.map.bpm、sd_card_test.rtlv_sg.cdb。这些是 Quartus 综合、布局布线、增量编译过程中生成的中间数据库不是源码。.ammdb是 Analysis Synthesis 的网表数据库.bpm是块布局记录.cdb是逻辑单元级网表。删掉它们不影响重新编译重新打开工程后 Quartus 会自动重建。真正要保留的是两类Verilog 源码文件顶层sd_card_test.v、按键消抖ax_debounce.v、数码管译码seg_decoder.v、扫描显示seg_scan.v以及底层 SD 控制器模块和 Quartus 工程文件.qpf、.qsf。.qsf里声明了器件型号、Verilog 文件列表、引脚分配这个文件改错一个字符工程就打不开。文件类型说明是否需要保留.vRTL 源码整个实验的核心必须保留.qpf/.qsf工程定义和约束必须保留.ammdb/.bpm/.cdb编译中间产物可删除.sof/.pof配置文件需要烧录时重新生成可删除文档说明原理图、引脚表、操作步骤必须保留2.2 顶层模块 sd_card_test 的接口设计顶层模块的端口体现了整个实验的数据流按键是输入SD 卡四线是 SPI 物理接口数码管是输出。SD_nCS 是 SPI 片选名字里的n表示低有效SD_DCLK 是 SPI 时钟SD_MOSI 和 SD_MISO 分别是主机发送和接收线。module sd_card_test( input clk, input rst_n, input key1, output SD_nCS, output SD_DCLK, output SD_MOSI, input SD_MISO, output [5:0] seg_sel, output [7:0] seg_data );端口命名有几个细节。rst_n 是低有效复位按键消抖模块ax_debounce里把rst接成了~rst_n也就是外部复位是高有效seg_sel 是 6 位位选信号seg_data 是 8 位段选信号这两个直接进seg_scan做扫描显示。SD_MISO 是输入不用额外约束但 SD_nCS、SD_DCLK、SD_MOSI 在 QSF 里建议设置弱上拉避免 SD 卡座上电瞬间电平飘忽导致误判。2.3 三段式状态机从 S_IDLE 到 S_END 的流转顶层核心是四状态状态机。原工程里参数定义是S_IDLE / S_READ / S_WRITE / S_END状态编号 0 到 3。按键第一次按下进入写操作写完自动切到读操作读完后停在 S_END再按一次按键重新读。这样设计的好处是写一次、读一次刚好能对比数码管上的写入值和读出值。parameter S_IDLE 4d0; parameter S_WRITE 4d1; parameter S_READ 4d2; parameter S_END 4d3; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else begin case (state) S_IDLE: if (sd_init_done button_negedge) state S_WRITE; S_WRITE: if (sd_sec_write_end) state S_READ; S_READ: if (sd_sec_read_end) state S_END; S_END: if (button_negedge) state S_READ; default: state S_IDLE; endcase end endsd_init_done是 SD 控制器完成初始化后拉高的标志。sd_sec_write_end和sd_sec_read_end是单块读写完成信号各拉高一个时钟周期。这两个信号是底层 SD 控制器和顶层状态机之间的握手信号如果只等数据有效信号而没等 end 信号状态机会提前跳走导致最后一个字节还没落进 FIFO 就被覆盖。wr_cnt 和 rd_cnt 是读写计数代码里声明为 10 位最大计到 1023而单块扇区是 512 字节计数范围够用。3. SPI 模式初始化时序CMD0、CMD8、ACMD41 一条链走通3.1 为什么选 SPI 模式而不是 SD 模式SD 卡支持 SD 和 SPI 两种总线模式。SD 模式至少要接 CLK、CMD、DAT0-DAT3 六根线还要在控制器里实现 SD 协议的命令通道和 4 位数据通道逻辑复杂度高。SPI 模式只要 CS、CLK、MOSI、MISO 四根线普通 IO 直接驱动时钟慢到 400kHz 也能初始化非常适合验证协议、读写扇区、在 FPGA 里挂 FATFS。代价是吞吐率低SPI 模式单根数据线读一个扇区 512 字节需要 512 个字节时钟周期远不如 4 位 SD 模式。上电后 SD 卡默认处于 SD 模式。要切到 SPI需要在 CS 拉低的情况下给 SD_DCLK 至少 74 个空时钟脉冲然后发送 CMD0。74 是协议要求的卡上电稳定周期。这个细节最容易踩坑如果初始化时只发 CMD0 没有先送 74 个 dummy 时钟卡会一直不回 R1 响应或者返回 0xFF 空数据。3.2 命令格式与 CRC 的取舍SD 卡命令固定 48 位1 位起始位0、1 位方向位1、6 位命令索引、32 位参数、7 位 CRC 和 1 位停止位。发送时按字节拆成 6 个字节先发命令索引起始位方向位6 位索引拼成一个字节再发 4 字节参数最后发 CRC 字节。命令发送首字节参数CRC 字节作用CMD00x400x000000000x95进入 SPI 模式CMD80x480x000001AA0x87检查卡电压范围CMD550x770x000000000x01通知下一命令是 ACMDACMD410x690x400000000x01启动初始化流程CMD170x51扇区号 90x00读单块CMD240x58扇区号 90x00写单块CMD8 发送一个特殊 pattern 0xAA卡如果支持 2.7V-3.6V 电压范围会回 R1 0x01 加 4 字节的操作条件信息。CMD8 的 CRC 必须是有效值 0x87这是整个初始化序列里唯一一个必须算对 CRC 的命令。ACMD41 的 CRC 在 SPI 模式下不强制很多控制器直接给 0x00 或 0x01 都能过但 CMD0 的 0x95 是协议里定死的值不能省。SPI 模式也可以先发 CMD59 关闭 CRC 校验但这里建议保持默认用标准值最省心。3.3 发送命令和等待 R1 响应的 Verilog 实现发送一个命令就是按位把命令索引、参数、CRC 拼成 48 位逐位送到 SD_MOSI同时从 SD_MISO 采样。我一般把单字节收发封装成 task方便在初始化状态机里反复调用。task spi_byte; input [7:0] tx_data; output [7:0] rx_data; integer i; begin rx_data 8h00; for (i 7; i 0; i i - 1) begin SD_MOSI tx_data[i]; (negedge SD_DCLK); // 时钟下降沿主机把数据放到 MOSI (posedge SD_DCLK); rx_data {rx_data[6:0], SD_MISO}; // 上升沿采样 MISO end end endtask这段代码的关键在采样沿的选择。SD 卡 SPI 模式默认 CPOL0、CPHA0也就是 SD_DCLK 空闲为低主机在下降沿输出、在上升沿采样。如果在非 SPI 模式下用同一个字节收发逻辑MISO 上采到的数据会整体错半拍。(negedge SD_DCLK)写的是事件控制实际工程里更推荐用可综合的时钟分频计数器驱动收发状态机但做验证和仿真时这个 task 足够直观。发送完命令后要等 R1 响应。R1 是 8 位最高位 bit7 固定为 0bit0 为 1 表示卡处于 idle 状态初始化完成后 R1 变成 0x00。如果总线空闲MISO 会持续输出 0xFF所以等待 R1 的逻辑就是不断读字节直到采到的字节最高位为 0task wait_r1; output [7:0] r1; integer timeout; begin timeout 0; r1 8hFF; while (timeout 20) begin spi_byte(8hFF, r1); if (r1[7] 1b0) begin timeout 20; // 收到有效 R1 end else begin timeout timeout 1; end end end endtask等待 R1 时 MOSI 要持续发送 0xFF因为 SPI 是同步全双工主机不发时钟和发送数据卡就无法输出响应字节。超时计数设成 20 字节初始化时 400kHz 时钟下每字节 8 个时钟周期20 字节大约 40ms卡如果在这个时间内没回应基本可以判定命令没被接受。3.4 初始化流程的状态机排布初始化不能一条命令走到底。正确顺序是上电延时、送 74 个时钟、CMD0、CMD8、循环发送 CMD55ACMD41 直到 R1 返回 0x00。ACMD41 有个特点是需要反复查询卡在内部上电初始化完成前会一直返回 0x01查询次数上限我一般设 100 次超过就回到起始状态重新上电。ACMD41 参数里 bit30HCS 位置 1 表示主机支持高容量卡。如果这一位没有置位SDHC 卡只会被识别为 SDSC容量显示和实际扇区地址会错位。读写的扇区地址是扇区号 9也就是左移 9 位变成字节地址。这个转换在执行 CMD17/CMD24 时经常被漏掉写成直接传扇区号会导致读写地址超出卡容量范围卡返回非法地址错误。4. Cyclone IV 上跑通 Quartus 17.1 工程约束、仿真与排错4.1 工程创建和器件选择在 Quartus 17.1 里新建工程时Family 选 Cyclone IV EDevice 选 EP4CE6F17C8。这个器件是 Cyclone IV E 系列的入门款逻辑单元约 6272 个本工程顶层状态机加 SD 控制器加数码管扫描资源占用很低编译不会遇到容量瓶颈。工程文件里如果直接打开原始.qpf先确认 Quartus 版本一致低版本打开高版本工程会提示工程文件版本不兼容17.1 的工程文件拿到 13.1 里打不开。QSF 文件里需要显式声明文件和器件型号set_global_assignment -name FAMILY Cyclone IV E set_global_assignment -name DEVICE EP4CE6F17C8 set_global_assignment -name VERILOG_FILE sd_card_test.v set_global_assignment -name VERILOG_FILE ax_debounce.v set_global_assignment -name VERILOG_FILE seg_decoder.v set_global_assignment -name VERILOG_FILE seg_scan.v set_global_assignment -name SDC_FILE sd_card_test.sdc注意 SDC 文件。时序约束对 SD 卡这种低速 SPI 接口不是必须的但 Quartus 在无约束编译时会自动把时钟网络设为理想时钟SignalTap 里看到的波形和实际布局可能有偏差。我在这个工程里只做一条约束把输入时钟约束成 50MHz避免工具推断时钟频率导致 Fitter 把逻辑放得太散。4.2 引脚约束与分频参数调试引脚约束按原理图填QSF 里用set_location_assignment指定具体引脚号这里用占位符示意。SD 卡四根信号线中 MISO 是输入不需要额外配置MOSI、DCLK、CS 建议在 QSF 里加上弱上拉set_location_assignment PIN_xx -to clk set_location_assignment PIN_xx -to rst_n set_location_assignment PIN_xx -to key1 set_location_assignment PIN_xx -to SD_nCS set_location_assignment PIN_xx -to SD_DCLK set_location_assignment PIN_xx -to SD_MOSI set_location_assignment PIN_xx -to SD_MISO set_location_assignment PIN_xx -to seg_sel[0] set_location_assignment PIN_xx -to seg_data[0] set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to SD_MISO分频系数是 SD 卡读写能否稳定跑过的直接影响因素。SD 卡 SPI 模式初始化推荐时钟不高于 400kHz读写数据时最高可以到 25MHz。如果工程里 clk 是 50MHz初始化时钟分频系数取 128得到约 390kHz读写时钟分频系数取 2得到 25MHz。切换时钟不能直接在 SD_DCLK 上用 mux 瞬间切常见做法是让分频计数器连续计数用计数器的某一位作为输出时钟切换位宽不会产生毛刺。阶段分频系数实际频率说明上电初始化50MHz / 128390kHz满足 400kHz 上限CMD8/ACMD4150MHz / 128390kHz保持低速完成握手CMD17/CMD24 数据传输50MHz / 225MHz提高读吞吐有些卡对时钟边沿很敏感初始化时是慢时钟读写时切到快时钟卡可能在这瞬间误判一个下降沿。保守做法是切时钟前先把 CS 拉高几个时钟周期再切频重新拉低 CS 发命令。这个步骤在验证时能明显降低偶发初始化失败的概率。4.3 Modelsim 仿真失败not obtain a verilog simulation licenseQuartus 17.1 调用 Modelsim 仿真经常报failure to obtain a verilog simulation license。这个报错通常不是 Modelsim 本身有问题而是 EDA 工具路径没有配对。在 Tools - Options - EDA Tool Options 里把 ModelSim 路径指向modelsim.exe所在目录确认版本是 10.5 或更高。另一个常见原因是工程里没有为仿真创建单独的 EDA 设置Tools - Options - EDA Tool Options 里 Simulation 指定为 ModelSim-Altera 或 ModelSim否则 Quartus 会默认按 Verilog-HDL 综合器去猜仿真器路径。如果路径正确但仍然报错检查环境变量MODELSIM是否指向了一个不存在的 modelsim.ini。ModelSim 启动时会先读这个配置文件文件缺失同样会报 license 相关错误。我在遇到这类问题时的处理顺序是先确认 Modelsim 能否独立启动再确认 Quartus 能否识别路径最后检查工程里是否勾选了 RTL simulation 的编译选项。这段排错思路在 Quartus 13.1 到 17.1 之间通用。4.4 上板常见故障写保护误报、初始化超时、数码管闪烁上板测试时最让人困惑的是「SD 卡没锁但写入报写保护」。卡座上通常有写保护检测引脚如果这个引脚悬空或电平逻辑反了即使卡本身没有锁SPI 模式下执行 CMD24 也可能被卡拒绝。处理方法是先换一张卡排除卡自身问题再用万用表量卡座 CD 开关脚在上卡和下卡时的电平变化确认检测脚没有悬空。初始化超时的排查顺序是先看 SD_DCLK 有没有先给足 74 个时钟再看 CMD0 发出的字节是不是 0x40 0x00 0x00 0x00 0x00 0x95再确认 CMD8 返回的 4 字节里后两个是不是 0x01 0xAA。如果 CMD8 返回 0x05说明卡的电压范围不匹配或者命令没被识别。数码管闪烁通常是 seg_scan 模块扫描时钟太慢扫描刷新率低于 60Hz 肉眼就能看到跳动把扫描时钟分频系数减小到几百 Hz 每位的扫描频率即可。5. 进阶验证读回校验、数据缓冲与单块读写收尾技巧5.1 读回校验的两种做法基础实验只验证「写一次、读一次」但调试 SD 卡控制器时只看数码管上一位数据不够。我习惯在读写完成后加比对逻辑写入时生成一个固定序列比如按字节递增 0x00 到 0xFF 循环填满 512 字节读回来逐字节和预期值比较把不一致的字节数累加到一个错误计数器里。预期值不需要额外存储因为序列是有规律的用当前地址和计数就能算出。reg [7:0] expect_data; always (posedge clk or negedge rst_n) begin if (!rst_n) begin read_data 8h00; end else if (sd_sec_read_data_valid) begin expect_data rd_cnt[7:0]; if (sd_sec_read_data ! expect_data) begin read_data 8hEE; // 比对失败数码管显示 0xEE end else begin read_data sd_sec_read_data; end end end这里用rd_cnt[7:0]作为预期数据正好利用 512 字节的计数低 8 位循环。如果卡写入时位序处理有误这个比对会立刻抓到问题。数码管上显示 0xEE 表示比对失败显示其他值就是读出数据的低 8 位。5.2 数据缓冲与 FIFO 深度选择单块读写是 512 字节顶层代码里sd_sec_write_data是 8 位每时钟一个字节。如果后续接图像采集或 FFT 模块用户逻辑时钟和 SD_DCLK 往往不同频直接把 SD 控制器的数据总线接过去会遇到跨时钟域问题。常见做法是插一个异步 FIFO写端口接 SD 控制器输出的字节流读端口接用户逻辑时钟。FIFO 深度选 512 的倍数读写两个端口之间留出至少一帧余量避免读请求到达时 FIFO 刚好读空。我一般选 1024 深度比一个扇区多一倍burst 读时能连续供数。5.3 锁存数据的时序边界最后一个细节容易忽略read_data的锁存时机。S_READ 状态下sd_sec_read_data_valid每来一个脉冲表示读到一个字节但读完成时最后一个合法数据字节落在sd_sec_read_end之前的时钟周期。如果锁存逻辑只关心sd_sec_read_data_valid而清空rd_cnt的动作放在sd_sec_read_end之后会把第一个字节的数据和最后一个字节的计数错开一位。实际表现是数码管上读到的第一个字节总是之前写过的那一次残留值。解决办法是把rd_cnt的清零条件从sd_sec_read_end改为sd_sec_read_valid的第一个有效脉冲也就是 rd_cnt 到达 0 并且 valid 拉高的那个时刻。这样锁存到的每个字节都和计数值严格对齐单块读写才算是真正闭环了。本文还有配套的精品资源点击获取