目录
一、SDIO卡简介
SDIO(Secure Digital Input Output) 是一种基于SD卡物理接口扩展的输入输出技术,在兼容传统SD存储功能的同时,通过扩展协议支持外设连接(如Wi-Fi、蓝牙、GPS等),可配置为高速4位总线模式(最高50MHz),早期广泛应用于移动设备扩展功能,现逐渐被USB等接口替代,但在嵌入式领域仍用于低成本、紧凑型设备的模块化扩展。
在STM32开发中,SD卡的块写入(Block Write)和FATFS文件系统是两种不同层级的操作方式,它们的核心区别在于抽象层级和功能目标。
1. SD卡硬件
2. FATFS简介
FATFS是一个为嵌入式系统设计的轻量级文件系统模块,支持FAT12、FAT16、FAT32及exFAT格式,提供标准的文件操作接口(如打开、读写、目录管理),兼容性强且资源占用低,适用于单片机等资源受限环境。其核心通过逻辑扇区地址(LBA)抽象底层存储介质(如SD卡),开发者需对接底层块读写函数以实现物理驱动。
3. FATFS文件写入 vs. 直接块写入的区别
FATFS写入:调用
f_write(&file, buffer, size, &bytes_written)
,数据自动按文件系统规则分配扇区并更新FAT表。块写入:调用
HAL_SD_WriteBlocks(&hsd, buffer, sector_num, block_count)
,直接向指定物理扇区写入数据块,需自行记录存储位置。
对比维度 | FATFS文件写入 | 直接块写入(Block Write) |
功能定位 | 文件级操作(文件/目录管理) | 物理扇区级操作(无文件系统结构) |
操作单位 | 按字节/数据流写入(自动管理扇区分配) | 按固定块(如512字节)写入 |
结构管理 | 维护FAT表、目录项等元数据 | 无文件系统结构,需手动管理数据存储逻辑 |
接口复杂度 | 高层API(f_open , f_write , f_close ) | 底层驱动函数(如HAL_SD_WriteBlocks ) |
性能开销 | 需处理文件系统元数据(略低效率) | 直接操作扇区(更高效率,无额外开销) |
适用场景 | 需文件管理(如日志存储、多文件存取) | 裸数据存储(如原始传感器数据流) |
二、创建工程
软件版本
cubemx6.1.2 MDK5.33.0.0 芯片用的是stm32f103zet6
1. 打开 STM32CubeMX 软件,新建工程
2.选择MCU这里以STM32F103ZET6为例
3. SDIO参数配置
选择 SD 4 bits Wide bus
四线SD模式,时钟分频系数设置为:6
4. DMA Settings配置
5. FATFS配置
勾选SD Card,并在code page选择中文
6. USART1配置
如图芯片引脚所示:
PA9 ------> USART1_TX
PA10 ------> USART1_RX
7. DMA配置
8. RTC配置
9. NVIC Settings配置
10. NVIC 配置
11. SYS 配置
12. RCC 配置
13. 时钟树配置
14. 代码生成
输入项目名和项目路径(路径中不可包含中文)
二、keil代码
在usart.c中先添加头文件"stdio.h"再在底部添加打印重定向函数如下所示
#include "usart.h"
/* USER CODE BEGIN 0 */
#include "stdio.h" //头文件
/* USER CODE END 0 */
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f) //打印重定向
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* @brief 重定向c库函数getchar,scanf到USARTx
* @retval None
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
/* USER CODE END 1 */
在stm32f1xx_it.c底部添加回调函数如下所示
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN TD */
uint8_t Buffer[1];
/* USER CODE END TD */
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit(&huart1, (uint8_t *)Buffer, 1, 0xffff);
HAL_UART_Receive_IT(&huart1, (uint8_t *)Buffer, 1);
}
}
/* USER CODE END 1 */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "fatfs.h"
#include "rtc.h"
#include "sdio.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h> //添加头文件stdio.h
#include <string.h> //添加头文件string.h
#include <time.h> //添加头文件time.h
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
#define RX_BUFFER_SIZE 1024 // 串口接收缓冲区大小
volatile uint8_t rxBuffer[RX_BUFFER_SIZE]; // DMA接收缓冲区
volatile uint16_t rxLength = 0; // 实际接收数据长度
volatile uint8_t fileIsOpen = 0; // 文件打开状态标志
FATFS fs; // FatFs文件系统对象
FIL file; // 文件对象
FRESULT f_res; // 文件操作结果
UINT fnum; // 文件读写计数器
// RTC相关变量
RTC_TimeTypeDef currentTime;
RTC_DateTypeDef currentDate;
char timeStamp[32]; // 时间戳缓冲区
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void processReceivedData(void);
void FormatTimestamp(void); // 新增时间格式化函数
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1) {
rxLength = Size; // 保存接收数据长度
processReceivedData(); // 处理接收到的数据
// 重新启动DMA接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, RX_BUFFER_SIZE);
}
}
/* USER CODE END 0 */
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_SDIO_SD_Init();
MX_USART1_UART_Init();
MX_FATFS_Init();
MX_RTC_Init();
/* USER CODE BEGIN 2 */
if(HAL_RTC_GetTime(&hrtc, ¤tTime, RTC_FORMAT_BIN) != HAL_OK) {
printf("RTC初始化失败!\r\n");
Error_Handler();
}
printf("\r\n****** 串口数据记录系统启动 ******\r\n");
// 挂载文件系统
f_res = f_mount(&fs, "0:", 1);
if(f_res != FR_OK) {
printf("SD卡挂载失败: %d\r\n", f_res);
Error_Handler();
}
// 文件打开逻辑
f_res = f_open(&file, "0:sd_test.txt", FA_WRITE | FA_OPEN_ALWAYS);
if(f_res == FR_OK) {
// 移动指针到文件末尾实现追加写入
f_res = f_lseek(&file, f_size(&file));
if(f_res != FR_OK) {
printf("文件指针定位失败: %d\r\n", f_res);
Error_Handler();
}
printf("成功打开日志文件(追加模式)\r\n");
} else {
printf("文件打开失败: %d\r\n", f_res);
Error_Handler();
}
fileIsOpen = 1;
// 启动DMA接收(带空闲中断)
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.LSIState = RCC_LSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
void FormatTimestamp(void)
{
// 获取精确时间(包含日期)
HAL_RTC_GetTime(&hrtc, ¤tTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, ¤tDate, RTC_FORMAT_BIN);
// 格式示例:[2023-08-20 14:30:45]
snprintf(timeStamp, sizeof(timeStamp), "[%04d-%02d-%02d %02d:%02d:%02d] ",
currentDate.Year + 2000, // RTC年份通常从2000开始
currentDate.Month,
currentDate.Date,
currentTime.Hours,
currentTime.Minutes,
currentTime.Seconds);
}
void processReceivedData(void)
{
if(rxLength > 0 && fileIsOpen) {
// 生成时间戳
FormatTimestamp();
// 写入日志的三部分
const char newline[] = "\r\n";
// 1. 写入时间戳
f_res = f_write(&file, timeStamp, strlen(timeStamp), &fnum);
if(f_res != FR_OK) goto write_error;
// 2. 写入接收数据
f_res = f_write(&file, (const void*)rxBuffer, rxLength, &fnum);
if(f_res != FR_OK) goto write_error;
// 3. 写入换行符
f_res = f_write(&file, newline, sizeof(newline)-1, &fnum);
if(f_res != FR_OK) goto write_error;
// 同步文件系统
f_res = f_sync(&file);
if(f_res != FR_OK) {
printf("同步错误: %d\r\n", f_res);
Error_Handler();
}
// 串口回显带格式的数据
HAL_UART_Transmit(&huart1, (uint8_t*)timeStamp, strlen(timeStamp), 100);
//HAL_UART_Transmit(&huart1, rxBuffer, rxLength, 100);
HAL_UART_Transmit(&huart1, (uint8_t*)newline, sizeof(newline)-1, 100);
HAL_UART_Transmit(&huart1, (uint8_t*)rxBuffer, rxLength, 100);
rxLength = 0;
return;
write_error:
printf("文件写入失败: %d\r\n", f_res);
Error_Handler();
}
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void) //修改错误处理函数部分
{
/* USER CODE BEGIN Error_Handler_Debug */
// 安全关闭文件系统
if(fileIsOpen) {
f_close(&file);
f_mount(NULL, "0:", 1);
fileIsOpen = 0;
}
// 错误提示音(可以自定义LED灯啥的)
// HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
// HAL_Delay(1000);
// HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);
printf("!! 系统进入安全模式 !!\r\n");
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
三、下载验证