当前位置: 首页 > news >正文

【CubeMX+STM32】SD卡 U盘文件系统 USB+FATFS

本篇,将使用CubeMX+Keil,  创建一个 USB+TF卡存储+FatFS 的虚拟U盘读写工程。

目录

一、简述

二、CubeMX 配置  SDIO + DMA + FatFs + USB

三、Keil 编辑代码

四、实验效果


串口助手,实现效果:

U盘,识别效果:


一、简述

上几篇,已循序渐进讲解了SD、SDIO、基础读写、DMA读写、FatFs。

这里不再啰嗦,有兴趣的可翻看之前的篇章:

1、【CubeMX+STM32】SD卡 基础读写

2、【CubeMX+STM32】SD卡 DMA读写

3、【CubeMX+STM32】SD卡 文件系统 FatFs+SDIO+DMA

本篇,再向前一步。

通过 CubeMX+Keil ,把SD卡模拟成U盘 ( SDIO+DAM+FatFs+USB),可以用代码进行文件读写,也可以插上USB线在电脑端进行文件读写。

对USB详细原理有兴趣的朋友,可参考大神的书籍《USB应用分析精粹》。

SD卡的接线原画图:

带SDIO的STM32芯片,都是一样的接法,不管是F1、F4。

对于第9脚:检测插入,文中另有详述。

USB端口接线原理图:

本篇所用电路, 如下图,也是STM32作为设备端的通用接法。

  • Type-C母座已连接STM32的PA11、PA12,  且PA12 (DP线) 经1.5K电阻上拉到3.3V。
  •  优点:电路简单;缺点:只能作设备端,不能作主机端。

注意:如果板子上有两个USB口,注意区分哪个是USB设备端口,哪个是USB转TTL口;


二、CubeMX 配置  SDIO + DMA + FatFs + USB

新建工程部分,略过 。

工程要点:STACK设置>=0x1000,配置好printf重定向。

  • 新建工程 参考:【STM32+CubeMX】 新建一个工程(STM32F407)
  • UART1 参考1:【STM32+CubeMX】USART1 DMA收发、printf
  • UART1 参考2:  STM32串口通信 -- bsp_UART.c 文件的移植和函数使用-CSDN博客

1、配置 SDIO、DMA

  • Mode :  SD 4 bits Wide bus ;
  • 参数 :F4系列不用修改配置,默认即可。F103系列,时钟分频系数 SDIOCLK Clock divide factor这一项,默认0,修改为8, 不然会通信失败。
  • DMA Settings :  添加SDIO_RX、SDIO_TX这两项;  本页其它参数默认;

2、配置 FatFs 文件系统

  • Mode:  打勾 SD Card
  • 参数 CODE_PAGE:简体中文
  • 参数 USE_LFN:长文件名称(缓存放在STACK,因此STACK得设置大一些,如>0x1000)
  • 参数 FS_EXFAT:ENABLE  (挂载、格式化时,会自动选择合适的FAT16、FAT32、exFAT)

3、FatFS 使用DMA

4、FatFS 是否使用检测引脚

让我们先回看一下原理图:

SD卡座的第9脚,用于检测是否已插入SD卡,如果已插入SD卡,CD脚会输出低电平。

不建议使用这个功能!因为:SD卡与U盘不同,没有完善的保护电路,不应该进行热插拔!而且,做SD卡项目调试时,默认状态应该是一直插着SD卡的。如果项目有需要,也可以自己写几行引脚电平检测,当引脚电平为低时,再对SD卡进行操作,这样更灵活。

回到FatFs配置界面 ,下方配置里,可以设置是否使用检测引脚:

  1. 如果你的SD卡已连接检测引脚,想使用CubeMX生成管理,可以在这里指定引脚。
  2. 如果不需要这个检测功能,就让它默认空着即可。不管是否已连接此引脚,都可空着。

CubeMX生成时注意:

        当不指定检测引脚,在最后生成工程时,会有弹窗警告,不用管它,到时点击Yes即可,将会正常生成工程,代码上不会有任何影响。

        如果不想有弹窗,可以随便指定一个空闲的引脚,这样在生成时就不会弹警告了,但是需要在生成的工程代码里,注释掉引脚检测功能。操作有点烦人,不建议此方法,如有需要,自行搜索操作的方法。

完成上面的配置后,工程已经能够使用代码进行文件系统挂载、读写了。

5、配置 USB

6、添加USB类 (大容量存储)

7、配置中断、优先级

  • 系统时基的中断优先级,修改为 0 ;  (如果不改,电脑端将无法识别成U盘)
  • SDIO global interrupt: 打勾
  • 修改下图四项的中断优先级;  中断值:SDIO < DMA < USB (值越小优先级越高) 

8、时钟设置

进入时钟树配置页面。

如果之前没配置过SDIO、USB,这时就会弹窗:是否自动配置所需时钟?

选择:NO ,手动修改即可。

不推荐 Yes,因为它将针对已使能的SDIO、USB进行必须值的配置,而已设置好的系统时钟,将会被修改成其它值。

F4系列,如果板子用的是25M晶振,使用下图配置即可;如果是8M晶振,修改晶振、分频两处为8即可。

重点:箭头所指的Q值,它用于控制USB 、SDIO和随机数生成器的时钟,这个时钟配置成 48M !  因此,箭头的Q值设置为 7; 

好了,SD卡 模拟成U盘 所需的 SDIO + FatFS + DMA + USB 已完成配置。

重新生成工程,这时,会有弹窗提示,因为我们没有指定SD卡的检测引脚。

点击 Yes 确认,继续生成即可!


三、Keil 编辑代码

1、打开keil 工程,先重新编译一次。

工程生成后,第一次编译会比较耗时,耐心等待。

  • 正常情况,编译是0 Error的。
  • 如果有Error,  应该是新建工程时,路径、名称有中文了,重新开建工程,改为英文即可。

2、重要修改:SD卡的初始化,使用 1-bit 模式

CubeMX生成的SDIO初始化代码,有一个bug,需要手动修改,操作如下: 

  • 右击 main.c 文件中函数 MX_SDIO_SD_Init(), 
  • 在弹出菜单中:Go To Ddfinition Of ...;  将跳转到SD卡初始化函数内部;

跳转到 sdio.c文件内的 MX_SDIO_SD_Init() 初始化函数,

  • 把函数内的 4B,改为 1B ;(如下图)

因为初始化时需要低速率,改用1线通信。如果不修改,初始化过程会导致程序卡死。

重要:CubeMX每次重新生成后,都要手动修改一次

至此,工程已经能够使用代码对SD卡文件进行读写了。

下面再添加USB的支持。

3、打开 sd_diskio.h

可以打开 sd_diskio.c,文件空白位置右击,再跳转到其头文件 sd_diskio.h;

进入sd_diskio.h后:

  • 在27行,添加头文件引用:#include "ff_gen_drv.h" ; 

4、修改 usbd_storage_if.c文件

这个文件,需要修改的地方比较多, 目的是为了把SD卡的文件系统与USB大容量类挂钩起来。

  • 增加两个头文件引脚
  • 修改四个函数 

详细如下,需要修改的位置,已用中文注释。没中文注释的,不用管,保持原样。

可对照修改,也可以直接复制替换原文件。

/* USER CODE BEGIN Header */
/********************************************************************************* @file           : usbd_storage_if.c* @version        : v1.0_Cube* @brief          : Memory management layer.******************************************************************************* @attention** Copyright (c) 2025 STMicroelectronics.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/
/* USER CODE END Header *//* Includes ------------------------------------------------------------------*/
#include "usbd_storage_if.h"/* USER CODE BEGIN INCLUDE */
#include "sdio.h"         // 增加这行
#include "sd_diskio.h"    // 增加这行/* USER CODE END INCLUDE *//* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*//* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*//* USER CODE END PV *//** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY* @brief Usb device.* @{*//** @defgroup USBD_STORAGE* @brief Usb mass storage device module* @{*//** @defgroup USBD_STORAGE_Private_TypesDefinitions* @brief Private types.* @{*//* USER CODE BEGIN PRIVATE_TYPES *//* USER CODE END PRIVATE_TYPES *//*** @}*//** @defgroup USBD_STORAGE_Private_Defines* @brief Private defines.* @{*/#define STORAGE_LUN_NBR                  1
#define STORAGE_BLK_NBR                  0x10000
#define STORAGE_BLK_SIZ                  0x200/* USER CODE BEGIN PRIVATE_DEFINES *//* USER CODE END PRIVATE_DEFINES *//*** @}*//** @defgroup USBD_STORAGE_Private_Macros* @brief Private macros.* @{*//* USER CODE BEGIN PRIVATE_MACRO *//* USER CODE END PRIVATE_MACRO *//*** @}*//** @defgroup USBD_STORAGE_Private_Variables* @brief Private variables.* @{*//* USER CODE BEGIN INQUIRY_DATA_FS */
/** USB Mass storage Standard Inquiry Data. */
const int8_t STORAGE_Inquirydata_FS[] =  /* 36 */
{/* LUN 0 */0x00,0x80,0x02,0x02,(STANDARD_INQUIRY_DATA_LEN - 5),0x00,0x00,0x00,'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ', /* Manufacturer : 8 bytes */'P', 'r', 'o', 'd', 'u', 'c', 't', ' ', /* Product      : 16 Bytes */' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ','0', '.', '0', '1'                      /* Version      : 4 Bytes */
};
/* USER CODE END INQUIRY_DATA_FS *//* USER CODE BEGIN PRIVATE_VARIABLES *//* USER CODE END PRIVATE_VARIABLES *//*** @}*//** @defgroup USBD_STORAGE_Exported_Variables* @brief Public variables.* @{*/extern USBD_HandleTypeDef hUsbDeviceFS;/* USER CODE BEGIN EXPORTED_VARIABLES *//* USER CODE END EXPORTED_VARIABLES *//*** @}*//** @defgroup USBD_STORAGE_Private_FunctionPrototypes* @brief Private functions declaration.* @{*/static int8_t STORAGE_Init_FS(uint8_t lun);
static int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size);
static int8_t STORAGE_IsReady_FS(uint8_t lun);
static int8_t STORAGE_IsWriteProtected_FS(uint8_t lun);
static int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
static int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
static int8_t STORAGE_GetMaxLun_FS(void);/* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION *//* USER CODE END PRIVATE_FUNCTIONS_DECLARATION *//*** @}*/USBD_StorageTypeDef USBD_Storage_Interface_fops_FS =
{STORAGE_Init_FS,STORAGE_GetCapacity_FS,STORAGE_IsReady_FS,STORAGE_IsWriteProtected_FS,STORAGE_Read_FS,STORAGE_Write_FS,STORAGE_GetMaxLun_FS,(int8_t *)STORAGE_Inquirydata_FS
};/* Private functions ---------------------------------------------------------*/
/*** @brief  Initializes the storage unit (medium) over USB FS IP* @param  lun: Logical unit number.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_Init_FS(uint8_t lun)
{/* USER CODE BEGIN 2 */UNUSED(lun);return (USBD_OK);/* USER CODE END 2 */
}/*** @brief  Returns the medium capacity.* @param  lun: Logical unit number.* @param  block_num: Number of total block number.* @param  block_size: Block size.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{/* USER CODE BEGIN 3 */UNUSED(lun);// *block_num  = STORAGE_BLK_NBR;     // 注释掉// *block_size = STORAGE_BLK_SIZ;     // 注释掉*block_num = hsd.SdCard.BlockNbr;     // 增加这行,用于获取SD卡的块数量*block_size = hsd.SdCard.BlockSize;   // 增加这行,用于获取SD卡的块大小return (USBD_OK);/* USER CODE END 3 */
}/*** @brief   Checks whether the medium is ready.* @param  lun:  Logical unit number.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_IsReady_FS(uint8_t lun)
{/* USER CODE BEGIN 4 */// UNUSED(lun);                       // 注释掉return (SD_Driver.disk_status(lun));  // 增加这行; 检查存储设备(如SD卡)指定逻辑单元的状态; 这行是非必需的,但增加后能更准确地反映设备的状态,避免在设备未准备好时进行读写操作,从而提高系统的可靠性和稳定性。// return (USBD_OK);                  // 注释掉/* USER CODE END 4 */
}/*** @brief  Checks whether the medium is write protected.* @param  lun: Logical unit number.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_IsWriteProtected_FS(uint8_t lun)
{/* USER CODE BEGIN 5 */UNUSED(lun);return (USBD_OK);/* USER CODE END 5 */
}/*** @brief  Reads data from the medium.* @param  lun: Logical unit number.* @param  buf: data buffer.* @param  blk_addr: Logical block address.* @param  blk_len: Blocks number.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{/* USER CODE BEGIN 6 */// UNUSED(lun);        // 注释掉// UNUSED(buf);        // 注释掉// UNUSED(blk_addr);   // 注释掉// UNUSED(blk_len);    // 注释掉// return (USBD_OK);   // 注释掉return (SD_Driver.disk_read(lun, buf, blk_addr, blk_len)); // 增加这行/* USER CODE END 6 */
}/*** @brief  Writes data into the medium.* @param  lun: Logical unit number.* @param  buf: data buffer.* @param  blk_addr: Logical block address.* @param  blk_len: Blocks number.* @retval USBD_OK if all operations are OK else USBD_FAIL*/
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{/* USER CODE BEGIN 7 */// UNUSED(lun);         // 注释掉// UNUSED(buf);         // 注释掉// UNUSED(blk_addr);    // 注释掉// UNUSED(blk_len);     // 注释掉// return (USBD_OK);    // 注释掉return (SD_Driver.disk_write(lun, buf, blk_addr, blk_len)); // 增加这行/* USER CODE END 7 */
}/*** @brief  Returns the Max Supported LUNs.* @param  None* @retval Lun(s) number.*/
int8_t STORAGE_GetMaxLun_FS(void)
{/* USER CODE BEGIN 8 */return (STORAGE_LUN_NBR - 1);/* USER CODE END 8 */
}/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION *//* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION *//*** @}*//*** @}*/

至此,整个工程需要修改的地方,都已修改好了。

先编译、保存一下。

下面,开始编写测试代码,用代码的方式进行文件读写测试。

5、常用函数

我们上几篇介绍SD卡的读写时,共用过6个函数,如下表。

这6个函数,在本工程中,还是可用的,本篇不啰嗦示范。

1、获取SD卡信息
HAL_SD_CardInfoTypeDef pCardInfo = {0};          // SD卡信息结构体
HAL_SD_GetCardInfo(&hsd, &pCardInfo);            // 获取 SD 卡的信息2、读数据
HAL_SD_ReadBlocks(&hsd, aOldData, 7, 2, 3000);   //  SD卡的句柄、数据、块地址、块数量、超时ms3、写数据
HAL_SD_WriteBlocks(&hsd, aTestData, 7, 2, 3000)  //  SD卡的句柄、数据、块地址、块数量、超时ms4、读数据_DMA
HAL_SD_ReadBlocks_DMA(&hsd, aOldData, 7, 2);    // 读取SD卡指定块的数据; 参数:SD句柄、数据地址、块起始地址、需要读取的块数量;5、写数据_DMA
HAL_SD_WriteBlocks_DMA(&hsd, aTestData, 7, 2);  // 向指定块写入数据; 参数:SD句柄、数据地址、块起始地址、需要写入的块数量;6、擦除数据
HAL_SD_Erase(&hsd, 7, 8)  //  SD卡的句柄、块起始地址、块结束地址

而用代码进行FatFS文件系统的操作、读写,会用到下面6个函数.

函数参数的具体作用,可以通过 Kimi 进行查询。

FRESULT f_res;1、挂载文件系统
f_res = f_mount(&myFatFs, "0:", 1);                                   // 在SD卡上挂载文件系统; 参数:文件系统对象、驱动器路径、读写模式(0只读、1读写)2、格式化
static uint8_t aMountBuffer[4096];                                   // 格式化时所需的临时缓存; 块大小512的倍数; 值越大格式化越快, 如果内存不够,可改为512或者1024; 当需要在函数内定义这种大缓存时,要用static修饰,令缓存存放在全局数据区内,不然,可能会导致stack溢出。
f_res = f_mkfs("0:", 0, 0, aMountBuffer, sizeof(aMountBuffer));      // 格式化SD卡; 参数:驱动器路径、文件系统(0自动\1FAT12\2FAT16\)、簇大小(0为自动选择)、格式化临时缓冲区、缓冲区大小3、打开文件
f_res = f_open(&myFile, "0:Test.txt", FA_CREATE_ALWAYS | FA_WRITE);  // 打开文件; 参数:要操作的文件对象、路径和文件名称、打开模式;4、关闭文件
f_close(&myFile);                                                    // 不再读写,关闭文件5、文件写入数据
f_res = f_write(&myFile, aWriteBuf, sizeof(aWriteBuf), &num);        // 向文件内写入数据; 参数:文件对象、数据缓存、申请写入的字节数、实际写入的字节数6、文件读取数据
f_res = f_read(&myFile, aReadData, sizeof(aReadData), &num);         // 从文件中读取数据; 参数:文件对象、数据缓冲区、请求读取的最大字节数、实际读取的字节数

6、具体操作示例代码

第一步:编写FatFs文件系统操作、读写的示范函数

在main()的上方,/* USER CODE BEGIN 0 */ 下面,编写以下代码(建议直接复制):

这个函数的作用是:判断是否需要格式化、挂载文件系统、创建文件、写入数据、读出数据。

// SD卡的FatFS文件系统挂载、格式化、读写测试
void FatFsTest(void)
{static FATFS myFatFs;                                                 // FatFs 文件系统对象; 这个结构体占用598字节,有点大,需用static修饰(存放在全局数据区), 避免stack溢出static FIL myFile;                                                    // 文件对象; 这个结构体占用570字节,有点大,需用static修饰(存放在全局数据区), 避免stack溢出static FRESULT f_res;                                                 // 文件操作结果static uint32_t num;                                                  // 文件实际成功读写的字节数static uint8_t aReadData[1024] = {0};                                 // 读取缓冲区; 这个数组占用1024字节,需用static修饰(存放在全局数据区), 避免stack溢出static uint8_t aWriteBuf[] =  "测试; This is FatFs Test ! \r\n";      // 要写入的数据// 重要的延时:避免烧录期间的复位导致文件读写、格式化等错误HAL_Delay(1000);                                                      // 重要:稍作延时再开始读写测试; 避免有些仿真器烧录期间的多次复位,短暂运行了程序,导致下列读写数据不完整。// 1、挂载测试:在SD卡挂载文件系统printf("\r\n\r\n");printf("1、挂载 FatFs 测试 ****** \r\n");f_res = f_mount(&myFatFs, "0:", 1);                                   // 在SD卡上挂载文件系统; 参数:文件系统对象、驱动器路径、读写模式(0只读、1读写)if (f_res == FR_NO_FILESYSTEM)                                        // 检查是否已有文件系统,如果没有,就格式化创建创建文件系统{printf("SD卡没有文件系统,开始格式化…...\r\n");static uint8_t aMountBuffer[4096];                                // 格式化时所需的临时缓存; 块大小512的倍数; 值越大格式化越快, 如果内存不够,可改为512或者1024; 当需要在函数内定义这种大缓存时,要用static修饰,令缓存存放在全局数据区内,不然,可能会导致stack溢出。f_res = f_mkfs("0:", 0, 0, aMountBuffer, sizeof(aMountBuffer));   // 格式化SD卡; 参数:驱动器、文件系统(0-自动\1-FAT12\2-FAT16\3-FAT32\4-exFat)、簇大小(0为自动选择)、格式化临时缓冲区、缓冲区大小; 格式化前必须先f_mount(x,x,1)挂载,即必须用读写方式挂载; 如果SD卡已格式化,f_mkfs()的第2个参数,不能用0自动,必须指定某个文件系统。if (f_res == FR_OK)                                               // 格式化 成功{printf("SD卡格式化:成功 \r\n");f_res = f_mount(NULL, "0:", 1);                               // 格式化后,先取消挂载f_res = f_mount(&myFatFs, "0:", 1);                           // 重新挂载if (f_res == FR_OK)printf("FatFs 挂载成功 \r\n");                            // 挂载成功elsereturn;                                                   // 挂载失败,退出函数}else{printf("SD卡格式化:失败 \r\n");                              // 格式化 失败return;}}else if (f_res != FR_OK)                                              // 挂载异常{printf("FatFs 挂载异常: %d; 检查MX_SDIO_SD_Init()是否已修改1B\r", f_res);return;}else                                                                  // 挂载成功{if (myFatFs.fs_type == 0x03)                                      // FAT32; 1-FAT12、2-FAT16、3-FAT32、4-exFatprintf("SD卡已有文件系统:FAT32\n");if (myFatFs.fs_type == 0x04)                                      // exFAT; 1-FAT12、2-FAT16、3-FAT32、4-exFatprintf("SD卡已有文件系统:exFAT\n");                         printf("FatFs 挂载成功 \r\n");                                    // 挂载成功}// 2、写入测试:打开或创建文件,并写入数据printf("\r\n");printf("2、写入测试:打开或创建文件,并写入数据 ****** \r\n");f_res = f_open(&myFile, "0:text.txt", FA_CREATE_ALWAYS | FA_WRITE);   // 打开文件; 参数:要操作的文件对象、路径和文件名称、打开模式;if (f_res == FR_OK){printf("打开文件 成功 \r\n");printf("写入测试:");f_res = f_write(&myFile, aWriteBuf, sizeof(aWriteBuf), &num);     // 向文件内写入数据; 参数:文件对象、数据缓存、申请写入的字节数、实际写入的字节数if (f_res == FR_OK){printf("写入成功  \r\n");printf("已写入字节数:%d \r\n", num);                         // printf 写入的字节数printf("已写入的数据:%s \r\n", aWriteBuf);                   // printf 写入的数据; 注意,这里以字符串方式显示,如果数据是非ASCII可显示范围,则无法显示}else{printf("写入失败 \r\n");                                      // 写入失败printf("错误编号: %d\r\n", f_res);                           // printf 错误编号}f_close(&myFile);                                                 // 不再读写,关闭文件}else{printf("打开文件 失败: %d\r\n", f_res);}// 3、读取测试:打开已有文件,读取其数据printf("3、读取测试:打开刚才的文件,读取其数据 ****** \r\n");f_res = f_open(&myFile, "0:text.txt", FA_OPEN_EXISTING | FA_READ);    // 打开文件; 参数:文件对象、路径和名称、操作模式; FA_OPEN_EXISTING:只打开已存在的文件; FA_READ: 以只读的方式打开文件if (f_res == FR_OK){printf("打开文件 成功 \r\n");f_res = f_read(&myFile, aReadData, sizeof(aReadData), &num);      // 从文件中读取数据; 参数:文件对象、数据缓冲区、请求读取的最大字节数、实际读取的字节数if (f_res == FR_OK){printf("读取数据 成功 \r\n");printf("已读取字节数:%d \r\n", num);                         // printf 实际读取的字节数printf("读取到的数据:%s\r\n", aReadData);                    // printf 实际数据; 注意,这里以字符串方式显示,如果数据是非ASCII可显示范围,则无法显示}else{printf("读取 失败  \r\n");                                    // printf 读取失败printf("错误编号:%d \r\n", f_res);                           // printf 错误编号}}else{printf("打开文件 失败 \r\n");                                     // printf 打开文件 失败printf("错误编号:%d\r\n", f_res);                                // printf 错误编号}f_close(&myFile);                                                     // 不再读写,关闭文件f_mount(NULL, "0:", 1);                                               // 不再使用文件系统,取消挂载文件系统
}

编写完成后,位置如下图:

第二步:编写SD卡信息获取函数

在刚才函数的下方,再编写一个SD卡信息获取函数(建议直接复制)。

这个函数的作用是:获取SD卡的基础信息、块数量 、块大小、卡容量。

// 获取SD卡信息
// 注意: 本函数需要在f_mount()执行后再调用,因为CubeMX生成的FatFs代码, 会在f_mount()函数内对SD卡进行初始化
void SDCardInfo(void)
{HAL_SD_CardInfoTypeDef pCardInfo = {0};                    // SD卡信息结构体uint8_t status = HAL_SD_GetCardState(&hsd);                // SD卡状态标志值if (status == HAL_SD_CARD_TRANSFER){HAL_SD_GetCardInfo(&hsd, &pCardInfo);                  // 获取 SD 卡的信息printf("\r\n");printf("*** 获取SD卡信息 *** \r\n");printf("卡类型:%d \r\n", pCardInfo.CardType);         // 类型返回:0-SDSC、1-SDHC/SDXC、3-SECUREDprintf("卡版本:%d \r\n", pCardInfo.CardVersion);      // 版本返回:0-CARD_V1、1-CARD_V2printf("块数量:%d \r\n", pCardInfo.BlockNbr);         // 可用的块数量printf("块大小:%d \r\n", pCardInfo.BlockSize);        // 每个块的大小; 单位:字节printf("卡容量:%lluG \r\n", ((uint64_t)pCardInfo.BlockSize * pCardInfo.BlockNbr) / 1024 / 1024 / 1024);  // 计算卡的容量; 单位:GB}
}

第三步:在 main()函数内,调用刚才那两个函数

调用位置,如下图:

至此,测试代码编写完成,可以编译、烧录了。


四、实验效果

串口助手输出:

电脑U盘识别效果:

重要:每次烧录程序后,需要手动插拔一次USB线,才能被重新识别。

如有错漏 ,望指正~~~!

相关文章:

【CubeMX+STM32】SD卡 U盘文件系统 USB+FATFS

本篇&#xff0c;将使用CubeMXKeil, 创建一个 USBTF卡存储FatFS 的虚拟U盘读写工程。 目录 一、简述 二、CubeMX 配置 SDIO DMA FatFs USB 三、Keil 编辑代码 四、实验效果 串口助手&#xff0c;实现效果&#xff1a; U盘&#xff0c;识别效果&#xff1a; 一、简述 上…...

在JVM的栈(虚拟机栈)中,除了栈帧(Stack Frame)还有什么?

在JVM的栈&#xff08;虚拟机栈&#xff09;中&#xff0c;除了栈帧&#xff08;Stack Frame&#xff09;&#xff0c;还有其他一些与方法调用相关的重要信息。栈的主要作用是存储方法调用的执行过程中的上下文信息&#xff0c;栈帧是其中最关键的组成部分。 栈的组成 栈帧&am…...

# 解析Excel文件:处理Excel xlsx file not supported错误 [特殊字符]

解析Excel文件&#xff1a;处理Excel xlsx file not supported错误 &#x1f9e9; 嘿&#xff0c;数据分析的小伙伴们&#xff01;&#x1f44b; 我知道在处理Excel文件的时候&#xff0c;很多人可能会遇到这样一个错误&#xff1a;Excel xlsx file not supported。别担心&…...

图片下载不下来?即便点了另存为也无法下载?两种方法教你百分之百下载下来

前言&#xff0c;我要讲的是网站没有禁鼠标右键&#xff0c;可以右键&#xff0c;也可以打开控制台&#xff0c;图片也不用付费这种。 一、用鼠标按住图片直接往桌面拖动&#xff0c;也可以打开开发者工具&#xff0c;在里面往外拖。 二、这个方法很有意思&#xff0c;在电脑的…...

Unity项目实战-Player玩家控制脚本实现

玩家控制脚本设计思路 1. 代码演变过程 1.1 初始阶段&#xff1a;单一Player类实现 最初的设计可能是一个包含所有功能的Player类&#xff1a; public class Player : MonoBehaviour {private CharacterController controller;private Animator animator;[SerializeField] …...

CP AUTOSAR标准之ICUDriver(AUTOSAR_SWS_ICUDriver)(更新中……)

1 简介和功能概述 该规范指定了AUTOSAR基础软件模块ICU驱动程序的功能、API和配置。   ICU驱动程序是一个使用输入捕获单元(ICU)来解调PWM信号、计数脉冲、测量频率和占空比、生成简单中断和唤醒中断的模块。   ICU驱动程序提供服务 信号边缘通知控制唤醒中断周期信号时间测…...

Python3 ImportError: cannot import name ‘XXX‘ from ‘XXX‘

个人博客地址&#xff1a;Python3 ImportError: cannot import name XXX from XXX | 一张假钞的真实世界 例如如下错误&#xff1a; $ python3 git.py Traceback (most recent call last):File "git.py", line 1, in <module>from git import RepoFile &quo…...

[学习笔记] Kotlin Compose-Multiplatform

Compose-Multiplatform 原文&#xff1a;https://github.com/zimoyin/StudyNotes-master/blob/master/compose-multiplatform/compose.md Compose Multiplatform 是 JetBrains 为桌面平台&#xff08;macOS&#xff0c;Linux&#xff0c;Windows&#xff09;和Web编写Kotlin UI…...

【R语言】t检验

t检验&#xff08;t-test&#xff09;是用于比较两个样本均值是否存在显著差异的一种统计方法。 t.test()函数的调用格式&#xff1a; t.test(x, yNULL, alternativec("two.sided", "less", "greater"), mu0, pairedFALSE, var.equalFALSE, co…...

flutter ListView Item复用源码解析

Flutter 的 ListView 的 Item 复用机制是其高性能列表渲染的核心&#xff0c;底层实现依赖于 Flutter 的渲染管线、Element 树和 Widget 树的协调机制。以下是 ListView 复用机制的源码级解析&#xff0c;结合关键类和核心逻辑进行分析。 1. ListView 的底层结构 ListView 的复…...

Spring Boot 配置 Mybatis 读写分离

JPA 的读写分离配置不能应用在 Mybatis 上, 所以 Mybatis 要单独处理 为了不影响原有代码, 使用了增加拦截器的方式, 在拦截器里根据 SQL 的 CRUD 来路由到不同的数据源 需要单独增加Mybatis的配置 Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) t…...

网络初识-

网络的相关概念 一、局域网和广域网 将各种计算机、外部设备等相互连接起来&#xff0c;实现在这个范围内数据通信和资源共享的计算机网络。它的覆盖范围通常在几百米到几公里之内。例如&#xff0c;一个小型企业的办公室&#xff0c;通过交换机将多台电脑连接在一起&#xf…...

DNS污染:网络世界的“隐形劫持”与防御

在互联网的底层架构中&#xff0c;DNS&#xff08;域名系统&#xff09;如同数字世界的“导航员”&#xff0c;将用户输入的域名翻译成机器可读的IP地址。然而&#xff0c;DNS污染&#xff08;DNS Poisoning&#xff09;正像一场无声的“地址篡改”危机&#xff0c;威胁着全球网…...

MQTT(Message Queuing Telemetry Transport)协议(三)

主题是什么 2. TCP 协议封装 tcp.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h>// 建立 TCP 连接 int tcp_connect(const char *server_ip, int s…...

多核cpu与时间片多线程的问题

在多核处理器中&#xff0c;每个核心可以独立运行一个线程。操作系统负责管理和调度这些线程&#xff0c;以确保高效利用处理器资源。下面详细解释如何获取时间片以及四个线程如何在四个核心上同时工作。 ### 时间片和调度 #### 1. 时间片&#xff08;Time Slice&#xff09;…...

电脑出现蓝屏英文怎么办?查看修复过程

电脑出现蓝屏英文是一种常见的电脑故障&#xff0c;它通常表示电脑遇到了严重的错误&#xff0c;需要停止运行以防止进一步的损坏。电脑蓝屏英文的原因可能有很多&#xff0c;比如硬件故障、驱动程序错误、系统文件损坏、病毒感染等。那么&#xff0c;当电脑出现蓝屏英文时&…...

安卓基础(第一集)

SharedPreferences&#xff08;本地存储简单数据&#xff09; 在 Android 中&#xff0c;SharedPreferences 用于存储小型数据。 &#xff08;1&#xff09;存储数据 // 获取 SharedPreferences 对象 SharedPreferences sharedPreferences getSharedPreferences("MyPre…...

【从零开始入门unity游戏开发之——C#篇56】C#补充知识点——模式匹配

考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…...

【数据可视化-16】珍爱网上海注册者情况分析

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…...

c/c++蓝桥杯经典编程题100道(21)背包问题

背包问题 ->返回c/c蓝桥杯经典编程题100道-目录 目录 背包问题 一、题型解释 二、例题问题描述 三、C语言实现 解法1&#xff1a;0-1背包&#xff08;基础动态规划&#xff0c;难度★&#xff09; 解法2&#xff1a;0-1背包&#xff08;空间优化版&#xff0c;难度★…...

电赛DEEPSEEK

以下是针对竞赛题目的深度优化方案&#xff0c;重点解决频率接近时的滤波难题和相位测量精度问题&#xff1a; 以下是使用NI Multisim 14.3实现本项目的详细解决方案&#xff1a; 一、基础要求实现方案&#xff08;模块化设计&#xff09; 1. 双频信号发生电路 电路结构&…...

VSOMEIP ROUTING应用和CLIENT应用之间交互的消息

#define VSOMEIP_ASSIGN_CLIENT 0x00 // client应用请求分配client_id #define VSOMEIP_ASSIGN_CLIENT_ACK 0x01 // routing应用返回分配的client_id #define VSOMEIP_REGISTER_APPLICATION 0x02 // client应用注册someip应用 #…...

HTML之基本布局div|span

HTML基本布局使用 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"width<device-width>, initial-scale1.0"><title>布局</title> <…...

Linux下学【MySQL】常用函数助你成为数据库大师~(配sql+实操图+案例巩固 通俗易懂版~)

绪论​ 每日激励&#xff1a;“唯有努力&#xff0c;才能进步” 绪论​&#xff1a; 本章是MySQL中常见的函数&#xff0c;利用好函数能很大的帮助我们提高MySQL使用效率&#xff0c;也能很好处理一些情况&#xff0c;如字符串的拼接&#xff0c;字符串的获取&#xff0c;进制…...

【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列

目录 一.TTL ???1.设置消息的TTL 2.设置队列的TTL 3.俩者区别? 二.死信队列 定义&#xff1a; 消息成为死信的原因&#xff1a; 1.消息被拒绝&#xff08;basic.reject 或 basic.nack&#xff09; 2.消息过期&#xff08;TTL&#xff09; 3.队列达到最大长度? …...

机器学习赋能的智能光子学器件系统研究与应用

机器学习赋能的智能光子学器件系统研究与应用 时间&#xff1a; 2025年03月29日-03月30日 2025年04月05日-04月06日 机器学习赋能的光子学器件与系统&#xff1a;从创新设计到前沿应用 课程针对光子学方面的从业科研人员及开发者&#xff0c;希望了解和实践在集成光学/空间…...

尚硅谷课程【笔记】——大数据之Linux【三】

课程视频链接&#xff1a;尚硅谷大数据Linux课程 七、定时任务调度 任务调度&#xff1a;指系统在某个时间执行的特定的命令或程序。 1&#xff09;系统工作&#xff1a;有些重要的工作必须周而复始地执行。 2&#xff09;个别用户工作&#xff1a;用户可能希望在某些特定的时…...

Visual Studio踩过的坑

统计Unity项目代码行数 编辑-查找和替换-在文件中查找 查找内容输入 b*[^:b#/].*$ 勾选“使用正则表达式” 文件类型留空 也有网友做了指定&#xff0c;供参考 !*\bin\*;!*\obj\*;!*\.*\*!*.meta;!*.prefab;!*.unity 打开Unity的项目 注意&#xff1a;只是看&#xff0…...

教程 | MySQL 基本指令指南(附MySQL软件包)

此前已经发布了安装教程安装教程&#xff0c;现在让我们来学习一下MySQL的基本指令。 一、数据库连接与退出 连接本地数据库 mysql -uroot -p # 输入后回车&#xff0c;按提示输入密码&#xff08;密码输入不可见&#xff09;若需隐藏密码显示&#xff0c;可使用&#xff1…...

企业数据集成案例:吉客云销售渠道到MySQL

测试-查询销售渠道信息-dange&#xff1a;吉客云数据集成到MySQL的技术案例分享 在企业的数据管理过程中&#xff0c;如何高效、可靠地实现不同系统之间的数据对接是一个关键问题。本次我们将分享一个具体的技术案例——通过轻易云数据集成平台&#xff0c;将吉客云中的销售渠…...