五、使用文件字符串处理的功能。
1、目的
验证f_printf()函数的功能,学习变参数函数的编写和使用方法。
掌握格式化字符串的一些定义和用途。
2、有关格式控制串
格式控制串总是以%开始,后跟一些参数:
%【flags】【宽度】【精度控制】type。
前面三个是可选,而type是必须的参数。
(1) flag用于控制输出的 符号+(+-)、对齐方式-(左对齐)等。
(2) 宽度只控制输出的宽度,03,当字符数小于3个时,左边补0.
(3) 精度控制,主要针对浮点数。比如12.3456 如果%.2则只输出2位,变成12.34。
(4) 类型是必须的。这里主要分析这个。
3、源代码分析
int f_printf (
FIL* fil, /* Pointer to the file object */
const char* str, /* Pointer to the format string */
... /* Optional arguments... */
){ va_list arp;
UCHAR c, f, r;
ULONG val;
char s[16];
int i, w, res, cc;
va_start(arp, str); //arp实际是一个通用类指针,指向str后的第一个参数。
for (cc = res = 0; cc != EOF; res += cc) {
c = *str++;
if (c == 0) break; /* End of string */
if (c != '%') { /* Non escape cahracter */
cc = f_putc(c, fil); //如果不是%,表示正常字符,写入文件
if (cc != EOF) cc = 1; //cc是本次写对应的字符数字。当然如果写入不成功,返回EOF,则跳出循环
continue; //res是总共写入的数字。
}
w = f = 0; //从此处开始表示遇到了%后的处理
c = *str++; //取出下一个字符。
if (c == '0') { /* Flag: '0' padding */
f = 1; c = *str++; //如果是字符0,表示要控制宽度,flag第0位标志置位。取出下一字符
}
while (c >= '0' && c <= '9') { /* Precision */
w = w * 10 + (c - '0');
c = *str++; //依次取出后面的字符,如遇到034,则w=34。字符输出 }
if (c == 'l') { /* Prefix: Size is long int */
f |= 2; c = *str++; //如果遇到字符'l',表示长整形,标志第1位置位。 }
if (c == 's') { /* Type is string */
cc = f_puts(va_arg(arp, char*), fil); //如果遇到 ‘s’标志,将arp转换为char*指针,并指向下一个参数。
continue; //cc是字符串的长度,本次输入文件的字符数。
}
if (c == 'c') { /* Type is character */
cc = f_putc(va_arg(arp, int), fil); //字符以int类型出现,要占四个字节。
if (cc != EOF) cc = 1;
continue;
} r = 0; //如果字符'0'和'l'后,不是s和c,则下面继续处理
if (c == 'd') r = 10; /* Type is signed decimal */
if (c == 'u') r = 10; /* Type is unsigned decimal *///十进制
if (c == 'X') r = 16; /* Type is unsigned hexdecimal */
if (r == 0) break; /* Unknown type */
if (f & 2) { /* Get the value */
val = (ULONG)va_arg(arp, long);
} else {
val = (c == 'd') ? (ULONG)(long)va_arg(arp, int) : (ULONG)va_arg(arp, unsigned int);
} //如果设置了'l',标志,则将数据转换为长整形。从参数中取出来。
/* Put numeral string */ //下面是将数字转换为字符串。
if (c == 'd') {
if (val & 0x80000000) {
val = 0 - val;
f |= 4; //标志第二位表明这是个负数。
} }
i = sizeof(s) - 1; s = 0; //字符串最后一为以 '\0'结束。i指向尾端,往前扩展。
do { //同时处理十进制和十六进制。
c = (UCHAR)(val % r + '0'); //十进制时,1234/10,余数4+‘0’,变成字符
if (c > '9') c += 7; //十六进制,3A表示大写A,但实际的字符A是0x41,中间差7.
s[--i] = c; //前一位存储该字符。
val /= r; //val变成123,也就是把尾端 一位去掉。
} while (i && val); //最多存储15位,要不然溢出。
if (i && (f & 4)) s[--i] = '-'; //如果为负数,还要加上符号标志。
w = sizeof(s) - 1 - w; //要求的宽度计算。8位宽,则填充从第7位开始。7-14,正好8位。
while (i && i > w) s[--i] = (f & 1) ? '0' : ' ';//如果0填充置位填充0,否则填充空格。i退到0,或者i推导W都结束填充。
cc = f_puts(&s, fil);
} va_end(arp);
return (cc == EOF) ? cc : res;
}
把代码看了一遍,基本还是能看懂。
4、移植。
(1)添加命令fstring,该命令一个参数,是用户字符串。以附加的写入到指定的文件fstring.txt。
(2)实习方法
以写文件的方式,打开文件,然后指针移动到文件末尾。调用代码f_printf(&File,”输入字符串%s/r/n”,(const char*)argv[1])来写入文件。
(3)代码实现
res=f_open( &FileStr, "0:/doc/fstring.txt",FA_WRITE ); //
res=f_lseek( &FileStr, FileStr.fsize );
f_printf (&FileStr,"用户输入:%s\r\n",(char*)argv[1] );
f_close (&FileStr );
功能比较简单,测试通过。
六、支持长文件名(一)。
1、代码页的功能
(1)在原来的程序中,我把 #define _CODE_PAGE 936,代码页定义为936.
这时候可以在磁盘上新建中文名称的文件夹和文件,(通过fwrite命令实现,只要数目小于4个就行,也可以用fread命令读出来),也可以显示中文文件名。(通过命令flist来实现)。
(2)我把 #define _CODE_PAGE 1,代码页定义为1后,调用flist能正常显示中文名。但是调用fread和fwrite命令不能读取和创建中文名文件了。
进入代码分析,显示时关键在函数 get_fileinfo(),它原封不动的将系统标准名 转换为一般字符串,对字符串不做任何处理,送到串口以后自然可以正常显示。
但是读取文件和写入文件时,首先调用f_open()函数,进入follow_path()函数,然后调用create_name(),当它遇到大于 0x80 的字符时,产生如下效果:
if (c >= 0x80) { /* Extended char */
#ifdef _EXCVT
c = cvt[c - 0x80]; /* Convert extend char (SBCS) */
#else
b |= 3; /* Eliminate NT flag if ext char is exist */
#if !_DF1S /* ASCII only cfg */
return FR_INVALID_NAME;
#endif
#endif
}
返回FR_INVALID_NAME,所以它不识别中文GB2312(OEM)代码。
如果定义了define _CODE_PAGE==936,则同时就会定义DF1S,也同时定义了IsDBCS1(c)和IsDBCS2(d)。因此可以将中文OEM代码转换为标准文件名,存储于文件系统的目录项里面。
2、如果定义长文件名_USE_LFN,而不定义UNICODE码。有什么效果呢?
根据要求,先要添加ff_convert() and ff_wtoupper()两个函数。
结果发现,光两个转换表就要150K左右,我这个128k的空间是远远不够了。只能看代码而不能测试了。不定义UNICODE码的含义是用户提供的路径名不是以UNICODE形式出现的。
以下,就只能纸上谈兵了。
3、如果0:/doc 目录下有一个文件名 (这是一个very long的文件.它里面是空的.txt文档)。
如果要显示这个目录下所有的文件信息,我会在命令界面上输入:
Flist 0:/doc。
执行这个命令,程序首先调用
F_opendir(DIR dj,const void *path)函数,对输入的路径进行分析。这个函数首先调用函数 chk_mounted()检查磁盘上是否有文件系统,如果已经取得了信息,则很快返回。如果没有,则初始化磁盘,并读取信息,填充文件系统信息结构体FATFS。它去掉了路径前面的 0:。
F_opendir接下来调用follow_path(dj, path),这个函数根据路径填充目录信息结构体。这个函数首先设置dj->sclust,如果前面有/,会去掉这个符号。接下来一直读取目录名直到下一个/。这个工作调用create_name(dj, &path)来完成。
进入create_name(dj, &path),首先做的工作是根据路径填充dj->lfn指向的内存单元,由于我的这个目录是短文件名:doc,所以填入缓冲区的是 ‘d0‘,’o0‘,’c0‘,由于目录到此结束,所以NS_LAST属性被设置,表明已经到路径末尾。同时lfn[di] = 0,di此时指向c0后面那个内存单元。注意,存储进长目录区是以unicode的形式,而path用的是ANSi码,汉字两个字节,英文字母一个字节。
create_name(dj, &path)继续执行,mem_set(dj->fn, ' ', 11),先将dj结构体的标准短文件名填充空格。if (si) cf |= NS_LOSS | NS_LFN通过这句话的判断,长文件名前面没有空格和.,没有超出8.3格式。当然后面还要继续判断。执行while (di && lfn[di - 1] != '.') di—这句后,di=0,表示没有找到扩展名(doc没有扩展名)。dj->fn[i++] = (BYTE)w,通过这个方式将doc存进了短文件名存储区。dj->fn[NS] = cf,短文件名存储区12个字节,前11个是标准短文件名,最后一个是路径属性字节:是否.目录、大小写标志,超出8.3格式,是否长文件名、是否路径结束标志等等。
所以create_name(dj, &path)的工作主要是根据路径填充dj->lfn和dj->fn,前者每个字符16Byte,以‘\0’结尾,后者12个字节,以路径的整体属性结尾。
执行路径返回follow_path(dj, path),接下来调用dir_find(dj),这个函数的作用是在当前目录(对应dj->sclust)下,搜索dj->lfn和dj->fn对应的目录项,找到以后让dj->index和dj->dir指向该目录项的数据。追踪那个进入该函数。
在函数dir_find(dj)里,首先dir_seek(dj, 0),将索引定位于0,目录第一项。ord = sum = 0xFF长目录项索引和校验和设为-1。c = dir[DIR_Name]取出目录项的第一个字符进行判断,a = dir[DIR_Attr] & AM_MASK取出属性进行判断。建立一个循环,不断移动目录缓冲(每次下移32个字节),通过dir_next(dj, FALSE)来实现。当找到”doc “目录项时,if (!(dj->fn[NS] & NS_LOSS) && !mem_cmp(dir, dj->fn, 11)) break跳出循环,此时的有效信息就是dj->sclust加上dj->index和dj->dir,它指向根目录里的“doc”目录项。
执行路径返回follow_path(dj, path),由于last = *(dj->fn+NS) & NS_LAST,标志被置位,很快跳出路径分解的循环,返回到F_opendir(DIR dj,const void *path)。通过下面这个计算:
dj->sclust = ((DWORD)LD_WORD(dir+DIR_FstClusHI) << 16) | LD_WORD(dir+DIR_FstClusLO)真正得到了分配给doc目录的起始簇的地址。接下来就可以读取 0:/doc 目录下所有的目录项,并显示它包含文件、目录的属性信息了。
回到主程序执行路径,接下来调用:
f_readdir()这个函数功能是根据DIR结构体提供的信息,填充文件属性结构体,包括文件名字符串、文件大小、文件修改日期等。进入该函数进行追踪。这里就快要找到我的长文件名目录项《这是一个very long的文件.它里面是空的.txt文档》了,它在目录项里是怎样的存在形式呢?
它共有29个字(包括空格和.),共占据3个长目录项和一个短目录项:其格式应该如下。
f_readdir()这个函数调用dir_read()函数,读取各个目录项的信息,然后调用get_fileinfo()函数得到文件的各项信息。下面进入dir_read()进行跟踪,看它是怎样对待长目录项的。
进入dir_read()函数,首先ord, sum = 0xFF,读取属性a = dir[DIR_Attr] & AM_MASK,if (a == AM_LFN),也就是当遇到长目录索引“43”的时候,sum = dir[LDIR_Chksum];c &= 0xBF; ord = c; dj->lfn_idx = dj->index;取得校验和,取得索引(这里是3),使长目录索引指向“43”目录项的索引号。
如果pick_lfn(dj->lfn, dir)返回TRUE,则索引自动ord=ord-1变为2。进入pick_lfn(dj->lfn, dir),它的作用是将当前目录项里的长文件名部分写入dj->lfn指向的名字缓冲区。先从偏移26开始存入“t文档”三个unicode字符,然后写入0,返回TRUE。也就是ord=2。
继续执行,dir_next(dj, FALSE),dj->index指向下一项,a = dir[DIR_Attr] & AM_MASK,重新取得属性。此时c = dir[DIR_Name]=02,满足(c == ord && sum == dir[LDIR_Chksum] && pick_lfn(dj->lfn, dir))这个表达式,所以索引又被设置为ord=1。
同上,再次读取ord=1的那一项,填充长文件名缓冲区,至此unicode名称已经完全存入dj->lfn指向的缓冲区,并且ord=0,表明长目录项已经结束。
通过dir_next()再次循环,接下来dj->index指向对应的短目录项,只要sum_sfn(dir)计算出来的校验和与前面长目录项里取得的校验和相等,dir_read()函数的任务就算完成了。它获得的信息包括:dj->lfnidx(长目录项开始索引),dj->lfn(完整的unicode长文件名),dj->index和dj->dir指向短目录项。执行路径回到f_readdir()。
f_readdir()函数接下来调用get_fileinfo(dj, fno)获取目录项的详细信息。追踪该函数的执行。这个函数共获取六个信息:大小、属性、修改时间、日期、短文件名、长文件名字符串(通过读取dj->lfn指向的缓冲区,并将unicode转换为OEM代码)。有了这个文件属性结构体的数据,用户就可以在串口终端显示目录下所有目录项的信息了。
文章评论(0条评论)
登录后参与讨论