简介:面向STM32F429嵌入式开发者的ADC+DMA例程,基于HAL库实现高效模拟量采集,适合学习单片机外设配合DMA传输的工程师。工程共297个文件、约12.05MB,其中包含92个.h头文件与80个.c源文件,覆盖标准外设驱动和HAL库配置;同时带有uvprojx工程文件、hex可执行文件及编译中间文件,便于直接打开工程、对照源码和烧录验证。已有169人学习,适合正在研究多通道采样、低CPU占用数据搬运或中断处理流程的读者。资源从ADC初始化、通道配置到DMA启动与数据接收均有完整代码,可帮助理解HAL_ADC_Start_DMA的调用链路,并延伸到错误检查、状态读取和资源释放等实际工程问题,对开发工业数据采集或物联网传感设备有直接参考价值。 去年下半年我接了一个工业数据采集的小项目,主控选的STM32F429,需要同时采集三路模拟信号,还要求不占用CPU时间去轮询。当时第一反应就是HAL库加ADC加DMA这套组合拳。从CubeMX生成工程到最终调通,前后折腾了差不多一周,中间踩了不少坑,也积累了一些比较实用的经验。借着整理例程包的机会,我把这套基于HAL的ADC+DMA完整流程重新梳理了一遍,从设计思路、CubeMX配置、代码实现到问题排查都写清楚,希望能帮到正在用F429做采集的朋友。
这套方案解决的核心问题其实很明确:让ADC在后台连续不断地把采样结果搬进内存,CPU该干嘛干嘛,等数据到了直接拿过来用就行。对于多通道模拟量采集、音频采样、示波器这类需要高频连续采样的场景,这套组合基本是标配做法。如果你刚开始接触HAL库,或者想搞明白ADC和DMA到底怎么配合,这篇内容应该能给你一个比较完整的参考。
1. 整体设计思路与方案选型
1.1 为什么选择HAL库而不是标准库
很多从STM32F103时代过来的老工程师习惯用标准库,寄存器操作写在明面上,逻辑也直白。但F429甚至更新的系列,ST官方已经把重心完全转移到HAL库和LL库上。我自己从HAL库角度做一个客观评价:封装程度高、代码可读性好,而且CubeMX能直接生成初始化代码,省去大量手写寄存器配置的时间。当然它也有一层层的结构体嵌套,刚开始看源码容易懵,不过在ADC+DMA这个场景下,HAL库抽象出来的调用接口比标准库确实省心不少。
另外HAL库的坑也不少,比如某些外设的回调函数需要自己在中断里手动调用、某些版本的库在DMA模式下有缓存一致性问题等,但这些都有固定的处理套路,后面在实例环节一一展开。
1.2 F429的ADC资源与DMA映射关系
STM32F429内部有3个独立的ADC,分别是ADC1、ADC2、ADC3,每个都是12位精度,最多支持19个通道(16个外部引脚加3个内部通道:温度传感器、VREFINT、VBAT)。ADC1和ADC2可以组成双模式同步采样,ADC3也可以加入组成三ADC同步采样,这对三相电机电流采集这类应用是很有用的特性。
DMA这块,F429的每个DMA控制器有8个数据流,每个数据流有8个通道请求映射。ADC1的DMA请求是映射到DMA2的Stream0和Stream4,如果你用的是ADC3,那对应的是DMA2的Stream1。这个映射关系在参考手册的DMA请求映射表里都能查到,但很多人第一次配置会搞错。如果CubeMX里配错了DMA数据流,ADC数据根本不会搬运,而且还不报错,只能干瞪眼。
我选ADC1+DMA2 Stream0这个组合,配置简单,逻辑也最顺。如果后续要做双ADC同步采样,再扩展到ADC1+ADC2的双DMA通道也不冲突。
1.3 连续采样vs单次采样的取舍
ADC采样的基本流程分两种:单次转换和连续转换。单次转换就是触发一次,采一个点,转换完成后停止等待下一次触发,适合低频、单点的电压读取。连续转换则是一旦启动,ADC就不停地采样、转换、更新数据寄存器,直到你主动调用停止函数。
在DMA配合下,连续转换才能发挥出真正价值。因为DMA能把每次转换结果自动送到内存缓冲区,CPU只需要定期来处理缓冲区的数据就行。如果单次转换配合DMA,每次都要重新启动一次转换,启动的软件开销反而把DMA的优势抵消了。所以我这个例程选用ADC连续转换加DMA循环模式,数据链路全程无CPU干预。
2. 核心细节解析与实操要点
2.1 时钟树配置:ADC时钟不能超36MHz
刚开始用CubeMX时容易忽略一个问题:ADC的输入时钟不是直接拿APB2时钟,而是要经过ADC预分频器。F429的APB2挂的是90MHz(如果主频跑180MHz),而ADC的时钟最高只能到36MHz,所以预分频系数最低也要4分频。
这个限制直接决定了ADC的采样速度上限。12位分辨率下,ADC的转换时间 = 采样周期 + 12个时钟周期。如果采样周期设为3周期,那么总转换时间是15个ADC时钟周期,36MHz时钟下就是约2.4Msps。如果外部阻抗较大、信号源输出能力弱,就只能把采样周期拉长,比如84周期或480周期,速度自然降下来。这个取舍要在配置前就想清楚,尤其做高速采集时,时钟树没配好,后面怎么调都白搭。
CubeMX里的具体做法是在Clock Configuration页面里,把APB2 Prescaler调到4分频以上,保证ADC输入时钟不超36MHz,然后将ADC的Clock Prescaler设为4分频,得到90/4=22.5MHz。这样既满足时钟上限,也给后续采样周期配置留了余量。
2.2 DMA核心参数:数据宽度、循环模式与内存递增
DMA配置里最容易出问题的三个参数,我逐个说。
第一个是外设数据宽度和内存数据宽度。ADC1的数据寄存器是32位的,但实际有效的只有低16位(12位精度外加对齐方式决定有效位位置)。因此外设数据宽度设置为半字(Half Word)更合理,内存端也设置半字,这样每个采样点占2字节,多少点点进去就是多少字节,不用自己算偏移。如果两边都用字(Word),内存占用翻倍,处理起来还容易把无效的位也带上。
第二个是循环模式(Circular Mode)。这个必须开。开了之后,DMA传输满一整轮缓冲区后,会自动把传输计数器重置回初始值,继续从头写数据。这个特性配合ADC连续转换,就是一套无限循环的数据采集流水线。如果不开循环模式,传输一轮就停了,ADC还在继续转换,但数据全丢。
第三个是内存地址递增(Memory Increment)。ADC外设地址始终是同一个寄存器地址,所以外设地址不递增;但内存地址要从缓冲区的第一个字节一直写到最后一个,必须开启递增。这两个递增设置是独立的,各配各的,很多人只开一个导致数据全写进同一个地址,到时候看到的现象就是缓冲区每个值都一样。
2.3 规则组与注入组的选择逻辑
F4系列ADC有两类转换通道组:规则组和注入组。规则组最多16个通道,按顺序依次转换,转换结果都进同一个数据寄存器,所以多通道时必须在DMA中断里根据通道序号去区分数据。注入组最多4个通道,优先级更高,可以打断规则组转换,转换结果各进各的寄存器,适合需要严格同步采样的场景。
对于绝大多数普通多通道采集需求,规则组就够了,配合DMA和扫描模式(Scan Mode),ADC会按配置的通道顺序一个一个转下去,DMA会按同样的顺序把结果摆进缓冲区,一一对应。需要注意是,规则组模式下,数据寄存器只有DR一个,所以如果不开启DMA,多通道数据读取会非常别扭,要不停地读DR和查状态位。DMA几乎是规则组多通道的标配。
3. 实操过程与核心环节实现
3.1 CubeMX配置完整步骤
这个例程的配置我建议全程在CubeMX里完成,手写裸寄存器太容易丢细节。第一步先选芯片型号STM32F429ZIT6,然后在Pinout视图里找到ADC1,把IN0、IN1、IN2三个引脚使能,对应的是PA0、PA1、PA2三个外部引脚。
ADC1的参数设置如下:
- Mode:Independent Mode(独立模式)
- Scan Conversion Mode:Enabled(多通道必须开)
- Continuous Conversion Mode:Enabled(连续转换)
- Discontinuous Conversion Mode:Disabled
- Number Of Conversion:3(三个通道)
- Rank配置:三个Rank分别选Channel 0、1、2,每个Rank的采样时间先都设为168 Cycles,后续根据信号源阻抗再优化
- ADC DMA Request:Enabled(打开DMA请求)
- DMA Continuous Requests:Enabled(这个选项必须开,不开的话每次转换完一轮就停)
DMA设置里,添加DMA2 Stream0,方向是Peripheral To Memory,外设数据宽度Half Word,内存数据宽度Half Word,循环模式Enabled,内存地址递增Enabled,外设地址不递增,优先级High。
时钟树部分,把主频配到180MHz,APB2分频为4,ADC预分频选4分频。如果暂时不需要这么高的主频,降到168MHz也行,只要保证ADC时钟不超36MHz就问题不大。
3.2 代码实现与初始化流程
CubeMX生成工程后,主要代码其实已经齐了,但有几个关键点要在用户代码区补上。先看初始化之后的核心启动代码:
#define ADC_BUF_SIZE 1024 __IO uint16_t adc_buf[ADC_BUF_SIZE]; __IO uint8_t adc_dma_flag = 0;主函数里启动ADC采集的调用方式:
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);这一行做完,ADC和DMA就自己跑起来了,不需要再做其他事。adc_buf就是DMA要填充的内存缓冲区,大小按需分配。如果开三个通道,缓冲区里数据的排列是ch0、ch1、ch2、ch0、ch1、ch2循环排列,每个通道占据总数三分之一的空间。
DMA传输完成一半和传输完成一整轮时,HAL库会调用对应的回调函数。在回调里做数据搬运是最省事的:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { // 一整轮DMA传输完成 adc_dma_flag = 1; } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { // 一半传输完成,可以做双缓冲处理 } }主循环里检测标志位,然后计算有效值或做滤波。
3.3 数据读取与滤波处理
DMA把原始数据拿回来后,如果直接拿来做反馈控制,数值抖动通常比较大。我一般会在应用层做两级处理,第一级是中值滤波,每次取5个点排序取中间值,把偶尔出现的尖峰干掉;第二级再做一次滑动平均,让波形平滑。
uint16_t get_median_filtered_value(uint32_t channel_index) { uint16_t sample[5]; uint16_t temp; for(int i = 0; i < 5; i++) { sample[i] = adc_buf[channel_index + i * 3]; if(i > 0 && sample[i] < sample[i - 1]) { temp = sample[i]; sample[i] = sample[i - 1]; sample[i - 1] = temp; } } return sample[2]; }注意这个排序只是简化版的气泡排序思路,实际使用中数据量大时建议用更好的排序算法。ADC多通道下,通道数据是交错存放的,取通道值时索引一定要按通道数对齐,这上面栽过跟头。
3.4 数据校准与多通道扩展
F429的ADC本身有校准功能,HAL库也提供了校准函数。启动ADC之前建议先调用一次校准:
HAL_ADCEx_Calibration_Start(&hadc1);校准能消除内部电容阵列的偏差,直接关系到最后转换结果的精度。如果对精度要求高,比如工业仪表这种,还需要自己做两点校准,用万用表实测几个电压点,然后做线性拟合,把增益误差和偏移误差都校正掉。HAL库不做这个事,应用层需要自己处理。
如果后续通道数不够用,可以考虑把ADC2也拉进来,两个ADC各管几路,或者组成双ADC同步采样模式。后者需要把Mode从Independent改成Dual Regular Simultaneous,DMA配置也要相应调整,F429的参考手册在这一块讲得比较详细,这里不展开。
4. 常见问题与排查技巧实录
4.1 问题速查表
我从实际调试中整理了一些高频问题,按症状、原因、解决方式列个表,方便对照排查:
| 症状 | 可能原因 | 解决方式 |
|---|---|---|
| DMA传输完成标志一直不置位 | DMA数据流选错或DMA连续请求没开 | 核对数据流映射,开启DMA Continuous Requests |
| 所有通道数据都一样 | 内存地址递增未开启 | 在DMA配置中开启Memory Increment |
| 数据只更新一次后不再变 | 循环模式未打开,或ADC连续转换未开启 | 开启Circular Mode和Continuous Conversion |
| 采集值整体偏高或偏低 | 未做ADC校准,或参考电压不准 | 先调用校准函数,必要时做两点校准 |
| 波形出现周期性毛刺 | 采样周期太短,信号源驱动能力不足 | 增大采样时间,比如从3周期改成84周期 |
| 多通道数据错位 | DMA缓冲区索引没和通道数对齐 | 检查取数据的索引算法,确保按通道交错读取 |
4.2 一个容易忽略的隐藏坑:规则组序列错误
F429的规则组通道转换顺序不是自动编号的,是通过Rank配置来定义。比如你把Rank1配成Channel 2、Rank2配成Channel 0,那转换顺序就是先CH2再CH0,和引脚编号完全无关。有一次我就是Rank顺序和预期不一致,导致采集回来的三路信号对不上号,检查半天才发现是CubeMX里Rank配置拖拽错了顺序。
所以多通道配置完成后,一定要回CubeMX的ADC1配置页里,逐个Rank确认Channel选择是否正确,顺序是否和预期一致。
4.3 DMA回调里做的事尽量少
HAL_ADC_ConvCpltCallback是在DMA中断上下文里被调用的,这个中断的频率不低,如果缓冲区是1024点、采样率2Msps,那么一轮传输完成就有约2毫秒一次的中断回调。在这个回调里如果去做耗时操作,比如打印日志、阻塞式发送串口、动态分配内存,很容易把中断拖死,甚至触发HardFault。
正确的做法是:回调里只设置标志位,最多做一次内存拷贝,把数据搬到应用缓冲区,然后立刻退出。后续的数据处理全部放到主循环或低优先级任务里做。
4.4 在线调试的小技巧
调试ADC+DMA时,我强烈建议先别急着上FreeRTOS或者复杂算法,把采集链路单独拉出来验证。用调试器在HAL_ADC_ConvCpltCallback里打一个断点,或者看adc_dma_flag是否置位,就能确认DMA链路是否通了。然后用调试器变量窗口周期性刷新观察adc_buf数组,检查数据变化是否有规律。如果adc_buf所有数值都是4095或者0,大概率是引脚配置和实际接线对不上;如果数值在正常范围内但跳变剧烈,就要考虑采样周期是不是太短或者信号源阻抗过高。
另外在用ST-Link调试时,如果开启了实时变量查看,DMA写入内存的频率很高,调试器读取内存的操作可能会影响采样时序。真遇到诡异问题,可以断开调试器,让程序独立跑,再用串口把数据发出来对比,排除干扰因素。
5. 实测效果与调参经验
5.1 不同采样时间下的表现差异
我把三路信号分别接到PA0、PA1、PA2,用信号发生器输出1kHz正弦波、直流偏置2V方波和三角波。第一次全部配置为3周期采样时间,采集回来的1kHz正弦波没有明显失真,但方波边沿位置有过冲抖动。把采样时间改到84周期后,波形明显变平滑,过冲消失,但最高采样率下降不少。如果信号频率低,不需要高速采样,用长采样时间换稳定性,是性价比很高的选择。
对于1kHz级别的信号,168周期采样时间下波形已经非常平滑了。实测10kHz以内信号,168周期完全够用。只有上到50kHz以上时才需要考虑缩短采样时间。
5.2 参考电压对ADC精度的影响
F429的VREF+引脚如果直接接3.3V,那ADC满量程就是3.3V。但3.3V往往来自板载LDO,精度和温漂都比较一般。对12位ADC来说,参考电压每偏差10mV,满量程读数就会偏差约12个LSB左右,这个误差对高精度测量是一个不可忽略的量。
讲究一点的方案是外部用专用参考电压芯片,比如REF3030这种输出3.0V的器件,然后软件上按实际参考电压去做归一化。如果手头没有外部基准,至少要用万用表实测一下板子上的3.3V实际值,在计算物理量时用实测值,不要直接用3.3这个名义值。
5.3 采样率与DMA中断频率的匹配
假设ADC时钟36MHz,采样周期84周期,12位分辨率下每个点转换时间是84+12=96个ADC时钟周期,约2.67微秒,理论采样率约375ksps。如果三通道扫描,一轮三个点耗时约8微秒,一轮采样率约125ksps。DMA缓冲区1024点,一轮传输完成中断约8.2毫秒一次,这个频率在中断里做标志位操作完全没压力。
如果采样周期降到3周期,单通道理论采样率能到2.4Msps,三通道扫描约800ksps,一轮1024点完成中断大概1.28毫秒一次。这种情况下,中断回调只做标志位,主循环里忙不过来可能会丢数据。这时候要考虑加大缓冲区,或者用双缓冲DMA(Half Transfer中断+Full Transfer中断),配合乒乓切换,CPU处理就有充足时间。
6. 例程包使用说明与扩展思路
6.1 例程包文件结构
压缩包里我放了这几个文件:
ADC_DMA.ioc:CubeMX工程文件,直接双击可以打开重新配置Core/Src/main.c、adc.c、dma.c:核心代码README.md:快速上手说明和接线图Doc/:包含STM32F429参考手册ADC相关章节和HAL库ADC驱动源码注释
拿到例程包后,先打开ioc文件看一下配置,确认和自己板子上的引脚是否一致。F429有很多封装,引脚排列不一样,如果板子上的ADC引脚不在PA0-PA2,需要重新分配引脚。CubeMX里改引脚非常方便,改完重新生成代码就行。
6.2 扩展方向一:多通道与多ADC协同
如果采集通道超过3个,可以在ADC1的Number Of Conversion里增加到16个通道,但扫描一轮的时间会变长,通道间切换也有额外开销。更合理的做法是ADC1管前8路、ADC2管后8路,两个ADC并行采样,DMA分别配置到两个不同缓冲区,或者用双ADC模式做交叉采样提高单通道采样率。
F429的双ADC模式里有一种叫交替模式(Alternate Trigger),可以让ADC1和ADC2交替采同一个通道,等效采样率提升一倍。对高速信号采集,这个模式值得深入研究。
6.3 扩展方向二:定时器触发ADC采样
连续转换适合匀速采样,但有些场景希望以精确的时间间隔采样,比如工频信号每周期采128个点、电机电角度每转采固定点数。这时候用定时器触发ADC就对了。在CubeMX里把ADC触发源改成Timer Trigger Out事件,同时配置一个定时器输出PWM或Update事件,ADC就会在定时器触发沿到来时才启动一次转换,DMA照样在后台搬数据,精度由定时器时钟保证,和主循环调度完全无关。
我自己做电机相电流采样就是用的这种方案,触发源用PWM中心对齐模式的Update事件,精确到纳秒级别,效果很稳。
6.4 扩展方向三:缓存一致性问题的注意点
如果用的是F4系列不带D-Cache,不存在缓存一致性问题。如果换了带D-Cache的芯片,比如F7、H7系列,DMA写入的内存如果被CPU读取,可能会出现读到的还是旧缓存数据的情况。解决套路是:要么DMA缓冲区放到非缓存的SRAM区,要么在CPU读数据之前调用SCB_InvalidateDCache_by_Addr,把对应内存区的缓存失效一下。
这个坑在F429上不用操心,但如果有人照着这个例程移植到H7上,一定要提醒他们注意,不然会莫名奇妙地读到旧数据。
说了这么多,我觉得这套ADC+DMA的玩法核心就两个:一个是把CubeMX里面的配置点全部理解透,另一个是理解DMA中断和主循环之间的数据流关系。把这两块吃透了,不管是F429还是其他F系列,配置思路完全通用。例程包里也留了一些调试用的辅助函数,比如把缓冲区的原始数据直接通过串口打印出来的工具,实测对排查问题非常有帮助。如果你的F429板子已经到手,建议直接烧进去跑一下,把三路信号接上,马上就能看到效果。
本文还有配套的精品资源,点击获取