目录
1、准备材料
2、实验目标
3、轮询方式读取SD卡流程
3.0、前提知识
3.1、CubeMX相关配置
3.1.0、工程基本配置
3.1.1、时钟树配置
3.1.2、外设参数配置
3.1.3、外设中断配置
3.2、生成代码
3.2.0、配置Project Manager页面
3.2.1、外设初始化调用流程
3.2.2、外设中断调用流程
3.2.3、添加其他必要代码
4、烧录验证
5、中断方式读写SD卡流程简述
5.1、CubeMX相关配置
5.2、生成代码
5.3、烧录验证
6、DMA方式读写SD卡流程简述
6.1、CubeMX相关配置
6.2、生成代码
6.3、实验现象
7、常用函数
8、注释详解
参考资料
正点原子stm32f407探索者开发板V2.4
STM32CubeMX软件(Version 6.10.0)
keil µVision5 IDE(MDK-Arm)
ST-LINK/V2驱动
野火DAP仿真器
XCOM V2.6串口助手
使用STM32CubeMX软件配置STM32F407开发板SDIO读写4线SD卡,实现以轮询方式读写SD卡、以中断方式读取SD卡和以DMA方式读取SD卡
安全数码卡(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均需外部上拉,硬件原理图如下图所示
打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立”
当在STM32CubeMX中启用SDIO功能后,时钟树中48MHz时钟便可以进行调节,该时钟一般如其名字一样配置为48MHz即可,也即将Main PLL(主锁相环)的Q参数调节为7即可,其他HCLK、PCLK1和PCLK2时钟仍然设置为STM32F407能达到的最高时钟频率,具体如下图所示
本实验需要需要初始化开发板上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的引脚是否一致,具体配置如下图所示
轮询方式读写SD卡无需配置中断
单击进入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小节
在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卡的初始化频率,具体如下图所示
轮询方式读写SD卡无配置中断
笔者使用的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
在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));
}
}
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,按下KEY0按键会读取SD卡块5和块6的数据,按下KEY1按键会写入一段字符串到SD卡块5,写入块6从1-256整形数字,具体串口输出信息如下图所示
工程、时钟树、外设参数等配置与轮询方式读取SD卡一致,中断方式读取SD卡只需要在CubeMX软件中启动SDIO的全局中断
在Pinout & Configuration页面左边System Core/NVIC中勾选SDIO全局中断,然后选择合适的中断优先级即可,如下图所示
修改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卡的函数即可
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致
按下KEY0按键以中断方式读取SD卡的块5数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块5的数据
按下KEY1按键会以中断方式写入一段字符串到SD卡块5,写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块5中的数据
具体串口输出信息如下图所示
工程、时钟树、外设参数等配置与轮询方式读取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 全局中断,然后选择合适的中断优先级即可,如下图所示
修改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
在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卡的函数即可
烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致
按下KEY0按键以DMA的方式读取SD卡块6数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块6的数据
按下KEY1按键会以DMA的方式写入1-256的数字到SD卡块6(一个字节写入一个数字),写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块6中的数据
具体串口输出信息如下图所示
/*读块*/
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)
注释1:图片来源自 维基百科-SD卡
注释2:图片来源自 STM32F407中文参考手册 RM009
STM32Cube高效开发教程(高级篇)