ARTICLE DETAIL

资讯详情

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

SoC低功耗唤醒失败排查:PLL锁定后系统无响应的根因与解决

SoC低功耗唤醒失败排查:PLL锁定后系统无响应的根因与解决 1. 唤醒失败的现场PLL锁定灯亮了系统却像死机一样如果你做过SoC低功耗相关的开发大概率遇到过这种让人抓狂的场景调试串口打印出PLL的LOCK位已经置1时钟树看起来一切正常但CPU就是卡在唤醒流程里出不来外设没有任何响应甚至连一个中断都进不去。你反复检查WFI指令前后的代码确认中断使能没问题电源域也恢复了可设备就是装死。这个现象在低功耗唤醒调试中非常典型尤其是涉及多电源域、多时钟源切换的SoC设计里。表面上看PLL锁定意味着主时钟已经稳定系统应该可以正常跑起来。但实际情况是PLL锁定只是唤醒链条上的一个环节它后面还挂着一长串依赖条件时钟切换是否真正完成、总线是否恢复、DMA是否已经静默、中断控制器是否重新使能、外设的时钟门控是否打开。任何一个环节没跟上系统就会卡在看起来正常但实际不动的状态。这篇文章面向的是做SoC底层驱动、低功耗固件、BSP开发的工程师也适合那些正在调试WFI唤醒异常、PLL切换后外设无响应的同行参考。我会从实际调试经验出发把PLL锁定与系统响应之间的断层拆开来讲重点覆盖时钟切换时序、DMA静默处理、中断恢复顺序这几个最容易出问题的环节并给出可复现的排查步骤和代码片段。需要先说明一点不同SoC的低功耗架构差异很大有的用硬件状态机管理唤醒序列有的靠固件手动恢复。我下面讲的内容基于常见的多电源域SoC设计实践具体寄存器名和流程需要你对照自己芯片的参考手册来调整。但排查思路和问题定位方法是通用的。2. PLL锁定不等于时钟就绪被忽略的切换完成标志2.1 LOCK位到底代表什么很多人对PLL LOCK的理解停留在锁定就是时钟稳定了。这个理解不算错但不够完整。PLL的LOCK位通常表示内部压控振荡器已经锁定到目标频率相位和频率误差在允许范围内。但它不表示以下事情已经完成时钟切换开关glitch-free mux已经切换到PLL输出分频器已经按照新频率稳定输出目标电源域的时钟树已经完成重新平衡依赖此时钟的外设已经退出复位状态我遇到过一块SoCPLL的LOCK位在唤醒后200微秒就置1了但时钟切换完成标志通常叫CLK_SW_DONE或类似名字要到800微秒才置位。如果固件在LOCK之后立刻去访问外设寄存器总线就会挂起因为时钟还没真正切过去。CPU取指失败看门狗可能直接复位或者更糟——系统进入一个未定义状态连复位都救不回来只能断电。2.2 时钟切换的硬件时序与软件等待策略典型的时钟切换流程是这样的唤醒事件触发后硬件先启动PLL等LOCK置位然后触发glitch-free mux切换切换完成后置位一个状态标志。这个标志有的芯片叫CLK_SW_STS有的叫SYSCLK_RDY名字不重要关键是你要找到它。下面是一段典型的等待时钟就绪的代码逻辑用伪代码表示/* 唤醒后等待PLL锁定 */ while (!(PLL_STATUS_REG PLL_LOCK_BIT)) { /* 可以加超时计数防止死等 */ if (timeout PLL_LOCK_TIMEOUT) { return ERR_PLL_LOCK_TIMEOUT; } } /* 关键等待时钟切换完成而不是直接往下跑 */ while (!(CLK_CTRL_REG CLK_SW_DONE_BIT)) { if (timeout CLK_SW_TIMEOUT) { return ERR_CLK_SWITCH_TIMEOUT; } } /* 此时才认为系统时钟真正可用 */这段代码里最容易犯的错误就是只等LOCK不等SW_DONE。有些芯片的参考手册会把这两个标志放在不同章节一个在PLL章节一个在时钟控制器章节很容易漏掉。注意有些SoC的时钟切换是硬件自动完成的固件不需要干预但你必须确认切换完成标志已经置位。不要假设LOCK了就等于切换好了。2.3 一个真实的排查案例之前调试一块带DMA的SoC时唤醒后串口没有任何输出。用调试器连上去看PC指针停在一个等待循环里循环条件是读取某个外设的状态寄存器。问题在于这个外设的时钟源在唤醒后没有正确切换读它的寄存器会返回全0或者挂起总线。而PLL的LOCK位确实是1所以固件开发者一直以为是外设本身的问题查了很久外设配置。后来我们用示波器抓了时钟输出引脚发现唤醒后主时钟在PLL LOCK之后还有一段大约500微秒的毛刺期频率不稳定。这段时间内如果访问外设行为不可预测。解决办法就是在LOCK和SW_DONE之间加足够的延时或者严格等待SW_DONE标志。加上之后串口输出立刻正常了。这个案例说明PLL LOCK是必要条件但不是充分条件。唤醒流程里必须把时钟切换完成作为独立的检查点。3. DMA没停干净唤醒后总线被悄悄占用的隐形杀手3.1 DMA在低功耗流程中的特殊地位DMA控制器是低功耗唤醒里最容易被忽视的模块之一。原因很简单DMA的工作是独立于CPU的它可以在CPU进入WFI之后继续搬运数据。很多低功耗设计里进入低功耗前会检查CPU和外设的状态但忘了检查DMA通道是否还有未完成的传输。如果DMA在系统进入低功耗时还有挂起的传输请求唤醒后DMA可能会立刻开始搬运数据占用总线带宽。这时候CPU虽然已经恢复运行但每次访问总线都要和DMA仲裁如果DMA优先级更高CPU就会一直等表现出来就是系统无响应。更隐蔽的情况是DMA的传输目标是一个已经掉电的外设。唤醒后外设还没恢复DMA却已经开始写数据导致总线错误。这种错误有的芯片会触发异常有的芯片直接挂起总线CPU连异常都进不去。3.2 进入低功耗前的DMA静默检查清单在執行WFI之前建议按下面的清单逐项确认检查项目的常见寄存器/方法DMA通道使能状态确认没有通道还在活动DMA_CH_EN寄存器DMA传输剩余计数确认没有未完成的传输DMA_CH_XFER_CNTDMA请求挂起标志确认没有排队的请求DMA_REQ_PENDDMA与低功耗握手确认DMA已进入空闲DMA_IDLE或类似状态位外设DMA请求使能关闭外设的DMA请求线外设DMA_CTRL寄存器我一般会在进入低功耗前加一段DMA静默等待/* 关闭所有外设的DMA请求 */ disable_all_peripheral_dma_requests(); /* 等待DMA控制器进入空闲状态 */ while (!(DMA_STATUS_REG DMA_IDLE_BIT)) { if (timeout DMA_IDLE_TIMEOUT) { /* 超时处理强制关闭DMA通道 */ force_disable_all_dma_channels(); break; } } /* 确认没有挂起的请求 */ if (DMA_REQ_PEND_REG ! 0) { clear_all_pending_dma_requests(); }这段代码的关键是先关请求再等空闲。如果先等空闲再关请求外设可能还在产生新的DMA请求导致永远等不到空闲。3.3 唤醒后的DMA恢复顺序唤醒后DMA的恢复也有讲究。不要一上来就重新使能DMA通道而是要按照下面的顺序先确认DMA控制器的时钟已经恢复清除DMA控制器里可能残留的错误标志重新配置DMA通道如果需要最后才使能外设的DMA请求如果顺序反了比如先使能了外设DMA请求但DMA控制器时钟还没稳定请求就会丢失或者触发总线错误。我见过一个案例唤醒后SPI DMA接收一直收不到数据查了半天发现是DMA控制器的时钟恢复比SPI外设慢SPI的DMA请求发出去了但DMA没响应数据就丢了。提示有些SoC的DMA控制器在低功耗期间会丢失配置唤醒后需要完整重新初始化。不要假设DMA寄存器在唤醒后还保持原值一定要读回来确认。4. 中断控制器的恢复时机为什么中断使能了却进不去4.1 中断恢复的常见误区唤醒流程里中断控制器的恢复往往被放在最后因为大家觉得系统跑起来了再开中断。但这里有个陷阱如果中断控制器在时钟恢复之前就被访问配置可能写不进去如果中断使能太晚唤醒事件本身的中断可能已经丢失。更麻烦的是有些SoC的中断控制器有独立的电源域唤醒后需要额外的恢复时间。如果你在它还没准备好的时候就去写使能寄存器写操作会被丢弃但读回来可能显示已使能造成一种配置成功的假象。4.2 唤醒事件中断的丢失问题WFI唤醒通常是由一个中断触发的。这个中断在唤醒流程中扮演双重角色它既是唤醒源也是需要被处理的事件。如果固件在唤醒后先清除了中断标志然后再使能中断控制器这个中断就永远不会被CPU响应了。正确的做法是/* 唤醒后先恢复中断控制器时钟和配置 */ restore_interrupt_controller_clock(); restore_interrupt_controller_config(); /* 确认中断控制器就绪 */ while (!(INT_CTRL_STATUS INT_CTRL_READY)) { /* 等待 */ } /* 然后再清除唤醒源的中断标志 */ clear_wakeup_source_flag(); /* 最后使能全局中断 */ enable_global_interrupt();注意这里清除唤醒源标志的位置。如果清得太早中断控制器还没恢复清除操作可能无效如果清得太晚中断可能已经触发了一次但被忽略。具体时机需要根据芯片的中断控制器行为来调整但原则是先让中断控制器就绪再处理中断标志。4.3 中断优先级与唤醒响应的关系还有一个容易被忽略的点唤醒后如果同时有多个中断挂起中断控制器的优先级仲裁需要时间。如果低优先级的中断先被响应高优先级的唤醒事件可能被延迟处理。在实时性要求高的场景里这会导致唤醒响应变慢。我一般会在唤醒后临时提升唤醒源中断的优先级等系统稳定后再恢复。有些中断控制器支持动态优先级调整有些需要在初始化时就配置好。如果你的SoC不支持动态调整那就需要在设计阶段就把唤醒源的中断优先级设到足够高。5. 从现象到根因一套可复现的唤醒问题排查链路5.1 第一步确认CPU到底停在哪里系统无响应时第一件事是确认CPU的PC指针停在哪里。如果有调试器直接连上去看PC和调用栈。如果没有调试器可以通过GPIO翻转或者串口打印来定位。我习惯在唤醒流程的关键节点加GPIO翻转gpio_set(DEBUG_PIN_1); /* 进入唤醒流程 */ wait_pll_lock(); gpio_set(DEBUG_PIN_2); /* PLL锁定 */ wait_clk_switch_done(); gpio_set(DEBUG_PIN_3); /* 时钟切换完成 */ restore_dma(); gpio_set(DEBUG_PIN_4); /* DMA恢复 */ restore_interrupt(); gpio_set(DEBUG_PIN_5); /* 中断恢复 */用逻辑分析仪或者示波器抓这几个引脚就能看出流程卡在哪一步。这个方法看起来笨但在没有调试器的现场非常有效。5.2 第二步区分是没跑还是跑了但没输出无响应有两种可能CPU根本没跑起来或者CPU跑了但外设没输出。区分方法很简单在唤醒流程里翻转一个GPIO如果GPIO有变化说明CPU在跑如果GPIO没变化说明CPU卡住了。如果CPU在跑但串口没输出问题就在串口外设或者它的时钟上。这时候要检查串口的时钟源是否恢复、波特率分频器是否重新配置、TX引脚是否被正确复用。5.3 第三步用最小系统法逐步排除如果一时找不到根因可以用最小系统法把唤醒流程精简到只做最必要的事情然后逐步加回功能看哪一步导致问题。最小唤醒流程通常包括等待PLL锁定等待时钟切换完成恢复CPU时钟跳转到主循环先确认这个最小流程能跑通然后依次加入DMA恢复、中断恢复、外设恢复每加一项测试一次。这样能快速定位到是哪个模块的恢复顺序有问题。5.4 第四步检查电源域的恢复顺序有些SoC有多个电源域唤醒时各电源域的恢复顺序是有要求的。比如IO电源域必须先于核心电源域恢复否则IO引脚可能处于不确定状态导致外设误触发。这个信息通常在芯片的电源管理章节里但很容易被忽略。如果你发现唤醒后某些外设行为异常但时钟和配置都没问题就要怀疑电源域恢复顺序。6. 几个容易踩的坑和实测有效的应对技巧6.1 超时机制不是可选项等待PLL锁定和时钟切换的循环一定要加超时。我见过因为PLL一直不锁定导致系统死等的案例现场设备只能断电重启。加上超时后即使PLL出问题系统也能走异常处理流程至少能打印错误信息或者触发安全复位。超时值怎么定我的经验是取正常唤醒时间的3到5倍。比如正常唤醒需要1毫秒超时就设5毫秒。太短了容易误触发太长了失去保护意义。6.2 唤醒后的第一次外设访问要格外小心唤醒后第一次访问外设寄存器时建议先读一个已知的只读寄存器比如ID寄存器确认总线能正常返回数据然后再进行写操作。如果读ID都失败说明总线或时钟还有问题这时候写操作可能会产生不可预期的后果。/* 唤醒后先读外设ID确认总线可用 */ uint32_t id read_peripheral_id(); if (id ! EXPECTED_ID) { /* 总线或时钟异常走错误处理 */ handle_wakeup_error(); return; } /* 确认无误后再配置外设 */ configure_peripheral();6.3 DMA的静默不等于关闭有些开发者为了省事进入低功耗前直接关闭DMA控制器时钟。这样做的问题是DMA控制器内部可能还有未完成的状态突然断时钟会导致状态机卡死唤醒后即使重新开时钟也无法恢复。正确的做法是先让DMA进入空闲再关闭时钟。如果芯片支持DMA低功耗握手用握手信号让DMA自己进入低功耗状态而不是强制断时钟。6.4 中断标志的清除时机要精确前面提到过中断标志清除的时机问题。这里再强调一点有些中断标志是写1清除有些是读清除有些需要先读状态寄存器再写清除寄存器。如果清除方式不对标志可能一直挂着导致中断反复触发或者永远不触发。我一般会在唤醒流程里加一段中断标志清理的代码把所有可能挂起的中断标志都清一遍然后再使能中断。这样能避免唤醒后立刻被一堆历史中断淹没。6.5 用WFI前后的内存屏障保证顺序WFI指令前后的代码顺序有时候会被编译器或者CPU乱序执行打乱。在关键操作前后加内存屏障DMB/DSB可以保证顺序。比如在等待PLL锁定之前加DSB确保之前的配置写入已经完成在WFI之后加ISB确保指令流正确。/* 进入低功耗前 */ dsb(); __wfi(); isb(); /* 唤醒后 */ dsb(); /* 继续执行唤醒流程 */这些屏障指令在ARM架构里很常见其他架构也有类似的指令。别小看它们在低功耗场景里顺序错乱导致的问题非常难查。7. 把唤醒流程当成一个状态机来设计调试多了之后我越来越倾向于把低功耗唤醒流程设计成一个显式的状态机而不是一堆顺序代码。状态机的每个状态对应一个恢复步骤状态转移条件明确超时和错误处理也容易加。比如状态动作转移条件超时处理WAIT_PLL等待PLL锁定LOCK置位报错并复位WAIT_CLK_SW等待时钟切换SW_DONE置位报错并复位RESTORE_DMA恢复DMADMA_IDLE置位强制关闭DMARESTORE_INT恢复中断INT_READY置位报错并复位RUNNING正常运行--这样设计的好处是每次唤醒失败都能知道卡在哪个状态调试信息一目了然。而且状态机容易扩展后面加新的恢复步骤只需要加一个状态。状态机的代码可以用switch-case实现也可以用状态表驱动。关键是每个状态的进入和退出条件要明确不要有隐含的依赖。8. 一些关于时钟和电源的底层经验PLL锁定时间跟温度、电压、工艺都有关系。低温下PLL锁定可能变慢低压下可能锁不定。如果你的产品要在宽温范围内工作唤醒流程里的超时值要留足余量。我一般会在高温和低温下各测一遍唤醒时间取最坏情况来定超时。时钟切换的glitch-free mux在切换瞬间可能会有短暂的时钟停顿这个停顿时间通常在几个周期到几十个周期之间。如果CPU在这段时间取指可能会取到无效指令。所以等待SW_DONE是必须的不能靠延时来猜。电源域的恢复时间跟负载电容有关。如果某个电源域上挂了大的去耦电容恢复时间会变长。这种情况下等待电源就绪标志比固定延时更可靠。还有一点唤醒后不要立刻把时钟调到最高频率。先让系统在较低频率下跑一段等电源稳定后再升频。突然升频可能导致电源跌落触发欠压复位。这个技巧在电池供电的设备里特别有用。9. 写在最后唤醒调试的耐心与记录低功耗唤醒调试是个细致活很多时候问题不在某一个点上而是多个因素的组合。我的习惯是每次调试都记录唤醒时间、各阶段耗时、异常现象、修改内容。积累多了之后再遇到类似问题就能快速定位。另外芯片的勘误表errata一定要看。有些唤醒相关的问题芯片厂商已经知道并给出了规避方法不看勘误表可能会在已知问题上浪费大量时间。我就吃过这个亏一个PLL切换的时序问题在勘误表里写得清清楚楚我却花了两天自己查。最后分享一个实用技巧如果条件允许在唤醒流程里加一个唤醒日志缓冲区把每个阶段的完成时间和状态记录下来。系统跑起来后可以通过串口或者调试接口读出来。这个日志在排查偶发唤醒失败时特别有用因为偶发问题很难复现有了日志就能看到失败那次到底卡在哪。
返回列表