嵌入式ADC入门:从模拟信号到STM32实操,避开常见坑

发布时间:2026/9/6 8:52:32
嵌入式ADC入门:从模拟信号到STM32实操,避开常见坑 嵌入式 ADC 入门最容易踩的坑往往不是代码本身而是对“模拟世界”和“数字世界”之间那道转换关卡缺乏直觉。很多初学的朋友一上来就对着寄存器配置猛啃结果量出来的电压要么跳得离谱要么压根不对最后绕一大圈才发现是对 ADC 的工作原理理解有偏差。这篇内容就来把这层窗户纸捅破从模拟信号和数字信号的区别讲起一直拆到 ADC 外设在单片机内部到底怎么工作最后再给一套可以直接照着抄的 STM32 实操流程。这篇文章适合什么人群刚接触单片机和嵌入式开发的学生、从纯软件转嵌入式方向的朋友以及那些用 ADC 好几年但一直“只调函数不看原理”的工程师。看完之后你会对分辨率、参考电压、采样时间、DMA 触发这些概念有一个完整的认知后面不管换哪家芯片上手速度都会快很多。1. 先搞清楚模拟信号和数字信号到底差在哪1.1 从生活场景理解两种信号先别急着看数据手册我们用体温计来打个比方。传统的水银体温计液柱会停在一个连续的位置上36.5 度、36.55 度、36.555 度温度是连续变化的你看到的是一个“模拟量”。而电子体温计显示 36.5 度实际上内部已经把连续的温度转换成了离散的数字36.5、36.6、37.2一步一个台阶这就是“数字量”。模拟信号的核心特征是连续。电压也好、电流也好理论上在时间上和数值上都可以无限细分。一个 0 到 3.3V 的电压信号可以停留在 1.234567V 这种任意位置。数字信号的核心特征则是离散它用有限个状态来表示信息在嵌入式系统里就是一堆 0 和 1通常以二进制数的形式存在。那为什么现代电子系统几乎都要把信号变成数字再处理因为数字信号抗干扰能力强、便于存储和运算。一个 8 位数字值从 A 端传到 B 端只要电压不超出噪声容限读到的依然是那个数不会像模拟信号那样线一长、干扰一进来1.5V 就变成了 1.7V误差直接被放大。你想想看如果助听器、心电图机这类设备全都用模拟信号直接处理环境干扰稍微大一点输出就没法听了。1.2 为什么单片机非要“翻译”模拟信号单片机本质上是个数字处理器它内部跑的是时钟、寄存器、流水线所有运算都是基于一个个离散的二进制数。但现实世界的温度、压力、光照、声音、电池电压全是连续的模拟量。单片机没法直接“感知”1.234567V 这个电压它只能读“寄存器里存了什么数”。这时候就需要一个翻译官把模拟电压翻译成数字这个翻译官就是 ADC全称 Analog-to-Digital Converter模拟数字转换器。打个比方ADC 就像一台“电压翻译机”。你给它一个 0 到 3.3V 之间的电压它告诉你一个数字0 到 4095。为什么是 4095因为常见的是 12 位 ADC2 的 12 次方等于 4096 个台阶对应的数字范围是 0 到 4095。分辨率越高台阶越密翻译得就越精细。而“外设”这个概念在这里也很清晰了ADC 就是单片机内部集成的一个功能单元你不需要外接单独的芯片直接在引脚上采样就行。这也解释了为什么 ADC 是嵌入式开发里最基础也最重要的外设之一。几乎任何一个带传感器采集的项目温度监测、电压检测、电流采样、触摸按键、音频采集底层都离不开 ADC。你可以不搞通信、不搞图形界面但只要你做的是嵌入式ADC 迟早要碰。注意模拟信号接地处理非常重要。ADC 采样的参考地如果处理不好轻则读数偏大重则整个信号被噪声吞掉这在后面实操部分我会专门展开。2. ADC 的核心工作原理一次采样到底发生了什么2.1 采样、保持、量化、编码四个动作拆开讲一次 ADC 转换表面上看是“读一个寄存器”实际上内部经历了四个阶段。理解这四个动作比背寄存器名字有用得多。第一步是采样。ADC 内部有一个采样开关开关闭合的一瞬间输入电压被连接到内部的采样电容上电容开始充电直到电压和输入信号基本一致。这个充电过程需要时间也就是你在数据手册里看到的采样时间。采样时间太短电容没充满采出来的值就会偏低这就是很多初学朋友读到的 ADC 值不准的隐藏原因之一。第二步是保持。采样开关断开采样电容把电压“冻结”住。保持阶段的意义在于给后面的量化电路一个稳定的输入。如果边转换边让电压变化比较器正在比对的当口电压又跑了出来的结果就没有意义。所以保持电路实际上是在给“测量”制造一个稳定的窗口就像拍照片前先让被拍的人别动一样。第三步是量化。这是核心也是 ADC 精度损失的主要来源。量化就是把连续的电压值划分到有限个台阶上。一个 12 位 ADC参考电压 3.3V那么每个台阶对应的电压是 3.3V / 4096 ≈ 0.806mV。任何落在两个台阶之间的电压只能就近取整必然产生量化误差。关于这一点我要多说一句这个误差是原理层面无法消除的只能通过提高分辨率或者过采样来减小它不是代码 bug别总想着靠调软件把量化误差调到 0。第四步是编码。量化出来的台阶序号被编码成二进制数写入 ADC 的数据寄存器。CPU 或者 DMA 去读这个寄存器就拿到了一串数字。整个过程走完一个模拟电压才真正变成了单片机可以处理的数字量。2.2 逐次逼近型 ADCSAR为什么是主流ADC 的架构有很多种逐次逼近型SAR、积分型、流水线型、Sigma-Delta、Flash 型每种都有自己的适用场景。但你在通用单片机里见到的绝大多数是 SAR ADC也就是 Successive Approximation Register ADC。SAR 的工作原理可以理解成“猜数字游戏”。比如让你猜一个 0 到 4095 的数最快的方法是什么先猜 2048对方说大了就往小了猜小了就往大了猜每次猜中间值最多猜 12 次就能锁定答案。SAR ADC 就是这么干的内部有一个比较器和一个逐次逼近寄存器先从最高位开始试根据比较结果逐位确定每一位是 0 还是 1。SAR 架构为什么普及因为它能在分辨率、速度、功耗、面积之间取得很好的平衡。12 位分辨率、几 MHz 采样率、功耗也不夸张正好落在通用 MCU 的需求区间。相比之下Sigma-Delta 精度可以做到很高但速度慢更适合音频、称重这类应用Flash 型极快但面积和功耗大一般不会集成在普通 MCU 里。所以你在 STM32、GD32、NXP 的 S32K 这类芯片上看到的 ADC 外设基本都是 SAR 型。2.3 分辨率、参考电压与转换时间选型前先看懂三个参数搞清原理之后实际配置时最常碰到的三个参数必须掰开揉碎讲清楚。第一个是分辨率。它直接决定你能分辨多小的电压变化。12 位 ADC一个 LSB 等于参考电压除以 4096。如果参考电压是 3.3V一个 LSB 就是约 0.806mV。假设温度传感器输出电压每度变化 10mV那么理论上的温度分辨率大约是 0.08 度这就够用了。但如果你用一个 10 位 ADCLSB 变成约 3.2mV温度分辨率就掉到 0.32 度。分辨率不是越高越好还要看噪声、速度和成本但理解 LSB 的概念是所有估算的起点。第二个是参考电压。ADC 的测量范围完全由参考电压决定通常就是 VREF 和 VREF-。大部分开发板直接接 VDD 和 GND。这里有个容易被忽略的细节如果参考电压接的是电源 VDD而 VDD 本身有纹波这个纹波会直接变成测量误差。所以对精度要求高的应用要么用外部基准芯片要么至少把参考电源做干净。这一点我在多个项目里踩过焊好板子发现 ADC 读数发飘查了半天结果就是参考电源纹波太大。第三个是转换时间。它等于采样时间加上量化时间。STM32 的 ADC 时钟来自 APB2经过分频后最高约 36MHz每次转换需要固定周期加上可配置的采样周期。采样周期从 1.5 到 239.5 个周期可选具体数值要根据信号源阻抗来算。信号源内阻越高采样电容充电越慢采样时间就要越长。一个常见的经验值是高阻传感器、分压电阻网络后面接 ADC采样周期尽量取大一点比如 55.5 个周期这样采出来的数据稳定得多。3. 嵌入式外设视角从引脚到寄存器ADC 是怎么接入系统的3.1 ADC 外设在 MCU 内部的典型结构现在视角切换到芯片内部。你在数据手册框图里看到的 ADC 外设通常包含输入多路开关MUX、采样保持电路、逐次逼近转换器、数据寄存器、控制寄存器以及中断和 DMA 接口。输入多路开关是第一个入口。MCU 的 ADC 引脚往往不止一个STM32F103 的 ADC1 有 16 个外部通道但不意味着你有 16 套独立的转换电路而是共用一套核心转换单元靠多路开关轮流切换。如果你同时采集多路信号就要考虑通道切换带来的建立时间问题。通道刚切过去的时候采样电容里还残存着上一路信号的电荷直接读的话会产生串扰误差。解决办法是用扫描模式并保证每一路有足够的采样时间或者在软件上丢弃前几次采样。数据寄存器和标志位负责“交接”。转换完成后硬件把结果写入寄存器同时置位 EOC 标志如果开了中断就会触发中断服务函数如果配置了 DMA 则直接启动数据传输。这三条路——轮询、中断、DMA——是你从 ADC 取数据的三种方式各有适用场景后面我会详细说。理解了这个结构你就明白“ADC 外设”并不是一个黑盒子而是一组可以用寄存器控制的功能单元。3.2 触发方式与 DMA别让 CPU 干等新手最容易犯的错误是把 ADC 转换当成“调用一个函数马上就有结果”然后 CPU 死等。在简单场景下这没问题但在实时性要求高的系统里CPU 等待转换完成这件事本身就是在浪费算力。ADC 的触发方式分为软件触发和硬件触发。软件触发就是你往控制寄存器写一个启动位简单直接。硬件触发包括定时器触发、外部引脚触发等。比如你要在一个固定的 PWM 周期里采样电流就可以用定时器的更新事件去触发 ADC 转换让采样时刻和 PWM 波形严格同步。这个特性在做电机控制、电源控制时非常关键很多新手在做电机项目时电流波形毛刺多其实就是因为采样点和 PWM 开关时刻对不齐。DMA 是另一个解放 CPU 的关键。ADC 转换完成后结果自动被 DMA 搬运到内存数组里全部转换结束再产生一次中断通知 CPU。整个过程 CPU 该干嘛干嘛只有数组填满后才被打断一次。配合循环模式DMA 可以持续不断地把最新采样值搬进缓冲区CPU 随时去读即可这就是做连续采样、音频采集、振动监测这类应用的标配做法。提示ADC 的采样时间、转换周期、DMA 搬运三者之间的时序关系是配置中最容易出错的环节。建议一开始先用轮询模式把数据读对再上 DMA不要一上来就全开。3.3 多通道采样与扫描模式当你需要轮流采集多个通道时有两种思路一种是在单次模式下手动切换通道转换一路、切换一路另一种是开启扫描模式让 ADC 自动按通道序列依次转换。扫描模式的好处是把“切换通道”这件麻烦事交给了硬件软件只在全部转换完成后统一取数据。扫描模式下多个通道的转换结果存放在 ADC 的数据寄存器里但寄存器只有一个。如果你不开启 DMA每转完一路都得马上去读寄存器否则下一路结果会覆盖掉上一路。这就是为什么“多通道 扫描 DMA”总是打包出现的根本原因DMA 在每路转换完成的瞬间把数据搬走才能保证每一路结果都不丢。我见过不少同学配置多通道扫描但忘了开 DMA读出来的数据全是最后一路的值就是这个原因。多通道采样还有一个容易忽略的坑各通道信号源阻抗不同需要的采样时间也不同。扫描模式下所有通道共用配置好的采样时间就要按最慢的那路来设置宁可采样时间长一点也不能让慢信号源采不准。比如说一个通道接的是运放输出输出阻抗很低另一个通道直接接了一个 100kΩ 的分压器那整个扫描周期的采样时间就得按第二个通道来放宽。4. 手把手实操用 HAL 库配置 STM32 ADC 单通道采样4.1 硬件连接与引脚选择先搭一个最简单的硬件场景用 STM32F103 的 PA1 引脚读取一个电位器分压电压电位器的输入端接 3.3V滑动端接 PA1地端接 GND。这样转动电位器时 PA1 上的电压可以在 0 到 3.3V 之间变化正好覆盖 ADC 的量程。引脚选择上有一个原则尽量避开和其他外设共用的引脚避免信号互相干扰。比如你要同时用 SPI就要查一下数据手册里 SPI 引脚是否和 ADC 通道引脚复用了。PA1 在 F103 上对应 ADC1 的通道 1复用的外设比较少适合做实验。用杜邦线连接时线越短越好跳线不要和电源线、PWM 线扎在一起。硬件连接还要注意电源和地的处理。如果开发板是 USB 供电VDD 上往往会叠加 USB 的高频噪声建议在 ADC 参考电压附近加一个 100nF 的退耦电容采样引线尽量短。电位器这种高阻源对噪声比较敏感可以在引脚上并联一个 10nF 电容到地能明显改善读数的抖动。这一步很多人不做但实测定不稳跳得厉害的时候加个小电容往往立竿见影。4.2 CubeMX 配置要点时钟、通道、触发与 DMA用 STM32CubeMX 生成工程时需要配置的关键项有这么几个我按顺序理一遍。第一步是打开 ADC1选择通道 IN1。在 Parameter Settings 里分辨率选 12 位这是默认也是够用的。数据对齐方式选右对齐因为左对齐一般用于特殊场景右对齐读出来的值就是标准的 0 到 4095。扫描方向不用开因为现在只有一个通道。转换模式选单次还是连续取决于你的应用需求。我想看电位器的实时变化就选连续转换模式让 ADC 一直在转DMA 一直在搬。第二步是采样时间。CubeMX 里有个 Sampling Time 的下拉框可以选 1.5 到 239.5 Cycles。对于电位器分压这种中等阻抗源我一般选 55.5 Cycles这样既不会太慢也能给采样电容充足的充电时间。单纯求快就选 13.5但对精度要求高的场合不建议少于 55.5。第三步是 DMA 配置。在 DMA Settings 里添加 ADC1 的 DMA 请求模式选 Circular 循环模式。这里有个非常容易踩的细节ADC 的数据寄存器在 STM32F1 上是 32 位地址对齐的有效数据只占低 16 位CubeMX 默认生成的数据宽度是 Word32 位。如果你定义一个 uint16_t 数组来接收 DMA 数据每个 32 位传输会占掉两个 uint16_t 位置后半段全是无效数据读出来的数组会呈现出“有效值、0、有效值、0”的规律。最稳的做法是直接用 uint32_t 数组接收解析的时候取低 16 位。添加 DMA 后还要把 ADC 的 Continuous Conversion Mode 打开两者配合才能实现不间断的自动采样。4.3 代码实现轮询、中断、DMA 三种读取方式生成完工程核心代码其实就那么几段。先看最简单的轮询方式。uint16_t adc_value 0; float voltage 0.0f; HAL_ADC_Start(hadc1); // 启动 ADC if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); // 读取转换结果 } voltage (float)adc_value * 3.3f / 4095.0f; // 换算成实际电压轮询方式适合主循环里对实时性要求不高的采样任务逻辑清晰排错容易。第 4 行的换算公式就是把数字量还原成模拟电压adc_value 除以 4095 再乘参考电压 3.3V。很多新手写成了除以 4096问题也不大但严格来说 12 位 ADC 的满量程对应值就是 4095习惯上用它做分母更准确。再看 DMA 方式这是实际项目中最常用的。#define ADC_BUF_SIZE 64 uint32_t adc_buf[ADC_BUF_SIZE] {0}; // 注意用 32 位数组接收 HAL_ADC_Start_DMA(hadc1, adc_buf, ADC_BUF_SIZE); // DMA 传输完成回调在中断上下文调用不要在这里做耗时操作 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // adc_buf 里每个元素的低 16 位是有效的采样值 // 建议只置标志位真正的数据处理放到主循环 g_adc_ready 1; } }这里要提醒一句HAL_ADC_Start_DMA 的第三个参数是长度它代表你要搬运多少个数据单元而不是字节数。结合前面说的 32 位数组这里填的 64 就代表 DMA 会往数组里搬 64 个 32 位数据。很多人在这里翻了车填了 64 * 4 字节结果 DMA 把数据写到了数组外面看不见的地方程序行为变得非常诡异。回调函数里也尽量只置标志位别在中断上下文做浮点运算、打印这类操作。4.4 数据滤波对抗噪声的第一道防线就算配置全对ADC 读数还是会抖动。抖动来自几个方面信号源本身有噪声、参考电压有纹波、采样电容和引线形成了天线效应。这时候需要软件滤波。最常用的方法有中值滤波、滑动平均滤波、一阶低通滤波。滑动平均滤波适合大多数缓慢变化信号。维护一个长度为 N 的环形缓冲区每采一个值就计算最近 N 次采样的平均值。N 越大越平滑但响应也越慢。对温度、电池电压这类慢变量N 取 16 或 32 效果不错对需要在控制回路里快速响应的信号N 取 4 到 8 就够了。一阶低通滤波适合有控制闭环的场景实现也非常简单。float filtered 0.0f; float alpha 0.2f; // 滤波系数越大响应越快越小越平滑 while (1) { uint16_t raw get_adc_value(); filtered filtered alpha * ((float)raw - filtered); // 用 filtered 参与后面的业务逻辑 }这里 alpha 的取值要按采样周期和信号频率来调。一个参考经验是如果你需要跟踪的信号变化频率是 10Hz采样频率是 1kHzalpha 大概在 0.06 到 0.1 之间如果只是滤掉高频毛刺0.2 已经是比较激进的取值。滤波参数的调定本质上是“平滑度”和“响应速度”之间的权衡没有万能值要按实际波形去调。5. 常见问题与排查技巧实录5.1 ADC 读数不准、跳变严重这是被问到最多的问题没有之一。读数跳变的排查顺序应该是先查硬件噪声再查采样时间最后查软件逻辑。硬件层面拿示波器看引脚波形如果毛刺很大优先检查退耦电容、接地和线长。我做过一个案例ADC 读数在某个数值附近上下跳 30 个 LSB最后发现是采样引脚旁边走了一根 I2C 数据线时钟边沿直接耦合进了模拟通道。把走线分开后跳变从 30 降到 2 个 LSB 以内。这提醒我们布局时模拟信号线和数字信号线要拉开距离模拟地和数字地要单点连接。软件层面先排除采样时间不足的问题。信号源内阻高时采样电容充不满现象是读数偏低而且源阻抗越高偏得越狠。把采样时间从 1.5 Cycles 改到 55.5 Cycles很多“读数偏低”的诡异问题就消失了。如果做了这些读数还是跳再用中值滤波兜底但别把滤波当成掩盖硬件问题的万能药。5.2 采样值一直为 0 或满量程这个问题的排查思路完全不同。如果读数始终为 0优先怀疑引脚配置有没有开模拟功能。GPIO 在复位后默认可能是浮空输入但你配置的是模拟输入如果 CubeMX 配置正确一般不会出这个问题。其次是检查参考电压和地有没有接好。如果读数为满量程 4095大多是输入引脚根本没信号处于悬空或浮空状态ADC 采到了一个随机的浮空电压。还有一种容易让人迷惑的情况引脚配置正确信号也接了但读数一直是 0。这时候要看你是不是选错了 ADC 实例和通道。F103 的 ADC1 通道 1 对应的是 PA1你要是把杜邦线接到了 PA0 却配置了 IN1读出的自然是 0。这种低级错误占了我遇到问题的一半以上建议先检查硬件连接对应关系再动代码。5.3 多路采集串扰问题多通道扫描模式下两路信号互相串扰是常见坑。典型现象是通道 A 的电压会影响通道 B 的读数或者两个通道读数都一样。原因一般是通道切换后采样电容上还残留上一路的电压而采样时间又不够残留电荷没有充分泄放。解决办法有这么几条一是加长采样时间给电容更多时间“忘掉”上一路的电压二是对不关心的通道也配置合理的采样时间不要配置成最短三是优化切换顺序把信号阻抗接近的通道放在相邻位置因为通道切换后电容要追赶新电压如果新旧电压差很大需要的时间就更长。实测下来把采样时间设到 55.5 Cycles 以上相邻通道之间的串扰就能压到可以接受的范围。5.4 快速排查速查表现象优先排查方向常见处理读数跳变剧烈噪声、接地、采样时间加大退耦电容、缩短引线、增加采样周期读数偏低采样时间短、源阻抗高采样周期设 55.5 Cycles 以上读数恒为 0引脚配置、通道对应关系核对 ADC 通道与引脚映射、检查模拟输入配置读数恒为 4095信号悬空、参考电压问题确认信号接入、检查 VREF多通道相互串扰采样时间、切换顺序加大采样周期、按阻抗排序通道DMA 数据只更新一半长度或数据宽度参数错误长度填采样个数而非字节数、确认数组位宽这张表是我在实际项目中反复用到的排查顺序按表排查能省很多时间。如果你遇到的问题不在表里多半是我们前面分析过的对齐、触发或时序问题建议从 CubeMX 的配置截图开始逐项核对。6. 进阶方向与一些个人体会6.1 从会用到底层值得深入的方向如果这篇内容你已经完全消化接下来有几个方向值得继续深入。第一个方向是过采样技术。用软件手段进一步提高有效分辨率以一个 12 位 ADC 为例通过 4 倍过采样加平均可以得到额外 1 位有效分辨率16 倍过采样加平均可以得到额外 2 位。这个技术在音频和测量类项目中很实用但在使用时要注意采样速度和噪声特性不是所有场景都适合过采样本质上是用速度换精度。第二个方向是 ADC 与 RTOS 的结合。在带 FreeRTOS 的项目里最忌讳把事情都堆在主循环里。正确做法是建一个采样任务和一个处理任务用消息队列传递转换结果外部事件用二值信号量通知。像“rtos3.0外设”“嵌入式wifi断线重连怎么弄”这类问题本质上都是外设如何在 RTOS 环境下正确工作的问题ADC 也是外设之一理解了任务间通信很多问题就迎刃而解。第三个方向是从寄存器级别深度理解。有了 HAL 库的基础之后强烈建议读一下同系列芯片的参考手册和 ADC 寄存器描述看懂 HAL_ADC_Start 背后到底写了哪些寄存器。你会发现所有封装库函数最终都是在对一堆寄存器操作理解了这层换芯片、移植代码、排查疑难问题都会轻松很多。6.2 个人实操中的一些体会最后分享几个我这些年积累的体会希望对你有帮助。第一个体会调试 ADC 问题顺序永远是从硬件到软件从简单到复杂。先确认硬件连接、引脚、电压都正常再怀疑代码。最怕一上来就觉得是代码问题改来改去浪费一整天。我见过太多所谓“玄学 ADC 问题”最后都是硬件接触不良或引脚接错。第二个体会修改采样参数时一次只改一个变量。比如你想验证采样时间对读数的影响那就只改采样时间其他保持不动。这样做对比才有意义。ADC 配置的参数之间相互影响一次改多个参数出了问题你根本不知道是哪个导致的。第三个体会自己写的滤波代码一定要在真实信号下验证不要在理想波形下验证。真实信号带噪声、带漂移、带毛刺有时候你以为滤波效果很好的代码接到真实硬件上才发现响应太慢或滤不掉毛刺。多拿示波器对比滤波前后的波形逐步调整参数比纸上谈兵有效得多。第四个体会对精度要求较高的应用强推外部基准电压芯片。内置参考电压的温度漂移和电源相关性再好也比不上专用基准如果你做的产品需要跨温度工作VREF 的影响很快就能体现出来。不要嫌加一颗芯片占面积稳定才是第一位的。嵌入式 ADC 这块内容说难不难说简单也不简单。难就难在它是模拟和数字两个世界的交界处既要懂硬件又要懂软件简单在于原理就那么几件事采样、保持、量化、编码再加上触发和搬运理解透了剩下的就是熟能生巧。希望这篇从零写的入门指南能帮你把这条线彻底打通也欢迎你在实际项目中踩到有意思的 ADC 问题时回来和我聊聊你的排查过程。