【STM32 + CubeMX】 ADC 多通道采集配置与代码实现 -- F407篇
本文介绍了基于STM32F4的ADC多通道采集实现方法。通过CubeMX配置ADC模块,采用DMA循环传输模式,实现3路模拟电压信号的自动采集与转换。文章详细讲解了ADC的基本原理、STM32内置ADC的特性(12位分辨率、0-3.3V输入范围、最高2.4MSPS采样率),并给出了CubeMX的具体配置步骤,包括通道选择、DMA参数设置和采样时间调整。代码实现部分重点展示了如何通过数组缓存和HAL
本文使用STM32F407作示范,通过 CubeMX 配置ADC多通道 + DMA传输, 然后, 代码读取转换值 !实现多路模拟电压信号的采集,最后通过串口将结果输出到电脑端显示。
一、什么是ADC
ADC(Analog-to-Digital Converter,模数转换器),是一种将外部连续的模拟信号(通常是电压)转换为单片机可处理的数字信号的关键模块。它是嵌入式系统与现实物理世界交互的重要桥梁。
简单来说,ADC 的核心功能是对电压进行精确测量。
示例说明1:电池电量检测
手机在运行过程中需持续测量锂电池的电压,并根据电压变化按一定比例估算剩余电量,从而实现电量显示与节能管理。
示例说明2:光敏自动控制
通过光敏电阻电路可将环境光线强度转换为电压信号(光线越弱,电压越高)。ADC 实时采集该电压,并将其转换为数字值(如 0~4095)。
程序通过判断该数值是否超过设定阈值,即可触发相应操作,例如:“环境变暗,自动开启灯光”。
这类应用体现了ADC 的核心作用:将现实世界中的模拟信号转换为可编程处理的数字信号,实现基于物理状态的自动化响应。
二、STM32 内置ADC 概述
对于高精度信号采集(如人体生物电信号),通常需采用独立的ADC芯片与运放搭建前端电路,以满足其在噪声、分辨率和动态范围等方面的严格需求。
而在一般精度应用场合(如小车电池电压检测或电子秤),STM32等微控制器内置的ADC模块已具备足够性能,能够满足常规项目的测量要求。
下文将针对STM32内置ADC模块进行介绍。
1、电压采集范围
- 有效输入范围:0 ~ 3.3V
- 更准确地说,应满足:VREF- ≤ VIN ≤ VREF+。在大多数的设计中,VREF- 接地 (0V), VREF+接3.3V。
请特别地注意:
ADC的调试,是除了电机外,最容易烧芯片的操作之一!
绝对不能直接采集 0~3.3V 以外的电压 ! 负1V不行、正5V也不行,必须0~3.3V。
负电压: 搭建外部电路,把负电压抬升至0~3.3V
高于3.3V: 搭建分压电路,把高于3.3V的电压降至0~3.3V范围
微弱信号:如uV、mV级的人体信号,搭建运放电路,放大数百倍+滤波,把满幅量程靠近3.3V。
2、 分辨率与精度
- 分辨率:12位 ; 输出数字量范围 0~4095;
- 精度:实测误差通常在 0.5% 左右。实际精度受供电质量、PCB 布局、参考电压稳定性等因素影响。
3、转换速度
- 最高采样率:2.4 MSPS(每秒百万次采样)。该速度是在ADC时钟配置为36MHz时实现的。
- 很多新手总爱瞎怼采样率,觉得越高就越“实时”。其实对大部分项目,ADC 每秒采样百次左右,已经绰绰有余——你得平衡单片机那紧张的运行资源:代码处理得过来,响应也完全跟得上,够用,稳定,才是正经。
4、通道与引脚
- 19个通道;可测量16个外部信号 + 3个内部信号源(温度传感器、内部参考电压等)
- 大部份STM32芯片,通常包含 3 个 ADC 单元(ADC1、ADC2、ADC3)
- ADC1 、ADC2 具有相同的外部通道引脚映射。ADC3 略有不同,请以所用芯片为准。

为什么相同的引脚,要设计成三组ADC?
很多人会好奇,通道的引脚大部分是相同的,为什么要设计成两个、三个ADC?
是为了实现 :并行、高速、协同 的工作模式!
如,三相电机控制、多通道示波器、振动分析、电力系统中的谐波分析 等等。
刚玩时无需死磕,当某天做项目有了真实需求时,方能理解这种设计的意义所在。
三、使用 CubeMX 配置ADC
CubeMX 的设计初衷是减少不同芯片间的配置差异,实现"一套配置,多芯片通用"。
然而在 ADC 配置上,各型号芯片存在显著差异!
即使是同属 F 系列的 STM32F103 与 STM32F407,其 ADC 配置结构 和 DMA 配置也诸多不同,无法直接通用。
因此,本文内容仅针对 STM32F4 进行适配和验证。其他型号(如 F103、H750 等)仅能作对比参考,请勿直接套用!
3-1、新建工程
若你已有现成的CubeMX工程,打开ioc文件即可在CubeMX里进行ADC功能的添加、配置 。
如果打算新建工程进行测试,本文不啰嗦新建的过程,可参考下面文章:
另外,文中将用到 printf,把ADC采集数据,输出到电脑串口助手进行观察。
如果你的工程中,没有提前做好printf的重定向,使用printf时,程序的运行将卡死。
具体操作,可参考下面文章:
3-2、启用ADC通道
具体操作,如下图所示:
- 打勾 需要的通道,即可启用此通道。
- 若通道显示为红色,表示该引脚已被分配其它功能使用,不可选。
如下图所示,本篇以3个ADC通道为例进行配置(只要引脚空闲,您可自由选择任意可用通道)。

3-3、 DMA传输参数配置
具体操作,如下图所示:
- Mode (模式):选择 Circular,即循环模式。实现持续自动传输。
- Peripheral - Increment Address (外设地址增量):不勾选。ADC数据寄存器地址固定,无需递增。
- Memory - Increment Address(内存地址增量):勾选。使多个通道的转换数据依次存入用户数组。
- Data Width(数据宽度): Half Word(16位)。适用于F/G系列12位ADC、H系列16位ADC。

获取ADC转换结果通常有三种方式:阻塞查询、中断触发和DMA传输。
不必纠结哪种方式更好——闭眼选DMA就对了!
用DMA循环模式,能覆盖绝大多数应用场景。代码简洁、效率高,CPU占用资源极少,堪称ADC采集的“最优解”👍。
3-4、ADC 工作参数配置
具体操作,如下图所示:
- 重点关注 3个工作模式设置 + 通道数量配置;
- 通过配置Rank组的参数,可以灵活设置各通道的采样顺序、采样时间长度。

4个参数说明:
| 参数 | 功能 | 备注 |
|---|---|---|
| Scan Conversion Mode | 扫描模式 | Disable-1个通道、Enabled-多个通道扫描 |
| Continuous Conversion Mode | 连续转换模式 | Enabled:持续自动转换 |
| DMA Continuous Requests | DMA连续请求 | Enabled:使用DMA传输 |
| Number Of Conversion | 转换通道数量 | 打勾了几个通道,这里就填几个 |
Rank配置说明:
- 当在"Number Of Conversion"中设置了N个转换通道后,系统会自动生成N组Rank配置项。
- Rank 的作用:决定通道的采样顺序、设置对应通道的采样时间。
- 采样时间说明:单位:ADC时钟周期数。建议选择最大值以获得更高精度。示例:36MHz时钟下,480个周期约13us。
- ADC工作时,将按Rank顺序依次采样:先完成Rank1指定通道的采样,再进行Rank2、Rank3...,直至完成所有通道采样。
- 重要提醒:每次ADC配置时,必须手动设置每个Rank的采样时间和通道顺序参数,否则可能导致采样异常!
3-5、生成工程配置
ADC模块的配置已全部完成。
具体操作,如下图所示:
- 点击"Generate Code",生成工程代码即可。

四、代码实现 读取ADC转换结果
使用CubeMX配置完成后,读取ADC转换结果只需三步:
- 创建数组,作为DMA缓存及结果存储区:uint16_t adcValue[3]; // 示例为3个通道
- 启动ADC转换与DMA传输:HAL_ADC_Start_DMA (&hadc1, (uint32_t*)adcValue, 3);
- 直接读取数组即可获取各通道转换结果 (注意:此为原始ADC值,非电压值)
网上许多教程罗列大量函数:
HAL_ADC_Start、
HAL_ADC_Stop、
HAL_ADC_Start_IT 等......
不必纠结这些!
采用本文的 "1个数组+1个函数" 方案,虽非最优,但能覆盖绝大多数应用场景,代码简洁、稳定可靠!
1、创建缓存数组
创建一个数组,作为DMA传输的缓存区。
ADC转换完成后,DMA会自动将各通道的转换结果存入该数组。
具体操作:
- 打开main.c (或者在其它需要调用ADC功能的源文件),添加以下数组定义;
- uint16_t adcValue[3] = { 0 }; // 数组大小与启用通道数保持一致;单通道则改 1
完成后,是下图这个样子:

数组定义说明
- 数组名称:可自定义,按喜好命名即可;
- 数据类型:uint16_t。DMA配置时数据宽度是Half Word,即16位。通用12、16位ADC;
- 数组长度:通道数的倍数。本篇示范是3个通道 ,所以这里设置为3。 平均值过滤法提示:通过设置数组长度为通道数的倍数,并调整HAL_ADC_Start_DMA()的第三个参数(传输数据量),可存储每个通道的多次采样值,便于后续求取平均值,实现最简单的滤波效果。如 uint16_t adcValue[30] = {0},即为保存每个通道采样10次的结果(3通道×10次)。
2、调用函数,启动ADC与DMA传输
STM32绝大部分型号(如F1、H7 等),在启动 ADC 转换前需要调用校准函数执行校准操作,以确保采样精度。
F4 系列是一个例外。其 ADC 模块在上电后无需手动校准即可直接工作。
因此,对于 STM32F407,在完成配置后,可直接开启 ADC 及 DMA 传输:
具体操作:
- 在
main.c的合适位置(如用户代码区), 调用下行的启动函数;- HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcValue, 3);

函数说明:HAL_ADC_Start_DMA()
参数详解
- 参数1:ADC 句柄。可理解为ADC设备的操作指针,需传入结构体地址 &hadc1。
- 参数2:目标缓冲区地址。因DMA需要32位指针,需对16位数组进行强制类型转换 (uint32_t*)adcValue。
- 参数3:传输数据长度。填写转换通道数(或其整数倍)。
工作流程
调用此函数后,ADC与DMA将自动协同工作:
- ADC按Rank顺序循环采样各通道,并不断循环该过程。
- DMA 自动将转换结果搬运至 adcValue 数组。新数据持续覆盖旧值。
- 数组中数据顺序,与 CubeMX 中配置的 Rank顺序完全一致。
调用函数后,ADC进入自动工作状态,我们只需直接读取 adcValue 数组即可获取最新转换值。
4、读取转换结果
如上所述,调用 HAL_ADC_Start_DMA() 启动后,ADC转换结果会自动存入数组 adcValue[ ] 中。
需要使用时,直接读取数组对应位置的值即可。
例如要获取通道2(PA2)的转换值,只需读取 adcValue[2](数组索引从0开始)。
具体操作:
- 在main.c的while循环中添加以下代码,实现每隔0.5秒printf一次 ADC 3个通道的转换值:
/** 延时间隔 **/ HAL_Delay(1-1); /** 规律地闪烁LED **/ static uint32_t ledTimes = 0; if (++ledTimes % 500) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_5); } /** 每500ms读取一次转换结果 **/ static uint32_t adcTimes = 0; if (++adcTimes % 500 == 0) { printf("IN0(PA0) = %04d ", adcValue[0]); printf("IN1(PA1) = %04d ", adcValue[1]); printf("IN2(PA2) = %04d \r", adcValue[2]); }
编写完成后,是这样子的:

提示1:关于延时参数为何 -1?
这与系统 tick 计数方式和调度机制有关。若想深入了解,可另行搜查资料:“HAL_Delay 偏差”或“RTOS tick 补偿”。目前只需记住:“-1”能让实际延时更接近预期值。
提示2:为何要闪烁 LED?
这是一种简单高效的调试手段!通过观察 LED 是否按预期闪烁,可快速判断程序是否正常运行。若 LED 不闪或闪烁异常,往往意味着程序卡死或逻辑错误。
提示3:IN0 、PA0、adcValue[0] 有什么特殊关系吗?
纯属巧合!三者均为“0”只是配置时的偶然,并无内在关联。需注意:
-
通道与引脚的对应关系,是固定的。如 IN9 只能对应 PB1,在第二部分中已附图表;
-
通道与数组位置的对应关系,由 CubeMX 中 Rank 的设置顺序决定;
提示4:最初曾考虑使用 IN7(PA7)、IN9(PB1) 、IN12(PC2) 等通道以避免混淆,但最终仍选择 IN0、IN1、IN2!正是为了强调容易引发的混淆:引脚、通道、数组索引之间没有额外的隐藏规则,一切依赖配置。
代码已完成!
接下来,我们准备编译、烧录,检验操作效果了。
具体操作:
- 打开串口助手
- PA0,用杜邦线,接GND,即0V (预期的ADC采集结果是 0);
- PA1、PA2, 用杜邦线,接3.3V排针 (预期的ADC采集结果是 4095);
- 编译并下载程序

串口助手已经成功输出ADC采集数据!
结果也如预期所料:GND(0V)对应0值,3.3V对应着满量程的4095。
你也可以用一节干电池(5号电池、1.7V)进行测试,GND接电池负极,PA0接电池正极。
五、ADC值转换为电压值
为了数据上更直观,可以把ADC值用代码换算成对应的电压值。
由于 0V 对应ADC值 0, 满量程的 3.3V 对应ADC值4095。
那么,我们可以用公式:(ADC值÷4095) x 3.3,即可把ADC值转化为对应比例的的电压值。
具体操作:
- 在刚才写的ADC读取代码下方,增加以下代码:
float vol_0 = ((float)adcValue[0] / 4095) * 3.3; // 把通道0的ADC值,按比例计算成电压值 float vol_1 = ((float)adcValue[1] / 4095) * 3.3; // 把通道1的ADC值,按比例计算成电压值 float vol_2 = ((float)adcValue[2] / 4095) * 3.3; // 把通道2的ADC值,按比例计算成电压值 printf("IN0(PA0) = %3.2fV ", vol_0); // printf 通道0 所采集到的电压值 printf("IN1(PA1) = %3.2fV ", vol_1); // printf 通道1 所采集到的电压值 printf("IN2(PA2) = %3.2fV \r\r", vol_2); // printf 通道2 所采集到的电压值完成后,是这个样子的:

再次编译、烧录运行,串口助手输出如下:

如上图所示,已能正常输出电压值!!
六、常见问题
本节收录ADC调试中的常见问题。
欢迎留言你调试ADC时遇到的问题、排查经验。将会及时整理,追加到末,以方便后面爬坑的兄弟。
1、悬空引脚也有读数?! 是不是芯片坏了?
很多新手都会发现:明明ADC引脚悬空没接线,为什么还能读到变化的数值?
按理说不是应该为0吗?
这个问题太常见了!经常有人怀疑:“程序没问题啊,难道芯片坏了?!”
真正原因:STM32的ADC引脚悬空时处于高阻抗状态,就像一根微型天线,会不断捕捉周围的电磁噪声(比如MCU本身、电源、甚至WiFi信号),导致转换值随机跳动。这完全是模拟电路的正常特性,并非硬件故障或程序错误。
浮空引脚有读数 = 正常物理现象 ≠ 芯片损坏
实际设计电路时,建议为ADC通道预留一个下拉电阻(如10kΩ到地),避免引脚悬空时产生不可预测的数据,从而导致程序误判。
2、引脚接GND,读数为什么不是0?
调试时,若将ADC通道引脚接到板子的GND测试点,正常情况下ADC读数应为0。
但如果出现数值很小(如100以内)、波动不大的非零值,建议排查:
检查板子原理图,确认芯片的3.3V和GND供电线是否串联了保险丝。
保险丝会引入微小压降,导致参考电压(VREF+和VREF-)出现偏差:
- 若GND串联保险丝,接GND测试时ADC结果可能略大于0(如30左右)
- 若VCC串联保险丝,接3.3V测试时ADC结果可能略小于4095(如4080左右)
供电串联保险丝的设计初衷是为了保护芯片,防止在异常情况下(如外接设备短路或反接)造成永久损坏。然而,这种保护在实际开发板应用中的作用往往比较有限——因为多数情况下芯片损坏是由于多引脚外接设备操作不当或电路设计缺陷导致的,单纯依靠保险丝并不能完全避免这些问题。
更重要的是,保险丝会引入额外的阻抗和压降,直接影响ADC参考电压(VREF+/VREF-)的精度,导致即使将输入引脚接GND也无法得到绝对的0值读数。
若对ADC精度有较高要求,可考虑将保险丝更换为0Ω电阻。这样做既保留了PCB布局的灵活性(便于后续调试或更改设计),又避免了不必要的压降,同时仍具备一定的过流保护能力(0Ω电阻本身也有一定的限流作用)。
3、引脚接3.3V,读数为什么不是4095 ?!
调试时,若将ADC通道引脚接到板子的3.3V测试点,正常情况下ADC读数应为4095(12位ADC)。
如果读数明显偏小(如4080左右),可按照以下步骤排查:
- 首先按第2点方法检查VCC供电是否串联保险丝(保险丝压降会导致电压降低)
- 若确认无保险丝,则很可能是:芯片供电VCC与外部3.3V测试点并非同一路LDO供电
近年来好些新款开发板、工业板卡采用双LDO设计:
- 一路LDO专供芯片VCC
- 另一路LDO供外部3.3V网络
这种设计可有效隔离外部电路异常(如短路/过载)对主芯片的影响,提升系统可靠性。
如果检查后,发现确实是有两路LDO,用万用表测量通道所接的3.3V测试点电压,通常会略低于芯片VCC电压(如3.28V vs 3.30V),这正是ADC读数小于4095的直接原因。
因此,读数非4095 ≠ ADC故障,而可能是:
- 保险丝压降:系统误差
- 双LDO设计:电压源差异
- 实际电压值 < 3.3V:读数按比例减小
这种差异,有一修正思路:使用ADC采集内部电压参考源(通道17),用采集的结果与理论参考值 1490(1.2V) 作对比,计算成补偿因子。
更多推荐



所有评论(0)