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)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐