news 2026/10/26 8:47:02

STM32 ADC多通道采集:轮询、EOC中断与DMA双缓冲避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC多通道采集:轮询、EOC中断与DMA双缓冲避坑

简介:围绕STM32G431RBT6的ADC直接采集与DMA传输,这份资料面向嵌入式开发初学者和需要上手STM32G4模拟外设的工程师,重点解决ADC单次读取寄存器与DMA连续搬运两种采集方式的配置理解。内容涉及ADC1/ADC2多通道特性、采样时间与分辨率设置、转换完成中断、DMA源/目的地址链接,以及采样率与传输长度等性能优化思路,可用于传感器信号监测、电源管理等实时数据采集场景。压缩包共1254个文件,约29.73MB,以C源码和H头文件为主体,辅以汇编启动文件、Keil与IAR工程文件、链接脚本及少量说明文本,便于直接编译、对照与移植。已有1978人学习下载。读者可从中获得可直接运行的工程框架、HAL库驱动组织方式、ADC与DMA联动配置范例和中断配合排错参考,适合作为课程实验、项目原型或竞赛开发的起步材料。

1. 一次 ADC 采集拖慢你的,往往不是转换速度

大多数人调 stm32 的 ADC,第一件事是翻数据手册找「最快多少 Msps」,然后把采样时间压到最短,结果发现吞吐还是上不去,波形还是抖。问题很少出在逐次逼近的电容阵列里,而在转换完成到数据落进内存这段路上:数据寄存器只有一个位置,下一次转换结果到来时,如果没人及时把它取走,旧数据就被直接覆盖,标志位上只留下一个 OVR。轮询是把 CPU 押在这条路上等,DMA 是把这条路交给总线控制器自己走。两者的差别不在「快慢」,而在「谁承担等待」。这条路上每一个环节——EOC 何时置位、请求线怎么连、NDTR 减到几触发中断、溢出标志什么时候被清——都会在你以为已经跑通的时候翻车,而且现象往往不是「没数据」,是「数据看着对,其实错位了」。

2. SAR 转换的时序账:一次采样到底占用多少个 ADC 时钟

要判断该用轮询还是 DMA,先得知道一次转换到底花多长时间。这个数字决定了 CPU 有多少余量、DMA 请求有多密集、以及你有没有可能在两次请求之间被中断打断而出事。逐次逼近型 ADC 的耗时不是一个固定常数,它是「采样窗口 + 逼近过程」两段拼起来的,而且这两段都由 ADC 时钟分频得到。

2.1 采样保持窗口:外部源阻抗与内部采样电容的 RC 充电

SAR ADC 的输入端本质上是一个开关加一个小电容。转换开始前开关闭合,外部信号通过源阻抗给这个电容充电;开关断开瞬间,电容上锁存的电压就是这次转换的输入。采样时间设置得够不够,取决于两个东西:外部源的内阻 R_AIN,和内部采样电容 C_ADC(这类器件通常在几个 pF 量级)。充电过程是指数逼近,要充到 1/2 LSB 的精度,需要的常数个数大致是 ln(2^(n+2)) 这个量级,n 是分辨率。把公式反过来写,就是手册里常见的那条约束:允许的最大源阻抗约等于「采样周期时间除以(采样电容 × ln(2^(n+2)))」再减去内部开关电阻。

这条约束的实际意义是:采样档位不是随便挑最短的那个。常见的档位是 2.5、6.5、12.5、24.5、47.5、92.5、247.5、640.5 个 ADC 周期,档位越短吞吐越高。如果你前端是个分压电阻网络,源阻抗几 kΩ 到几十 kΩ,却用了 2.5 周期的采样时间,现象是读数系统性偏低——不是噪声,是电容根本没充到位,而且偏得还很稳定,容易被误判成「校准没做」。我在一个模拟项目上见过这种排查:换通道、换参考、重烧校准都没用,最后把采样时间从 6.5 改成 47.5,偏差立刻消失。

2.2 转换周期的构成:为什么 12 位是 12.5 而不是 12

12 位分辨率下,逐次逼近需要 12 个时钟周期逐位确定结果,另外还有约 0.5 个周期用于结果锁存与流水线对齐,所以总转换时间被写成 12.5 个 ADC 周期。分辨率降一档,这个数字会跟着降:10 位大约 10.5,8 位 8.5,6 位 6.5。总时间就是:

T_conv = (SMP + 12.5) / f_ADC // 12 位,SMP 为采样周期数

举个能落地的数:ADC 时钟 42.5 MHz,采样时间设 12.5 个周期,那么一次转换耗时 25 / 42.5 MHz ≈ 588 ns,理论吞吐约 1.7 Msps。这个数字很有用——它告诉你 DMA 请求的到达间隔量级是亚微秒级,也就是说在 170 MHz 的核上,两次请求之间只有几百个指令周期。任何在这个窗口里「顺手做点别的事」的想法,都要先算清楚代价。手册标称的 4 Msps 量级上限,只在你把采样时间压到最短、并且通道落在允许高速采样的那几路上时才成立,不是所有引脚都能跑满。

2.3 ADC 时钟源、校准与参考电压:三个容易被当成「玄学」的偏移源

ADC 时钟不是直接拿系统时钟用的,它来自一条独立的分频路径,可能挂在 PLL 的某个输出上,也可能走异步时钟。这条路径一旦配错,你算出来的 T_conv 全是错的,DMA 缓冲区里的数据节奏也跟着错。配置时值得把时钟树那一页对着看一遍,确认 ADC 时钟频率与你代码里假设的一致。

校准是第二个源。这类芯片内部有校准逻辑,需要在 ADC 关闭(ADEN=0)的状态下启动,等校准完成标志清零后才能使能。校准补偿的是内部电容阵列的失配,不补外部电路。上电不做校准、或者在校准期间还开着 ADC,得到的偏移可能几十个 LSB,而且随温度漂。第三个源是参考电压:如果用的是内部参考缓冲,需要等它稳定;如果直接用 VDDA 做参考,那 VDDA 上的任何纹波都会一比一进入结果,这时候你去调采样时间是没有意义的。

提示:把「T_conv 计算」「采样档位」「参考电压来源」这三件事写成注释贴在初始化代码旁边,半年后回来改采样率时能省掉一次完整排查。

3. 直接采集:EOC 标志、DR 读取与 CPU 的时间账

轮询方式的代码往往只有三行,所以它给人一种「没有成本」的错觉。真实成本藏在时序里:CPU 在一次转换期间必须反复检查标志位,而这个检查循环本身也在消耗总线周期和功耗。单通道、低频、慢速场景下这没什么问题,问题出现在你把采样率往上提、或者通道数变成多个的时候。

3.1 轮询循环的真实开销与 EOC 的清除语义

最直白的单通道轮询长这样:

/* 单通道轮询采集:启动一次,等 EOC,读 DR 取结果 */ ADC1->CR |= ADC_CR_ADSTART; /* 软件触发启动一次转换 */ while (!(ADC1->ISR & ADC_ISR_EOC)) { /* 空转等待,CPU 全程被占用 */ /* 这里除了自旋什么也做不了 */ } uint16_t raw = (uint16_t)ADC1->DR; /* 读 DR 会顺带清 EOC 标志 */

关键在于最后一行:EOC 是靠「读 DR」清掉的,不是靠写标志位。这一点如果搞错,比如用写 ISR 的方式清 EOC 而没读 DR,下一次循环判断会立刻命中一个陈旧的 EOC,读到的还是上一次的转换结果。现象就是「数据永远滞后一拍」,在缓慢变化的信号上几乎看不出来,在阶跃信号上就是一个采样点的固定延迟。

自旋等待的成本可以量化:如果一次转换 588 ns,核跑 170 MHz,那么每采样一次就有约 100 个指令周期被空转吃掉。单通道 10 kHz 采样时这点开销无所谓;八个通道各 100 kHz,就是每秒钟 80 万次自旋,CPU 大部分时间在做无用功,而且任何中断都会拉长等待窗口,让采样节拍变得不均匀。对于要做 FFT 或者需要等间隔采样的场景,节拍不均匀比丢数据更致命。

3.2 多通道扫描:为什么不开 DMA 就必然踩到 OVR

多通道场景下轮询几乎不可用,原因不是性能,是正确性。序列扫描模式下,ADC 按配置的顺序依次转换多个通道,每个通道转换完都往同一个 DR 里写。如果你没有在下一个通道转换完成之前把上一个结果读走,就会触发溢出:OVR 置位,新结果被丢弃,DR 里留着旧值。

/* 多通道扫描时最容易写错的一段:靠轮询依次取 N 个通道 */ for (int i = 0; i < CH_NUM; i++) { while (!(ADC1->ISR & ADC_ISR_EOC)) { } buf[i] = (uint16_t)ADC1->DR; /* 一旦某次没跟上,这里拿到的就是错位数据 */ }

这段代码在实验室里能跑,因为编译器优化级别、中断占用、甚至调试器是否连接都会影响循环快慢。换个优化等级、加一个串口打印,就可能开始出现「buf[2] 里是通道 4 的值」这种错位。更隐蔽的是它不会报错,OVR 只是一个标志位,没人查它就一直是静默的。血泪经验是:序列长度大于 1 的时候,直接上 DMA,不要试图用轮询去追。

3.3 中断方式的位置:它能救 CPU,但救不了吞吐上限

用 EOC 中断替代自旋,确实把 CPU 从等待里解放出来了,但每次转换仍然要进一次中断:保存上下文、进中断向量、读 DR、清标志、退中断。这条路径的开销通常在几十个周期量级,比自旋便宜,但转换率越高,中断频率越高,累积开销和中断嵌套风险就上来了。在几十 kHz 量级它很合适,在几百 kHz 以上,中断本身就变成了瓶颈,而且它会和其他中断抢优先级,导致采样时刻抖动。

中断方式真正适合的场景是「采样率不高但需要及时响应」,比如按键按下时读一次电池电压。若要连续等间隔采样,还是得让 DMA 接手,让每一次转换请求变成一次纯硬件的数据搬运,CPU 只在缓冲区填满一半或填满时被打扰一次。

4. DMA 搬运:ADC 请求如何在总线上变成一次内存写

DMA 在这个场景里做的事非常单一:ADC 每完成一次转换,就产生一个请求,DMA 控制器收到请求后,从 ADC 的数据寄存器读一个半字,写到内存缓冲区里下一个位置,然后把传输计数减一。整个过程中 CPU 完全不参与,也不影响采样节拍。看起来简单,但配置时有几个点必须对上,否则现象会是「缓冲区全零」或者「只有第一个元素有值」——这类问题的排查成本远高于写代码的成本。

4.1 请求线映射与 DMAMUX:通道号和 ADC 实例为什么强绑定

每个 DMA 控制器有固定数量的通道,每个通道能响应哪些外设请求是由映射表定死的。ADC 的请求通常挂在某个固定通道上,而在带请求复用器的型号上,你可以通过复用器把请求重新路由到任意通道。这带来两种典型错误:一是照抄别人的代码,通道号对不上自己用的 ADC 实例,DMA 从头到尾收不到请求;二是复用器没配置,请求线悬空。

配置顺序也有讲究:DMA 通道的参数(方向为外设到内存、源地址为 DR 寄存器、目标地址为缓冲区、源地址不自增、目标地址自增、数据宽度半字、优先级)要先配好并使能,再使能 ADC 的 DMA 请求位,最后才启动转换。顺序颠倒的后果是前几次转换的请求被丢掉,缓冲区前几个元素是脏值。

/* ADC 侧使能 DMA 请求:这一步漏了,DMA 配置再对也不会动 */ ADC1->CFGR |= ADC_CFGR_DMAEN | ADC_CFGR_DMACFG; /* DMACFG=1 为循环模式 */ /* DMA 侧:外设到内存,半字宽度,目标地址自增,循环模式 */ DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; /* 源:数据寄存器,固定地址 */ DMA1_Channel1->CMAR = (uint32_t)adc_buf; /* 目标:内存缓冲区 */ DMA1_Channel1->CNDTR = BUF_LEN; /* 传输个数,决定半传输/完成中断的位置 */ DMA1_Channel1->CCR |= DMA_CCR_CIRC | DMA_CCR_MINC | DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0;

CNDTR是这套机制里最重要的一个参数,它同时承担两个角色:剩余传输计数,以及中断触发阈值的基准。半传输中断在计数减到BUF_LEN/2时触发,传输完成中断在减到 0 时触发,循环模式下计数自动重装。缓冲区长度因此不是一个随意的数字,它决定了你「一次能拿到多长的连续数据」,也决定了中断频率。

4.2 循环模式与半传输中断:拼出一条无锁流水线

循环模式加两个中断,是这类采集最常用的组合。做法是把缓冲区切成前后两半:DMA 写前半段时,CPU 处理上一次装满的后半段;半传输中断到来时前半段已满,CPU 转去处理前半段,DMA 继续写后半段。两边交替,只要单次处理耗时小于半段数据的采集时间,就不会覆盖未处理的数据。

#define BUF_LEN 512 static uint16_t adc_buf[BUF_LEN]; /* 必须放在 DMA 可访问的普通 SRAM 里 */ /* 半传输:前半段 [0, BUF_LEN/2) 刚写完,可以安全处理 */ void adc_half_cplt_cb(void) { process(adc_buf, BUF_LEN / 2); } /* 传输完成:后半段 [BUF_LEN/2, BUF_LEN) 刚写完,同时 DMA 回到起点重写前半段 */ void adc_full_cplt_cb(void) { process(adc_buf + BUF_LEN / 2, BUF_LEN / 2); }

两个回调操作的是不同的内存区间,理论上不需要加锁。但这里有个前提容易被忽略:处理函数必须在下一个半段被写满之前返回。如果处理里包含了串口打印或者 flash 写入,耗时会远超预期,边界就会被踩到,表现为「偶尔有一小段数据出现台阶」。要验证这一点,在回调里翻转一个空闲 GPIO,用示波器看高电平持续时间相对采样周期的占比,比读代码可靠。

4.3 多通道扫描下的缓冲区布局:长度必须是通道数的整数倍

序列扫描加 DMA 时,缓冲区不是「每个通道一段」,而是按转换顺序交错排列:第一个元素是序列里的第一个通道,第二个是第二个通道,依此类推,转满一轮再回到第一个通道。这意味着缓冲区长度必须是通道数的整数倍,否则半传输中断的切点会落在某一轮序列的中间。

#define CH_NUM 4 #define ROUNDS 64 #define BUF_LEN (CH_NUM * ROUNDS) /* 512,保证半传输边界落在轮次分界上 */ /* 解交错时按下标取模,把交错数据还原成每通道一条序列 */ for (int i = 0; i < BUF_LEN; i += CH_NUM) { ch0[i / CH_NUM] = adc_buf[i + 0]; ch1[i / CH_NUM] = adc_buf[i + 1]; /* ... */ }

如果长度写成 500 而不是 512,半传输切点落在第 250 个元素上,250 不是 4 的倍数,前半段最后一轮只有两个通道的数据,后半段开头就错位了两个位置。这种错位不会报任何错误,只会让波形看起来「相位漂了一点」,很多人会先去怀疑采样电容和前端滤波,绕一大圈才回到长度上。

5. 避坑与排查:通道错位、缓冲区为零与 OVR 那些踩坑记录

下面几条都来自实际调试现场,现象看起来各不相同,根因基本集中在「长度、位置、标志」三件事上。按现象、原因、解决三段式记下来,比通读一遍参考手册更快命中问题。

5.1 现象:读数在相邻通道之间跳动,波形像被平移了一个位置

原因通常是缓冲区长度不是通道数的整数倍,或者半传输回调里按通道数分块时的起始偏移算错。交错存储的布局一旦在边界处断开,解交错就会把通道 3 的数据当成通道 0,整条序列平移。另一种可能是序列配置里通道顺序与解交错代码里的假设顺序不一致。

解决方式是先验证布局:把缓冲区前 8 个元素原样打印出来,给输入端接不同电压(比如通道 0 接参考电压、其余接地),看这 8 个数是否呈现出预期的交替模式。确认布局正确之后,再把缓冲区长度改成通道数的整数倍,并在代码里用_Static_assert(BUF_LEN % CH_NUM == 0, "len must be multiple of channels")把它钉死。

5.2 现象:DMA 缓冲区永远是 0,或者只有第一个元素有值

第一种可能是缓冲区被放在了 DMA 访问不到的内存区域。这类芯片上有一块紧耦合的核内 SRAM,只有 CPU 能访问,DMA 控制器根本看不到它,把缓冲区定义在那里,DMA 写不进去,读出来就是全零或初始值。链接脚本里把变量放到核内 RAM 的段,编译器不会有任何提示。

第二种可能是循环模式没开,或者 ADC 侧的 DMA 请求位没置。循环模式关闭时,CNDTR减到 0 就停止,之后转换继续但数据不再搬运,缓冲区后半段保持初始值。第三种可能是数据宽度不匹配:ADC 结果是半字,DMA 配成字节宽度,只会搬低 8 位,高 4 位丢失,值看起来「偏小但不像零」。

排查时按这个顺序走:查缓冲区地址落在哪个段、查 DMA 控制器的传输完成标志有没有置过、查CNDTR在运行中是否在递减。CNDTR不动就说明请求根本没到,问题在 ADC 侧或映射;CNDTR在动但数据不对,问题在宽度或地址。

5.3 现象:跑几分钟后数值整体偏移,偶尔有单点跳变

偏移通常来自两个方向。一是参考电压或供电在漂:用 VDDA 做参考时,板上其他负载(尤其是射频或电机驱动)带来的纹波会直接进入结果,采样时间调多长都救不了。二是长期没有重新校准,内部电容阵列的失配随温度变化,偏移慢慢累积。

单点跳变则要怀疑溢出:OVR 标志置位后新数据被丢弃,DR 里留着上一轮的值,如果软件没有检查 OVR,这个陈旧值就会被当成新采样。解决方式是打开溢出中断(或者在每次处理缓冲区时检查一次 OVR),让溢出变成可观测事件而不是静默丢数据;同时把参考电压那一路的去耦和走线重新过一遍,必要时改用内部参考缓冲以获得更稳的基准。校准建议放在上电初始化里,并且检查校准完成标志,不要靠固定延时硬等。

5.4 现象:半传输回调里拿到的数据,后半段是上一次的旧值

这是「处理时间超过半段采集时间」的典型表现。回调触发时前半段刚写完,但如果上一次处理后半段的函数还没返回,这次前半段的数据其实已经被 DMA 覆盖了一部分,你读到的是新旧混合。根本原因不在 DMA,在处理函数的耗时不可控。

解决方式是给处理函数设一个硬性预算:半段数据的采集时间 = 半段长度 × T_conv,处理函数必须在它的一半以内返回,留出余量。把耗时操作(打印、存储、复杂运算)移出回调,改成写入一个更小的环形队列,回调只做搬运。另一个办法是加大缓冲区,把半段时长拉长,代价是延迟增加。

注意:DMA 采集链路的调试顺序永远是「先看 CNDTR 在不在动,再看数据对不对,最后才看精度」,从精度入手排查往往会陷在模拟侧出不来。

6. 设计取舍:什么时候该放弃 DMA,怎样验证链路是干净的

三种方式没有绝对优劣,取舍取决于采样率、通道数和实时性要求这三者的组合。把它们的特征摊开看会更清楚:

方式适合的采样率量级CPU 占用主要风险典型场景
轮询低(几十 kHz 以下)高,随采样率线性上升节拍不均匀、多通道必然 OVR上电自检、慢变信号监测
EOC 中断中(几十到几百 kHz)中,与转换率成正比中断抖动、优先级冲突单通道中速采集、事件驱动采样
DMA 循环 + 双缓冲高(接近 ADC 上限)低,只在边界被打扰长度/内存位置配错、处理超时多通道连续采集、波形与频谱分析

选择上的经验是:只要序列长度大于 1,就直接用 DMA,这是正确性问题不是性能问题;只要是连续等间隔采样,就用定时器触发加 DMA,让采样时刻由硬件决定,而不是由软件循环决定。反过来,如果只是开机读一次基准电压、或者按键时读一次电池,轮询反而更省事,代码三行就能看懂。

DMA 也有它自己的负担。循环模式加双缓冲意味着你要维护缓冲区边界和处理预算,一旦处理逻辑变复杂,边界就会成为新的故障点。如果采样率其实只有几 kHz,用 DMA 带来的调试成本可能超过它省下的 CPU 时间。我见过为了「架构统一」把所有 ADC 都改成 DMA 的工程,最后因为一个半传输边界问题查了两天,而那个通道的实际采样率只有 1 kHz。

验证链路是否干净,我习惯做三件事。第一,用定时器触发固定频率的已知方波输入,把缓冲区数据按顺序导出,看相邻采样点的时间间隔是否严格相等,间隔抖动会暴露触发源或者中断抢占的问题。第二,在回调里翻转 GPIO,测高电平占空比,确认处理耗时远小于缓冲区周期。第三,把输入接地和接参考电压,各采一段,看噪声的峰峰值和均值偏移,这两组数能同时反映参考稳定性和采样时间是否足够。

从那以后我每次配 ADC 的 DMA,都会先把CNDTR和缓冲区首地址打印出来确认一遍,再开始看波形。这一步花不到一分钟,但省掉过好几次「数据看着对其实错位」的返工。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 8:46:58

STM32G431多通道ADC采集:采样时间、DMA与溢出排查

简介&#xff1a;面向STM32G431RBT6的ADC采集工程包&#xff0c;适合具备一定HAL库基础的嵌入式开发者、竞赛学生与课程设计者&#xff0c;用于理解直接读取与DMA搬运两种采样路径的差异。包内共1254个文件&#xff0c;以C源码和头文件为主体&#xff0c;辅以汇编启动文件、工程…

作者头像 李华
网站建设 2026/10/26 8:46:58

专科生AI论文高效检索与阅读工具指南

1. 专科生如何高效获取AI论文资源作为一名在人工智能领域摸爬滚打多年的从业者&#xff0c;我深知专科生在学术资源获取上的困境。今天要分享的这个工具&#xff0c;正是为了解决这个痛点而生——千笔写作工具&#xff0c;一个专为专科生设计的AI论文资源平台。记得我刚入行时&…

作者头像 李华
网站建设 2026/10/26 8:46:57

LoRA微调技术:大模型高效适配的突破性方案

1. LoRA微调技术概述 LoRA&#xff08;Low-Rank Adaptation&#xff09;是当前大模型微调领域最具突破性的技术之一。我在实际项目中验证过&#xff0c;相比传统全参数微调&#xff0c;LoRA能减少90%以上的显存占用&#xff0c;同时保持95%以上的模型性能。这项技术的核心思想是…

作者头像 李华
网站建设 2026/10/26 8:46:41

Janus-Pro-R1:视觉理解与生成协同的强化学习框架

1. 项目概述&#xff1a;Janus-Pro-R1的视觉协同革命 2025年NIPS会议即将发布的Janus-Pro-R1项目&#xff0c;正在重新定义机器视觉的理解与生成边界。这个代号源自罗马双面神的名字&#xff0c;暗示着系统同时具备观察世界和创造内容的能力。不同于传统单向的计算机视觉模型&a…

作者头像 李华
网站建设 2026/10/26 8:46:41

AI权限管理:风险、误区与最佳实践

1. 当AI获得过高权限时会发生什么 最近三年&#xff0c;我参与过47家企业AI系统的安全审计工作&#xff0c;其中32家都存在AI权限过大的问题。最典型的案例是某电商平台的智能客服系统&#xff0c;原本只被授权访问客户服务数据库&#xff0c;却因为一个配置错误获得了整个用户…

作者头像 李华
网站建设 2026/10/26 8:44:57

WinHex定位FAT32文件首扇区:从MBR/DBR到起始簇号换算

简介&#xff1a;这是一份面向IT运维、数据恢复与安全分析初学者的PPT课件&#xff0c;聚焦用WinHex查看磁盘底层数据并定位文件首个扇区。内容围绕MBR与分区表、DBR、FAT32根目录及目录项展开&#xff0c;串联分区起始扇区、FAT表位置、簇号与扇区号换算等关键概念&#xff0c…

作者头像 李华