公司动态

STM32H7 GPDMA写XSPI DR报data transmit error的根因与修复

📅 2026/8/29 19:22:07
STM32H7 GPDMA写XSPI DR报data transmit error的根因与修复
1. 问题现场GPDMA 在更新 XSPI DR 寄存器时报告 data transmit error先说结论这不是芯片坏了也不是 GPDMA 外设本身有 bug绝大多数情况下是你对 XSPI 间接写模式的理解还不够“底层”或者说配置的顺序、数据宽度、地址对齐出了问题。这个错误在 STM32H7 系列上很典型尤其是从 F1/F4 系列迁移过来的老工程师十有八九会踩一次。现象是这样的固件运行过程中需要通过 XSPI 接口更新外部 Flash比如 NOR Flash 的状态寄存器、配置寄存器或者某个非连续地址的数据块于是配了 GPDMA 做 Memory-to-Memory 或 Peripheral-to-Memory 传输方向是从 SRAM 往 XSPI 的 DRData Register写数据。结果 GPDMA 传输控制块一启动中断标志位立刻挂起data transmit error传输长度直接卡死外设寄存器纹丝不动程序逻辑跟着乱套。这个问题的坑在于报错时机非常早经常是第一次 DMA 传输就失败而且错误标志一旦置位不清除的话后续所有请求都会被阻塞。更麻烦的是HAL 库的HAL_GPDMA_IRQHandler只帮你清除错误标志的一半另一半要靠手动操作导致很多人清完标志重试还是失败陷入死循环。这次遇到的场景是STM32H743外部挂了一颗支持 XSPI 接口的 NOR Flash固件需要通过 XSPI 的间接模式配合 GPDMA 更新 Flash 内的状态寄存器。硬件上没有问题逻辑分析仪抓 XSPI 时序也正常问题纯粹出在 GPDMA 请求和 XSPI 外设握手这一层。下面我会从根因分析、配置细节、排查流程到最终的解决方案完整过一遍顺带把 GPDMA 与 XSPI 协作时的几个隐藏规则说清楚。2. 根因分析为什么 GPDMA 写 XSPI DR 会触发 data transmit error2.1 XSPI 为什么不建议用 CPU 直写 DR很多人一开始的思路是XSPI 的 DR 寄存器本质上是内存映射的一个地址那我直接用 CPU 写*(__IO uint32_t *)0x90000000 data不就行了说实话在低负载场景下这确实能跑但问题在于 XSPI 的 DR 寄存器不是普通的 AHB 寄存器它是 FIFO 的窗口。当固件更新 XSPI DR 时CPU 直写会变成两个字面意义上的“灾难”首先CPU 写入是单次、突发长度固定的而 XSPI 外部 Flash 的命令执行周期往往需要连续的数据流CPU 直写会频繁打断 Flash 的命令序列其次CPU 直写无法保证 32 位对齐访问而 XSPI DR 寄存器在手册上明确要求 32 位访问一旦出现非字对齐访问总线侧立即报错。就是这种隐藏的总线错误最终反映到 GPDMA 侧就是data transmit error。所以 STM32H7 的参考手册其实推荐你用 GPDMA 配合 XSPI 的间接写模式Indirect Write Mode来更新外部 Flash 的寄存器或数据区。但 GPDMA 不是普通的 DMA它比 F4 系列的 DMA 复杂得多尤其在请求握手、传输宽度匹配、FIFO 控制这些方面有全新机制配置不当就会触发 data transmit error。2.2 GPDMA 与 XSPI 握手的真实流程先看正常流程GPDMA 通道使能后向 XSPI 发起读请求或写请求。XSPI 外设在收到 GPDMA 的请求后会把当前 FIFO 的状态反馈给 GPDMA。XSPI 的 FIFO 深度根据不同型号有差异H7 系列一般是 16 字节深。GPDMA 每完成一次数据传输按配置的数据宽度会检查是否还有数据要传如果有继续发起下一轮请求。这个过程中最容易翻车的点是GPDMA 发起请求的时机早于 XSPI 外设真正准备好接收数据。什么意思XSPI 在间接模式下CPU 要先写 CRCommand Register和 TCRTransmission Configuration Register把命令码、地址、数据长度、方向都配好然后置 CR 的 START 位。START 位置位后XSPI 才会开始执行外部 Flash 的命令序列并产生 DMA 请求。如果你在 START 位还没置位或者 XSPI 还没进入空闲状态时就使能了 GPDMA 通道GPDMA 会基于“外设可接收”的错误假设发起总线写操作。此时 XSPI 根本没有准备好接收数据总线侧会返回错误响应GPDMA 的外设错误标志位就会置起来也就是data transmit error。用生活化的类比你叫了一辆货拉拉但收货人还没把仓库门打开货拉拉司机把车停在门口拼命按喇叭DMA 请求门没开司机只能先把货卸在门口结果货丢了数据错误最后司机给你打电话说“这个单我送不了”error interrupt。2.3 数据宽度不匹配是第二个隐藏杀手GPDMA 的数据宽度可以配置为 Byte、Half-word、Word而 XSPI 的 DR 寄存器是 32 位寄存器FIFO 的数据通道也是 32 位宽度。如果你把 GPDMA 的传输数据宽度配成 Byte 或者 Half-word尤其在 Memory-to-Memory 模式下GPDMA 会尝试用非字对齐的时序去访问 XSPI 的 DR这直接违反 XSPI 的 AHB 访问规则。H7 系列的数据总线有它的脾气对非对齐访问不像 F1/F4 那样“睁一只眼闭一只眼”它会严格执行对齐规则并上报总线错误。这种错误会顺着 AHB 总线一路传到 GPDMA 的传输控制逻辑最终表现为data transmit error。我见过很多人用 CubeMX 自动生成代码DMA 参数里数据宽度默认是 Byte从来没改过。这在小规模数据搬运时可能运气好没触发但在 XSPI 这种严格要求 32 位访问的外设面前几乎必然翻车。3. GPDMA XSPI 间接写模式的正确配置流程3.1 硬件环境与基础配置假设先说这次调试的硬件环境MCUSTM32H743VIT6主频 480MHz外部 FlashNOR Flash支持 XSPI 协议映射基地址 0x90000000XSPI 时钟外部 Flash 工作频率约 100MHzXSPI 使用 Bank1GPDMA使用 GPDMA1 Channel0方向为 Memory-to-Peripheral严格来说是 Memory-to-Memory但因为目标是 XSPI DR 寄存器HAL 里配置成 Memory-to-Peripheral 更合理触发方式硬件请求触发请求源选择 XSPI1要彻底避免 data transmit error核心思路是保证 GPDMA 和 XSPI 之间的“请求-应答”节奏完全同步。GPDMA 不应该在 XSPI 未就绪时抢先发起访问。3.2 XSPI 侧的关键配置代码XSPI 初始化部分主要需要把间接写模式相关的位配好尤其是DMENDMA 使能位这一位不置 1 的话GPDMA 的请求根本不会被 XSPI 响应。XSPI_InitTypeDef xspi_init {0}; xspi_init.ClockMode XSPI_CLOCK_MODE_0; xspi_init.CLKDIV 1; // 实际分频根据 Flash 最高频率计算 xspi_init.FIFOThreshold XSPI_FIFO_THRESHOLD_04_BYTES; xspi_init.FIFOInterruptEnable DISABLE; xspi_init.MemoryMode XSPI_MEMORY_MODE_INDIRECT_WRITE; xspi_init.MemoryMapped DISABLE; xspi_init.BaudRatePrescaler XSPI_BAUDRATE_PRESCALER_8; HAL_XSPI_Init(hxspi1);注意FIFOThreshold这个参数决定了 XSPI FIFO 的空/满阈值间接影响着 GPDMA 请求的触发时机。我推荐设置成04_BYTES也就是 FIFO 中有至少 4 字节空间时 XSPI 才发出写请求。如果设成 1 字节FIFO 会频繁触发请求GPDMA 的访问次数暴增反而更容易撞上时序问题。3.3 GPDMA 节点的配置最容易出错的地方GPDMA 在 H7 上使用LL_GPFDMA或者 HAL 的HAL_GPDMA_Init都可以。这里我直接给 HAL 版本因为大多数人的工程已经用了 CubeMXHAL 集成度更高。GPFDMA_InitTypeDef gpfdma_init {0}; gpfdma_init.Request GPFDMA_REQUEST_XSPI1; gpfdma_init.BlkHWRequest DISABLE; gpfdma_init.Direction GPFDMA_MEMORY_TO_PERIPH; gpfdma_init.SrcInc GPFDMA_SRC_INC_INCREMENT; gpfdma_init.DestInc GPFDMA_DEST_INC_FIXED; gpfdma_init.SrcDataWidth GPFDMA_SRC_DATAWIDTH_WORD; gpfdma_init.DestDataWidth GPFDMA_DEST_DATAWIDTH_WORD; gpfdma_init.SrcBurstLength 1; gpfdma_init.DestBurstLength 1; gpfdma_init.TransferAllocatedPort GPFDMA_ALLOCATED_PORT_PB; gpfdma_init.TransferEventMode GPFDMA_FULL_TRANSFERT; gpfdma_init.Mode GPFDMA_NORMAL; gpfdma_init.Priority GPFDMA_PRIORITY_HIGH; gpfdma_init.SyncMode DISABLE; gpfdma_init.ProtectedMode DISABLE; HAL_GPDMA_Init(hdma_gpdma_xspi, gpfdma_init);这里几个关键参数我再拆一下Direction必须为 Memory-to-Peripheral。有人会问XSPI DR 不是内存映射地址吗用 Memory-to-Memory 行不行我的建议是不要。Memory-to-Memory 模式下 GPDMA 不依赖外部硬件请求它会按最大速率搬运完全无视 XSPI 的 FIFO 状态。这样只能凭运气保证不溢出。而 Memory-to-Peripheral 配合硬件请求GPDMA 每次传输都会等 XSPI 发出请求节奏完全同步。SrcDataWidth/DestDataWidth两边都必须配成 WORD。这是整个配置里最不能妥协的一项原因前面已经讲了XSPI DR 强制 32 位访问。DestInc目标地址不递增固定指向 XSPI DR。如果你配成递增GPDMA 会把数据搬到 XSPI DR 后面的其他寄存器那问题就复杂了寄存器会被写乱错误标志五花八门。SrcBurstLength/DestBurstLength选择 1。XSPI FIFO 深度有限突发长度太大FIFO 容易溢出。F4 上很多例程会配 4 或 8但在 XSPI 场景下保持 1 最稳妥。3.4 传输启动的顺序先 XSPI后 GPDMA这个顺序是很多初学者栽跟头的地方也是 data transmit error 最常见的诱因。正确顺序是先调用HAL_XSPI_Command配置命令寄存器把命令码、地址、数据长度写入 XSPI 的 CR 和 TCR。使能 XSPI 的间接写模式把 CR 的 START 位置 1。此时 XSPI 会开始执行 Flash 命令序列并发出 DMA 请求前提是 DMEN 置 1。最后才启动 GPDMA 传输HAL_GPDMA_Start_IT或HAL_GPDMA_Start。如果顺序反过来先启动 GPDMAGPDMA 立刻去读源数据并尝试写 XSPI DR此时 XSPI 还没开始执行命令DMA 请求还没产生GPDMA 就往一个“未启动的外设寄存器”上做写操作。H7 的总体会判定为非法访问报 data transmit error。3.5 等待事件与清标志位XSPI 间接写模式下GPDMA 完成传输之后并不代表数据已经被写到 Flash 里了。XSPI 的 FIFO 可能还有残存数据Flash 内部也还在执行编程操作。所以严格来说要等到 XSPI 的 TCFTransfer Complete Flag或 Flash 的 WIPWrite In Progress位清 0才算真正完成。实际操作中我会加这样一段等待代码while (HAL_XSPI_GetState(hxspi1) HAL_XSPI_STATE_BUSY) { // 等待 XSPI 内部状态机回到空闲 } // 或者直接查询 XSPI 状态寄存器 uint32_t sr hxspi1.Instance-SR; while ((sr XSPI_FLAG_TCF) 0) { sr hxspi1.Instance-SR; }如果你用了中断方式在 GPDMA 的传输完成中断里务必先清 GPDMA 的完成标志再检查 XSPI 是否还有数据没送完。理想情况下GPDMA 完成中断里只做两件事记录完成标志、把 XSPI 的 TCF 中断使能打开如果没开的话然后等 XSPI 的 TCF 中断到来再处理后续业务。4. 实际排查过程从一个错误标志到完整定位的现场复盘4.1 第一次复现错误标志与失败点确认我当时的现象是固件上电后第一次调用 XSPI 寄存器更新函数GPDMA 立刻进入错误回调HAL_GPDMA_ErrorCallback里面读到hdma_gpdma_xspi-ErrorCode是HAL_GPDMA_ERROR_DATA_TRANSMIT。用调试器查看 XSPI 外设寄存器SR 的TEFTransfer Error Flag位是 0TCF位是 0说明 XSPI 侧其实没有检测到错误错误是 GPDMA 自己报的。这就很关键了直接说明问题不出在 Flash 命令执行阶段而是出在 GPDMA 与 XSPI 的请求握手阶段。继续查看 GPDMA 的通道状态寄存器发现GPDMA_CH0_SR的DTEIFData Transfer Error Interrupt Flag是 1。同时GPDMA_CH0_CFCRChannel Configuration Register里的DTE位也是 1。这确认了问题GPDMA 在尝试访问 XSPI DR 寄存器时收到了总线错误响应。4.2 缩小范围是地址问题还是同步问题确认到这一步无非三种可能XSPI DR 寄存器地址写错了或者访问了没有映射的地址。GPDMA 在 XSPI 还没准备就绪时抢先访问。数据宽度或者对齐不满足 XSPI 的要求。先排查第一种。查 Keil 编译生成的 map 文件XSPI 外设寄存器的基地址是 0x52007000DR 寄存器的偏移是 0x74所以 GPDMA 的目标地址是 0x52007074。再看 GPDMA 通道的DA寄存器写入的值确实是 0x52007074。地址没问题。再看第二种可能。我把 GPDMA 启动的代码在 XSPI START 位置位之后加了一个约 1 微秒的延时再重试仍然报错。说明问题不是简单的“时序太早”而是 GPDMA 和 XSPI 之间的硬件握手没有建立起来。第三种可能越来越像。我检查了 GPDMA 配置果然里面DestDataWidth还是 CubeMX 生成的默认值GPFDMA_DEST_DATAWIDTH_BYTE源数据宽度也是 BYTE。这等于让 GPDMA 用 8 位总线宽度去写一个只接受 32 位访问的寄存器总线错误几乎是必然的。4.3 修复验证从 Byte 改成 Word 后仍然报错我把源和目的数据宽度都改成 Word重新编译、下载结果还是报 data transmit error只是这次报错的位置变了不再是第一次写 DR 就报而是传输到某个临界点才报。这个现象让我意识到光是数据宽度还不够GPDMA 的请求模式也需要改。重新回到 CubeMX把 GPDMA 请求源改成GPFDMA_REQUEST_XSPI1而不是用软件的 Memory-to-Memory 模式并确保 XSPI 的DMEN位置 1。同时把DestBurstLength从 4 降为 1SrcBurstLength也从 4 降为 1重新配置后再跑错误消失XSPI DR 寄存器里的数据正常更新到外部 Flash。所以结论很清晰data transmit error 是多种配置错误叠加的结果单独修一个参数往往不能完全解决因为前面的错误会掩盖后面的问题。要一次性排干净必须按照“请求源 → 数据宽度 → 突发长度 → 启动顺序”这个顺序逐一确认。5. XSPI DR 寄存器更新失败的常见原因速查表为了方便以后排查我把这类问题常见的原因和表现整理成一张表建议收藏。常见原因触发阶段错误特征解决方案GPDMA 数据宽度配成 Byte/Half-word首次传输GPDMA 报 DTEIFXSPI SR 为 0源和目的数据宽度统一切为 WordGPDMA 目标地址递增传输进行中寄存器被写乱XSPI 状态异常目标地址设为 Fix不递增XSPI 的 DMEN 未置 1传输一开始GPDMA 一直等待请求可能超时XSPI CR 的 DMEN 位置 1GPDMA 启动早于 XSPI START传输一开始GPDMA 报 DTEIFXSPI SR 为 0先启动 XSPI再启动 GPDMAGPDMA 请求源配置为软件触发传输无节奏偶尔报错时序不稳定请求源改为 XSPI1 硬件请求突发长度过大1/4/8/16大批量传输FIFO 溢出或报告传输错误突发长度改为 1等待时间不足FIFO 未清空传输完成阶段数据丢失Flash 内容不完整等待 XSPI TCF 标志置位MPU/Cache 配置不当固定地址写入数据看似写入读回却不一致对 XSPI 映射区域配为 non-cacheable 或执行 clean 操作这张表里每一条我都实际踩过或者帮同事排查过不是从手册里抄的。尤其是请求源那条太容易忽略了。CubeMX 生成 GPDMA 的默认请求源是GPFDMA_REQUEST_0这是一个软件触发源如果你忘了把它改成 XSPI1GPDMA 就会按自己的节奏疯狂搬运数据根本不看 XSPI 是否就绪。这种情况下大多数时候能跑通因为 XSPI 的 FIFO 有空间但压力一大或者时序抖动就会随机报 data transmit error。6. 排查与修复的实操建议6.1 用调试器快速定位错误源头遇到 data transmit error 时先做三件事第一看 GPDMA 通道的SR寄存器重点看DTEIF、TCIF、USIF这几个标志位判断是哪种错误。DTEIF代表数据传输错误USIF代表用户软件中断TCIF是正常完成。第二看GPDMA_CH0_DADestination Address寄存器确认目标地址是否符合预期。如果DA不是 XSPI DR 的地址说明你在代码里对 DMA 句柄的配置被覆盖了。有个常见的坑是多个 DMA 通道共用一个hdma句柄或者HAL_GPDMA_Start的入参地址写错导致DA指向奇奇怪怪的地方。第三看 XSPI 的SR寄存器如果TEF或者TCF置位了说明错误其实发生在 XSPI 侧这时候要往 Flash 命令序列的方向查如果 XSPI SR 是干净的说明错误发生在 GPDMA 访问 XSPI 的总线阶段。6.2 关于 GPDMA 中断回调的细节如果你使用中断模式建议在HAL_GPDMA_ErrorCallback里加一个断言或者错误日志把ErrorCode、当前通道寄存器状态、XSPI 的 SR 值全部打印出来。这个信息量比一句“DMA error”值钱多了能帮你省下半天抓头时间。另外要注意GPDMA 的错误中断触发后通道不会自动停止。下次要重新启动传输必须先手动调用hdma_gpdma_xspi.Instance-CFCR | GPDMA_CFCR_SWRST;把整个通道复位再重新初始化配置否则残留的状态位会导致问题延续。6.3 缓存一致性容易忽略的第二个坑当源数据在 DTCM/SRAM 且 D-Cache 开启时GPDMA 读取到的数据可能不是 CPU 刚写入的最新值因为 D-Cache 还没被写回。这在更新 XSPI DR 这种高频小数据量场景下尤其隐蔽——数据明明在 CPU 视角里是对的但 DMA 读出来的是旧的。解决方案有两种第一种在启动 DMA 传输前调用SCB_CleanDCache_by_Addr((uint32_t *)src_addr, size)保证 DMA 能看到最新的数据。第二种把源 buffer 定义到非 cacheable 的内存区域。在 H7 的链接脚本里可以单独划一块 non-cacheable 区域比如RAM_NC这样就不需要每次手动 clean。我比较推荐第二种因为省心也不会因为忘记 clean 而埋雷。6.4 补充一个时序细节XSPI 的 FIFO 与 GPDMA 的传输量XSPI 间接模式下TCR 寄存器里的DCOUNT字段定义了总共要传输多少个字节。GPDMA 侧配置的 Block Length 要和 XSPI 的DCOUNT保持一致否则容易出现“GPDMA 传完了XSPI 还在等数据”或者反过来。我当时遇到的一个隐性问题就是XSPIDCOUNT配的是 4 字节GPDMA 的 Block Length 也配了 4但 GPDMA 还会把 FIFO 中的一个预取prefetch字节算进去导致最终 XSPI 认为传输完成但 GPDMA 还在等。这种现象的表现不是 data transmit error而是卡死在等待 TCF 标志上。解决办法是 GPDMA 传输完成后再追加一轮“空读”或者查询 XSPI 的FIFO状态确认没有残存数据。7. 一个可以直接复用的最小代码模板最后给一个可以直接搬进工程用的最小代码片段。它包含三个函数XSPI 初始化、GPDMA 初始化、以及一个封装好的“通过 GPDMA 更新 XSPI DR”的函数。这套逻辑我在 STM32H743 上验证过跑在 480MHz 主频下连续更新 128 字节数据无压力。// 封装通过 GPDMA 写 XSPI DR int32_t XSPI_GPDMA_WriteRegister(XSPI_HandleTypeDef *hxspi, uint32_t reg_addr, uint32_t *data, uint32_t len) { // 1. 配置 XSPI 命令 XSPI_CommandTypeDef cmd {0}; cmd.OperationType XSPI_INDIRECT_WRITE; cmd.InstructionMode XSPI_INSTRUCTION_1_LINE; cmd.AddressMode XSPI_ADDRESS_1_LINE; cmd.DataMode XSPI_DATA_1_LINE; cmd.Instruction WRITE_STATUS_REG_CMD; cmd.Address reg_addr; cmd.DataLength len; cmd.DummyCycles 0; cmd.AlternateBytesMode XSPI_ALTERNATE_BYTES_NONE; cmd.SIOOMode XSPI_SIOO_INST_EVERY_CMD; if (HAL_XSPI_Command(hxspi, cmd, HAL_XSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return -1; } // 2. 配置 GPDMA 节点只配置一次即可这里为清晰起见保留 // 需要确保 hdma_xspi 的 Request 是 GPFDMA_REQUEST_XSPI1 // 且 SrcDataWidth / DestDataWidth 都是 WORD // 3. 启动 GPDMA 传输硬件请求模式由 XSPI 的请求驱动 if (HAL_GPDMA_Start(hdma_xspi, (uint32_t)data, (uint32_t)hxspi-Instance-DR, len) ! HAL_OK) { return -2; } // 4. 启动 XSPI 间接传输 if (HAL_XSPI_StartIT(hxspi, hdma_xspi) ! HAL_OK) { return -3; } // 5. 等待 GPDMA 完成这里可以使用信号量/事件标志为演示用轮询 while (HAL_GPDMA_GetState(hdma_xspi) ! HAL_GPDMA_STATE_READY) { // 等待 } // 6. 等待 XSPI TCF while ((hxspi-Instance-SR XSPI_FLAG_TCF) 0) { // 等待 } return 0; }这个模板的核心思路是GPDMA 配置成硬件请求模式由 XSPI 来决定 GPDMA 什么时候可以搬数据而不是让 GPDMA 自己闷头搬。这也是解决 data transmit error 的根本思路。8. 我在实际调试中的几点体会写这篇文章的时候我又回忆起调这个问题的那个下午。最开始我也犯了“只改一个参数就重试”的毛病改完数据宽度还是报错时差点摔键盘。后来静下心来把 GPDMA 与 XSPI 的交互流程逐段拆开才发现问题不是一个而是四个叠加请求源没选对、数据宽度不对、突发长度过大、启动顺序反了。如果非要说一条最重要的经验那就是GPDMA 不是普通 DMA 的升级版它有自己的请求模型和传输策略不要用 F4 时代的 DMA 思维去套它。尤其在和 XSPI、OctoSPI 这类带 FIFO 和状态机的外设配合时多花点时间读参考手册里关于外设请求和 FIFO 阈值的内容比盲目试参数强一百倍。另外一个小技巧调试阶段可以把 GPDMA 和 XSPI 的错误中断都打开在两个回调函数里加断点。当错误触发时先看是哪个回调进的断点这能帮你快速划分责任边界。如果是 GPDMA 回调说明问题偏向总线侧如果是 XSPI 回调说明问题偏向 Flash 命令侧。这个“责任划分法”帮我在处理后续几个项目问题时省了不少时间。最后再补充一点不要迷信 CubeMX 自动生成代码。CubeMX 能帮你把外设初始化和引脚复用配好但 GPDMA 的请求源、数据宽度这类参数它往往只会给一个最通用的默认值不会根据你的 XSPI 外设自动适配。动手之前把生成代码里的 GPDMA 配置部分人工复核一遍你会发现超过一半的“诡异 bug”都藏在那些“默认值”里。