一、所用材料
- STM32F407ZGT6
- SD卡
二、实现功能
当我们需要存储的数据量非常庞大时,STM32F407ZGT6自带的192K字节的SRAM或是一些板子上配备的外部SRAM内存不够用时,可以存入SD卡中。本代码实现的功能是利用代码中的一个小数组去接收数据,当数据个数达到数组的最大个数时,会按顺序将数据存放在SD卡相应的块上,并准备进行下一次的存储。
三、实现过程
3.1 CubeMX
1、基础配置
2、配置RCC
与以往不同的是SDIO适配器的时钟是单独配置的,需要专用的SDIOCLK,标准工作在48MHz。
3、配置SDIO
3.2 KEIL
1、更改代码
在生成的SDIO初始化函数MX_SDIO_SD_Init()中需要将参数配置中的SD卡数据总线宽度从默认的4位手动修改为1位,在SD卡初始化时应该以不超过400KHz的速率,1位总线宽度进行初始化,如果不修改这里SD卡将无法成功初始化。我第一次尝试的时候程序一直跑不通都找不到原因,最后网上找了很久才知道是这个原因。
2、添加代码
在main.c中添加获取SD信息并打印的代码,测试单片机与SD卡是否已经连接好。
HAL_SD_GetCardInfo(&hsd, &pCardInfo);
printf("pCardInfo.CardType = %u\r\n",pCardInfo.CardType); //SD卡类型
printf("pCardInfo.CardVersion = %u\r\n",pCardInfo.CardVersion); //版本
printf("pCardInfo.BlockNbr = %u\r\n",pCardInfo.BlockNbr); //SD卡块数
printf("pCardInfo.BlockSize = %u\r\n",pCardInfo.BlockSize); //每一块大小
printf("\r\n");
倘若可以正常输出信息,则可以进行下一步的SD卡读写的试验。这边的定义简单说明一下,因为SD卡的一个块中有512个字节,所以为了方便起见,我定义了一个八位的数组,即一个数据占一个字节的内存,可存储0-255之间的数据。而我又将数组中的数据个数定义为512,那么当一个数组的数据存满时,一整个数组的内存大小正好是512个字节,与SD卡中一个块的大小相同。
#define BLOCK_SIZE 512 //一个块的字节数
#define NUMBERS_PER_CHUNK 512 //多少个数据存一次
#define INT_SIZE 1 //一个数值占几个字节
#define BUFFER_SIZE (NUMBERS_PER_CHUNK * INT_SIZE) //存一次sd卡的数组的大小(其实就是512)
uint8_t buffer_TX[NUMBERS_PER_CHUNK]; // 用于暂时存储需要发送到SD卡的数据
uint8_t buffer_RX[NUMBERS_PER_CHUNK]; // 用于接收从SD卡中读取来的数据
在测试开始前这边需要先介绍一下SD卡的写入函数,读取函数同理。
HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)
1、*hsd: 这是一个指向SD卡句柄(SD_HandleTypeDef)的指针,该句柄包含了SD卡操作所需的配置和状态信息。通常在程序初始化阶段会对这个句柄进行配置,以设置SD卡的通信参数等。2、*pData: 这是一个指向数据缓冲区的指针,该缓冲区存储着要写入SD卡的数据。通常这是一个字节数组。
3、BlockAdd: 这个参数指定了写入操作的起始块地址。在SD卡中,数据存储是按块进行的,每个块通常有512字节。这里的数值表示从SD卡中的第几个块开始写入。
3、NumberOfBlocks:这个参数指定了要写入的块的数量。
4、Timeout:这是超时值,单位是毫秒。这个参数定义了操作的最长等待时间。如果在1000毫秒内操作没有完成,则函数返回一个超时错误。
接下来是SD卡的写入测试 ,以下代码实现的是先在buffer_TX[ ]数组中存满0-255的数字,存满512个字节后放到SD的第一个块上,然后chunk+1,下一次就将存储到SD卡的下一个块中。
uint16_t blockNum = 0; // 起始块号
/*--------------------------SD卡写测试----------------------------------*/
for (int chunk = 0; chunk < 2; chunk++)
{
// 填充缓冲区
for (int i = 0; i < NUMBERS_PER_CHUNK; i++)
{
buffer_TX[i] = i/2;
printf("%d ",buffer_TX[i]);
}
// 计算写入的块数量
uint32_t blocksToWrite = BUFFER_SIZE / BLOCK_SIZE; //(512*1=512)
// 将数据写入SD卡
if (HAL_SD_WriteBlocks(&hsd, (uint8_t *)buffer_TX, blockNum, 1, 10000) == HAL_OK)
{
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); //处于传输状态退出
printf("\r\nWriteBlocks Successfully\r\n");
}
else
{
printf("WriteBlocks Failed\n");
}
blockNum += 1; // 更新块号以写入下一个段
}
如果运行顺利的话,你的串口调试助手中将会收到以下的内容。
接下来就是就是从SD卡中读出数据,原理与SD卡的写入相同。
blockNum = 0; // 起始块号
for (int chunk = 0; chunk < 2; chunk++)
{
// 计算读取的块数量
uint32_t blocksToRead = BUFFER_SIZE / BLOCK_SIZE;
// 从SD卡读取数据
if (HAL_SD_ReadBlocks(&hsd, (uint8_t *)buffer_RX, blockNum, 1, 1000) == HAL_OK)
{
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER);//返回到传输状态退出
printf("ReadBlocks Successfully\r\n");
for (int i = 0; i < NUMBERS_PER_CHUNK; i++)
{
printf("%d ", buffer_RX[i]);
}
}
else
{
printf("WriteBlocks Failed\n");
}
blockNum += 1; // 更新块号以读取下一个段
}
如果运行顺利的话 ,你的串口调试助手中将会收到以下的内容。说明已经从SD卡的相应块中读出了刚才存入的数据。
3、代码拓展
场景为智能小车在循迹时,需要将运动时产生的数据记录下来,并在需要时复刻出循迹时的数据,源代码的实现是有一个定时器中断,即每过一段相应的时间,摄像头就向单片机传输一次数据。以下是需要用到的存取数据的代码。
/*-----------------写入函数-----------------*/
void write_sd (int put)
{
if(i<NUMBERS_PER_CHUNK){
buffer_TX[i] = put;
// printf("%d,", buffer_TX[i]);
i++;
}
else
{
HAL_SD_GetCardInfo(&hsd, &pCardInfo);
if (HAL_SD_WriteBlocks(&hsd, (uint8_t *)buffer_TX, blockNum, 1, 10000) == HAL_OK)
{
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); //处于传输状态退出
printf("WriteBlocks Successfully\r\n");
}
i=0;
buffer_TX[i] = put;
blockNum += 1;
}
}
/*-----------------读取函数-----------------*/
void read_sd(void)
{
// 从SD卡读取数据
if (HAL_SD_ReadBlocks(&hsd, (uint8_t *)buffer_RX, blockNum, 1, 1000) == HAL_OK)
{
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER);//返回到传输状态退出
printf("ReadBlocks Successfully\r\n");
}
blockNum+= 1; // 更新块号以读取下一个段
}
整个代码需要实现的效果是,每一次定时器中断都往buffer_TX中存入一个数据,当buffer_TX数组被填满时,就存放入SD卡中。当小车停下时就将未存满的buffer_TX直接放入相应的块中,并更新块号,便于存储多套数据。而读取数据要做的是,每当buffer_TX数组内的数据被使用完时,就重新从SD卡的下一块中读取新的一组数据。
HAL_SD_CardInfoTypeDef pCardInfo;//定义结构体用来接收卡信息
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == htim6.Instance)
{
if(flag=1) //存入模式,speed_dif 为任意你获取的并想存入SD卡的数据
{
write_sd(speed_dif);
else if(stop == 1) //当数据采集停止时
{
if(first_stop == 1) //达到终点时要将没有集满SD一个块的数据仍存放进SD卡中
{
if (HAL_SD_WriteBlocks(&hsd, (uint8_t *)buffer_TX, blockNum, 1, 10000) == HAL_OK)
{
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); //处于传输状态退出
printf("WriteBlocks Successfully\r\n");
}
first_stop = 0;
blockNum += 1; //从下一块重新开始记录
blockdata_first = blockNum; //记下当前记录到的块数
i = 0; //从0开始
}
}
}
if(flag=2) //读出模式
{
if(flag2_stop == 0)
{
if(first_flag2) //如果刚到读出模式,则读取一次数据
{
read_sd();
first_flag2 = 0;
}
printf("%d ", buffer_RX[j]); //把读出来的数据给电机
j++;
if(buffer_RX[j-1]==0&&buffer_RX[j]==0&&buffer_RX[j+1]==0) //判断SD卡中保存的几组数据是否读完,当读到一连串的0时,说明数据没有保存到这个为止,即数据读取完毕,读出模式可以结束
flag2_stop = 1;
}
}
}
}
这部分代码因为应用的场景比较特殊,大家需要根据自己的需求更改其中的变量以及内容,这里只能算是提供一种思路上的参照,具体的配置和逻辑还是需要大家自行判断。
四、结语
这篇博客主要是趁自己还有印象的时候记录一下前几天配置SD卡的过程,因为前几天做的一题需要将循迹小车的运动轨迹复刻下来,所以临时学习了SD卡的存储,配置和写代码的过程中花费了很长的时间,所以感觉有必要在博客中记录一笔,也便于日后自己翻阅。文章没有深入探讨SD卡存取的底层逻辑,只是一个使用的方案,大家如果有什么问题,或者这篇博客有什么不足,欢迎讨论哦。