ARTICLE DETAIL

资讯详情

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

LPBAM链表如何放对SRAM4:STM32低功耗DMA内存布局实战

LPBAM链表如何放对SRAM4:STM32低功耗DMA内存布局实战 1. 先搞清楚为什么 LPBAM 对 SRAM 有“额外要求”做 LPBAM 移植的第一件事往往不是盯在代码上而是先理解它和普通 DMA 依赖链的本质区别。LPBAM 的核心执行者是 LPDMA而 LPDMA 执行任务的方式是“读取链表节点”。CPU 提前把任务描述成一串链表节点源地址、目的地址、数据长度、数据宽度、突发长度、下一个节点指针全部写在一段连续内存里。然后 CPU 进入低功耗模式LPDMA 不再需要 CPU 参与自己顺着链表一遍一遍读取并执行。这里就出现一个硬约束这些链表节点所在的内存在 CPU 休眠时必须依旧保持可读、可写而且必须让 LPDMA 无时钟依赖地访问。如果链表恰好落在 CPU 一睡觉就断电或者进入低功耗保持状态的 SRAM 里那么 LPDMA 第一次读链表节点就可能读到无效数据或者总线访问直接失败最终表现为低功耗模式唤醒后数据错乱、传输中断甚至直接触发 hardfault。这不是代码逻辑能解决的本质是内存放置问题。我最早调试时也绕了弯路只盯着 DMA 的初始化函数反复确认参数无误但一进入 STOP 模式就乱。后来翻 ST 的 AN5606LPBAM 应用笔记才反应过来链表所在内存才是问题根源。官方指导非常明确LPBAM 链表、上下文缓冲区和辅助数据应当放到 LPDMA 在低功耗模式下仍然能访问的 SRAM4 中并且这块 SRAM 在 STOP 模式下被设计为保持供电与数据。为了后续操作不盲目先把几个名词理清楚LPBAM 是 Low-Power Background Autonomous Mode 的缩写LPDMA 是带链表功能、可在深度低功耗下自主运行的低功耗 DMA链表节点则是由软件构造、由 LPDMA 硬件解析的描述符。整个体系的目标只有一个——低功耗模式下外设仍然能自主跑数据搬运和信号处理CPU 完全休息。1.1 LPBAM 是什么链表为什么必须常驻LPBAM 与普通 DMA 最大的区别在于低功耗场景。普通的 DMA 虽然也能在 CPU 不干预的情况下传输数据但传输链路的建立、中断处理、下一次传输的描述符更新通常还是需要 CPU 参与。LPBAM 就不一样它把整个传输序列预先组织成链表LPDMA 一个节点一个节点执行并且可以在执行完所有节点后自己循环回到队首或者产生事件唤醒 CPU。这样做的价值在于CPU 可以真正进入 STOP 模式整个核心域时钟停止系统功耗降到微安级别但外设仍然在该工作的继续工作。比如 ADC 定时采样、传感器数据搬运、比较器事件触发传输、数字滤波后台计算这些都可以由 LPBAM 在低功耗下自主完成。但前提是 LPDMA 读得到链表。CPU 在 STOP 模式下软件不可能再去给 DMA 喂描述符DMA 完全依赖内存里的链表。链表一旦丢失整个任务链就断了。因此链表必须放在一个在低功耗模式下“身份不变”的内存区域不仅要数据保持还要总线访问正常。普通 SRAM 如果在低功耗模式下进入保持状态虽然数据不丢但访问时序和供电可能已经不支持外设读取或者根本被切断了时钟这时的 DMA 就会像一个睡醒后找不到备忘录的人一样乱走或者停在原地。这也是为什么很多初学者在 STM32CubeMX 里配置好了 LPBAM生成代码也完全正确可实际跑起来却连第一个节点都执行不了。根本原因不是代码生成错了而是内存放置没做对。链表关系的是 DMA 的“执行依据”永远不能放在有掉电风险或者低功耗不可访问的区域。1.2 为什么是 SRAM4而不是 SRAM1/2/3以 STM32U5 系列为例芯片内部有多块 SRAM地址布局大致如下不同子型号有差异一定以具体参考手册以及头文件中的内存图为准SRAM 块常见起始地址常见容量低功耗模式下的特点SRAM10x20000000256 KB可配置进入低功耗状态数据保持但访问受限SRAM20x2004000064 KB同上SRAM30x2005000064 KB同上SRAM40x200B000016 KB多种 STOP 模式下保持供电且保持外设总线访问能力SRAM1/2/3 并不是说进入低功耗就完全断电而是可以被配置为停止状态停止之后即使数据不丢LPDMA 也没有办法保证实时访问。而且这几块 RAM 主要用于 CPU 运行时的程序数据启动文件里的栈、堆、全局变量基本都默认丢在 SRAM1 及其延续区域。如果 LPBAM 链表也随手放在这里大概率会因为栈的覆盖、堆的动态分配或者链接脚本的内存布局变化被冲得面目全非。SRAM4 之所以特殊是因为它在芯片设计上被定位为低功耗模式下仍然可访问的外设 RAM 区域。它由低功耗域持续供电并且挂在 LPDMA 能够直接访问的总线上在进入 STOP 模式后依然可以作为 DMA 描述符的驻地。ST 的官方 LPBAM 例程里几乎都能看到把队列上下文、链表数组和某些关键缓冲区放在 SRAM4 的做法。明白了这一点后面所有操作就都有明确目标了链接脚本里为 SRAM4 定义独立内存区域然后通过编译器属性把 LPBAM 链表变量精确地放进这个区域。下面就从链接脚本开始一步步落地。2. 链接脚本改造让变量安全落入 SRAM4很多人一听到“改链接脚本”就觉得复杂其实对 LPBAM 场景来说改动非常小核心就三件事确认 SRAM4 地址和大小在 MEMORY 里增加一块 RAM4新增一个输出段并把它映射到 RAM4。下面以 STM32CubeIDE 默认的 GCC 工具链为例。2.1 先确认目标 SRAM4 的基地址和大小动手改之前先打开 STM32U5 系列参考手册的内存映射章节别直接抄网上的地址。以我常用的 STM32U575/585 为例SRAM4 常见基地址是 0x200B0000大小 16 KB十六进制 0x4000。但不同封装和型号可能调整所以一定要以你手上的芯片数据手册为准。还有一个更简单的校验办法打开设备的头文件比如stm32u5xx.h搜索SRAM4_BASE看到的值就是当前工程的准确基地址。确认完地址后打开现有链接脚本。STM32CubeIDE 生成的 .ld 文件结构一般长这样MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 2048K RAM (xrw) : ORIGIN 0x20000000, LENGTH 256K RAM2 (xrw) : ORIGIN 0x20040000, LENGTH 64K RAM3 (xrw) : ORIGIN 0x20050000, LENGTH 64K }注意这里默认可能没有 RAM4 的定义。如果工程是通过 CubeMX 生成的内存布局通常会预留 SRAM1/2/3但 SRAM4 不一定自动加入。我们的目标就是在 MEMORY 块里补上一行 RAM4。2.2 在 .ld 文件中新增 RAM4 区域和输出段在 MEMORY 块末尾添加RAM4 (xrw) : ORIGIN 0x200B0000, LENGTH 16K然后需要在 SECTIONS 块里新增输出段。这里有一个很重要的细节LPBAM 链表变量在启动时并不需要被 C 运行库清零甚至某些场景下还希望 SRAM4 里的数据在上电后保留上一次的值所以输出段建议标记为NOLOAD。NOLOAD的意思是链接器不生成对该段内存进行初始化的加载区域运行时也不会有人去自动清零它。在 SECTIONS 的合适位置可以这样加.sram4 (NOLOAD) : { . ALIGN(4); KEEP(*(.sram4)) KEEP(*(.sram4.*)) . ALIGN(4); _esram4 .; } RAM4这里解释几行关键语句的作用. ALIGN(4);强制四字节对齐保证后面的链表节点起始地址满足 LPDMA 的对齐要求。KEEP(*(.sram4))和KEEP(*(.sram4.*))把所有使用.sram4或.sram4.xxx段属性的变量都吸收入这个输出段。_esram4 .;记录 SRAM4 段的结束地址方便程序里计算剩余空间。 RAM4指定该段整体放入 RAM4 内存区域。改完后保存编译链接如果有undefined symbol之类错误多半是语法问题检查括号和分号即可。如果链接时报region RAM4 overflowed说明往 SRAM4 里放的变量超过了 16 KB需要精简链表节点数量或者把部分缓冲区挪回 SRAM1。2.3 ARMCC/IAR 等工具链的替代做法工程不一定是 GCC。Keil MDK 的 ARM Compiler 6 也支持通过__attribute__((section(.sram4)))放置变量但链接脚本写法不同。在 Keil 里你可以在分散加载描述文件.sct中定义LR_IROM1 0x08000000 0x00200000 { ER_IROM1 0x08000000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00040000 { .ANY (RW ZI) } RW_SRAM4 0x200B0000 0x00004000 { *(.sram4) *(.sram4.*) } }IAR 下则有几种方式最简单的是在变量声明处使用定位#pragma location 0x200B0000 LPDMA_LinkNodeTypeDef lp_link_node[8];也可以使用段名方式在 IAR 的链接配置文件中额外定义place in RAM4规则使代码更灵活。无论哪种工具链本质都是把.sram4段映射到 SRAM4 物理地址上。代码本身并不会有太大差别。2.4 启动初始化时的注意事项这里必须多说一句NOLOAD段不会像.bss那样在启动时自动清零。这意味着你的 LPBAM 链表变量在芯片上电第一次使用之前内容是完全随机的。无论你声明的是局部数组还是全局数组都必须在使用前手动初始化。对于 LPBAM 队列如果启动后立刻就要进入低功耗模式那么初始化链表是必须的比如清空所有链接节点、把队列的头指针设置好、给每个节点的下一个节点指针赋好值。不要指望编译器帮你处理。反过来这也能成为一个小优势如果 SRAM4 保留能力足够在某些低功耗场景下你可以保留甚至复用上次运行时的链表状态减少恢复时间。但这属于进阶用法新手阶段建议老老实实初始化。3. 写代码把 LPBAM 链表变量指定到 SRAM4链接脚本只是铺好了路真正要让变量“住进” SRAM4还需要在 C 代码里给变量加上段属性。下面用一个典型例子说明完整流程。3.1 定义带 section 属性的链表变量在 STM32CubeU5 固件包里LPDMA 相关的类型定义在stm32u5xx_ll_lpdma.h中链表节点类型是LPDMA_LinkNodeTypeDef。我的习惯是定义一个专用数组作为 LPBAM 的链表池比如#define LPBAM_MAX_LINK_NODE_NB 8U __attribute__((section(.sram4), aligned(4))) static LPDMA_LinkNodeTypeDef lp_link_node_array[LPBAM_MAX_LINK_NODE_NB];这一行的作用很直接数组被放到.sram4段并保证四字节对齐。链表节点这种数据结构内部往往包含 DMA 的配置寄存器字段如果对齐不对LPDMA 读取时可能出现总线错误或者性能下降。虽然 LL 库内部也可能对结构体做了对齐处理但自己显式声明aligned(4)能减少不同编译器之间的差异。这里还有个小设计建议链表池也最好单独规划一个 SRAM4 区域不要和需要保存的采集数据混在一起。把链表数组、队列控制块、状态标志分别定义成不同数组每个数组都标注.sram4这样排查问题时能通过调试器的 Memory 窗口直接检查每个区域的内容。3.2 确认链表节点大小与对齐如果你想知道一个链表节点占多少字节可以这么看uint32_t node_size sizeof(LPDMA_LinkNodeTypeDef); printf(link node size: %d\n, (int)node_size);不同固件版本、不同芯片型号这个结构体的大小可能有变化。早期版本可能只有 16 字节后来的版本可能会包含更多控制字段膨胀到 32 字节甚至更多。知道节点大小后就能估算 SRAM4 16 KB 到底能放多少个节点。如果 8 个节点不够改成 16 个也没问题只要总大小别超过 16 KB。关于对齐再提一句实际经验LPDMA 描述符本身的硬件要求通常是 4 字节对齐但如果你把链表放在 32 字节对齐的地址上对后续某些需要 cache 维护的 MCU 更友好。虽然 STM32U5 没有复杂的数据 cache 机制但养成写aligned(32)的习惯也没有坏处最多浪费一点空间。链表数组本身不大浪费几乎可以忽略不计。3.3 初始化方式在唤醒后构建还是启动时构建LPBAM 构建链表有两种常见时机。第一种是主程序启动后在进入低功耗之前一次性构建好链表然后反复进入低功耗LPBAM 每次都按同一条链表工作。这种适合周期性采样、固定 DMA 搬运的场景。第二种是每次唤醒后重新构建链表。因为 LPBAM 链表的内容可能依赖上一次唤醒后的结果比如下一次要搬的缓冲区变了或者采样次数变了。这时候如果链表里记录的还是旧地址LPDMA 就会把数据搬错地方。ST 官方也有类似 “RB_AddLinkNodeToQueue / RB_RemoveLinkNodeFromQueue” 之类的接口来动态维护队列本质就是为了方便每次执行前调整链表。从内存角度看不管哪种时机链表变量本身的物理位置必须在 SRAM4而变量的内容可以在运行时更新。比如uint32_t src_addr (uint32_t)adc_buffer; uint32_t dst_addr (uint32_t)result_buffer; LPDMA_SetSrcAddress(lp_link_node_array[0], src_addr); LPDMA_SetDstAddress(lp_link_node_array[0], dst_addr); LPDMA_SetBlkLen(lp_link_node_array[0], transfer_length); LPDMA_SetSrcDataWidth(lp_link_node_array[0], LL_LPDMA_SRC_DATAWIDTH_WORD); LPDMA_SetDstDataWidth(lp_link_node_array[0], LL_LPDMA_DST_DATAWIDTH_WORD);以上这些 LL 函数最终都是直接操作链表节点结构体里的字段并不会改变变量本身的存储位置。地址写对了链表在 SRAM4 里怎么改都安全。3.4 队列和链表的搭配使用LPBAM 不只支持单个链表节点。实际场景中你要搬运的数据往往来自多个外设或者多个缓冲区这时就要用队列。最简单的队列操作方式是使用 ST 封装好的 LPDMA 队列接口LPDMA_QueueTypeDef lp_queue; LPDMA_InitQueue(lp_queue); LPDMA_AddLinkNodeToQueue(lp_queue, lp_link_node_array[0]); LPDMA_AddLinkNodeToQueue(lp_queue, lp_link_node_array[1]);这里同样要保证lp_link_node_array在 SRAM4 中。队列本身并不需要额外的硬件内存它只是软件层面的管理容器但队列上下文结构体如果不想放在栈上也可以加到.sram4段里__attribute__((section(.sram4), aligned(4))) static LPDMA_QueueTypeDef lp_queue;把所有和 LPBAM 运行相关的状态数据统一放到 SRAM4至少有一个明显好处调试时打开 Memory 窗口输入 SRAM4 的基地址就能把链表、队列、缓冲区的现状全套看一遍一目了然。4. 调试与踩坑变量不见、调试器断开、DMA 乱跳LPBAM 调试和普通程序调试完全两个体验。因为 CPU 会进入 STOP 模式调试器经常在关键时刻“掉线”新手很容易一脸懵。下面把最常见的几类问题集中列一下并给出排查思路。4.1 调试器报“没有可用的调试程序”在 STM32CubeIDE、Keil 或 IAR 中当目标芯片进入 STOP 模式后内核时钟停止调试接口也随之中断。这时如果点暂停按钮调试器可能会弹“没有可用的调试程序”或者类似的连接错误因为它已经拿不到目标 CPU 的状态。这不是程序跑飞也不是烧录失败而是芯片确实在一个深度低功耗状态。遇到这个情况绝大多数时候不需要慌张。先按一下核心板上的复位键或者通过调试器执行“复位并重新连接”把芯片从低功耗状态下拉回来。需要注意复位操作会清掉大部分运行状态SRAM4 里的数据能不能保留取决于复位类型和芯片的电源配置。如果想既不复位又能把 CPU 唤醒可以在代码里加一个定时唤醒或者通过外部中断唤醒然后再让调试器连接。4.2 调试界面提示“无法发送 Variables”还有一种很典型的场景程序确实停在某个断点但调试器 Variables 窗口显示“无法发送 variables”或读取超时。这是因为变量本身位于一个当前无法访问的内存区域。如果变量的地址落在 SRAM1/2/3 中而这几块 SRAM 在进入低功耗模式时被配置为低功耗状态调试器访问时自然失败。而你把它放在 SRAM4 之后无论芯片在 RUN 模式还是 STOP 模式调试器都能正常读取问题直接消失。所以遇到“无法访问变量”的第一反应应该是查看变量的地址落在哪块内存用调试器的 Memory 窗口输入变量地址确认。如果地址果然不在 SRAM4 范围那就说明链接脚本里的段映射没生效或者代码里变量并没有声明到正确的段属性。检查方式很简单在编译生成的 .map 文件里搜索变量名看它被分配到了哪个区域。如果发现落在了起始地址 0x20000000 附近段属性或者链接脚本多半没生效。4.3 现象LPBAM 运行后 DMA 传输乱跳这个问题的排查难度要高一些。如果链表节点没有正确设置“下一个节点指针”LPDMA 在完成第一个节点后可能随机跳到某个地址继续执行。轻则漏掉一次传输重则直接总线错误。典型原因包括链表节点数组没有清零残留的旧数据被 DMA 当成下一个节点指针。节点初始化时没有使用 LL 库提供的方法而是手动修改了结构体字段但某些保留字节偏移搞错了。链表队列构建顺序不对头尾节点没有接回来导致循环队列在最后一个节点处断开。解决办法也很明确确保每次进入低功耗前链表池都被正确初始化。如果使用动态方式构建队列检查LPDMA_InitQueue是否成功检查每个节点是否加入了队列最后一个节点的下一个节点指针是否指向期望的节点。静态链表可以这样兜底memset(lp_link_node_array, 0, sizeof(lp_link_node_array)); for (uint32_t i 0; i LPBAM_MAX_LINK_NODE_NB - 1; i) { lp_link_node_array[i].next lp_link_node_array[i 1]; } lp_link_node_array[LPBAM_MAX_LINK_NODE_NB - 1].next lp_link_node_array[0];这里把链表设计成循环结构所有节点的 next 指针都明确赋值即使某次配置漏了末尾节点至少不会跳到完全未知的地址上。4.4 现象编译时 RAM4 溢出这个错误最容易理解就是 SRAM4 里放的变量总量超过了 16 KB。但有一种隐蔽情况链接脚本里定义的 RAM4 区域只有 16 KB而实际芯片的 SRAM4 可能更大或者反之你定义的地址已经越过芯片实际 SRAM4 的尾部。排查时打开 .map 文件确认.sram4段的 VMA虚拟地址是否位于 SRAM4 地址范围内同时确认 LMA加载地址没有产生意外的数据拷贝。另一个常见坑是有些启动文件会把部分数据初始化的加载段也塞进 SRAM4导致 RAM4 被加载数据挤爆。如果遇到这种情况检查.sram4输出段是否误加了LOADADDR或者是否包含已经初始化过的只读数据。LPBAM 链表属于运行时结构用NOLOAD是最稳的。下面用表格总结一下排查方向现象常见原因排查动作调试器掉线CPU 处于 STOP 模式使用复位后重新连接或添加唤醒源Variables 无法读取变量位于低功耗不可访问区域用 Memory 窗口检查变量地址DMA 乱跳next 指针未初始化或残留脏数据检查链表池清零和 next 指针赋值编译 RAM4 溢出SRAM4 段塞入过多变量打开 .map 文件统计 .sram4 段占用低功耗唤醒后数据错链表缓冲区不在 SRAM4确认变量地址和链接脚本映射4.5 调试器的低功耗透传设置如果需要保留调试能力同时又要验证 LPBAM 执行流程可以临时打开调试组件对低功耗模式的穿透功能。STM32U5 的 DBGMCU 控制寄存器里有关闭低功耗模式外部调试的位典型接口是LL_DBGMCU_EnableDBGStopMode();这会让 ST-Link 在 STOP 模式下仍然能访问内核。代价是功耗会显著上升低功耗效果被破坏所以只适合调试阶段临时用功能验证没问题后一定要关掉。我也见过有人忘记关这个配置然后报告“为什么我的待机电流有几十毫安”查了半天最后发现是调试位没关。5. 一点个人经验总结写到这里核心操作基本讲完了链接脚本加 RAM4 区域定义.sram4段代码里给 LPBAM 链表变量加段属性最后在调试时学会看.map文件确认地址。步骤本身不多但每一步都关系到 LPBAM 能不能在真实低功耗模式下跑稳。我个人在实际操作中的最大感受是LPBAM 出问题第一反应永远先查内存放置而不是查传输配置。很多看起来像是“DMA 初始化错误”的现象本质都是链表地址不对。项目里如果出现只在睡醒后才出错的诡异 bug建议第一时间把相关变量的地址列出来。打开调试器的表达式窗口输入lp_link_node_array[0]看一眼地址如果落在 0x200B0000 开头的区域再往深处查链条如果地址在 0x20000000 附近先把链接脚本修好再说避免在错误的方向上浪费时间。另外一个值得养成的习惯是维护一份自己项目的链接脚本内存布局表。芯片有哪几块 RAM每块分别放什么栈放哪堆放哪LPBAM 链表放哪都要写清楚。等板子调试到后期各种地址冲突、RAM 溢出问题出现时这张表能帮你快速定位问题。实际项目里我见过太多次“数据被莫名覆盖”的案例最后发现都是链接脚本里不同段重叠了。把 SRAM4 单独规划出来后至少 LPBAM 相关的覆盖风险彻底隔离了。最后再分享一个小细节SRAM4 空间有限别把大块的 DMA 数据缓冲区也一股脑放进去。链表和控制结构放 SRAM4数据缓冲区放在 SRAM1/2/3 中而且确保这些缓冲区在进入低功耗前有明确的保留配置即可。这样才能在小空间里装下足够的链表描述符同时让低功耗模式的功耗表现达到预期。
返回列表