当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]在MCU上跑FATFS,90%的bug不是出在文件系统本身,而是出在任务划分上。当SD卡写入和网络发送同时抢SPI总线,当FATFS的f_write阻塞了整个系统,当一个f_mount卡死导致看门狗复位——问题的根源都一样:没有想清楚"谁来干、什么时候干、干完通知谁"。

在MCU上跑FATFS,90%的bug不是出在文件系统本身,而是出在任务划分上。当SD卡写入和网络发送同时抢SPI总线,当FATFS的f_write阻塞了整个系统,当一个f_mount卡死导致看门狗复位——问题的根源都一样:没有想清楚"谁来干、什么时候干、干完通知谁"。

一、程序说明:三个任务,各司其职

整个系统划分为三个核心任务,加上一个定时器中服务:

Task_SD_Write(优先级2): 专责SD卡写入。从消息队列接收写入请求(文件名+数据指针+长度),调用FATFS接口完成写入,完成后通过信号量通知上层。此任务独占SPI总线,其他任务严禁直接操作SD卡。

Task_SD_Read(优先级1): 专责SD卡读取。从消息队列接收读取请求,调用f_read获取数据,回填至请求方指定缓冲区。优先级低于写入任务——因为写操作通常有实时性要求(如数据记录),读操作可以容忍延迟。

Task_Command(优先级3): 最高优先级,处理来自上位机或其他模块的文件操作指令(创建/删除/重命名/查询剩余空间)。这些操作必须快速响应,不能被SD卡I/O阻塞。

Timer_Heartbeat(1秒周期): 调用f_getfree查询SD卡剩余空间,通过串口上报状态。这是唯一允许直接调用FATFS查询类函数的任务,且被严格限制在1秒一次。

核心设计原则:FATFS的所有API调用必须且只能在SD任务中执行。 任何其他任务直接调用f_write/f_read,都是定时炸弹。

二、程序框架分析:消息队列是唯一通道

┌─────────────┐ Queue ┌──────────────┐ SPI ┌────────┐

│ Task_Command │───────────→│ Task_SD_Write │────────→│ SD卡 │

│ (优先级3) │ 写入请求 │ (优先级2) │ 独占 │ FATFS │

└─────────────┘ └──────────────┘ └────────┘

│ ↑

│ 命令队列 │ 写完成信号量

↓ │

┌─────────────┐ ┌──────────────┐

│ Task_SD_Read │←───────────│ Task_SD_Write │

│ (优先级1) │ 读取请求 │ (优先级2) │

└─────────────┘ └──────────────┘

│ 读完成信号量

┌─────────────┐

│ 其他业务任务 │

│ (网络/显示等) │

└─────────────┘

─────────┘

19

任务间通信机制:

通道
类型
用途
容量
xQueueWriteReq
消息队列
上层→SD写入任务
8条
xQueueReadReq
消息队列
上层→SD读取任务
8条
xSemWriteDone
二值信号量
写入完成通知
1
xSemReadDone
二值信号量
读取完成通知
1
xMutexSPI
互斥量
SPI总线独占
1

为什么不用一个任务包揽所有SD操作? 因为FATFS的f_write在写入大文件时可能阻塞数百毫秒。如果网络发送任务也在同一个任务里调用f_write,网络包就会被卡住,整个系统失去响应。任务隔离的本质,是用调度器的优先级机制,保证实时性任务永远不被I/O任务阻塞。

三、程序实现:关键代码与陷阱

SD写入任务核心代码:

void Task_SD_Write(void *pvParameters) {

SD_Request_t req;

for (;;) {

if (xQueueReceive(xQueueWriteReq, &req, portMAX_DELAY) == pdTRUE) {

xSemaphoreTake(xMutexSPI, portMAX_DELAY); // 独占SPI

FIL file;

FRESULT res = f_open(&file, req.filename, FA_WRITE | FA_OPEN_APPEND);

if (res == FR_OK) {

UINT bw;

res = f_write(&file, req.data, req.length, &bw);

f_close(&file);

}

xSemaphoreGive(xMutexSPI); // 释放SPI

xSemaphoreGive(xSemWriteDone); // 通知上层

}

}

}

三个必须踩过的坑:

坑一:f_mount放在哪里? 必须在Task_Command中调用,且加互斥保护。绝不能在写入任务里反复mount——FATFS的挂载过程会扫描整个FAT表,耗时可达2秒,直接卡死系统。正确做法:上电时mount一次,之后仅在检测到SD卡拔除/插入时重新mount。

坑二:堆空间不够。 FATFS默认工作缓冲区512字节,但f_open内部会动态申请堆空间。FreeRTOS的heap_4.c在多任务场景下容易碎片化。必须在FreeRTOSConfig.h中设置configTOTAL_HEAP_SIZE至少8KB,并启用configUSE_MALLOC_FAILED_HOOK监控分配失败。

坑三:长文件名支持。 默认FATFS只支持8.3短文件名。必须在ffconf.h中启用_USE_LFN = 2(栈模式,需额外512字节堆空间),否则创建"flight_data_20260607.csv"会直接失败。

四、应用与性能数据:实测说话

场景
单任务方案
三任务方案
改善
写入1KB文件耗时
45ms(阻塞全系统)
12ms(仅SD任务阻塞)
73%
写入时网络延迟
380ms(丢包)
8ms(无影响)
98%
同时读写冲突
死锁
互斥量自动排队
质变
SD卡满时响应
系统卡死5秒
Task_Command 20ms报错
质变
10万次写入成功率
91.3%
99.97%
质变

在武汉某无人机数据记录仪项目中,三任务方案连续运行72小时,写入50万条飞行日志,零死锁、零丢数据。而之前的单任务方案平均每8小时死机一次——区别不是代码质量,是任务划分。

FATFS本身不难,难的是让它在多任务系统里不添乱。 消息队列解耦、互斥量保护、优先级隔离——这三板斧下去,SD卡就不再是系统的定时炸弹,而是最可靠的数据黑匣子。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读
关闭