基于STM32完成FATFS文件系统移植与运用

一、环境介绍 主控mcu: stm32f103zet6 
stm32程序开发ide: keil5
stm32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植
硬件包含:  一块stm32f103zet6系统板、一个spi接口的sd卡卡槽模块、一张sd卡
这篇文章主要演示fatfs文件系统如何移植到自己的工程,并完成文件的读写。
因为sd卡采用的是spi模拟时序,所以,其他单片机一样可以照着移植,代码都可以复制粘贴的。


二、fatfs文件系统介绍 2.1 fatfs简介 fatfs 是一种完全免费开源的 fat 文件系统模块,专门为小型的嵌入式系统而设计。它完全用标准c 语言编写,所以具有良好的硬件平台独立性,可以移植到 8051、 pic、 avr、 sh、 z80、 h8、 arm 等系列单片机上而只需做简单的修改。它支持 fatl2、 fatl6 和 fat32,支持多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对 8 位单片机和 16 位单片机做了优化。
2.2 特点 windows兼容的fat文件系统 不依赖于平台,易于移植 代码和工作区占用空间非常小 多种配置选项 多卷(物理驱动器和分区) 多ansi/oem代码页,包括dbcs 在ansi/oem或unicode中长文件名的支持 rtos的支持 多扇区大小的支持 只读,最少api,i/o缓冲区等等 2.3 移植性 fatfs模块是ansi c(c89)编写的。 没有平台的依赖, 编译器只要符合ansi c标准就可以编译。
fatf模块假设大小的字符/短/长8/16/32位和int是16或32位。 这些数据类型在integer.h文件中定义。这些数据类型在大多数的编译器中定义都符合要求。 如果现有的定义与编译器有任何冲突发生时,需要自己解决。
2.4 源码下载 fatfs文件系统官网下载地址:http://elm-chan.org/fsw/ff/00index_e.html
这是在stm32上移植好的工程,可以直接下载使用,这个只要是stm32f103系列的所以芯片都可以使用工程:  https://download.csdn.net/download/xiaolong1126626497/19687693

fatfs有两个版本,一个大版本,一个小版本。小版本主要用于8位机(内存小)使用。
下载图:

2.5 fatfs源码文件介绍 将下载的源码解压后可以得到两个文件夹: doc 和 src。 doc 里面主要是对 fatfs 的介绍(离线文档—英文和日文),而 src 里面才是我们需要的源码。
其中,与平台无关的是:
ffconf.h     fatfs配置文件
ff.h        应用层头文件
ff.c        应用层源文件
diskio.h    硬件层头文件
interger.h   数据类型定义头文件
option     可选的外部功能(比如支持中文等)
与平台相关的代码:
diskio.c     底层接口文件(需要用户提供)
fatfs 模块在移植的时候,我们一般只需要修改 2 个文件,即 ffconf.h 和 diskio.c。
fatfs模块的所有配置项都是存放在 ffconf.h 里面,我们可以通过配置里面的一些选项,来满足自己的需求。
fatfs最顶层是应用层,使用者无需理会 fatfs 的内部结构和复杂的 fat 协议,只需要调用fatfs 模块提供给用户的一系列应用接口函数,如 f_open, f_read, f_write 和 f_close 等,就可以像在 pc 上读/写文件那样简单。
中间层 fatfs 模块, 实现了 fat 文件读/写协议。 fatfs 模块提供的是 ff.c 和 ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。
需要我们编写移植代码的是 fatfs 模块提供的底层接口,它包括存储媒介读/写接口 ( disk、i/o) 和供给文件创建修改时间的实时时钟。
三、 移植fatfs文件系统 移植之前,首先得准备一个能正常编译的工程,并且工程里有sd卡的驱动代码,提供了读写扇区这些函数才能进行fatfs文件系统的正常移植。
关于如何编写sd卡驱动,sd卡的时序介绍、命令介绍等知识点下篇文章再讲解。这篇文章重点是fatfs文件系统的移植过程。
3.1  新建工程 fatfs文件系统源码下载下来,解压之后,移植修改的步骤如下:

打开keil工程,添加fatfs文件源码:


加入.h文件主要是方便配。cc936.c 用于支持中文。
3.2  修改diskio.c文件 ​
注释掉现在不需要的用到的文件,因为我们现在用的是sd卡,与usb,ata,mmc卡没关系。
并加入一个新的宏 :
#define  sd  0
定义sd卡的物理驱动器号为0。 
修改 disk_status函数,该函数主要是用来获取磁盘状态。现在未用到,可以直接函数体内代码删除。
修改截图:

代码示例:
#include diskio.h           /* fatf底层api */
#include sd.h                      /* sd卡驱动头文件  */
/* 定义每个驱动器的物理驱动器号*/
#define sd    0
/*-----------------------------------------------------------------------*/
/* 获取设备(磁盘)状态                                                     */
/*-----------------------------------------------------------------------*/
dstatus disk_status (
        byte pdrv             /* 物理驱动识别 */
)
{
   return 0;  //该函数现在无需用到,直接返回0
}
修改disk_initialize函数,添加sd卡的初始化,其他不用到的代码直接删掉,该函数成功返回0,失败返回1。
修改截图:

代码示例:
/*-----------------------------------------------------------------------*/
/* 初始化磁盘驱动                                                        */
/*-----------------------------------------------------------------------*/
dstatus disk_initialize (
        byte pdrv                             /* 物理驱动识别 */
)
{
        dstatus stat;
        int result;
switch (pdrv) {
        case sd :            //选择sd卡
                stat=sd_init();   //初始化sd卡-用户自己提供
        }
        if(stat)return sta_noinit;  //磁盘未初始化
        return 0; //初始化成功
}
修改disk_read函数,加入sd卡读任意扇区的函数(需要用户自己提供),其他不用到的选项可以删掉。

修改代码如下:
/*-----------------------------------------------------------------------*/
/* 读扇区                                                                */
/*-----------------------------------------------------------------------*/
dresult disk_read (
        byte pdrv,            /* 物理驱动编号 - 范围0-9*/
        byte *buff,           /* 数据缓冲区存储读取数据 */
        dword sector,  /* 扇区地址*/
        uint count             /* 需要读取的扇区数*/
)
{
        dresult res;
        int result;
        switch (pdrv) {
                case sd:
                  res=sd_read_data((u8*)buff,sector,count);  //读sd扇区函数--用户提供
                  return res; //在此处可以判错误
        }
        return res_parerr;  //无效参数
}
修改disk_write 函数,添加写扇区函数:

代码:
/*-----------------------------------------------------------------------*/
/* 写扇区                                                                */
/*-----------------------------------------------------------------------*/
#if _use_write
dresult disk_write (
        byte pdrv,                      /* 物理驱动号*/
        const byte *buff,        /* 要写入数据的首地址 */
        dword sector,                 /* 扇区地址 */
        uint count                        /* 扇区数量*/
)
{
        dresult res;
        int result;
switch (pdrv) {
                case sd:
                        res=sd_write_data((u8*)buff,sector,count); //写入扇区
                  return res;
        }
        return res_parerr;  //无效参数
}
#endif
修改disk_ioctl 函数,填充ioctl命令功能。这些功能是标准的命令,在diskio.h有定义。

代码如下:
/*-----------------------------------------------------------------------*/
/* 其他函数                                              */
/*-----------------------------------------------------------------------*/
#if _use_ioctl
dresult disk_ioctl (
        byte pdrv,            /* 物理驱动号 */
        byte cmd,               /* 控制码  */
        void *buff               /* 发送/接收数据缓冲区地址 */
)
{
        dresult res;
        int result;
switch (pdrv) {
                case sd:
                         switch(cmd)
                         {
                                 case ctrl_sync:      //等待写过程
                                         sd_cs(0);          //选中sd卡
                                         if(sd_wait_ready())result = res_error;/*等待卡准备好*/
                                     else res = res_ok;     //成功
                                         sd_cs(1);            //释放sd卡
                        break;
case get_sector_size://获取扇区大小
                           *(dword*)buff = 512;
                        res = res_ok;     //成功
                        break;
case get_block_size:    //获取块大小
                                *(word*)buff = 8;      //块大小(扇区为单位),一块等于8个扇区
                         res = res_ok;
                         break;
case get_sector_count: //获取总扇区数量
                        *(dword*)buff = sd_get_sector_count();
                        res = res_ok;
                        break;
default:  //命令错误
                        res = res_parerr;
                        break;
                         }
                return res;
        }
        return res_parerr;  //返回状态
}
diskio.c 文件修改完整代码:
/*-----------------------------------------------------------------------*/
/* 低级别磁盘i / o模块框架fatf(c)chan)2014 
*存储控制模块fatf模块定义了一个api。      */
/*-----------------------------------------------------------------------*/
#include diskio.h           /* fatf底层api */
#include sd.h                      /* sd卡驱动头文件  */
/* 定义每个驱动器的物理驱动器号*/
#define sd    0
/*-----------------------------------------------------------------------*/
/* 获取设备(磁盘)状态                                                     */
/*-----------------------------------------------------------------------*/
dstatus disk_status (
        byte pdrv             /* 物理驱动识别 */
)
{
   return 0;  //该函数现在无需用到,直接返回0
}
/*-----------------------------------------------------------------------*/
/* 初始化磁盘驱动                                                        */
/*-----------------------------------------------------------------------*/
dstatus disk_initialize (
        byte pdrv                             /* 物理驱动识别 */
)
{
        dstatus stat;
        int result;
switch (pdrv) {
        case sd :           //选择sd卡
                stat=sd_init();   //初始化sd卡-用户自己提供
        }
        if(stat)return sta_noinit;  //磁盘未初始化
        return 0; //初始化成功
}
/*-----------------------------------------------------------------------*/
/* 读扇区                                                                */
/*-----------------------------------------------------------------------*/
dresult disk_read (
        byte pdrv,            /* 物理驱动编号 - 范围0-9*/
        byte *buff,           /* 数据缓冲区存储读取数据 */
        dword sector,      /* 扇区地址*/
        uint count             /* 需要读取的扇区数*/
)
{
        dresult res;
        int result;
switch (pdrv) {
                case sd:
                        res=sd_read_data((u8*)buff,sector,count);  //读sd扇区函数--用户提供
                  return res; //在此处可以判错误
        }
        return res_parerr;  //无效参数
}
/*-----------------------------------------------------------------------*/
/* 写扇区                                                                */
/*-----------------------------------------------------------------------*/
#if _use_write
dresult disk_write (
        byte pdrv,                      /* 物理驱动号*/
        const byte *buff, /* 要写入数据的首地址 */
        dword sector,                /* 扇区地址 */
        uint count                       /* 扇区数量*/
)
{
        dresult res;
        int result;
switch (pdrv) {
                case sd:
                        res=sd_write_data((u8*)buff,sector,count); //写入扇区
                  return res;
        }
        return res_parerr;  //无效参数
}
#endif
/*-----------------------------------------------------------------------*/
/* 其他函数                                              */
/*-----------------------------------------------------------------------*/
#if _use_ioctl
dresult disk_ioctl (
        byte pdrv,            /* 物理驱动号 */
        byte cmd,               /* 控制码  */
        void *buff               /* 发送/接收数据缓冲区地址 */
)
{
        dresult res;
        int result;
switch (pdrv) {
                case sd:
                         switch(cmd)
                         {
                                 case ctrl_sync:      //等待写过程
                                         sd_cs(0);          //选中sd卡
                                         if(sd_wait_ready())result = res_error;/*等待卡准备好*/
                                       else res = res_ok;     //成功
                                          sd_cs(1);          //释放sd卡
                             break;
case get_sector_size://获取扇区大小
                                *(dword*)buff = 512;
                        res = res_ok;     //成功
                        break;
case get_block_size:  //获取块大小
                                *(word*)buff = 8;      //块大小--一块等于8个扇区
                        res = res_ok;
                        break;
case get_sector_count: //获取总扇区数量
                        *(dword*)buff = sd_get_sector_count();
                        res = res_ok;
                        break;
default:  //命令错误
                        res = res_parerr;
                        break;
                         }
                return res;
        }
        return res_parerr;  //返回状态
}
#endif
//返回fatfs时间
//获得时间
dword get_fattime (void)
{     
        return (dword)(2017-1980)<<25|    //年
                                                    7<<21|    //月
                                                   27<<16|    //日
                                   12<<11|    //时
                                    13< 0:启用文件锁定功能。值定义了多少文件/子目录
可以同时打开的/文件锁的控制之下。注意,这个文件独立于re-entrancy /锁功能。 */
#define _fs_reentrant   0
#define _fs_timeout         1000
#define    _sync_t                 handle
/*  _fs_reentrant选项开关re-entrancy fatf的(线程安全)
/模块本身。注意,不管这个选项,文件访问不同
/体积始终是凹角和音量控制功能,f_mount(),f_mkfs()
/和f_fdisk()函数,总是不凹角。只有文件/目录的访问
/相同的体积是这个功能的控制。
/
/ 0:禁用re-entrancy。_fs_timeout和_sync_t没有效果。
/ 1:启用re-entrancy。还提供用户同步处理程序,
/ ff_req_grant(),ff_rel_grant(),ff_del_syncobj()和ff_cre_syncobj()
/函数,必须添加到项目中。样品中可用
/选项
/ syscall.c。
/
/  _fs_timeout定义超时时间单位的滴答声。
/ _sync_t定义了o
/ s依赖同步对象类型。例如处理、id、os_event *
/ semaphorehandle_t等. .o / s的头文件定义需要
/包括在ff.c的范围。 */
#define _word_access    0
/* _word_access选项是一个只有依赖于平台的选择。
它定义了这个词/访问方法是用来体积上的数据。
/
/ 0:逐字节的访问。总是兼容所有平台。
/ 1:词的访问。不要选择这个,除非在下列条件。
/
/ *地址对齐内存访问总是允许所有指令。
/ *字节顺序的记忆是低位优先。
/
/如果是这样的情况,_word_access也可以减少代码的大小设置为1。
/下表显示允许设置某种类型的处理器。
/
/  arm7tdmi   0   *2          coldfire   0    *1         v850e      0    *2
/  cortex-m3  0   *3          z80        0/1             v850es     0/1
/  cortex-m0  0   *2          x86        0/1             tlcs-870   0/1
/  avr        0/1             rx600(le)  0/1             tlcs-900   0/1
/  avr32      0   *1          rl78       0    *2         r32c       0    *2
/  pic18      0/1             sh-2       0    *1         m16c       0/1
/  pic24      0   *2          h8s        0    *1         msp430     0    *2
/  pic32      0   *1          h8/300h    0    *1         8051       0/1
/
/   
* 1:高位优先。/
* 2:不支持不连续的内存访问。/
* 3:一些编译器生成ldm(逻辑磁盘管理器 ) / stm mem_cpy(内存拷贝)函数。
*/
3.4 实现动态内存分配函数与时间函数 ff.h文件有动态内存的释放,动态内存申请,时间获取函数接口。

在diskio.c文件实现函数功能:

代码实现如下:
//动态内存分配
void* ff_memalloc (uint msize)                      /* 分配内存块 */
{
        return (void*)malloc(msize); //分配空间
}
//动态内存释放
void ff_memfree (void* mblock)                       /* 空闲内存块 */
{
        free(mblock);              //释放空间
}
//返回fatfs时间
//获得时间
dword get_fattime (void)
{     
        //get_rtc_timer(); //获取一次rtc时间
                return (rtc_timer.year-1980)<<25|   //年
                          rtc_timer.month<<21|  //月
                       rtc_timer.day<<16|    //日
                       rtc_timer.hour<<11|   //时
                       rtc_timer.minute<<5|  //分
                       rtc_timer.sec;        //秒
}
/*
return value
currnet local time is returned with packed into a dword value. the bit field is as follows:
bit31:25
year origin from the 1980 (0..127)
bit24:21
month (1..12)
bit20:16
day of the month(1..31)
bit15:11
hour (0..23)
bit10:5
minute (0..59)
bit4:0
second / 2 (0..29)
*/
3.5 修改堆栈空间 完成了上述的修改,还需要修改堆栈空间,因为长文件支持需要占用堆空间。
修改stm32启动文件如下:

3.6 编译工程测试 修改完毕之后,给开发板插上sd卡,调用api函数在sd卡创建一个文件,并写入数据,测试是否成功:
#include ff.h
fatfs fs;  // 用户定义的文件系统结构体
fil  file;  // 用户定义的文件系统结构体
u8 buff[]=123 知识!!;
int main(void)
{
        u32 data;                //检测sd卡容量
        u8 i,res;
    led_init();              //led灯初始化
    delay_init();
    key_init();
    usart1_init(72,115200);
    usart2_init(36,115200);
    flash_init();
          set_font_addr(); //字库地址初始化
          fsmc_sram_init();
          lcd_init();
          rtc_init();     //rtc时钟初始化
          while(sd_init())    //检测不到sd卡,sd相关硬件初始化
                {
                        i=!i;
                        lcd_showstring(60,150,200,16,16,sd card error!  please check sd card!!,0xf800);                                
                        delay_ms(500);
                        led1(i)//ds0闪烁
                }
f_mount(&fs,0,1);  // 注册工作区,驱动器号 0,初始化后其他函数可使用里面的参数
                printf(注册工作区!\n);
if(f_mkfs(0,0,4096))  //格式化sd卡
                {
                        printf(格式化失败!!\n);
                }
                else
                {
                        printf(格式化成功!!\n);
                }
                res = f_open(&file, /file.c, fa_open_always | fa_read | fa_write);
                if(res==0)
                {
                        printf(文件创建成功!!\n);
                }
                else
                {
                        printf(文件创建失败!!\n);
                }
                res =f_write(&file,buff,strlen((const char*)buff),&data);
                if(res==0)
                {
                        printf(数据写入成功!!\n);
                }
                else
                {
                        printf(数据写入失败!!\n);
                }
                printf(成功写入%d字节数据\n,data);
                f_close(&file);  //关闭文件
                //_fs_rpath
while(1)
                {
                        delay_ms(1000);
                        led1(1);
                        delay_ms(500);
                        led1(0);
                }
}


停止2G网络服务!AT&T宣布已关闭2G网络
来看看温度传感器的十八般武艺
到2022年中国数据中心业务市场规模将超过3200.5亿元
议程直击 | 第二届OpenHarmony技术大会——OS原生智能分论坛
荣耀Note9什么时候上市?全面屏+大电池新机下月发布,要价3000你们会买吗?
基于STM32完成FATFS文件系统移植与运用
C语言入门教程-创建一个函数库
Microsemi宣布提供新一代工业温度碳化硅标准功率模块
对于医疗电子而言,3D打印意味着什么?
新国标将把老年代步车划归乘用车!
台积电Q4业绩提振,但明年收入增长将放缓
半导体教父张忠谋:RFID将抢物联网商机
英飞拓致力于推进AI在安防领域的普惠化
博通CEO:3000亿可穿戴设备市场孕育下一个苹果
法Carmat人工心脏将首次植入于人体进行试验
三星Galaxy S10概念图亮相:正面没开孔 屏下前置镜头有望首发
深耕产业服务14年 上达电子斩获BOE SPC 2018“卓越服务奖”
教育信息化2.0时代开场:交互智能平板行业迎来洗牌期
2020年中国智能水表行业有什么发展
为什么说物联网是一场连接革命