ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CCM内存导致以太网DMA通信故障:一次嵌入式排查实录

CCM内存导致以太网DMA通信故障:一次嵌入式排查实录 前阵子调一块带以太网的设备被一个非常隐蔽的问题折腾了整整两天。现象说起来很简单板子能正常 link upPHY 的状态也正常但一跑 TCP 通信就断断续续最后彻底 ping 不通主控这边还时不时 HardFault。一开始我也没多想以为是硬件问题从网络变压器到 PHY 周边电路甚至把晶振都换了一遍照样复现。后来静下心来翻工程改动记录才意识到问题出在一个看起来人畜无害的内存区域——CCMCore Coupled Memory。如果你在嵌入式开发里做过带 DMA 功能的以太网、USB、SDIO 这类外设建议把这篇看完能帮你省掉一次至少两天的 Debug 时间。先说明一下这里的 CCM 不是摄像头模组也不是可鉴别加密里的 CCM 模式。这三个词都叫 CCM但完全不是一回事。我在查资料时发现很多刚入门的朋友会把它们搞混所以下面会先用一张表把这几个概念区分开。本文要讲的是 ARM Cortex-M 系列内核里一种特殊的内存区域全称叫 Core Coupled Memory中文一般叫内核紧耦合内存。它本身是给实时性要求高的代码和数据准备的速度极快但恰恰因为它“太贴近 CPU”反而成了很多外设 DMA 的盲区。我用的主控是 STM32F407以太网协议栈是 LwIP收发描述符和 DMA 缓冲区的分配方式沿用了原厂评估板的写法。问题就出在我在优化内存布局的时候把一块 16KB 的收发缓冲区手工放到了 CCM 区域里。结果测试时才发现这种在 CPU 眼里“飞一般快”的内存以太网 MAC 的 DMA 控制器根本访问不到数据收发直接瘫痪。1. 现象复现与问题描述把问题完整复现一遍方便大家对照自己的场景。我的测试环境是 STM32F407 LAN8720A 这颗 RMII 接口的 PHYMAC 地址和 IP 都配置好上位机用 TCPUDP 调试工具连板子。正常情况下板子上电后会广播一个 UDP 数据包上位机能收到然后双方建立 TCP 连接进行持续的数据双向传输。1.1 三个典型异常现象第一个现象是 link 正常但收不到包。板子启动后 PHY 的 link 状态是通的用 netstat 或者抓包工具看网口链路层也是 up 的但 TCP 服务器始终收不到上位机发来的任何数据。用 Wireshark 抓包能看到板子发出过 ARP 请求但后续的 TCP 握手包就像石沉大海完全没有回应。第二个现象是丢包率极高且极不稳定。偶尔能收到一两次数据但很快又断了。这种情况最迷惑人因为它时好时坏容易让人误判为电源纹波、晶振不稳定或者网络干扰。我有个习惯遇到这种“偶尔正常偶尔异常”的问题会优先怀疑软件逻辑而非硬件但当时还是被带偏到了硬件排查上浪费了不少时间。第三个现象是开启调试器抓取接收描述符时发现 ETH_DMARxDesc 的状态位始终是空的。CPU 这边明明看到接收缓冲区地址是连续且合法的但 DMA 就是没有把收到的数据搬进来。这个现象后来成了定位问题的最强论据——不是网络协议栈的问题而是内存访问路径断掉了。1.2 最初的方向判断为什么是错的当时我的第一反应是硬件电路出了问题。排查方向很自然先量 RMII 接口的 REF_CLK 时钟再用逻辑分析仪抓 MDIO 和 MDC确认 PHY 寄存器能不能正常读写最后用万用表量网络变压器的中心抽头电压和共模线圈的直流电阻。这些检查做下来硬件全部正常。接着我把 PHY 的配置寄存器从头到尾翻了一遍包括 LED 配置、中断状态、自动协商结果都没有发现异常。MAC 这边也把过滤模式从混杂模式改成了单播过滤甚至把 DMA 的 burst 长度都改了一遍问题依旧。一直到我在 map 文件里扫描各个段的内存分配情况注意到ccmram段里躺着几个大数组而这些数组的变量名正好是以太网 DMA 描述符和收发缓冲区的声明。这时候我才猛然意识到问题出在内存区域的选择上而不是硬件和协议栈本身。2. CCM 三类易混淆概念快速扫盲先说清楚社区里搜索“CCM”时会出来三种完全不同的含义都缩写为 CCM。如果方向理解错了很容易浪费时间缩写全称领域说明CCMCore Coupled Memory嵌入式/Arm 内核紧耦合内存连接在 CPU 私有总线上外设 DMA 不可访问CCMCounter with CBC-MAC密码学一种认证加密模式常用于无线通信的数据加密CCMCamera Compact Module消费电子/安防摄像头模组比如手机摄像头、车载摄像头模块本文只探讨第一种。后两种如果大家感兴趣我可以以后单独写但千万别把它们混在一起查资料。我在排查过程中就见过有人把 Ethernet 通信失败往加密算法上靠方向完全跑偏了。2.1 为什么 CCM 这个名字本身就有误导性CCM 字面上是“内核紧耦合内存”听上去像是“更快的内核 RAM”容易让人自然联想到“把关键数据放进这里”就是白捡的性能优化。这种思路在大方向上没错但它忽略了一个致命前提这块内存只连接在 CPU 内核的总线上它并不是系统内存映射里一个通用的、所有总线主机都能访问的普通 SRAM。以 STM32F4 系列为例CCM 通常从地址0x10000000开始大小从 64KB 到 128KB 不等。常规 SRAM 则从0x20000000开始这两者都能被 CPU 直接寻址但只有后者能通过总线矩阵与 DMA 控制器、以太网 MAC、USB OTG HS 这类外设建立通路。CCM 就像一栋楼里只有业主自己手里有钥匙的专属电梯——你住里面当然方便但快递员DMA永远上不来。所以 CCM 的“误导性”在于它的性能和功耗特性好得诱人但如果对总线拓扑结构不清楚就很容易把它当成普通 SRAM 的平替。一旦把需要 DMA 搬运的数据放进去表面上程序能运行、中断也能触发可外设根本无法访问这片区域结果就是各种诡异故障。2.2 适合放进 CCM 的数据类型虽然 Ethernet 通信场景里 CCM 是坑但不能否认它在某些场景下非常香。真正适合放 CCM 的数据有几类中断服务函数里高频读写的关键变量比如 RTOS 的任务切换标志、系统 Tick 计数、时间戳变量。CPU 密集型算法中的临时缓冲区比如加密算法、FFT、电机控制里的运算中间量。启动阶段就要用到的关键栈空间前提是栈里不会跑 DMA 相关的中断回调。我在电机控制项目里把 FOC 电流环的 PI 参数和正弦查表数据放到 CCM 里实测性能有约 10% 的提升因为避免了额外的总线仲裁等待。但同样的优化手段直接搬到网络通信项目里就会引发灾难。3. 为什么 CPU 能流畅访问的 CCM 反而是 DMA 的盲区要理解为什么 DMA 访问不了 CCM得简单看一下 Cortex-M 内核的总线结构不需要深入研究硬件架构把关键点讲清楚就够了。3.1 Cortex-M 内核的总线拓扑与 CCM 位置Cortex-M4 内核有两条主要的内部总线路径一条是系统总线通过总线矩阵连接到 Flash、SRAM、外设寄存器以及各种 DMA 控制器另一条是内核私有的总线路径直接连接到 CCM不经过总线矩阵。正因为不经过总线矩阵CPU 内核访问 CCM 时没有任何总线仲裁冲突。多核或者多个外设同时在访问 SRAM 时可能产生的等待周期在 CCM 上完全不存在。这是它速度快的主要原因。但同样的特性也决定了以太网 MAC 里的 DMA 控制器它作为系统总线上的一个主机它的地址空间可以访问的是系统总线连接的存储器区域。CCM 存在于内核私有总线上DMA 控制器在发起读或写时无法寻址到这块区域。这张简化的视图表达出来就是CCM 紧贴着 CPU但外设 DMA 只能通过系统总线访问普通 SRAM。两者物理上和逻辑上都不在同一个总线域内。访存发起方CCM0x10000000普通 SRAM0x20000000CPU 内核可访问速度最快可访问可能总线仲裁有等待以太网 MAC DMA不可访问可访问USB OTG DMA不可访问可访问SDIO DMA不可访问可访问DMA1/DMA2 控制器不可访问可访问看到这张表的瞬间就应该明白所有需要 DMA 搬运的数据像收发缓冲区、描述符列表、USB FIFO都必须无条件放在普通 SRAM 中。3.2 以太网 DMA 描述符与缓冲区为什么对内存位置极度敏感以太网控制器本身自带一个 DMA 引擎它负责两件事把内存里准备好的发送描述符和发送缓冲区数据搬给 MAC把 MAC 接收到的数据搬回内存中的接收描述符和接收缓冲区。整个过程不需要 CPU 逐字节参与。如果发送描述符放在 CCM 里DMA 引擎在读取描述符时就会拿到无效数据更严重的是它可能直接把描述符当作指向非法地址的指针导致发送操作直接失败。如果接收缓冲区放在 CCM 里DMA 引擎虽然可以正常接收帧但写完缓冲区时会因为不可访问而触发总线错误最终表现为接收中断永远不来或者接收描述符状态标志没有更新。所以不仅是缓冲区本身描述符表、包括 RTOS 里为网卡驱动分配的 MPU 配置相关的内存以及 LwIP 内存池全部不能落在 CCM 上。这个检查清单比“只看缓冲区”要全面得多请务必记录下来。4. 排查路径全记录从怀疑硬件到定位内存段整个排查过程相当曲折我按顺序写下来每个人踩坑的顺序可能不同但思路可以参考。有效的排查不是漫无目的地乱试而是要带着证据链往下走每一步都要有要么排除硬件、要么排除软件的依据。4.1 第一步硬件层排查清单先用示波器和万用表把硬件过一遍重点检查这些点RMII 接口的 REF_CLK 信号频率必须稳定在 50MHz时钟抖动不能太离谱。LAN8720A 可以使用外部 50MHz 振荡器也可以由主控输出。我这里主控输出时钟波形质量正常。PHY 的 nINT 中断引脚是否有意外脉冲有时 PHY 的寄存器值会因为供电不稳而偶发跳变。检查网络变压器的中心抽头电容是否短接对地以及 1:1 隔离变压器绕组的同名端方向是否接反。把 PHY 的复位引脚用示波器盯住确认上电后复位信号高电平稳定不出现毛刺。这一轮排查没有任何异常全是正常的。但说实话硬件排查不能省不排除硬件因素后面定位到软件问题时心里也不踏实。4.2 第二步软件寄存器级排查排除硬件以后进入软件排查。我习惯先做寄存器级的最小检测程序不借助协议栈而是直接操作 MAC 和 DMA 寄存器做以太网回环测试。具体分两步把 MAC 配置成内部回环模式这个模式下不需要外部 PHY 参与数据在 MAC 内部直接由发送通路送到接收通路。发送一个已知的测试帧然后等待接收描述符的状态更新。内部回环测试如果通过说明 MAC 和 DMA 引擎本身工作正常。这一步的结果是内部回环完全正常。我连续做了 100 次发送和接收全部成功。当时我就判断MAC 硬件没问题DMA 也没坏问题出在外部 PHY 通路或者内存布局上。后来证明判断大方向是对的但漏了一个关键变量内部回环模式走的是 MAC 内部数据路径,外部模式则需要 DMA 写入真实的接收缓冲区而这个缓冲区恰恰就在 CCM 里。4.3 第三步通过 map 文件锁定内存段用 KEIL MDK 开发的话map 文件里可以看到每一个 .o 文件生成的段被分配到哪个内存区域。如果用的是 STM32CubeIDE 或者 GCC对应查看.ld链接脚本和生成的.map文件即可。我当时例行扫描 map 文件用关键字eth过滤看到下面的内容.bss.eth_rx_buffer 0x10000800 0x2000 eth.o .bss.eth_tx_buffer 0x10002800 0x2000 eth.o .bss.eth_rx_desc 0x10004800 0x140 eth.o .bss.eth_tx_desc 0x10004940 0x140 eth.o地址以0x1000开头这正是 CCM 区域的基地址。这几个数组全部集中在片内 CCM 里和 EXTERNAL SRAM 无关。看到这里整条证据链闭合了所有和以太网 DMA 相关的内存都放进了 DMA 访问不到的 CCM 里内部回环能过是因为它不需要真实访问外部缓冲区的完整路径而外网通信必然失败。还要补充说明像 GCC 里如果链接脚本使用了类似. ALIGN(4); .ccmram : { ... } CCM这样的定义那么任何__attribute__((section(.ccmram)))的变量都会被放进 CCM。如果工程里某些驱动库的代码也声明了这种属性更要提高警惕。5. 修复操作与验证结果定位到问题后修复其实不复杂但细节比较多。我的做法是对以太网相关的所有 DMA 内存进行强制分区设计而不是简单地“把它挪回普通 RAM”就完事。5.1 修改链接脚本给普通 SRAM 预留 DMA 安全区以 GCC 的链接脚本为例在原来普通 RAM 区域内增加一个明确的 DMA 区域并且把所有以太网相关内存都放进这个区域。这样做的好处是以后新添加的 DMA 外设也可以用同一片区域管理起来很直观。MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 512K SRAM (rwx) : ORIGIN 0x20000000, LENGTH 128K CCM (rwx) : ORIGIN 0x10000000, LENGTH 64K }然后在 SECTIONS 里显式定义一个dma_mem段SECTIONS { .dma_mem (NOLOAD) : { . ALIGN(4); __dma_mem_start__ .; *(.dma_mem) __dma_mem_end__ .; } SRAM }在 C 源文件里为以太网缓冲区变量加上段属性__attribute__((section(.dma_mem), aligned(4))) uint8_t eth_rx_buffer[ETH_RX_BUF_SIZE]; __attribute__((section(.dma_mem), aligned(4))) uint8_t eth_tx_buffer[ETH_TX_BUF_SIZE]; __attribute__((section(.dma_mem), aligned(4))) ETH_DMADescTypeDef eth_rx_desc[ETH_RX_DESC_CNT]; __attribute__((section(.dma_mem), aligned(4))) ETH_DMADescTypeDef eth_tx_desc[ETH_TX_DESC_CNT];注意aligned(4)虽然字节数组本身对齐要求不高但 DMA 描述符有时要求 32 字节对齐所以加上 4 只是保底如果硬件手册要求更高的对齐条件需要按硬件手册设置。我在这里就曾因为对齐不足引发过另一个隐蔽问题一会儿会专门说。5.2 LwIP 内存池也要避开 CCM只改缓冲区不够。LwIP 协议栈的底层内存池memp_memory和ram_buffer如果被分配段属性影响了同样会出问题。检查一下你的 LwIP 移植文件里有没有这种声明#if !MEM_LIBC_MALLOC static u8_t mem_memory[MEM_SIZE] __attribute__((section(.dma_mem))); #endif如果没有默认 LwIP 会用普通内存不需要改。但如果你的工程和我一样为了优化内存布局把 LwIP 内存池指向了某个段那必须把它的段属性也改成.dma_mem区域。另一个容易忽略的点是如果使用 RTOS网卡驱动里申请 DMA 缓冲区时可能走的是pvPortMalloc并且指定了堆区。如果 RTOS 的堆恰好被放在了 CCM那你申请到的缓冲区依然是 CCM 内的不可用内存。最简单的排查方式是在驱动初始化完成后打印缓冲区地址检查地址是否落在0x20000000起始的范围内而不是0x10000000起始的范围内。5.3 修复后的验证方法修改完内存布局后重新编译烧录。验证不能只测试“能 ping 通”因为 ping 通只说明 ICMP 回显链路通了无法覆盖大流量、多帧并发的情况。我建议按下面的顺序做一轮压测第一步持续 ping 500 个包确保丢包率 0%。第二步用 iperf 或者网络调试助手向板子发送 10000 包 UDP 数据上位机同时统计板子回发的数据。第三步跑 TCP 长连接维持 30 分钟观察是否有断连和 CRC 错误计数。第四步在跑 TCP 的同时用示波器观察 ETH 的 TX_EN 引脚确认数据在持续发送而不是短时间内就停掉。我实际操作下来修复后一次性通过全部测试。连续跑了一天协议栈再也没有出现丢包和 HardFault。把同样的改动合并到另一个 USB 项目里也解决了类似的问题——原本 USB 枚举偶发失败就是因为我把 USB 的 FIFO 描述符内存放在了 CCM 区域。6. 排查中总结的避坑清单这段时间的折腾让我对内存分区有了新的认识。嵌入式开发里内存不仅要“够用”还要“位置正确”。下面这些坑是我实际踩过或者帮同事排错时遇到的值得记下来。6.1 最容易忽视的几个细节只改收发缓冲区、不改 DMA 描述符依然会出问题。很多人以为只要收发数据的大块缓冲区放在普通 SRAM 就行但 DMA 描述符同样会被 DMA 控制器访问它放在 CCM 一样会失败。对齐要求容易被忽略。即使内存区域选对了如果 DMA 描述符的地址没有按照硬件手册要求的字节对齐常见是 4 字节或 32 字节DMA 操作也可能会错乱。手动指定段属性时要显式aligned。中断服务函数里如果使用了 DMA 完成的标志位这个标志位本身可以放在 CCM但 DMA 缓冲区数据不能放在 CCM。这一点很多人的理解是有偏差的应该区分控制标志和数据存储。某些厂商的 HAL 库只在部分代码里使用了__attribute__((section(.ccmram)))如果你在启用这些库的同时又把以太网相关变量定义在了同一个段里就会出现“别人工程没问题、我的工程有问题”的假象。6.2 如何快速判断一个外设能否访问某块内存最保险的方法是查对应芯片的参考手册找到系统总线架构图。如果觉得看英文手册太累可以看启动文件里对于堆栈和内存的定义但最直观的方式是搞清楚外设 DMA 的地址映射范围。记住一句话所有挂在 AHB/APB 总线上的外设 DMA它访问的内存地址必须经过总线矩阵能访问的是系统总线地址空间不能访问挂在 CPU 私有总线上的 CCM。6.3 对 CCM 的正确定位搞明白了 CCM 的限制之后不要因噎废食从此不敢用它。合理的使用方式是优先把 CPU 密集型任务的数据放进去例如 DSP 算法库的系数表、编码器的角度采样序列、加密算法的 S 盒和扩展密钥。凡是需要 DMA 外设搬运的数据一律采用单独的 DMA 安全内存区域。我在后续版本里就用 CCM 优化了中断响应时间把音频解码的中间缓冲放进了 CCM效果立竿见影而网络部分完全不受影响。这才是 CCM 的正确打开方式。7. 遗留的补充与思考这个问题解决后我又回头想了想为什么一开始会那么轻易地把缓冲区放进 CCM。原因其实很简单当时觉得 CCM 访问速度更快内存零等待就顺手把协议栈的缓冲区放过去了完全没考虑 DMA 的访问路径问题。这种“性能优化”的本能反应在嵌入式开发里有时候是会害人的。排查这一类问题最忌讳的是反复修改 PHY 配置、MAC 寄存器之类的参数试图靠“调参”来撞运气。正确的做法是抓住一条明确的证据链从数据流向一步步反推。在本案例里关键证据就是 map 文件里以0x1000开头的地址。只要保持冷静把现象和数据统一起来往往能很快锁定真凶。最后再分享一个小技巧我习惯在工程里维护一个持久的内存区域分配表每一块内存的用途、对应的段名、能否被 DMA 访问都明确记录。每新增一个驱动先查这张表而不是随手往某个区域里塞数据。这样可以有效避免以后再犯同类错误。
返回列表