1、准备材料
STM32CubeMX软件(Version 6.10.0)
keil µVision5 IDE(MDK-Arm)
逻辑分析仪nanoDLA
2、实验目标
使用STM32CubeMX软件配置STM32F407开发板SDIO读写4线SD卡,实现轮询方式读写SD卡、以中断方式读取SD卡和以DMA方式读取SD卡
3、轮询方式读取SD卡流程
3.0、前提知识
安全数码卡(Secure Digital Memory Card),简称SD卡,是嵌入式设备上常用的一种存储介质,通常可以将SD卡分为标准SD卡、miniSD卡和microSD卡(TF卡)三种类型,每种卡形状大小不一,除标准SD卡卡身上拥有一个写保护开关外,其他的功能三张卡一致,如今miniSD卡正逐渐被microSD卡所取代,如下图所示为三种不同类型SD卡形状 (注释1)
按照SD卡容量大小的不同可以将其分为SD、SDHC、SDXC等型号,按照SD卡读写机制速度的不同又可以将其分为Standard、High-speed、UHS-I等型号,具体如下表所示
STM32F407提供了一个SDIO接口可以直接通过HAL库来驱动1/4位总线宽度的SD卡或1/4/8位总线宽度的多媒体卡,其完全兼容SD卡规范版本2.0,但只支持高速SD卡,也即与SD卡进行数据传输最大速度为25MHz
SDIO由APB2接口和SDIO适配器两部分组成,SDIO适配器提供了驱动SD/MMC卡的全部功能,APB2接口则可以访问SDIO适配器寄存器在适当时候向内核发起中断/DMA请求
SDIO适配器由48MHz的SDIOCLK驱动,根据SDIOCLK时钟频率、 SDIO Clock divider bypass 参数和 SDIOCLK clock divide factor 参数就可以确定与SD卡通信时SDIO_CLK的时钟频率,当时钟分频器旁路使能时,SDIO_CLK=SDIOCLK;当时钟分频器旁路不使能时,SDIO_CLK=SDIOCLK / (2+时钟分频因子);
根据上面的描述,由于STM32F407的SDIO只支持高速SD卡,因此时钟分频器旁路常常不使能,这样当时钟分频因子为0时,SDIO_CLK则达到最大速度48MHz / 2 = 24Mhz,但在实际的使用中往往稍微降低该时钟频率,否则可能会出现读写SD卡失败的现象
另外值得提醒的是SD卡初始化的时候应该以不超过400KHz的速率,1位总线宽度进行初始化,否则将初始化失败
如下图所示为STM32F407内部的SDIO接口结构框图 (注释2)
笔者使用的开发板上SD卡槽设计为了4位总线宽度,在硬件设计时需要注意MCU与SD卡通信的1/4根数据线SDIO_D0/1/2/3和命令线SDIO_CMD均需外部上拉,硬件原理图如下图所示
3.1、CubeMX相关配置
3.1.0、工程基本配置
打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立”
3.1.1、时钟树配置
当在STM32CubeMX中启用SDIO功能后,时钟树中48MHz时钟便可以进行调节,该时钟一般如其名字一样配置为48MHz即可,也即将Main PLL(主锁相环)的Q参数调节为7即可,其他HCLK、PCLK1和PCLK2时钟仍然设置为STM32F407能达到的最高时钟频率,具体如下图所示
3.1.2、外设参数配置
本实验需要初始化开发板上WK_UP、KEY2、KEY1和KEY0用户按键,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”
本实验需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”
单击Pinout & Configuration页面左边功能分类栏目Connectivity/SDIO,将其模式选择为4位宽总线SD卡
Clock transition on which the bit capture is made (时钟跳变沿捕获数据配置):数据捕获边沿设置,可设置为上升沿/下降沿
SDIO Clock divider bypass (时钟分频器旁路使能):使能该参数时,SDIO_CLK=SDIOCLK;否则SDIO_CLK频率由时钟分频因子决定
SDIO Clock output enable when the bus is idle (空闲模式时钟输出使能):节能模式,此实验不使能
SDIO hardware flow control (硬件流控):设置是否使能SDIO的硬件流控,此处不使能
SDIOCLK clock divide factor (时钟分频因子):当不使能时钟分频器旁路时,SDIO_CLK=SDIOCLK / (2+时钟分频因子)
SDIO驱动4位宽总线SD卡的参数配置大多按照默认参数配置即可,但是要注意SD卡读写频率过高可能会导致读写失败,因此这里设置SD_CLK频率为8MHz,另外需要注意默认的SDIO复用引脚和开发板上的实际控制SD的引脚是否一致,具体配置如下图所示
3.1.3、外设中断配置
轮询方式读写SD卡无需配置中断
3.2、生成代码
3.2.0、配置Project Manager页面
单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示
详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节
3.2.1、外设初始化调用流程
在main.c文件main()函数中调用MX_SDIO_SD_Init()对SDIO参数配置,并调用HAL_SD_Init()函数对SD卡初始化,最后将SD卡切换到4位宽总线模式
在stm32f4xx_hal_sd.c文件HAL_SD_Init()中调用HAL_SD_MspInit()函数对SDIO时钟使能和所使用到的引脚功能复用,如果配置了中断或DMA,该函数中还会相应的出现NVIC/DMA相关配置,最后在真正的SD卡初始化函数HAL_SD_InitCard()中对SD卡初始化完毕
具体外设初始化函数调用流程如下图所示
初始化配置中时钟分频因子为4,SD_CLK=8MHz,为什么SD卡还可以初始化成功?
这里读者需要搞清楚真正对SD卡初始化时使用的参数配置是不是我们设置的参数,上面提到真正的SD卡初始化函数为HAL_SD_InitCard(),进入该函数发现实际初始化SD卡时用到的并不是用户配置的参数,而是使用的默认初始化参数,这里时钟分频因子被设置为了0x76,也即118,根据上面提到的公式计算可知48MHz / (118 + 2) = 400KHz,满足SD卡的初始化频率,具体如下图所示
3.2.2、外设中断调用流程
轮询方式读写SD卡无配置中断
3.2.3、添加其他必要代码
笔者使用的STM32CubeMX版本为6.10.0,在生成的SDIO初始化函数MX_SDIO_SD_Init()中需要将参数配置中的SD卡数据总线宽度从默认的4位手动修改为1位(STM32CubeMX软件BUG?),在SD卡初始化时应该以不超过400KHz的速率,1位总线宽度进行初始化,如果不修改这里SD卡将无法成功初始化,如下图所示
在sdio.c中添加SD卡读、写、擦除和输出SD卡信息测试函数
/*显示SD卡的信息*/
void SDCard_ShowInfo(void)
{
//SD卡信息结构体变量
HAL_SD_CardInfoTypeDef cardInfo;
HAL_StatusTypeDef res = HAL_SD_GetCardInfo(&hsd, &cardInfo);
if(res!=HAL_OK)
{
printf("HAL_SD_GetCardInfo() error\r\n");
return;
}
printf("\r\n*** HAL_SD_GetCardInfo() info ***\r\n");
printf("Card Type= %d\r\n", cardInfo.CardType);
printf("Card Version= %d\r\n", cardInfo.CardVersion);
printf("Card Class= %d\r\n", cardInfo.Class);
printf("Relative Card Address= %d\r\n", cardInfo.RelCardAdd);
printf("Block Count= %d\r\n", cardInfo.BlockNbr);
printf("Block Size(Bytes)= %d\r\n", cardInfo.BlockSize);
printf("LogiBlockCount= %d\r\n", cardInfo.LogBlockNbr);
printf("LogiBlockSize(Bytes)= %d\r\n", cardInfo.LogBlockSize);
printf("SD Card Capacity(MB)= %d\r\n", cardInfo.BlockNbr>>1>>10);
}
//获取SD卡当前状态
void SDCard_ShowStatus(void)
{
//SD卡状态结构体变量
HAL_SD_CardStatusTypeDef cardStatus;
HAL_StatusTypeDef res = HAL_SD_GetCardStatus(&hsd, &cardStatus);
if(res!=HAL_OK)
{
printf("HAL_SD_GetCardStatus() error\r\n");
return;
}
printf("\r\n*** HAL_SD_GetCardStatus() info ***\r\n");
printf("DataBusWidth= %d\r\n", cardStatus.DataBusWidth);
printf("CardType= %d\r\n", cardStatus.CardType);
printf("SpeedClass= %d\r\n", cardStatus.SpeedClass);
printf("AllocationUnitSize= %d\r\n", cardStatus.AllocationUnitSize);
printf("EraseSize= %d\r\n", cardStatus.EraseSize);
printf("EraseTimeout= %d\r\n", cardStatus.EraseTimeout);
}
/*SD卡擦除测试*/
void SDCard_EraseBlocks(void)
{
uint32_t BlockAddrStart=0;
uint32_t BlockAddrEnd=10;
printf("\r\n*** Erasing blocks ***\r\n");
if(HAL_SD_Erase(&hsd, BlockAddrStart, BlockAddrEnd)==HAL_OK)
printf("Erasing blocks,OK\r\n");
else
printf("Erasing blocks,fail\r\n");
HAL_SD_CardStateTypeDef cardState=HAL_SD_GetCardState(&hsd);
printf("GetCardState()= %d\r\n", cardState);
while(cardState != HAL_SD_CARD_TRANSFER)
{
HAL_Delay(1);
cardState=HAL_SD_GetCardState(&hsd);
}
printf("Blocks 0-10 is erased.\r\n");
}
/*SD卡写入测试函数*/
void SDCard_TestWrite(void)
{
printf("\r\n*** Writing blocks ***\r\n");
// BLOCKSIZE为512,在stm32f4xx_hal_sd.h中被定义
uint8_t pData[BLOCKSIZE]="Hello, welcome to UPC\0";
uint32_t BlockAddr=5;
uint32_t BlockCount=1;
uint32_t TimeOut=1000;
if(HAL_SD_WriteBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK)
{
printf("Write to block 5, OK\r\n");
printf("The string is: %s\r\n", pData);
}
else
{
printf("Write to block 5, fail ***\r\n");
return;
}
for(uint16_t i=0;i<BLOCKSIZE; i++)
pData[i]=i;
BlockAddr=6;
if(HAL_SD_WriteBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK)
{
printf("Write block 6, OK\r\n");
printf("Data in [10:15] is: ");
for (uint16_t j=11; j<=15;j++)
{
printf("%d,", pData[j]);
}
printf("\r\n");
}
else
printf("Write to block 6, fail ***\r\n");
}
/*SD卡读取测试函数*/
void SDCard_TestRead(void)
{
printf("\r\n*** Reading blocks ***\r\n");
uint8_t pData[BLOCKSIZE];
uint32_t BlockAddr=5;
uint32_t BlockCount=1;
uint32_t TimeOut=1000;
if(HAL_SD_ReadBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK)
//if(HAL_SD_ReadBlocks_IT(&hsd,pData,BlockAddr,BlockCount) == HAL_OK)
{
printf("Read block 5, OK\r\n");
printf("The string is: %s\r\n", pData);
}
else
{
printf("Read block 5, fail ***\r\n");
return;
}
BlockAddr=6;
if(HAL_SD_ReadBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut)== HAL_OK)
//if(HAL_SD_ReadBlocks_IT(&hsd,pData,BlockAddr,BlockCount) == HAL_OK)
{
printf("Read block 6, OK\r\n");
printf("Data in [10:15] is: ");
for (uint16_t j=11; j<=15;j++)
{
printf("%d,", pData[j]);
}
printf("\r\n");
}
}
在sdio.h中声明定义的这些测试函数
/*在sdio.h中声明*/
void SDCard_TestRead(void);
void SDCard_TestWrite(void);
void SDCard_ShowInfo(void);
void SDCard_EraseBlocks(void);
在main.c文件主循环中添加按键逻辑控制程序,WK_UP按键按下输出SD卡信息,KEY2按键按下擦除SD卡块0-10,KEY1按键按下测试SD卡写功能,KEY0按键按下测试SD卡读功能
具体源代码如下所示
/*WK_UP按键按下*/
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
SDCard_ShowInfo();
while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
}
}
/*KEY2按键按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
SDCard_EraseBlocks();
while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
}
}
/*KEY1按键按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
SDCard_TestWrite();
while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
}
}
/*KEY0按键按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
SDCard_TestRead();
while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
}
}
4、烧录验证
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,按下KEY0按键会读取SD卡块5和块6的数据,按下KEY1按键会写入一段字符串到SD卡块5,写入块6从1-256整形数字,具体串口输出信息如下图所示
5、中断方式读取SD卡流程简述
5.1、CubeMX相关配置
工程、时钟树、外设参数等配置与轮询方式读取SD卡一致,中断方式读取SD卡只需要在CubeMX软件中启动SDIO的全局中断
在Pinout & Configuration页面左边System Core/NVIC中勾选SDIO全局中断,然后选择合适的中断优先级即可,如下图所示
5.2、生成代码
修改STM32CubeMX工程重新生成工程代码后,读者应注意再次手动修改MX_SDIO_SD_Init()函数中SD卡数据总线宽度从默认的4位手动修改为1位
在sdio.c中增加以中断方式读写SD卡的测试函数,具体代码如下所示
/*SD卡中断写入测试函数*/
void SDCard_TestWrite_IT(void)
{
printf("\r\n*** IT Writing blocks ***\r\n");
uint32_t BlockCount=1;
uint16_t BlockAddr=5;
HAL_SD_WriteBlocks_IT(&hsd, TX, BlockAddr, BlockCount);
}
/*SD卡中断读取测试函数*/
void SDCard_TestRead_IT(void)
{
printf("\r\n*** IT Reading blocks ***\r\n");
uint32_t BlockCount=1;
uint16_t BlockAddr=5;
HAL_SD_ReadBlocks_IT(&hsd, RX, BlockAddr, BlockCount);
}
在sdio.c中新增加SD卡Tx/Rx传输完成回调函数HAL_SD_TxCpltCallback()和HAL_SD_RxCpltCallback(),具体代码如下所示
/*SD Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("IT Write to block 5, OK\r\n");
printf("The string is: %s\r\n", TX);
}
/*SD Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("IT Read block 5, OK\r\n");
printf("The string is: %s\r\n", RX);
}
在sdio.c中定义全部变量发送缓存数组TX和接收缓存数组RX,并在sdio.h中声明修改后的中断方式的SD卡写入测试函数和SD卡读取测试函数,源代码如下
/*sdio.c中定义的发送、接收缓存数组*/
uint8_t TX[BLOCKSIZE] = "Hello, welcome to UPC\0";
uint8_t RX[BLOCKSIZE];
/*sdio.h中对函数声明*/
void SDCard_TestRead_IT(void);
void SDCard_TestWrite_IT(void);
最后在main.c文件主循环中实现与轮询读写SD时一致的按键逻辑程序,并用修改后的以中断方式读写SD卡的函数替换以轮询方式读写SD卡的函数即可
5.3、烧录验证
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致
按下KEY0按键以中断方式读取SD卡的块5数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块5的数据
按下KEY1按键会以中断方式写入一段字符串到SD卡块5,写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块5中的数据
具体串口输出信息如下图所示
6、DMA方式读取SD卡流程简述
6.1、CubeMX相关配置
工程、时钟树、外设参数等配置与轮询方式读取SD卡一致,以DMA方式读取SD卡只需要在CubeMX软件中增加SDIO的DMA请求即可
在Pinout & Configuration页面单击Connectivity/SDIO页面,在Configuration配置页面中点击DMA Settings选项卡对SDIO的DMA进行配置,单击ADD增加SDIO的RX/TX两个DMA请求,SDIO的两个DMA请求除了内存地址递增可以设置外,其他的包括Mode、Use Fifo、Data Width和Burst Size等参数都不可以设置
对DMA参数不理解的可以阅读”STM32CubeMX教程12 DMA 直接内存读取“实验,SDIO的具体DMA配置参数如下图所示
在System Core/NVIC中勾选SDIO全局中断、DMA2 stream3 全局中断和 DMA2 stream6 全局中断,然后选择合适的中断优先级即可,如下图所示
6.2、生成代码
修改STM32CubeMX工程重新生成工程代码后,读者应注意再次手动修改MX_SDIO_SD_Init()函数中SD卡数据总线宽度从默认的4位手动修改为1位
在sdio.c中增加以DMA方式读写SD卡的测试函数,具体代码如下所示
/*SD卡DMA写入测试函数*/
void SDCard_TestWrite_DMA(void)
{
printf("\r\n*** DMA Writing blocks ***\r\n");
uint32_t BlockCount=1;
uint16_t BlockAddr=6;
for(uint16_t i=0;i<BLOCKSIZE; i++)
TX[i]=i;
HAL_SD_WriteBlocks_DMA(&hsd, TX, BlockAddr, BlockCount);
}
/*SD卡DMA读取测试函数*/
void SDCard_TestRead_DMA(void)
{
printf("\r\n*** DMA Reading blocks ***\r\n");
uint32_t BlockCount=1;
uint16_t BlockAddr=6;
HAL_SD_ReadBlocks_DMA(&hsd, RX, BlockAddr, BlockCount);
}
在sdio.h中对增加的函数声明
/*sdio.h中对函数声明*/
void SDCard_TestWrite_DMA(void);
void SDCard_TestRead_DMA(void);
DMA的回调函数使用的是外设的中断回调函数
当启用了SDIO TX DMA请求和SDIO全局中断,并以 HAL_SD_WriteBlocks_DMA() 写入SD卡块数据完成之后,会调用传输完成回调 HAL_SD_TxCpltCallback()
当启用了SDIO RX DMA请求和SDIO全局中断,并以 HAL_SD_ReadBlocks_DMA() 从SD卡块读取数据完毕之后,会调用读取完成回调函数 HAL_SD_RxCpltCallback()
故直接重新实现HAL_SD_RxCpltCallback/HAL_SD_TxCpltCallback两个函数即可,源代码如下所示
/*DMA Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA Write to block 6, OK\r\n");
printf("Data in [10:15] is: ");
for (uint16_t j=10; j<=15;j++)
{
printf("%d,", TX[j]);
}
printf("\r\n");
}
/*DMA Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA Read block 6, OK\r\n");
printf("Data in [10:15] is: ");
for (uint16_t j=10; j<=15;j++)
{
printf("%d,", RX[j]);
}
printf("\r\n");
}
最后在main.c文件主循环中实现与轮询读写SD时一致的按键逻辑程序,并用修改后的以DMA方式读写SD卡的函数替换以中断方式读写SD卡的函数即可
6.3、实验现象
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致
按下KEY0按键以DMA的方式读取SD卡块6数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块6的数据
按下KEY1按键会以DMA的方式写入1-256的数字到SD卡块6(一个字节写入一个数字),写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块6中的数据
具体串口输出信息如下图所示
7、常用函数
/*读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)
/*写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)
/*擦除块*/
HAL_StatusTypeDef HAL_SD_Erase(SD_HandleTypeDef *hsd, uint32_t BlockStartAdd, uint32_t BlockEndAdd)
/*获取SD卡信息*/
HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd, HAL_SD_CardInfoTypeDef *pCardInfo)
/*获取SD卡状态*/
HAL_StatusTypeDef HAL_SD_GetCardStatus(SD_HandleTypeDef *hsd, HAL_SD_CardStatusTypeDef *pStatus)
/*以中断方式读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
/*以中断方式写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
/*以DMA方式读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
/*以DMA方式写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
/*SD卡Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
/*SD卡Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
8、注释详解
注释1:图片来源自 维基百科-SD卡
注释2:图片来源自 STM32F407中文参考手册 RM009