十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

ADC+DMA缓冲区随机0的排查:从采样时间到DDS配置

ADC+DMA缓冲区随机0的排查:从采样时间到DDS配置 做过多通道ADC采集的嵌入式工程师十有八九见过这种场面波形在示波器上干净利落数据落到DMA缓冲区之后却隔三差五冒出一个0而且位置还不固定。我最初是在一套8通道模拟量采集板上栽的跟头STM32F407的ADC1开了扫描模式配合DMA循环搬运到内存统计分析时总有间歇性跳零。起初怀疑传感器坏了又怀疑前端运放虚焊把调试器挂在内存窗口里盯了半天硬件波形一直是好的缓冲区里却实实在在出现了0。这个现象在英文社区里有个很经典的问题描述Random 0s in DMA buffer when using ADCDMA。它表面上是DMA的锅但真正查起来会发现DMA只是个老实巴交的搬运工问题往往藏在ADC配置、缓冲区尺寸、采样时间这些容易被忽略的细节里。这篇文章就把我在这类问题上的完整排查思路、修复方案和验证手段整理出来给正被同样现象折磨的朋友一个可复现的参考。1. 复现现场先把“随机0”拆成三种情况1.1 典型的故障配置长什么样先说一个典型场景方便对号入座。MCU用的是STM32F407ADC1配置为4通道扫描模式软件触发连续转换开启DMA设置为Circular循环模式缓冲区是一个长度为4的uint16_t数组。代码里没有对缓冲区做任何清零操作启动后直接调用HAL_ADC_Start_DMA开始采集。运行之后用调试器观察缓冲区可能长这样// 第一次读取 adc_buf[0] 0x0A2D; adc_buf[1] 0x0000; // 异常0 adc_buf[2] 0x0B12; adc_buf[3] 0x0A2D; // 下一次读取 adc_buf[0] 0x0A2D; adc_buf[1] 0x0B12; adc_buf[2] 0x0000; // 异常0跑到这里来了 adc_buf[3] 0x0B12;注意“随机”两个字很关键。如果0固定在某个位置比如每次都是adc_buf[1]为0那是通道配置或DMA搬运顺序的问题比较好定位。但如果0的位置在变甚至有时候一整个周期都正常那就说明不是单纯的“某个通道坏了”而是时序或者配置层面的问题。1.2 三种“随机”对应的不同排查方向我后来总结了一个分类方法遇到随机0先别急着改代码先分清楚它属于哪一类。现象特征优先排查方向0固定出现在缓冲区某个索引位置DMA长度参数、规则序列寄存器配置、通道顺序0出现在缓冲区不同位置但概率稳定DMA传输长度与缓冲区大小不匹配、读写竞争0完全随机通道和位置都没有规律采样时间过短、信号源阻抗过高、连续DMA请求未开启第一种情况通常是配置错位比如SQR寄存器里注册了4个通道但DMA的Length参数只写了1那么只有第一个通道的数据会被搬运剩下的位置保持上电初始值0看起来就是“固定位置的随机0”——位置固定但值可能在不同启动批次里略有差别。第二种情况往往是缓冲区尺寸和通道数不是整数倍关系或者DMA在循环回绕时产生了写覆盖导致某些位置读到的数据是旧值残留。第三种情况最难查因为它涉及ADC模拟部分的采样过程。SAR型ADC在采样阶段需要对内部采样电容充电如果充电时间不够转换结果就会不可靠轻则数值偏低重则直接得到0。这个在后面专门展开。2. 搬运链路排查DMA模式、缓冲区尺寸与内存一致性2.1 DMA传输长度和缓冲区循环模式之间的关系DMA出问题的概率其实很低但配置错误的概率很高。我在多台设备上踩过同一个坑HAL_ADC_Start_DMA的Length参数写错。这个参数不是“有几个缓冲区”而是“要搬运多少个半字数据”。在4通道扫描模式下每完成一个通道的转换就会触发一次DMA请求搬运一个半字到内存。所以Length应该等于4而不是1。如果Length写成1DMA只搬运第一个通道的转换结果缓冲区后面三个位置永远保持初始值0。此时如果代码在中断里每隔一段时间读一次缓冲区你会发现adc_buf[0]一直在变adc_buf[1]到adc_buf[3]全是0像是“ADC只采集了第一个通道”。但问题是ADC本身四个通道都在转只是后面三个通道的数据没有被搬运到内存而已。如果把Length写成8而缓冲区大小只有4那就更麻烦了。DMA会试图往缓冲区后面写4个半字直接越过数组边界写到相邻内存地址去。轻则污染其他变量重则触发HardFault。缓冲区大小必须和Length严格对应并且Length最好是通道数的整数倍这样每一轮转换结果都能被完整搬运。Circular模式下DMA写满缓冲区后会重新从头部开始写相当于环形缓冲。这种模式适合“ADC持续采样、CPU稍后读取”的场景但有一个隐患如果你在读取缓冲区的同时DMA正在写入同一个位置就可能读到半新半旧的数据。好在ADC转换结果是16位DMA搬运也是半字操作单次搬运是原子的不会出现字节撕裂但可能读到和当前采样周期对不上的旧数据。2.2 未初始化缓冲区与首次读取的陷阱还有一种特别容易忽略的情况缓冲区本身就没初始化。如果定义的数组是全局变量编译器通常会把它放在BSS段上电后自动清零。DMA还没开始搬运时缓冲区里全是0。如果代码在执行HAL_ADC_Start_DMA之后立刻去读缓冲区读到的一大批0会让你误以为ADC出了问题。我见过有人用局部数组定义缓冲区没有加static修饰结果DMA写入时缓冲区地址已经失效栈空间被别的函数覆盖读出来的数据乱七八糟其中就包含大量0。DMA缓冲区必须确保生命周期覆盖整个采集过程全局变量或者static修饰的局部变量是最稳妥的选择。更隐蔽的情况是DMA已经启动了但ADC还没有完成第一次转换。ADC从启动到第一个规则通道转换完成需要一点时间包括校准、稳定、采样、转换这几个阶段。在这段时间里DMA缓冲区保持初始0。如果代码在首轮转换完成前就对数据做了处理前面几个样本就是0。解决方法是让ADC完成一次转换后再开始处理数据具体可以通过DMA传输完成中断来同步。2.3 带Cache的MCU要处理缓存一致性问题如果你是STM32H7或者其他带D-Cache的MCU那“随机0”还有一个完全不同的来源缓存一致性。DMA外设直接访问内存不经过CPU的Cache。如果DMA把数据写到了物理内存而CPU的Cache里还保留着这一地址的旧数据CPU读到的就是缓存里的旧值。如果旧值是0那看起来就是“DMA缓冲区里出现了0”。这个问题的关键在于缓冲区所在内存区域是否被配置为Cacheable。默认情况下如果缓冲区定义在普通SRAMCPU读取时走CacheDMA写入时不经过Cache两边数据就对不上。处理方式有两种一种是把缓冲区放在非Cacheable区域比如H7的AXI SRAM如果不配置MPU就是非Cacheable另一种是在读取前主动失效Cache强制CPU从物理内存重新加载。// 读取DMA缓冲区前先失效对应地址的D-Cache SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));注意失效的地址要按32字节对齐否则实际失效范围可能比预期小反而漏掉一部分数据。缓冲区定义时可以用__attribute__((aligned(32)))强制对齐。3. 数据链路排查ADC序列、采样时间与DMA请求的握手3.1 SQR规则序列多配、漏配、顺序错乱DMA链路查完没有问题时就该把目光放回ADC本身。规则序列寄存器SQR是排查重点。ADC扫描模式要求你明确指定“这一轮扫描要转换哪些通道、按什么顺序转换”。在STM32的HAL库中每个通道通过ADC_ChannelConfTypeDef结构体单独配置但很多人容易忽略一个细节sConfig.Channel和sConfig.Rank必须和初始化结构体中的NbrOfConversion对应。比如你要转换4个通道就必须调用4次HAL_ADC_ConfigChannel并且Rank分别设为1、2、3、4。如果漏配了一个Rank或者两个通道的Rank重复扫描序列就会错位某些通道的数据不会出现在预期位置。寄存器层面多通道扫描时SQR1、SQR2、SQR3共同决定了序列顺序。调试时可以打开调试器的外设寄存器窗口直接核对SQR寄存器的内容确认每个Rank对应的通道号是否和代码意图一致。还有一点容易被忽略如果配置了扫描模式但没有在SQR中注册通道就直接启动ADC某些MCU上转换不会触发DMA请求缓冲区保持全0。这类问题在F1系列上尤其常见因为F1的ADC规则通道配置比较死板漏配任何一个SQR位都可能让整个扫描序列失效。3.2 采样时间过短SAR ADC的“接水桶”效应这一节可能是整篇文章里最能解释“完全随机0”的地方。SAR ADC的转换过程分为采样和转换两个阶段。采样阶段内部采样开关闭合外部信号源通过一个很小的开关电阻向内部采样电容充电。充电时间由采样时间决定如果采样时间太短采样电容还没充满就开始转换比较器的输入电压就不等于真实信号电压。用生活里的话说这就像拿一个水桶去接水管的水接水时间太短桶里的水位就偏低。ADC转换结果就相当于在“桶里水位”的基础上做量化水位不够结果自然偏低。当信号源阻抗较高时充电回路的RC常数变大需要更长的充电时间。如果采样时间配置为1.5个ADC时钟周期而信号源阻抗有几十千欧那每一次采样都可能处在“充了90%还是99%”的不确定状态转换结果就会忽高忽低严重时直接采到接近0的值。多通道扫描模式下这个问题会更突出因为每个通道的采样时间由SMPR寄存器统一或分别控制。如果一个通道的信号源阻抗高、另一个通道阻抗低共用同一套采样时间配置高阻抗通道就容易出现偶发跳零。修复方法很直接把采样时间加大。STM32F4系列的SMPR2寄存器可以设置3到480个周期的采样时间。对于高阻抗信号源建议至少设置84周期以上。我一般先试84周期如果还有跳变直接上480周期。代价是ADC采样率下降高频率采样场景需要权衡。sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES;3.3 连续转换与DMA请求的握手细节这是“随机0”的另一个高频根因而且非常隐蔽。在STM32F1和F4系列上ADC的DMA请求通过ADC_CR2寄存器的DMA位使能每次规则通道转换完成都会产生DMA请求。F1比较简单只要DMA位使能DMA请求就一直存在。但F4有一个DDS位DMA disable selection如果DDS被清零那么最后一个规则通道转换结束后DMA请求会被禁止如果DDS置1DMA请求持续产生。在HAL库中这两个配置一般由ContinuousConvMode和DiscontinuousConvMode控制。如果开启了连续转换模式DDS通常也会被置位DMA请求就是持续状态。但如果你用的是库函数的低级配置可能只开了DMA位没设置DDS就会出现一个诡异现象第一轮扫描的4个通道数据正常搬运到缓冲区之后ADC虽然在继续转换但DMA请求被禁用了缓冲区里的数据不再更新。下一轮读取时只有前4个位置有值后面全是0。在F0、G0、L4这些较新的系列上ADC_CFGR寄存器里的DMACFG位明确区分了两种模式DMACFG1是连续DMA请求每一次规则通道转换都会触发DMA请求DMACFG0是单次模式只有整个序列转换完成后才触发一次DMA请求。如果这里的配置和代码期望不一致缓冲区就会出现“只更新一部分位置其他位置全是0”的现象。从现象上看这种0不是完全随机而是“一开始有数据后面全是0”或者在缓冲区的前半部分有数据、后半部分是0。但如果代码在多个地方读取缓冲区读取时机不一样看到的0的位置也会不同容易误判成随机。调试这类问题有一个很实用的技巧在调试器里观察DMA的NDTR寄存器剩余传输次数寄存器。如果NDTR持续在减小说明DMA一直在搬运如果NDTR固定在0说明DMA已经完成了一轮传输而且没有循环启动。这个信息能帮你快速判断是ADC请求问题还是DMA配置问题。4. 修复代码从HAL到寄存器级以及GD32适配4.1 STM32 HAL标准修复示例如果用的是STM32CubeMX生成工程建议手动核对这几个关键配置。我贴一下F407上的标准配置可以作为对照模板。ADC初始化部分hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; // 扫描模式必须开启 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换配合DMA循环 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.NbrOfDiscConversion 0; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发简单可靠 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 4; // 4个规则通道DMA初始化部分hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; 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_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; // 循环模式持续搬运 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH;启动部分uint16_t adc_buf[4]; // 缓冲区长度必须等于NbrOfConversion HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4);这段配置里最容易被改错的就是最后的Length参数。很多人在CubeMX里生成代码后习惯性把Length改成缓冲区大小值填了数组字节数而不是半字数结果DMA一直搬运超出范围的数据缓冲区后面被写满垃圾值。正确做法是填半字个数也就是ADC转换结果个数对应通道数乘以采样轮数。4.2 寄存器级排查关键点HAL库封装了太多细节排查问题时容易抓瞎。这时候直接看寄存器更直观。ADC方面重点检查ADC1-CR1的SCAN位和ADC1-CR2的DMA位、CONT位。F4系列注意CR2的DDS位必须为1确保DMA请求连续产生。DMA方面F4每个DMA流都有独立的NDTR寄存器比如DMA2_Stream0的NDTR在地址0x40026410附近CubeMX生成的工程可以在调试器外设窗口里直接查看。NDTR的值表示还有多少个数据要传如果它在不断变化说明DMA正常工作如果固定为0说明传输已经完成并且没有重新启动。如果NDTR停在0而ADC还在转换那就要检查DMA请求是否被阻断。常见原因包括DMA配置成了Normal模式而非Circular模式或者ADC的DDS位被清零导致请求被禁止。把DMA改成Circular把DDS置1问题通常就解决了。4.3 GD32同源问题的适配修改GD32的ADC和DMA架构与STM32非常相似但库函数接口有差异网上搜到GD32 ADC DMA的帖子也很多。这里说两个重点。第一个是DMA请求模式。GD32F30x系列中使能ADC的DMA后还要配置DMA请求是“单次”还是“多次”。对应函数是adc_dma_request_after_transfer参数可以传ADC_DMA_REQUEST_ONCE或ADC_DMA_REQUEST_MULTIPLE。如果用循环DMA并且希望ADC持续产生DMA请求必须选MULTIPLE。选成ONCE的话第一次转换完成后DMA请求就停了缓冲区后面全是0现象和STM32F4上DDS没置位一模一样。adc_dma_mode_enable(ADC0); adc_dma_request_after_transfer(ADC0, ADC_DMA_REQUEST_MULTIPLE);第二个是DMA配置的细节。GD32的DMA初始化结构体里方向、地址递增、数据宽度这些字段和STM32很接近但外设基地址和内存基地址的类型不一样有些固件库要求显式赋值不能依赖默认值。尤其是内存地址递增这一项如果忘了使能DMA会反复往同一个地址写缓冲区其他位置始终保持0。GD32不同型号的固件库函数名有差异如果找不到adc_dma_request_after_transfer就在adc库头文件里搜一下dma_request通常能找到对应的配置接口。5. 验证与防复发用数据说话5.1 数据验证方法修复之后不能只看一两眼就收工要用数据验证确认问题真正解决。我的做法是写一个简单的统计函数采集连续N个样本统计0值出现的次数和位置。N取10000如果修复前0值出现几十次修复后应该一次都没有。uint32_t zero_count 0; uint32_t zero_positions[100]; for (uint32_t i 0; i 10000; i) { if (adc_buf[i % 4] 0) { zero_count; if (zero_count 100) { zero_positions[zero_count - 1] i % 4; } } }如果0值完全消失说明数据链路的时序问题已经解决。如果还有零星0重点看它的位置。固定位置说明DMA配置还有问题随机位置则更可能是采样时间不足或信号源阻抗问题。如果有信号发生器可以输入一个已知幅值的正弦波观察DMA缓冲区里的数据是否连续平滑。正弦波数据里如果出现明显的凹陷到0基本可以确定是采样或时序问题。5.2 防复发检查清单踩过几次坑之后我总结了一个检查清单每次写ADCDMA的初始化代码都会过一遍。检查项正确配置错误后果DMA Length参数等于半字个数即通道数乘以轮数缓冲区部分位置保持0DMA工作模式CircularNormal模式下传输完成后停止ADC扫描模式开启只有最后一个通道有数据ADC连续转换开启只能采集一轮F4的DDS位置1DMA请求在首轮后停止F0/G0/L4的DMACFG置1只有单次DMA请求缓冲区生命周期全局或static局部数组地址失效Cache一致性失效Cache或配置Non-CacheableCPU读到旧值或0采样时间根据信号源阻抗调整偶发跳变到0这个清单不敢说覆盖所有MCU型号但在STM32和GD32这两大系列上能解决绝大多数“ADCDMA缓冲区随机0”的问题。调试这类问题我的习惯是先把DMA排除掉再查ADC配置最后才怀疑模拟前端。因为DMA的配置项少寄存器一目了然查起来快ADC配置项多时序复杂需要慢慢梳理。每次看到“随机0”提醒自己一句话DMA不会凭空造数据0要么来自没被更新的内存要么来自没充满电的采样电容。顺着这个思路往下查方向就不会歪。
返回列表