简介基于STM32G474的ADC采样完整工程源码包面向单片机开发者与嵌入式工程师覆盖模拟量采集开发中最关键的配置与实现可用于物联网设备、工业控制及现场数据采集等项目的快速验证。压缩包共1279个文件以C源文件667个、头文件271个和汇编文件97个为主体另含工程配置、链接脚本、静态库以及调试辅助文件整体约42.58MB目录结构完整便于在IAR或STM32CubeMX环境中直接对照参考。该项目已有797人学习下载。源码演示了ADC初始化、多通道配置、采样时间与分辨率设置、软件或外部触发、连续/扫描转换以及通过HAL库实现中断和DMA读取转换结果同时包含转换完成回调、数据滤波、低功耗管理、ADC校准和内部温度传感器读取等实际工程细节附带PWM相关代码可配合验证。阅读者还可从工程中学习HAL_ADC_Start_IT()、HAL_ADC_GetValue()等常见API的调用方式适合作为G4系列ADC开发的入门模板和排错参考。1. 拿到 stm32g474 的 ADC 采样工程后先别急着跑做数字电源或电机控制的人多半翻过“实现了stm32g474的ADC采样功能”这类工程包。G474 这颗片子的 ADC 不是普通的 12 位外设它带差分输入、硬件过采样、多触发源采样周期和 DMA 配合稍有偏差采集出来的波形就像蒙了一层雾。这个工程真正解决的问题是把 ADC、定时器触发、DMA 搬运和滤波串成一条完整的数据链路让你拿到的是可信的数值而不是一个能跑但说不清准不准的 demo。适合谁正在调电流环、做电池巡检、写仪器采样的固件工程师。接下来的内容是我基于 G474 外设手册和实际调板经验整理的落地路径照着配置能少走不少弯路。2. 采样前先立住G474 的 ADC 硬件参数与选型理由2.1 分辨率、时钟与采样周期信噪比的起点G474 的 ADC 标称 12 位主频跑到 170 MHz 时ADC 时钟经分频后最高能到 60 MHz 左右单次转换最快约 5 Msps。这个速度在 MCU 内置 ADC 里算第一梯队但真实信噪比不取决于标称位数而取决于你给采样电容留了多少充电时间。采样周期由 SMPR 寄存器控制范围从 2.5 个 ADC 时钟周期到 640.5 个周期选多少由信号源阻抗决定。我一般把规则记成信号源阻抗在 1 kΩ 以下SMPR 设 12.5 到 24.5 个周期即可阻抗到 10 kΩ 量级就加到 84.5 个周期如果前端是几百 kΩ 的分压电阻直接选 640.5 个周期。采样周期设短了采样电容充不满测出来电压偏低且跳动大设长了单次转换占用的总线时间变长DMA 搬运压力也上来。这里有个容易被忽略的点G474 的 ADC 时钟分频系数不是随便选的ADC 时钟超过规格上限会导致转换结果非线性所以要先算清楚 PCLK 的来路再定分频。信号源阻抗建议采样时间典型场景 1 kΩ2.5 ~ 12.5 cycles运放直接驱动、DAC 输出直连1 ~ 10 kΩ24.5 ~ 84.5 cyclesNTC 分压、电位器采样10 ~ 100 kΩ192 ~ 640.5 cycles高阻分压网络、电池电压检测2.2 差分模式与硬件过采样什么时候值得开G474 的 ADC 值得多花时间研究的两个特性是差分输入和硬件过采样。差分模式适合采样双极性信号比如电机相电流经互感器或采样电阻转成的差分电压。开启后ADC 内部会把两个输入引脚做减法共模噪声被抵消一部分信噪比能提升不少。代价是可用通道数量减半而且差分输入对引脚配置有硬性要求不是随便两个脚都能凑成一对。硬件过采样则是对抗随机噪声的利器。G474 内置过采样单元可以把多次转换结果累加后右移输出等效分辨率从 12 位最高扩展到 16 位。软件上做滑动平均也能达到类似效果但硬件过采样不占 CPU也不会因为中断频繁而漏数据。我常用 16 次过采样加 4 位移位等效 14 位分辨率噪音明显下降速度还能剩 300 kSps 左右。如果你想采的是一路缓慢变化的电压信号过采样是性价比最高的信噪比改善手段。2.3 触发源选择软件、定时器还是外部引脚ADC 什么时候开始转换三种触发方式各有适用场景。软件触发最省事写一句启动命令就采一次适合不需要精确时序的数据采集定时器触发适合需要把采样点精确对齐到 PWM 某个相位比如电机控制的电流采样要避开功率管开关瞬间外部引脚触发适合外部设备给脉冲时同步采样比如编码器索引信号或外部同步时钟。做定时器触发时注意 G474 的 ADC 触发映射表TIM1 和 TIM2 的 TRGO 都能触发 ADC但具体到 ADC1/ADC2/ADC3 的映射不同配置错了一个通道的触发源现象就是“ADC 不启动”或者“频率完全不对”。我建议初期先用软件触发跑通链路再切定时器触发不然容易把问题搞复杂。3. 用 DMA 把 ADC 数据搬进内存最小工程与参数设置3.1 CubeMX 外设树与时钟配置拿到 G474 工程第一步不是写代码而是把 CubeMX 里的时钟树和外设配置拉出来核对一遍。ADC 时钟源可以选 PCLK 两分频或四分频也可以选由系统时钟经 PLL 分出的专用时钟。常见做法是把 ADC 时钟设在 30~60 MHz 之间既能保证速度又不会超过规格上限。CubeMX 里在 RCC 时钟树可以看到 ADC 时钟分频的结果记得把 PCLK 先设到 170 MHz再调 ADC 分频。引脚分配上ADC 输入通道要避开 PF0、PF1 这类与调试口复用的引脚不然下载程序后引脚电平异常会导致采样结果被拉偏。G474 的 ADC 通道从 PA0 到 PC5 都有分布我一般优先选 PA 口和 PB 口物理上离 DMA 通道近布线也方便。3.2 多通道顺序采样stm32 adc 切换通道的配置细节多通道采样最容易翻车的地方是通道切换。G474 的 ADC 支持扫描序列你只需要在 Rank 里把通道排好顺序硬件会在每次转换结束后自动切到下一个通道。不要在应用程序里改通道选择寄存器那是在做低效且容易出错的事。正确姿势是把队列交给硬件软件只负责读取结果。下面是一段 CubeMX 生成后再做裁剪的 ADC1 初始化代码void MX_ADC1_Init(void) { hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; // ADC 时钟约 42.5 MHz hadc1.Init.Resolution ADC_RESOLUTION_12B; // 12 位分辨率 hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; // 扫描模式按 Rank 顺序采样 hadc1.Init.ContinuousConvMode DISABLE; // 单次模式靠定时器触发 hadc1.Init.DiscontinuousConvMode DISABLE; // 不使能不连续模式 hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T3_TRGO; // TIM3 触发 hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; // 上升沿启动 hadc1.Init.Overrun ADC_OVR_DATA_OVERWRITTEN; // DMA 未及时读走则覆盖 hadc1.Init.SamplingTimeCommon ADC_SAMPLINGTIME_COMMON_84CYCLES; if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } }这里几个参数要解释清楚。ClockPrescaler 用 DIV4是因为 PCLK2 配置成 170 MHz 后四分频得到 42.5 MHz既保证转换速度又留有余量。ScanConvMode 打开后 ADC 才会按 Rank 表依次转换漏掉它多通道配置根本不生效。ContinuousConvMode 必须关掉否则一次定时器触发会引发连续多轮扫描数据量暴增DMA 根本忙不过来。ExternalTrigConvEdge 只在触发模式下有意义软件触发时这个字段会被忽略。3.3 DMA 循环模式与内存地址对齐数据从 ADC 数据寄存器搬到内存DMA 是唯一不丢数据的办法。G474 的 DMA 支持循环模式配好后它会自动把数据写到指定数组满了就回绕覆盖。对 ADC 采样来说我建议开 DMA 循环模式配合一个足够大的数组这样 CPU 随时可以读取最新一批数据而不需要每次都进中断抄数据。配置 DMA 时外设地址是 ADC1 的数据寄存器内存地址是 uint16_t 数组首地址数据宽度两边都设 Half Word。有的工程师把数据宽度设成 Word然后发现读取的数据全是错位的因为 ADC 的寄存器是 16 位DMA 按 32 位搬运会把两个采样值拼成一个正确率直接减半。/** DMA 配置ADC1 采集结果搬到 adc_values 数组 */ hdma_adc1.Instance DMA1_Channel1; hdma_adc1.Init.Request DMA_REQUEST_ADC1; // 请求源选 ADC1 hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; // 外设到内存 hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址固定 hdma_adc1.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; // 16 位 hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; // 16 位 hdma_adc1.Init.Mode DMA_CIRCULAR; // 循环模式 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1);参数说明PeriphInc 必须禁用因为 ADC 数据寄存器只有一个MemInc 使能后 DMA 会把采样结果连续写到数组里。Mode 用 CIRCULAR 而不是 NORMAL是因为循环模式配合数组回绕才能实现后台持续采集。Priority 给 HIGH避免在 DMA 通道较多时ADC 数据被其他外设挤掉导致 Overrun。4. 定时器触发与软件滤波采样稳定性靠这两步4.1 TIM 触发 ADC 的校准过程G474 的数字电源和电机控制场景里几乎没有软件触发采样的空间。PWM 开关瞬间会注入巨大的共模噪声必须在功率管导通稳定后、关断前避开边沿采样。常见做法是用定时器的一条 TRGO 信号触发 ADC在 PWM 中心计数值匹配时产生上升沿ADC 收到边沿后启动一轮扫描。配置 TIM 触发 ADC 有三步第一步把 TIM 的 Update 事件或 OC 比较事件映射到 TRGO第二步在 ADC 外设里选对应的触发源第三步把 TIM 启动起来。以 TIM3 为例更新事件作为 TRGO 输出然后 ADC1 的触发源选 TIM3_TRGO。实现代码里只改定时器的主从模式void MX_TIM3_Init(void) { TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 170 - 1; // 170 MHz / 170 1 MHz htim3.Init.Period 999; // 1 kHz 触发频率 htim3.Init.CounterMode TIM_COUNTERMODE_CENTER_ALIGNED1; // 中央对齐 HAL_TIM_Base_Init(htim3); sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; // 更新事件映射到 TRGO sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim3, sMasterConfig); }这里有个血泪经验很多工程把 Prescaler 算错导致触发频率和预期差一个数量级。170 MHz 主频下预分频 170 得到 1 MHz 计数时钟Period 999 意味着每个计数周期 1000 个脉冲所以触发频率正好 1 kHz。中央对齐模式可以保证更新事件发生在计数值到达 Period 的瞬间对 PWM 中心采样友好。要注意 MasterOutputTrigger 在不使用定时器输出比较时仍要配置否则 TRGO 无输出ADC 永远等不到上升沿。4.2 C 语言 ADC 值滤波函数滑动平均与一阶低通ADC 数值从 DMA 数组里读出来通常不能直接用。机械振动、电源纹波、PCB 上的噪声都会叠加到采样值上轻则数值小幅跳动重则波动几十个 LSB。我常用的两套方案滑动平均适合对实时性要求不高的慢速信号比如温度、电压巡检一阶低通适合需要快速响应的控制环路。滑动平均的 C 语言实现维护一个大小为 N 的环形缓冲区每次采样插入新值取 N 个值的平均输出。关键点在于处理数组回绕避免越界。代码示例#define ADC_FILTER_N 8 uint16_t adc_moving_average(uint16_t new_sample) { static uint16_t buf[ADC_FILTER_N] {0}; static uint8_t index 0; static uint32_t sum 0; uint8_t i 0; sum - buf[index]; // 减去最旧的值 buf[index] new_sample; // 写入新值 sum new_sample; // 累加新值 index (index 1) % ADC_FILTER_N; // 回绕索引 return (uint16_t)(sum / ADC_FILTER_N); }这个代码的重点是 sum 必须是 uint32_t否则 8 个 12 位 ADC 值累加会溢出 uint16_t。index 的回绕用取模运算实现代码可读性高但编译器在 Cortex-M4 上会把取模优化成先判断后减位不需要手动优化。滤波后的数值延迟大约 N/2 个采样周期对 1 kHz 采样率来说就是 4 ms 延迟温度、电压类信号完全可以接受。控制环路里我更常用一阶低通因为它的群延迟更可控uint16_t adc_lowpass_filter(uint16_t new_sample, float alpha) { static float filtered 0; filtered alpha * (float)new_sample (1.0f - alpha) * filtered; return (uint16_t)filtered; }调用时 alpha 取 0.2~0.4 之间数值越小滤波越强、延迟越大。注意浮点运算在 G474 有 FPU 支持不心疼这几条指令。alpha 取 0.2 时截止频率大约为采样率的 5% 左右能滤掉大部分工频和高频毛刺。滤波器不是玄学参数选多大取决于信号带宽。如果只是想把毛刺抹平滑动平均就够了如果要进电流环一阶低通更稳妥。4.3 多通道切换的常见误用stm32 adc 切换通道最常见的误用是每个通道都用不同的采样时间。G474 支持每通道独立设置采样时间但 CubeMX 默认是统一值有些人没注意到 Rank 配置里可以单独改 SMPR直接把所有通道都当成同一个采样时间处理。高阻通道采样不足低阻通道采样时间浪费。正确做法是在 CubeMX 的 ADC1 Rank 列表里逐通道核对 SamplingTime或者直接在代码里对每个 Rank 单独调 HAL_ADCEx_ConfigChannel。另一个常见误用是扫描模式下开了连续转换。连续转换会刷爆 DMA 数组而且因为优先级问题低优先级通道的数据会被高优先级通道频繁覆盖最终数组里看到的是“每个通道都有一点但每个通道都不完整”。扫描模式加单次转换、配合定时器周期触发才是稳定的多通道采样组合。5. 避坑G474 ADC 采样中我踩过的五个问题5.1 现象上电后第一次转换结果偏离真实值很多上电后第一次 ADC 转换结果比真实电压低几百个 LSB之后的数据恢复正常。原因是 ADC 上电后内部模拟电路没有完成校准G474 虽然出厂有校准值但环境温度和供电电压变化后原来的校准数据不再准确。解决方法是每次上电初始化时都手动执行一次校准序列先调用 HAL_ADCEx_Calibration_Start再启动转换。这个坑绝大多数新人都踩过代码里少了三行调用表现就是“第一个值不准”。5.2 现象相邻通道串扰一个通道电压变化另一个也跟着变最典型的场景两个通道分别接电位器转动通道 A 的电位器通道 B 的数值也轻微变化。原因多半是采样周期太短采样电容没有完全充电前一个通道的残余电荷影响了下一个通道的建立时间。解决方法是把共享的采样时间往上加我通常直接统一设到 84.5 个周期串扰幅度能压到 1~2 个 LSB 以内。如果加了采样时间还串检查 PCBA 上两个引脚之间是否有长距离平行走线。5.3 现象DMA 半满中断里读数据发现数组后半段全是旧值开了 DMA 循环模式后用半满/全满中断处理数据发现中断标志触发了但数组内容更新缓慢。原因是我把 DMA 中断优先级配置得太低又被定时器中断和串口中断挤占ADC 的 Overrun 标志已经置位但 DMA 没有及时响应。解决方法是给 DMA 中断分配高于串口、低于定时器的优先级并在 Overrun 中断里加恢复逻辑。加个保险HAL_ADC_ErrorCallback 里检测 Overrun并重新启动 ADC这是很多人忽略的后悔药。5.4 现象VREF 引脚悬空ADC 数值整体偏大且不稳定G474 有些封装把 VREF 单独引出没有和 VDD 直接短接。如果硬件工程师把它悬空或者接了一个滤波电容却忘了接基准源ADC 会以内部约 2.9 V 为参考而你的程序按 3.3 V 换算电压结果整体偏大。解决方法是确认硬件原理图VREF 建议直接接 VDDA或者用外部基准芯片供电。软件层面如果无法改板子可以在 G474 内部使能 VREFBUF将内部参考缓冲输出 2.5 V 给 VREF但精度受内部基准限制只能作为应急手段。5.5 现象采样周期设得越长数值反而越偏这是个看起来反直觉的坑。上升沿触发后ADC 首先对采样电容充电充电完成后进入逐次逼近比较阶段。如果采样周期很长信号源又有微小的直流偏置或热噪声采样电容会把这段偏置也充进去导致结果偏离真实值。另外从宏观角度看采样周期过长导致单轮扫描时间超出定时器触发周期数据持续被覆盖实际看到的是一个错乱的混合值。解决方法是掐表计算一轮扫描时间等于采样时间加 12.5 个固定周期的转换时间乘以通道数结果必须小于触发间隔。我这边的习惯是留 20% 余量比如 1 kHz 触发、采样时间 84.5 周期、ADC 时钟 42.5 MHz单通道一轮约 2.28 us远小于 1 ms完全没问题但如果触发频率提到 100 kHz就需要把采样时间压到 12.5 周期了。6. 验证手法与调试手段给采样数据做一次回环测试验证 ADC 采样链路是否可靠不要只看串口打印的数值漂不漂那只能说明链路通不通不能说明准不准。我习惯用 DAC 回环加已知电压点校验两步走。第一步用 G474 内部 DAC 输出一个确定电压比如 1.2 V用杜邦线接到 ADC 输入通道然后采样并换算回电压值。看误差在正负 5 mV 以内说明硬件链路和参考电压都正常。你可以用内部 DAC 从 0.2 V 开始每 0.2 V 步进到 VDDA 附近记录实测值和理论值的偏差偏差曲线如果大致平缓就说明 ADC 线性度没问题如果在某个区段出现台阶式跳动多半是参考电压不稳定或 PCB 地线处理不当。第二步把 DMA 数组里的原始值用调试器的 Live Expression 窗口实时观察。很多工程师只盯着串口输出的最终结果看不到采样序里的毛刺。调试器里拉出 adc_values 数组以图像模式查看波形毛刺是一眼就能看出来的。我遇到过一个案例采集链路输出数值非常稳定但波形里面有周期性的尖刺最后发现是定时器中断服务函数里做浮点运算拖得太长导致 ADC 触发边沿被延迟。这类问题光看平均值永远发现不了。关于采样周期的验证我自己养成的习惯是改完参数后不用示波器看模拟波形而是用 ADC 采一个干净的直流信号观察 LSB 级别的跳动值。比如 VDDA 干净、输入接精密电阻分压采样结果应在正负 2 个 LSB 内跳动。超过这个范围先加采样周期再加 DMA 搬运检查最后才怀疑运放电路。顺序反了容易把时间耗在玄学里。希望这些配置顺序和排查思路对正在调 G474 ADC 的你有所帮助。本文还有配套的精品资源点击获取