☰
STM32G431 ADC DMA连续采集实战:CubeMX配置与多通道数据搬运
2026/10/19 12:35:56 网站建设 项目流程

简介:面向STM32G431RBT6微控制器的ADC采集工程,完整演示直接读取转换结果与通过DMA搬运数据的两种实现方式,适合嵌入式初学者和需要快速搭建模拟采样通道的工程师参考;工程基于HAL库与寄存器混合方式编写,便于理解驱动细节。压缩包约29.73MB,共1254个文件,其中C源码676个、头文件275个、汇编启动文件97个,承担ADC驱动、DMA传输与底层初始化;另有链接脚本、IAR/MDK工程文件、CubeMX初始化文件,以及编译库文件、调试映射等内容,可直接打开编译并烧录运行。此工程已有1972人学习下载,在同类驱动开发验证中具备一定示范性。工程不仅提供ADC1直接采集与ADC2通过DMA采集的完整例程,还深入涵盖多通道配置、转换序列设计、中断与DMA协同、采样率和传输效率优化等核心知识点,帮助读者理清直接读取与DMA搬运的适用边界:前者适合低速低数据率场景,后者适合高速连续采集;同时附带CubeMX初始化文件,可快速修改采样引脚和时钟,迁移到传感器监测、电源管理或音频处理等实时项目中,实现方案直接可复用。

1. 先把场景想清楚:直接采集和DMA采集到底差在哪

1.1 型号底子与ADC模块

拿到 STM32G431RBT6 这块板子,很多人第一件事就是试它的 ADC。G431 的内核是 Cortex-M4F,主频最高能跑到 170MHz,型号里的 R 代表 LQFP64 封装,B 代表 128KB Flash,T6 是工业级温度范围。这颗芯片做电机控制、数字电源、电池巡检、信号采集这类项目非常多,我最近手里的几个传感器数据采集方案也直接用上了它。

很多人只看重它的内核性能,其实 G431 的外设比 F1/F4 老系列强不少。ADC 模块除了 12 位分辨率,还支持多个转换组、硬件过采样、注入组以及内置校准。单通道转换速度在实际工程里可以做到几 Msps,和 DMA 配合后基本能把 CPU 从搬运数据的琐事里解放出来。G4 系列还带 DMAMUX,外设的 DMA 请求可以灵活路由到不同的 DMA 通道,不像老芯片那样固定映射死,想改路由还得查数据手册。

ADC 的校准功能也不是摆设。芯片出厂后,ADC 的失调值会随温度和供电电压变化,跑高精度测量前最好每次上电都做一次校准。这个动作在 HAL 库里就是一行HAL_ADCEx_Calibration_Start,但顺序错了照样会出问题,后面我会专门说。如果你以前只用过 F103 那种老芯片,刚上手 G431 时最大的感觉应该是“参数太多”:采样时间分了好几组,扫描模式、连续模式、DMA 连续请求随便组合,稍不注意就会生成一份“看起来能用、实际数据错乱”的工程。

1.2 直接模式与 DMA 模式的“分工”

直接采集,说白了就是 CPU 自己盯着 ADC。发起启动,等转换完成,再手动把结果从数据寄存器里读出来。HAL 库里的标准写法是HAL_ADC_Start加HAL_ADC_PollForConversion,最后用HAL_ADC_GetValue取值。优点是代码最短、依赖最少,适合旋钮电压检测、按键检测、慢速温度传感器这类低频场景,几十次到几百次每秒的采样完全够用。缺点是 CPU 在转换期间不能干别的,数据量一高系统就会卡。

DMA 采集就完全不同。转换结束后,ADC 会产生 DMA 请求,数据由 DMA 硬件自动搬到内存数组里,CPU 不需要一条条去读。CPU 只在缓冲区半满或满的时候处理一次数据。配合 G4 的循环模式,DMA 写满缓冲区后自动回到数组头部继续写入,等于形成了一条永不停歇的“数据传送带”。我常用一个类比:直接采集像餐厅服务员一桌一桌等客人点菜,DMA 像厨房里的环形传送带,做好的菜自己往前走,厨师只需要在固定位置取餐。

两种模式的分工差异,直接看表更清楚:

对比项直接采集DMA 采集
代码复杂度低中
适合频率低频(几十Hz~几kHz)高频连续流(几十kHz~Msps)
CPU 介入每次转换都要介入缓冲区满/半满时介入
多通道支持可以,但顺序读取繁琐自动按扫描序列写入
典型场景按钮、电位器、慢速传感器音频采样、电流波形、定时器同步采样

直接采集并不是一个“要被淘汰”的方案,它是最快的验证手段。你在新板上判断 ADC 引脚有没有接对、参考电压是否正常,直接用轮询读一个通道最方便。DMA 则适合真正需要连续采样和数据后处理的场景。选 DMA 不意味着一劳永逸,缓冲区大小、循环模式、扫描序列顺序、数据对齐,任何一项没配好,结果都可能是一堆 0 或者错位数据。

2. CubeMX 配置:给 ADC 安排一条搬运流水线

2.1 时钟树和 ADC 频率怎么选

用 CubeMX 建工程时,第一件事就是把时钟树理顺。G431 的 ADC 时钟可以取自 AHB 分频,也可以用独立的外设时钟源,具体在 ADC 参数页里的Clock Prescaler配置。我的习惯是把 ADC 时钟放到 36MHz 左右,不直接冲最高频率。ADC 时钟越高,理论上采样率越高,但外部电路阻抗稍微大一点,采样电容还没充满电就结束采样,读数反而会偏低或跳动。这种硬件层面的误差,软件滤波都很难完全救回来。

采样率计算有一个非常基础的公式:

转换时间 = 采样时间 + 12.5 个 ADC 时钟周期。

比如 ADC 时钟 36MHz,采样时间选 2.5 周期,单次转换时间就是 (2.5 + 12.5) / 36MHz,约等于 0.417μs,换算下来大概 2.4Msps。如果只是采 10kHz 的正弦波,这个速度绰绰有余。如果在后端还要开硬件过采样,有效吞吐率会明显下降,所以在 CubeMX 里面先算好再配置,免得后期发现数据量不够用。

Sampling Time不建议上来就选最短。普通传感器串一个 1kΩ 电阻进 ADC,至少选 6.5 周期以上,高阻输出还要更长。快速变化的信号配合运放缓冲输出,才能把采样时间压低。项目里如果只管追求速度,最后通常都要回来给传感器加缓冲器,这一课我踩过不止一次。

2.2 DMAMUX 与 DMA 传输方向配置

G4 的 DMA 请求是可以通过 DMAMUX 灵活映射的。CubeMX 里添加 DMA 通道时,把请求来源选成 ADC1,方向选Peripheral To Memory。这一步看着简单,但经常有人选错请求号,导致 ADC 转换完成信号根本没接到 DMA 上,后期查半天都是白查。

DMA 参数里还有一个最容易被忽略的开关:DMA Continuous Requests,中文界面里有的版本叫“DMA 连续请求”。要做连续采样,这个选项必须开。不开的话,DMA 只在第一次规则转换结束后响应一次,后面 ADC 还在转,DMA 却一动不动。开了它以后,只要 ADC 不断产生结果,DMA 就不停搬运,配合循环模式就是我前面说的传送带效果。很多“只有第一批数据、后面全不变”的问题,根源就是这个开关没打开。

数据宽度也要提前定好。G431 的 ADC 结果是 12 位,外设侧选Half Word,内存侧也选Half Word,然后定义一个uint16_t数组。选 32 位不是不能跑,但内存占用翻倍,处理时还容易把高 16 位当成有效数据。我见过有人调试时一直看到很大的数值,其实就是因为数组用的uint32_t,低 12 位是 ADC 值,高 16 位是垃圾,看起来像数据异常。

2.3 连续模式、扫描模式与触发方式

ADC 规则组里,ContinuousConvMode控制是否连续转换。单通道时开启它,ADC 会不停转换同一个通道;关掉就是转一次停一次,需要再次手动触发。ScanConvMode控制是否多通道扫描,开启后 ADC 会按照 Rank 顺序把序列里的通道轮流转一遍。

这里有个容易混的地方:扫描模式和连续模式不是互斥的。扫描模式管的是“这一轮是否把通道 1、2、3、4 都转完”,连续模式管的是“转完一轮后是否立刻开始下一轮”。只开扫描,不开连续,那就是把序列转完一轮之后停下来,下一次启动再转一轮。同时打开,DMA 就会一直接收多通道数据,缓冲区里会不断写入 Rank1、Rank2、Rank3、Rank4、Rank1、Rank2……非常有规律。

触发方式我默认选软件触发,ExternalTrigConv设为ADC_SOFTWARE_START。做电机控制的同学会更熟悉定时器触发,比如用 PWM 的中心对齐信号触发 ADC,让电流采样点固定落在开关管导通的中间时刻。这种场景在 CubeMX 里要把触发源改成定时器输出,但 DMA 搬运和回调处理的做法完全一样。最开始验证功能时,建议先保持软件触发,调通了再换硬件触发,排查问题会更快。

3. HAL 层代码实战:从单通道到多通道

3.1 初始化结构体拆解

直接从 CubeMX 生成的代码会带一堆配置项,我简化出一份核心配置方便看逻辑:

ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc1.Init.LowPowerAutoWait = DISABLE; hadc1.Init.LowPowerAutoPowerOff = DISABLE; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLINGTIME_COMMON_1; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } }

两个重点说一下。一是ClockPrescaler必须和时钟树配套,改了时钟树后回去看 ADC 参数。二是Overrun我习惯设成ADC_OVR_DATA_OVERWRITTEN,连续采样场景里宁可用新数据覆盖旧数据,也不要把整组数据停在这里。EOCSelection在单通道时用ADC_EOC_SINGLE_CONV,多通道扫描时改成ADC_EOC_SEQ_CONV,否则回调里的转换结束判断会有偏差。

3.2 单通道 DMA 连续采集

初始化完成之后,整个启动代码其实很精简:

#define ADC_BUF_LEN 512 uint16_t adc_buf[ADC_BUF_LEN] __attribute__((aligned(4))); void adc_dma_start(void) { HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); }

HAL_ADCEx_Calibration_Start在 G4 系列里几乎是必须的。第一次用的时候我贪省事,没校准就直接读,结果所有数据都带了一个固定偏移。不是说白屏或者 0,而是整体偏高或者偏低几个 LSB,做开关量判断还行,做精密测量完全不行。后来老老实实每次启动前校准。

DMA 工作在循环模式时,数据写满 512 个点之后会自动回来继续写。如果主循环正在读数组,DMA 也在写数组,就可能出现“读了半新半旧”的数据。最常用的解决方式是用半满中断和全满中断把缓冲区切成两半,读数据和写数据天然错开:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { flag_half = 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { flag_full = 1; } }

主循环里看到flag_half就处理前 256 个点,看到flag_full就处理后 256 个点。回调里不要做计算、滤波、打印这类耗时操作,只置标志位就够了。真正要更精细的双缓冲,可以后期用 DMA 的双缓冲模式,但现在这个“半满/全满 + 两块逻辑分区”的方法,对绝大多数项目已经足够稳。

3.3 多通道扫描 + DMA 搬运

多通道场景下,初始化结构体要改三处:ScanConvMode改成ENABLE,NbrOfConversion改成通道总数,EOCSelection改成ADC_EOC_SEQ_CONV。四通道配置示意如下:

hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.NbrOfConversion = 4; hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLINGTIME_COMMON_1; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_6; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_9; sConfig.Rank = 4; HAL_ADC_ConfigChannel(&hadc1, &sConfig);

启动 DMA 之后,缓冲区顺序固定是adc_buf[0]=Rank1、adc_buf[1]=Rank2、adc_buf[2]=Rank3、adc_buf[3]=Rank4,紧接着第二轮又是 Rank1 开始。这个顺序跟 ADC 通道号没有直接关系,通道号只是告诉你硬件接在哪个引脚上,Rank 才是软件处理的顺序。

缓冲区长度有一个非常重要的要求:最好是通道数的整数倍。四通道环境下,我一般把缓冲区长度设成 16 或 32,这样每一帧数据都对齐到缓冲区边界,处理函数不需要额外判断“这一帧从哪里开始”。如果缓冲区长度设成了 15,循环 DMA 写满后会越界到第 16 个位置,而那个位置可能是数组末尾或别的变量,轻则数据错位,重则把内存踩了。

多通道还有一个优势,不同通道可以配不同的采样时间。G4 的采样时间寄存器分成 COMMON_1 到 COMMON_4 几组,比如接高阻传感器的通道给长采样时间,接运放输出的通道给短采样时间。这样能尽量压缩一轮扫描的总时长,又不牺牲那一两路高阻通道的精度。具体枚举名看当前 HAL 版本,CubeMX 里直接选即可。

3.4 滤波和过采样的一点点经验

DMA 采出来的原始值一般不会直接用,软件滤波是家常便饭。滑动平均适合信号本身平稳、只想把毛刺抹平的场景;中值滤波对付偶尔出现的脉冲干扰很有效,但窗口不能太大,否则实时性变差;一阶低通滤波实现简单、响应快,适合控制类系统。先想清楚信号特征,再选滤波方式,不要上来就套一个 64 点平均,那样采样率会掉得很心疼。

G4 的 ADC 还自带硬件过采样,能把结果输出到更高位数。它本质上就是一种高效的硬件平均,但代价是转换时间成倍增加。我的实验感受是,4 倍过采样对噪声改善已经比较明显,16 倍过采样如果不右移,输出值会整体变大,容易超出 12 位范围;右移 4 位后数值回到 12 位,但噪声底明显降低。如果做音频或振动采集,这个特性非常实用。不过千万别为了追求分辨率把过采样开到 256 倍,采样率掉得太厉害,连续采样就变成慢速采样了。

4. 实测中绕不开的坑与排查手册

4.1 校准、启动顺序和常见返回错误

G431 的校准函数在校准失败时会返回错误码。最常见的一种情况是:校准前没有给 ADC 和参考电压足够的上电稳定时间,尤其从低功耗唤醒后立刻校准,偶尔会失败。我的做法是校准前加一个微秒级以上的延时,并检查返回值,失败就重试一次,而不是忽略它继续往下面跑。

启动顺序也要注意。HAL_ADC_Start_DMA并不是任何时候调用都安全。ADC 还在转换中时重复启动,状态机可能卡住。停止时更要讲究顺序,先用HAL_ADC_Stop_DMA停掉 ADC 和 DMA,再去读__HAL_DMA_GET_COUNTER。如果直接停 DMA,ADC 端可能出现 overrun,下次启动时状态不对。重新启动时,我还是建议按“校准 -> Start_DMA”的固定顺序走。这看起来多此一举,但能省掉很多“板子刚上电正常、过一会儿才异常”的疑难杂症。

4.2 DMA 数据不更新或全为 0

DMA 缓冲区里全是 0,这是新手遇到最多的问题。先说硬件:ADC 输入引脚在 CubeMX 里必须设成 Analog 模式,不是普通的 GPIO Input。有人习惯性把引脚配上拉或下拉,这会改变输入阻抗特性,读数就会不对。再查 VDDA 和 VREF+,G431 的 VREF+ 引脚如果没接好,ADC 读数很容易归零或只有噪声。硬件没问题后,再回头看 DMA 请求号,必须选DMA_REQUEST_ADC1。

如果只有第一次有数据,后面全部不更新,大概率是DMAContinuousRequests没开。这个选项关了,DMA 在第一次转换后就不再请求数据,缓冲区里永远只有最初那批值。调试时可以先打断点看缓冲区,第一轮有数据,后面不变,基本就是这个问题。

还有一种隐蔽情况是数组类型。用uint32_t存储 12 位 ADC 数据时,如果又开启了某种硬件过采样,数据实际占用位数变多,后处理时很容易把未定义的位也算进去。我建议统一用uint16_t数组,并加__attribute__((aligned(4)))。这不是玄学,是防止 DMA 搬运过程中访问未对齐地址造成的不可预期问题。

4.3 多通道数据错位与 DMA 长度

多通道扫描最常见的现象是:第 1、3 路看起来正常,第 2、4 路读数像是上一次的残留,或者通道之间互相串。绝大多数原因是缓冲区长度不是通道数的整数倍。四通道但缓冲区长度取 15,DMA 循环写一轮后,下一轮的第一个通道落在位置 16,已经越过数组边界。所以我通常选 8、16、32 这类长度,并在处理逻辑里按通道数取模,这样每一轮数据都对齐得严严实实。

还有一个和芯片状态机有关的坑:DMA 循环模式 + 扫描模式一起用,中途用HAL_ADC_Stop_DMA停止后再次启动,有些状态机会把扫描序列指针指到上次停止的位置,导致通道顺序错乱。我老化测试时遇到过,不是每次都能复现,但一旦踩中很头疼。稳妥做法是停止后延迟几十微秒,再重新校准启动。如果项目允许,最好只在系统初始化时启动一次 DMA,后续不要频繁启停。

4.4 排查速查表

现象优先检查项常见解法
DMA 数组全为 0引脚模式、VDDA、DMAMUX 请求号、Continuous Requests引脚改 Analog,检查硬件供电,选择 ADC1 请求,开启连续请求
只有第一次有数据DMA Continuous Requests 未开在 ADC 初始化中使能 DMAContinuousRequests
多通道串数据缓冲区长度不是通道数倍数缓冲区长度取通道数整数倍,按 Rank 处理
读数整体偏低或偏高没做校准;VREF+ 不稳启动前HAL_ADCEx_Calibration_Start,检查参考电压
中断里处理太久丢数据回调里做滤波、打印、延时回调只置标志,数据处理移出中断
启动后状态机卡住上次 stop 后 overrun 位没清检查HAL_ADC_GetError,必要时重新初始化

4.5 一个小提醒:先跑通直接采集再上 DMA

不要觉得直接采集太低级就跳过。我每块新板子拿到手,都会先用轮询读一个通道,确认引脚、电压、校准都没问题,再切 DMA。直接采集的代码只要“Start -> PollForConversion -> GetValue”三步,能最快暴露硬件问题。等直接采集稳定了,再上 DMA 单通道,最后才加多通道扫描,这样出错时排查范围很小。

第一次跑 DMA 时,前 128 个点建议不要直接用。启动瞬间的状态未必稳定,尤其从低功耗唤醒之后。我会预先把缓冲区填充成 0xFFFF,等第一次半满中断产生后再取数据。这个小习惯让我少查了很多“玄学”问题。ADC 和 DMA 一旦配合好,后面的数据处理、波形显示、控制算法都会省心很多。

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

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

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

立即咨询