ARTICLE DETAIL

资讯详情

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

STM32U5 DMA卡在busy?中断不触发与回调不执行的排查指南

STM32U5 DMA卡在busy?中断不触发与回调不执行的排查指南 做嵌入式开发的朋友估计对DMA是又爱又恨。爱是因为它能解放CPU重负载场景下配合中断和回调可以让外设数据吞吐完全不需要CPU操心恨的是出了问题你往往抓不住它到底卡在哪里。最近在STM32U5上调试一个ADC多通道扫描循环采样DMA的方案就遇到了一模一样的现象DMA初始化一切正常可一启动传输状态就变成了busy中断不触发回调也不执行。翻遍寄存器、查遍CubeMX最后才发现问题出在一个特别容易忽略的细节上。这篇文章就基于这个案例聊一聊“U5 DMA not setting interrupt or executing callback, stays busy”这类问题为什么会卡在busy传输完成标志为什么没有置位、中断链路断在哪里以及应该按什么顺序排查。如果你现在正被同样的问题折磨或者刚用U5准备做ADC、串口、SPI的DMA传输这篇文章应该能帮你少走不少弯路。1. 问题现象与初步判断1.1 打开工程时的第一眼一切正常但就是不工作我遇到的情况很典型。使用STM32CubeMX生成工程ADC配置了多通道扫描DMA配成循环模式外设到内存缓冲区和数据宽度都检查过初始化代码也调用了。执行HAL_ADC_Start_DMA()之后返回值是HAL_OK看起来一切正常。但程序就是没有反应没有ADC转换数据更新DMA中断没有触发HAL_ADC_ConvCpltCallback也没有被调用。更奇怪的是如果直接读DMA状态通道的BSY位始终是1也就是它认为自己一直在忙。状态寄存器里如果有TCIF标志读出来又像是没有发生过任何传输完成事件。HAL_DMA_GetState()返回的也是HAL_DMA_STATE_BUSY整个DMA通道就像被锁死了一样。这类问题之所以折磨人是因为它既不是崩溃也不是编译错误它只是“不干事”。如果你的程序里还有别的外设中断在跑甚至系统表面上是活的只是DMA数据不更新那排查起来就更考验耐心。1.2 遇到问题先别急着怀疑芯片理清排查方向很多朋友遇到DMA卡死会第一时间怀疑芯片坏了或者怀疑HAL库有bug。实际上绝大多数“stays busy”案例都是配置层面的问题而且往往不是一处错误而是“配置组合”出来的怪现象。我一般会按下面这几个方向去理思路DMA通道本身是否处于可启动状态是不是上一次传输没结束DMA的中断标志、中断使能位以及NVIC中断通道是否真正配置正确外设发出的DMA请求信号是否已经产生有没有选对DMA请求映射中断服务例程里是否调用了HAL库的HAL_DMA_IRQHandler回调函数是否在用户文件里被正确定义Cache、MPU、总线矩阵等U5新特性是否对DMA产生了额外影响。把这几个方向一步步查完绝大多数问题都能浮出水面。下面我先把U5的DMA工作机制拆开讲清楚你理解了状态机和中断链路之后排查起来就会有方向感而不是靠瞎试。2. DMA工作原理解析读懂busy标志与中断回调的关系2.1 U5的DMA通道与请求映射机制STM32U5系列用的DMA和老的F1、F4系列不太一样它有着更灵活的请求映射方式通常是通过DMAMUX或者通道控制寄存器来把外设请求连接到对应的DMA通道。一个DMA通道可以服务于多个外设请求关键在于你怎么配这个“请求选择”。比如ADC的转换完成请求可以被连接到DMA1的Channel0也可以被连接到另一个通道具体取决于寄存器里的请求选择位。很多人只记得配置DMA通道的方向、地址、数据宽度却忘了配置“这个通道到底听谁的指挥”。如果你在外设请求源那里选错了或者干脆没选那么即使DMA通道被使能它也不会开始搬运数据。外设产生请求时DMA压根不知道自己需要响应最终表现就是NDTR不变、BSY不变、中断不触发。所以排查这类问题时我第一件事就是确认“DMA请求映射”。在STM32CubeMX里面你可以在DMA设置里看到“Request”下拉选项这里必须选对实际使用的外设。比如ADC1就是ADC1不能选成TIM1或者别的什么。U5的寄存器手册里会有详细的映射表实在不确定就翻手册看一下。2.2 busy标志到底什么时候会被清除DMA通道控制寄存器里有一个BSY位表示当前通道是否正在传输。按照STM32的DMA设计当一次传输完成、半传输或者错误事件发生时BSY位并不会自动清除而是需要你先处理对应的事件标志也就是要写IFCR寄存器把TCIF、HTIF、TEIF等标志清除。只有清除了这些标志DMA控制器才会认为这个通道“空闲”从而允许下一条传输启动。这就是“stays busy”最核心的机制之一。如果DMA传输已经发生并且完成但是中断没有执行、标志没有清除DMA就会一直认为通道正在忙。后续你再调用HAL_DMA_Start_IT()HAL库发现通道状态是BUSY直接返回HAL_BUSY连启动都启动不了。哪怕你用寄存器操作绕过HAL层强行启动BSY位因为旧标志没清也会导致行为异常。还有一点要注意U5的中断标志和早期M3内核的DMA不太一样有些系列把标志分成了多个通道独立的中断标志寄存器读写方式也有差异。我建议拿到新芯片后先仔细看参考手册里对DMA_ISR和DMA_IFCR的位定义不要直接照搬F1/F4的老代码。2.3 从外设请求到回调函数完整中断链路是什么要理解“中断不触发、回调不执行”得先把整条链路看清楚。一次正常的DMA中断流程如下外设产生DMA请求比如ADC规则组转换完成、USART接收寄存器非空、SPI收发准备好DMA控制器收到请求后按照配置好的通道参数进行搬运并把数据写到目的地址当传输长度计数NDTR减到0或者发生半传输/传输错误时DMA控制器会置位对应的中断标志比如传输完成标志TCIF如果DMA通道控制寄存器的TCIE、HTIE、TEIE相应中断使能位为1DMA就会向NVIC发出中断请求NVIC需要对应DMA通道的中断被使能且优先级分组配置正确CPU才会跳转到中断向量表进入DMA中断服务函数在中断服务函数里通常要调用HAL库的HAL_DMA_IRQHandler()这个函数会根据标志位情况自动调用用户重写的回调函数比如HAL_DMA_XferCpltCallback()。如果你看到的现象是“中断没反应”就要沿着这条链路一段一段查外设有没有发请求DMA有没有接受到请求标志有没有置位标志置位后有没有触发NVICCPU有没有进中断服务函数中断服务函数有没有调用HAL库处理函数HAL库处理函数有没有找到用户回调2.4 HAL库返回HAL_BUSY背后的状态机逻辑HAL库的DMA实现里每个DMA通道对应一个DMA_HandleTypeDef结构体其中State成员记录当前状态。启动传输前HAL库会检查这个State如果是HAL_DMA_STATE_BUSY直接返回HAL_BUSY。所以如果调用HAL_DMA_Start_IT()或HAL_ADC_Start_DMA()时返回HAL_BUSY说明DMA通道的软件状态还停在忙状态没有恢复。这个状态通常和硬件标志是联动的。HAL库在传输完成中断里会更新State然后调用回调如果你关了中断或者中断没有正确执行HAL库就永远没有机会去更新State。于是下一次启动自然就是HAL_BUSY。所以排查的时候不要只看返回值还要看底层的标志位。有时候HAL_OK不代表硬件正常它只是说软件配置参数合法同样HAL_BUSY也未必是硬件卡住可能只是软件状态没同步。理解了这层机制排查思路就清晰了。3. 系统化排查与修复实操3.1 第一时间读取DMA寄存器的“现场状态”遇到问题不要盲目改配置第一步应该把DMA的寄存器状态抓出来。你需要重点看这几个字段通道控制寄存器CCR看看通道是否使能传输方向、模式、地址递增、中断使能位有没有配对传输数量寄存器NDTR如果传输已经开始这个值应该随搬运而递减如果一直是初始值说明DMA根本没有在搬数据中断状态寄存器DMA_ISR或对应通道的标志位看TCIF、HTIF、TEIF有没有置位这能告诉你硬件到底发生了什么事件中断清除寄存器DMA_IFCR通过它清除标志后看BSY位是否会变化。我在调试时习惯在HAL_ADC_Start_DMA()之前和之后各抓一次寄存器状态脚本或调试器直接看比较方便。也可以用串口打印这些寄存器的值但注意打印本身也可能干扰时序生产代码里不要留。3.2 检查中断是否从DMA一路送到了NVIC先检查DMA通道的CCR里有没有使能传输完成中断。比如if (hdma-Instance-CCR DMA_CCR_TCIE) { // 传输完成中断已在DMA层使能 }如果这一位是0说明就算外设请求来了、DMA搬完了也不会产生中断回调自然永远不会执行。接下来要检查NVIC层。在HAL里通常在CubeMX生成的MX_DMA_Init()函数里会调用HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()你要确认DMA通道对应的IRQn是哪一个并且在启动文件里存在对应的中断处理函数。有一个容易踩的坑STM32U5系列有些DMA通道的中断向量名和F系列不完全一样如果工程是从旧型号复制过来的很可能中断服务函数名对不上。链接器不会报错但中断永远不会进你的函数。检查方式是在启动文件里搜DMA相关IRQHandler确认名字与你调用HAL_DMA_IRQHandler的地方一致。3.3 确认外设请求信号真的来了DMA是“被动”的它靠外设请求来触发搬运。所以如果NDTR一直不动大概率是外设根本没发出DMA请求。下面列几个常见外设的检查点ADC的DMA请求在ADC配置里必须使能DMA请求。有些系列是ADC_CR2的DMA位有些是多通道配置里的DMAContinuousRequest启动时还要调用带DMA的启动函数比如HAL_ADC_Start_DMA()而不是单纯HAL_ADC_Start()串口的DMA发送往发送数据寄存器写数据后才会产生DMA发送请求。如果你用HAL_UART_Transmit_DMA()它本身会触发但如果底层串口没使能或者波特率配置异常请求也可能不产生SPI的DMA收发SPI的DMA请求需要在SPI初始化后使能且SPI主模式下时钟不启动就不会有数据请求需要注意极性和相位是否匹配定时器触发如果用定时器触发ADC还得确认定时器确实在跑并产生了PWM或者比较更新事件否则ADC不会启动转换自然也没有DMA请求。如果外设请求一直没有产生你可以临时用逻辑分析仪抓外设事件引脚或者在调试器里读外设状态寄存器确认转换是否完成。不要凭空假设“外设肯定在工作”。3.4 回调函数为何“人间蒸发”HAL库的机制是中断服务函数里调用回调但回调函数本身是弱函数需要你在用户代码里重新定义。很多人写了回调但写错了文件或者没注意前缀。正确的做法是在使用DMA的源文件里定义一个全局函数void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma) { // 判断是哪个DMA通道 if (hdma-Instance DMA1_Channel0) { // 处理你的逻辑 } }如果你用的是ADC的DMA通常不是直接重写DMA的HAL_DMA_XferCpltCallback而是重写ADC相关的回调比如HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)。这两个回调的触发链路不一样前者是DMA中断里调用的后者是ADC中断或DMA回调里进一步调用的。如果名字写错了编译不会报错但回调不会执行尤其是在用了HAL_ADC_Start_DMA时很多人只在ADC的回调里处理数据却忽略了底层DMA回调的存在。我在项目里的习惯是先用一个简单的全局标志或串口打印在HAL_DMA_IRQHandler()入口和出口打点确认入口有没有进、出口调用的是哪个回调。只要这一层通了再往上找数据问题就容易了。3.5 中断标志残留堪称“stays busy”的头号元凶前面说到BSY位在事件标志没清除前不会复位这个情形我在调试中碰到的概率最高。典型场景是第一次DMA传输正常完成但因为某种原因中断没有执行TCIF标志留在那里第二次启动DMA时DMA控制器发现TCIF还没清除认为上一个传输过程还没结束忙标志一直置位新请求根本不会处理。特别是在配置了Normal模式却希望循环传输时最容易触发这个现象。Normal模式传完一次就停下来了NDTR归零TCIF置位如果你没有在中断里清除标志下一次调用HAL_DMA_Start_IT()就可能拿到HAL_BUSY。处理方式很简单启动前先清除所有残留标志必要时先调用HAL_DMA_Abort()。__HAL_DMA_CLEAR_FLAG(hdma, DMA_FLAG_TC | DMA_FLAG_HT | DMA_FLAG_TE);这行代码在每次启动DMA前执行能避免很多“第二次就卡住”的问题。如果你用的是LL库也别忘了类似操作。4. 常见配置错误案例分析4.1 CubeMX里DMA请求源选错编译不报错但运行诡异STM32CubeMX里配置DMA时会要求你为每个DMA通道选择一个“Request来源”。比如你要用UART1接收DMA就应该在UART1的DMA设置里添加通道并让CubeMX自动生成对应的请求选择。如果你手工改过配置或者从别的工程复制过来很容易出现DMA通道选对了、但请求源还是之前外设的情况。这种错误最麻烦的地方在于初始化代码是正常的寄存器配置也没毛病但DMA收不到正确外设的请求所有数据都不会动。检查方法很简单看生成的代码里比如hdma_uart1_rx.Init.Request DMA_REQUEST_UART1_RX;这个值一定要和实际外设匹配。一旦发现这里不匹配CubeMX里重新生成或者手动改成对应值即可。4.2 Normal模式被当成循环模式用很多新手用DMA以为DMA搬完一次后会自己从头再来。实际上在Normal模式下搬运指定长度的数据后通道就停止工作了NDTR变成0通道使能位也会被清零。如果你在主循环里只调用了一次HAL_ADC_Start_DMA之后就等着数据持续刷新根本等不到。这种情况下DMA不是“卡住”了而是已经正常结束只是你没有重新启动它。处理办法要么改成Circular模式实现自动回绕要么在传输完成回调里再次调用启动函数。U5的DMA在Circular模式下搬运完一组数据之后NDTR会自动重载从而持续搬运。像ADC多通道扫描循环采样这种场景Circular模式是标配除非你要处理的是“一次性的搬运任务”。4.3 地址递增方向配反数据乱飞或根本不更新DMA配置里有两个地址相关选项外设地址是否递增、内存地址是否递增。外设寄存器往往固定地址比如ADC的数据寄存器地址固定不变所以外设地址递增要关闭而内存缓冲区通常是一段连续地址需要开启内存地址递增。如果这两个搞反了DMA可能把外设数据写到同一个地址反复覆盖或者从内存往外的地址递增得乱七八糟最后表现为数据不对、内存被写花、甚至触发HardFault。某些情况下还会导致DMA传输提前结束或异常中断标志可能会设置错误反而让busy状态更混乱。配置这类参数时我习惯在脑子里过一遍“数据流方向”外设地址固定还是递增内存缓冲是一段数组还是单个变量传输方向是从外设到内存还是从内存到外设这样逐项确认基本不会错。4.4 数据宽度不一致NDTR计数和实际搬运量对不上很多情况下NDTR寄存器的值是传输“数据单元”的数量而不是字节数。如果外设数据寄存器是32位你配置的DMA数据宽度却是16位那么实际搬运的数据量就会翻倍导致缓冲区溢出或者只搬了一半数据。在ADC多通道采样中如果ADC分辨率是12位或14位数据寄存器通常是32位建议DMA数据宽度配成Word内存缓冲区用uint32_t数组。如果你用uint16_t数组配Word寻址空间虽然也能容纳但每次搬运会跨越两个数组元素最后数据错位很难查。数据宽度不一致也可能会让DMA产生错误事件触发TEIF标志如果错误中断没处理好一样会卡在busy。4.5 开启Cache之后DMA与CPU看到的数据不一致STM32U5部分型号内置Cache如果开启了D-Cache而DMA直接搬运内存CPU读到的可能是Cache里的旧数据或者DMA写到内存时绕过了Cache导致数据不同步。这类问题不一定让DMA卡死但会让程序逻辑看起来像是DMA没工作。解决方法是合理配置MPU把DMA缓冲区标记为non-cacheable或者在访问数据前后做Cache clean/invalidate操作。在HAL中可以用SCB_CleanDCache()、SCB_InvalidateDCache()等函数。如果只是验证DMA功能最简单的方法是在CubeMX里先不开启D-Cache或者把缓冲区放到non-cacheable区域先把问题范围缩小。U5和F系列不同很多例程里没有提前处理Cache的问题我建议刚接触U5的朋友先确认自己用的型号有没有Cache、工程有没有开启再往下查DMA。5. 实战案例U5 ADC多通道扫描循环采样DMA的完整排查过程5.1 需求拆解与初始配置我当时的应用需求很普通ADC1采集三路模拟电压分别是CH0、CH1、CH2使用扫描模式连续转换DMA循环传输到内存数组。CubeMX里大致配置是ADC1开启Scan Conversion ModeNumber of Conversion设为3采样时间拉长避免阻抗问题DMA请求选择ADC1方向Peripheral To Memory模式Circular数据宽度Word内存地址递增同时使能DMA中断。初始化代码生成后我手动调用HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, 3)。调试时发现程序没有进回调adc_buf一直是初始值。5.2 我按什么顺序一步步查的第一步看DMA状态寄存器发现BSY位为1TCIF没有置位。这说明DMA本身没完成任何传输忙状态是“假忙”可能是启动前就卡住了。第二步看CCR寄存器通道使能、外设到内存、内存递增、循环模式、TCIE都看起来正常说明DMA通道本身的配置没有明显问题。第三步看NDTR发现数值始终是3完全没有递减。DMA没有搬运数据那问题很可能在于没有请求到来或者请求映射不对。第四步回到ADC侧。检查ADC状态寄存器发现ADC已经启动扫描模式也开了但是规则通道转换完成后DMA请求没有拉起来。对照参考手册发现ADC的DMA请求使能位没有设置成功。重新检查CubeMX配置发现DMA配置页面虽然选了ADC1作为请求源但ADC自身的DMA请求使能被隐藏在另一个配置项里没有勾选上。生成代码里也因此没有写入对应的寄存器位。5.3 修复方式与最终代码在CubeMX里找到ADC DMA相关配置确保ADC的DMA请求被使能然后重新生成代码。如果不用CubeMX也可以手动在ADC初始化后加入设置请求使能的代码。修改后我再启动DMANDTR开始递减TCIF置位中断触发进到HAL_DMA_IRQHandler()之后ADC回调被调用数据正常更新。一个简化版的关键配置逻辑大概是hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DMAContinuousRequests ENABLE; // 这个必须开 // ... hdma_adc1.Init.Request DMA_REQUEST_ADC1; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_adc1.Init.Mode DMA_CIRCULAR; // ...注意DMAContinuousRequests这个参数在STM32U5的HAL库里它决定了ADC是否连续产生DMA请求。如果这个没开即使你在DMA层配置了循环模式ADC只会在第一组转换结束后产生一次请求之后因为请求被关闭DMA不会再收到新的触发源看起来就像DMA卡住了。其实DMA没卡是ADC没有继续请求。5.4 案例总结全局视角比单独看DMA更重要这个案例最有价值的点在于DMA本身没问题问题在ADC侧。如果你只盯着DMA寄存器和中断要排查到天荒地老。所以排查DMA问题时一定要把“请求源”和“DMA数据流”当成一个整体看而不是只研究DMA一个模块。外设没有产生请求、请求没有映射到DMA通道、DMA通道没有使能中断、中断没有进NVIC任何一个环节出问题最终表象都一样interrupt not set, callback not executed, stays busy。6. 常见问题速查表与排错经验6.1 一张表快速定位DMA卡死问题下面这个表格是我这些年做DMA调试时总结出来的速查表遇到问题可以直接对着找现象可能原因排查方法解决办法启动函数返回HAL_BUSYDMA通道软件状态未复位查看HAL_DMA_GetState()返回值调用HAL_DMA_Abort()或清除DMA标志DMA的BSY位一直为1传输完成/错误标志未清除读中断状态寄存器写IFCR清除TC、HT、TE标志传输完成中断不触发DMA中断使能位未开或NVIC没使能检查CCR的TCIE检查NVIC的IRQ打开中断使能位调用HAL_NVIC_EnableIRQ中断服务函数执行了但回调不执行未重写回调函数或回调函数名不对搜索工程里的HAL_DMA_XferCpltCallback添加回调函数定义确认函数原型NDTR一直不递减外设请求没来或请求映射错误检查外设DMA请求使能位配置外设DMA请求重新选择DMA Request数据搬运错乱地址递增方向、数据宽度配置错误核对DMA Init结构体改PeriphInc、MemInc、PeriphDataAlignment、MemDataAlignment第二次启动时卡住中断标志残留观察启动前状态寄存器每次启动前清除标志或Abort开启Cache后数据不更新缓存一致性问题查看MPU配置将缓冲区设为non-cacheable或做clean/invalidate这张表不是万能的但覆盖了论坛里八成以上的“DMA卡死”提问。只要你按这个表对照检查大部分问题都能自己解决。6.2 我自己的调试习惯把寄存器打印做成“标配”我调试DMA类问题很少上来就改代码。就算找到了可疑点也喜欢先用一段调试代码把关键寄存器值打印出来。打印这些寄存器可以帮助你建立“直觉”什么状态下应该是什么值什么异常值对应什么问题。下面这段代码是我调试时常用的伪代码风格很简单void dump_dma_state(DMA_HandleTypeDef *hdma) { printf(CCR: 0x%08lx\n, hdma-Instance-CCR); printf(NDTR: 0x%08lx\n, hdma-Instance-NDTR); printf(ISR: 0x%08lx\n, hdma-DMA_ChannelIndex ? /* 取对应标志 */ : 0); }在启动DMA前、启动后、以及几十毫秒后再各打一次对比三次数据就能看出DMA到底有没有在工作。这个方法比单独看某个变量可靠得多因为它反映的是硬件真实状态。实际项目中打印函数本身可能占用时间调试完要记得删掉或做成条件编译。还有一点如果你用RTOS在中断服务里不要直接调用复杂打印否则会影响实时性甚至造成死锁把寄存器值存到一个全局结构体里等任务去打印更安全。6.3 关于U5的DMA我还踩过这些坑最后补充一些U5特有的经验。U5的时钟树比老系列更复杂DMA时钟需要确保在RCC里已使能CubeMX生成的HAL_RCC_DMA_CLK_ENABLE()一般会自动加上但如果你从旧工程迁移很容易漏掉。U5的DMA描述符、安全和特权级机制也需要留意。有些DMA请求和中断受到TrustZone影响如果开启了TrustZone但没有正确配置安全分配非安全状态下的DMA代码可能无法正常工作。这个坑在普通教程里很少提到但实际项目可能会遇到。另外U5的CubeMX代码生成逻辑和F4有一些细微差别比如有些配置项名字改成了“DMAMUX Request”不要看到“Request”变了下名字就一脸懵本质是一样的。拿到芯片后先看手册里的DMA框图确认信号流向能省下很多调试时间。我个人体会是DMA这类问题只要状态寄存器读得懂、中断链路理得清、请求源找得准基本没有解决不了的疑难杂症。
返回列表