背景项目从 NXP1052Cortex-M7 SD Card 平台迁移到 STM32H743Cortex-M7 eMMC 平台后出现偶发的sdcard_open失败问题。应用层逻辑完全一致底层从 SD 卡换成了 eMMCSDMMC 控制器自带内部 DMAIDMA。在 NXP 平台上从未出现过此问题。问题现象上位机上传 NC 文件到设备后发送打开加工命令设备端open()文件失败重试3次均返回 -1。日志如下[sdcard_open]open fail: P55D短NC_N.NC, flag0x2, retry0, fd-1 [sdcard_open]open fail: P55D短NC_N.NC, flag0x2, retry1, fd-1 [sdcard_open]open fail: P55D短NC_N.NC, flag0x2, retry2, fd-1 [sdcard_open]open final fail: P55D短NC_N.NC, flag0x2, total_retry3更严重的是多次失败后 fd 表被占满[E/DFS] DFS fd new is failed! Could not found an empty fd entry.排查过程阶段一怀疑 D-Cache 写路径问题STM32H7 的 Cortex-M7 有 D-Cache而 NXP1052 的 Cortex-M4 没有。首先怀疑是 D-Cache 导致 DMA 缓存不一致。在 SDIO 驱动的写路径添加了 D-Cache cleanif (data-flags DATA_DIR_WRITE) { uint32_t align_addr (uint32_t)pkg.buff ~(SDIO_ALIGN_LEN - 1); SCB_CleanDCache_by_Addr((uint32_t*)align_addr, data-blksize * data-blks (uint32_t)pkg.buff - align_addr); }结果问题依旧。排查board.c发现已经开启了 D-Cache 强透写模式SCB_EnableDCache(); SCB-CACR | (1 2); // 开启Dcache强透写强透写模式下CPU 每次写数据都会同时写入 D-Cache 和 RAMSDIO DMA 从 RAM 读到的就是最新数据。所以SCB_CleanDCache_by_Addr在这个配置下是冗余的加了也没有效果。阶段二怀疑 fd 泄漏日志中close()返回 -1分析 RT-Thread 的close()实现// rt-thread/components/dfs/src/dfs_posix.c int close(int fd) { d fd_get(fd); result dfs_file_close(d); fd_put(d); if (result 0) { rt_set_errno(result); fd_put(d); // close失败时额外put一次, 使ref_count归零 return -1; } fd_put(d); return 0; }close()失败时会额外调用一次fd_put使 ref_count 归零理论上不会泄漏。但实际运行中 fd 表确实满了。于是添加了 fd 强制清理机制和文件系统恢复机制unmount remount mkfs。结果能恢复但治标不治本。根本问题还是open()为什么失败。阶段三errno 捕获不到真实错误码尝试用rt_get_errno()获取错误码sd_dev-fd open(file_name, open_flag); saved_errno rt_get_errno();结果errno0。open()明明失败了errno 却是 0。分析rt_get_errno实现rt_err_t rt_get_errno(void) { rt_thread_t tid; if (rt_interrupt_get_nest() ! 0) return __rt_errno; tid rt_thread_self(); if (tid RT_NULL) return __rt_errno; return tid-error; // 返回线程局部的error }open()内部调用链open()→dfs_file_open()→ FatFsf_open()→ SDIO 驱动含互斥锁和线程等待。在这个调用链中SDIO 驱动会阻塞让出线程调度器切换到其他线程。如果其他线程在此期间调用了rt_set_errno()就会覆盖tid-error。等open()返回后rt_get_errno()拿到的可能已经不是open()设置的错误码了。阶段四绕过 errno直接拿 DFS 内部返回值不再用 POSIXopen()包装直接调用 DFS 内部 APIstatic int sdcard_dfs_open(const char *path, int flags) { int fd, result; struct dfs_fd *d; fd fd_new(); if (fd 0) return -0x1000; // fd表满 d fd_get(fd); if (d NULL) return -0x1001; result dfs_file_open(d, path, flags); // 直接拿返回值 if (result 0) { fd_put(d); fd_put(d); return result; // 真实错误码, 不经过errno } fd_put(d); return fd; }结果拿到 err-2即 -ENOENT文件不存在。阶段五文件为什么不存在文件明明上传过了为什么open()说文件不存在排查整个上传流程上位机 → protocol_handler → cnc_app_update_nc → mc_nc_write → sdcard_write → POSIX write()关键发现sdcard_write只调用了write()没有调用fsync()static int sdcard_write(void *dev, uint32_t offset, uint8_t *buf, uint32_t size) { _sdcard_dev_t sd_dev (_sdcard_dev_t)dev; ret write(sd_dev-fd, buf, size); // 没有 fsync! return ret; }而且sdcard_set_file_checksum校验函数在校验失败时会删除文件static int sdcard_set_file_checksum(void *dev, uint32_t offset, uint8_t *buf, uint32_t size) { // ...计算本地文件CRC32... if (checksum不匹配) { close(sd_dev-fd); unlink((char*)sd_dev-file_name); // 删除文件! return (-RT_ERROR); } }代码中还有一行注释暴露了我早已意识到问题但是当时是想到sync函数但是发现这函数在我现有的库上面根本就不存在所以当时就用了延迟去代替结果发现效果不佳// sdcard.c, sdcard_delete函数中 rt_thread_mdelay(200); // 如果没有 sync()可以用 rt_thread_mdelay(200) 替代根因分析三层数据存储模型数据从 CPU 到 eMMC 要经过三个存储位置CPU寄存器 → D-Cache → RAM内存 → eMMC物理芯片强透写只管第一层CPU → RAMFatFs 的扇区缓存管第二层RAM → eMMC这是两个独立的层面。强透写解决什么强透写保证 CPU 写数据时数据同时进入 D-Cache 和 RAM。SDIO DMA 从 RAM 读取时能拿到最新数据。CPU写数据 → D-Cache RAM ← 强透写管的这一步没有强透写时CPU 写数据只进 D-Cache 不进 RAMDMA 从 RAM 读到旧数据。开了强透写后这个问题不存在。FatFs 扇区缓存才是问题所在FatFs 内部有一个 512 字节的 RAM 缓冲区叫win。f_write修改win后FatFs 不会立即调用disk_write把它写回 eMMC而是标记为脏等特定时机才写f_close()或f_sync()被调用时需要访问另一个扇区时move_window先 flush 当前脏扇区f_write 流程: 1. CPU 把数据写到 win[512] (RAM中) 2. 强透写: 数据同步到 RAM ✓ 3. 标记 win 为脏 4. 直接返回, 不调 disk_write 5. 此时 eMMC 上还是旧数据!只有f_close/f_sync被调用时FatFs 才会把win的脏数据刷到 eMMCf_close → f_sync: 1. 发现 win 是脏的 → disk_write 写到 eMMC ← 数据真正落盘 2. 读目录项所在扇区到 win (disk_read) 3. CPU 修改文件大小、起始簇号等目录信息 4. disk_write 把修改后的目录扇区写回 eMMC完整的写路径CPU写 → D-Cache → RAM(win缓冲区) → [等f_close/f_sync] → eMMC ↑强透写管这里 ↑FatFs软件缓存管这里强透写只保证数据从 CPU 到 RAM。从 RAM 到 eMMC 这一步只能靠f_close/f_sync触发disk_write来完成。文件丢失的完整过程上位机上传 NC 文件f_write把文件数据和 FAT 表项写到 RAM 中的win缓冲区上传完成但close()没成功执行fd 泄漏或 close 失败f_sync没被调用eMMC 上的 FAT 表和目录项还是旧的文件不存在下次open()时FatFs 从 eMMC 读 FAT 表 → 文件不存在 → -ENOENT即使做了 unmount remountremount 清掉了 RAM 缓存重新从 eMMC 读eMMC 上确实没有 → 还是 -ENOENT为什么 NXP1052 没问题NXP1052 是 Cortex-M4没有 D-Cache。STM32H7 是 Cortex-M7有 D-Cache。但这不是核心区别。核心区别在于NXP1052 上close()能正常执行。在 NXP 平台上整个文件操作流程没有 fd 泄漏问题close()被正确调用f_sync触发数据落盘文件不会丢失。在 STM32H7 平台上由于 SDIO 驱动的 D-Cache invalidate 可能存在问题close()内部的f_close→f_sync在读目录项时可能读到 D-Cache 中的旧数据导致close()失败进而 fd 泄漏最终文件数据没有落盘。为什么 D-Cache 成为负担D-Cache 的设计前提是只有 CPU 访问内存。它不知道 DMA 的存在。D-Cache 加速的是 CPU ↔ RAM 这段。但它制造了一个 CPU 看不到 DMA 操作的盲区读场景: CPU之前读过某地址 → D-Cache存了一份(旧数据) SDIO DMA 从 eMMC 读新数据 → 直接写入RAM CPU再读该地址 → D-Cache说我有 → 返回旧数据 ✗强透写只保证 CPU写时数据到 RAM不管 CPU读时从哪里读。DMA 写了 RAM但 CPU 读 D-Cache 命中的话直接返回缓存中的旧数据不去 RAM 看 DMA 写的新数据。读路径要靠SCB_InvalidateDCache_by_Addr让 D-Cache 丢弃旧数据强制从 RAM 重新读。一旦某个地址漏了 invalidate或者 invalidate 的地址范围不对CPU 就会读到旧数据。NXP1052 没有 D-CacheDMA 和 CPU 之间不存在缓存屏障天然一致。修复方案核心修复sdcard_write 后添加 fsyncstatic int sdcard_write(void *dev, uint32_t offset, uint8_t *buf, uint32_t size) { _sdcard_dev_t sd_dev (_sdcard_dev_t)dev; ret write(sd_dev-fd, buf, size); if (ret 0) { fsync(sd_dev-fd); // 强制数据从FatFs win缓冲区刷到eMMC } return ret; }调用链fsync→dfs_file_flush→dfs_elm_flush→f_sync→ 刷脏扇区 更新 FAT 目录项。每次 write 后立即 fsync数据立即从 RAM 写到 eMMC不再依赖close()是否被正确调用。辅助修复校验前 fsyncstatic int sdcard_set_file_checksum(void *dev, uint32_t offset, uint8_t *buf, uint32_t size) { // 先fsync确保缓存数据落盘, 否则校验读到的是旧数据 fsync(sd_dev-fd); // ...计算CRC32并比较... }校验前先 fsync确保 FatFswin缓冲区的数据已经写入 eMMC避免校验时读到旧数据导致误删文件。辅助修复close 的 fd 有效性检查static int sdcard_close(void *dev) { int ret 0; _sdcard_dev_t sd_dev (_sdcard_dev_t)dev; if (sd_dev-fd 0) // 避免对无效fd调用close { ret close(sd_dev-fd); } sd_dev-fd -1; return RT_EOK; }错误码诊断绕过 errnostatic int sdcard_dfs_open(const char *path, int flags) { int fd, result; struct dfs_fd *d; fd fd_new(); if (fd 0) return -0x1000; d fd_get(fd); if (d NULL) return -0x1001; result dfs_file_open(d, path, flags); if (result 0) { fd_put(d); fd_put(d); return result; // 直接返回dfs_file_open的错误码 } fd_put(d); return fd; }rt_get_errno()返回的是tid-error线程局部变量在open()内部的 SDIO 驱动阻塞让出线程时可能被其他线程的rt_set_errno()覆盖。直接用dfs_file_open()的返回值不经过 errno不受线程调度影响。常见错误码对照err 值宏定义含义-4096-(0x1000)fd 表满fd_new 失败-2-ENOENT文件不存在 / 路径无效-5-EIOSDIO 传输错误 / FatFs 磁盘错误-12-ENOMEM内存分配失败-22-EINVAL文件名非法-17-EEXIST文件已存在经验总结D-Cache 强透写不等于不需要 fsync。强透写解决的是 CPU→RAM 的缓存一致性fsync 解决的是 RAM→eMMC 的数据落盘。两个层面完全独立。rt_get_errno()在多线程环境下不可靠。SDIO 驱动内部有互斥锁和线程等待调度过程中 errno 可能被覆盖。需要真实错误码时直接用 DFS 内部 API 的返回值。FatFs 的win扇区缓存是软件层面的缓存跟硬件 D-Cache 无关。f_write修改win后不会立即写 eMMC必须靠f_close/f_sync触发disk_write。从 M4 迁移到 M7 时D-Cache 是最大的隐患。M4 没有 D-CacheDMA 和 CPU 之间天然一致。M7 有 D-Cache读写路径都需要手动维护缓存一致性而且 D-Cache 把一个硬件问题变成了需要软件逐个地址维护的负担。排查问题的顺序很重要。先拿到真实错误码-ENOENT才能确定方向是文件不存在而不是缓存不一致或fd 泄漏。绕了弯路的原因是 errno0 误导了排查方向。