韩晓雪,曾鸣,邵贝贝
(清华大学 工程物理系,北京100084)
随着信息技术的发展,嵌入式系统简单地对存储介质按地址、字节进行读写的方式已经不能满足实际应用的需求,利用文件系统对存储介质进行管理成为嵌入式系统的一个发展方向。虽然目前存在很多版本的文件系统,但Windows的广泛应用使得FAT文件系统仍然是最通用的文件系统之一。本文中基于MC9S12UF32单片机,结合开源文件系统FatFS,设计实现了使用FAT文件系统的大容量数据存储模块。
数据存储系统框架如图1所示。该数据存储模块以Freescale公司的 MC9S12UF32为核心,串行通信接口SCI接收到的数据可以直接通过单片机以FAT文件的形式存储在micro SD卡中。用户程序可以通过编程控制单片机,直接对micro SD卡中的任意文件进行读写,实现存储数据。由于实现了FAT文件系统,用户也可以通过单片机内置的USB接口将本数据模块识别为U盘,用 PC机进行读写操作。此外,模块中的SD卡也可以取出,使用标准的读卡器可在任何PC机上读出。
本文所介绍的数据存储插件由 MC9S12UF32、DS12887实时钟模块、micro SD卡、串行通信接口以及USB接口5部分组成。实时时钟,可以为数据存储模块的文件系统提供正确的时间戳信息,在精简的系统设计中,这个部分则可以省略。
图1 系统框架
Freescale公司生产的这款16位单片机具有3.5 KB RAM和32 KB Flash EEPROM。它最大的特点在于拥有USB2.0接口、ATA5接口以及 SD/MMC、SmartMedia、MemoryStick等多种存储卡接口。本文所介绍的数据存储插件采用MC9S12UF32单片机内部集成的SD主控制器模块(SDHC)实现micro SD卡的底层读写。
(1)SDHC模块
SD1.0规范协议中定义了对SD卡的两种访问模式:SD模式和SPI模式。使用SDHC(Secured Digital card Host Controller)模块对SD卡读写采用了SD模式。该模块将SD总线转换为MC9S12UF32内部的IPBus总线或者IQUE总线,使用者只需要对SDHC模块相关的寄存器进行配置,就可以实现向SD卡发送各种命令和读写数据的功能。SD卡与SDHC的连接如图2所示。MC9S12UF32内部集成的 SDHC模块支持SD卡1.0版本的物理层协议,所以本系统使用标准 micro SD卡(而非SDHC卡),其存储容量最大为2G。这样的存储容量已经完全可以满足大多数嵌入式应用的需求。
(2)MC9S12UF32与SD卡之间的数据传输
在完成对时钟频率和传输数据线宽度的配置之后,通过发送相应的读写命令就可以实现单片机与SD卡之间的数据传输了。在单片机向SD卡写入数据的过程中,编程者将需要写入的数据写入SDHC模块的SDATA寄存器(16位)之后,该数据将被转移至发送数据FIFO中。与此同时,只要发送数据FIFO非空,其中的数据就会不断地通过数据线被写入SD卡的相应位置。在单片机从SD卡中读取数据的过程中,SD卡中的数据将不断发送至接收数据FIFO中。只要接收FIFO非空,单片机就可以不断地通过读SDATA寄存器得到接收数据FIFO中的数据。
图2 SD卡与SDHC的连接
DS12887模块使用数据/地址复用的并行异步总线,可以为单片机提供100年以内的实时钟信息(年/月/日/时/分/秒)。它内部具有石英晶振和锂电源,首次使用时,需要对该模块内部的寄存器进行相应配置,激活晶振使其进入工作状态。由于DS12887内部带有锂电源,所以一旦晶振被激活,即使外部掉电,该模块依然可以保存并提供正确的实时钟信息。
设定DS12887的时间和从DS12887中读取时间信息的底层程序比较简单,只需参照芯片手册对寄存器进行合理配置。但是,需要特别注意的是,在写时钟和读取时钟之前需要锁存时钟信息相关的buffer,防止在读写过程中由于出现时钟信息自动更新情况而导致的错误。系统中,在每次读写实时钟之前,查询DS12887内部控制寄存器A最高位UIP是否为0,以避免上述错误的产生。因为芯片手册中给出,一旦UIP=0,那么在244 μ s内实时钟模块都不会自动更新当前的时钟信息,而这段时间足以让单片机完成读写实时钟的过程。
采用文件系统,是为了在单片机能够对SD卡进行数据读写的同时,保证其读写的数据能够被大多数通用设备识别。换言之,数据在存储器内的组织型式,需要遵循一些已有的工业标准和规范。例如使用FAT文件系统,数据存储单元的SD卡取下来后,可以使用任何标准的读卡器在Windows、Linux等PC机上读出。
当前著名的嵌入式文件系统有若干种选择,比如EFSL(Embedded File system Library)、uC/FS 、/TinyFatFS等。这之中EFSL和FatFs都是开放源码的,具有十分详尽的文档和函数手册,除错更新也十分及时,在本文的设计中我们采用的是FatFS。
FatFs采用使用ANSI C编写,具有很好的硬件平台独立性,使用者只需要对源程序进行简单的修改和配置,就可以将其移植到各种系列的单片机上。此外,它的内存开销很小,ROM的占有量在十几KB的量级,使用者可以根据不同的应用方便的对代码进行裁减。FatFs支持FAT12、FAT16和FAT32,可以建立独立的缓冲区对多个文件进行读写。FatFs是一个不断更新完善的软件,大量的相关信息可以从原作者的主页上得到(http://elmchan.org/fsw/ff/00index_e.html),同时原作者也做了很多性能测试的工作。
可从FatFS的主页上下载得到FatFs R0.07版本。FatFs的主程序包含 5个文件,即diskio.c、diskio.h、ff.c、ff.h和integer.h。其中,diskio.c和diskio.h是与底层硬件I/O相关的函数;ff.c和ff.h是应用函数,主要涉及FatFs的配置和裁减;而integer.h中定义了FatFs软件所使用的各种数据类型。
移植FatFs的过程中基本不需要对diskio.h和ff.c进行修改。除了核实integer.h中的数据类型定义是否与MC9S12U32数据类型相符之外,移植的重点工作在于diskio.c中6个主要函数的实现和ff.h中对于文件系统的裁减配置。dikio.c包含的6个接口函数:disk_initialize,disk_status,disk_ioctl,disk_read,disk_write和disk_fattime。它们分别实现存储介质的初始化、读取/写入若干个扇区的数据和获取实时钟信息的功能。
具体移植过程如下:
(1)存储媒介初始化函数
DSTATUS disk_initialize(BYTE drv)
由于采用的存储媒介是SD卡,所以该函数的实际功能是对SD卡进行初始化。drv是存储介质号码,由于Tinv-FatFs只支持一个存储介质,所以此处drv始终取0值。执行无误,则返回值=0;执行中出现错误,则返回非0值。
(2)状态检测函数
DSTATUS disk_status(BYTE drv)
该函数用于检测是否支持当前的存储介质。此处的drv仍然恒为0。对Tiny-FatFs而言,只要drv为0,就认为支持当前介质,函数直接返回0值即可。
(3)读扇区函数
DRESULT disk_read(BYTE drv,BYTE*buff,DWORD sector,BYTE.count)
该函数是在“单片机从SD卡读取一个扇区”的函数基础上编写而成的,其功能是从SD卡读取一个或多个扇区的数据。*buff用于存储已经读取的数据,sector是待读取扇区的起始扇区数,count是需要读取的扇区数。如果执行无误则返回0值,否则返回非0值。
(4)写扇区函数
DRESULT disk_write(BYTE drv,const BYTE*buff,DWORD sector,BYTE count)
与disk_read相似,该函数是在“单片机向SD卡写入一个扇区”的函数基础上编写而成的,其功能是向SD卡导入一个或多个扇区的数据。*buff用于保存将要写入的数据,sector是待写入扇区的起始扇区数,count是需要写入的扇区数。如果执行无误则返回0值,否则返回非0值。
(5)存储介质控制函数
DRESULT disk_ioctl(BYTE drv,BYTE ctrl,VoiI*buff)
ctrl是控制代码,*buff用于保存或接收需要控制的数据数据。使用者可以在此函数里添加自己需要的功能代码,例如获得存储介质的容量、扇区数等。如果是简单的应用,也可以不执行任何功能,直接返回0值。本文采用的就是这一方法。
(6)实时钟函数
DWORD disk_fattime(Void)
该函数将读取的实时钟信息保存在一个32位无符号整数中,并将其作为函数的返回值。时钟信息在这32位中的具体分布如表1所列。
表1 返回值DWORD中包含的时钟信息
FatFs提供了丰富的库函数,可以实现创建、读取文件夹,创建、读写文件,移动文件指针,向文件中写入或读取字符串,甚至是类似与C语言fprintf()的格式化输入等各种功能。使用者可以根据自己的需求设置相应的宏,对FatFs进行裁减,仅保留需要的功能函数,从而精简文件系统的内存开销。FatFs提供的函数与宏的对应关系如图3所示。
图3 FatFs提供的库函数
FatFs的裁减,不仅仅是函数层面的。更重要的是,在内部机制上形成一个精简版本,称为Tiny-FatFs。它与标准版FatFs相比,主要的区别在于Tiny-FatFs仅支持一个物理存储介质,而且不再针对每个开启的文件建立512字节的缓存,整个文件系统和物理介质使用同一个缓存。显然,Tiny-FatFs需要的内存开销比标准版FatFs更低,只要1 KB左右的RAM。可以说,Tiny-FatFs是专门为小型嵌入式系统而设计的文件系统模块。本文介绍的数据存储系统使用的正是Tiny-FatFs版本。
在表2中,对ff.h中主要配置宏的含义进行了说明,同时给出了本文所介绍的数据存储模块采用的取值。
表2 ff.h中的主要参数配置说明
FatFs文件系统中涉及2个基本的数据结构:文件系统(磁盘)的数据结构FATFS和文件的数据结构FIL。这两个结构是FatFs软件主要的RAM开销,FATFS数据结构中有针对磁盘的512字节读写缓存,FIL则有针对每个文件的缓存。而采用Tiny FatFS配置则不会开设文件读写缓存,节约RAM。
依次使用f_mount、f_open、f_read/f_write、f_close 可以完成基本的读写。FatFs允许对同一文件同时复数读取,但完全不支持对同一文件同时复数的写入操作,因为这会引起文件系统错误。具体每一个函数,特别是字符串读写、格式化读写等,可以参见原始帮助和例程。
此外,由于嵌入式系统具有突然掉电的可能性,一些关键代码段可能导致文件系统错误,所以要注意调用f_sync()及时写入。当然,如果是一组连续的f_write()写入,而每次写完都f_sync(),则会极大地影响速度,可以全部写完后f_sync()。
V0.07以后版本的FatFS,增加了以下新的功能:
①_FS_TINY。Tiny模式变成了一个宏选项,而不是独立的代码包。
②_FS_RPATH。决定是否有当前路径的概念,这将影响两个相关函数的参数。
③_USE_LFN。启用长文件名支持,可为1或2,为2时可重入。由于长文件名存在堆栈上,而且启用LFN会依据代码页增加一个很大的转换表,占掉几十~几百KB,所以不推荐。
④_LFN_UNICODE。长文件名使用Unicode,实验阶段,尚未正式写入文档。
MC9S12UF32单片机内部集成的 SDHC模块,可将SD总线转换为单片机内部的IP总线,开发者只需要对SD协议的基本内容有所了解,通过读写相应的寄存器就可以方便地实现对SD卡的底层读写,大大简化了硬件的开发过程。同时,独立于硬件平台的FatFs软件包可以方便地移植到各种嵌入式系统中,研发者只需要对该软件包的diskio.c和 ff.h进行修改,即可完成移植,从而使用FatFs提供的丰富且易于使用的各种接口函数。
应用上述主要技术实现的具有嵌入式文件系统的数据模块如图4所示。
图4 数据存储模块实物图
该模块体积小巧、存储数据的灵活性和通用性很高,可以通过模块自带的串行通信接口接收数据,并以文件的形式存储起来。用户既可以直接通过USB接口将本模块识别为U盘进行数据读写和分析,也可以将micro SD卡拔出,在任意一个具有micro SD读卡器功能的设备上读写数据。上述功能特性使得这款数据存储模块具有很良好的应用前景。
[1]Motorola Inc.MC9S12UF32 System on a Chip Guide V01.04,2004.
[2]Dallas Semiconductor.DS12887 Real-Time Clock.