CubeMX中已经集成了FatFS库,生成代码时能够自动生成SD卡驱动并整合到FatFS中,移植起来非常的方便。下面以SD卡为例,讲解移植方法。

CubeMX配置

SDIO配置

首先需要配置SDIO,通常来讲都是按照下图的模板进行配置:
屏幕截图 2024-12-27 154949.png
其中Mode可以自行选择使用1条或4条数据线。下方的Parameter Steeings一般不需要修改。

检测引脚配置

SD卡有一个检测引脚,这个可以连接到STM32的任意引脚上,我连接到了PC7,因此需要配置PC7引脚为输入模式。按照规格书,这个检测引脚为低电平有效,因此如果没有外置的上拉电阻,需要配置为上拉模式。这里我已经在外部接了上拉电阻,因此配置为无上下拉。
屏幕截图 2024-12-27 155706.png

FatFS配置

在左侧的Middleware中选择FATFS,会进入FATFS配置界面:
屏幕截图 2024-12-27 155907.png
在这里可以选择SD卡或者用户定义的设备(一般为FLASH或EEPROM),这里勾选SD Card
下方可以配置FatFS的各项参数,其实就和修改ffconf.h的宏定义一样,各选项的作用可查看野火或网上的教程。
如果你和我一样,使用了SD卡检测引脚,那么还需要配置Platform Settings,选择检测引脚:
屏幕截图 2024-12-27 160234.png

至此,CubeMX配置完成,生成代码即可。

代码讲解

文件结构

打开生成的工程,会发现其中与FatFS相关的文件有这些:
屏幕截图 2024-12-27 161005.png
涉及底层驱动实现的文件主要有以下三个:

ff_gen_drv.c:FATFS提供的通用驱动文件的实现

/**
  * @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;

/**
  * @brief  Global Disk IO Drivers structure definition
  */
typedef struct
{
  uint8_t                 is_initialized[_VOLUMES];
  const Diskio_drvTypeDef *drv[_VOLUMES];
  uint8_t                 lun[_VOLUMES];
  volatile uint8_t        nbr;

}Disk_drvTypeDef;

/* Exported functions ------------------------------------------------------- */
uint8_t FATFS_LinkDriver(const Diskio_drvTypeDef *drv, char *path);
uint8_t FATFS_UnLinkDriver(char *path);
uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, BYTE lun);
uint8_t FATFS_UnLinkDriverEx(char *path, BYTE lun);
uint8_t FATFS_GetAttachedDriversNbr(void);

sd_diskio.c:针对SD底层驱动实现,封装成为通用的底层驱动API

//sd_diskio.c文件内容
const Diskio_drvTypeDef  SD_Driver = {
  SD_initialize,    //指向diskio.c中的disk_initialize()
  SD_status,    //指向diskio.c中的disk_status()
  SD_read,    //指向diskio.c中的disk_read()
#if  _USE_WRITE == 1
  SD_write,    //指向diskio.c中的disk_write()
#endif /* _USE_WRITE == 1 */  
#if  _USE_IOCTL == 1
  SD_ioctl,    //指向diskio.c中的disk_ioctl()
#endif /* _USE_IOCTL == 1 */
};    //定义Diskio_drvTypeDef类型的SD_Driver

/* -------------------以上各函数的实现---------------------------*/
DSTATUS SD_initialize(BYTE lun){
  Stat = STA_NOINIT;  
  if(BSP_SD_Init() == MSD_OK){
    Stat &= ~STA_NOINIT;
  }
  return Stat;
}

DSTATUS SD_status(BYTE lun){
  Stat = STA_NOINIT;
  if(BSP_SD_GetCardState() == MSD_OK){
    Stat &= ~STA_NOINIT;
  }
  return Stat;
}

DRESULT SD_read(BYTE lun, BYTE *buff, DWORD sector, UINT count){
  DRESULT res = RES_ERROR;
  uint32_t timeout = 100000;
  if(BSP_SD_ReadBlocks((uint32_t*)buff,(uint32_t) (sector), count, SD_DATATIMEOUT) == MSD_OK){
    while(BSP_SD_GetCardState()!= MSD_OK){
      if (timeout-- == 0)
        return RES_ERROR;
    }
    res = RES_OK;
  } 
  return res;
}

#if _USE_WRITE == 1
DRESULT SD_write(BYTE lun, const BYTE *buff, DWORD sector, UINT count){
  DRESULT res = RES_ERROR;
  uint32_t timeout = 100000;
  if(BSP_SD_WriteBlocks((uint32_t*)buff,(uint32_t)(sector),count, SD_DATATIMEOUT) == MSD_OK){
    while(BSP_SD_GetCardState()!= MSD_OK){
      if (timeout-- == 0)
        return RES_ERROR;
    }    
    res = RES_OK;
  }
  return res;
}
#endif /* _USE_WRITE == 1 */

#if _USE_IOCTL == 1
DRESULT SD_ioctl(BYTE lun, BYTE cmd, void *buff){
  DRESULT res = RES_ERROR;
  BSP_SD_CardInfo CardInfo;  
  if (Stat & STA_NOINIT) return RES_NOTRDY;  
  switch (cmd){
  /* Make sure that no pending write process */
  case CTRL_SYNC :
    res = RES_OK;
    break;
  /* Get number of sectors on the disk (DWORD) */
  case GET_SECTOR_COUNT :
    BSP_SD_GetCardInfo(&CardInfo);
    *(DWORD*)buff = CardInfo.LogBlockNbr;
    res = RES_OK;
    break;
  /* Get R/W sector size (WORD) */
  case GET_SECTOR_SIZE :
    BSP_SD_GetCardInfo(&CardInfo);
    *(WORD*)buff = CardInfo.LogBlockSize;
    res = RES_OK;
    break;
  /* Get erase block size in unit of sector (DWORD) */
  case GET_BLOCK_SIZE :
    BSP_SD_GetCardInfo(&CardInfo);
    *(DWORD*)buff = CardInfo.LogBlockSize;
    res = RES_OK;
    break;
  default:
    res = RES_PARERR;
  } 
  return res;
}
#endif /* _USE_IOCTL == 1 */

bsp_driver_sd.c:HAL库的二次封装,将基于SD卡的操作都在该文件下实现

//bsp_driver_sd.h文件内容
uint8_t BSP_SD_Init(void);
uint8_t BSP_SD_ITConfig(void);
void    BSP_SD_DetectIT(void);
void    BSP_SD_DetectCallback(void);
uint8_t BSP_SD_ReadBlocks(uint32_t *pData, uint32_t ReadAddr, uint32_t NumOfBlocks, uint32_t Timeout);
uint8_t BSP_SD_WriteBlocks(uint32_t *pData, uint32_t WriteAddr, uint32_t NumOfBlocks, uint32_t Timeout);
uint8_t BSP_SD_ReadBlocks_DMA(uint32_t *pData, uint32_t ReadAddr, uint32_t NumOfBlocks);
uint8_t BSP_SD_WriteBlocks_DMA(uint32_t *pData, uint32_t WriteAddr, uint32_t NumOfBlocks);
uint8_t BSP_SD_Erase(uint32_t StartAddr, uint32_t EndAddr);
void BSP_SD_IRQHandler(void);
void BSP_SD_DMA_Tx_IRQHandler(void);
void BSP_SD_DMA_Rx_IRQHandler(void);
uint8_t BSP_SD_GetCardState(void);
void    BSP_SD_GetCardInfo(HAL_SD_CardInfoTypeDef *CardInfo);
uint8_t BSP_SD_IsDetected(void);
void    BSP_SD_AbortCallback(void);
void    BSP_SD_WriteCpltCallback(void);
void    BSP_SD_ReadCpltCallback(void);

其余文件都是FatFS中的,其作用可查看相关教程。

初始化流程

为了更灵活的使用生成的代码,我们需要对其初始化流程有一个大致的了解。
查看生成的main函数,发现其仅仅调用了这两个初始化函数:

MX_SDIO_SD_Init();
MX_FATFS_Init();

MX_SDIO_SD_Init()函数在main.c文件中定义:

static void MX_SDIO_SD_Init(void)
{
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B; //这里一定要改为1B
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 0;
}

此函数主要对hsd结构体进行了一些初始化配置,但是没有应用。如果你之前生成过不含FatFS的SD代码,你会发现这里少了HAL_SD_Init()HAL_SD_ConfigWideBusOperation(),它们被搬到了bsp_driver_sd.c中,后面会讲到。

注意:这里的hsd.Init.BusWide一定要改为1B,Cube自动生成的参数是4B
这里就不得不说一下HAL库初始化过程中对SDIO配置的次数了。目前我查看代码得到的是2次:
第一次,HAL_SD_Init()函数中,使用默认的400khz、1B,进行CMD命令初始化。
第二次,CMD命令初始化完成后,在HAL_SD_ConfigWideBusOperation()函数中,时钟频率和宽度修改为cubemx中的设置。

MX_FATFS_Init()函数在fatfs.c文件中定义:

void MX_FATFS_Init(void)
{
  /*## FatFS: Link the SD driver ###########################*/
  retSD = FATFS_LinkDriver(&SD_Driver, SDPath);
}

递归的找下去,实际是调用了这个函数:

uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, uint8_t lun)
{
  uint8_t ret = 1;
  uint8_t DiskNum = 0;

  if(disk.nbr < _VOLUMES)
  {
    disk.is_initialized[disk.nbr] = 0;
    disk.drv[disk.nbr] = drv;
    disk.lun[disk.nbr] = lun;
    DiskNum = disk.nbr++;
    path[0] = DiskNum + '0';
    path[1] = ':';
    path[2] = '/';
    path[3] = 0;
    ret = 0;
  }

  return ret;
}

这部分初始化了路径和磁盘号相关参数。
注意:到这里为止,SDIO外设还没有完成初始化,因为只对结构体参数进行了配置,并没有实际应用

CubeMX生成的代码,将SDIO的初始化放到了FatF的挂载部分,当执行f_mount()挂载函数时,会调用到find_volume(),而后调用disk_initialize()。函数内容如下:

DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber to identify the drive */
)
{
  DSTATUS stat = RES_OK;

  if(disk.is_initialized[pdrv] == 0)
  {
    stat = disk.drv[pdrv]->disk_initialize(disk.lun[pdrv]);//这里通向初始化函数
    if(stat == RES_OK)
    {
      disk.is_initialized[pdrv] = 1;
    }
  }
  return stat;
}

disk.drv[pdrv]->disk_initialize(disk.lun[pdrv])指向了定义在`sd_diskio.c文件中的SD_initialize()函数,其内容如下:

DSTATUS SD_initialize(BYTE lun)
{
Stat = STA_NOINIT;

#if !defined(DISABLE_SD_INIT)

  if(BSP_SD_Init() == MSD_OK)
  {
    Stat = SD_CheckStatus(lun);
  }

#else
  Stat = SD_CheckStatus(lun);
#endif

  return Stat;
}

其中调用了BSP_SD_Init(),其内容如下:

__weak uint8_t BSP_SD_Init(void)
{
  uint8_t sd_state = MSD_OK;
  /* Check if the SD card is plugged in the slot */
  if (BSP_SD_IsDetected() != SD_PRESENT)
  {
    return MSD_ERROR;
  }
  /* HAL SD initialization */
  sd_state = HAL_SD_Init(&hsd);
  /* Configure SD Bus width (4 bits mode selected) */
  if (sd_state == MSD_OK)
  {
    /* Enable wide operation */
    if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)
    {
      sd_state = MSD_ERROR;
    }
  }

  return sd_state;
}

可以了,我们看到这里总算调用了两个SDIO初始化函数,至此,我们的SD设备初始化完成。

移植现有工程

根据我们之前的分析,将生成的代码移植到现有工程,需要移植FatFS文件和SD驱动文件。另外,由于在SDIO初始化时会调用MX相关函数,因此stm32f4xx_hal_msp.c文件中的SDIO部分也需要移植。

复制文件

将生成的代码目录下的./FATFS/App ./FATFS/Target ./Middlewares/Third_Party/FatFs/src复制到目标工程目录下的FATFS文件夹中。
Keil中新建Group,命名为FATFS,添加相关文件:
屏幕截图 2024-12-27 171158.png
添加include路径(参考自己的路径修改):
屏幕截图 2024-12-27 171319.png
注释掉ffconf.h中的includes,防止编译错误(自动生成的工程不会报错,原因未知。报错是由于include顺序造成的,提示找不到相关定义):
屏幕截图 2024-12-27 171737.png
此时应该就可以正常编译并运行了,附上一份测试程序,需要启用MicroLIB

void Fatfs_test(void)
{
  uint32_t byteswritten;                /* File write counts */
    uint32_t bytesread;                   /* File read counts */
    uint8_t wtext[] = "This is STM32 working with FatFs"; /* File write buffer */
    uint8_t rtext[100];                   /* File read buffers */
    char filename[] = "STM32cube.txt";
    char SensorBuff[100];
    printf("********* STM32CubeMX FatFs Example *********\r\n\r\n");    
    if(f_mount(&SDFatFS,SDPath,1) == FR_OK){
        printf("f_mount sucess!!! \r\n");
        if(f_open(&SDFile,filename,FA_CREATE_ALWAYS|FA_WRITE) == FR_OK){
              printf("f_open file sucess!!! \r\n");
            if(f_write(&SDFile,wtext,sizeof(wtext),&byteswritten) == FR_OK){
                printf("f_write file sucess!!! \r\n");
                printf("f_write Data : %s\r\n",wtext);
                if(f_close(&SDFile) == FR_OK)
                    printf("f_close sucess!!! \r\n");
                else
                    printf("f_close error : %d\r\n",retSD);
              }
            else
                printf("f_write file error\r\n");    
        }
        else
            printf("f_open file error\r\n");  
      }else
        printf("f_mount error : %d \r\n",retSD);
        
    retSD = f_open(&SDFile, filename, FA_READ);
    if(retSD)
        printf("f_open file error : %d\r\n",retSD);
    else
        printf("f_open file sucess!!! \r\n");

    retSD = f_read(&SDFile, rtext, sizeof(rtext), (UINT*)&bytesread);
    if(retSD)
        printf("f_read error!!! %d\r\n",retSD);
    else{
        printf("f_read sucess!!! \r\n");
        printf("f_read Data : %s\r\n",rtext);
    }

    retSD = f_close(&SDFile);
    if(retSD)
        printf("f_close error!!! %d\r\n",retSD);
    else
        printf("f_close sucess!!! \r\n");

    if(bytesread == byteswritten)
        printf("FatFs is working well!!!\r\n");

    if(f_open(&SDFile,(const char*)"Sensor.csv",FA_CREATE_ALWAYS|FA_WRITE) == FR_OK){
        printf("Sensor.csv was opened/created!!!\r\n");
        sprintf(SensorBuff, "Item,Temp,Humi,Light\r\n");
        f_write(&SDFile,SensorBuff,strlen(SensorBuff),&byteswritten);
    
        for(int i = 0; i < 10; i++){
            sprintf(SensorBuff, "%d,%d,%d,%d\r\n",i + 1, i + 20, i + 30, i + 40);
            f_write(&SDFile,SensorBuff,strlen(SensorBuff),&byteswritten);
            f_sync(&SDFile);
        }
        f_close(&SDFile);
  }
}

直接调用Fatfs_test()即可。
如果你发现STM32cube.txt文件打开失败,是由于FatFs默认没有开启长文件名支持导致的。需要将_USE_LFN设置为0以外的数值,比如2。
设置完后,你会发现编译报错,缺少两个函数定义。这是缺少大小写转换函数,你需要把_CODE_PAGE改为936并导入源码中的cc936.c文件。

参考文章:
STM32CubeMX系列|FATFS文件系统
STM32F1 HAL库读写SD卡的操作要点
如何设置FatFs文件系统支持长文件名