STM32G431 ADC直接与DMA采集:配置、校准与踩坑排查

发布时间:2026/10/20 9:22:41
STM32G431 ADC直接与DMA采集:配置、校准与踩坑排查
简介针对STM32G431RBT6微控制器的ADC直接采集与DMA传输资料包适合嵌入式开发者在传感器信号处理、电源管理、音频采集等需要连续高速数据读取的场景中使用。资源围绕ADC1直接模式与ADC2 DMA模式展开从初始化配置到转换触发、内存搬运、中断协同逐层拆解覆盖HAL库调用、DMA通道链接、采样参数调整与性能优化思路结合多通道配置与单次/连续转换模式说明可帮助开发者理解并复现典型数据采集链路。包体为zip格式共1254个文件以c源文件676个、h头文件275个为主另有s汇编、txt说明、uvprojx工程文件及编译产物压缩包约29.73MB。目前已有1978人学习下载适合正在搭建STM32G4数据采集项目或希望深入掌握ADC直采与DMA搬运机制的嵌入式开发者参考。1. STM32G431RBT6 ADC 的直接与 DMA 采集先分清需求再动手做过电机控制或电池监测的人应该都有体会ADC 本身只是读一个寄存器但当采样点要和 PWM 周期对齐、多通道要轮着扫、数据还不能丢时直接采集与 DMA 采集就成了两条分岔路。STM32G431RBT6 的 ADC 带硬件过采样、双 ADC 同步和灵活触发源直接把采集做对能让后续 FOC 或电压环省下大量调试时间。这篇笔记按我实际拆过的工程顺序讲清楚直接模式与 DMA 模式下怎么配时钟、怎么写代码、又在哪些环节最容易翻车适合正在用 G4 系列做采样链路的新手也适合被多通道 DMA 数据搞晕的老手拿来对照。2. G431 的 ADC 硬件底子三个 ADC 与时钟树直接采集前的一课2.1 ADC1/2/3 的资源盘点通道、序列和内部信号STM32G431RBT6 这颗芯片虽然是 64 引脚小封装但核内集成了三个 12 位逐次逼近型 ADCADC1、ADC2、ADC3。很多人只把它当普通 MCU 用其实这三个 ADC 的定位不同ADC1 和 ADC2 都能独立工作也支持双 ADC 同步采样模式这是电机 FOC 里做相电流重构最常用的组合。ADC3 在很多封装上可用的外部引脚更少但它的触发源和同步机制和 ADC1/2 一样灵活常被用来做母线电压或温度等慢速监测。从规则通道来看每个 ADC 支持 16 个规则序列注入通道有 4 个。所谓规则序列就是一次启动后按顺序把多个通道都采一遍结果按顺序进入数据寄存器或者被 DMA 搬到内存。注入通道则类似“插队”优先级高于规则组适合需要紧急采集的某一路信号。内部通道方面G431 的 ADC 还能采内部温度传感器、VREFINT内部基准电压和 VBAT。这里有个容易忽略的点VREFINT 不只是用来做校准参考的在电池供电设备里它也可以反推 VDDA 的实时电压进而修正其他通道的绝对精度。很多直接采集的读数偏差根源不是 ADC 坏而是 VDDA 波动没被考虑进去。2.2 时钟树与采样周期分辨率 12 位速度和精度要一起看G4 系列的 ADC 是异步时钟架构由 RCC 单独分配时钟源上限一般可以跑到 60MHz 左右。这点比 F1 系列幸福很多F1 时代 ADC 时钟超过 14MHz 就会出现精度下降G431 在满足采样时间的前提下能把转换节奏拉得很快。以 12 位分辨率为例一次完整转换的时间公式是Tconv (采样周期 12.5) / ADC_CLK采样周期通过 SMPR 寄存器设置可选档位从 1.5 个周期一直到 640.5 个周期。CubeMX 里可以直接下拉选择。我一般的原则是信号源阻抗低、走线短用 2.5 到 11.5 个周期的短采样时间换采样速度。信号源是电阻分压、阻值超过 10kΩ或者电路板上有电容滤波采样周期至少给到 92.5不然采样电容还没充满就开始转换读数会整体偏低。还有一个需要同步设置的叫“过采样”。G431 的 ADC 支持硬件过采样最高 32 倍。它和我们平时软件里多次采样求平均不一样硬件过采样在转换层面做累加和移位不占 CPU。代价是有效转换速率降低但分辨率能往上提。比如 12 位 ADC 配 4 倍过采样输出是 14 位精度的结果。慢速采样场景我很推荐把过采样开起来后面第 6 章会聊到具体配置。2.3 校准与参考电压G4 上不能跳过的 ADCCALF1 系列的 ADC 没有专门的校准步骤上电即可读。G431 不一样数据手册里明确要求上电后执行 ADC 校准也就是设置 ADCAL 位等待校准完成。校准过程会计算出一个校准因子自动写入寄存器。这个因子补偿的是 ADC 内部的比较器失调电压和电容失配如果不做典型后果是零点偏和满量程偏同时出现读数偏差可能到几十毫伏。在 HAL 库中校准接口是HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);常见做法是在初始化参数之后、启动转换之前调用。如果用了差分输入第二个参数要换成 ADC_DIFFERENTIAL_ENDED并且此时两路输入都接地或接共模电压再校准。参考电压方面G431 有 VREF 引脚默认直接接 VDDA。如果对精度要求比较高可以打开内部的 VREFBUF生成 2.048V 或 2.5V 的稳定基准但要注意模拟输入范围会受限。做直接采集时最容易犯的错误是把 VDDA 当成绝对稳定的 3.3V一旦 VDDA 来自 LDO 且电流波动大读数跟着漂是必然的。3. 直接采集实现轮询与中断两套代码参数怎么配才不翻车3.1 CubeMX 工程配置从引脚到 ADC 参数的落地顺序直接采集虽然在代码上简单但 CubeMX 里的参数一旦配错后面排查成本很高。我通常按这个顺序配置选择目标引脚在芯片图上把 ADC1_IN1、ADC1_IN2 等引脚激活为 Analog。将 ADC1 模式设置为“Single-ended”使能对应通道。分辨率选 12 bit采样周期先按信号源阻抗决定别上来就图快。转换模式连续转换模式这里要特别想清楚。轮询读单个通道时打开 Continuous Conversion Mode 可以省掉重复调用启动的代码但如果用中断模式且希望每次转换都触发回调连续模式会让回调频繁触发反而不好控制。触发源选“Software Start”直接采集场景先用软件触发跑通再考虑定时器触发。CubeMX 生成的初始化函数里ADC 参数是放在hadc1.Init结构里的。生成后不要乱动MX_ADC1_Init()后续调采样时间就改里面一行hadc1.Init.SamplingTimeCommon ADC_SAMPLETIME_92CYCLES_5;注意G4 系列有个SamplingTimeCommon公共配置芯片会自动把该采样时间应用到当前规则序列的所有通道。如果你需要不同通道用不同采样时间就要通过 LL 库或者直接写寄存器按通道逐个设置这是 G4 异于 F1 的一个点。3.2 轮询模式概念验证时最简路径轮询模式的代码逻辑非常直白启动转换死等转换完成标志读数据寄存器。以一个通道为例uint16_t adc_value 0; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); }这里HAL_ADC_PollForConversion的第二个参数是超时时间单位毫秒。我习惯写 10ms实际转换只需几微秒这个超时只是防止 ADC 因为时钟配置错误而永远挂起。如果返回HAL_TIMEOUT大概率不是信号问题而是 ADC 时钟没起来。HAL_ADC_GetValue返回的是一个 32 位无符号整数但实际有效数据只有低 12 位。除以 4096 再乘以参考电压就是真实电压float voltage (float)adc_value * 3.3f / 4096.0f;轮询模式的问题也很明显在等待转换期间 CPU 不能干别的。一个 12 位单次转换大约 1µs 左右看起来不长但如果你在一个控制周期内要采 8 路通道还要做滤波和控制算法轮询会白占掉几个微秒。这个开销对 FOC 这种 16kHz 中断频率的应用非常敏感所以轮询只适合开机自检或调试打点。3.3 中断模式频率上限和回调里的注意事项中断模式的改法很小但心里要有数它并没有降低 CPU 负担只是把“死等”换成“中断跳出”。每转换完一个规则序列硬件触发一次 EOC 中断CPU 在中断处理函数里读数据。代码如下volatile uint16_t g_adc_value 0; HAL_ADC_Start_IT(hadc1); void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { g_adc_value HAL_ADC_GetValue(hadc); // 如果关闭了连续转换必须在这里重新启动 HAL_ADC_Start_IT(hadc); } }这里有一个 G4 新手高频翻车点如果不把连续转换打开中断回调执行完后 ADC 会停下来下一次转换永远不会发生。所以要么在 CubeMX 里打开 Continuous Conversion Mode要么像上面代码一样在回调末尾重新启动。两种写法都有人用我建议在中断模式里保持单次转换 回调末尾重新启动这样每次中断之间你可以在主循环里控制采样频率而不是让 ADC 全速跑把 CPU 打断得七零八落。采样率的上限也值得算一下。假设 ADC 时钟 60MHz采样周期 11.512 位分辨率单次转换时间约为 24.5 / 60MHz ≈ 408ns。理论上中断频率可以到 2MHz 以上但这是指单通道。如果开了 8 个通道扫描一次扫描完成才触发一个中断那么中断频率大约降为 250kHz。这个水平依然很高加上中断进出开销CPU 基本就没空干别的了。所以中断模式适合中等速率采样一旦通道数多且转换快就应该直接上 DMA。4. DMA 采集全流程循环缓冲与定时器触发把 CPU 彻底解放4.1 DMA 请求映射与数据宽度先绕开一个隐蔽的坑G431 有两个 DMA 控制器DMA1 和 DMA2每个控制器下面又有多个通道。ADC1 和 ADC2 的 DMA 请求通过定时器等外设映射到对应的 DMA 通道在 CubeMX 的 DMA 设置里选择加一个 DMA Request 即可具体用哪个通道是芯片自动建议的。但在代码层面有一个数据宽度的坑必须提前说。ADC 的数据寄存器ADC_DR虽然是 32 位宽度但真正有效的只有低 16 位。HAL 库的HAL_ADC_Start_DMA接口签名里缓冲区指针是uint32_t*这意味着标准做法是把 DMA 缓冲区声明为uint32_t数组#define ADC_BUF_LEN 64 uint32_t adc_buf[ADC_BUF_LEN]; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);有人图省事声明成uint16_t数组并强制转换传进去这在一些 F1 例程里能跑但在 G4 上风险很高。因为 DMA 搬运时按外设数据宽度和内存数据宽度配置如果外设宽度是 Word、内存宽度也是 Word那么每次 DMA 写的是 4 字节uint16_t数组的地址会连续被覆盖不仅数据错位还可能踩到相邻变量。我的做法是 CubeMX 里把 DMA 参数固定为DMA 参数取值说明ModeCircular循环模式启动一次持续采样Increment AddressMemory外设地址固定内存地址递增PeriphDataWidthWord对应 ADC_DR 的 32 位寄存器MemDataWidthWord缓冲区按 32 位元素组织PriorityHigh多通道扫描时避免 FIFO 溢出4.2 多通道扫描 循环缓冲代码里的三种数据处理方式配置好 DMA 后多通道扫描就成了常态操作。CubeMX 中需要把 ADC1 的 Number of Conversion 改为 2、3 或更多并在每个通道配置里指定 Rank 和采样时间。生成代码后规则序列的优先级由 Rank 决定Rank 1 会先转换数据也先进入缓冲区。启动代码非常简单HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);这里第三个参数是缓冲区长度注意它的单位是“DMA 传输次数”。如果 ADC1 配置了 3 个规则通道那么每轮完整扫描需要 3 次 DMA 传输所以缓冲区长度应该设为 3 的整数倍。比如我想保存最近 20 轮扫描数据就把长度设为 60。DMA 工作在循环模式时缓冲区的使用有两种处理方式。方式一利用半满和全满中断把缓冲区拆成两个半区。G4 的 DMA 可以配置 Half Transfer 和 Transfer Complete 中断前者表示缓冲区前半被填满后者表示后半被填满。在主循环里检查对应标志按期处理数据这在连续采样场景里最常用volatile uint8_t half_done 0; volatile uint8_t full_done 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { half_done 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { full_done 1; } }方式二不用中断主循环直接轮询缓冲区当前写入位置。你可以通过读取 DMA 的剩余计数寄存器__HAL_DMA_GET_COUNTER反推当前写到了缓冲区的哪个位置只处理已稳定的区域。这个方法对时序要求不高的场景很实用但注意不能在 DMA 正在写某个数组元素时去读它否则可能出现撕裂数据。4.3 定时器触发与 PWM 同步把采样点钉在波形上直接模式和普通 DMA 模式都是在需要时触发一次采样但电机控制和电源控制里真正难的是让 ADC 采样点精确对齐到 PWM 的特定相位。G431 的 ADC 支持外部事件触发CubeMX 的 Trigger Source 下拉列表里能看到Timer 1 Trigger Out event、Timer 8 Trigger Out event等选项。常见的做法是把 TIM1 的 Update Event 作为 TRGO 输出。将 ADC1 触发源选为 TIM1_TRGO。在 DMA 循环模式下每次定时器更新事件一到ADC 自动启动规则转换转换完自动搬进缓冲区。代码层面只需要在初始化完成后启动一次 DMAHAL_DMA_Start_IT(hdma_adc1, (uint32_t)hadc1.Instance-DR, (uint32_t)adc_buf, ADC_BUF_LEN); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);需要注意如果配置了定时器触发软件触发函数HAL_ADC_Start就不再起作用ADC 会等待硬件触发。很多人在调试时发现 DMA 启动后数据不更新去掉HAL_ADC_Start_DMA前的软件启动代码反而就好了原因就在这里。还有一点是关于双 ADC 同步采样的。G431 支持 ADC1 和 ADC2 同时采样ADC_MODE_DUAL_SLAVE模式下两个 ADC 各采一个通道结果按固定顺序交替写入。这在 FOC 中采集两相电流时能保证两个采样点时间完全一致。DMA 缓冲区长度要设置为双倍通道数数据处理时按偶数索引和奇数索引分别取出两路数据。5. 常见问题排查G431 ADC 采集的五个真实踩坑点与修复5.1 现象所有通道读数整体偏高或偏低约几十毫伏刚上电时采集 1.5V 基准电压读出来 1.53V而且随着环境温度变化读数还漂。原因没有执行 ADC 校准。G431 的 ADC 每次上电后校准因子未知内部失调没有消除。此问题在直接与 DMA 模式下都会出现且不是硬件故障。解决初始化完成后强制加入校准流程HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);校准完成后等待 10ms 再开始采样之后读数通常会回到正常范围内。从那以后我每写一个 G4 采集工程都会把校准放在电源稳定之后。5.2 现象高阻信号源读数偏低且采样时间越短越明显例如用两个 100kΩ 电阻分压得到 1.65V接到 ADC 引脚后读出来只有 1.58V加大采样周期到 640.5 周期后读数恢复。原因ADC 内部采样电容需要在采样阶段完成充电。信号源阻抗越高RC 充电时间常数越大采样窗口太短就没充够。解决换上短采样时间只是为了速度此时要配套降低信号源阻抗。常见的做法是加一个运算放大器做电压跟随器或者至少把分压电阻降低到 1kΩ 级别。如果必须用高阻分压且不引运放采样周期选择 92.5 或 247.5 个周期速度牺牲有限精度有保障。5.3 现象多通道 DMA 采完缓冲区里数据顺序和预想不一致三个通道分别是 IN1、IN2、IN3配置 Rank 顺序也是 1、2、3但读取缓冲区发现索引 0 对应的是 IN2。原因CubeMX 里 Rank 配置和引脚分配的先后顺序、是否启用了注入通道、其他外设对通道映射的复用都可能影响实际规则顺序。更隐蔽的是 DMA 缓冲区起点在循环模式下每次中断的复位时机不同导致读取位置与通道错位。解决不要在代码里猜测通道顺序。调试时先给三个通道接不同电压比如 0V、1.65V、3.3V打印缓冲区前 6 个值据此建立一张通道与索引的映射表。如果发现顺序乱了回 CubeMX 重新核查 Rank 或手动调整 SQR1 寄存器。5.4 现象DMA 采一段时间后某个缓冲区值突然跳变现象是中断回调还能触发但缓冲区数据不再连续某一段值全部是 0 或满量程。原因DMA 优先级不够或者 ADC 时钟配置超过上限。G4 的 ADC 在极端配置下连续转换速率太高DMA 请求来不及搬走溢出标志置位。另一个原因是进了中断后对HAL_ADC_Start_DMA重复调用覆盖了正在进行的 DMA 传输。解决把 DMA 优先级设为 High同时检查 ADC 时钟配置按主频推算不要超过 60MHz。中断服务函数里不要重复启动 DMA循环模式下启动一次即可如果必须重启先调用HAL_ADC_Stop_DMA再重新启动。5.5 现象开启连续转换后中断模式采样频率失控在中断模式里开了 Continuous Conversion Mode发现主循环几乎被中断占满系统卡顿严重。原因连续模式下ADC 转换完一个序列立即开始下一个序列每一次转换结束都会触发中断。当采样时间短、通道少时中断频率可能高到每秒几十万次。解决中断模式配合单次转换在回调里不重启而是由主循环或定时器决定下一次启动时机。连续转换留给 DMA 模式用因为 DMA 不需要逐次进中断只在半满和全满时回报一次。6. 验证手段与进阶技巧数据一致性检查和强制校准习惯工程代码写完不能只是肉眼看一下波形大体对就固化。我习惯在正式接入控制算法之前做一次 DMA 与直接采集的交叉验证。具体做法是把同一个通道同时配置成两个 ADC 都不合适因为引脚不能复用。更可行的方案是先在直接模式下采集 100 次记录平均值和方差再切到 DMA 模式下采集同样数量数据做统计对比。验证代码很简单uint32_t sum_direct 0; uint32_t sum_dma 0; uint16_t max_diff 0; for (int i 0; i 100; i) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { sum_direct HAL_ADC_GetValue(hadc1); } } for (int i 0; i 100; i) { uint32_t val adc_buf[i * 3]; // 假设 IN1 是规则序列第一个 sum_dma val; } uint32_t avg_direct sum_direct / 100; uint32_t avg_dma sum_dma / 100; // 两者相差不超过 3 个 LSB 即可认为 DMA 链路正常这个办法能同时查出来两类问题一类是 DMA 数据宽度配错导致的值整体异常另一类是通道顺序错位导致的均值对不上。如果两边均值接近但方差差异大说明 DMA 循环模式下存在部分撕裂数据需要回看半满/全满中断处理逻辑。进阶技巧方面慢速监测通道我强烈建议打开硬件过采样。CubeMX 里在 ADC 设置页有 OVERSAMPLING 配置Ratio 选 16 倍偏移右移 4 位分辨率就从 12 位提升到等效 16 位。数据平滑效果立竿见影而且不占用 CPU。硬件过采样在 DMA 模式下照常工作缓冲区里存的是过采样后的结果不需要在 DMA 回调里再跑软件滤波。最后还有一个心态层面的经验。ADC 是最容易让人陷入玄学的外设读数不对时先不要怀疑电源干扰按“校准、时钟、采样时间、DMA 宽度、通道顺序”这个顺序查一遍绝大多数问题都能定位。G4 的 ADC 没有 F1 那么皮实但也正因为如此它才配得上更高的采样率和同步精度。它的校准机制、循环 DMA、定时器触发的组合能让一部分之前需要 FPGA 或专用采样芯片才能完成的采集任务直接在 MCU 内搞定关键是你别把它当普通外设来用。从那以后我每次用 STM32G431RBT6 做采集都强制走一遍固定流程先校准 ADC再核对 DMA 数据宽度最后用基准电压源做一次滑动校验。这三步看着机械但每一回都能提前暴露至少一个隐患。希望这些记录对你的 G4 采样工程有切实帮助。本文还有配套的精品资源点击获取