0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

【嵌入式SD NAND】基于FATFS/Littlefs文件系统的日志框架实现

jim 来源:雷龙发展 作者:雷龙发展 2024-03-14 18:13 次阅读

文章目录

嵌入式】基于FATFS/Littlefs文件系统的日志框架实现

1. 概述

2. 设计概要

3. 设计实现

3.1 初始化 `init`

3.2 日志写入 `write`

3.3 日志读取 `read`

3.4 注销 `deinit`

3.5 全部代码汇总

4. 测试

5. 总结

1. 概述

那么在移植好了文件系统之后,我们又应该如何应用文件系统呢?

很多人会说,这个简单,就操作文件嘛!open、read、write、close不就行了吗!当然对于简单的使用,掌握open、read、write、close,去存储一两个文件或者从一两个文件中简单的读取下数据这确实没有什么难度。但在实际应用中,特别是产品开发过程中,往往不只是简单的操作一两个文件就可以的,如果真是这样,那费那么大劲移植文件系统多少有点浪费!

在实际项目开发中,往往需要依托文件系统操作诸多的文件,操作诸多的数据。如通过配置文件配置机器设备信息、通过升级文件进行产品升级、通过存放字库文件实现多语言支持等等,这些都是比较简单的操作,读写不是很频繁,相对来说实现比较简单,还有一类需求读写会相当频繁,且大多数产品内都希望存在的,那便是日志文件,通过日志文件来记录设备的运行数据。日志文件不同于其他功能,其往往需要具备几个基本特性需求:

单个文件大小限制

日志总大小空间占用限制

自动循环覆盖

网上也有一些开源的日志框架,如 Log4j,不过大都是基于 java / c ++ 实现的,虽然功能比较全面,但比较繁杂,且也难以移植应用于嵌入式开发中。而在嵌入式开发中,可能也受限于资源限制,并没有发现不错的基于文件系统的开源日志框架(至少博主目前没有发现,有的话欢迎大家评论区讨论 ),对于如何实现一个日志框架很多人一下子可能没有头绪,综上,本文将分享一个简单的基于文件系统的日志程序以供大家思考。

2. 设计概要

我们需要实现的日志模块的核心需求为:

单个文件大小限制

日志总大小空间占用限制

自动循环覆盖

对于一个模块,对外仅需提供其操作的接口即可,内部的算法实现均无需对外开放,而对于此日志模块,对外只需提供基本的以下四个接口即可:

初始化 init

写日志 write

读日志 read

注销 deinit

关于日志存储的核心思想如下:

写数据之前先判断当前操作的文件是否超出单个文件大小限制,如超出大小限制则进行日志轮转,创建一个新的日志文件并判断日志文件总大小是否超出限制,如果超出则删除最早的那一份日志文件

关于日志存储的详细设计如下:

日志文件格式采用:.log ,当当前文件达到单个文件大小之后,进行文件轮转;

假定当前限制日志每个日志文件大小为2048Byte,最多存储10个文件;

当当前文件达到单个文件大小之后,迭代修改日志文件名:

.log -> .log.0

.log.0 -> .log.1

.log.1 -> .log.2

.log.8 -> .log.9

删除 .log.9

ps:注意实际代码操作的时候,文件修改顺序是反过来的,也就是先 删除.log.9再将.log.8->.log.9

3. 设计实现

3.1 初始化init

初始化部分代码主要功能是完成日志数据结构体的构造,并通过传入参数log_file_cfg_t cfg配置日志文件的配置信息,如单个日志文件大小、日志文件名、最多存放的日志文件数等内容,日志模块初始化部分代码如下:

log_file_t log_storage_init(log_file_cfg_t cfg)

{

log_file_t log = NULL;

log_file_cfg_t log_cfg = NULL;

log_file_read_t log_read = NULL;

log = (log_file_t)malloc(sizeof(struct log_file_config));

if (log == NULL)

goto error;

log_cfg = (log_file_cfg_t)malloc(sizeof(struct log_file_config));

if (log_cfg == NULL) {

free(log);

log = NULL;

goto error;

}

log_read = (log_file_read_t)malloc(sizeof(struct log_file_read));

if (log_read == NULL) {

free(log);

log = NULL;

free(log_cfg);

log_cfg = NULL;

goto error;

}

memcpy(log_cfg, cfg, sizeof(struct log_file_config));

log_read->rotate_index = 0;

log_read->file_offset = 0;

log->cfg = log_cfg;

log->read = log_read;

log->user_data = NULL;

error:

return log;

}

3.2 日志写入write

日志写入部分代码主要分为两大部分,一部分是正常写入,另一部分是文件轮转;当写入的文件超过单个文件大小限制时,即会触发文件轮转操作。

在文件轮转中,主要做的是:创建一个新的日志文件并判断日志文件总大小是否超出限制,如果超出则删除最早的那一份日志文件,具体设计细节可参考上文设计概要中的详细设计部分。

实现代码如下:

static int log_rotate(log_file_t log)

{

int ret = 0;

FILE *fp;

char old_filename[NAME_MAX + 10] = {0};

char new_filename[NAME_MAX + 10] = {0};

for (int i = log->cfg->rotate_num; i > 0; i --) {

memset(old_filename, 0, sizeof(old_filename));

memset(new_filename, 0, sizeof(new_filename));

snprintf(old_filename, sizeof(old_filename), i ? "%s_%d.log" : "%s.log", log->cfg->filename, i - 1);

snprintf(new_filename, sizeof(new_filename), "%s_%d.log", log->cfg->filename, i);

printf("old:%s new:%sn", old_filename, new_filename);

if ((fp = fopen(new_filename, "r")) != NULL) {

if (fclose(fp) != 0) {

ret = -1;

goto error;

}

if (remove(new_filename) != 0) {

ret = -2;

goto error;

}

}

if ((fp = fopen(old_filename, "r")) != NULL) {

if (fclose(fp) != 0) {

ret = -1;

goto error;

}

if (rename(old_filename, new_filename) != 0) {

ret = -3;

goto error;

}

}

}

error:

return ret;

}

int log_storage_write(log_file_t log, const unsigned char *buf, unsigned int len)

{

int ret = 0;

int file_size = 0;

char full_filename[NAME_MAX + 5] = {0};

FILE *fp = NULL;

if (log == NULL || log->cfg == NULL || log->read == NULL || buf == NULL || len == 0) {

ret = -1;

goto param_error;

}

snprintf(full_filename, sizeof(full_filename), "%s.log", log->cfg->filename);

printf("fullfilename:%sn", full_filename);

log_file_lock();

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

fseek(fp, 0L, SEEK_END);

file_size = ftell(fp);

printf("file_size:%dn", file_size);

if ((file_size + len) > log->cfg->max_size) {

if (fclose(fp) != 0) {

ret = -3;

goto error;

}

int j = 0;

j = log_rotate(log);

printf("log rotate:%dn", j);

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

}

if (fwrite(buf, len, 1, fp) != 1) {

fclose(fp);

ret = -4;

goto error;

}

error:

if (fp != NULL) {

if (fclose(fp) != 0) {

ret = -3;

goto error;

}

}

log_file_unlock();

param_error:

return ret;

}

3.3 日志读取read

此处日志读取在本文主题中非重点设计内容,因此此处做简单设计,通过传入参数判断应该读取哪一份文件之后进行直接读取。设计代码如下:

int log_storage_read(log_file_t log, unsigned int rotate_num, unsigned char *buf, unsigned int *len)

{

int ret = 0;

int file_size = 0;

char full_filename[NAME_MAX + 5] = {0};

FILE *fp = NULL;

if (log == NULL || log->cfg == NULL || log->read == NULL || buf == NULL || len == 0) {

ret = -1;

goto param_error;

}

if (rotate_num == 0)

snprintf(full_filename, sizeof(full_filename), "%s.log", log->cfg->filename);

else

snprintf(full_filename, sizeof(full_filename), "%s.log.%d", log->cfg->filename, rotate_num);

log_file_lock();

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

/* check file length. */

fseek(fp, 0L, SEEK_END);

file_size = ftell(fp);

printf("file_size:%dn", file_size);

if (file_size < *len)

*len = file_size;

fseek(fp, 0L, SEEK_SET);

if (fread(buf, *len, 1, fp) != 1) {

ret = -3;

fclose(fp);

goto error;

}

error:

if (fp != NULL) {

if (fclose(fp) != 0) {

ret = -4;

goto error;

}

}

log_file_unlock();

param_error:

return ret;

}

3.4 注销deinit

注销的主要功能是将我们在init时创建的数据结构进行回收,如果模块内部有功能处于打开装填,也应关闭模块的功能,此处我们仅需对init时创建的log_file_t log数据结构体进行注销、内存回收即可,具体代码实现如下:

int log_storage_deinit(log_file_t log)

{

if (log == NULL)

return -1;

if (log->cfg != NULL)

free(log->cfg);

if (log->read != NULL)

free(log->read);

if (log->user_data != NULL)

free(log->user_data);

free(log);

return 0;

}

3.5 全部代码汇总

日志模块内核头文件:simple_storage.h

#ifndef __SIMPLE_STORAGE_H__

#define __SIMPLE_STORAGE_H__

#define NAME_MAX 40

struct log_file_config {

const char filename[NAME_MAX]; /* Filename of this type. */

int max_size; /* single file max size. */

int rotate_num; /* The number of files that support rotate. */

};

typedef struct log_file_config* log_file_cfg_t;

struct log_file_read {

int rotate_index; /* The rotate file index. */

int file_offset; /* The offset of the currently read file. */

};

typedef struct log_file_read* log_file_read_t;

struct log_file {

log_file_cfg_t cfg;

log_file_read_t read;

void *user_data;

};

typedef struct log_file* log_file_t;

log_file_t log_storage_init(log_file_cfg_t cfg);

int log_storage_write(log_file_t log, const unsigned char *buf, unsigned int len);

int log_storage_read(log_file_t log, unsigned int rotate_num, unsigned char *buf, unsigned int *len);

int log_storage_deinit(log_file_t log);

#endif /* __SIMPLE_STORAGE_H__ */

日志模块内核文件:simple_storage.c

#include "simple_storage.h"

#include "simple_storage_port.h"

#include

#include

log_file_t log_storage_init(log_file_cfg_t cfg)

{

log_file_t log = NULL;

log_file_cfg_t log_cfg = NULL;

log_file_read_t log_read = NULL;

log = (log_file_t)malloc(sizeof(struct log_file_config));

if (log == NULL)

goto error;

log_cfg = (log_file_cfg_t)malloc(sizeof(struct log_file_config));

if (log_cfg == NULL) {

free(log);

log = NULL;

goto error;

}

log_read = (log_file_read_t)malloc(sizeof(struct log_file_read));

if (log_read == NULL) {

free(log);

log = NULL;

free(log_cfg);

log_cfg = NULL;

goto error;

}

memcpy(log_cfg, cfg, sizeof(struct log_file_config));

log_read->rotate_index = 0;

log_read->file_offset = 0;

log->cfg = log_cfg;

log->read = log_read;

log->user_data = NULL;

error:

return log;

}

static int log_rotate(log_file_t log)

{

int ret = 0;

FILE *fp;

char old_filename[NAME_MAX + 10] = {0};

char new_filename[NAME_MAX + 10] = {0};

for (int i = log->cfg->rotate_num; i > 0; i --) {

memset(old_filename, 0, sizeof(old_filename));

memset(new_filename, 0, sizeof(new_filename));

snprintf(old_filename, sizeof(old_filename), i ? "%s_%d.log" : "%s.log", log->cfg->filename, i - 1);

snprintf(new_filename, sizeof(new_filename), "%s_%d.log", log->cfg->filename, i);

printf("old:%s new:%sn", old_filename, new_filename);

if ((fp = fopen(new_filename, "r")) != NULL) {

if (fclose(fp) != 0) {

ret = -1;

goto error;

}

if (remove(new_filename) != 0) {

ret = -2;

goto error;

}

}

if ((fp = fopen(old_filename, "r")) != NULL) {

if (fclose(fp) != 0) {

ret = -1;

goto error;

}

if (rename(old_filename, new_filename) != 0) {

ret = -3;

goto error;

}

}

}

error:

return ret;

}

int log_storage_write(log_file_t log, const unsigned char *buf, unsigned int len)

{

int ret = 0;

int file_size = 0;

char full_filename[NAME_MAX + 5] = {0};

FILE *fp = NULL;

if (log == NULL || log->cfg == NULL || log->read == NULL || buf == NULL || len == 0) {

ret = -1;

goto param_error;

}

snprintf(full_filename, sizeof(full_filename), "%s.log", log->cfg->filename);

printf("fullfilename:%sn", full_filename);

log_file_lock();

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

fseek(fp, 0L, SEEK_END);

file_size = ftell(fp);

printf("file_size:%dn", file_size);

if ((file_size + len) > log->cfg->max_size) {

if (fclose(fp) != 0) {

ret = -3;

goto error;

}

int j = 0;

j = log_rotate(log);

printf("log rotate:%dn", j);

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

}

if (fwrite(buf, len, 1, fp) != 1) {

fclose(fp);

ret = -4;

goto error;

}

error:

if (fp != NULL) {

if (fclose(fp) != 0) {

//TODO: check the amount of disk space, delete if there is not enough space.

ret = -3;

goto error;

}

}

log_file_unlock();

param_error:

return ret;

}

int log_storage_read(log_file_t log, unsigned int rotate_num, unsigned char *buf, unsigned int *len)

{

int ret = 0;

int file_size = 0;

char full_filename[NAME_MAX + 5] = {0};

FILE *fp = NULL;

if (log == NULL || log->cfg == NULL || log->read == NULL || buf == NULL || len == 0) {

ret = -1;

goto param_error;

}

if (rotate_num == 0)

snprintf(full_filename, sizeof(full_filename), "%s.log", log->cfg->filename);

else

snprintf(full_filename, sizeof(full_filename), "%s.log.%d", log->cfg->filename, rotate_num);

log_file_lock();

fp = fopen(full_filename, "a+b");

if (fp == NULL) {

ret = -2;

goto error;

}

/* check file length. */

fseek(fp, 0L, SEEK_END);

file_size = ftell(fp);

printf("file_size:%dn", file_size);

if (file_size < *len)

*len = file_size;

fseek(fp, 0L, SEEK_SET);

if (fread(buf, *len, 1, fp) != 1) {

ret = -3;

fclose(fp);

goto error;

}

error:

if (fp != NULL) {

if (fclose(fp) != 0) {

ret = -4;

goto error;

}

}

log_file_unlock();

param_error:

return ret;

}

int log_storage_deinit(log_file_t log)

{

if (log == NULL)

return -1;

if (log->cfg != NULL)

free(log->cfg);

if (log->read != NULL)

free(log->read);

if (log->user_data != NULL)

free(log->user_data);

free(log);

return 0;

}

在日志模块源文件的代码中,我们可以看到实际每次操作文件的时候,都有调用一个函数锁操作,考虑到不同平台的锁操作实现不一样,因此将此部分通过函数导出来,放置在模块的端口文件中。不同的平台、系统根据各自的平台和系统的情况进行实现,如像裸机编程这类不需要进行锁操作的不进行函数实现即可。

日志模块端口头文件:simple_storage_port.c

#ifndef __SIMPLE_STORAGE_PORT_H__

#define __SIMPLE_STORAGE_PORT_H__

int log_file_init(void);

int log_file_lock(void);

int log_file_unlock(void);

#endif /* __SIMPLE_STORAGE_PORT_H__ */


日志模块端口源文件:simple_storage_port.h

#include "simple_storage_port.h"

int log_file_init(void)

{

return 0;

}

int log_file_lock(void)

{

return 0;

}

int log_file_unlock(void)

{

return 0;

}

4. 测试

将以上代码进行运行测试,硬件平台如下:

控制器stm32f103vet6,野火指南者开发板

存储芯片: CS创世 SD nand,型号:CSNP4GCR01-AMW

文件系统: FATFS,注意此日志不受文件系统限制

操作系统RT-Thread,此模块与操作系统无关,此处只是方便使用故自行移植了rtthread

wKgZomXyza2APA-TACTREhVCSEw691.png

应用层代码如下:

int main(void)

{

/* 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_SDIO_SD_Init();

MX_USART1_UART_Init();

MX_FATFS_Init();

/* USER CODE BEGIN 2 */

struct log_file_config log_cfg = {

.filename = "test",

.max_size = 2048,

.rotate_num = 10,

};

log_file_t log = NULL;

log = log_storage_init(&log_cfg);

if (log == NULL)

return;

/* USER CODE END 2 */

/* Infinite loop */

/* USER CODE BEGIN WHILE */

unsigned char buf[2048] = {0};

int len = 0;

while (1) {

// ... 省略用户代码

/* 写入测试 */

for (int i = 0; i < 2048; i++) {

log_storage_write(log, "hello world", sizeof("hello world"));

rt_thread_mdelay(100);

}

/* 读取测试 */

len = sizeof(buf);

memset(buf, 0, sizeof(buf));

log_storage_read(log, 1, buf, &len);

for (int i = 0; i < len; i ++)

rt_kprintf("%c", buf[i]);

rt_thread_mdelay(1000);

}

}


测试结果如下:

msg> hello worldhello world hello world hello world hello world hello world hello world hello world hello world ...省略

msh > ls

test.log 2046

test.log.0 2046

test.log.1 2046

test.log.2 2046

test.log.3 2046

test.log.4 2046

5. 总结

综上便是基于文件系统的简易日志模块设计的全部内容了,虽然简陋了点,但相信对于大部分没有接触过日志系统设计的人来说提供了很好的一条设计思路。

也正因为简易,给大家对于日志系统设计的优化留足了大量的优化空间。比如:

文件轮转的时候需要对每个文件的文件名进行修改,是否可以有更好的方式不用每个文件都修改呢?

文件名的设计是不方便阅读的,是否可以引入时间参数?

文件名设计如何引入了时间参数,当设备RTC备用电池掉电的时候又如何保证文件不会被错误覆盖?

文件的读取显然优化空间更大,实际上用户不应该传入rotate_num 参数,因为这是模块内部的参数,用户不可感知的

文件读取如何做到分多次读取一个文件的内容,且不会重复,是顺序读取?

等等,以上只是我简单想到的几点内容,大家不妨思考下如何实现方案更好呢?当然又还有哪些需求是需要引入的呢,也欢迎大家在评论区留言,关注我,后续抽时间再分享下改良版日志系统!!!

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 文件系统
    +关注

    关注

    0

    文章

    273

    浏览量

    19676
  • 开发板
    +关注

    关注

    25

    文章

    4434

    浏览量

    94018
  • 存储芯片
    +关注

    关注

    11

    文章

    797

    浏览量

    42454
  • RT-Thread
    +关注

    关注

    31

    文章

    1149

    浏览量

    38894
收藏 人收藏

    评论

    相关推荐

    STM32CubeMx入门教程(10):Fatfs文件系统的应用

    导语"fatfs是一个小型的文件系统,在小型的嵌入式系统中使用非常的广泛,STM32CubeMx自带该文件系统,我们通过简单的配置就能够使用
    发表于 07-12 11:39 2631次阅读
    STM32CubeMx入门教程(10):<b class='flag-5'>Fatfs</b><b class='flag-5'>文件系统</b>的应用

    STM32+SD NAND(贴片SD卡)完成FATFS文件系统移植与测试

    这篇文章就手把手教大家,在STM32上完成FATFS文件系统的移植;主控芯片采用STM32F103ZET6, 存储芯片我这里采用(雷龙) CS创世 SD NAND
    的头像 发表于 07-17 17:24 4579次阅读
    STM32+<b class='flag-5'>SD</b> <b class='flag-5'>NAND</b>(贴片<b class='flag-5'>SD</b>卡)完成<b class='flag-5'>FATFS</b><b class='flag-5'>文件系统</b>移植与测试

    嵌入式SD NAND】基于FATFS/Littlefs文件系统日志框架实现

    文章目录 【嵌入式】基于FATFS/Littlefs文件系统日志框架
    发表于 03-14 18:12

    转:STM32CubeMX系列教程18:文件系统FATFS

    FATFS简介 FatFS是一个为小型嵌入式系统设计的通用FAT(File Allocation Table)文件系统模块。
    发表于 07-06 16:57

    嵌入式文件系统µC/FS的日志使用

    尽管在PC领域NTFS已经取代了FAT,但FAT文件系统仍然是嵌入式开发的首选。除了为嵌入式应用程序提供与PC(因为Windows继续支持FAT)的无缝交互,对于电源不稳定的设备开发者来说
    发表于 09-19 16:41

    读写SD嵌入式系统中的作用

    读写SD嵌入式系统中一个比较基础的功能,在很多应用中都可以用得上SD卡。折腾了几天,总算移植成功了 最新版Fatfs
    发表于 08-05 08:14

    fatFs/LittleFs/RelianceEdge Fs/LwExt4嵌入式文件系统写入速度对比哪个快?

    fatFs/LittleFs/RelianceEdge Fs/LwExt4嵌入式文件系统写入速度对比哪个快?
    发表于 12-27 06:37

    讲一讲在FatFs文件系统下读取SD卡的该如何做

    1、前言上一篇文章我讲述了在SDIO模式下读取SD卡,在文章最后说了需要注意的地方,同时也是裸机下利用SDIO模式的不足,今天给大家讲一讲在FatFs文件系统下读取SD卡的该如何做,以
    发表于 03-02 07:08

    求一种在rtthread系统上添加并使用文件系统的设计方案

    里创建几个目录,用于elm-FAT文件系统littlefs文件系统的挂载点。elm-FAT文件系统FatFs 是一个通用的
    发表于 05-06 14:42

    基于SD卡的FATFS文件系统的研究与应用_崔鹏伟

    基于SD卡的FATFS文件系统的研究与应用_崔鹏伟。
    发表于 04-14 16:46 40次下载

    Fatfs文件系统的移植)

    Fatfs文件系统的移植)一、文件系统介绍二、移植条件、说明1、FatFs模块在可移植性方面设定了以下条件:2、数据类型说明3、系统
    发表于 11-15 18:51 22次下载
    <b class='flag-5'>Fatfs</b>(<b class='flag-5'>文件系统</b>的移植)

    文件系统FatFs文件系统嵌入式芯片LPC18XX上的移植

    文件系统FatFs文件系统嵌入式芯片LPC18XX上的移植
    发表于 12-04 10:51 12次下载
    【<b class='flag-5'>文件系统</b>】<b class='flag-5'>FatFs</b><b class='flag-5'>文件系统</b>在<b class='flag-5'>嵌入式</b>芯片LPC18XX上的移植

    基于OpenHarmony3.1的LittleFS文件系统hdf驱动实现

    一、简介LittleFS是一个小型的Flash文件系统,它结合日志结构(log-structured)文件系统和COW(copy-on-write)
    的头像 发表于 06-22 09:42 514次阅读
    基于OpenHarmony3.1的<b class='flag-5'>LittleFS</b><b class='flag-5'>文件系统</b>hdf驱动<b class='flag-5'>实现</b>

    基于STM32+CS创世 SD NAND(贴片SD卡)完成FATFS文件系统移植与测试(下篇)

    四、移植FATFS文件系统前面第3章,完成了SDNAND的驱动代码编写,这一章节实现FATFS文件的移植。4.1
    的头像 发表于 03-03 13:52 878次阅读
    基于STM32+CS创世 <b class='flag-5'>SD</b> <b class='flag-5'>NAND</b>(贴片<b class='flag-5'>SD</b>卡)完成<b class='flag-5'>FATFS</b><b class='flag-5'>文件系统</b>移植与测试(下篇)

    嵌入式SD NAND】基于FATFS/Littlefs文件系统日志框架实现

    文章目录【嵌入式】基于FATFS/Littlefs文件系统日志框架
    的头像 发表于 03-14 18:12 716次阅读
    【<b class='flag-5'>嵌入式</b><b class='flag-5'>SD</b> <b class='flag-5'>NAND</b>】基于<b class='flag-5'>FATFS</b>/<b class='flag-5'>Littlefs</b><b class='flag-5'>文件系统</b>的<b class='flag-5'>日志</b><b class='flag-5'>框架</b><b class='flag-5'>实现</b>