
简介一份针对STM32初学者的ADC单通道连续转换工程源码配套标准外设库与多个硬件驱动模块帮助理解STM32模拟量采集、连续转换模式配置及中断/轮询处理流程。压缩包共80个文件含36个头文件、34个C源文件并包含工程配置文件、启动文件、链接脚本等整体仅286KB结构易读。工程内除了ADC核心代码还整合了SYSTEM延时、FWLIB标准库以及HARDWARE层的按键、蜂鸣器、LED、光敏传感器、OLED显示等驱动便于在开发板上直接验证或二次开发。已有91人学习下载从文件组织看分CORE、SYSTEM、FWLIB、HARDWARE、USER等模块目录层次清晰适合对照数据手册逐行理解寄存器或库函数调用逻辑。对于刚接触STM32的开发者这套代码能省去搭建工程与移植驱动的时间直接聚焦ADC单通道连续转换的实验现象也适合作为课程设计或毕业设计的前期基础。 做单片机开发这几年ADC是躲不开的外设。我见过太多人在处理模拟信号采集时第一版代码都是调一次HAL_ADC_GetValue读个值然后发现读数飘得没法看。我最初做电池电压监测也这样后来认认真真把ADC单通道连续转换这块弄明白才算把数据采稳。这篇就以STM32平台为主线把单通道连续转换从配置到编码、从滤波到避坑完整梳理一遍顺便带上一些跨平台差异。适合正在学STM32、或者被ADC采样数据抖动折磨的嵌入式开发朋友。1. 单通道连续转换到底解决了我什么问题1.1 最基本的区分单次、连续、扫描各是什么很多教程把ADC的三种模式混在一起讲新手很容易绕晕。我习惯用一个简单的类比单次转换就像用手机拍一张照片拍完就停连续转换就像录像一直在录扫描模式则是把几个摄像头轮流拍一遍。在STM32的HAL库里这几个开关是分开配置的Scan Conversion Mode是否启用扫描模式。单通道场景下这个开关其实可以不打开多通道才需要。Continuous Conversion Mode是否启用连续转换。这是本文的核心开关打开它之后ADC完成一次转换立刻自动开始下一次不需要软件再次触发。Discontinuous Conversion Mode间断模式。这个一般用不上初学者可以直接忽略。单通道连续转换就是把Continuous Conversion Mode打开固定采集一个通道的值让ADC硬件持续不断地出数据。这样MCU不需要在转换这件事上频繁操心数据源源不断。1.2 为什么需要连续转换而不是循环调用单次转换最常见的直觉方案是主循环里不停调用HAL_ADC_StartHAL_ADC_PollForConversionHAL_ADC_GetValue循环读取ADC值。这个方案不是不行但有几个实际问题。第一软件开销大。每读一次值都要启动一次转换启动ADC转换需要往寄存器写控制位然后轮询等待EOC标志中间还有函数调用开销。一次两次无所谓如果主循环里还要处理显示、按键、通信采样率会被软件逻辑拖得极不稳定。第二采样间隔不均匀。主循环里有条件判断、延时函数上一轮循环可能3ms采一次下一轮可能8ms才轮到采集到的数据时间轴是零乱的。连续转换由硬件自动触发下一次转换转换间隔完全由ADC时钟和采样周期决定时间轴均匀这对后续做数字滤波和信号分析非常重要。第三连续转换模式下可以配合DMA转换结果由硬件自动存进内存CPU几乎不参与这是单次转换循环方案做不到的。1.3 连续转换的适用场景和不适用场景连续转换适合以下场景电池电压、电源轨电压的持续监测模拟传感器的连续数据采集温度、湿度、光强等缓变信号音频采样、振动信号采集这种需要连续数据流的场景峰值检测、包络检测不太适合的场景超低功耗待机中偶尔醒过来读一次电压这种情况直接单次转换转换完立刻进停机模式没必要让ADC持续跑需要精确在某个时间点采样的场合比如电机控制里ePWM触发ADC采样这种应该用外部触发单次转换而不是自由运行的连续转换一个引脚要检测多个按键组合电阻分压的场景这种需要的是单次转换加软件消抖连续转换反而会读到中间态电压搞清楚这些边界后面配置CubeMX的时候心里就有底了。2. STM32CubeMX里的ADC配置参数逐项拆解2.1 时钟树ADC时钟从哪里来分频后多快很多人配置ADC只盯着Channel和Sampling Time忽略了对ADC外设本身的时钟设置。ADC不是一个独立时钟源的外设在STM32上它通常挂在APB2桥上然后通过预分频器分频得到ADC时钟。以STM32F103这样比较经典的型号为例APB2最高72MHzADC预分频可选2、4、6、8分频最终ADC时钟建议不要超过14MHz。我习惯配置到12MHz留一点余量。ADC转换有固定节奏单次转换时间 采样周期 转换周期。对于12位分辨率的逐次逼近型ADC转换周期固定是12.5个ADC时钟周期。比如把Sampling Time配置为55.5个周期那么总周期数 55.5 12.5 68个周期在12MHz ADC时钟下单次转换时间约为5.67微秒对应采样率约170kSPS左右。这个计算逻辑我建议每个做ADC的都自己算一遍后面判断采样率够不够用、要不要降采样周期都是基于这个公式。2.2 核心参数分辨率、扫描、连续、采样周期、对齐CubeMX里ADC的配置项主要有这些我用一张表拆解配置项常用选项说明Resolution12位/10位/8位/6位位数越高精度越高转换时间也越长一般用12位Scan Conversion ModeEnable/Disable单通道场景可不打开多通道必须打开Continuous Conversion ModeEnable/Disable本文核心单通道连续转换要打开Discontinuous Conversion ModeDisable常规项目用不上保持默认End of Conversion SelectionEOC Flag/Sequence Flag单通道用EOC Flag即可External Trigger软件触发/定时器触发/外部引脚连续转换一般选软件触发启动第一轮AlignmentRight/Left12位右对齐最常用方便直接用变量读取Sampling Time1.5~239.5周期采样时间越长采集越准但速度越慢这里重点说下采样周期。ADC内部通过采样开关对输入电压充电开关闭合时间就是采样时间。如果信号源内阻较大、外部滤波电容比较小采样时间太短会导致电容没充到位转换结果偏小。常见做法信号源内阻是几百欧到几K欧级别采样周期用55.5或以上比较稳如果做高频信号采集才考虑用1.5个周期这种短采样。2.3 校准与参考电压的基本观念STM32的ADC内部有比较器和电容阵列芯片出厂时会有工艺偏差。大部分系列上电后需要做一次校准HAL库封装成了HAL_ADCEx_Calibration_Start这个函数做的是内部自动校准把增益误差尽量修掉。很多例程不写校准导致ADC读数整体偏大或偏小我实测过不校准和校准能差十几个LSB对做阈值判断的项目来说十几个码值可能就影响结果了。参考电压VREF同样关键。STM32F103这类芯片的VREF通常直接接VDDA也就是3.3V此时12位ADC每个LSB对应的电压 3.3V / 4096 ≈ 0.8mV。如果板子上的VDDA没做好滤波供电纹波会直接耦合到ADC结果里。自己画板时务必在VDDA和VREF引脚加LC滤波或至少加一个1uF陶瓷电容这个细节经常被忽略。3. 从初始化到数据读取HAL库连续转换的标准写法3.1 两种读取方式轮询和DMA先想清楚再选硬件配置好之后代码层面有两种主流方案轮询读取和DMA搬运。轮询读取的流程是启动ADC之后在循环里不断查询转换完成标志一旦置位就读取数据寄存器。这个方案代码简单适合采样率不高、主循环不繁忙的场景。但连续转换模式下有个隐患如果主循环光顾着处理其他任务一段时间没来读数据寄存器已经被后来的转换结果覆盖了读到的是“最新状态”而不是“每个状态”。DMA搬运则是把ADC数据寄存器通过DMA通道自动搬到内存变量里CPU全程不用管。ADC每次转换完DMA自动把数据送到指定数组数组满了还可以配置为循环模式继续写入。这个方案连贯、稳定不丢数据适合连续转换的常态使用。两者的取舍很清楚方案CPU开销数据完整性适用场景轮询读取高循环里空转等待可能丢中间值慢速采集、主循环简单DMA搬运极低硬件自动搬数据完整不丢连续转换首选3.2 轮询模式代码与转换时间计算轮询模式先直接给标准写法// 启动ADC单通道连续转换 HAL_ADC_Start(hadc1); while (1) { if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { uint16_t adc_val HAL_ADC_GetValue(hadc1); // 处理adc_val } }注意这里HAL_ADC_PollForConversion第二个参数是超时时间单位毫秒。如果10毫秒内没有等到EOC标志函数返回超时。连续转换模式下每次转换完成后EOC标志会自动置位轮询基本都能成功。但这个写法只适合采样率不高、对数据连续性要求不苛刻的项目。比如你在主循环里同时处理OLED刷新刷新一帧可能耗时20毫秒这期间ADC已经完成了数千次转换HAL_ADC_GetValue读到的只是最近一次的结果中间的变化根本没有被记录。3.3 DMA模式代码与缓存设计DMA模式是连续转换的正确打开方式代码反而更简洁uint16_t adc_buf[16]; // 启动ADC的DMA传输每转换一次自动搬一个数据到adc_buf HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 16);CubeMX配置时把ADC的DMA Continuous Requests设为EnableDMA模式设为Circular这样adc_buf会被不断填入最新数据。通过回调函数来消费数据void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // 到这里adc_buf里已经积累了一轮数据 // 可以做滤波、计算、阈值判断 } }缓存大小按需设置。16个uint16_t一共才32字节放在普通内存就行。如果想做更精细的滤波可以加大到64或128配合DMA半传输中断做双缓冲处理这里先不展开。3.4 为什么连续转换配DMA才是常态我用实际项目总结的结论连续转换的真正价值只有在DMA配合下才能体现出来。原因很直接。连续转换让ADC硬件自主跑步DMA让数据自动落袋CPU只需要在数据攒够时处理一轮。这样ADC采样率可以做得比较高还能保证主循环的实时性不被打乱。我见过不少做数据采集的工程开头只用了轮询后来为了增加采样率把代码改成DMA模式整个系统流畅度提升非常明显。配合DMA之后数据已经到了内存怎么把数据变成稳定可用的数值就是下一个问题。4. 连续数据的抖动问题滤波才是决定体验的关键4.1 实测的原始数据长什么样连续转换模式跑起来之后如果直接把ADC原始值通过串口打印出来你会看到即使输入电压是稳定的读数也会有小幅跳动。比如电位器分压得到一个理论1.65V的电压12位ADC上对应理论值2047附近实际读出来可能是2041到2053之间随机跳动。这个跳动来源很多参考电压本身有纹波、外部信号源有噪声、PCB上走线耦合进来的干扰、ADC本身的量化误差。所以滤波不是可有可无而是ADC采集必备的一步。4.2 滑动平均和中值滤波怎么选针对连续转换数据有两种最常用的滤波方案滑动平均连续取N个采样值求平均对高斯白噪声抑制效果好但对突变信号有滞后中值滤波连续取N个采样值排序取中间值对尖峰脉冲干扰抑制效果好我做慢速传感器采集温度、电池电压时用滑动平均N取8到16。做电流采样或者保护电路检测时用中值滤波N取5或7刚好能滤掉偶然的毛刺。两者的对比可以参考这张表滤波方式抗白噪声抗尖峰脉冲滞后程度计算开销滑动平均强一般中等低中值滤波一般强较大中需要排序一阶低通EMA强一般可调极低4.3 一阶低通滤波代码与参数整定嵌入式系统里最常用的是指数移动平均滤波也就是一阶低通。代码很短运算量极低适合在主循环里频繁调用。#define ADC_EMA_ALPHA 8 // 0~256数值越大响应越快 uint32_t adc_ema 0; uint32_t adc_filter(uint32_t adc_raw) { if (adc_ema 0) { adc_ema adc_raw; } else { adc_ema (adc_ema * (256 - ADC_EMA_ALPHA) adc_raw * ADC_EMA_ALPHA) 8; } return adc_ema; }ADC_EMA_ALPHA决定新值占的比重。Alpha为8时每次新值约影响总值的3%对噪声比较柔和大概要采样几十次才能逼近真实值Alpha为32时响应快很多但滤波效果会变差。这个参数的整定路子是先小后大看实际波形调整滤波后的数据波动控制在2~3个LSB以内同时响应延迟能接受即可。这里有个重要提醒滤波参数不能盲目套用。采样率5kSPS和50kSPS的场景同样Alpha值效果天差地别因为单位时间内的样本数不同。4.4 单个ADC引脚检测多按键的特殊处理热词里提到的“单ADC引脚检测4键组合16种状态”是电阻分压网络配合ADC的经典应用。多个开关并联不同的分压电阻按下不同按键时ADC引脚电压不同读出不同的ADC值就能判断状态。这个场景用连续转换时有个特殊问题按键按下瞬间会经过一个中间电压变化过程连续转换可能采集到这个中间值导致状态误判。解决思路是加状态判定逻辑——连续读到同一个ADC区间多次并且每次间隔一定时间才认为按键状态稳定。具体实现可以用一个简单的状态机记录上一次有效键值连续N次采集值落在同一判定区间才更新状态。本质上这和按键消抖是一个道理。5. 连续转换模式最容易踩的四个坑5.1 坑一把外部触发和连续转换混在一起很多人配置了定时器触发ADC采集期望每个定时器周期到点时来一次采样于是同时打开了Continuous Conversion Mode结果发现ADC一直在转换完全不受定时器控制采样率也根本不是预期值。原因在于连续转换的含义是一轮转换结束后自动开始下一轮外部触发只负责启动第一轮。如果开了连续模式定时器触发的意义就被稀释了它只是拉响了起跑哨之后ADC自己一直跑。要做到“每隔固定时间采一次”应该用单次转换模式外部触发转换完成后再由软件等待下一个触发源或者用注入通道组来配合。5.2 坑二轮询读取连续转换时拿到的数据滞后使用轮询方式读取连续转换结果时如果主循环不够快数据寄存器可能已经被覆盖了好几轮。HAL_ADC_GetValue拿到的不是真正“当前时刻”的数据而是最近一次硬件转换结果。这个坑的特点是不容易发现——数据看起来是对的电压读数也能对上但如果拿去做FFT或者波形分析点与点之间的时间间隔是乱的。排查方法用逻辑分析仪或示波器同时观察EOC引脚如果有映射和主循环里的某个GPIO翻转信号用GPIO翻转标记“读取时刻”你就会发现读取节奏和ADC转换节奏完全对不上。我的建议很简单采样率低、主循环固定时轮询还能凑合一旦需求要求高速或连续采集直接上DMA别在轮询上浪费时间。5.3 坑三没有做上电校准读数系统性偏移STM32大部分系列在上电后应该执行一次HAL_ADCEx_Calibration_Start(hadc1)这个调用会校准ADC内部比较器的失调。很多从例程抄来的代码根本没做这一步导致ADC读数整体偏高或偏低。我有一次排查电池电压测量不准硬件工程师反复检查分压电阻结果问题出在软件没做ADC校准。同一块板子执行校准前后电压读数差了约30mV对精度要求不高的项目还能忍做电量计量的项目就会出大事。正确写法是在ADC初始化完成后、开始转换前执行校准// 初始化完成后执行 HAL_ADCEx_Calibration_Start(hadc1); // 再启动转换 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 16);5.4 坑四多通道扫描连续转换时缓存顺序错位有些项目从单通道扩展到多通道时把Scan Conversion Mode打开配置了多个通道然后沿用单通道连续转换的思路。结果发现DMA缓存里的数据和通道对不上号比如把通道1的数据当成通道0的来用。原因在于多通道扫描模式下ADC按序列顺序逐通道转换DMA缓存里的数据排列也按通道顺序来。如果你配置了2个通道DMA缓存大小为16那么数组下标0、2、4是通道0的数据下标1、3、5是通道1的数据。如果用循环模式DMA会持续写入缓存里每隔一个元素就是一个通道的数据不按规则解析就会错乱。碰到这种情况要么把缓存按通道数分开要么在DMA全传输中断里按通道索引解析。最简单的方式是多通道就用多路DMA缓冲或者干脆用扫描模式时缓存大小设为通道数的整数倍然后按index % channel_count来取对应通道的值。6. 跨平台扩展ESP32、S32K、C2000的连续转换思路6.1 ESP32的ADC连续模式差异ESP32的ADC驱动和STM32的HAL库差距比较大。ESP-IDF里专门有一套adc_continuous接口配置时要设置adc_continuous_handle_cfg指定max_store_buf_size、conv_frame_size这些参数采样结果通过DMA搬进一个大缓冲区然后调用读函数按块取出数据。有一个非常大的差异点ESP32的ADC输入电压检测范围和衰减配置需要显式设置衰减档位决定了量程而STM32通常只需要配置参考电压和采样周期。做跨平台移植时习惯STM32思维的去配ESP32很容易漏掉衰减配置导致电压超过量程时读数被削顶。6.2 车规MCU和电机控制里的连续转换热词里出现了S32K312、TC49X、280049这类车规或电机控制芯片这些平台的ADC配置和STM32很不一样。以TI C2000系列为例它的ADC通常由ePWM模块硬件触发做电机电流环时要求PWM周期同步采样因此连续转换不是重点精确的触发时机才是重点。S32K3系列的ADC支持自检、通道复用、触发源选择逻辑和STM32更接近但配置在MCAL层的Adc驱动里通过Adc_SetupResultBuffer和Adc_StartGroupConversion这类接口控制。车规代码强调确定性连续转换的使用边界比MCU裸机开发要严格得多一般还是外部事件触发为主。6.3 RFSoC这类超高速ADC的思维差异热词提到RFSoC很多ADC和DAC每个tile都有时钟输入。RFSoC这类射频采样ADC的工作方式跟MCU内置SAR ADC完全是两个世界采样率在GSPS级别数据通过JESD204B高速串行接口流式传输到FPGA不存在“转换完成CPU读寄存器”这一说数据是源源不断的流。MCU上理解“连续转换”本质是硬件自动起停、DMA搬运、定时去读RFSoC里“连续”从时钟进入芯片那一刻就成了常态软件关心的是数据通路怎么对齐、怎么处理海量流式数据。这两种系统对“连续”的解决思路完全不同但底层逻辑是一样的用硬件替代CPU去维持采样的持续性和稳定性。我个人的体会是无论哪个平台ADC连续转换的核心思想都是相通的——让硬件自动完成数据采集和搬运软件专注于数据处理本身。刚开始上手时先画一条时序线ADC时钟 → 采样周期 → 转换周期 → 数据寄存器 → DMA搬运 → 滤波输出把每一段的耗时或软件开销标出来很多问题一眼就能看懂。最后分享一个调试小技巧开发初期把滤波前的原始数据和滤波后的数据都通过串口或者调试器实时打印出来对比观察一段时间再决定滤波参数。千万别把滤波算法写死之后再去调参数那样你根本不知道滤波到底丢了什么信息。连续转换只是第一步后续做阈值判断时最好再加滞回逻辑比如电压高于3.1V判高、低于2.9V判低中间区间维持原状态否则哪怕有滤波状态切换也会出现抖动。这套组合在项目里跑下来非常稳推荐你也试试。本文还有配套的精品资源点击获取