FATFS学习(3.4):ff.c(f_read)

发布于:2025-03-07 ⋅ 阅读:(85) ⋅ 点赞:(0)

一:f_read

FRESULT f_read (
	FIL* fp, 	/* Open file to be read */
	void* buff,	/* Data buffer to store the read data */
	UINT btr,	/* Number of bytes to read */
	UINT* br	/* Number of bytes read */
)
{
	FRESULT res;
	FATFS *fs;
	DWORD clst;
	LBA_t sect;
	FSIZE_t remain;
	UINT rcnt, cc, csect;
	BYTE *rbuff = (BYTE*)buff;


	*br = 0;	/* Clear read byte counter */
	res = validate(&fp->obj, &fs);				/* Check validity of the file object */
	if (res != FR_OK || (res = (FRESULT)fp->err) != FR_OK) LEAVE_FF(fs, res);	/* Check validity */
	if (!(fp->flag & FA_READ)) LEAVE_FF(fs, FR_DENIED); /* Check access mode */
	remain = fp->obj.objsize - fp->fptr;
	if (btr > remain) btr = (UINT)remain;		/* Truncate btr by remaining bytes */

	for ( ; btr > 0; btr -= rcnt, *br += rcnt, rbuff += rcnt, fp->fptr += rcnt) {	/* Repeat until btr bytes read */
		if (fp->fptr % SS(fs) == 0) {			/* On the sector boundary? */
			csect = (UINT)(fp->fptr / SS(fs) & (fs->csize - 1));	/* Sector offset in the cluster */
			if (csect == 0) {					/* On the cluster boundary? */
				if (fp->fptr == 0) {			/* On the top of the file? */
					clst = fp->obj.sclust;		/* Follow cluster chain from the origin */
				} else {						/* Middle or end of the file */
#if FF_USE_FASTSEEK
					if (fp->cltbl) {
						clst = clmt_clust(fp, fp->fptr);	/* Get cluster# from the CLMT */
					} else
#endif
					{
						clst = get_fat(&fp->obj, fp->clust);	/* Follow cluster chain on the FAT */
					}
				}
				if (clst < 2) ABORT(fs, FR_INT_ERR);
				if (clst == 0xFFFFFFFF) ABORT(fs, FR_DISK_ERR);
				fp->clust = clst;				/* Update current cluster */
			}
			sect = clst2sect(fs, fp->clust);	/* Get current sector */
			if (sect == 0) ABORT(fs, FR_INT_ERR);
			sect += csect;
			cc = btr / SS(fs);					/* When remaining bytes >= sector size, */
			if (cc > 0) {						/* Read maximum contiguous sectors directly */
				if (csect + cc > fs->csize) {	/* Clip at cluster boundary */
					cc = fs->csize - csect;
				}
				if (disk_read(fs->pdrv, rbuff, sect, cc) != RES_OK) ABORT(fs, FR_DISK_ERR);
#if !FF_FS_READONLY && FF_FS_MINIMIZE <= 2		/* Replace one of the read sectors with cached data if it contains a dirty sector */
#if FF_FS_TINY
				if (fs->wflag && fs->winsect - sect < cc) {
					memcpy(rbuff + ((fs->winsect - sect) * SS(fs)), fs->win, SS(fs));
				}
#else
				if ((fp->flag & FA_DIRTY) && fp->sect - sect < cc) {
					memcpy(rbuff + ((fp->sect - sect) * SS(fs)), fp->buf, SS(fs));
				}
#endif
#endif
				rcnt = SS(fs) * cc;				/* Number of bytes transferred */
				continue;
			}
#if !FF_FS_TINY
			if (fp->sect != sect) {			/* Load data sector if not in cache */
#if !FF_FS_READONLY
				if (fp->flag & FA_DIRTY) {		/* Write-back dirty sector cache */
					if (disk_write(fs->pdrv, fp->buf, fp->sect, 1) != RES_OK) ABORT(fs, FR_DISK_ERR);
					fp->flag &= (BYTE)~FA_DIRTY;
				}
#endif
				if (disk_read(fs->pdrv, fp->buf, sect, 1) != RES_OK) ABORT(fs, FR_DISK_ERR);	/* Fill sector cache */
			}
#endif
			fp->sect = sect;
		}
		rcnt = SS(fs) - (UINT)fp->fptr % SS(fs);	/* Number of bytes remains in the sector */
		if (rcnt > btr) rcnt = btr;					/* Clip it by btr if needed */
#if FF_FS_TINY
		if (move_window(fs, fp->sect) != FR_OK) ABORT(fs, FR_DISK_ERR);	/* Move sector window */
		memcpy(rbuff, fs->win + fp->fptr % SS(fs), rcnt);	/* Extract partial sector */
#else
		memcpy(rbuff, fp->buf + fp->fptr % SS(fs), rcnt);	/* Extract partial sector */
#endif
	}

	LEAVE_FF(fs, FR_OK);
}

整体思考:

        首先,初始化变量和检查参数有效性。函数开始时清除已读取字节数,验证文件对象的有效性,检查文件是否以读模式打开,并调整要读取的字节数以不超过剩余文件大小。

        然后,进入主循环,处理按扇区读取数据。这里需要理解扇区对齐和簇的处理逻辑。当文件指针位于扇区边界时,计算当前簇和扇区位置。可能需要使用FAT表来查找下一个簇,特别是当使用快速查找功能时。

        接着,处理连续扇区的读取。如果剩余字节足够多,直接读取多个连续扇区以提高效率。这里需要考虑缓存一致性,比如处理脏数据回写。如果没有连续扇区可读,则使用缓存扇区数据,处理可能的缓存更新和读取。

        然后,处理部分扇区数据的读取。当需要读取的数据不足一个扇区时,从缓存中复制数据到用户缓冲区。这里分为使用Tiny模式下的窗口缓存和普通模式下的文件对象缓存两种情况。

        最后,处理错误和退出。在每一步操作中,都可能因磁盘错误或其他问题导致函数提前终止,返回相应的错误代码。

        在分析过程中,需要注意FATFS的条件编译选项,如FF_USE_FASTSEEK、FF_FS_READONLY、FF_FS_TINY等,这些宏定义会影响代码的执行路径。此外,缓存管理、簇链遍历、磁盘I/O操作是关键部分,需要详细理解其逻辑。

        总结时,需要归纳函数的工作流程、关键设计点、优化策略(如连续扇区读取、缓存利用)以及错误处理机制。同时,对比不同配置下的代码路径,理解如何通过宏定义实现不同功能模块的切换,增强代码的可移植性和灵活性。

        可能遇到的疑问点包括:簇和扇区的转换逻辑、快速查找(CLMT)的实现细节、缓存脏数据的处理流程,以及不同配置(如Tiny模式)对缓存管理的影响。需要结合FAT文件系统的基本原理和FATFS库的设计文档来澄清这些部分。

        通过这样的逐步分析,可以深入理解`f_read`函数的实现机制,掌握FATFS库在嵌入式系统中高效读取文件数据的方法,以及如何处理各种边界条件和错误情况。

1. 函数功能

f_read 是 FATFS 文件系统库中的核心函数,用于 从已打开的文件中读取数据。其核心任务包括:

  • 按需读取文件内容:支持从任意文件位置(fp->fptr)读取指定字节数(btr)。

  • 处理文件簇链:通过 FAT 表或 CLMT(快速查找表)定位数据所在的簇和扇区。

  • 缓存管理:利用文件对象或全局窗口缓存优化小数据块的读取。

  • 错误处理:检测文件状态、磁盘错误和无效操作,返回错误码。


2. 参数说明

参数 类型 说明
fp FIL* 已打开的文件对象指针。
buff void* 用户提供的缓冲区,用于存储读取的数据。
btr UINT 请求读取的字节数。
br UINT* 实际读取的字节数(输出参数)。

3. 函数逻辑流程

步骤 1:初始化与参数校验
*br = 0;                                  // 初始化已读字节数为 0
res = validate(&fp->obj, &fs);            // 验证文件对象有效性
if (res != FR_OK || ...) LEAVE_FF(...);   // 检查错误或权限问题
if (!(fp->flag & FA_READ)) LEAVE_FF(...); // 检查文件是否以读模式打开
remain = fp->obj.objsize - fp->fptr;      // 计算剩余可读字节数
if (btr > remain) btr = (UINT)remain;     // 调整请求字节数,避免越界
  • 关键点:确保文件状态合法,避免读取越界或未授权的访问。


步骤 2:主循环(按扇区读取数据)
for ( ; btr > 0; ...) {
    // 处理扇区对齐逻辑
    if (fp->fptr % SS(fs) == 0) {         // 文件指针位于扇区边界?
        csect = ...;                      // 计算当前簇内的扇区偏移
        if (csect == 0) {                 // 位于簇边界?
            // 获取当前簇号(可能通过 CLMT 或 FAT 表遍历)
            clst = get_cluster(...);      // 关键:簇链遍历或 CLMT 查找
            fp->clust = clst;             // 更新文件对象的当前簇号
        }
        sect = clst2sect(fs, fp->clust);  // 簇号转物理扇区号
        sect += csect;                    // 加上簇内扇区偏移
        // 尝试读取连续多个扇区(优化)
        if (cc = btr / SS(fs)) {          // 剩余字节 >= 扇区大小?
            disk_read(...);               // 直接读取多个连续扇区
            rcnt = SS(fs) * cc;           // 更新已读字节数
            continue;
        }
        // 处理单个扇区缓存
        if (fp->sect != sect) {           // 缓存扇区是否需要更新?
            if (缓存脏) disk_write(...);   // 写回脏数据(若需要)
            disk_read(fp->buf, sect);     // 读取新扇区到缓存
            fp->sect = sect;              // 更新缓存扇区号
        }
    }
    // 从缓存中复制部分数据到用户缓冲区
    rcnt = ...;                           // 计算本次读取的字节数
    memcpy(rbuff, fp->buf + offset, rcnt); // 复制数据
}
  • 关键点

    • 扇区对齐优化:当请求数据跨越多个扇区时,优先读取连续扇区(减少磁盘操作次数)。

    • 缓存管理:维护文件对象的 sect 和 buf,避免重复读取同一扇区。

    • 簇链遍历:通过 get_fat 或 CLMT 查找下一个簇号,支持大文件随机访问。


4. 关键设计思想

(1) 簇链与扇区定位
  • 簇链遍历
    文件数据存储在多个簇中,每个簇对应一组连续扇区。get_fat 函数通过 FAT 表查找下一簇号。

  • 快速查找(CLMT)
    若启用 FF_USE_FASTSEEK,使用预生成的 CLMT(Cluster Link Map Table)直接定位簇,避免遍历 FAT 表,加速大文件随机访问。

(2) 缓存策略
  • 文件对象缓存(非 Tiny 模式)
    每个文件对象(FIL)维护一个扇区缓存(fp->buf),用于存储当前访问的扇区数据。

  • 全局窗口缓存(Tiny 模式)
    若启用 FF_FS_TINY,所有文件共享一个全局缓存(fs->win),节省内存但降低并发性能。

  • 脏数据写回
    在读取新扇区前,若缓存数据被修改(FA_DIRTY 标志),需先写回磁盘。

(3) 错误处理
  • ABORT 宏
    统一处理错误,设置 fs->err 并跳转到退出点(LEAVE_FF)。

  • 磁盘操作检查
    所有 disk_read 和 disk_write 调用均检查返回值,确保数据完整性。


5. 优化策略

  • 连续扇区批量读取
    当剩余读取字节数超过扇区大小时,直接读取多个连续扇区,减少磁盘 I/O 次数。

  • 缓存复用
    避免重复读取同一扇区,利用缓存提高小数据块的读取效率。

  • 延迟写回
    仅在需要替换缓存扇区时写回脏数据,减少不必要的磁盘写入。


6. 条件编译与配置

宏定义 功能说明
FF_USE_FASTSEEK 启用 CLMT 快速查找,加速大文件随机访问。
FF_FS_TINY 使用全局窗口缓存(节省内存),替代文件对象缓存。
FF_FS_READONLY 禁用写操作相关代码(如脏数据写回)。
FF_FS_MINIMIZE 控制功能裁剪级别(影响缓存策略)。

7. 典型场景示例

FIL fil;
UINT br;
uint8_t buffer[512];

// 打开文件并读取前 512 字节
f_open(&fil, "file.txt", FA_READ);
f_read(&fil, buffer, 512, &br);
f_close(&fil);
  • 流程

    1. f_read 检查文件状态,确定可读范围。

    2. 根据文件指针 fptr 计算簇和扇区位置。

    3. 若数据跨多个扇区,批量读取连续扇区;否则从缓存复制数据。

    4. 更新文件指针和已读字节数。


8. 总结

特性 说明
核心逻辑 按扇区读取数据,处理簇链遍历和缓存管理。
性能优化 批量读取连续扇区、缓存复用、快速查找(CLMT)。
资源管理 动态管理缓存,支持 Tiny 模式节省内存。
错误鲁棒性 严格校验参数和磁盘操作结果,确保数据一致性。
可配置性 通过条件编译支持多种功能裁剪(如只读模式、Tiny 缓存)。

        通过 f_read 函数,FATFS 实现了高效、灵活的文件读取机制,适用于资源受限的嵌入式系统,同时兼顾性能与内存开销。