☰
STM32H743多通道ADC采集:定时器触发+DMA链路配置实战
2026/10/21 9:21:13 网站建设 项目流程

做多通道ADC采集,我最开始用的办法特别笨:while(1)里轮询,读完一个通道再切下一个。通道少、频率低的时候还能忍,频率一旦拉到几十k赫兹,CPU 基本上就干不了别的活了,采样间隔还抖得厉害。后来在一个STM32H743项目里改用定时器触发ADC + DMA搬运这套标准打法,配合CubeMX把链路一次理顺,整个系统干净了很多。这篇文章就把我这次配置的过程、思路和踩过的坑完整过一遍,适合正在做传感器采集、电力波形监控、电机电流采样这类多通道高速采集的朋友参考。

1. 从软件轮询到定时器触发:这套链路到底解决了什么

很多人一开始做 ADC 采集,都是这么干的:调用HAL_ADC_Start()启动一次转换,然后HAL_ADC_PollForConversion()死等转换结束,再读数据。单通道、几十Hz 的场合完全没问题。可一旦进入真正的工程场景,这套做法的三个短板会立刻暴露。

1.1 软件轮询的三个痛点:等待、丢触发、抖动的采样间隔

第一个痛点是CPU 被白白耗在等待里。ADC 转换本身很快,但转换结束之后的搬运、通道切换、下一次启动,全都需要 CPU 插手。多通道意味着这个循环要被重复很多次,采集期间 CPU 几乎无法响应别的任务。

第二个痛点是采样间隔抖动。软件触发的时间点取决于前一次转换什么时候结束,而前一次转换结束又可能被中断、任务调度、总线仲裁延迟。你本来想让每 10us 采一个点,实际采出来的点间隔可能是 10.2us、9.7us、10.5us 这样跳来跳去。做波形分析、FFT 这类对等时间间隔敏感的应用,这种抖动是很致命的。

第三个痛点是高频率下容易“丢触发”。CPU 一旦忙着做数据处理,下一次HAL_ADC_Start()可能没赶上采样窗口,通道数据就不是同一时刻的,分析出来的相位关系全是错的。

定时器触发从根本上解决这些问题:采样时机完全由硬件定时器决定,CPU 只是数据的使用者,不是采集过程的参与者。配合 DMA,数据转换完自动进内存,CPU 从头到尾不用管。

1.2 定时器触发信号是怎么“一路走到 DMA”的

这套链路非常漂亮,可以看成一条完整的硬件流水线:

  1. 定时器计数溢出,通过TRGO引脚输出一个脉冲;
  2. 这个脉冲作为ADC 的外部触发源,启动一轮规则组转换;
  3. ADC 按规则组里预先排好的通道列表,一个接一个完成转换;
  4. 每个通道转换完成后,ADC 的 DMA 请求线自动发出请求;
  5. DMA 收到请求,把转换结果搬到内存里的数组。

关键点是:第 1 到 5 步全部是硬件完成的。CPU 只需要在 DMA 搬运完成的中断回调里处理数据。

这就像安排了一个定时闹钟、一个采样员、一个搬运工。闹钟一响,采样员按既定顺序把 4 个通道挨个测一遍,搬运工把测好的结果搬到仓库。老板(CPU)只需要等仓库里的货凑齐了一批,过来看一眼就行。

1.3 触发方式的横向对比

触发方式采样时刻可控性CPU 占用适用场景
软件触发一般,取决于代码执行路径高,每次转换都要等低速、非实时、调试用
外部引脚触发由外部信号决定中,仍需软件启动转换同步于外部事件
定时器 TRGO 触发精确,由定时器硬件决定极低,配合 DMA 几乎零占用等间隔采样、高频采集、多通道扫描

这里再强调一次:定时器触发不是让你不用写代码,而是让你把“什么时候采”这件事完全交给硬件。你只在初始化时设置好周期,剩下的交给定时器本身。

2. CubeMX 里四页配置必须联动着看:时钟、ADC、DMA 和 TIM

在 CubeMX 里配置时最容易犯的错,就是只盯着 ADC 那一页,把时钟、DMA、定时器当成三件独立的事。其实这四个页面是相互咬合的:ADC 时钟决定转换速度,定时器时钟决定触发频率,DMA 决定数据怎么搬,定时器决定什么时候搬。任何一个页面配错了,整套系统都跑不起来。

2.1 时钟树:ADC12 时钟和定时器时钟的来源

先说 ADC。H743 的 ADC 内核时钟不是直接来自系统主频,而是来自专门的adc_ker_ck。在 CubeMX 的 Clock Configuration 页面,你会看到 RCC 设置里有 ADC12 的时钟源选项,可以从 PLL 的输出分频得到。

这块有个非常实际的注意点:ADC 时钟不是越高越好,它有一个由芯片数据手册决定的最高上限。如果你在 CubeMX 里把 ADC12 时钟配得过高,时钟树页面会直接标红报错。不同批次、不同型号的 H743 手册上写的上限可能不一样,我习惯按保守值来配,很多例程里常见的是 50MHz 左右,但也有人按 36MHz 配。建议先查手里这颗芯片的数据手册,把adc_ker_ck配到不超过上限的值。

定时器时钟则来自 APB1 总线的定时器时钟域。H743 的 APB1 定时器时钟往往比 APB1 外设时钟高一倍,在 CubeMX 的 Clock Configuration 页面里可以直接看到。配置定时器之前,先把这组频率记下来,后面计算 PSC 和 ARR 要用。

2.2 ADC1 配置:规则组、扫描模式与外部触发边沿

进入 ADC1 配置页之后,我按下面这个顺序理思路。

先选择要用到的通道引脚。多通道采集一般用规则组,因为规则组天然支持扫描模式,而且 DMA 就是为规则组准备的最佳搭档。CubeMX 里把需要的通道勾上,比如 IN0、IN1、IN2、IN3,对应到板子上的 PA0、PA1、PA2、PA3 之类,按你自己的原理图选。

然后设置这几个关键参数:

  • Number Of Conversion:改成你的通道数,这里的数字决定了规则组里共有几个转换槽位;
  • Scan Conversion Mode:Enabled;
  • Continuous Conversion Mode:Disabled;
  • External Trigger Conversion Source:选Timer 6 Trigger Out event或者对应定时器的 TRGO 选项;
  • External Trigger Conversion Edge:Rising edge;
  • DMA Continuous Requests:Enabled。

关于连续转换和外部触发的关系,我多说一句。很多人看到 Continuous 被禁用就慌了,觉得“不连续怎么采?”其实在定时器触发模式下,“连续转换”这个选项本来就不需要开。因为定时器本身在持续产生触发脉冲,每一个上升沿都会启动一轮转换,硬件层面的“连续性”由定时器保证,而不是由 ADC 自身的连续模式保证。开了连续转换反而会把外部触发源晾在一边,行为不符合预期。

每个 Rank 对应的通道和采样时间也要在这里配好。采样时间不是越大越好,也不是越小越好,后面第 4 章单独展开。

2.3 DMA 配置:Circular、Half Word、缓冲区大小

在 ADC1 配置页下方的 DMA Settings 标签页里,Add 一条 DMA Request。CubeMX 会自动帮你选好 DMA 控制器和 Stream,不建议手动改它的映射。需要注意三个参数:

  • Mode:Circular。这是整个 DMA 配置里最关键的一项。如果用 Normal,第一轮规则组转换搬完数据之后 DMA 就停了,后面的数据永远不会更新。Circular 模式让 DMA 在搬完一轮后自动回到起始地址继续下一轮,形成环形流水线;
  • Data Width:Peripheral 和 Memory 都选Half Word。H743 的 ADC 是 12 位,转换结果存放在 16 位数据寄存器里,Half Word 正好匹配。选错了位宽,搬进来的数据要么被截断,要么把相邻数据挤在一起,一错错一片;
  • Buffer size:在代码里通过HAL_ADC_Start_DMA的第三个参数指定,这个值必须是通道数的整数倍。最简单的做法是先定一个足够大的数组,比如要周期性地处理 N 轮扫描结果,就设成通道数 * N。

有一点要特别留意:DMA 缓冲区大小和 CubeMX 里的 DMA Settings 关系不大,它是在HAL_ADC_Start_DMA()调用时以参数形式传进去的。CubeMX 只负责配置 DMA 的通道、优先级、方向这些硬件属性。

2.4 定时器配置:用 PSC 和 ARR 算出目标采样率

定时器在这里扮演的角色很纯粹,就是一个脉冲发生器。基本定时器 TIM6、TIM7 没有输出引脚,专干内部触发这种事,非常适合。选择 TIM6 之后,配置就两件事:内部时钟源和TRGO 事件。

在 TIM6 的配置页里,Trigger Output (TRGO) Event选Update Event。这样定时器每一次溢出更新,都会在 TRGO 线上产生一个脉冲。

然后是分频参数。目标采样率、定时器时钟频率、PSC、ARR 四者的关系是:

采样率 = 定时器时钟频率 / ((PSC + 1) * (ARR + 1))

举个例子。假设定时器时钟是 200MHz,想得到每通道 50kHz 的采样率,也就是触发周期 20us:

(PSC + 1) * (ARR + 1) = 200MHz / 50kHz = 4000

可以选择 PSC = 39,ARR = 99,这样每 100 个计数脉冲溢出一次,配合 40 分频,正好 4000 个时钟周期产生一次更新事件。也可以是 PSC = 199,ARR = 19,数学结果相同,但前一种配合对定时器可靠性更好,因为计数器溢出间隔更长,省去频繁中断带来的总线压力。

注意:PSC 和 ARR 的具体数值必须结合你自己的时钟树频率重新算,不要照搬我的例子。确认一下 CubeMX 里显示的定时器时钟频率,再决定分频系数。

2.5 一个能直接用的配置参考表

我整理了一份常见配置项,照着这个表核对一遍,基本不会漏:

配置项推荐设置说明
ADC12 内核时钟芯片手册允许范围内配太高 CubeMX 会标红
Scan Conversion ModeEnabled多通道顺序转换
Continuous Conversion ModeDisabled由定时器触发保证连续性
External Trigger SourceTIM6_TRGO看 CubeMX 里的具体名称
External Trigger EdgeRising edge每次溢出上升沿触发一轮
DMA Continuous RequestsEnabled配合环形 DMA
DMA ModeCircular搬完自动循环
DMA Data WidthHalf Word匹配 12 位 ADC 结果
TIM6 TRGO EventUpdate Event溢出作为触发脉冲

3. 代码生成之后还有三道工序:校准、启动顺序、缓冲区选址

CubeMX 做完之后会生成一堆MX_xxx_Init(),很多人以为在main()里把这些函数调用起来,采集就能跑了。实际上在 H743 上,还有三道工序是 CubeMX 不会替你做的,少一步都可能让系统看起来“完全没数据”。

3.1 H743 的 ADC 校准:我为什么是在这里补的

STM32H7 系列和老的 F1/F4 不一样,ADC 上电后必须做一次校准,否则转换结果会有明显偏差,甚至乱跳。CubeMX 生成的初始化代码里并不会自动调用校准函数,需要你在启动 ADC 之前手动补上。

校准代码大概是这样的:

if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED) != HAL_OK) { Error_Handler(); }

这段代码必须放在HAL_ADC_Start_DMA()之前。如果你用的是差分输入模式,第二个参数要改成对应的差分选项;单端采集用ADC_SINGLE_ENDED就好。

值得多提一句的是:校准依赖稳定的 ADC 时钟。如果你在时钟树里把 ADC 时钟配得很奇怪,比如还没稳定就进入校准流程,校准可能一直超时卡住。遇到HAL_ADCEx_Calibration_Start返回超时,优先回头检查时钟配置。

3.2 启动顺序:先 ADC 的 DMA,再让定时器跑起来

初始化代码生成后,主函数里的启动顺序我固定这么写:

HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_data, ADC_CH_NUM * ADC_SAMPLE_NUM); HAL_TIM_Base_Start(&htim6);

顺序的逻辑是这样的:先让 ADC 和 DMA 进入待命状态,再启动定时器产生触发脉冲。反过来如果先启动定时器,第一个触发脉冲到来时 DMA 可能还没有完全准备好,这个触发就白丢了。虽然丢了之后第二个触发脉冲还能继续工作,但总归不是一个严谨的时序。

注意这里用的是HAL_TIM_Base_Start(),不要画蛇添足去开定时器中断。定时器在这里只需要输出 TRGO 脉冲,不需要中断参与。开不开中断完全是另一件事,我们的链路里没有它发挥的空间。

3.3 缓冲区要放在 DMA 够得着的地方:H7 的 RAM 分域

这个坑是我在 H743 上新踩的,值得单独拿出来讲。H7 的内存和以前 F 系列很不一样,它分了好几个 RAM 域。默认的0x20000000这段是 DTCM,CPU 核心访问它非常快,但问题是DMA 访问不到 DTCM。

如果你的 DMA 缓冲区是一个普通全局数组,编译链接后恰好被丢进 DTCM 地址段,DMA 搬进搬出都完不成,表现就是采集回来的数组永远是全 0 或者老数据。很多人查了半天 ADC 参数、DMA 配置,最后才发现是缓冲区地址选错了。

解决办法是给缓冲区指定到 DMA 可以访问的 RAM 域,比如 AXI SRAM(0x24000000)或者 D2 域的 SRAM(0x30000000)。不同的链接方式和 IDE 写法不太一样,我用的方法是给缓冲区声明加一个 section 属性,让它落到 AXI SRAM 段:

__ALIGN_BEGIN static uint16_t adc_data[ADC_CH_NUM * ADC_SAMPLE_NUM] __ALIGN_END;

如果你的链接脚本里没有可用 section,就手动在工程里增加一个 RAM 区域,然后把数组丢进去。检测方法也很简单:打印出adc_data的地址,看看是不是在0x24000000或0x30000000附近。如果地址落在0x20000000段,说明踩坑了。

3.4 回调里只置标志位,别做数据处理

DMA 搬完一轮数据,会触发HAL_ADC_ConvCpltCallback。我的习惯是在里面只做两件事:翻一个 GPIO、置一个标志位。

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { HAL_GPIO_TogglePin(DEBUG_GPIO_Port, DEBUG_Pin); data_ready = 1; } }

主循环里等data_ready,处理完一批数据之后再清掉这个标志位。为什么不在回调里直接算电压、做滤波?因为回调运行在中断上下文,如果干活太多,会影响整个系统的实时性。尤其是后面要做 FFT、数字滤波这类重活,就更不能在中断里做。

4. 实测数据错位与频率对不上:我的排查顺序

配置完成、代码跑起来之后,麻烦才刚刚开始。前面只是把链路搭通,链路通不通、数据对不对,是另一回事。我习惯按固定顺序排查,这个顺序能省掉大量无意义的时间。

4.1 先用翻转 GPIO 验证触发链路通没通

第一步永远是验证“有没有在被采集”。最简单的方法就是回调里翻转一个 GPIO,用示波器或者逻辑分析仪看这个引脚的波形。如果引脚在稳定翻转,说明 DMA 中断链路已经通了;如果完全没动静,先别急着分析数据,回过头查启动顺序和 DMA 配置。

频率对不对也很容易判断。设置触发周期是 20us,那么 GPIO 翻转的周期应该接近 40us,因为每次回调翻转一次,高低电平各对应一轮转换完成。看到波形频率和预期差了一个数量级,大概率就是定时器分频算错了,或者 ADC 时钟没按设想来。

4.2 通道数据错位:Rank 顺序、DMA 位宽、缓冲区大小

链路通了,接下来面对的是数据错位问题。数组第一个元素照样有值,但拿万用表一测,第一个元素对应的却不是你以为的那个通道。

这类问题我排查的顺序很固定:

  1. 先看CubeMX 里的 Rank 配置。DMA 是按 Rank 顺序把数据搬进数组的,Rank 1 对应adc_data[0],Rank 2 对应adc_data[1],以此类推。如果你觉得数组第 0 个是 PA0,但 CubeMX 里 Rank 1 选的却是 PA1,那数据错位就是必然的。这一条能解决九成以上的错位问题;
  2. 再看DMA 位宽。如果 Peripheral 选成了 Word 而数组是uint16_t,DMA 会把相邻数据拼到一起,表现成“每个数都大得离谱”或者“全是乱码”;
  3. 最后看缓冲区大小是不是通道数的整数倍。如果大小不是整数倍,DMA 每次搬运会丢弃最后一个半字,长期跑下来数据整体偏移。

我特别建议调试时把前两路通道分别接到 GND 和 3.3V,打印原始值,正常情况下应该分别接近 0 和 4095。这个“接地验证法”能非常快地暴露接线问题和地址问题。

4.3 采样周期与触发周期之间的博弈

即使链路通了,还有一个被很多人忽略的硬件约束:ADC 在一轮规则组扫描完成之前,不能响应下一个触发脉冲。如果你的定时器触发周期比整轮扫描总时间还短,ADC 就会溢出报错,数据开始乱。

单通道转换时间可以估算:

单通道转换时间 = (采样周期 + 固定转换周期) / ADC时钟频率

12 位分辨率下,固定转换周期一般是 8.5 个 ADC 时钟周期。假设采样周期配成 2.5 cycles,ADC 时钟 50MHz:

单通道转换时间 = (2.5 + 8.5) / 50MHz = 220ns

4 个通道一轮扫描就是约 880ns。看起来很快,但要注意如果某个通道因为源阻抗高,把采样周期拉大到 64.5 cycles,单通道时间就变成:

(64.5 + 8.5) / 50MHz = 1.46us

4 个通道就接近 5.84us,对应的最大触发频率只有约 170kHz。这时你如果还按原来的 50kHz 设计定时器当然没问题,但想提升到 500kHz 就会出问题。

所以配一组新参数时,一定先算一下整轮转换时间,再回头定定时器周期。给定时器触发周期留 10%~20% 的裕量,避免温度变化、时钟漂移导致临界冲突。

现象可能原因排查办法
GPIO 不翻转DMA/定时器未启动或启动顺序反了检查启动顺序、校准是否超时
数组全 0缓冲区落在 DTCM,DMA 访问不到打印缓冲区地址,改到 AXI SRAM
数据乱序Rank 顺序和预期不一致用一路 GND、一路 3.3V 实测验证
高频时数据跳变触发周期小于一轮转换总时长按公式核算转换时间,加大定时器周期
首帧数据异常校准后首个序列尚未稳定丢弃前几轮数据,从第 N 轮开始处理

5. 留几个进阶玩法和一点个人体会

基础链路跑通之后,这套方案还有很多可以往上加的东西,取决于你的项目到底想要什么。

5.1 乒乓缓冲与半转移回调

如果一轮一轮采集、一轮一轮处理已经满足需求,可以不用折腾。但如果你希望 CPU 在处理上一轮数据的同时,DMA 还能继续往另一边搬新数据,那就是经典的乒乓缓冲。

STM32 的 DMA 在循环模式下有半转移中断,数据搬到缓冲区一半时会触发HAL_ADC_ConvHalfCpltCallback。通过这个机制,可以把缓冲区看成前后两段:CPU 处理前一半数据时,DMA 往后一半写;CPU 处理后一半时,DMA 往前一半写。处理流程在时间上完全并行,不会出现“为了读数据把 DMA 停掉”的尴尬。

我的做法是把缓冲区大小设为通道数 * 2 * 采样轮次,然后在两个回调里各自置位,主循环根据当前处于哪一段决定处理哪一半。代码逻辑不复杂,但对实时性的提升非常明显。

5.2 想真正同步采样:双 ADC 模式

注意一个前提:定时器触发单 ADC 规则组,通道之间的采样不是同时的,而是串行的。一次触发之后,ADC 按 Rank 一个一个采。如果四路信号是几乎同时变化的波形,这种串行采集会引入一个最多几十微秒甚至几百纳秒的时间偏移,具体取决于转换时间。

如果你要求多个通道在同一个时刻采样,规则组模式满足不了,需要双 ADC 同步模式。H743 有多个 ADC,可以配置成同步规则采样,两个 ADC 在同一时刻各自采各自的通道。配置思路和这套链路本质相同,只是触发源和 DMA 数据结构要重新设计,复杂度上一个台阶。

5.3 测量精度相关的几个隐藏点

做采集项目,数据准不准往往不只是触发方式决定的。

参考电压 VREF 是第一个重点。H743 的 ADC 是 12 位,假设 VREF 是 3.3V,满量程 4095,那么 1 个 LSB 大概是 0.8mV。如果你的 VREF 本身带了 20mV 纹波,低 5 位数据基本就是噪声。要求高的项目,我会单独用高精度基准芯片给 VREF 供电,而不是直接接板上的 3.3V。

第二个重点是采样时间与信号源阻抗的关系。ADC 内部是一个采样电容,源阻抗越高,充电时间越长。如果你用高阻分压器给 ADC 做输入,采样时间给太短,电荷还没充满,转换值会偏低。此时宁可在 CubeMX 里把采样时间调大,牺牲一点采样率上限,换取准确度。

第三个是缓冲走线。长走线加高源阻抗,很容易在采样瞬间产生电压跌落。一般我会在 ADC 引脚附近加一个 100nF 电容做电荷池,效果立竿见影。

5.4 个人体会:这套方案适合什么、不适合什么

就我自己的实践感受来说,定时器触发 + DMA 这套方案最适合的场景是:通道数固定、采样率固定、数据以批次为单位周期性产生。不管是三相电流采样、多路温度巡检还是振动信号采集,只要满足这个特征,这套方案就是最省 CPU 的。

它不适合的场景是:采集频率非常低(比如每秒一次),或者通道数随时会变。低频率下定时器触发反而要处理 16 位定时器溢出问题,为了每秒一次单独开一个定时器有点小题大做;随时变通道数则会让 DMA 缓冲区和 Rank 配置不停跟着改,维护成本高。

最后再分享一个小技巧。我在调试整套链路时,会专门留一路 GPIO 做“心跳引脚”,初始化代码里让它翻转一次,启动采集后回调里再翻转一次。从示波器上能同时看到触发周期和实际采集周期,一眼就能判断出到底是配置没生效,还是 DMA 搬运出了问题。这个习惯让我省了非常多定位问题的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询