47. SDIO驱动SD卡
一、SD卡简介
SD 卡的规范由 SD 卡协会明确,可以访问 https://www.sdcard.org 查阅更多标准。SD 卡主要有 SD、Mini SD 和 microSD(原名 TF 卡,2004 年正式更名为 Micro SD Card)三种类型,Mini SD 已经被 microSD 取代,使用得不多。
上述表格的 “脚位数”,对应于实卡上的 “金手指” 数,不同类型的卡的触点数量不同,访问的速度也不相同。SD 卡允许了不同的接口来访问它的内部存储单元。最常见的是 SDIO 模式和 SPI 模式。
SD 卡和 micorSD 只有引脚和形状大小不同,内部结构类似,操作时序完全相同,可以使用完全相同的代码驱动。
二、SD卡寄存器
SD 卡有自己的寄存器,但它不能直接进行读写操作,需要通过命令来控制,SDIO 协议定义了一些命令用于实现某一特定功能,SD 卡根据收到的命令要求对内部寄存器进行修改。
三、命令和响应
一个完整的 SD 卡操作过程是:主机(单片机等)发起 “命令”,SD 卡根据命令的内容决定是否发送响应信息及数据等,如果是数据读/写操作,主机还需要发送停止读/写数据的命令来结束本次操作,这意味着主机发起命令指令后,SD 卡可以没有响应、数据等过程,这取决于命令的含义。
- 命令:应用相关命令(ACMD)和通用命令(CMD),通过命令线 CMD 传输,固定长度 48 位。
- 响应:SD 卡接收到命令,会有一个响应,用来反应 SD 卡状态。有 2 种响应类型:短响应(48 位,格式与命令一样)和 长响应(136 位)。
- 数据:主机发送的数据 / SD 发送的数据。SD 数据是以块(Block)形式传输,SDHC 卡数据块长度一般为 512 字节。数据块需要 CRC 保证数据传输成功。
SD 卡的命令固定为 48 位,由 6 个字节组成,字节 1 的最高 2 位固定为 01,低 6 位为命令号,串行逐位发送时先发送最高位(MSB),然后是次高位。。字节 2 ~ 5 为命令参数,有些命令是没有参数的。字节 6 的高七位为 CRC 值,最低位恒定为 1。
使用 SDIO 接口驱动,CRC7 校验值必须正确;而 SPI 接口驱动,CRC7 校验默认关闭,即伪 CRC。
SD 卡的命令总共有 12 类,分为 Class0 ~ Class11。
SD 卡常用的命令如下:
发送 ACMD 命令之前,必须先发送 CMD55,接下来要发送的是应用命令(APP CMD),而非标准命令。
上表中,大部分的命令是初始化的时候用的,而表中的 R1、R1b、R2、R3、R6 和 R7 等是 SD 卡的应答信号。在主机发送有响应的命令后,SD 卡都会给出相对应的应答,以告知主机该命令的执行情况,或者返回主机需要获取的数据。使用 SDIO 接口时,响应通过 CMD 线传输。
SD 的响应大体分为短响应 48bit 和长响应 136bit,每个响应也有规定好的格式。R1、R1b、R3、R6 和 R7 属于短响应,而 R2 属于长响应,它们具体作用如下表所示。
- R1 响应:如果有传输到卡的数据,那么在数据线 0 有 busy 信号(R1b)。
通过响应内容中的 command index 获知响应哪个命令。
- R2 响应:CID 寄存器内容作为 CMD2 和 CMD10 响应,CSD 寄存器内容作为 CMD9 响应。
- R3 响应:OCR 寄存器的值作为 ACMD41 的响应。
- R7 响应:专用于命令 CMD8 的响应,返回卡支持电压范围和检测模式。
- R6 响应:专用于命令 CMD3 的响应(RCA 响应)。
SPI 模式没有 RCA 寄存器,也不支持 CMD3 命令,没有 R6 响应。
四、SD卡操作模式
SD 卡系统(包括主机和 SD 卡)定义了 SD 卡的工作模式,在每个操作模式下,SD 卡都有几种状态,状态之间通过命令控制实现卡状态的切换。
对于我们来说两种有效操作模式:卡识别模式 和 数据传输模式。
在系统复位后,主机处于 卡识别模式,寻找总线上可用的 SDIO 设备,对 SD 卡进行数据读写之前需要识别卡的种类:V1.0 标准卡、V2.0 标准卡、V2.0 高容量卡或者不被识别卡;同时,SD 卡也处于 卡识别模式,直到被主机识别到,即当 SD 卡在卡识别状态接收到 CMD3(SEND_RCA)命令后,SD 卡就进入 数据传输模式,而主机在总线上所有卡被识别后也进入 数据传输模式。
4.1、卡识别模式
在 卡识别模式 下,主机会复位所有处于 “卡识别模式” 的 SD 卡,确认其工作电压范围,识别 SD 卡类型,并且获取 SD 卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要求 SD 卡工作在识别时钟频率 FOD 的状态下。卡识别模式下 SD 卡状态转换如下图所示。
主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送 GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会复位。
主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD 卡有一个电压支持范围,主机当前电压必须在该范围可能才能与卡正常通信。SEND_IF_COND(CMD8)命令就是用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,如果卡支持主机电压就产生响应,否则不响应。而主机则根据响应内容确定卡的电压匹配性。CMD8 是 SD 卡标准 V2.0 版本才有的新命令,所以如果主机有接收到响应,可以判断卡为 V2.0 或更高版本 SD 卡。
SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41 命令的 VDD 电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。对于对 CMD8 有响应的卡,把 ACMD41 命令的 HCS 位设置为 1,可以测试卡的容量类型,如果卡响应的 CCS 位为 1 说明为高容量 SD 卡,否则为标准卡。卡在响应 ACMD41 之后进入准备状态,不响应 ACMD41 的卡为不可用卡,进入无效状态。ACMD41 是应用特定命令,发送该命令之前必须先发 CMD55。
ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在发送 CID 之后就进入识别状态。之后主机就发送 SEND_RELATIVE_ADDR(CMD3)命令,让卡自己推荐一个相对地址(RCA)并响应命令。这个 RCA 是 16bit 地址,而 CID 是 128bit 地址,使用 RCA 简化通信。卡在接收到 CMD3 并发出响应后就进入数据传输模式,并处于待机状态,主机在获取所有卡 RCA 之后也进入数据传输模式。
4.2、数据传输模式
只有 SD 卡系统处于 数据传输模式 下才可以进行数据读写操作。数据传输模式下可以将主机 SD 时钟频率设置为 FPP,默认最高为 25MHz,频率切换可以通过 CMD4 命令来实现。数据传输模式下,SD 卡状态转换过程见下图所示。
CMD7 用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态,必须选择一个 RCA 地址目标卡使其进入传输状态才可以进行数据通信。同时通过 CMD7 命令也可以让已经被选择的目标卡返回到待机状态。
数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传输状态下可以通过命令对卡进行数据读写、擦除。CMD12 可以中断正在进行的数据通信,让卡返回到传输状态。CMD0 和 CMD15 会中止任何数据编程操作,返回卡识别模式,注意谨慎使用,不当操作可能导致卡数据被损坏。
在数据模式下我们可以对 SD 卡的存储块进行读写访问操作。SD 卡上电后默认以一位数据总线访问,可以通过指令设置为宽总线模式,可以同时使有 4 位总线并行读写数据,这样对于支持宽总线模式的接口(如:SDIO 和 SPI 等)都能加快数据操作速度。
SD 卡有两种数据模式,一种是常规的 8 位宽,即一次按一字节传输,另一种是一次按 512 字节 传输。当按 8-bit 连续传输时,每次传输从最低字节开始,每字节从最高位(MS)开始发送。
当使用一条数据线时,只能通过 DAT0 进行数据传输,那它的数据传输结构如下图所示。
当使用 4 线模式传输 8-bit 结构的数据时,数据仍按 MSB 先发送的原则,DAT[3:0] 的高位发送高数据位,低位发送低数据位。硬件支持的情况下,使用 4 线传输可以提升传输速率,其数据传输结构如下图所示。
【1】、SD 卡单块数据块读取流程
CMD16 设置的数据块大小,一般为 512 字节,此设置直接决定 SD 卡的块大小,SD 卡默认的块大小自动失效。
【2】、SD 卡多块数据块读取流程
【3】、SD 卡单块数据块写入流程
【4】、SD 卡多块数据块写入流程
ACMD 指令仅对 SD 卡有效,另外需要先发送 CMD55 指令。
五、原理图
六、程序源码
6.1、SD卡初始化
SD 卡初始化函数:
SD_HandleTypeDef g_sd_handler;
/**
* @brief SD卡初始化函数
*
* @return uint8_t 0: 代表成功
*/
uint8_t SD_Init(void)
{
// 初始化时的时钟不能大于400KHZ, SD传输时钟频率最大 25MHZ
g_sd_handler.Instance = SDIO;
g_sd_handler.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING; // 数据或指令变化的时钟沿
g_sd_handler.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE; // 不设置旁路分频器,直接用 HCLK 进行分频得到SDIO_CK
g_sd_handler.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE; // 空闲时不关闭时钟电源
g_sd_handler.Init.BusWide = SDIO_BUS_WIDE_1B; // SDIO总线位宽
g_sd_handler.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE; // 使能硬件流控制
g_sd_handler.Init.ClockDiv = 1; // 设置SDIO时钟分频
return HAL_SD_Init(&g_sd_handler);
}
SD 卡底层初始化函数:
/**
* @brief SD卡底层初始化函数
*
* @param hsd SDIO句柄
*/
void HAL_SD_MspInit(SD_HandleTypeDef *hsd)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hsd->Instance == SDIO)
{
__HAL_RCC_SDIO_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_2;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
}
6.2、SD卡读取数据
SD 卡块读取数据函数:
/**
* @brief SD卡块读取数据函数
*
* @param hsd SDIO句柄
* @param blockAddress 扇区地址
* @param numberOfBlocks 读取的扇区个数
* @param buffer 保存读取数据的缓冲区
*
* @return uint8_t 读取成功返回0,否则返回1
*/
uint8_t SD_ReadData(SD_HandleTypeDef *hsd, uint32_t blockAddress, uint32_t blockCount, uint8_t *buffer)
{
uint8_t status = 0;
uint32_t timeOut = 1000;
HAL_StatusTypeDef sd_status = HAL_OK;
__disable_irq(); // 关闭总中断(POLLIN模式,严禁中断打断SDIO读写操作
sd_status = HAL_SD_ReadBlocks(hsd, buffer, blockAddress, blockCount, 0xffff);
status = (sd_status == HAL_OK) ? 0 : 1;
// 等待SD卡读完
while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER)
{
status = (timeOut--) ? status : 2;
}
__enable_irq(); // 开启总中断
return status;
}
SD 卡块写入数据函数:
/**
* @brief SD卡块写入数据函数
*
* @param hsd SDIO句柄
* @param sectorAddress 扇区地址
* @param buffer 写入数据的缓冲区
* @param sectorCount 写入的扇区个数
*
* @return uint8_t 成功写入返回0,否则返回1
*/
uint8_t SD_WriteData(SD_HandleTypeDef *hsd, uint32_t blockAddress, uint32_t blockCount, uint8_t *buffer)
{
uint8_t status = 0;
uint32_t timeOut = 1000;
HAL_StatusTypeDef sd_status = HAL_OK;
__disable_irq(); // 关闭总中断(POLLIN模式,严禁中断打断SDIO读写操作
sd_status = HAL_SD_WriteBlocks(hsd, buffer, blockAddress, blockCount, 0xffff);
status = (sd_status == HAL_OK) ? 0 : 1;
// 等待SD卡写完
while (HAL_SD_GetCardState(hsd) != HAL_SD_CARD_TRANSFER)
{
status = (timeOut--) ? status : 2;
}
__enable_irq(); // 开启总中断
return status;
}
6.3、main()函数
main() 函数:
char writeData[512];
char readData[512];
int main(void)
{
uint8_t result = 0;
HAL_SD_CardInfoTypeDef HAL_SD_CardInfoStruct = {0};
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
SD_Init();
// 读取SD卡信息
HAL_SD_GetCardInfo(&g_sd_handler, &HAL_SD_CardInfoStruct);
printf("SD Card Info:\r\n");
printf("SD Card Type: %ld\r\n", HAL_SD_CardInfoStruct.CardType);
printf("SD Card Block Size: %ld\r\n", HAL_SD_CardInfoStruct.BlockSize);
printf("SD Card BlockNbr: %ld\r\n", HAL_SD_CardInfoStruct.BlockNbr);
printf("SD Card Version: %ld\r\n", HAL_SD_CardInfoStruct.CardVersion);
for (uint32_t i = 0; i < 512; i++)
{
writeData[i] = 'A' + (i % 26);
}
result = SD_WriteData(&g_sd_handler, 0, 1, (uint8_t *)writeData);
printf("SD Card Write Result: %d\r\n", result);
result = SD_ReadData(&g_sd_handler, 0, 1, (uint8_t *)readData);
printf("SD Card Read Result: %d\r\n", result);
printf("SD Card Read Data:\r\n");
for (uint32_t i = 0; i < 512; i++)
{
printf("%c ", readData[i]);
}
printf("\r\n");
while (1)
{
}
return 0;
}