
简介面向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 FlashT6 是工业级温度范围。这颗芯片做电机控制、数字电源、电池巡检、信号采集这类项目非常多我最近手里的几个传感器数据采集方案也直接用上了它。很多人只看重它的内核性能其实 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~MspsCPU 介入每次转换都要介入缓冲区满/半满时介入多通道支持可以但顺序读取繁琐自动按扫描序列写入典型场景按钮、电位器、慢速传感器音频采样、电流波形、定时器同步采样直接采集并不是一个“要被淘汰”的方案它是最快的验证手段。你在新板上判断 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改成ENABLENbrOfConversion改成通道总数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。如果直接停 DMAADC 端可能出现 overrun下次启动时状态不对。重新启动时我还是建议按“校准 - Start_DMA”的固定顺序走。这看起来多此一举但能省掉很多“板子刚上电正常、过一会儿才异常”的疑难杂症。4.2 DMA 数据不更新或全为 0DMA 缓冲区里全是 0这是新手遇到最多的问题。先说硬件ADC 输入引脚在 CubeMX 里必须设成 Analog 模式不是普通的 GPIO Input。有人习惯性把引脚配上拉或下拉这会改变输入阻抗特性读数就会不对。再查 VDDA 和 VREFG431 的 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 路读数像是上一次的残留或者通道之间互相串。绝大多数原因是缓冲区长度不是通道数的整数倍。四通道但缓冲区长度取 15DMA 循环写一轮后下一轮的第一个通道落在位置 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 一旦配合好后面的数据处理、波形显示、控制算法都会省心很多。本文还有配套的精品资源点击获取