从NXP SD卡到STM32H7 eMMC:一个偶发open失败问题的完整排查记录
背景项目从 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 误导了排查方向。

相关新闻

10天极简C++入门:从环境搭建到实战通讯录系统

10天极简C++入门:从环境搭建到实战通讯录系统

1. 项目概述:为什么是C,为什么是10天?如果你点开这篇文章,大概率是想在短时间内,对C这门“古老”又“强大”的编程语言建立一个清晰、可用的认知。在这个Python、JavaScript大行其道的时代,为什么还要学C&a…

2026/10/16 4:16:27 阅读更多 →
Windows 11 设置多个ip

Windows 11 设置多个ip

netsh interface ip add address "以太网" 192.168.1.65 255.255.255.0

2026/10/13 18:37:00 阅读更多 →
微信公众号文章目录系统开发实践

微信公众号文章目录系统开发实践

1. 公众号文章目录的价值与痛点作为一个运营了五年公众号的老编辑,我深刻体会到内容沉淀的重要性。每次新粉丝关注后,他们最常问的问题就是:"有没有往期文章合集?"、"XX主题的文章在哪里看?"。而每…

2026/10/2 11:44:47 阅读更多 →

最新新闻

Spring Boot+MyBatis扫描不到@Mapper?核心原因与十分钟定位排查实战

Spring Boot+MyBatis扫描不到@Mapper?核心原因与十分钟定位排查实战

你要是被项目里那句“Field userMapper in ... required a bean of type ...Mapper that could not be found”折磨过,肯定懂我说的场景:代码写得没问题,注解也加了,启动就是失败。这个“Spring Boot MyBatis 扫描不到 Mapper”的…

2026/10/16 4:36:22 阅读更多 →
Linux 下 GeoClue AccessDenied 报错排查与修复指南

Linux 下 GeoClue AccessDenied 报错排查与修复指南

开篇:这个报错比你想象的常见如果你在 Linux 桌面上跑过任何带位置查询功能的应用——比如天气组件、地图软件、或者通过浏览器访问某个想拿你地理位置的网页——那你八成见过这条报错:"Unable to start GeoClue client: GDBus.Error:org.freedeskt…

2026/10/16 4:36:22 阅读更多 →
并发编程锁的10个高频陷阱:从加锁失效到死锁与性能优化

并发编程锁的10个高频陷阱:从加锁失效到死锁与性能优化

并发编程里的锁,从来不是“加了synchronized就万事大吉”这么简单。这些年我做代码评审和线上问题排查,见过太多锁相关的离奇事故:有人明明加了锁,数据照样错乱;有人用Integer做锁,锁到最后发现每个线程拿的…

2026/10/16 4:36:22 阅读更多 →
Spring Boot+MyBatis中@Mapper扫不到?原理与排查思路一次讲透

Spring Boot+MyBatis中@Mapper扫不到?原理与排查思路一次讲透

前两天在群里看到有人贴报错:启动类能跑,但Service一注入Mapper的时候就挂,提示Consider defining a bean of type xxx.xxx.UserMapper in your configuration.点开代码,明明每个Mapper接口上都写了Mapper,标题也搜的“mybaties扫描不到mapper”。这个问题在Spring Boot MyBat…

2026/10/16 4:36:22 阅读更多 →
计算机网络体系结构:分层思想与TCP/IP模型核心考点精讲

计算机网络体系结构:分层思想与TCP/IP模型核心考点精讲

1. 为什么第一章要从“体系结构”开始很多同学刚开始复习计算机网络,翻到目录看到第一章是“计算机网络体系结构”,心里会嘀咕:这章看起来全是抽象概念,什么分层、协议、服务、接口,又不讲具体技术,是不是可…

2026/10/16 4:36:22 阅读更多 →
基于SSM框架的智能密室逃脱信息管理系统设计与实践

基于SSM框架的智能密室逃脱信息管理系统设计与实践

1. 项目概述1.1 一句话理解这个项目这是一个基于SSM框架(Spring SpringMVC MyBatis)开发的信息管理系统,业务场景切的是“智能密室逃脱”。它不是一个游戏引擎,也不做密室内的机关控制,而是管理密室逃脱门店的日常运…

2026/10/16 4:35:22 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →