对于固态存储器,其存储容量可以很大,往往需要一款文件系统对存储器用户数据进行组织文件的管理。它对文件存储器空间进行组织和分配,负责文件的存储并对存入的文件进行保护和检索。在嵌入式系统中,往往需要采用windows兼容的文件系统,像相机的照片、视频监控、语音产品等,很多都需要从windows计算机上提取资源或在windows计算机上进一步处理。Fatfs由于其开源免费,支持fat32,受到了广泛的应用,笔者此处就s3c2416移植Fatfs,对sd卡进行读写访问作一个简单的介绍。

1、Fatfs概述

Fatfs是由日本工程师ChaN所编写的fat文件系统模块,从06年发布第一个Fatfs版本开始,作者就从未停止维护和更新。Fatfs的编写遵循ANSI C,并且完全与磁盘I/O层分开。它不依赖于硬件架构,代码和工作区占用空间小,使之可以嵌入到各个低成本的微控制器中,如AVR、8051、PIC、ARM、Z80、68K等。

2、代码准备

Fatfs源码,请读者自行从Fatfs官网 http://elm-chan.org/fsw/ff/00index_e.html 下载最新的源码。 s3c2416启动代码工程,启动代码是s3c2416/50/51这系列arm9芯片在运行用户c代码main函数之前必须先运行的代码,启动代码支持sd、Nand启动,为用户设置系统时钟,初始化内存,自动识别启动设备并搬移代码到RAM,MMU映射,中断管理等,用户只需专注于用c开发其它功能函数即可。关于启动代码以及启动代码的实现过程,笔者前面章节有非常详细的介绍。此处以GCC下移植Fatfs为讲解,下载”GCC启动代码工程应用实例”中的启动代码源码即可。如果在MDK下开发,下载”MDK启动代码工程应用实例”中的启动代码源码。

用户代码,用c开发的所有功能代码,其中,用户代码入口为main()函数,在这里需要实现sd卡驱动模块等。

3. 工程搭建

在linux操作系统下任一路径下新建一个Fatfs_GCC的工程目录,该目录下新建Fatfs目录,下载Fatfs最新源码并解压,把src目录内容全部拷贝到Fatfs目录下。

把启用代码目录start_code拷贝到Fatfs_GCC目录下,这部分代码无需任何的修改。并保留其中的Makefile这些文件,按照Makefile的模板添加各个目录的Makefile。GCC启动代码下的工程管理Makefile提取自uboot,可以方便地增加源代码以及代码目录。

在Fatfs_GCC目录下新建apps目录,用来保存应用相关的源码以及各个模块驱动。

4. Fatfs移植

Fatfs模块完全独立于磁盘I/O层,因此底层磁盘I/O访问并不属于Fatfs的模块部分,用户必须自己实现这部分用来访问存储设备。通常在diskio.c中实现这六个函数disk_initialize()、disk_status()、disk_read()、disk_wirte()、disk_ioctl()、get_fattime()即可。如果使能了OS相关的特性,则还需额外实现进程/内存函数。sd卡底层驱动实现在前面的章节有详细的介绍,此处直接在Fatfs移植接口中调用sd驱动模块中的相关函数。

4.1. disk_initialize函数

初始化存储设备,若设备初始化成功,应清除STA_NOINIT这个标志返回。若初始化不成功,应置位STA_NOINIT标志再返回。如果在初始化时,未检测到卡,可设置STA_NODISK标志表明无卡,检测到写保护,可设置STA_PROTECT标志表明写保护。

  1. static DSTATUS State = STA_NOINIT;
  2. DSTATUS disk_initialize (
  3. BYTE pdrv /* Physical drive nmuber (0..) */
  4. )
  5. {
  6. if (pdrv != 0) {
  7. return STA_NOINIT; // 仅支持driver0
  8. }
  9. if (!Hsmmc_Init()) { // 调用sd卡初始化
  10. State &= ~STA_NOINIT; // 初始化成功
  11. } else {
  12. State |= STA_NOINIT;
  13. }
  14. return State;
  15. }

4.2. disk_status函数

获取设备的状态,返回STA_NOINIT、STA_NODISK、STA_PROTECT这三个标志的组合。磁盘设备的状态随时都可能发生变化,例如初始化后的sd卡在某一时刻被拔出,此时无卡,Fatfs通过disk_status函数重新获知STA_NODISK无卡这一标志。

  1. DSTATUS disk_status (
  2. BYTE pdrv /* Physical drive nmuber (0..) */
  3. )
  4. {
  5. if (pdrv != 0) {
  6. return STA_NOINIT; // 仅支持driver0
  7. }
  8. return State;
  9. }

4.3. disk_read函数

读取扇区,Fatfs通过该函数从磁盘某一扇区地址开始获取一块或多块扇区的数据,Fatfs最多支持一次性读写128个扇区的数据,通常磁盘都支持多块读、多块写,并且这样的读写性能远远好于分单块的读写。

  1. DRESULT disk_read (
  2. BYTE pdrv, /* Physical drive nmuber (0..) */
  3. BYTE *buff, /* Data buffer to store read data */
  4. DWORD sector, /* Sector address (LBA) */
  5. UINT count /* Number of sectors to read (1..128) */
  6. )
  7. {
  8. if (pdrv || !count) {
  9. return RES_PARERR;
  10. }
  11. if (State & STA_NOINIT) {
  12. return RES_NOTRDY;
  13. }
  14. if (!Hsmmc_ReadBlock(buff, sector,count)) {
  15. return RES_OK; // 读取成功
  16. } else {
  17. return RES_ERROR; // 读取出错
  18. }
  19. }

4.4. disk_wirte函数

写扇区,Fatfs通过该函数从磁盘某一扇区地址开始写入一块或多块扇区的数据。如果只读(_FS_READONLY == 1),可以不实现该函数。

  1. #if _USE_WRITE
  2. DRESULT disk_write (
  3. BYTE pdrv, /* Physical drive nmuber (0..) */
  4. const BYTE *buff, /* Data to be written */
  5. DWORD sector, /* Sector address (LBA) */
  6. UINT count /* Number of sectors to write (1..128) */
  7. )
  8. {
  9. if (pdrv || !count) {
  10. return RES_PARERR;
  11. }
  12. if (State & STA_NOINIT) {
  13. return RES_NOTRDY;
  14. }
  15. if (State & STA_PROTECT) {
  16. return RES_WRPRT;
  17. }
  18. if (!Hsmmc_WriteBlock((unsignedchar *)buff, sector, count)) {
  19. return RES_OK; // 写成功
  20. } else {
  21. return RES_ERROR; // 写错误
  22. }
  23. }
  24. #endif

4.5. disk_ioctl函数

控制设备相关的功能,Fatfs使用5个设备独立的命令控制/获取设备特定的功能。

CTRL_SYNC:写同步,在关闭文件等操作时,如果磁盘I/O口层使用了写缓存,那么通知磁盘I/O口层把写缓存中的数据写回到磁盘中。对于没有写缓存,即每次disk_write均写入到磁盘中,无需处理该命令,只需返回RES_OK即可。在可写时_FS_READONLY == 0,该命令才会被使用。

GET_SECTOR_COUNT:获取磁盘的总扇区数,在用f_mkfs()格式化文件系统,f_fdisk对磁盘分区时均会使用这个命令来获取磁盘的总扇区数,对于sd卡,通过CSD获取卡容量信息。在支持格式化文件系统或多分区的情况下(_USE_MKFS == 1 或 _MULTI_PARTITION== 1),该命令才会被使用。

GET_SECTOR_SIZE:获取磁盘一个扇区的字节数,有效值为512、1024、2048或4096。对于大部分的系统,所有内存卡,硬盘,通常返回扇区大小为512字节,但对于flash,一页可能为512字节,也可能为1k字节,2k字节,4k字节,需要根据具体的flash页大小进行配置。

GET_BLOCK_SIZE:以扇区为单位获取擦除块的大小。在用f_mkfs()格式化文件系统时,用来使数据区对齐到擦除块。例如,第一个擦除块往往用来保存系统信息等,真正的数据在第二个擦除块位置开始存放。该命令并不重要,可直接返回1表明1个扇区对齐。此处与原作者移植例程保持一致,对sd2.0版本卡,返回AU(可分配单元)的大小,sd1.0版本卡,返回擦除块大小。在_USE_MKFS == 1时,该命令才被使用。

CTRL_ERASE_SECTOR:擦除某一段扇区,对于flash,都是要先擦除才能正确写入,对于nor、nand flash,某一个文件不再使用时(例如删除或被覆盖),先发出这个命令强制设备擦除这个文件所在的空间区域,之后在disk_write无需再对flash进行擦除操作,因为每次文件不再使用时,都已经先擦除了这部分。对于sd卡等,这个命令没有任何用处,因为sd卡接收到块写命令均是先擦除再写。如果再开启这个命令_USE_ERASE == 1,相当重复擦除。对于sd卡,建议设置_USE_ERASE == 0,不使用这个命令。

  1. #if _USE_IOCTL
  2. DRESULT disk_ioctl (
  3. BYTE pdrv, /* Physical drive nmuber (0..) */
  4. BYTE cmd, /* Control code */
  5. void *buff /* Buffer to send/receive control data */
  6. )
  7. {
  8. unsigned char CSD[16];
  9. unsigned charSdState[64];
  10. unsigned int c_size,c_size_multi, read_bl_len, sector_size, au_size;
  11. DRESULT Result =RES_ERROR;
  12. if (pdrv) {
  13. return RES_PARERR;
  14. }
  15. if (State &STA_NOINIT) {
  16. return RES_NOTRDY;
  17. }
  18. switch (cmd) {
  19. case CTRL_SYNC:
  20. Result = RES_OK;// 写sd卡驱动函数确保了写完才返回,已写同步了
  21. break;
  22. case GET_SECTOR_COUNT:/* Get drive capacity in unit of sector (DWORD) */
  23. if(!Hsmmc_Get_CSD(CSD)) {
  24. if((CSD[15]>>6) == 1) { // CSD v2.00->SDHC卡
  25. c_size =((CSD[8]&0x3f) << 16) + (CSD[7]<<8) + CSD[6]; // [69:48]
  26. // 卡容量为字节(c_size+1)*512K byte,以1扇区512 byte字,卡的扇区数为
  27. *(DWORD*)buff = (c_size+1) << 10;
  28. } else { //CSD v1.0->sd V1.x, sd v2.00 standard
  29. read_bl_len= CSD[10] & 0xf; // [83:80]
  30. c_size_multi= ((CSD[6] & 0x3) << 1) +
  31. ((CSD[5] & 0x80) >> 7); // [49:47]
  32. c_size =((WORD)(CSD[9]&0x3) << 10) +
  33. ((WORD)CSD[8]<<2)+ (CSD[7]>>6); // [73:62]
  34. // 卡容量为字节(c_size+1)*2^(c_size_multi+2)*2^(read_bl_len)
  35. // 以1扇区 512 byte计,卡扇区数为
  36. *(DWORD*)buff = (c_size + 1) << (read_bl_len +
  37. (c_size_multi + 2) - 9);
  38. }
  39. Result =RES_OK;
  40. }
  41. break;
  42. case GET_SECTOR_SIZE : /* Get sector size in unit of byte (WORD) */
  43. *(WORD *)buff =512;
  44. Result = RES_OK;
  45. break;
  46. case GET_BLOCK_SIZE:/* Get erase block size in unit of sector (DWORD) */
  47. if(!Hsmmc_Get_CSD(CSD)) {
  48. if((CSD[15]>>6) == 1) { // CSD v2.00, SDHC卡
  49. // v2.00扇区大小等信息在CSD中不再有效,需从sd state获取这些信息
  50. if(!Hsmmc_GetSdState(SdState)) {
  51. //Allocation Unit(AU)[431:428],SdState[0]保存的是状态的高位
  52. au_size= (SdState[10] >> 4);
  53. // 1au单位为16k,转化为512字节扇区数
  54. *(DWORD*)buff = 16UL << au_size;
  55. Result= RES_OK;
  56. }
  57. } else { //CSD v1.0,sd version 1.x, sd version 2.00 standard
  58. sector_size =((CSD[5] & 0x3f) << 1) + (CSD[4] >> 7); // [45:39]
  59. // 擦除块大小为扇区大小*块长度(512为单位)
  60. *(DWORD*)buff = (sector_size + 1) << ((CSD[2] >> 6) - 1);
  61. Result =RES_OK;
  62. }
  63. }
  64. break;
  65. caseCTRL_ERASE_SECTOR: /* Erase a block of sectors (used when _USE_ERASE == 1) */
  66. if (!Hsmmc_EraseBlock(((DWORD*)buff)[0], ((DWORD *)buff)[1])) {
  67. Result =RES_OK;
  68. }
  69. break;
  70. default:
  71. break;
  72. }
  73. return Result;
  74. }
  75. #endif

4.6. get_fattime函数

用来获取当前RTC的时间,从1980年开始计算时间戳,用来记录文件的创建时间,修改时间等。如果觉得文件的时间记录无关紧要,可直接返回0。在只读系统中(_FS_READONLY == 1),该函数不会被调用。

  1. #if _USE_WRITE
  2. DWORD get_fattime()
  3. {
  4. RTC_Time Time;
  5. RTC_GetTime(&Time);
  6. return (((Time.Year - 1980)<< 25) | (Time.Month << 21) |
  7. (Time.Day << 16) |(Time.Hour << 11) |
  8. (Time.Min << 5) |(Time.Sec << 1));
  9. }
  10. #endif

get_fattime函数需要用到RTC模块驱动,对于s3c2416,RTC驱动模块RTC.c的实现如下:

  1. #include "s3c2416.h"
  2. #include "RTC.h"
  3. void RTC_GetTime(RTC_Time *pTime)
  4. {
  5. rRTCCON |= (1 << 0);
  6. pTime->Year = 2000 + ((rBCDYEAR& 0xf0) >> 4)*10 + (rBCDYEAR & 0xf);
  7. pTime->Month =((rBCDMON>>4) & 0x1)*10 + (rBCDMON & 0xf);
  8. pTime->Day =((rBCDDATE>>4) & 0x3)*10 + (rBCDDATE & 0xf);
  9. pTime->Week = (rBCDDAY &0x7) - 1; // Sunday = 0
  10. pTime->Hour =((rBCDHOUR>>4) & 0x3)*10 + (rBCDHOUR & 0xf);
  11. pTime->Min =((rBCDMIN>>4) & 0x7)*10 + (rBCDMIN & 0xf);
  12. pTime->Sec = ((rBCDSEC>>4)& 0x7)*10 + (rBCDSEC & 0xf);
  13. rRTCCON &= ~(1 << 0);
  14. }
  15. void RTC_SetTime(RTC_Time *pTime)
  16. {
  17. rRTCCON = (1<<0); // Tick1hz
  18. rBCDYEAR =((pTime->Year%100/10) << 4 | (pTime->Year%100%10) << 0);
  19. rBCDMON = ((pTime->Month/10)<< 4) | ((pTime->Month%10) << 0);
  20. rBCDDATE = ((pTime->Day/10)<< 4) | ((pTime->Day%10) << 0);
  21. rBCDDAY = (pTime->Week+1); //Sunday = 0;
  22. rBCDHOUR = ((pTime->Hour/10)<< 4) | ((pTime->Hour%10) << 0);
  23. rBCDMIN = ((pTime->Min/10)<< 4) | ((pTime->Min%10) << 0);
  24. rBCDSEC = ((pTime->Sec/10)<< 4) | ((pTime->Sec%10) << 0);
  25. rRTCCON &= ~(1 << 0);
  26. }
  27. void RTC_Init(const RTC_Time *pTime)
  28. {
  29. unsigned char YearTens, YearOnes;
  30. rRTCCON |= (1 << 0);
  31. YearTens = (rBCDYEAR & 0xf0)>> 4;
  32. YearOnes = (rBCDYEAR & 0xf)>> 0;
  33. // 初始化RTC为2014,若已是2014认为时间设置已初始化
  34. if ((YearTens != 1) || YearOnes< 4 || YearOnes >= 8) {
  35. rRTCCON = (5<<5) |(0<<4) | (1<<0); // Tick 1hz
  36. rBCDYEAR =((pTime->Year%100/10) << 4 | (pTime->Year%100%10) << 0);
  37. rBCDMON =((pTime->Month/10) << 4) | ((pTime->Month%10) << 0);
  38. rBCDDATE = ((pTime->Day/10)<< 4) | ((pTime->Day%10) << 0);
  39. rBCDDAY = (pTime->Week+1);// Sunday = 0;
  40. rBCDHOUR =((pTime->Hour/10) << 4) | ((pTime->Hour%10) << 0);
  41. rBCDMIN = ((pTime->Min/10)<< 4) | ((pTime->Min%10) << 0);
  42. rBCDSEC = ((pTime->Sec/10)<< 4) | ((pTime->Sec%10) << 0);
  43. }
  44. rRTCCON &= ~(1 << 0);
  45. }

RTC模块头文件RTC.h如下:

  1. #ifndef __RTC_H__
  2. #define __RTC_H__
  3. #ifdef __cplusplus
  4. extern "C" {
  5. #endif
  6. typedef struct RTC_Time {
  7. unsigned short Year;
  8. unsigned char Month;
  9. unsigned char Day;
  10. unsigned char Hour;
  11. unsigned char Min;
  12. unsigned char Sec;
  13. unsigned char Week;
  14. } RTC_Time;
  15. void RTC_SetTime(RTC_Time *pTime);
  16. void RTC_GetTime(RTC_Time *pTime);
  17. void RTC_Init(const RTC_Time *pTime);
  18. #ifdef __cplusplus
  19. }
  20. #endif
  21. #endif /*__RTC_H__*/

至此Fatfs模块磁盘底层IO调用接口全部移植完成。

5. 应用测试

移植好Fatfs后,即可在main中调用Fatfs中的api函数实现对sd卡读写文件的操作。这部分的测试内容main.c如下:

  1. #include "s3c2416.h"
  2. #include "UART0.h"
  3. #include "ff.h"
  4. #include "diskio.h"
  5. #include "RTC.h"
  6. const unsigned char TestData[] = {
  7. "\tGCC下Fatfs的移植\r\n"
  8. "象棋小子\t1048272975\r\n"
  9. "对于固态存储器,其存储容量可以很大,往往需要一款文件系统对存储器用户数据\r\n"
  10. "进行组织文件的管理。它对文件存储器空间进行组织和分配,负责文件的存储并对\r\n"
  11. "存入的文件进行保护和检索。在嵌入式系统中,往往需要采用windows兼容的文件\r\n"
  12. "系统,像相机的照片、视频监控、语音产品等,很多都需要从windows计算机上\r\n"
  13. "提取资源或在windows计算机上进一步处理。Fatfs由于其开源免费,支持fat32,\r\n"
  14. "受到了广泛的应用,笔者此处就s3c2416移植Fatfs,对sd卡进行读写访问作一个\r\n"
  15. "简单的介绍。\r\n"
  16. };
  17. unsigned char TestData1[sizeof(TestData)];
  18. int main()
  19. {
  20. unsigned int i;
  21. FATFS fs;
  22. FIL file;
  23. FRESULT Res;
  24. unsigned int ByteWrite, ByteRead;
  25. RTC_Time Time = {
  26. 2014, 5, 22, 23, 00, 0, 5
  27. };
  28. RTC_Init(&Time);
  29. Uart0_Init();
  30. RTC_GetTime(&Time);
  31. Uart0_Printf("Time:%4d/%02d/%02d %02d:%02d:%02d\n\r", Time.Year,
  32. Time.Month, Time.Day,Time.Hour, Time.Min, Time.Sec);
  33. f_mount(&fs, "" ,0);
  34. /* Res = f_mkfs("", 0,4096);
  35. if (Res != RES_OK) {
  36. Uart0_Printf("f_mkfserror %d", Res);
  37. while(1);
  38. }
  39. */
  40. Res= f_open(&file, "test.txt", FA_WRITE | FA_CREATE_ALWAYS);
  41. if (Res != RES_OK) {
  42. Uart0_Printf("Create filefailed\n\r");
  43. while(1);
  44. }
  45. Res = f_write(&file, (unsignedchar *)&TestData, sizeof(TestData), &ByteWrite);
  46. if (Res != RES_OK) {
  47. f_close(&file);
  48. Uart0_Printf("Write fileerror\n\r");
  49. while(1);
  50. }
  51. Uart0_Printf("WriteText.txt:\r\n%s\r\n", TestData);
  52. f_close(&file);
  53. Res = f_open(&file, "test.txt",FA_READ | FA_OPEN_EXISTING);
  54. if (Res != RES_OK) {
  55. Uart0_Printf("Open filefailed\n\r");
  56. while(1);
  57. }
  58. Res = f_read(&file, (unsignedchar *)&TestData1, sizeof(TestData1), &ByteRead);
  59. if (Res != RES_OK) {
  60. Uart0_Printf("Read fileerror\n\r");
  61. while(1);
  62. }
  63. Uart0_Printf("ReadText.txt:\r\n%s\r\n", TestData1);
  64. f_close(&file);
  65. if (ByteRead != ByteWrite) {
  66. Uart0_Printf("Comparefailed\r\n");
  67. }
  68. for (i=0; i<sizeof(TestData);i++) {
  69. if (TestData1[i] !=TestData[i]) {
  70. break;
  71. }
  72. }
  73. Uart0_Printf("Total compare%d, abort at i = %d", sizeof(TestData), i);
  74. while(1) {
  75. }
  76. }

6. 附录

由于在GCC在进行裸机开发,glibc并不适合,应通过arm-linux-gcc编译生成嵌入式库,如newlib,uclibc等。并在Makefile中更正嵌入式c库的路径,才能make。

Fatfs_GCC,GCC下Fatfs移植工程,更改Makefile中c库的路径,进行make即可。

http://pan.baidu.com/s/1gtXhG

Fatfs_MDK,MDK下Fatfs移植工程。

http://pan.baidu.com/s/1ntE30EX