资讯动态

STM32F407以太网通信故障排查:CCM内存导致DMA无法访问的坑与修复

发布时间:2026/8/29 12:18:13 来源:尧图企业网站定制
最近在一台 STM32F407 上栽了一次跟头固件跑得好好地唯独以太网通信就是不通。折腾了一整天最后发现罪魁祸首不是硬件、不是协议栈而是我自己把一块 CCM 内存分给了以太网 DMA 缓冲区。这个问题的隐蔽性很强程序不报错、不宕机、寄存器看起来也正常但数据就是发不出去。这篇文章就把整个排查过程、背后的总线访问原理、以及最终的修复方案完整记录下来希望对正在用 STM32 LwIP 做以太网通信的朋友有帮助。先说结论STM32F4 系列中的 CCM RAMCore Coupled Memory内核耦合内存是一块只有 CPU 内核才能访问的内存区域DMA 控制器、以太网 MAC、USB 等外设都无法访问。如果你把以太网的 DMA 描述符或缓冲区放到了 CCM那么 CPU 写入的一切数据都“看似正常”但 DMA 根本摸不到这些内存通信自然全断。接下来我会从故障现场开始一步步还原排查思路和修复方法。1. 故障现场与初步判断1.1 项目配置与故障表现这次项目的硬件平台是 STM32F407VET6软件用的是 LwIP 2.1.2实时系统用的 RT-ThreadPHY 芯片是 LAN8720A接口模式是 RMII。这套组合在之前的几个项目里已经跑得很稳定所以一开始我完全没有怀疑到内存分配上。故障现象非常典型PHY 的 link 状态正常通过 MDIO 读取 PHY 寄存器也能读到正确的 IDDHCP 却一直拿不到地址。我手动配置静态 IP 之后ping 对端完全不通偶尔能发出一两个包但基本有去无回。从应用层看LwIP 的初始化流程走完了netif 状态也是 UP整个协议栈像是好的但底层数据就是出不去。第一反应是怀疑硬件毕竟网络这种模拟信号链路PCB 布线、PHY 外围配置电阻、50MHz 晶振精度都可能出问题。于是先拿示波器测 REF_CLK频率准确50MHz 没问题。RMII 的 TXD、TXD_EN 也有波形说明 MAC 至少往 PHY 送了数据。RX 方向没有信号还算正常因为在发送不成功的情况下对端发来的报文也没法被完整处理。1.2 一开始的排查思路接下来排除 PHY 配置。LAN8720A 的 BMCR 寄存器读出来是 0x3000Auto-Negotiation 已开启BSR 寄存器 bit 2 为 1链路已建立PHY 器件 ID 0x0007C0F1 也能正确读出。到这一步基本可以判定物理层和 PHY 驱动没有问题。剩下的就是 MAC 层和 DMA 层。我先在初始化完成后把以太网 DMA 的状态寄存器读出来看没有报错标志。发送一个测试报文缓冲区里的数据也确实是完整的。这就有意思了所有 CPU 能看到的环节都正常但实际通信就是不通。这种“上层正常、底层沉默”的问题往往不是简单的代码逻辑错误而更可能是内存访问路径出了问题。我当时隐隐感觉和 DMA 缓冲区位置有关于是把排查重心转向了DMA 描述符和缓冲区的实际物理地址。2. CCM 内存的定位与访问限制2.1 CCM 在总线矩阵上的位置STM32F4 系列里CCM RAM 是一块很有意思但又容易踩坑的内存。它叫 Core Coupled Memory直译就是内核耦合内存。普通 SRAM 位于 0x2000_0000 起始地址段通过总线矩阵连接到 CPU、DMA1/DMA2、以太网 DMA、USB DMA 等多个主设备。而 CCM RAM 的起始地址是 0x1000_0000它没有挂在总线矩阵上而是直接连接在 CPU 的 D-Bus 上。这句话翻译成实际效果就是CPU 访问 CCM 非常快但 CPU 之外的所有主设备都看不见这块内存区域。你可以把普通 SRAM 想象成一个有多个门的小区快递员、外卖、住户都能进出而 CCM 是一个只有户主本人有钥匙的独立房间外面的服务人员根本进不去。在这个场景里以太网 DMA 就是那个“快递员”它想把数据送入缓冲区但发现地址在 CCM 里总线矩阵根本不响应这个请求于是数据就永远卡住了。2.2 为什么 DMA 到不了 CCM在 STM32F4 的内存架构中总线矩阵是核心的“中控台”。CPU 通过 I-Bus、D-Bus、S-Bus 访问各类外设和内存DMA 控制器也通过总线矩阵发起读写。但 CCM RAM 不走这条路径它专门给 CPU 提供了一个私有的高速通道。所以当以太网 DMA 控制器试图访问 0x1000_0000 地址段的 CCM 时它发出的请求根本不会被总线矩阵响应。DMA 不会像 CPU 那样触发一个清晰可见的异常而是表现为“访问失败”或“一直等待状态”。数据手册里其实有注释通常在内存映射图附近一整句话“CCM data RAM is not accessible by the DMA controller.” 可惜这句话太不起眼很多开发者根本没注意到包括我。2.3 这类问题最常见的出现方式根据我见过和踩过的坑CCM 被用错通常有三种典型情况在链接脚本中把整个.bss段或堆区域指向了 CCM目的是压缩 SRAM 占用。通过__attribute__((section(.ccmram)))主动把某个大缓冲区放到 CCM但没有仔细核实这个缓冲区是否会被 DMA 访问。使用某个 OS 或协议栈自带的内存池管理功能内存池恰好被安排在 CCM 区域。最常见的一句“坑队友”话就是“这块缓冲区会被高频访问把它放到 CCM 里提高速度。” 结果就是缓冲区确实是快了但访问它的外设反而“看不见”了。3. 从现象到根因的排查实录3.1 检查 PHY、MDIO 和中断路径我先把 PHY 层面彻查了一遍。通过 MDIO 读取 PHYIDR1 和 PHYIDR2读到的是 0x0007 和 0xC0F1和 LAN8720A 的手册一致。然后再读 BSR 寄存器bit 2 为 1说明链路已经建立。这里我多做了一个测试把 PHY 的 loopback 模式打开让它自己发自己收看 MAC 能否收到数据。结果依然没有反应这说明问题不在 PHY 的外部链路而是在 MAC 或 DMA 这一层。接着看中断路径。我检查了 EXTI 和以太网中断是否正常触发。PHY 的中断脚没有触发是正常的因为我还没开相关事件但以太网 DMA 的中断标志位也一直没有变化这就有问题了。我读了一下 ETH-DMASR 寄存器里面的正常中断汇总位NIS是 0说明 DMA 根本没有产生过任何发送或接收完成的中断。CPU 这边看起来一切正常但底层 DMA 纹丝不动。3.2 检查 DMA 描述符状态I then went into the ETH descriptor ring. Reading the DMATXDLAR register gives the address of the TX descriptor list. That address, to my surprise, read 0x10000000区域的一个地址也就是 CCM 的地址范围。这里得稍微展开说一下以太网 DMA 的工作方式。ETH DMA 不会直接拿到数据缓冲区的地址而是先读取一串“描述符”。描述符里记录了缓冲区物理地址、数据长度、控制标志等。CPU 在发送前把描述符和缓冲区准备好然后置位描述符里的 OWN 位DMA 就会顺着描述符去搬运数据。发送完成后DMA 会清除 OWN 位并更新状态。接收方向同理DMA 会往缓冲区写数据并更新描述符。我在调试器里查看 TX 描述符的 TDES0发现 OWN 位一直是 1。也就是说 CPU 已经把这包数据“委托”出去了但 DMA 一直没有处理也没有把 OWN 位清掉。再到 RX 描述符看RDES0 的 OWN 位也一直没被 DMA 修改过等于接收路径也完全没启动。这个证据已经很接近答案了DMA 根本没有碰过这些描述符。3.3 map 文件里找到了决定性证据为了确认描述符和缓冲区到底放在哪里我打开了编译生成的.map文件直接搜以太网相关的符号。结果一目了然.bss.eth_dma_tx_desc 0x10000200 size 0x80 .bss.eth_tx_buffer 0x10000400 size 0x800 .bss.eth_dma_rx_desc 0x10000C00 size 0x80 .bss.eth_rx_buffer 0x10000E00 size 0x800这些地址全部落在 0x1000_0000 段也就是 CCM RAM 区域。真相大白我之前为了“提高访问速度”用__attribute__((section(.ccmram)))把以太网 DMA 描述符和缓冲区都放进了 CCM。CPU 访问它们毫无问题但以太网 DMA 根本够不着。4. 问题的本质DMA 与 CCM 的“错配”4.1 以太网 DMA 如何搬数据这个问题从根上说是“地址可见性”的问题。以太网 DMA 是独立于 CPU 工作的主设备它去某个地址搬数据时不是 CPU 去搬而是 DMA 自己发起的 AHB 总线事务。这部分事务能不能成功取决于目标地址是否在 DMA 的可访问地址映射里。在 STM32F407 上ETH DMA 能够访问的是从 0x2000_0000 开始的常规 SRAM 区域以及外部存储器接口等经过总线矩阵映射的区域。CCM 的 0x1000_0000 地址段不在其可访问列表里所以描述符读不到、缓冲区写不进。4.2 为什么“看起来”一切正常却不报错我最开始非常困惑一个问题如果 DMA 访问了非法地址为什么没有触发 Bus Fault 或者 HardFault原因在于 CCM 对于 CPU 来说是合法且可访问的地址。CPU 写缓冲区、CPU 操作描述符这些操作全部成功因为它们确实是 CPU 执行的操作。问题出在 DMA 那边它的总线事务在到达总线矩阵时被“无视”了。这个失败不会被映射成 CPU 的异常因为它不是 CPU 发起的事务DMA 自己也未必能把错误状态同步到 CPU 能看到的地方。于是现象就极其迷惑协议栈认为数据发出去了CPU 侧的内存里也全是正确数据但物理链路上就是没有包。只有把 DMA 描述符的 OWN 位拿出来看才会发现 DMA 从始至终没有真正接管过这些缓冲区。4.3 还有哪些 MCU 有类似限制CCM 的问题不只是 STM32F4 独有很多 MCU 都存在“部分内存区域无法被 DMA 访问”的限制STM32F2/F3/F4 系列有 CCM RAMDMA 不可访问STM32F7 系列有 DTCM 和 ITCMDMA 也不能直接访问部分 Cortex-M 内核产品有紧耦合内存同样只对 CPU 可见一些 SoC 的 DDR 分区配置或 MPU 配置不当也会导致外设看不到某些区域。我现在的习惯是只要碰到 DMA 外设和自定义内存区域第一步就确认物理地址是否在 DMA 的可访问范围内。这个动作看似简单但能省掉大量低效的调试图。5. 修复方案与工程修改5.1 修改链接脚本把以太网缓冲区放到 SRAM问题原因明确了修复方案其实很简单把以太网 DMA 描述符和缓冲区强制放到常规 SRAM也就是 0x2000_0000 段。我在链接脚本里专门新增了一个.eth_dma段并显式指定放在 SRAM 区域/* 链接脚本片段 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K CCMRAM (rw) : ORIGIN 0x10000000, LENGTH 64K SRAM (xrw) : ORIGIN 0x20000000, LENGTH 112K } SECTIONS { .eth_dma (NOLOAD) : { . ALIGN(32); *(.eth_dma) *(.eth_dma.*) . ALIGN(32); } SRAM .ccmram : { . ALIGN(4); *(.ccmram) *(.ccmram.*) . ALIGN(4); } CCMRAM }这里把.eth_dma段放在 SRAM同时保留了.ccmram段给其他真正适合放 CCM 的内容。NOLOAD关键字表示这是一个不要求上电初始化的数据段因为它们在运行前会被代码主动初始化符合 DMA 缓冲区的使用方式。5.2 代码中显式指定内存区域代码侧我用一个宏来统一标注以太网相关的内存变量#define ETH_DMA_MEM __attribute__((section(.eth_dma))) ETH_DMA_MEM ETH_DMADescTypeDef dma_tx_desc[ETH_TX_DESC_CNT]; ETH_DMA_MEM ETH_DMADescTypeDef dma_rx_desc[ETH_RX_DESC_CNT]; ETH_DMA_MEM uint8_t tx_buffer[ETH_TX_DESC_CNT][ETH_TX_BUF_SIZE]; ETH_DMA_MEM uint8_t rx_buffer[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE];这里有个容易被忽略的细节DMA 描述符和缓冲区最好按 32 字节对齐。STM32F4 的以太网 DMA 对 32 字节对齐有最佳支持如果不对齐轻则性能下降重则可能出现边界覆盖问题。链接脚本里的. ALIGN(32)就是这个目的。如果你的程序里还用了直接内存访问的 USB 或 SDIO同样建议把对应的缓冲区单独安排到 SRAM 的一个明确 section并在 map 文件里确认落点。5.3 验证修复结果与回归测试改完编译我先打开 map 文件确认地址已经变化.eth_dma 0x20006000 ...地址落在 0x2000_0000 段符合预期。烧录后上电DHCP 大约几秒钟就拿到了地址静态 IP 也能正常 ping 通。连续跑了一整晚的压力测试没有出现断连、丢包或 DMA 错误标志。我顺手把之前为了“提速”而放进 CCM 的其他内容也过了一遍。最后保留在 CCM 里的只有一块高频计算用的临时缓存该缓存完全不涉及任何 DMA 外设读写都靠 CPU 内核这才是 CCM 的正确用法。6. 常见问题速查与预防经验6.1 常见排查问题速查表现象可能原因处理办法以太网发送/接收完全不通但 PHY link 正常ETH DMA 缓冲区或描述符被分配到 CCM 或其他 DMA 不可访问区域将相关变量强制放到普通 SRAM 的段中发送偶尔成功接收完全收不到描述符在 SRAM但 RX buffer 在 CCM检查所有 RX 方向变量的 section 和地址程序能跑但改完内存分配后网络突然异常堆或内存池被放到 CCMLwIP 动态分配的 pbuf 落在不可访问区域不要把堆或内存池定义在 CCM 中DHCP 拿不到地址或 ARP 不回底层接收路径异常RX DMA 没有正常工作查看 RX 描述符 OWN 位是否被 DMA 清除初始化后网络中断不触发DMA 中断被屏蔽或 DMA 根本没开始工作结合 map 文件确认所有网络缓冲区地址6.2 工程习惯与预防建议第一项目初期就画一张“内存与外设访问关系表”把哪些地址段可以被哪些主设备访问写清楚。尤其是网络 DMA、USB DMA、摄像头 DMA、SDIO 这类拿数据比较猛的外设必须逐个确认。第二不要轻易把整个.bss或堆挪到 CCM 区域。确实有项目为了节约 SRAM会打 CCM 的主意但全局变量覆盖范围太大很容易让某些 DMA 外设踩空。CCM 更适合放 CPU 频繁使用、且明确不会被外设访问的数据结构。第三每次编译后花 30 秒搜索.map文件确认关键数组和缓冲区实际落点。命令行一行就能完成grep -nE eth_dma|dma_tx|dma_rx|ccmram build/app.map看到地址前缀是 0x1000 还是 0x2000心里就有数了。最后再分享一个小技巧如果调试时怀疑缓冲区被放到了错误区域不用重新编译直接在调试器里查看相关类型的变量地址对比一下芯片手册里的内存映射图就行。确认地址段落在 CCM 里又有 DMA 外设访问它那问题基本就锁定在这个原因上了。这次踩坑让我长了不少记性也希望大家看完能少走这一步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价