news 2026/9/27 20:28:37

STM32 ADC从原理到实战:采样时间、DMA多通道与滤波调试指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC从原理到实战:采样时间、DMA多通道与滤波调试指南

做嵌入式这几年,ADC这个外设几乎出现在每块板子上。单片机之间传数据走的是数字量,而温度、压力、光照、电流这些物理量天生是连续的模拟信号,要让STM32认识它们,就必须先把模拟量数字化——这就是ADC(模数转换器)的核心任务。也正因为如此,ADC怎么用、用得好不好,直接决定了一个采集类项目能不能落地。这篇文章把STM32的ADC从原理到应用编程完整过一遍,包括逐次逼近型内部结构、分辨率与采样时间这些关键参数、HAL库配置代码、DMA多通道采集、软件滤波和常见坑。

先把结论放在前面:ADC本身并不难,真正折磨人的是它的周边——参考电压干不干净、输入阻抗搭不搭、采样时间够不够、DMA有没有对齐。这篇文章会沿着这个顺序一层层往下走,适合刚开始学STM32的朋友,也适合已经在做传感器采集、电源监测、仪器仪表类项目的工程师,当成一份排查手册来翻。

1. ADC原理:模拟量到数字量,中间发生了什么

1.1 逐次逼近型(SAR)是怎么一路“猜”出来的

STM32主流型号搭载的ADC大多采用逐次逼近型结构(SAR,Successive Approximation Register),这种结构在分辨率、速度和功耗之间取得了不错的平衡,适合绝大多数MCU场景。它内部不算复杂,核心就是比较器、数模转换器DAC网络、逐次逼近寄存器SAR,外加一个采样保持电路。

你可以把它的工作过程想象成一个“猜价格”游戏:主持人心里定了一个0到4095之间的数字,你每次报一个数,他只告诉你“猜高了”还是“猜低了”,你要用最少的次数把这个数猜中。最优策略闭着眼都知道——二分法:先猜2048,如果高了说明真实值在0~2048之间,再猜1024;如果低了说明真实值在2048~4095之间,再猜3072。每猜一次把范围缩小一半,12位精度最多猜12次就能锁定结果。

SAR ADC干的事完全一样。转换开始前,采样保持电路先把输入电压的瞬时值“冻结”到采样电容上,然后SAR寄存器开始干活:先把最高位置1,让内部DAC输出一个Vref/2的参考电压,比较器把这个参考电压和采样到的输入电压做比较。如果输入更高,最高位保留为1,说明真实电压落在上半区间;否则清零最高位,落到下半区间。接着用同样的逻辑去试探下一位。每一位试探都需要一个ADC时钟周期,12位精度就是12个比较周期,转换完成后,SAR里的二进制码就是最终的转换结果。

这里有个容易忽略的点:采样保持电容在充电瞬间会从输入端“抽取”电荷,如果信号源的内阻太高,电容没充满就开始比较,结果必然偏低。很多新人只盯着转换时间看,却忽略了输入源阻抗,结果读到的值总差那么几个LSB,怎么查都查不出来,根子其实就在采样阶段。

1.2 分辨率、参考电压与量化误差:读数精度的天花板

STM32常见的ADC是12位,意思是把参考电压分成4096格。这个“刻度尺”的粗细决定了最小分辨率:如果参考电压Vref+是3.3V,那么1个LSB(最低有效位)就是3.3 ÷ 4096 ≈ 0.8057mV。读到一个码值比如3800,对应的电压大约是3800 × 0.8057mV ≈ 3.06V。

很多人用3.3/4096算系数,这个思路没错,但忽略了一件事:板子上实际的参考电压并不一定严格等于3.3V。F103的VDDA和Vref+通常直接接在3.3V电源上,可LDO输出本身有温漂,线路压降也可能让参考电压落到3.27V。这时候再用3.3做分母去反推真实电压,算出来的结果自然信不过。严谨的做法是使用片内的VREFINT通道去反推实际VDDA,或者在硬件上接一颗精密基准源。

量化误差则是ADC的“出身缺陷”。输入电压是连续量,但输出码值只有4096个整数值,任何落在两个码值之间的电压,都只能四舍五入到某一个整数上。这个误差最多±0.5 LSB,是结构性的、无法通过滤波消除的随机误差。滤波能滤掉的是叠加在信号上的随机噪声,滤不掉的是量化误差和系统性的增益偏差,这一点想明白,后面做校准时才不会瞎折腾。

不同位数的ADC,最小分辨率差距很大,直观感受一下:

ADC位数满量程档位数3.3V参考下的1 LSB电压
8位256约12.89mV
10位1024约3.22mV
12位4096约0.81mV
16位65536约50μV

位数越高,分辨能力越强,但成本和转换时间也会上去。STM32的12位ADC对于电池电压、环境温度、普通压力传感器这类应用已经完全够用,没必要盲目追求高位数。

1.3 采样时间与转换时间:快和准之间的权衡

ADC的时钟频率不是随便给的。F103的数据手册规定ADC时钟最高不能超过14MHz,一般工程上配置为12MHz,也就是APB2的72MHz做6分频。想用2分频得到36MHz?不行,直接超规格。

一次完整的转换由采样阶段和转换阶段组成。转换阶段固定消耗12.5个ADC时钟周期,而采样阶段可以通过寄存器配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个周期这几个档位。于是最短转换时间就是(1.5 + 12.5) ÷ 12MHz ≈ 1.17μs,最长则是(239.5 + 12.5) ÷ 12MHz ≈ 21μs。差距近20倍,怎么选其实取决于输入端源阻抗。

为什么源阻抗会影响采样时间?因为采样电容要充满电才能准确反映输入电压。一阶RC充电模型下,充到12位精度大概需要8个左右的时间常数。假设外部源阻抗是47kΩ,内部开关等效电阻约1kΩ,采样电容按8pF估算,时间常数τ≈48kΩ × 8pF ≈ 384ns,8个τ约3μs,换算成ADC时钟就是3μs × 12MHz ≈ 36个周期。这时候选1.5周期的采样时间,读数必然偏低,至少得选55.5周期那一档才稳。

所以选采样周期第一原则是:慢不怕,但一定要让电容充满。实际项目里我一般是先按55.5周期起步,确认读数稳定后,再根据信号变化速度去逐档压缩。不要一上来就追求最快采样率,那只是在给排查问题制造难度。

2. 读懂STM32的ADC外设:通道、序列与触发

2.1 引脚、规则组和注入组:ADC也有自己的“任务队列”

STM32的ADC不像独立ADC芯片那样只有两三个输入引脚,它内部自带一个多通道模拟开关。拿F103C8T6来说,ADC1有PA0~PA7、PB0~PB1共10个外部通道,外加一个内部温度传感器通道和一个内部基准电压VREFINT通道。每个引脚被复用为ADC输入时,对应的GPIO必须配置成模拟输入模式,这一点经常有人漏掉——配成复用推挽或者浮空输入,采样结果都会出问题。

通道之上还有“规则组”和“注入组”两套概念。规则组可以理解成一个普通的排队序列,最多能编排16个转换项,可以重复编同一个通道,转换结果依次落进唯一的数据寄存器DR。由于DR只有一个,多通道扫描模式下必须在每次转换完成时立刻把数据读走,否则下一次转换结果会直接把它覆盖掉。注入组则像一个“特权通道”,可以在规则转换过程中插队,适合那些需要抢占采样周期的场景,比如电机控制里的三相电流同步采样。

实际调试中,90%的场景用规则组就够了。注入组听起来高级,但对大多数传感器采集项目来说属于锦上添花的功能,不用急着掌握。

2.2 四种转换模式怎么选:别一上来就“全开”

STM32的ADC支持单次转换、连续转换、扫描模式和间断模式。这四个概念可以自由组合,但新手最容易把它们一股脑全开,结果行为完全失控。

单次转换模式适配“软件触发一次,只转一个通道,转完就停”的用法,适合低速轮询,功耗也低。连续转换模式则是转换完成立即开始下一次,适合持续检测某一组信号,多通道时配合扫描模式就能循环转换整个序列。扫描模式是指一次触发能够把规则组里排好的多个通道依次转换完。间断模式相对冷门,它把序列拆成几个小批次,每触发一次只转换一小批,用在某些需要把大序列切碎的特定场景。

我的建议是:单通道采集用“单次转换+关闭扫描”,多通道数据需要用循环刷新用“连续转换+扫描模式”,只要你不在一次转换里同时要多个通道的结果,就不要开扫描DMA以外的额外功能。项目调试阶段,配置越简单越容易定位问题。

这里还要注意一个非常重要且容易踩的坑:当启用“扫描模式+连续转换”,且用DMA搬运数据时,DMA会持续不断地把转换结果往内存里填。如果DMA配置成Normal模式只搬一轮,那么一轮搬完后就永远停在那了,后续的转换结果无人接管;如果配置成Circular模式,则会形成一个环形缓冲区,数据不断被刷新。两种模式用途完全不同,很多人数据“不动”了,其实就是把Normal模式当成Circular模式在用。

2.3 外部触发与DMA:让ADC学会自己干活

ADC不一定要用软件启动。很多型号支持定时器触发或者外部引脚触发,典型应用是电机控制和开关电源:PWM的定时器更新事件在MOSFET开关的特定时刻触发ADC,精确地采到该时刻的电流值。如果用软件轮询去碰这个时机,很难做到真正的同步。F103里这类触发源通过ADC的EXTSEL位来选,具体映射要在参考手册里查,CubeMX的图形界面也能直接选择。

软件触发适合普通传感器采集,但一旦进入多通道+高频采样,软件轮询基本没法腾出手来干正事。此时ADC的核心搭档是DMA:每次转换结束产生EOC信号,硬件自动把DR里的数据搬进内存,CPU完全不用干预。代码上看就是调一个HAL_ADC_Start_DMA,剩下的全由外设自己跑。

DMA这里有三个细节必须说清楚。第一,数据宽度建议配成Half Word并对应uint16_t数组,不要配Word宽度然后申请uint32_t数组——虽然也能用,但高16位始终是残留值,容易让你怀疑人生。第二,DMA循环模式下缓冲区会被硬件持续覆盖,想拿到某一时刻的数据,要么用DMA传输完成中断做标志位,要么用双缓冲切换。第三,多通道扫描时,缓冲区里的数据顺序和Rank编排顺序一一对应,不要指望代码能替你重新排序。

3. 应用编程实战:从CubeMX到真实数值

3.1 CubeMX图形化配置:先跑起来,再谈优化

用STM32CubeMX做初始化并不是偷懒,它能帮你少翻几百页参考手册,项目初期效率高得多。以F103C8T6配单通道为例,流程大致是:

  1. 时钟树里确认系统时钟跑在72MHz,APB2输出72MHz,ADC分频器设为6分频,让ADCCLK维持在12MHz附近。
  2. 在ADC1的Channel配置里勾选你要用的通道,比如IN0对应PA0。
  3. 参数配置页里把Resolution选12位,ScanConvMode按需打开或关闭,ContinuousConvMode按需使能,采样时间先选55.5周期,外部触发选Software Trigger。
  4. 如果要多通道循环采集,在DMA Settings里添加一个ADC1到Memory的请求,Mode选Circular,数据宽度选Half Word。
  5. 生成代码,工程里就会自动生成MX_ADC1_Init。

有个经验想单独提一下:第一次上板,采样时间一定先往大里配。比如55.5、71.5甚至在低速信号上用239.5周期。先确认通道、参考电压、电源环境都正常,再慢慢压缩采样时间换取速度。如果一上来就追求1.5周期的极限速度,万一板子布线不太好,读到的数据乱跳,你会分不清到底是配置问题还是硬件问题。

3.2 单通道轮询代码:最小可用的完整流程

CubeMX生成的初始化代码长这样:

ADC_HandleTypeDef hadc1; void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }

读取一个值的函数更简单:

uint16_t read_adc_once(void) { uint16_t value = 0; HAL_ADC_Start(&hadc1); // 启动转换 if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { value = HAL_ADC_GetValue(&hadc1); // 从DR寄存器读结果 } HAL_ADC_Stop(&hadc1); return value; }

这段代码有几个点值得展开:HAL_ADC_Start负责真正给ADC上电启动,HAL_ADC_PollForConversion会阻塞等待转换结束,超时时间设10ms已经足够,HAL_ADC_GetValue读到的是右对齐的12位原始码值。注意在F1的HAL库里,单次启动后最好调用HAL_ADC_Stop把外设停掉,否则一些状态位会残留到下一次启动。轮询模式的缺点是CPU在转换期间被白白占住了,只适合低速采样,速度要求高或者有多通道刷新需求就得上DMA。

3.3 多通道扫描+DMA采集:真正实用的写法

多通道循环采集在工程里最常见,CubeMX里做完初始化,主程序只需要做两件事:把多通道都配进规则组,然后启动DMA搬运。

// 假设要采集PA0、PA1、PB0三个通道 ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_8; // 注意PB0对应的是IN8 sConfig.Rank = ADC_REGULAR_RANK_3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); uint16_t adc_buf[3]; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 3);

这段代码配合CubeMX里的“扫描模式+连续转换+DMA循环模式”,此后adc_buf[0]对应PA0的值,adc_buf[1]对应PA1,adc_buf[2]对应PB0,硬件会一直自动刷新,你只在需要的时候去读数组就行。

通道序号需要格外小心:PA0~PA7对应IN0~IN7,PB0、PB1对应IN8、IN9,而PC0~PC5是IN10~IN15,不同的封装引脚不一样,CubeMX里勾选引脚时它会自动帮你映射,但手写代码时非常容易把通道号搞错。特别是PB0,新手经常写成ADC_CHANNEL_0或者ADC_CHANNEL_10,结果读出来永远是看起来“合理但没意义”的数。缓冲区类型用uint16_t,别用uint32_t数组去接收Half Word宽度的DMA转移,我之前提到过,高半字没数据,调试时那串地址里的“脏数据”会让你误以为DMA配错了。

4. 数据滤波与抗干扰:让读数从“能看”变“能用”

4.1 ADC读数为啥会乱跳:噪声源逐个排查

读ADC,最气的不是读不出来,而是读出来了但数据一直在跳。这时候先别急着怀疑单片机,按下面几个来源逐个排查,多数情况下几分钟就能定位。

第一个也是最常见的:输入引脚悬空。输入什么都没有,引脚周围的电磁场耦合、板子上的漏电流都会在内部采样电容上形成随机电压,读出来的数当然是杂乱无章的。测试时要么短接到GND,要么短接到VCC,没有明确电平来源的“空读数”没有任何意义。

第二个是电源噪声。ADC的VDDA和VSSA如果没做好去耦,开关电源的纹波、LED驱动、电机换相这些瞬态干扰就会直接叠加到参考电平上。注意VDDA不仅是给ADC模拟电路供电,它还作为内部转换的参考基准,它脏了,读数必然跳。

第三个是采样时机不巧。比如采样瞬间恰好落在某个开关干扰脉冲上,偶尔会读到一个大跳变。这是典型的偶发毛刺,软件滤波能消化掉一部分,但根治手段还是要用定时器触发把采样点固定到干扰最小的时刻。

第四个是地弹和串扰。模拟信号线跟PWM线或SPI时钟线并排走得很长,数字边沿的高频分量会通过耦合电容串进模拟输入。这不是软件能完全弥补的,必须从Layout层面解决。

4.2 硬件层面三板斧:去耦、RC滤波与layout

硬件上的抗干扰思路其实很朴素:让ADC看到的输入电平更干净,让参考电压更稳定。

第一板斧是电源去耦。VDDA和VSSA之间就近放一个100nF的陶瓷电容,最好再并联一个10μF的钽电容或电解电容。如果芯片有独立的Vref+引脚,同样处理。这个电容不是装饰,它是给采样保持电路瞬间抽取电荷用的充电宝。

第二板斧是输入RC低通滤波。在信号源到ADC引脚之间串一个电阻、并一个电容到地,能有效压制高频噪声。但要算一下截止频率:fc = 1 ÷ (2πRC)。比如R=1kΩ、C=10nF,fc≈16kHz,高于这个频率的有用信号就会被衰减。如果被采信号本身是几千赫兹的音频,这个参数没问题;如果你采的是一毫秒级别的缓变电压,那可以放心把电容加大。加了RC之后还需要重新核算采样时间,因为R提高了源阻抗,采样电容充电变慢,采样周期可能要相应调大。

第三板斧是Layout。ADC输入线尽量短而粗,远离PWM输出和时钟线,模拟地和数字地做单点连接。这些属于PCB设计基本功,但在MCU小板上经常被压缩空间牺牲掉,事后拿软件滤波去补,其实补不回来。

4.3 软件滤波算法C语言实现:从简易到实用

软件滤波是ADC项目里性价比最高的部分,有时候一组简单的算法代码就能让读数从“没法看”变成“挺稳”。这里给出几种我在项目里反复用的写法。

最简单的是限幅滤波,核心思想是真实物理量不会瞬间跳变,相邻两次采样差值应该在某个范围内,超过阈值就认为新值受到了干扰:

#define LIMIT_THRESHOLD 50 uint16_t limit_filter(uint16_t last_val, uint16_t new_val) { int16_t diff = (int16_t)new_val - (int16_t)last_val; if (diff < 0) diff = -diff; if (diff <= LIMIT_THRESHOLD) { return new_val; } return last_val; }

中值滤波对付尖峰毛刺最好用,取5次采样排序后选中间值,能轻松剔除偶发的单点脉冲。代价是需要临时数组和排序开销,适合低速采集:

uint16_t median_filter(uint16_t *buf, uint8_t count) { for (uint8_t i = 0; i < count - 1; i++) { for (uint8_t j = i + 1; j < count; j++) { if (buf[j] < buf[i]) { uint16_t tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } } } return buf[count / 2]; }

滑动平均是我最常用的,它相当于一个低通滤波器,能有效平滑连续噪声,配合循环队列实现还特别省时间:

#define M_WINDOW 8 uint16_t window_buf[M_WINDOW]; uint8_t window_idx = 0; uint32_t window_sum = 0; uint16_t moving_average(uint16_t sample) { window_sum -= window_buf[window_idx]; window_buf[window_idx] = sample; window_sum += sample; window_idx = (window_idx + 1) % M_WINDOW; return (uint16_t)(window_sum / M_WINDOW); }

这段代码有个细节:它维护的是窗口总和,每次滑动只需要一次减法和一次加法,不需要重新遍历整个窗口求和。8个点的窗口除以8可以用右移3位进一步优化,能省掉除法指令。

如果对动态响应有要求,可以用一阶低通加权滤波,也就是让新值和历史值按比例加权:

// alpha = 1/4 的一阶低通 uint16_t lowpass_filter(uint16_t new_sample, uint16_t last_out) { // y = 0.25 * new + 0.75 * last return (uint16_t)(((uint32_t)new_sample + 3u * last_out) >> 2); }

第一次调用时记得把last_out初始化成第一个采样值,否则输出会从0慢慢爬上去,看起来像在上电漂移。滤波算法切记不要一个项目全上:先找噪声到底是什么形态,慢漂移用低通、尖峰用中值、连续抖动用滑动平均,对症下药比堆砌算法更有意义。

4.4 两点校准与自校准:把系统偏差补回来

如果你发现读数变化规律正常、也不乱跳,但就是和万用表差那么一点点,那大概率是系统偏差问题。原因可能是参考电压不是标准的3.3V,可能是输入分压电阻有5%误差,也可能是ADC内部电路存在增益和偏移误差。这时候就该做校准了。

最实用的是两点校准。用两个已知且稳定的电压作为标准点,比如0.5V和3.0V,分别让ADC采集得到码值adc_low、adc_high。然后线性拟合出真正的电压换算斜率和截距:

// 通过校准得到:V = adc_value * cal_gain + cal_offset float cal_gain = (3.0f - 0.5f) / (float)(adc_high - adc_low); float cal_offset = 0.5f - ((float)adc_low * cal_gain); float adc_to_voltage(uint16_t adc_value) { return (float)adc_value * cal_gain + cal_offset; }

两点校准能同时纠正增益误差和偏移误差,前提是系统线性度本身够好。STM32的SAR ADC线性度不错,两点校准后的精度足以满足绝大多数传感器采集场景。另外,F103的ADC还支持自动校准,HAL库里对应函数是HAL_ADCEx_Calibration_Start,初始化后调用一次即可。它校准的是ADC内部电路的自有偏差,跟外部参考电压的偏差没关系,两者不是一回事,别搞混。

5. 常见问题排查实录与速查表

5.1 读数全0:先从“配置遗漏”查起

ADC值恒为0,这是新手最容易遇到的现象。多数情况下不是硬件坏了,而是配置层面的遗漏。

比如ADC外设的时钟没给,比如GPIO引脚没配成模拟输入模式,又比如CubeMX里明明勾了PA0,但实际接线接到的是PA1。还有一种隐蔽情况:配置好之后忘了调用HAL_ADC_Start就开始等转换结果了,那DR寄存器里永远是复位值0。

遇到全0时我习惯直接看寄存器状态:调试点,展开hadc1.State,看转换是否已经启动;再展开hadc1.Instance的SR寄存器,看EOC位有没有置1。如果EOC从来没置过1,说明转换压根没跑起来,问题多半在启动函数或者时钟配置。用调试器的这个习惯能帮你省下大量翻阅代码的时间。

5.2 读数满码4095:多半是悬空或超量程

反过来,读到的值永远等于4095,那大概率是输入悬空或输入电压超过量程上限。

输入悬空很好理解:内部采样电容的漏电流会把节点电压往某个方向拉,有时凑巧就拉到接近Vref的位置,表现为满码。更麻烦的是如果输入端再接了一根很长的测试线,感应的交流信号还可能让读数在满码和某些随机值之间来回跳。建议先把输入短接到GND确认系统正常,再接真实信号。

真正危险的超量程情况也要注意:如果输入电压超过Vref+,读数是满码,但如果超得太多,通道引脚内部的保护二极管会被强制导通,时间长了可能损坏芯片。用分压电阻把信号幅度压到ADC范围内,是任何采集项目的第一道工序。

5.3 抖动大、漂移明显:电源、采样时刻与电磁干扰

读数来回跳或者缓慢漂移,通常指向三个原因。

电源噪声首当其冲。给板子单独供电、断开后期接入的电机或继电器,如果读数立刻变稳,基本就是电源被干扰了。测试时用示波器夹在VDDA引脚上,看转换时刻附近有没有毛刺,一抓一个准。

采样时间不够也会表现为抖动,特别是源阻抗比较大的时候。把采样周期从1.5周期改到55.5周期,如果跳变幅度明显下降,说明就是充电时间不足。这时候加RC滤波还得重新计算采样时间,两者是有联动关系的。

还有一类“抖动”其实是采样时刻撞上了周期性的开关干扰,比如PWM信号的高频边沿。用定时器触发把采样点移到干扰最小的窗口里,配合DMA循环采集,往往能立竿见影。

5.4 电压换算有偏差:从参考电压到源阻抗逐一核对

读数稳定但电压算出来和万用表不一致,按这个顺序排查:先量板子上VDDA的实际电压,用这个值替换掉代码里写死的3.3作为分母;再用一个精密稳压源做两点校准,得到自己的gain和offset;最后检查输入分压电阻的精度,1%电阻和5%电阻的成本差几毛钱,但精度差几条街。

源阻抗因素也要排:如果ADC输入前面串联了10kΩ以上的电阻去分压,采样时间又很短,读到的电压会偏低。低频信号下最简单的验证办法是把采样时间调到最大档,看读数是否明显变大,变大了就说明充电时间不够。

5.5 问题速查表

现象可能原因快速排查手段解决思路
读数恒为0时钟未使能、引脚模式错、未启动转换检查hadc1.State与SR寄存器补齐配置调用链
读数恒为4095引脚悬空、输入超量程、通道选错短接输入到GND观察变化接明确电平,做分压
读数乱跳输入悬空、电源噪声、采样时间不足短接GND、查看VDDA波形增加滤波、调大采样周期
电压偏低源阻抗大、采样时间短、参考电压偏小改最大采样时间对比加缓冲器或RC、校准
电压偏高参考电压偏大、分压电阻误差实测VDDA用实测值做系数校准
多通道数据串位Rank顺序和代码读取顺序不对单通道逐个测试按Rank顺序取数组

6. 我的几点实操体会与下一步可以玩的方向

我接触STM32的ADC这几年,最深的体会是:ADC项目绝大多数问题都不是芯片本身造成的,而是外部环境把它的“感知能力”破坏了。只要把参考电压、输入阻抗、采样时间、电源去耦这四个基础问题理顺,一个裸奔的ADC也能读到很干净的数据。软件滤波是锦上添花,不是雪中送炭,别指望用一大堆滤波算法去挽救硬件设计上的缺陷。

另外一个小建议是:调ADC时一定要留一个调试串口,把原始码值以十六进制打出来。看到的是0x0FFF还是0x0F90,比看到一个已被加工过的“电压浮点数”更能暴露问题。原始码值稳定了,再去做滤波、校准、换算电压,问题范围一下子就能收缩。

如果你已经能把单通道、多通道DMA玩顺,下一步可以试试用定时器触发ADC做固定间隔采样,配合DMA双缓冲做一个简易的音频波形采集;或者利用内部温度传感器和VREFINT通道做一个无需外部传感器的自检功能。再进阶一点,结合FFT,用这个片上ADC做一个简易的频谱分析仪,练手效果非常好。ADC本身是一个很经典的模块,但它延伸出来的东西几乎覆盖了信号链路的全部知识,值得多花点时间把它吃透。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 20:27:06

嵌入式固件烧录版本管理:构建-烧录-验证全链路管控方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:26:47

STM32F407+LAN8720以太网实战:CubeMX配置与LwIP协议栈全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:26:29

Android数据库内容变化的监听:TaoToken统一Key接入与配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:24:42

汽车电子闭环链路故障诊断:从传感器到ECU的偶发问题排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:24:25

本地大模型部署实战:从Ollama入门到显存优化与离线运行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:24:22

2025年3D模型网站推荐:国内外平台深度评测与引擎适配指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华