本篇将使用CubeMX+Keil, 创建一个SD卡读写的工程。
目录
一、SD卡要点速读
二、SDIO要点速读
三、SD卡座接线原理图
四、CubeMX新建工程
五、CubeMX 生成 SD卡的SDIO通信部分
六、Keil 编辑工程代码
七、实验效果
一、SD卡 速读
SD卡,全称Secure Digital Memory Card(安全数码卡),是嵌入式设备上常用的一种存储介质。
1、尺寸大小 分类
按卡的大小分类,可以为3种:
- 标准SD卡 :体积较大,卡侧带写保护开关;常见于相机和摄像机中,用于存储高分辨率照片和视频;
- mini SD卡 :现在较少看到,已逐渐被microSD卡取代;
- Micro SD卡:旧称 TF卡,2004年更名为 Micro SD Card, 常用于扩展手机和平板电脑的存储空间。
每种卡形状大小不一,但功能一样:遵循相同的 SD卡协议、相同的命令集、相同的块大小(512)。只需确保SDIO引脚配置正确,并且遵循SD卡协议发送正确的命令,程序即可通用。
都是SD卡,但习惯上,标准SD叫SD卡,Micro SD叫TF卡。
目前,STM32开发板、Linux开发板 等,预留的卡座,一般是TF卡座,因为它占用空间最少。
2、卡的容量及标准 分类
在SD卡的表面丝印上,会有HC、XC等字样,表示它所使用的存储标准。
- SD: 早期的版本,基本停用,最高 2GB, 分区格式为 FAT12(FAT)、FAT16。
- SDHC:容量范围 2GB ~ 32GB, 分区格式为 FAT32。
- SDXC:容量范围 32GB ~ 2TB, 分区格式为 exFAT。
- SDUC:容量范围 2TB ~ 128TB, 分区格式为 exFAT。
3、SD卡的传输速度
SD卡的可变时钟频率:0~25MHz。当运行在25M+数据带宽4位时,最大理论传输速度是12.5MB/s。
而操作中,会明显低于理论速度,其受限于不同品牌的芯片优化、制造工芯、采用标准等。
SD卡是Flash存储,读写速度特点是:读快、写慢。
SD卡的最低写入速度,用Class等级来标识。
在表面丝印上,一般会有Class字样,它后面的数表示最低写入速度,单位是:MB/s。
或者,会用一个外面带半圆的数字表示。
- Class 2:2MB/s
- Class 4:4MB/s
- Class 6:6MB/s
- Class 10:10MB/s
附:常用的SD卡读写速率参考,非严谨值。
SD卡容量 | 文件系统 | 写入速度 | 读取速度 |
32G(SDHC) | FAT32 | 2MB/s | 8MB/s |
32G(SDHC) | exFAT | 3.5MB/s | 8.5MB/s |
64G(SDXC) | exFAT | 4MB/s | 8.5MB/s |
4、SD卡的使用寿命
一般是指:擦除的最大次数。
写入数据时需要先擦除扇区内容。读数据是不影响使用寿命的,写数据才会影响使用寿命。
因此,应避免频繁地对同一地址(扇区)进行写数据。如:使用程序每隔一秒保存一次数据到同一地址,这是不妥当的。
- TLC:1000~3000次
- MLC:3000 ~1万次
- SLC:可达10万次
擦写次数对使用寿命影响较小,而更容易直接“致死”的是:带电插拔,很容易坏卡,主要是静电原因!
二、SDIO要点速读
原理比较复杂,有兴趣的请自行csdn搜更详细的技术文档,或STM32的官方文档。
- SD卡的读写通信操作,可以用 SPI、SDIO,本示例使用SDIO。
- SDIO接口是在SD内存卡接口的基础上发展起来的;
- SDIO接口除了能读写SD内存卡,还能连接其它SDIO接口的设备;
- 常用的STM32F103C8,没有SDIO接口,F103系列R型号起,才带SDIO;
- STM32F4系列芯片,带更完善的SDIO主机接口,能与MMC卡、SD卡、SDI/0卡、EC-ATA设备进行通信;
- 三种总线模式:1-bit、4-bit、8-bit(不常用);
三、SD卡座接线原理图
STM32的SDIO外设与SD卡通信,通用接线如下图。
注:当使用弹簧式SD卡座,会有第9个脚(CD), 可不接。它用于判断SD卡是否插入,当插入SD卡时,此脚输出低电平。
四、CubeMX新建工程
建议复制一个已带UART1、printf的工程,这样更省时。
如果没有,可参考以下步骤。
1、新建一个普通的工程
新手可参考如下图解,老司机请直接跳过。
【STM32+CubeMX】 新建工程_STM32F407
2、为工程添加UART1通信、printf输出
用于把SD卡的测试信息,(通过USB转TTl),输出到串口助手观察。
如果,你已知晓如何通过printf输出信息,自行添加,跳过即可。
USART1 DMA发送、DMA空闲中断 接收不定长数据
UART1 快速实现移植、通信 ( bsp_UART.c 、bsp_UART.h)
五、CubeMX 配置 SD卡的SDIO 初始化
通过 CubeMX配置SDIO, 极度简单。
本节为方便测试,只使用普通的读写方式,后续篇章再添加DMA、FATFS等方式。
1、使能SDIO
- Mode:选择SD的四线模式,即 SD 4 bits Wide bus.
- 参数部分:F4系列不用修改配置,默认即可。F103系列,需把时钟分频系数修改为 6,即SDIOCLK Clock divide factor这一项,由默认0改为6, 不然会通信失败。
2、时钟设置
进入时钟树配置页面。
这时可能会弹出一个询问窗:是否自动配置所需时钟?
选择:NO ,手动修改即可。
如果Yes,它将针对已使能的SDIO进行必须值的配置,而系统时钟值,会被修改为其它值。不推荐。
F4系列,如果板上是25M的晶振,用如下参数值;要是8M的晶振,修改晶振、分频两处为8即可。
重点:箭头所指的Q值,它用于控制USB 、SDIO和随机数生成器的时钟。
这个时钟,必须是 48M !
好了,已完成配置。
重新生成工程,即可!
六、Keil 编辑工程代码
1、打开keil 工程,先重新编译一次。
- 正常情况,编译是0 Error的。
- 如果有Error, 应该是新建工程时,路径、名称有中文了,重新开建工程,用英文即可。
2、重要修改:SD卡的初始化,使用 1-bit 模式
CubeMX生成的SDIO初始化代码,有一个bug,需要手动修改,操作如下:
- 编译后,右击 main.c 文件中函数 MX_SDIO_SD_Init(),
- 在弹出菜单中:Go To Previous Reference To ...; 将跳转到函数内部;
跳转到 sdio.c文件的 MX_SDIO_SD_Init()函数内部后,
把下图位置中的 4B,改为 1B ;
它下面还有一个4B,不用修改,只修改刚才那个即可。不要改错位置了!
重点:每次重新生成后,都要手动修改一次。如果不修改,初始化过程会导致程序卡死。
3、编写测试代码
在 main函数内的 /* USER CODE BEGIN 2 */ 注释下方,编写以下代码(可复制):
代码就不解释了,已附详细注释,比较容易理解。
- 获取SD卡信息
- 读取测试块的原数据
- 写入测试
- 擦除测试
- 写回原数据
/***************** SD卡读写通信测试 *****************/
/* 1、获取卡信息,打印到串口助手 */
/* 2、读测试:读出测试位置原数据,保存在 aOldData[] */
/* 3、写测试:在测试的块上,写入指定数据 */
/* 读出刚才写入的块数据,打印到串口助手观察 */
/* 4、擦除测试:擦除指定块上的数据 */
/* 读出刚才擦除块的数据,打印到串口助手观察 */
/* 5、写回原数据到指定位置 */
/* 读出刚才写入的块数据,打印到串口助手观察 */
#define SD_TEST_SIZE 1024 // 测试数据的字节数,刚好是2个块大小:2x512
static uint8_t aOldData[SD_TEST_SIZE] = {0}; // 用于存放旧数据,先读出来,测试完了,再把旧数据写回去
static uint8_t aTestData[SD_TEST_SIZE] = {0}; // 临时缓存,用来存放测试数据
HAL_SD_CardInfoTypeDef pCardInfo = {0}; // SD卡信息结构体
uint8_t status = HAL_SD_GetCardState(&hsd); // SD卡状态标志值
if (status == HAL_SD_CARD_TRANSFER)
{
/* 1、获取卡信息,打印到串口助手 */
HAL_SD_GetCardInfo(&hsd, &pCardInfo); // 获取 SD 卡的信息
printf("\r1、获取SD卡信息 ... \r\n");
printf("卡类型:%d \r\n", pCardInfo.CardType); // 类型返回:0-SDSC、1-SDHC/SDXC、3-SECURED
printf("卡版本:%d \r\n", pCardInfo.CardVersion); // 版本返回:0-CARD_V1、1-CARD_V2
printf("块数量:%d \r\n", pCardInfo.BlockNbr); // 可用的块数量
printf("块大小:%d \r\n", pCardInfo.BlockSize); // 每个块的大小; 单位:字节
printf("卡容量:%lluG \r\n", ((unsigned long long)pCardInfo.BlockSize * pCardInfo.BlockNbr) / 1024 / 1024 / 1024); // 计算卡的容量
HAL_Delay(1000); // 重要:稍作延时再开始读写测试; 避免有些仿真器烧录期间的多次复位,短暂运行了程序,导致下列读写数据不完整。
/* 2、读测试:读出测试位置原数据,保存在 aOldData[] */
printf("\r2、读取测试块的原数据 ... \r\n");
memset(aOldData, 0, SD_TEST_SIZE); // 清0数组的数据
if (HAL_SD_ReadBlocks(&hsd, aOldData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 原数据
printf("%X ", aOldData[i]);
printf("\r\n");
}
else
{
printf("SD卡 读测试 失败!\n");
}
/* 3-1、写测试:在测试的块上写入数据 */
printf("\r3、SD卡 写入测试 ...\r\n");
memset(aTestData, 0x8, SD_TEST_SIZE); // 为数组准备要写入的测试数据:整个数组填充指定值
if (HAL_SD_WriteBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 向SD卡写入数据块; 参数:SD结构体、数据地址、块起始地址、写入的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
printf("对指定块写入结束! \r写入的数据是:\n");
for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 写入的数据
printf("%X ", aTestData[i]);
printf("\r\n");
}
else
{
printf("SD卡 写测试 失败!\n");
}
/* 3-2、读出刚才写测试的块内数据 */
printf("\r现在块内的数据是:\r\n");
memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据
if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 写入后块内现在数据
printf("%X ", aTestData[i]);
printf("\r\n");
}
else
{
printf("SD卡 读测试 失败!\n");
}
/* 4-1、擦除测试:擦除指定块上的数据 */
printf("\r4、擦除块测试 ...\r\n");
if (HAL_SD_Erase(&hsd, 7, 8) == HAL_OK) // 擦除SD卡上的数据; 参数:SD结构体、块的起始地址、块的结束地址
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
printf("擦除 成功! \r\n");
}
else
{
printf("擦除 失败! \r\n");
}
/* 4-2、读取,擦除后指定块上的数据 */
printf("擦除后,现在块内的数据是:\r\n");
memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据
if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 块内现在的数据
printf("%X ", aTestData[i]);
printf("\r\n");
}
else
{
printf("SD卡 读测试 失败!\n");
}
/* 5-1、写回测试块上的原数据 */
printf("\r5、写回原数据 ...\r\n");
//memset(aOldData, 1, SD_TEST_SIZE);
if (HAL_SD_WriteBlocks(&hsd, aOldData, 7, 2, 3000) == HAL_OK) // 向SD卡写入数据块; 参数:SD结构体、数据地址、块起始地址、写入的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
printf("写入结束! \n");
}
else
{
printf("SD卡 写回原数据 失败!\n");
}
/* 5-2、读取,写入后的数据 */
printf("现在块内的数据是: \r\n");
memset(aTestData, 0, SD_TEST_SIZE); // 清0数组的数据
if (HAL_SD_ReadBlocks(&hsd, aTestData, 7, 2, 3000) == HAL_OK) // 读SD卡数据块; 参数:SD结构体、数据地址、块起始地址、读的块数量、超时时间;
{
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); // 等待卡的读写操作结束
for (uint32_t i = 0; i < SD_TEST_SIZE; i++) // 打印 块内现在的数据
printf("%X ", aTestData[i]);
printf("\r\n\r\n");
}
else
{
printf("SD卡 读测试 失败! \r\n");
}
printf("SD卡 读写测试结束!\r\n");
}
完成后,位置如下图:
七、实验效果
程序运行后,串口助手输出如下:
如有错漏 ,望指正~~~!