更多交流欢迎关注作者抖音号:81849645041
熟悉SD卡和SDIO工作原理。掌握SD卡的读写。
大多单片机系统都需要大容量存储设备,以存储数据。目前常用的有 U 盘,FLASH 芯片, SD 卡等。他们各有优点,综合比较,最适合单片机系统的莫过于 SD 卡了,它不仅容量可以做到很大(32GB 以上),支持 SPI/SDIO 驱动,而且有多种体积的尺寸可供选择(标准的 SD 卡尺寸,以及 TF 卡尺寸等),能满足不同应用的要求。只需要少数几个 IO 口即可外扩一个高达 32GB 以上的外部存储器,容量从几十 M 到几十G 选择尺度很大,更换也很方便,编程也简单,是单片机大容量外部存储器的首选。
控制器对 SD 卡进行读写通信操作一般有两种通信接口可选,一种是 SPI 接口,另外一种就是 SDIO 接口。
SDIO 全称是安全数字输入/输出接口,多媒体卡(MMC)、SD 卡、SD I/O 卡都有 SDIO 接口。
SD I/O 卡本身不是用于存储的卡,它是指利用 SDIO 传输协议的一种外设。比如 Wi-Fi Card,它主要是提供 Wi-Fi 功能,有些 Wi-Fi 模块是使用串口或者 SPI 接口进行通信的,但 Wi-Fi SDIO Card 是使用 SDIO 接口进行通信的。并且一般设计 SD I/O 卡是可以插入到 SD 的插槽。
SD 卡物理结构
一张 SD 卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器 5 个部分。
存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证 SD 卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制 SD 卡的运行状态,它包括有 8 个寄存器;接口驱动器控制 SD 卡引脚的输入输出。
SD 卡总共有 8 个寄存器,用于设定或表示 SD 卡信息。这些寄存器只能通过对应的命令访问,对 SD 卡进行控制操作并不是像操作控制器 GPIO 相关寄存器那样一次读写一个寄存器的,它是通过命令来控制,SDIO 定义了 64 个命令,每个命令都有特殊意义,可以实现某一特定功能,SD 卡接收到命令后,根据命令要求对 SD 卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现 SD 卡的控制以及读写操作。
SDIO 总线
SD 卡总线拓扑如图。虽然可以共用总线,但不推荐多卡槽共用总线信号,要求一个单独 SD 总线应该连接一个单独的 SD 卡。
SD 卡使用 9-pin 接口通信,其中 3 根电源线、1 根时钟线、1 根命令线和 4 根数据线,具体说明如下:
CLK:时钟线,由 SDIO 主机产生,即由 STM32 控制器输出;
CMD:命令控制线,SDIO 主机通过该线发送命令控制 SD 卡,如果命令要求 SD 卡提供应答(响应),SD 卡也是通过该线传输应答信息;
D0-3:数据线,传输读写数据;SD 卡可将 D0 拉低表示忙状态;
VDD、VSS1、VSS2:电源和地信号。
SDIO 不管是从主机控制器向 SD 卡传输,还是 SD 卡向主机控制器传输都只以 CLK 时钟线的上升沿为有效。SD 卡操作过程会使用两种不同频率的时钟同步数据,一 个是识别卡阶段时钟频率 FOD,最高为 400kHz,另外一个是数据传输模式下时钟频率FPP,默认最高为 25MHz,如果通过相关寄存器配置使 SDIO 工作在高速模式,此时数据传输模式最高频率为 50MHz。
SD 总线通信是基于命令和数据传输的。通讯由一个起始位(“0”),由一个停止位(“1”)终止。SD 通信一般是主机发送一个命令(Command),从设备在接收到命令后作出响应(Response),如有需要会有数据(Data)传输参与。
SD 数据是以块(Black)形式传输的,SDHC 卡数据块长度一般为 512 字节,数据可以从主机到卡,也可以是从卡到主机。数据块需要 CRC 位来保证数据传输成功。CRC 位由 SD卡系统硬件生成。STM32 控制器可以控制使用单线或 4 线传输,本开发板设计使用 4 线传输。
命令格式
SD 命令由主机发出,以广播命令和寻址命令为例,广播命令是针对与 SD 主机总线连接的所有从设备发送的,寻址命令是指定某个地址设备进行命令传输。
SD 命令格式固定为 48bit,都是通过 CMD 线连续传输的(数据线不参与)。
SD 命令的组成如下:
起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为 0,终止位为 1。
传输标志:用于区分传输方向,该位为 1 时表示命令,方向为主机传输到 SD 卡,该位为 0 时表示响应,方向为 SD 卡传输到主机。
命令主体内容包括命令、地址信息/参数和 CRC 校验三个部分。
命令号:它固定占用 6bit,所以总共有 64 个命令(代号:CMD0~CMD63),每个命令都有特定的用途,部分命令不适用于 SD 卡操作,只是专门用于 MMC 卡或者SD I/O 卡。
地址/参数:每个命令有 32bit 地址信息/参数用于命令附加内容,例如,广播命令没有地址信息,这 32bit 用于指定参数,而寻址命令这 32bit 用于指定目标 SD 卡的地址。
CRC7 校验:长度为 7bit 的校验位用于验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失败,也意味着命令传输失败,SD卡不执行命令。
命令类型
SD 命令有 4 种类型:
无响应广播命令(bc),发送到所有卡,不返回任务响应;
带响应广播命令(bcr),发送到所有卡,同时接收来自所有卡响应;
寻址命令(ac),发送到选定卡,DAT 线无数据传输;
寻址数据传输命令(adtc),发送到选定卡,DAT 线有数据传输。
命令描述
SD 卡系统的命令被分为多个类,每个类支持一种“卡的功能设置”。
响应
响应由 SD 卡向主机发出,部分命令要求 SD 卡作出响应,这些响应多用于反馈 SD 卡的状态。SDIO 总共有 7 个响应类型(代号:R1~R7),其中 SD 卡没有 R4、R5 类型响应。特定的命令对应有特定的响应类型,比如当主机发送 CMD3 命令时,可以得到响应 R6。与命令一样,SD 卡的响应也是通过 CMD 线连续传输的。根据响应内容大小可以分为短响应和长响应。短响应是 48bit 长度,只有 R2 类型是长响应,其长度为 136bit。
SD 卡初始化流程
从图中,我们看到,不管什么卡(这里我们将卡分为 4 类:SD2.0 高容量卡(SDHC,最大32G),SD2.0 标准容量卡(SDSC,最大 2G),SD1.x 卡和 MMC 卡),首先我们要执行的是卡上电(需要设置 SDIO_POWER[1:0]=11),上电后发送 CMD0,对卡进行软复位,之后发送 CMD8命令,用于区分 SD 卡 2.0,只有 2.0 及以后的卡才支持 CMD8 命令,MMC 卡和 V1.x 的卡,是不支持该命令的。CMD8 的格式如表。
我们需要在发送 CMD8 的时候,通过其带的参数我们可以设置 VHS 位,以告诉 SD卡,主机的供电情况,VHS 位定义如表:
这里我们使用参数 0X1AA,即告诉 SD 卡,主机供电为 2.7~3.6V 之间,如果 SD 卡支持CMD8,且支持该电压范围,则会通过 CMD8 的响应(R7)将参数部分原本返回给主机,如果不支持 CMD8,或者不支持这个电压范围,则不响应。
在发送 CMD8 后,发送 ACMD41(注意发送 ACMD41 之前要先发送 CMD55),来进一步 确认卡的操作电压范围,并通过 HCS 位来告诉 SD 卡,主机是不是支持高容量卡(SDHC)。 ACMD41 的命令格式如表:
ACMD41 得到的响应(R3)包含 SD 卡 OCR 寄存器内容,OCR 寄存器内容定义如表:
对于支持 CMD8 指令的卡,主机通过 ACMD41 的参数设置 HCS 位为 1,来告诉 SD 卡主 机支 SDHC 卡,如果设置为 0,则表示主机不支持 SDHC 卡,SDHC 卡如果接收到 HCS 为 0则永远不会反回卡就绪状态。对于不支持 CMD8 的卡,HCS 位设置为 0 即可。
SD 卡在接收到 ACMD41 后,返回 OCR 寄存器内容,如果是 2.0 的卡,主机可以通过判断OCR 的 CCS 位来判断是 SDHC 还是 SDSC;如果是 1.x 的卡,则忽略该位。OCR 寄存器的最后一个位用于告诉主机 SD 卡是否上电完成,如果上电完成,该位将会被置 1。
对于 MMC 卡,则不支持 ACMD41,不响应 CMD55,对 MMC 卡,我们只需要在发送 CMD0 后,在发送 CMD1(作用同 ACMD41),检查 MMC 卡的 OCR 寄存器,实现 MMC 卡的初始化。
至此,我们便实现了对 SD 卡的类型区分,流程图中,最后发送了 CMD2 和 CMD3 命令,用于获得卡 CID 寄存器数据和卡相对地址(RCA)。
CMD2,用于获得 CID 寄存器的数据,CID 寄存器数据各位定义如表:
SD 卡在收到 CMD2 后,将返回 R2 长响应(136 位),其中包含 128 位有效数据(CID 寄存器内容),存放在 SDIO_RESP1~4 等 4 个寄存器里面。通过读取这四个寄存器,就可以获得SD 卡的 CID 信息。
CMD3,用于设置卡相对地址(RCA,必须为非 0),对于 SD 卡(非 MMC 卡),在收到CMD3 后,将返回一个新的 RCA 给主机,方便主机寻址。RCA 的存在允许一个 SDIO 接口挂 多个 SD 卡,通过 RCA 来区分主机要操作的是哪个卡。而对于 MMC 卡,则不是由 SD 卡自动返回 RCA,而是主机主动设置 MMC 卡的 RCA,即通过 CMD3 带参数(高 16 位用于 RCA 设置),实现 RCA 设置。同样 MMC 卡也支持一个 SDIO 接口挂多个 MMC 卡,不同于 SD 卡的是所有的 RCA 都是由主机主动设置的,而 SD 卡的 RCA 则是 SD 卡发给主机的。
在获得卡 RCA 之后,我们便可以发送 CMD9(带 RCA 参数),获得 SD 卡的 CSD 寄存器内容,从 CSD 寄存器,我们可以得到 SD 卡的容量和扇区大小等十分重要的信息。
至此,我们的 SD 卡初始化基本就结束了,最后通过 CMD7 命令,选中我们要操作的 SD 卡,即可开始对 SD 卡的读写操作了。
MDK5 开发环境。
STM32F4xx HAL库。
STM32F407 开发板。
STM32F4xx 参考手册。
STM32F4xx 数据手册。
STM32F407 开发板电路原理图。
#ifndef __BSP_SD_H
#define __BSP_SD_H
#include "stm32f4xx.h"
// 支持的SD卡定义
#define SDIO_STD_CAPACITY_SD_CARD_V1_1 ((uint32_t)0x00000000)
#define SDIO_STD_CAPACITY_SD_CARD_V2_0 ((uint32_t)0x00000001)
#define SDIO_HIGH_CAPACITY_SD_CARD ((uint32_t)0x00000002)
#define SDIO_MULTIMEDIA_CARD ((uint32_t)0x00000003)
#define SDIO_SECURE_DIGITAL_IO_CARD ((uint32_t)0x00000004)
#define SDIO_HIGH_SPEED_MULTIMEDIA_CARD ((uint32_t)0x00000005)
#define SDIO_SECURE_DIGITAL_IO_COMBO_CARD ((uint32_t)0x00000006)
#define SDIO_HIGH_CAPACITY_MMC_CARD ((uint32_t)0x00000007)
// GPIOC D0-D3 SCK
#define Pin_SDIO_D0_D3 GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11
#define Pin_SDIO_SCK GPIO_PIN_12
// GPIOD CMD
#define Pin_SDIO_CMD GPIO_PIN_2
uint8_t SDCard_Init(void); // 初始化函数
uint8_t SD_GetCardInfo(HAL_SD_CardInfoTypeDef *cardinfo); // 获取sd卡信息
void SDCard_Show_Info(HAL_SD_CardInfoTypeDef *cardinfo); // 打印sd卡信息
uint8_t SD_ReadDisk(uint8_t* buf, uint32_t sector, uint32_t cnt); // 读取数据
uint8_t SD_WriteDisk(uint8_t *buf, uint32_t sector, uint32_t cnt); // 写入数据
#endif
#include "bsp_sd.h"
SD_HandleTypeDef SD_Handle;
/**
* 函数名:SD_Init
* 描述:SD 卡初始化
* 输入:
* 输出:返回值:0 初始化正确;
*/
uint8_t SDCard_Init(void)
{
uint8_t sta;
__HAL_RCC_SDIO_CLK_ENABLE();
__SDIO_CLK_ENABLE();
SD_Handle.Instance = SDIO;
SD_Handle.Init.BusWide = SDIO_BUS_WIDE_1B; // 1 位数据线
SD_Handle.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE; // 不使用 bypass 模式
SD_Handle.Init.ClockDiv = SDIO_TRANSFER_DIR_TO_SDIO; // SD 传输时钟频率
SD_Handle.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING; // 上升沿
SD_Handle.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE; // 空闲时不关闭时钟电源
SD_Handle.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE; // 关闭硬件流控
sta = HAL_SD_Init(&SD_Handle);
HAL_SD_ConfigWideBusOperation(&SD_Handle,SDIO_BUS_WIDE_4B); // 使能4 位宽总线模式
return sta;
}
/**
* 函数名:HAL_SD_MspInit
* 描述:SD 卡初始化回调函数 用于引脚初始化
* 输入:hsd句柄
* 输出:返回值:0 初始化正确;
*/
void HAL_SD_MspInit(SD_HandleTypeDef *hsd)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;
GPIO_InitStruct.Pin = Pin_SDIO_D0_D3 | Pin_SDIO_SCK; // D0~D3 SCK引脚
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.Pin = Pin_SDIO_CMD; // CMD引脚
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
/**
* 函数名:SD_GetCardInfo
* 描述:获取卡信息
* 输入:HAL_SD_CardInfoTypeDef
* 输出:状态值
*/
uint8_t SD_GetCardInfo(HAL_SD_CardInfoTypeDef *cardinfo)
{
uint8_t sta;
sta = HAL_SD_GetCardInfo(&SD_Handle, cardinfo); // 获取卡信息
return sta;
}
// 打印SD卡信息
void SDCard_Show_Info(HAL_SD_CardInfoTypeDef *cardinfo)
{
switch(cardinfo->CardType) //卡类型
{
case SDIO_STD_CAPACITY_SD_CARD_V1_1:
printf("Card Type:SDSC V1.1\r\n");
break;
case SDIO_STD_CAPACITY_SD_CARD_V2_0:
printf("Card Type:SDSC V2.0\r\n");
break;
case SDIO_HIGH_CAPACITY_SD_CARD:
printf("Card Type:SDHC V2.0\r\n");
break;
case SDIO_MULTIMEDIA_CARD:
printf("Card Type:MMC Card\r\n");
break;
}
printf("Card CardVersion:%d\r\n", cardinfo->CardVersion); // 版本号
printf("Card RelCardAdd:%d\r\n", cardinfo->RelCardAdd); // 卡相对地址
printf("Card BlockNbr:%d\r\n", cardinfo->BlockNbr); // 显示块数量
printf("Card BlockSize:%d\r\n", cardinfo->BlockSize); // 显示块大小
}
/**
* 函数名:SD_ReadDisk
* 描述:读 SD 卡
* 输入:buf:读数据缓存区 sector:扇区地址 cnt:扇区个数
* 输出:返回值:错误状态;0,正常;
*/
uint8_t SD_ReadDisk(uint8_t* buf, uint32_t sector, uint32_t cnt)
{
uint8_t sta;
sta = HAL_SD_ReadBlocks(&SD_Handle, buf, sector, cnt, 1000);
return sta;
}
/**
* 函数名:SD_WriteDisk
* 描述:写 SD 卡
* 输入:buf:写数据缓存区 sector:扇区地址 cnt:扇区个数
* 输出:返回值:错误状态;0,正常;
*/
uint8_t SD_WriteDisk(uint8_t *buf, uint32_t sector, uint32_t cnt)
{
uint8_t sta;
sta = HAL_SD_WriteBlocks(&SD_Handle, buf, sector, cnt, 1000);
return sta;
}
#include "bsp_clock.h"
#include "bsp_uart.h"
#include "bsp_key.h"
#include "bsp_sd.h"
#include "bsp_led.h"
HAL_SD_CardInfoTypeDef pCardInfo;
int main(void)
{
uint8_t sta,size;
uint8_t txbuf[] = {"SD Card ReadWrite Test"};
size = sizeof(txbuf);
uint8_t rxbuf[size];
CLOCLK_Init(); // 初始化系统时钟
UART_Init(); // 串口初始化
KEY_Init(); // 按键初始化
LED_Init(); // LED初始化
sta = SDCard_Init();// sd初始化 返回状态
if(sta == HAL_OK) // 初始化成功
{
LED1_ON;
SD_GetCardInfo(&pCardInfo); // 获取sd卡设备信息
HAL_Delay(50);
SDCard_Show_Info(&pCardInfo); // 显示sd卡设备信
}
while(1)
{
uint8_t key = KEY_Scan(0);
if( key == 1) // KEY0按下
{
SD_WriteDisk(txbuf, 0, 1); // 发送测试数据
printf("Write buf: %s \n", txbuf);
}
if( key == 2) // KEY1按下
{
SD_ReadDisk(rxbuf, 0, 1); // 读取
printf("Read buf: %s \n", rxbuf);
}
HAL_Delay(50);
}
}
打开串口助手,把编译好的程序下载到开发板。首先打印SD卡信息。
按下KEY0 写入数据。按下key1从地址读取数据。