目录
2)通用Disk IO函数通过指针指向器件的专用Disk IO函数
本文旨在详细介绍STM32单片机访问(阻塞模式、非阻塞模式)SD卡,和STM32单片机开启文件系统FATFS后访问SD卡,在私有应用函数里调用STM32CubeIDE自动生成的各类驱动函数的最佳路径和方法。选择不同的驱动函数,编程的路径和方法是不一样的,但正确编程的结果是一样的。
为了发布本文,本文作者已经发布相关文章4篇,为的是证明,选择不同的驱动函数,编程的路径和方法是不一样的。但一定有一种方法是最简单和最实用的。这些参考文章依次为:
参考文章1:细说STM32单片机使用轮询模式直接访问SD卡的方法及其应用-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/149218456?spm=1011.2415.3001.5331
参考文章2:细说STM32单片机使用DMA模式直接访问SD卡的方法及其应用_stm32 dma stream-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/149242997?spm=1011.2415.3001.5331
参考文章3:细说STM32单片机SD卡的FatFS文件系统并使用轮询模式访问SD卡的方法及其应用_bsp code for sd-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/149288991?spm=1011.2415.3001.5331
参考文章4:细说STM32单片机SD卡的FatFS文件系统并使用DMA模式访问SD卡的方法及其应用-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/149643115?spm=1011.2415.3001.5331
一、阻塞模式直接读写访问SD卡的流程
1、阻塞式访问SD卡的文件架构
所需要的函数都在这里,且只能是在这里。
2、展开这个文件,查看其内的函数
3、这些函数的使用方法
在自己的应用里,调用stm32f4xx_hal_sd.c里面的HAL_前缀或SD_前缀的函数。比如
二、DMA模式直接访问读写SD卡的流程
1、DMA访问SD卡的文件架构
所需要的函数都在这里,且只能是这里。直接访问无论何种模式文件位置都是一样的。
2、展开这个文件后,看其内的函数
这个函数与阻塞模式的函数架构一模一样:
3、这些函数的使用方法
在自己的应用里,调用stm32f4xx_hal_sd.c里面的HAL_前缀或SD_前缀的函数。与阻塞模式的区别是,DMA模式的函数,通过DMA转换完成后,应该重写回调函数,以证明读写完成。
比如:
/* HAL_SD_WriteBlocks_DMA(), Write SD card */
void SDCard_TestWrite_DMA() //DMA mode, test write
{
printf("\r\n*** DMA Writing blocks *** \r\n\r\n");
for(uint16_t i=0;i<BLOCKSIZE; i++)
SDBuf_TX[i]=i; //generate data
printf("Writing block 6. \r\n");
printf("Data in [10:15] is: %d ",SDBuf_TX[10]);
for (uint16_t j=11; j<=15;j++)
{
printf(", %d", SDBuf_TX[j]);
}
printf("\r\nHAL_SD_WriteBlocks_DMA() is to be called. \r\n");
uint32_t BlockAddr=6; //Block Address
uint32_t BlockCount=1; //Block Count
HAL_SD_WriteBlocks_DMA(&hsd,SDBuf_TX,BlockAddr,BlockCount); //can erase block automatically
}
回调函数是弱函数,需要重写的。
回调函数是弱函数,需要重写的。
/**
* @brief Rx Transfer completed callbacks
* @param hsd: Pointer SD handle
* @retval None
*/
__weak void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(hsd);
/* NOTE : This function should not be modified, when the callback is needed,
the HAL_SD_RxCpltCallback can be implemented in the user file
*/
}
重写后:
/* SD write callback func */
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA write complete. \r\n");
printf("HAL_SD_TxCpltCallback() is called. \r\n");
printf("Reselect menu item or reset. \r\n");
}
/* SD read callback func */
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
printf("DMA read complete. \r\n");
printf("HAL_SD_RxCpltCallback() is called. \r\n");
printf("Data in [10:15] is: %d\r\n",SDBuf_RX[10]);
for (uint16_t j=11; j<=15;j++)
{
printf(", %d", SDBuf_RX[j]);
}
printf("\r\nReselect menu item or reset. \r\n");
}
三、创建FATFS文件系统后访问SD卡的流程
如图,创建文件系统后,有3个路径访问SD卡,其中,
路径1,是根据IDE生成的文件,对SD卡及FATFS的初始化路径,是自动生成的,一般不需要编程;
路径2,是根据IDE生成的文件,利用FATFS的通用文件ff_c里通用函数,f_前缀,按照图示路径,访问SD卡;
路径3,是根据IDE生成的文件,利用FATFS的针对SD卡生成的专用驱动函数,SD_前缀、BSP_前缀,按照图示路径,访问SD卡;
四、创建FATFS文件系统并通过阻塞模式访问SD卡
1、文件架构
SD卡开启了FATFS后,会在文件树上,产生2个文件夹FATFS和MiddleWares:
(1)路径1,是2种方法的公共路径,用于初始化
fatfs.c/.h → ff_gen_drv.h/.c → sd_diskio.h/.c
具体:
main.c → 初始化所有配置过的外设,其中,MX_FATFS_Init(); → 在此函数里调用fatfs.c里的void MX_FATFS_Init(void),函数中只有一句,retSD = FATFS_LinkDriver(&SD_Driver, SDPath);这个函数调用ff_gen_drv.c里的FATFS_LinkDriver(),把SD_Driver(驱动器)和SDPath(磁盘路径)关联起来了,其中SD_Driver,在sd_diskio.c/.h里定义,SDPath在fatfs.c里定义。
(2)路径2,使用通用Disk IO函数
初始化后,可以使用路径2的方法操作SD卡,在自己的应用里,调用ff.c里的函数,比如f_read() → 进一步地,在f_read()里调用diskio.c/.h里的disk_为前缀的5个函数(至少1个)→ 通过指针映射到sd_diskio.c/.h。→ bsp_driver_sd.c → HAL_驱动。
具体:
先调用f_open()打开(创建)一个文件,然后再调用ff.c里的其他函数,比如f_read()、f_write(),最后调用f_close(&file);关闭文件。
1)以f_read()为例解析
APP → ff.c里的f_read() → diskio.c里的disk_read() → sd_diskio.c里的SD_read() → bsp_driver_sd.c里的BSP_SD_ReadBlocks() → stm32f4xx_hal_sd.c里HAL驱动HAL_SD_ReadBlocks()。
extern Disk_drvTypeDef disk;
/**
* @brief Reads Sector(s)
* @param pdrv: Physical drive number (0..)
* @param *buff: Data buffer to store read data
* @param sector: Sector address (LBA)
* @param count: Number of sectors to read (1..128)
* @retval DRESULT: Operation result
*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
res = disk.drv[pdrv]->disk_read(disk.lun[pdrv], buff, sector, count);
return res;
}
函数其中有两个重要的结构体,在ff_gen_drv.h里定义:
disk.drv 是一个指向磁盘驱动器数组的指针,pdrv 是物理驱动器号,用于索引到具体的驱动器。
disk.drv[pdrv] 获取该数组中索引(磁盘列表)为 pdrv 的驱动器。并且它的数据格式是Diskio_drvTypeDef,而结构体Diskio_drvTypeDef的一个成员就是disk_read()。表示为:
disk.drv[pdrv]->disk_read(),获取该驱动器的读取函数。
2)通用Disk IO函数通过指针指向器件的专用Disk IO函数
在sd_diskio.h中声明:
extern const Diskio_drvTypeDef SD_Driver;
在sd_diskio.c中定义结构体Diskio_drvTypeDef 的实体SD_Driver。
const Diskio_drvTypeDef SD_Driver =
{
SD_initialize,
SD_status,
SD_read,
#if _USE_WRITE == 1
SD_write,
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
SD_ioctl,
#endif /* _USE_IOCTL == 1 */
};
对应地,在diskio.h/.c中存在类似的声明和定义:
/*---------------------------------------*/
/* Prototypes for disk control functions */
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
DWORD get_fattime (void);
对应地,在ff_gen_drv.h中定义结构体Diskio_drvTypeDef:
/**
* @brief Disk IO Driver structure definition
*/
typedef struct
{
DSTATUS (*disk_initialize) (BYTE); /*!< Initialize Disk Drive */
DSTATUS (*disk_status) (BYTE); /*!< Get Disk Status */
DRESULT (*disk_read) (BYTE, BYTE*, DWORD, UINT); /*!< Read Sector(s) */
#if _USE_WRITE == 1
DRESULT (*disk_write) (BYTE, const BYTE*, DWORD, UINT); /*!< Write Sector(s) when _USE_WRITE = 0 */
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
DRESULT (*disk_ioctl) (BYTE, BYTE, void*); /*!< I/O control operation when _USE_IOCTL = 1 */
#endif /* _USE_IOCTL == 1 */
}Diskio_drvTypeDef;
于是,diskio.c里的函数就和sd_diskio.c里的函数一一对应起来了。
定义这个结构体变量实体内部成员,为器件的专用Disk IO函数,这样就通过指针,把通用Disk IO函数指向了器件专用Disk IO函数。
使用中间件里面生成的函数,去访问SD卡。这个方法的特点是操作函数,并不区分硬件是什么?
基础的行为函数,比如,创建、读、写、打开文件、关闭文件、等,f_前缀,f_putc。
例如:
void fatTest_ReadBinFile(TCHAR *filename)
{
FIL file;
FRESULT res=f_open(&file,filename, FA_READ);
if(res == FR_OK)
{
TCHAR str[50];
f_gets(str,50, &file); //Read 1 string
UINT bw=0; //The actual number of bytes read
uint32_t pointCount, sampFreq; //Save variables for reading data
f_read(&file, &pointCount, sizeof(uint32_t), &bw); //Number of data points
f_read(&file, &sampFreq, sizeof(uint32_t), &bw); //Sampling frequency
uint32_t value;
for(uint16_t i=0; i< pointCount; i++)
f_read(&file, &value, sizeof(uint32_t), &bw);
//USART display
printf("Reading BIN file: %s",str);
printf("Sampling freq: %ld\r\n",sampFreq);
printf("Point count: %ld\r\n",pointCount);
}
else if (res==FR_NO_FILE)
printf("File does not exist.\r\n");
else
printf("f_open() error.\r\n");
f_close(&file);
}
(3)路径3,使用SD专用Disk IO函数
在sd_diskio.c的沙箱里创建私有函数 → 调用sd_diskio.c里的SD_read() → bsp_driver_sd.c里的BSP_SD_ReadBlocks() → stm32f4xx_hal_sd.c里HAL驱动HAL_SD_ReadBlocks()。
/* SD_write(), Write SD card */
void SDCard_TestWrite() //DMA mode, test write
{
printf("\r\n*** Writing blocks *** \r\n\r\n");
for(uint16_t i=0;i<BLOCKSIZE; i++)
SDBuf_TX[i]=i; //generate data
printf("Writing block 6. \r\n");
printf("Data in [10:15] is: %d ",SDBuf_TX[10]);
for (uint16_t j=11; j<=15;j++)
{
printf(", %d", SDBuf_TX[j]);
}
printf("\r\n SD_write() is to be called. \r\n");
uint32_t BlockAddr=6; //Block Address
uint32_t BlockCount=1; //Block Count
BYTE lun={};
if(SD_write(lun,SDBuf_TX,BlockAddr,BlockCount) == RES_OK) //can erase block automatically
//SD_write(lun,SDBuf_TX,BlockAddr,BlockCount);
{
printf("SD_write complete. \r\n");
}
}
类似SD_write()、 SD_read()这样的函数,比如上面调用的SD_write()是否开启DMA,SD_write()的实现的代码是不一样的,但外函数的表象是一样的,所以类似上面的函数,无论在DMA模式还是在阻塞模式,都可以正常使用。
五、创建FATFS文件系统并通过DMA模式访问SD卡
1、文件架构
2、调用函数的流程
例如,在应用里给SD卡写操作,建立一个私有函数,这个私有函数要建立在sd_diskio.c里面。
SDCard_TestRead_DMA(), →在这个函数里调用SD_read() , → 在这个函数里调用BSP_SD_ReadBlocks_DMA(),这个函数是弱函数,是可以重写的,→ 在这个函数里调用HAL_SD_ReadBlocks_DMA();
3、回调函数的对应关系
调用BSP_SD_ReadBlocks_DMA()完成DMA转换后的回调函数void BSP_SD_ReadCpltCallback(void),在sd_diskio.c里面都写好了的。不需要用户再去重写。
/**
* @brief Rx Transfer completed callbacks
* @param hsd: SD handle
* @retval None
*/
void BSP_SD_ReadCpltCallback(void)
{
ReadStatus = 1;
}
调用HAL_SD_ReadBlocks_DMA()完成DMA转换后的回调函数void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)在bsp_driver_sd.c里面都写好了的。并且就是调用BSP_SD_ReadBlocks_DMA()的回调函数BSP_SD_ReadCpltCallback,不需要用户再去重写。
/**
* @brief Rx Transfer completed callback
* @param hsd: SD handle
* @retval None
*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
BSP_SD_ReadCpltCallback();
}
4、其它函数
- 如,SD_read、SD_write、SD_status、SD_ioctl、SD_initialize、SD_CheckStatusWithTimeout、SD_CheckStatusWithTimeout都经历同样的操作流程:APP里调用sd_diskio.c里面前缀为SD_的函数,→调用或重写bsp_driver_sd.c里面的前缀为BSP_的弱函数,其中的回调函数也是自动完成的→调用bsp_driver_sd.c里面的HAL_前缀的函数,其中的回调函数也是自动完成的,并且等效于BSP_前缀的回调函数。
- 除了上面的操作SD卡的流程,其他的函数,先在自己的应用里调用bsp_driver_sd.c,然后→调用HAL_前缀的函数。