尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H743 + FatFS + FreeRTOS 实战:SD卡MDMA读写避坑指南

STM32H743 + FatFS + FreeRTOS 实战:SD卡MDMA读写避坑指南 最近把一个数据采集项目从 STM32F407 往 STM32H743 上迁移别的外设都很顺利唯独 SD 卡读写这一段让我整整折腾了一周。FATFS 配 FreeRTOS再加 H7 系列特有的 MDMA这三样东西单拎出来都不算难但凑到一起后时序、缓存一致性、任务调度的问题全冒出来了。这篇文章不是教程复读是我自己在 CubeMX 里配置 SDMMC1 MDMA 驱动 FATFS在 FreeRTOS 多任务环境下跑 SD 卡读写时实打实踩过的坑以及最终稳定运行的整套配置。如果你的项目也是 STM32H743 FatFS FreeRTOS 这种组合或者正准备把老工程的 SD 卡读写部分迁到 H7 上这篇文章值得你花十分钟读完能帮你少走不少弯路。1. 项目背景与方案选型H743 上 SD 卡读写的“正确打开方式”1.1 我的硬件环境与项目需求我这个项目的硬件核心是 STM32H743VIT6主频跑 480MHz外部 25MHz 晶振板载 TF 卡座通过 SDIO 接口引出数据总线用了 4 位模式。项目本身是一个数据记录设备需要把传感器数据实时写入 TF 卡同时后面还要接一个简单的 UI 界面做参数显示和操作。原来是基于 STM32F407 的迁移到 H743 主要是看中了更高的主频、更大的 RAM以及后续要接摄像头做图像采集的扩展空间。需求梳理下来其实并不复杂一是要稳定设备可能在现场连续跑几天SD 卡写入绝对不能中途掉链子二是要实时传感器数据是持续产生的FATFS 写入不能成为瓶颈三是要好维护代码结构要清晰方便后续加功能。这个需求听起来很常规但真正在 H743 上落地时我发现网上大多数教程都停留在“CubeMX 默认配置 裸机轮询”的水平一旦涉及到 RTOS 多任务并发、H7 特有的 MDMA 和 D-Cache就几乎没有系统性的避坑指南了。我这次把整个调试过程中踩过的坑和最终的方案一起记录下来希望能给后来者一个完整的参考。1.2 为什么用 SDMMC 4 位模式 FATFS FreeRTOS先说说方案选型。SD 卡通信方式主要有两种SPI 模式和 SDMMCSDIO模式。SPI 模式接线简单大部分 MCU 都支持但速度上限很低而且很多 TF 卡在 SPI 模式下并不完全兼容时不时会出现初始化失败的问题。SDMMC 模式走专用接口支持 1 位或 4 位数据总线在 4 位模式下读写速度能到十几 MB/s 甚至更高明显更符合我这种持续写入的场景。文件系统方面我选了 FatFS原因很直接它开源自用协议免费、移植文档齐全、ST 官方已经把它集成到了 CubeMX 里生成代码后只需要关注底层接口。对于嵌入式设备来说TF 卡作为 FAT32/exFAT 格式的移动存储介质在电脑上直接可读维护和取数都非常方便。操作系统方面项目里已经有 FreeRTOS所以 SD 卡读写只作为其中一个任务存在。这里有个关键点FATFS 本身不是线程安全的在 RTOS 环境下必须开启重入保护否则多任务同时调用 f_open、f_read、f_write 时轻则数据错乱重则直接死机卡死。CubeMX 里 FatFS 的配置项有 FF_FS_REENTRANT这个开关在 FreeRTOS 工程里必须打开后面我会详细说。1.3 MDMA 并不是炫技选它的真实原因先聊一个很多新手会困惑的问题STM32H7 上已经有 DMA1/DMA2 了为什么还要用 MDMA我第一次从数据手册里看到 MDMA 时也觉得这东西有点多余但在实际项目中还真有必须用它的时候。简单说STM32H743 的内存架构是分域的D1 域CPU 主频域、D2 域外设域和 D3 域低速外设域。传统的 DMA1/DMA2 分布在 D2 域它们虽然能访问大部分内存但在跨域访问时有限制而且一个 DMA 通道只能服务一个外设。MDMA 是 H7 系列特有的独立 DMA 控制器可以访问全部内存空间支持 32 位/64 位/128 位的搬运粒度还支持 linked-list 链表传输。我在这个项目里选择 MDMA 的核心理由有两点。第一SD 卡的读写往往是几百上千字节的大块数据搬运一次 MDMA 配置能完成全部传输中间不需要 CPU 干预第二DMA2 的通道在项目里已经被其他高速外设占得差不多了SD 卡这种大块数据用 MDMADMA 通道留给那些频繁小批量的外设更合适。不过我必须提醒一句MDMA 不是万金油它的配置比 DMA1/DMA2 复杂初始化参数一错SD 卡初始化都过不去。如果你的工程 DMA 通道够用用普通 DMA 也能跑得很稳但如果你想用 MDMA这篇文章第 2、3 部分的配置和代码实现就是给你准备的。2. CubeMX 一步步配置时钟、SDMMC、MDMA、FatFS、FreeRTOS2.1 时钟树先从 PLL1Q 把 SDMMC 时钟喂饱很多人配置 STM32H743 的第一反应是先把主频拉到 480MHz这没错但 SD 卡能不能稳定工作关键在 SDMMC 的时钟源配置。在 CubeMX 的 Clock Configuration 页面里需要关注三个时钟SYSCLK、SDMMC1 的内核时钟以及最终输出给 SD 卡的时钟引脚频率。我的配置思路是这样的外部 25MHz 晶振作为 HSEPLL1 倍频到 VCO 960MHz再分频得到 SYSCLK 480MHz。SDMMC1 的时钟源我选了 PLL1Q目标频率 48MHz。这里有个容易犯的错误PLL1Q 默认分频可能不是 20如果直接沿用默认SDMMC 内核时钟可能高达 240MHz 甚至更高远超内核允许的 120MHz 上限。配置完一定要看 CubeMX 右上角的时钟树颜色提示红色表示超出范围必须调整分频系数。实际操作中我把 PLL1Q 设成了 48MHz。SDMMC 实际输出给 SD 卡的时钟是内核时钟再分频SDMMC_CK SDMMC_CKIN / (2 × CLKDIV)。当 CLKDIV 设为 1 时卡时钟是 24MHz这个频率对绝大多数 TF 卡都足够稳定如果想要更高速度后面再通过卡初始化命令切换到高速模式或 DDR 模式。初次调试建议先稳稳跑 24MHz等读写全部通了再考虑提频这样能避免把“卡时序问题”和“配置问题”混在一起。2.2 SDMMC 外设参数模式、位数、分频系数时钟配置好之后进入 Pinout Configuration 页面在 Connectivity 分类下找到 SDMMC1。Mode 选择“SD 4 bits Wide bus”然后展开参数配置项。关键的几个参数我列一下Clock Divider设为 1配合 48MHz 内核时钟输出 24MHz 卡时钟。Clock Bypass保持 Disable让 SDMMC 内部进行时钟分频。Clock Power Save可以 EnableSD 卡空闲时自动关闭卡时钟降低功耗。Bus Width这里指的是初始化阶段的总线宽度选“4 bits”。Hardware Flow Control建议 Enable。这个功能启用后SDMMC 的 FIFO 会在达到阈值时自动暂停总线避免数据上溢/下溢是稳定性的一大保障。SDMMC 的中断也要打开。在 NVIC 设置里勾选 SDMMC1 global interrupt优先级建议设为比 FreeRTOS 可管理中断的最低优先级高数值更小否则在 RTOS 里可能出现中断被关掉或优先级倒挂的问题。这里记住一个原则FreeRTOS 的临界区会屏蔽所有优先级数值大于等于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断所以外设中断优先级不要设得比这个值小即优先级更高否则你在中断里调用 FreeRTOS API 时会断言失败。2.3 MDMA 请求配置参数含义逐项说这一步是标题里 MDMA 的关键所在。在 SDMMC1 的配置页下方有一个 DMA Settings 标签点击 Add 之后选择 MDMA而不是普通的 DMA。这里从 CubeMX 6.x 版本开始可以直接看到 MDMA 选项如果你用的版本比较老看不到建议升级。MDMA 的详细参数是这样的Request选择 SDMMC1这个是请求源必须有 DMA 请求映射关系。Direction根据读写方向分开配置读卡用 Peripheral to Memory写卡用 Memory to Peripheral。Memory Data Size选 Word32 位因为 SDMMC 的 FIFO 是 32 位宽的MDMA 按 32 位粒度搬运正好匹配。Peripheral Data Size同样选 Word。Increment AddressMemory 侧 EnablePeripheral 侧 Disable。因为 SDMMC FIFO 的地址是固定的而内存地址需要顺序递增。Priority建议 Medium 起步如果你的系统里还有其他 MDMA 请求再根据实际调整。这条配置生成代码后HAL 库会在 stm32h7xx_hal_sd.c 内部把 MDMA 句柄挂到 SD 句柄上。你直接用 HAL_SD_ReadBlocks_DMA 和 HAL_SD_WriteBlocks_DMA 就能触发 MDMA 传输不需要自己手动操作 MDMA 寄存器。这里最需要注意的是 Memory Data Size 和外设数据宽度一定要匹配如果配成 Byte 或者 Half WordMDMA 传输的数据会错位SD 卡读写出来的文件就是一堆乱码。2.4 FatFS 与 FreeRTOS 的耦合设置CubeMX 里 FatFS 在 Middleware and Software Packs 分类下Mode 选择 SD Card如果有多个 SD 卡选项H7 上选 SDMMC1 对应的那个。切换到配置页后几个选项必须仔细设FF_FS_REENTRANT设为 Enabled。这个选项让 FatFS 支持重入也就是说允许多个任务同时访问文件系统时进行互斥保护。FF_USE_LFN设为 Enabled长文件名并且要选动态堆分配模式。如果你不开长文件名大部分 SD 卡上的中文文件名和长文件名都会读取失败。FF_VOLUMES设置为 1我们只用一张卡。FF_MAX_SS设成 4096。现在的 TF 卡高级格式化Advanced Format很多扇区大小是 4096 字节如果这个值还是默认的 512某些卡格式化或读取会出现 FR_MKFS_ABORTED 错误。CubeMX 在生成 FatFS 代码时如果检测到工程里启用了 FreeRTOS会自动把 FF_SYNC_t 类型映射为 RTOS 的互斥锁类型并生成 f_syscall.c 和 f_mem.c。在 FreeRTOS 的配置页面Heap Size 建议至少设到 16KB具体看你任务数量和队列数量我留了 32KB否则后面创建任务、信号量、互斥锁可能会失败。2.5 代码生成后的第一眼检查点 Generate Code 之后别急着编译烧录先花两分钟检查几个关键位置能省掉后面大量排查时间。第一打开 Core/Src/sd_diskio.c确认 USER_Read 和 USER_Write 函数是存在且被引用的。CubeMX 有时候会因为路径配置问题没有把 sd_diskio.c 加进编译导致 FatFS 底层调用的是空函数。第二打开 stm32h7xx_it.c确认 SDMMC1_IRQHandler 和 MDMA_IRQHandler 都已经存在。MDMA 有两个中断向量MDMA_IRQHandler 是常规通道中断另一个是错误中断。如果你发现只有错误中断没有常规中断优先检查 CubeMX 里 MDMA 的 NVIC 配置。第三打开 FreeRTOSConfig.h确认 configUSE_TIMERS、configSUPPORT_DYNAMIC_ALLOCATION、configUSE_MUTEXES 这些宏都是 1。FatFS 在 RTOS 模式下编译 f_syscall.c需要用到互斥锁支持否则链接时直接报错。3. 代码层面的关键改造同步机制、低层驱动、缓冲区对齐3.1 用信号量把 MDMA 中断和 FATFS 串起来CubeMX 生成的 FatFS 底层调用链是disk_read - USER_Read - HAL_SD_ReadBlocks_DMA。这个函数只负责启动一次 DMA 传输然后立即返回并不会等着传输完成。如果你直接在 USER_Read 里调用它就返回状态FATFS 会认为数据已经到内存了但实际上 MDMA 还在搬运读出来的数据必然不全。正确的做法是加入信号量同步。我在工程里定义了一个二进制信号量 sd_xfer_sem在 MDMA 传输完成中断里释放在 USER_Read/USER_Write 里等待。核心代码如下SemaphoreHandle_t sd_xfer_sem; void SDMMC1_IRQHandler(void) { HAL_SD_IRQHandler(hsd1); } void MDMA_IRQHandler(void) { HAL_MDMA_IRQHandler(hmdma_mdma); } void HAL_MDMA_XferCpltCallback(MDMA_HandleTypeDef *hmdma) { BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(sd_xfer_sem, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }在 USER_Read 内部发起 DMA 传输后调用 xSemaphoreTake等 MDMA 完成中断把信号量给出后再返回。为了避免 SD 卡异常导致永远等不到中断等待时间要设置超时。我设的是 1000ms实测正常读写一个 512 字节扇区根本用不了这么多但异常时不至于把整个系统锁死。这里有个很容易被忽略的细节二进制信号量初始化时是空的如果第一次传输还没发起信号量被误 take 到会导致提前返回。我在初始化代码里先故意给一次信号量让信号量的初始计数为 1然后再在每次传输完成后只给不抢这样两边的节奏就对齐了。3.2 sd_diskio.c 底层读写的改造思路CubeMX 生成的 sd_diskio.c 里默认通过宏判断是裸机还是 RTOS。在 RTOS 模式下它实际上已经在内部调用 osMutex 来做互斥了但那只保护了驱动函数的调用过程不会帮你等待 DMA 完成。所以我改造的思路是在 USER_Read 和 USER_Write 内部加上信号量同步同时对入参做一些防御性检查。USER_Read 的典型结构是这样的DRESULT USER_Read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { uint32_t timeout 1000; // 对齐检查 if (((uint32_t)buff 0x1F) ! 0) { return RES_PARERR; } // 发起 DMA 读 if (HAL_SD_ReadBlocks_DMA(hsd1, buff, sector, count) ! HAL_OK) { return RES_ERROR; } // 等待完成 if (xSemaphoreTake(sd_xfer_sem, pdMS_TO_TICKS(timeout)) ! pdTRUE) { return RES_TIMEOUT; } // 读操作需要使 Cache 失效 SCB_InvalidateDCache_by_Addr((uint32_t *)buff, count * 512); return RES_OK; }有两点要特别说明。第一HAL_SD_ReadBlocks_DMA 的 sector 参数是逻辑块地址 LBA不是字节地址FATFS 传进来的 sector 和 count 直接透传即可不需要自己乘 512。第二每次 DMA 传输完成后中断回调里只做“给信号量”这一件事不要在里面做耗时的打印或日志操作中断处理越短越好。写操作的 USER_Write 结构类似只是方向反过来并且要在发起 DMA 之前做 Cache Clean后面马上讲。3.3 Cache 一致性处理Clean 和 Invalidate 的顺序别搞反STM32H7 性能强劲但带 D-Cache 也带来了嵌入式领域最经典的坑Cache 一致性问题。DMA 是直接访问内存的它不经过 Cache而 CPU 读写内存时会先查 Cache。这就导致一个局面写卡时如果你要发送的数据还在 Cache 里没被写回内存DMA 搬运出去的就是内存里的旧数据读卡时DMA 已经把新数据写进内存了但 CPU 读出来的还是 Cache 里的旧数据。解决方案说起来很简单写操作前做 CleanDCache读操作完成后做 InvalidateDCache。但顺序一定不能反而且范围一定要算对。写操作的例子// 先确保内存里的数据是最新的 SCB_CleanDCache_by_Addr((uint32_t *)buff, count * 512); if (HAL_SD_WriteBlocks_DMA(hsd1, buff, sector, count) ! HAL_OK) { return RES_ERROR; }读操作的例子if (HAL_SD_ReadBlocks_DMA(hsd1, buff, sector, count) ! HAL_OK) { return RES_ERROR; } // 等 DMA 完成后丢弃 Cache 中的旧数据 if (xSemaphoreTake(sd_xfer_sem, pdMS_TO_TICKS(timeout)) pdTRUE) { SCB_InvalidateDCache_by_Addr((uint32_t *)buff, count * 512); return RES_OK; }还有一点MDMA 和 DMA 一样缓冲区地址要求 32 字节对齐。CubeMX 生成的代码不会自动帮你对齐你需要在定义缓冲区时加上对齐属性__attribute__((aligned(32))) static uint8_t sd_buffer[4096];如果缓冲区地址没对齐MDMA 传输时可能直接进入 HardFault或者产生未对齐访问错误。很多人在调试 SD 卡时遇到 HardFault 第一反应是查中断优先级其实根源往往只是缓冲区没对齐。4. 我踩过的坑从“能初始化”到“稳定读写”之间隔着的雷4.1 坑一缓存上线后 f_read 读出来全是旧数据这个坑是我调试过程中最抓狂的一次。现象是这样的在 FreeRTOS 任务里用 f_read 读 SD 卡上的一个配置文件第一次读出来的数据是正确的第二次再读的时候返回的数组内容还是第一次的旧值。无论我重新 f_open、f_close 多少次读到的都是第一次的数据。排查的时候我先怀疑是不是文件系统缓存的问题但把 FF_USE_FASTSEEK、FF_USE_MKFS 等参数都翻了一遍也没找到问题。后来我在 USER_Read 函数的 HAL_SD_ReadBlocks_DMA 调用后面直接看内存数据发现内存地址里的内容确实已经被 DMA 更新了说明数据已经成功从卡里读出来了。问题出在随后 FatFS 上层访问这些数据时数据被 Cache 挡住CPU 读取的还是 Cache 里的老数据。解决办法就是前面说的在 DMA 传输完成之后对目标缓冲区执行 SCB_InvalidateDCache_by_Addr。这个操作我一开始只加在 f_read 路径上后来发现 fatfs 在挂载卷、读目录项时也会调用底层读函数于是在 USER_Read 函数里统一处理一劳永逸。这里提醒一下Invalidate 的地址范围要按实际传输的字节数来千万不要图省事只 Invalidate 第一个扇区。4.2 坑二RTOS 下 f_open/f_read 随机卡死单任务裸机环境下 SD 卡读写完全没问题但把 SD 卡读写放到 FreeRTOS 任务里再把显示任务跑起来之后偶尔就会 f_open 卡死整个系统像被冻住一样看门狗也不复位。我最初怀疑是堆栈溢出把任务栈加大了一倍还是偶尔卡。后来打开调试器看卡住的位置发现程序卡在 FatFS 内部的 ff_enter 函数里这是一个超时重试循环等待某个同步对象。原因一下就清晰了CubeMX 生成的 ffconf.h 里 FF_FS_REENTRANT 虽然是 Enabled但 FreeRTOS 的互斥锁并没有真正参与进来或者参与的方式不对。解决办法分两步。第一步进入 CubeMX 的 Middleware - FATFS确认 FF_FS_REENTRANT 勾选为 Enabled然后检查 middleware/Third_Party/FatFs/ffconf.h 里的配置是否与 CubeMX 同步有时候手动改过配置后 CubeMX 重新生成代码会把你改的覆盖掉。第二步在应用层再加一把互斥锁把一次完整的文件操作流程打开、读写、关闭全部包起来。为什么底层的 FF_SYNC_t 不够用因为 FatFS 的重入保护只是保护 FatFS 内部数据结构不保护你自己定义的共享缓冲区如果两个任务同时对同一个缓冲区做 f_write照样会踩内存。实际代码是这样的static SemaphoreHandle_t fatfs_mutex; void sd_file_write_task(void *arg) { xSemaphoreTake(fatfs_mutex, portMAX_DELAY); res f_open(file, data.bin, FA_WRITE | FA_CREATE_ALWAYS); if (res FR_OK) { res f_write(file, buf, len, br); f_close(file); } xSemaphoreGive(fatfs_mutex); }4.3 坑三主频 480MHz 的 H743 被 SD 卡时钟频率卡脖子系统运行到稳定阶段之后我开始尝试提高 SD 卡读写速度把 SDMMC 的时钟分频从 Divider1 调到 Divider0理论输出 48MHz 给卡。刚改完的时候测试了几次都挺好读取速度确实上去了但跑了大概十几分钟写文件的时候突然报 FR_DISK_ERR再往卡里写就频繁出错了读卡也会偶发超时。排查后发现问题出在 SD 卡的模式切换上。SD 卡上电后默认是默认速度模式Default Speed最高 25MHz要想跑高速High Speed最高 50MHz必须通过 CMD6 切换卡的工作模式。CubeMX 生成的初始化代码不会自动执行这个切换HAL 库只是把 SDMMC 外设的时钟参数配置好了卡本身还停留在低速模式。在这种状态下硬跑 48MHz卡内部时序完全跟不上短时间可能侥幸没问题时间一长或者温度一高就会出现位错误。解决方案也很直接要么把卡时钟降回 24MHz稳妥跑默认模式要么在初始化阶段调用 HAL_SD_ConfigSpeed 之类的接口先读卡 CSD 寄存器确认卡支持高速模式再发 CMD6 切换。我的项目对写入稳定性要求更高最终选择了 24MHz 保平安实测顺序写文件的速度也在可接受范围内。4.4 坑四f_write 偶发 FR_DISK_ERR单块写却正常这个坑的现象很恶心用循环写单块 512 字节每次都能成功但用 FatFS 一次写 32KB 的大缓冲区写个几次就会出现一次 FR_DISK_ERR。因为是偶发最开始我甚至怀疑是 SD 卡质量问题换了几张卡都是同样的概率性报错。仔细看 FATFS 的底层返回值发现 disk_write 返回的是 RES_ERROR再追进 HAL_SD_ErrorCallback错误标置位是 SDMMC_STA_DTIMEOUT 或者 CRC 错误。层层排查后发现两个问题叠加了。第一个问题是我在 USER_Write 里只对缓冲区做了一个简单的取模对齐检查但没有真正处理缓冲区的物理地址对齐。FATFS 传入的缓冲区有时候是 FatFS 内部的一个全局数组它的对齐属性在 FatFS 配置里并不能完全保证 32 字节对齐。第二个问题是信号量等待时间太短大块数据写入时 MDMA 搬运时间比单块长很多如果传输完成中断稍微慢一点disk_write 就超时返回了。最终修复方案是双管齐下在 USER_Write 函数里如果检测到缓冲区地址不是 32 字节对齐就用一个静态对齐缓冲区做中转信号量超时时间从 100ms 拉长到 1000ms。实测修改后连续写 100MB 大文件没有再复现偶发 FR_DISK_ERR。4.5 坑五MDMA 配置后 SD 初始化卡在 SD_WaitReadOperation这个坑是我在尝试纯 MDMA 方案时遇到的。配置好 MDMA 之后SD 卡初始化 HAL_SD_Init 能正常通过但第一次调用 HAL_SD_ReadBlocks_DMA程序就卡在 HAL_SD_ReadBlocks_DMA 内部的等待循环里具体位置是 SD_WaitReadOperation 函数。从调试器看到的死循环点是等待 DMA 传输完成标志但 MDMA 的中断一直没有进来。排查了一圈问题不是 MDMA 本身坏了而是 CubeMX 生成的 MDMA 中断回调函数名的匹配问题。在 H7 的 HAL 库里SDMMC 使用外部 DMA 时SD 库内部先注册了自己的回调函数这个函数叫 HAL_SD_DMA_XferCpltCallback。SD 库在配置 DMA 之前会先把 HAL_SD_DMA_XferCpltCallback 挂给 DMA/MDMA 的 XferCpltCallback 指针。你如果自己再写一个 HAL_MDMA_XferCpltCallback 回调反而把 SD 库内部注册的回调覆盖掉了导致 SD 库认为 DMA 永远没完成。解决方法是不要自己重新实现 HAL_MDMA_XferCpltCallback而是把信号量给信号量的代码放在 HAL_SD_DMA_XferCpltCallback 里。这个函数是 SD 库的回调入口由它内部去调用 MDMA 的回调链。搞清楚这条回调链之后我把自定义回调去掉只保留void HAL_SD_DMA_XferCpltCallback(SD_HandleTypeDef *hsd) { BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(sd_xfer_sem, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }程序立刻恢复正常。这个坑说明一个通用原则在 HAL 库里当外设驱动内部已经封装了中断回调路径时尽量不要直接和外设的内部回调函数抢占钩子先认真读一遍驱动源码再动手。4.6 常见问题速查表这里把我实际遇到的和同事项目中常见的问题整理成一张表方便你遇到类似现象时快速定位。现象可能原因排查区间解决办法f_read 读出来是旧数据D-Cache 未失效USER_Read 函数DMA 完成后执行 SCB_InvalidateDCache_by_Addr写卡后文件数据错乱缓冲区未 CleanUSER_Write 函数DMA 前执行 SCB_CleanDCache_by_Addrf_open/f_write 随机卡死FATFS 重入未开启或应用层未加锁ffconf.h、应用层互斥开启 FF_FS_REENTRANT文件操作整体加互斥锁大块写偶发 FR_DISK_ERR缓冲区未对齐或超时过短USER_Write 函数做地址对齐超时设为 1000ms高时钟下读写不稳定卡未切换高速模式时钟 Divider、CSD 读取改用 24MHz 或通过 CMD6 切换卡模式SD 初始化卡死SDMMC 中断优先级/时钟源错误时钟树、NVIC 配置确认 SDMMC 内核时钟不超过 120MHz中断优先级合适MDMA 传输无完成中断回调函数被覆盖stm32h7xx_it.c、sd_diskio.c使用 HAL_SD_DMA_XferCpltCallback 作为入口f_mount 反复重试失败卡座接触不良或卡初始化时序硬件、时钟上电顺序确认上电延时大于 2ms卡检测脚处理正确5. 性能实测与优化记录5.1 不同配置下的读写吞吐对比整个调试稳定后我做了一轮性能对比测试测试环境是 H743 主频 480MHzSDMMC1 4 位模式TF 卡是普通的 Class 10 卡时钟分别跑了默认 24MHz 和尝试过的 48MHz 两档。在默认速度模式下24MHz未切换卡高速模式顺序写 128KB 缓冲区FatFS 实际写速度大概在 3MB/s 左右顺序读速度稍快能到 4MB/s 左右。这个速度对于一般的数据记录设备完全够用毕竟常见的传感器数据每秒也就几十 KB 到几百 KB。如果切换到高速模式并跑 48MHz读速度能明显提升到 10MB/s 以上写速度也能到 8MB/s 左右。但正如前面说的由于卡的模式切换逻辑需要额外代码而我的项目写稳定性优先最终生产配置停在 24MHz。如果你的项目对速度敏感建议把模式切换的代码加到初始化流程里先读 CSD 寄存器确认卡支持 High Speed再发 CMD9 和 CMD6 系列命令切换。5.2 进一步优化方向这个方案稳定之后我留了几个后续可以优化的方向。第一个是写文件时尽量使用 FatFS 的 f_lseek 预分配簇避免文件碎片化碎片多了之后SD 卡的随机写开销会显著增加长期运行性能下降明显。第二个方向是用双缓冲区配合环形队列采集任务先写入 RAM 缓冲区SD 卡写任务再从另一个缓冲区搬数据这样 DMA 传输时间和采集时间能重叠做到真正的流水线写入。另外排查任务栈余量可以用 FreeRTOS 提供的 uxTaskGetStackHighWaterMark 接口在任务里打印最低水位。我在 SD 卡写任务里加了这行代码发现任务栈用了大概 60%心里就有底了。如果你发现任务栈长期接近 100%别急着加大栈先看看是不是某些局部大数组占的栈空间太多改成静态分配或者堆分配更合理。关于这个方案我个人最后想说的整套配置从开始折腾到稳定运行前后花了一周时间踩过的坑一半在网络上都查不到明确的解决方案只能自己对着调试器和源码一点点猜。回头总结最值钱的其实是三个意识第一H7 的 Cache 和 DMA 是必须主动处理的不要侥幸第二RTOS 环境下文件系统必须做好重入保护和超时保护不要把系统稳定性寄托在“刚好不冲突”上第三不要跳步骤先裸机调通 SD 卡再上 FreeRTOS最后再加 MDMA每层都验证过再合到一起排查问题时能省一半时间。如果你的工程也卡在 H743、SD 卡、FatFS、FreeRTOS 这几个关键词的组合上照着第 3 章的代码结构和第 4 章的避坑清单走一遍大概率能一次跑通。至少我这边的项目已经连续运行了一个多月SD 卡读写没有出过一次问题。后续如果我再做 NPCX 或者别的平台上的文件系统移植也会继续把这类实战记录整理出来毕竟能让大家少走弯路的东西才是最有分享价值的。
返回列表