这一篇在干嘛?
SD 卡是嵌入式设备里最常见的「小硬盘」,内部自带 NAND Flash 控制器,主机只需要发命令就能读写数据。这一篇我们让 FPGA 通过 SPI 总线对 SD 卡的扇区做最底层的读写:先初始化卡片,再把一个字节写进第一扇区、断电重启后读出来验证——相当于 FPGA 版的「数据掉电不丢」。
实验目标与背景
SD 卡内部集成了 NAND Flash 控制器,负责磨损均衡、坏块管理等麻烦事,对主机暴露的是一套简单的命令接口。本实验的核心目标是:
- 理解 SD 卡在 SPI 模式下的命令/响应协议;
- 掌握 SD 卡 2.0 版的初始化流程(CMD0 → CMD8 → CMD55+ACMD41);
- 完成单个扇区(512 字节)的读与写,并用数码管显示结果。
文件系统不在此实验范围内
实际的 SD 卡上通常有 FAT 等文件系统,可以按文件名和目录路径读写文件,但文件系统非常复杂,本实验不涉及。我们直接按扇区地址读写裸数据。后续实验会通过搜索特定文件头的方式读出音频、图片等文件,那是以本实验的扇区读写能力为基础的。
实验原理:SPI 模式与命令协议
为什么选 SPI 模式
SD 卡支持多种接口模式。在对读写速度要求不高的场合,SPI 模式是最佳选择——只需要四条线就能完成全部数据交换:
- CS(片选):低电平选中卡片;
- SCLK(时钟):主机提供同步时钟;
- MOSI:主机 → 卡,命令与数据;
- MISO:卡 → 主机,响应与数据。
命令格式:6 字节命令包
SD 卡的协议是一种「命令/响应」协议:全部命令由主机发起,卡收到命令后返回响应,响应的内容和长度随命令不同而不同。
SD 卡命令是一个 6 字节的命令包:
| 位置 | 内容 | 说明 |
|---|---|---|
| 第 1 字节 | 命令号 | bit7、bit6 固定为 01,低 6 位是具体命令号 |
| 第 2~5 字节 | 参数 | 32 位命令参数,高位在前(MSB First) |
| 第 6 字节 | CRC + 结束位 | 7 bit CRC 校验加 1 bit 结束位(SPI 模式下 CRC 可选) |
命令通常用十进制命名,例如 CMD17,其中 Command 就是十进制的 17。
响应格式:R1 / R2 / R3
SPI 模式下有三种响应格式。R1 只有一个字节,每一位的含义如下:
| Bit | 含义 |
|---|---|
| 7 | 起始位,恒为 0 |
| 6 | 参数错误(Parameter Error) |
| 5 | 地址错误(Address Error) |
| 4 | 擦除序列错误(Erase Sequence Error) |
| 3 | CRC 错误(CRC Error) |
| 2 | 非法命令(Illegal Command) |
| 1 | 擦除复位(Erase Reset) |
| 0 | 处于 Idle 状态(In Idle State) |
R2 在 R1 的基础上追加第二个字节(Out of Range、Erase Parameter、Write Protect Violation、Card ECC Failed、Card Controller Error 等);R3 在 R1 的基础上追加 4 字节的操作条件寄存器(OCR,MSB First)。
SD 卡 2.0 版初始化步骤
初始化是 SD 卡驱动里最绕的部分,流程如下:
- 上电后延时至少 74 个时钟,等待 SD 卡内部操作完成;
- 片选 CS 拉低选中 SD 卡;
- 发送 CMD0,需要返回 0x01,表示进入 Idle 状态;
- 发送只有 SD 2.0 才有的 CMD8:若返回无错误,初步判断为 2.0 卡,接着循环发送 CMD55+ACMD41,直到返回 0x00,确定是 SD2.0 卡;
- 若 CMD8 返回错误,则是 1.0 卡或 MMC 卡:循环发送 CMD55+ACMD41,返回无错误即为 SD1.0 卡;若循环一定次数仍报错,再发送 CMD1 初始化,返回无错误即为 MMC 卡;若仍失败则无法识别该卡,初始化结束;
- 最后把 CS 拉高。
CMD16 可以改变块长度
通过 CMD16 命令可以修改 SD 卡一次性读写的长度,默认值是 512 字节,正好是一个扇区,本实验直接使用默认值。
读一个块的步骤
- 发送 CMD17(单块读)或 CMD18(多块读),返回 0x00 表示命令被接受;
- 接收数据:开始令牌
fe(多块时为fc)+ 正式数据 512 字节 + CRC 校验 2 字节。
写一个块的步骤
- 发送 CMD24(单块写)或 CMD25(多块写),返回 0x00;
- 发送数据:开始令牌
fe(或fc)+ 正式数据 512 字节 + CRC 校验 2 字节; - 连续读取数据响应令牌,读到
8'bxxx00101表示写成功; - 继续读做忙检测:读到 0x00 表示卡正忙,读到 0xff 表示写操作完成。
程序设计
工程分为两层:底层是 spi_master 模块,负责 SPI 一个字节的收发(在其他实验中已讲解);上层是 sd_card_cmd 模块,负责 SD 卡基本命令、上电初始化的 88 个周期时钟以及数据块的读写。sd_card_cmd 的状态机如下:

命令号的「或 0x40」小机关
查阅其他 SD 卡资料时,你会看到命令号和例程里的写法有区别——其实只是表示方法不同。主机向 SD 卡写命令的实质是:把十六进制命令号的高 8 位与 0x40 做或运算后再写入(因为首字节高两位固定为 01,即 0x40)。所以代码里有了这一段:
if (byte_cnt == 16'd0)
send_data <= (cmd[47:40] | 8'h40);else if (byte_cnt == 16'd1)
send_data <= cmd[39:32];
else if (byte_cnt == 16'd2)
send_data <= cmd[31:24];
else if (byte_cnt == 16'd3)
send_data <= cmd[23:16];
else if (byte_cnt == 16'd4)
send_data <= cmd[15:8];
else if (byte_cnt == 16'd5)
send_data <= cmd[7:0];
else
send_data <= 8'hff;可以看到 48 位命令被拆成 6 个字节依次发出:第 0 字节或上 0x40,后面字节原样发送,发完后补发 0xff 以产生时钟。
sd_card_cmd 模块端口
| 信号名称 | 方向 | 说明 |
|---|---|---|
| sys_clk | in | 时钟输入 |
| rst | in | 异步复位输入,高复位 |
| spi_clk_div | in | SPI 时钟分频,SPI 时钟频率=系统时钟/((spi_clk_div+2)*2) |
| cmd_req | in | sd 卡命令请求 |
| cmd_req_ack | out | sd 卡命令请求应答 |
| cmd_req_error | out | sd 卡命令请求错误 |
| cmd | in | sd 卡命令,命令+参数+CRC,一共 48bit |
| cmd_r1 | in | sd 卡命令期待的 R1 响应 |
| cmd_data_len | in | sd 卡命令后读取的数据长度,大部分命令没有读取数据 |
| block_read_req | in | 块数据读取请求 |
| block_read_valid | out | 块数据读取数据有效 |
| block_read_data | out | 块数据读取数据 |
| block_read_req_ack | out | 块数据读取请求应答 |
| block_write_req | in | 块数据写请求 |
| block_write_data | in | 块数据写数据 |
| block_write_data_rd | out | 块数据写数据请求,提前 block_write_data 一个时钟周期 |
| block_write_req_ack | out | 块数据写请求应答 |
| nCS_ctrl | out | 到 SPI master 控制器,cs 片选控制 |
| clk_div | out | 到 SPI Master 控制器,时钟分频参数 |
| spi_wr_req | out | 到 SPI Master 控制器,写一个字节请求 |
| spi_wr_ack | in | 来自 SPI Master 控制器,写请求应答 |
| spi_data_in | out | 到 SPI Master 控制器,写数据 |
| spi_data_out | in | 来自 SPI Master 控制器,读数据 |
模块里定义了两个分频参数,分别对应初始化阶段的慢时钟和数据读写阶段的快时钟:
parameter SPI_LOW_SPEED_DIV = 248
parameter SPI_HIGH_SPEED_DIV = 0sd_card_sec_read_write 模块
sd_card_sec_read_write 模块继续完成 SD 卡初始化,然后等待扇区读写指令,并完成扇区的读写操作。状态机转换图如下:首先发送 CMD0 命令,然后发送 CMD8 命令,再发送 CMD55,接着发送 ACMD41,如果应答正常,SD 卡初始化完成,等待扇区的读写。

sd_card_sec_read_write 模块状态机
| 信号名称 | 方向 | 说明 |
|---|---|---|
| clk | in | 时钟输入 |
| rst | in | 异步复位输入,高复位 |
| sd_init_done | out | sd 卡初始化完成 |
| sd_sec_read | in | sd 卡扇区读请求 |
| sd_sec_read_addr | in | sd 卡扇区读地址 |
| sd_sec_read_data | out | sd 卡扇区读出的数据 |
| sd_sec_read_data_valid | out | sd 卡扇区读出的数据有效 |
| sd_sec_read_end | out | sd 卡扇区读完成 |
| sd_sec_write | in | sd 卡扇区写请求 |
| sd_sec_write_addr | in | sd 卡扇区写请求应答 |
| sd_sec_write_data | in | sd 卡扇区写请求数据 |
| sd_sec_write_data_req | out | sd 卡扇区写请求数据读取,提前 sd_sec_write_data 一个时钟周期 |
| sd_sec_write_end | out | sd 卡扇区写请求完成 |
| spi_clk_div | in | SPI 时钟分频,SPI 时钟频率=系统时钟/((spi_clk_div+2)*2) |
| cmd_req | in | sd 卡命令请求 |
| cmd_req_ack | out | sd 卡命令请求应答 |
| cmd_req_error | out | sd 卡命令请求错误 |
| cmd | in | sd 卡命令,命令+参数+CRC,一共 48bit |
| cmd_r1 | in | sd 卡命令期待的 R1 响应 |
| cmd_data_len | in | sd 卡命令后读取的数据长度,大部分命令没有读取数据 |
| block_read_req | in | 块数据读取请求 |
| block_read_valid | out | 块数据读取数据有效 |
| block_read_data | out | 块数据读取数据 |
| block_read_req_ack | out | 块数据读取请求应答 |
| block_write_req | in | 块数据写请求 |
| block_write_data | in | 块数据写数据 |
| block_write_data_rd | out | 块数据写数据请求,提前 block_write_data 一个时钟周期 |
| block_write_req_ack | out | 块数据写请求应答 |
初始化必须用慢时钟,读写才能切快时钟
SD 卡的初始化过程必须先用慢时钟发送命令和配置,等初始化成功后才能切换到快时钟进行数据读写。这也是为什么模块里要定义
SPI_LOW_SPEED_DIV = 248(很慢)和SPI_HIGH_SPEED_DIV = 0(很快)两个参数。上电就用高速时钟跟卡通信,是新手最常见的「初始化一直失败」的原因。
引脚约束
时钟 CLK 选择核心板时钟一>6,复位键1,键2累加,TF卡引脚根据《开发板手册》。
set_pin_assignment { SD_DCLK } { LOCATION = A14; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { SD_MISO } { LOCATION = B14; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { SD_MOSI } { LOCATION = A13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { SD_nCS } { LOCATION = A12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key1 } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { rst_n } { LOCATION = B2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { seg_data[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_data[7] } { LOCATION = A6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }
set_pin_assignment { seg_sel[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8;
PULLTYPE = NONE; }注意 SD_MISO(B14)配置了上拉(PULLUP),这是因为 SPI 主机在卡片未驱动 MISO 时需要一个确定的电平。
上板流程与现象
- 插好 TF 卡(卡槽在开发板背面),下载实验程序;
- 数码管显示一个数字——这是 SD 卡第一扇区的第一个数据,数据是随机的;
- 按下按键 2,数字加一,并写入 SD 卡;
- 再次下载程序(相当于重新上电读第一扇区),可以看到直接显示更新后的数据,证明写入确实保存到了卡里。


现象与总结
这个实验打通了 FPGA 访问大容量存储的「最后一公里」:从 SPI 字节收发,到 6 字节命令包组装(高 8 位或 0x40),到初始化状态机(CMD0 → CMD8 → CMD55+ACMD41),再到 512 字节扇区的读写与忙检测。掌握这套流程后,后续做音频播放、图片读取显示时,只需要在扇区数据之上按文件头搜索定位内容即可。
自测
自测
- SD 卡 SPI 模式下一共需要几根信号线?各自的作用是什么?
- SD 卡命令包共 6 字节,请写出每个字节的构成,并解释为什么例程中发送第一个字节时要与 0x40 做或运算。
- SD 2.0 卡的初始化流程依次发送哪些命令?CMD8 返回错误时应该如何继续判断卡的类型?
- 读单块使用哪个命令?读到的数据格式是怎样的(令牌、数据、校验各多少字节)?
- 写块成功后如何判断?忙检测时读到什么值表示卡正在忙、什么值表示写操作完成?