1、简介
1.1 SD卡简介
很多单片机系统都需要大容量存储设备,以存储数据(常用的有U盘、FLASH芯片、SD卡等),比较而言SD卡是单片机大容量外部存储的首选,只需要少数几个IO口即可外扩一个容量从几十M到几十G的,且有多种体积尺寸可选(标准SD卡、TF卡等)的外部存储器
SD卡(Secure Digital Memory Card)即:安全数码卡,它是在MMC的基础上发展而来,是一种基于半导体快闪记忆器的新一代记忆设备,它被广泛地于便携式装置上使用,例如数码相机、个人数码助理(PDA)和多媒体播放器等。SD卡由日本松下、东芝及美国SanDisk公司于1999年8月共同开发研制。 SD卡按容量分类,可以分为3类:SD卡、SDHC卡、SDXC卡,如下表所示:
SD卡和SDHC卡协议基本兼容,但是SDXC卡的区别比较大,这里仅介绍SD/SDHC卡(简称SD卡),SD卡由9个引脚与外部通讯,支持SPI和SDIO两种操作模式,不同模式下SD卡引脚功能描叙如下图表示:
1.2 SD卡的物理结构及内部框图
SD卡的物理结构一般包括以下5个部分:
- 存储单元:是存储数据部件;
- 存储单元接口:存储单元通过存储单元接口与卡控制单元进行数据传输;
- 电源检测单元:保证SD卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;
- 卡及接口控制单元:控制SD卡的运行状态,它包括有8个寄存器;
- 接口驱动器:控制SD卡引脚的输入输出
SDIO由SDIO适配器和APB2接口两部分组成:
- SDIO适配器:提供特定于MMC/SD/SD I/O卡的所有功能,如时钟生成单元、命令和数据传输
- APB2接口:访问SDIO适配器寄存器,并且生成中断和DMA请求信号
下图是SDIO功能框图及SDIO适配器框图:
1.3 SD卡命令
SD卡命令由主机发出,命令格式固定为48位,通过CMD线连续传输,数据线不参与。SD命令结构如下图示:由6个字节组成,字节1的最高2位固定为01、低6位为命令号(比如CMD16);字节2 ~ 5为命令参数(有的命令没有参数);字节6的高7位为CRC、最低位恒定为1
SD命令组成的详细说明如下:
- 起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为 0,终止位为 1。
- 传输标志:用于区分传输方向,该位为 1 时表示命令,方向为主机传输到 SD 卡,该位为 0时表示响应,方向为 SD卡传输到主机。 - 命令主体内容包括命令、地址信息/参数和 CRC 校验三个部分
- 命令号:它固定占用 6bit,所以总共有 64个命令(代号:CMD0~CMD63),每个命令都有特定的用途,部分命令不适用于 SD 卡操作,只是专门用于 MMC卡或者SD I/O卡。
- 地址/参数:每个命令有 32bit地址信息/参数用于命令附加内容,例如,广播命令没有地址信息,这 32bit用于指定参数,而寻址命令这 32bit用于指定目标 SD卡的地址。
- CRC7 校验:长度为 7bit的校验位用于验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失败,也意味着命令传输失败,SD卡不执行命令。
1.4 SD卡响应
SD卡命令的响应由SD卡向主机发出,部分命令要求SD卡作出响应,这些响应多用于反馈SD卡的状态。基本特性如下:
- SDIO总共有7个响应类型(代号:R1~R7),其中SD卡没有R4、R5类型响应。特定的命令对应有特定的响应类型,比如当主机发送CMD3命令时,可以得到响应R6。
- 与命令一样,SD卡的响应也是通过CMD线连续传输的。
- 根据响应内容大小可以分为短响应和长响应。短响应是48bit长度,只有R2类型是长响应,其长度为136bit。
SD的读写操作是以块为操作对象。先发送命令开始传输,然后传输数据块,传输完数据块紧接着传输CRC检验值。最好发送停止命令停止数据传输
1.5 SD卡的操作模式及切换
SD卡有多个版本,STM32控制器目前最高支持《Physical Layer Simplified Specification V2.0》定义的SD卡,STM32控制器对SD卡进行数据读写之前需要识别卡的种类:V1.0标准卡、V2.0标准卡、V2.0高容量卡或者不被识别卡。
SD卡系统定义了两种操作模式:卡识别模式和数据传输模式
在系统复位后,主机处于卡识别模式,寻找总线上可用的SDIO设备;同时,SD卡也处于卡识别模式,直到被主机识别到,即当SD卡接收到SEND_RCA(CMD3)命令后,SD卡就会进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。
2. 硬件设计
LED2指示灯用来提示系统运行状态,S1写入数据,S2读取数据,串口用来打印SD卡的容量、类型等信息
- LED2指示灯
- USART1
- S1,S2按键
- TF卡
3、 STM32CubeMX设置
RCC设置外接HSE,时钟设置为72M
- PE5设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平
- PE3/PE4设置为GPIO输入模式、上拉模式
- USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位
- 激活SDIO,选择4线SD模式,分频因子设为4,使能流控,其余默认设置
在 Parameter Settings
进行具体参数配置。
Clock transition on which the bit capture is made: Rising transition。主时钟 SDIOCLK 产生 CLK 引脚时钟有效沿选择,可选上升沿或下降沿,它设定 SDIO 时钟控制寄存器(SDIO_CLKCR)的 NEGEDGE 位的值,一般选择设置为上升沿。
SDIO Clock divider bypass: Disable。时钟分频旁路使用,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 BYPASS 位。如果使能旁路,SDIOCLK 直接驱动 CLK 线输出时钟;如果禁用,使用 SDIO_CLKCR 寄存器的 CLKDIV 位值分频 SDIOCLK,然后输出到 CLK 线。一般选择禁用时钟分频旁路。
SDIO Clock output enable when the bus is idle: Disable the power save for the clock。节能模式选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 PWRSAV 位的值。如果使能节能模式,CLK 线只有在总线激活时才有时钟输出;如果禁用节能模式,始终使能 CLK 线输出时钟。
SDIO hardware flow control: The hardware control flow is enabled。硬件流控制选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 HWFC_EN 位的值。硬件流控制功能可以避免 FIFO 发送上溢和下溢错误。
SDIOCLK clock divide factor:4。时钟分频系数,它设定 SDIO_CLKCR 寄存器的 CLKDIV 位的值,设置 SDIOCLK 与 CLK 线输出时钟分频系数:CLK 线时钟频率=SDIOCLK/([CLKDIV+2])。
SDIO_CK 引脚的时钟信号在卡识别模式时要求不超过 400KHz,而在识别后的数据传输模式时则希望有更高的速度(最大不超过 25MHz),所以会针对这两种模式配置 SDIOCLK 的时钟。
这里参数描述建议将SDIOCLK clock divede factor 参数使用默认值为0,SDIOCLK为72MHz,可以得到最大频率36MHz,但请注意,有些型号的SD卡可能不支持36MHz这么高的频率,所以还是要以实际情况而定。
- 添加 SDIO 对应 DMA2 的通道4。DMA模式选择循环模式,方向选为内存到外设,优先级设置为low
SDIO 外设支持生成 DMA 请求,使用 DMA 传输可以提高数据传输效率,因此在 SDIO 的控制代码中,可以把它设置为 DMA 传输模式或轮询模式,ST 标准库提供 SDIO 示例中针对这两个模式做了区分处理。应用中一般都使用DMA 传输模式。
Priority:
当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通 道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
Mode:
Normal 表示单次传输,传输一次后终止传输。
Circular 表示循环传输,传输完成后又重新开始继续传输,不断循环永不停止。
Increment Address:
Peripheral 表示外设地址自增。
Memory 表示内存地址自增。
Data Width:
Byte 一个字节。
Half Word 半个字,等于两字节。
Word 一个字,等于四字节。
- 设置SDIO和DMA的中断,原则是全局中断优先级高于DMA中断
- 最好激活CRC功能,以避免后续读写SD卡报CRC校验错误
- 输入工程名,选择工程路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
4、程序编程
在sdio.c文件下可以看到sdio初始化函数,在stm32f1xx_hal_sd.c文件中可以查看SDIO的相关操作函数,主要用到的函数有;
//可读取SD卡的基础信息,如内存
HAL_SD_CardStateTypeDef HAL_SD_GetCardState(SD_HandleTypeDef *hsd)//获取SD卡的ID
HAL_StatusTypeDef HAL_SD_GetCardCID(SD_HandleTypeDef *hsd, HAL_SD_CardCIDTypeDef *pCID)//擦除SD卡
/*** @brief 擦除给定SD卡的指定存储区域。* @param hsd: 指向SD句柄的指针* @param BlockStartAdd:起始块地址* @param BlockEndAdd:结束块地址* @retval HAL status*/
HAL_StatusTypeDef HAL_SD_Erase(SD_HandleTypeDef *hsd, uint32_t BlockStartAdd, uint32_t BlockEndAdd)//擦除SD卡内容块
//例:
HAL_SD_Erase(&hsd,0,1);//SD 卡写入数据
/*** @brief 将块写入卡中的指定地址* @param hsd: 指向SD句柄的指针* @param pData: 指向将包含要传输的数据的缓冲区的指针* @param BlockAdd: 写入数据的块地址* @param NumberOfBlocks: 要写入的SD块数* @param Timeout: 指定超时值* @retval HAL status*/
HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)
//例:
HAL_SD_WriteBlocks(&hsd,Buffer_Tx,0,1,0xff);//SD 卡读取数据
/*** @brief 从卡中的指定地址读取块. * @param hsd: 指向SD句柄的指针* @param pData: 指向将包含接收到的数据的缓冲区的指针* @param BlockAdd: 读取数据的块地址* @param NumberOfBlocks: 要读取的SD块数* @param Timeout: 指定超时值* @retval HAL status*/
HAL_StatusTypeDef HAL_SD_ReadBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);
//例:
HAL_SD_ReadBlocks(&hsd,Buffer_Rx,0,1,0xff);
在 main.c
头部添加全局变量
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define BLOCK_START_ADDR 0 /* Block start address */
#define NUM_OF_BLOCKS 1 /* Total number of blocks */
#define BUFFER_WORDS_SIZE ((BLOCKSIZE * NUM_OF_BLOCKS) >> 2) /* Total data size in bytes */
/* USER CODE END PD *//* USER CODE BEGIN PV */
uint8_t Buffer_Tx[512],Buffer_Rx[512] = {0};
uint32_t i;
/* USER CODE END PV */
main函数中添加测试程序
int main(void)
{
//***省略**//printf("Micro SD Card Test...\r\n");
/* 检测SD卡是否正常(处于数据传输模式的传输状态) */
if(HAL_SD_GetCardState(&hsd) == HAL_SD_CARD_TRANSFER) //获取当前sd卡数据状态
{ printf("Initialize SD card successfully!\r\n");// 打印SD卡基本信息printf(" SD card information! \r\n");printf(" CardCapacity : %llu \r\n", (unsigned long long)hsd.SdCard.BlockSize * hsd.SdCard.BlockNbr);// 显示容量printf(" CardBlockSize : %d \r\n", hsd.SdCard.BlockSize); // 块大小printf(" LogBlockNbr : %d \r\n", hsd.SdCard.LogBlockNbr); // 逻辑块数量printf(" LogBlockSize : %d \r\n", hsd.SdCard.LogBlockSize);// 逻辑块大小printf(" RCA : %d \r\n", hsd.SdCard.RelCardAdd); // 卡相对地址printf(" CardType : %d \r\n", hsd.SdCard.CardType); // 卡类型// 读取并打印SD卡的CID信息HAL_SD_CardCIDTypeDef sdcard_cid;HAL_SD_GetCardCID(&hsd,&sdcard_cid);//读取SD卡的信息CID寄存器。printf(" ManufacturerID: %d \r\n",sdcard_cid.ManufacturerID);
}
else
{printf("SD card init fail!\r\n" );
}/* 擦除SD卡块 */
printf("------------------- Block Erase -------------------------------\r\n");
if(HAL_SD_Erase(&hsd, BLOCK_START_ADDR, NUM_OF_BLOCKS) == HAL_OK)
{/* Wait until SD cards are ready to use for new operation */while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER){}printf("\r\nErase Block Success!\r\n");
}
else
{printf("\r\nErase Block Failed!\r\n");
}/* 填充缓冲区数据 */
memset(Buffer_Tx, 0x15, sizeof(Buffer_Tx));/* 向SD卡块写入数据 */
printf("------------------- Write SD card block data Test ------------------\r\n");
if(HAL_SD_WriteBlocks(&hsd, Buffer_Tx, BLOCK_START_ADDR, NUM_OF_BLOCKS, 10) == HAL_OK)
{while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER){}printf("\r\nWrite Block Success!\r\n");for(i = 0; i < sizeof(Buffer_Tx); i++){printf("0x%02x:%02x ", i, Buffer_Tx[i]);}//printf("%s",Buffer_Tx);printf("\r\n");
}
else
{printf("\r\nWrite Block Failed!\r\n");
}/* 读取操作之后的数据 */
printf("------------------- Read SD card block data after Write ------------------\r\n");if(HAL_SD_ReadBlocks(&hsd, Buffer_Rx, BLOCK_START_ADDR, NUM_OF_BLOCKS, 10) == HAL_OK)
{while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER){}printf("\r\nRead Block Success!\r\n");for(i = 0; i < sizeof(Buffer_Rx); i++){printf("0x%02x:%02x ", i, Buffer_Rx[i]);}printf("\r\n");printf("中考%s",&Buffer_Rx[0]);printf("\r\n");
}
else
{printf("\r\nRead Block Failed!\r\n");
}while (1){HAL_GPIO_TogglePin(LED3_GPIO_Port,LED3_Pin);HAL_Delay(500);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}}
注意:
- 要先插入SD卡,要不然SDIO初始化时会失败(仅针对原始的HAl生成程序)
- 如果读写失败,可能SD通信速度太高,可将hsd.Init.ClockDiv值改大
- 操作SD卡后最好先用函数HAL_SD_GetCardState()确定一下卡的状态再进行其他操作。
- 注意先擦除后写入。
5、下载验证
编译无误后下载到板子上,查看串口的打印信息:
6 、扩展DMA
DMA的读取和写入其实跟普通的方法相识,主要用到的是以下的读写函数
//DMA读取函数
/*** @brief 从卡中的指定地址读取块。数据传输由DMA模式管理。* @param hsd:指针SD句柄* @param pData: 指向将包含接收数据的缓冲区的指针* @param BlockAdd:读取数据的块地址* @param NumberOfBlocks: 要读取的块数。* @retval HAL status*/
HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);//DMA写入函数
/*** @brief 将块写入卡中的指定地址。数据传输由DMA模式管理。* @param hsd: Pointer to SD handle* @param pData: 指向将包含要传输的数据的缓冲区的指针* @param BlockAdd:写入数据的块地址* @param NumberOfBlocks: 要写入的块数* @retval HAL status*/
HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks);
但是要注意的是STM32F103的SDIO DMA每次由读数据变为写数据或者由写数据变为读数据时,都需要重新初始化DMA(主要是为了更改数据传输的方向)。
编写读写函数
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
HAL_StatusTypeDef SDIO_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
{HAL_StatusTypeDef Return_Status;HAL_SD_CardStateTypeDef SD_Card_Status;do{SD_Card_Status = HAL_SD_GetCardState(hsd);}while(SD_Card_Status != HAL_SD_CARD_TRANSFER );/* SDIO DMA DeInit *//* SDIO DeInit */HAL_DMA_DeInit(&hdma_sdio);/* SDIO DMA Init *//* SDIO Init */hdma_sdio.Instance = DMA2_Channel4;hdma_sdio.Init.Direction = DMA_PERIPH_TO_MEMORY;hdma_sdio.Init.PeriphInc = DMA_PINC_DISABLE;hdma_sdio.Init.MemInc = DMA_MINC_ENABLE;hdma_sdio.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;hdma_sdio.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;hdma_sdio.Init.Mode = DMA_NORMAL;hdma_sdio.Init.Priority = DMA_PRIORITY_LOW;if (HAL_DMA_Init(&hdma_sdio) != HAL_OK){Error_Handler();}__HAL_LINKDMA( hsd,hdmarx,hdma_sdio);Return_Status = HAL_SD_ReadBlocks_DMA( hsd,pData, BlockAdd, NumberOfBlocks);return Return_Status;
}HAL_StatusTypeDef SDIO_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
{HAL_StatusTypeDef Return_Status;HAL_SD_CardStateTypeDef SD_Card_Status;do{SD_Card_Status = HAL_SD_GetCardState(hsd);}while(SD_Card_Status != HAL_SD_CARD_TRANSFER );/* SDIO DMA DeInit *//* SDIO DeInit */HAL_DMA_DeInit(&hdma_sdio);/* SDIO DMA Init *//* SDIO Init */hdma_sdio.Instance = DMA2_Channel4;hdma_sdio.Init.Direction = DMA_MEMORY_TO_PERIPH;hdma_sdio.Init.PeriphInc = DMA_PINC_DISABLE;hdma_sdio.Init.MemInc = DMA_MINC_ENABLE;hdma_sdio.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;hdma_sdio.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;hdma_sdio.Init.Mode = DMA_NORMAL;hdma_sdio.Init.Priority = DMA_PRIORITY_LOW;if (HAL_DMA_Init(&hdma_sdio) != HAL_OK){Error_Handler();}__HAL_LINKDMA(hsd,hdmatx,hdma_sdio); Return_Status = HAL_SD_WriteBlocks_DMA(hsd,pData, BlockAdd, NumberOfBlocks);return Return_Status;
}
/* USER CODE END 0 */
main函数中直接调用读写函数即可
int main()
{
//****省略***///* 向SD卡块写入数据 */printf("------------------- Write SD card block data Test ------------------\r\n");SDIO_WriteBlocks_DMA(&hsd,Buffer_Tx, BLOCK_START_ADDR, NUM_OF_BLOCKS);printf("write status :%d\r\n",Return_Status);/* 读取SD卡块数据 */ Return_Status=SDIO_ReadBlocks_DMA(&hsd,Buffer_Rx, BLOCK_START_ADDR, NUM_OF_BLOCKS);printf("read status :%d\r\n",Return_Status);for(i = 0; i < sizeof(Buffer_Rx); i++){printf("0x%02x:%02x ", i, Buffer_Rx[i]);}while(1)
{
}}
编译无误后下载验证:
7、参考文献
STM32CubeMX学习笔记(26)——SDIO接口使用(读写SD卡)_cubemx sdio-CSDN博客
STM32CubeMX系列 | SD卡 - 知乎 (zhihu.com)