1. 为什么我要用片内信号做ADC演示而不是外接传感器
很多人第一次接触STM32的ADC,第一反应是找个电位器或者光敏电阻接上去,拧一拧、遮一遮,看串口输出的数字跳来跳去,觉得“跑通了”。但真到了项目里,比如做数字电源的电压电流采样、做温控的NTC采集,你会发现外接信号源反而成了干扰项——电位器本身的接触噪声、杜邦线的天线效应、面包板的寄生参数,都会让你分不清到底是ADC配置有问题,还是信号源本身就不干净。
我后来带新人的时候,一律要求先用片内信号把ADC的完整链路跑通。STM32片内至少有两个天然可用的信号源:一个是内部温度传感器,接在ADC的某个固定通道上;另一个是内部参考电压VREFINT,也是一个固定通道。这两个信号不需要任何外部接线,上电就有,数值稳定可预期,非常适合用来验证ADC的时钟配置、采样时间、校准流程、DMA搬运、串口输出这一整条链路是否正常。
这篇内容适合谁看?如果你已经能点亮LED、能跑通串口打印,但ADC这块总是似懂非懂——采样值忽大忽小、DMA配置了不搬数据、校准不知道什么时候做——那这篇就是写给你的。我会以STM32F103为主力平台(毕竟存量最大),把片内信号采集这件事从原理到代码到踩坑完整讲一遍,最后再延伸到多通道扫描加DMA的实战结构。
先明确一个核心认知:ADC不是一个孤立的模块,它和时钟树、GPIO模式、DMA控制器、中断向量表是绑在一起的。你只盯着ADC那几个寄存器看,永远调不通。下面我按实际调试顺序来拆。
2. 片内温度传感器和VREFINT到底怎么接进ADC的
2.1 内部通道的映射关系不是猜出来的
STM32F103的ADC1和ADC2共用一组外部通道0到17,其中通道16和通道17是内部专用的。通道16接的是内部温度传感器,通道17接的是内部参考电压VREFINT。这个映射关系在参考手册的ADC章节里有明确的表格,不是随便分配的。你要做的第一件事就是翻到你手上那颗芯片对应的参考手册,确认这两个通道号。不同系列会有差异,比如有些型号VREFINT在通道0或者别的编号上,不能想当然。
温度传感器的原理很简单:它输出一个随温度线性变化的电压,手册里给了两个标定点——25摄氏度时的典型电压值,以及温度系数(通常是每摄氏度几毫伏)。用公式反推就能得到温度。但这里有个大坑:这个电压是相对于VREF+的比值,而VREF+在实际板子上往往就是VDDA,VDDA又可能不是精确的3.3V。所以如果你直接用3.3V去算,温度会有好几度的偏差。
2.2 VREFINT的真正用途是反推实际参考电压
VREFINT的出厂校准值存在系统存储区的一个固定地址里,这个值是在特定条件下测出来的,对应一个已知的参考电压。你在运行时读到的VREFINT转换结果,和出厂校准值一比,就能反推出当前实际的VDDA是多少。这个技巧在电池供电或者电源不稳的场景里特别有用——你不需要外部基准芯片,就能知道ADC的参考电压漂了多少。
具体做法是:先读VREFINT通道的ADC原始值,然后用公式VDDA = 出厂校准值 × 出厂参考电压 / 当前VREFINT原始值算出实际参考电压。之后所有外部通道的电压换算都用这个VDDA,精度会明显提升。这一步很多人跳过,结果就是采样值总是差那么一截,还找不到原因。
2.3 采样时间必须给够,否则内部通道读数就是垃圾
内部温度传感器和VREFINT的输出阻抗比较高,手册里明确要求采样时间要大于某个最小值。以F103为例,内部通道建议采样时间不低于17.1微秒。你如果按外部通道那种1.5个周期的采样时间来配,读出来的值会明显偏低而且跳动大。
采样时间的计算和ADC时钟频率有关。假设ADCCLK是14MHz,一个周期约71.4纳秒,17.1微秒对应大约239个周期。F103的采样时间可选1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个周期,所以内部通道直接选239.5周期最稳妥。外部通道如果信号源阻抗低,可以选小一点来提高转换速率,但内部通道不要省这个时间。
注意:采样时间不是越长越好。太长会降低整体采样率,在多通道扫描时可能拖慢整个序列。内部通道给239.5周期,外部低阻抗通道给13.5或28.5周期,这样搭配比较合理。
3. 从零配置ADC的完整链路与时钟树陷阱
3.1 时钟使能顺序错了,后面全白搭
STM32的ADC挂在APB2总线上,但它的时钟来源可以独立选择。F103的ADC时钟来自PCLK2经过一个分频器,分频系数由RCC_CFGR的ADCPRE位控制,可选2、4、6、8分频。ADCCLK不能超过14MHz,这是硬性上限。如果你的系统主频是72MHz,PCLK2也是72MHz,那至少要用6分频才能降到12MHz,用4分频就是18MHz,超频了,转换结果会不可靠。
使能顺序我习惯这样:先开GPIO时钟(如果用外部通道)、再开DMA时钟(如果用DMA)、再开ADC时钟、最后开AFIO时钟(如果需要重映射)。虽然ADC本身不依赖AFIO,但如果你同时用了其他复用功能,顺序乱了容易出玄学问题。开完时钟再配置ADC的CR2寄存器做校准和使能。
3.2 校准不是可选项,是必选项
F103的ADC有一个校准流程:先使能ADC、等待稳定、然后置位CAL位启动校准、等待CAL位被硬件清零、校准完成。这个过程只需要一次,通常在初始化阶段做完就行。不校准的后果是零点偏移,表现为输入接地时读数不是0而是几十甚至上百。
校准的代码逻辑是:ADC_Cmd(ADC1, ENABLE)之后延时一小会儿,然后ADC_ResetCalibration(ADC1),等复位完成,再ADC_StartCalibration(ADC1),等校准完成。每一步都要用while循环等标志位,不能只靠延时。我见过有人用delay_ms(100)代替标志位等待,在低温或者低压环境下就翻车了。
3.3 规则组和注入组的区别决定了你的软件结构
规则组就是常规的扫描序列,按你设定的顺序依次转换,结果放在同一个数据寄存器里,需要配合DMA或者中断来搬运。注入组是插队用的,可以打断规则组的转换,有自己的数据寄存器,适合做紧急采样。
片内信号演示用规则组就够了。如果你要同时采温度、VREFINT和几个外部通道,就把它们都放进规则序列,用DMA把结果搬到数组里。注入组在这个场景里用不上,但你要知道它的存在,因为有些项目里需要“规则组慢速扫描+注入组快速保护”的结构,比如数字电源的过流保护。
4. 单通道轮询、多通道DMA、串口输出三种方式实测对比
4.1 单通道轮询:最简单但最容易踩的坑
单通道轮询就是启动转换、等EOC标志、读数据。代码短,适合验证。但有两个坑:第一,EOC标志的清除方式,F103上读DR寄存器会自动清EOC,但如果你用了DMA,EOC行为会变。第二,连续转换模式和单次转换模式的选择,单次模式下每次都要手动置位SWSTART,连续模式下启动一次就一直转。
我实测下来,验证片内温度传感器用单次转换就够了,读一次温度不需要连续。但如果你要做温度监控的实时曲线,连续转换加定时器触发更合适。
4.2 多通道扫描加DMA:数据搬移的完整配置
这是实战中最常用的结构。规则序列里放多个通道,DMA循环模式,ADC每转换完一个通道就触发一次DMA请求,把DR的值搬到内存数组。配置要点:
- DMA方向是外设到内存,外设地址固定为ADC->DR,内存地址递增。
- 数据宽度:ADC的DR是16位,但实际有效12位,DMA内存端用半字(16位)就行。
- 循环模式必须开,否则搬完一轮就停了。
- ADC的DMA使能位要在ADC_CR2里单独置位,不是DMA控制器那边使能就完事了。
这里有个经典问题:DMA搬了一轮之后数据错位。原因通常是ADC的规则序列长度和DMA的缓冲区长度不一致,或者DMA没有配成循环模式。我建议缓冲区长度严格等于规则序列长度,用宏定义绑在一起,改一个就改另一个。
4.3 串口输出:别在中断里做浮点运算
采样值要通过UART打印出来看。很多人习惯在DMA传输完成中断里直接算电压、算温度、再sprintf格式化,然后串口发送。这个做法在低速率下没问题,但如果你采样率上去了,中断里做浮点和格式化会严重拖慢响应。
我的做法是:DMA搬完一轮数据后置一个标志位,主循环里检测标志位,在主循环里做换算和打印。中断里只做最轻量的操作。串口发送本身可以用DMA或者简单的阻塞发送,取决于数据量。调试阶段用阻塞发送最省事,printf重定向到UART就行。
提示:printf重定向时记得开MicroLIB,否则代码体积会大很多。Keil里在Target选项里勾选Use MicroLIB。
5. 那些让我熬夜的ADC+DMA+UART联调问题
5.1 DMA搬数据但数组里全是同一个值
这个问题我遇到过两次。第一次是因为DMA的内存地址没有设置递增,所有数据都覆盖到同一个位置。第二次是因为ADC的连续转换模式没开,DMA触发了一次之后ADC就停了,后面DMA搬的都是旧数据。
排查思路:先看DMA的CCR寄存器,确认MINC位(内存递增)是1。再看ADC的CR2寄存器,确认CONT位(连续转换)和DMA位都是1。如果这两个都对,用调试器看ADC的DR寄存器是否在变化,如果DR不变,说明ADC根本没在转换,问题在ADC的触发源上。
5.2 串口打印出来的温度值跳变超过正负5度
片内温度传感器本身的精度就不高,手册标称正负1.5度左右,但实际使用中跳变更大。原因有三个:一是没有用VREFINT反推实际VDDA,参考电压漂了;二是采样时间不够,内部通道没采稳;三是没有做软件滤波。
我的处理方式是:连续采16次,去掉最大最小各2个,剩下的取平均。这个滤波在主循环里做,不占中断时间。做完之后跳变能控制在正负1度以内,对于温度监控足够了。
5.3 用CubeMX生成代码后ADC不工作
CubeMX是个好工具,但它的ADC配置有几个默认值容易坑人。比如扫描模式默认关闭,你加了多个通道但它只转第一个。还有DMA请求的默认设置,有时候生成的代码里DMA使能顺序不对,ADC先使能了DMA还没配好。
我的习惯是:CubeMX生成代码后,一定手动检查ADC的初始化顺序,确保DMA在ADC之前初始化。另外CubeMX的ADC校准代码有时候放在HAL_ADC_Init里面,有时候要手动调HAL_ADCEx_Calibration_Start,这个要看具体系列。F103上需要手动调校准函数。
5.4 中断优先级冲突导致串口丢数据
如果你同时开了DMA传输完成中断和串口接收中断,优先级配置不当会导致串口数据丢失。DMA中断的优先级如果高于串口,且DMA中断里处理时间过长,串口接收就会溢出。
我的配置原则:串口接收中断优先级最高,因为串口数据来得快,丢了就没了。DMA传输完成中断优先级可以低一些,因为它只是通知数据准备好了,晚一点处理没关系。在NVIC里把串口的抢占优先级设为0或1,DMA设为2或3。
6. 从片内信号延伸到多通道采集的工程化结构
6.1 把通道配置做成表,别硬编码
当你的项目从2个通道扩展到8个通道时,如果每个通道的配置都散落在代码各处,维护起来就是灾难。我的做法是定义一个结构体数组,每个元素包含通道号、采样时间、对应的缓冲区索引。初始化时遍历这个数组去配置规则序列,DMA缓冲区的大小直接由数组长度决定。
这样加通道只需要在数组里加一行,其他地方不用动。换算电压的时候也用这个数组来索引,代码整洁很多。
6.2 定时器触发ADC是更专业的做法
轮询和连续转换适合调试,但真正的采集系统应该用定时器触发。TIM的某个通道配置成PWM或者输出比较模式,触发信号连到ADC的外部触发源。这样采样率由定时器精确控制,不受软件循环时间的影响。
F103上ADC1的规则组外部触发可以选TIM1_CC1、TIM1_CC2、TIM1_CC3、TIM2_CC2、TIM3_TRGO、TIM4_CC4等。配置的时候注意触发边沿的选择,上升沿还是下降沿,要和定时器的输出极性匹配。
6.3 双缓冲让数据处理和采集并行
DMA的双缓冲模式(也叫乒乓缓冲)在F103上没有硬件支持,但可以用两个缓冲区加两个DMA通道来模拟,或者用半传输中断加传输完成中断来实现。思路是:DMA搬前半段的时候,CPU处理后半段的数据;搬后半段的时候,CPU处理前半段。
这个结构在高速采集里很有用,能避免数据覆盖。实现上就是利用DMA的HT(半传输)和TC(传输完成)两个中断,在中断里切换处理指针。注意中断里只做指针切换和标志置位,实际处理放主循环。
6.4 电压换算的精度控制
最后说换算。ADC原始值是12位,0到4095。电压等于原始值 × VDDA / 4095。这里的VDDA如果是用VREFINT反推出来的,精度会好很多。温度换算用(V25 - 当前电压) / 斜率 + 25,V25和斜率从手册里查。
浮点运算在F103上没有硬件FPU,用软件浮点会慢。如果对速度有要求,可以用定点数运算,把电压放大1000倍用整数算,最后显示的时候再插小数点。这个技巧在采样率高的场景下能省不少CPU时间。
注意:所有换算公式里的常数都要用你手上那颗芯片的手册值,不同系列、不同批次的参数可能有差异。不要抄网上的代码直接用。
7. 我个人在实际操作中的几点体会
片内信号做ADC演示这件事,看起来简单,但它是理解整个模拟采集链路的钥匙。我自己的经验是,每次换新芯片或者新板子,第一件事就是用内部温度和VREFINT跑一遍ADC,确认时钟、校准、DMA、串口这条链路全通。这个过程花不了半小时,但能帮你排除掉后面90%的玄学问题。
另外,DMA加空闲中断接收串口数据这个组合,在调试阶段特别有用。你可以用串口助手发命令来切换采样通道、调整采样率,不用反复烧录程序。空闲中断的判断逻辑是:串口接收中断里填缓冲区,总线空闲中断里置完成标志,主循环处理命令。这个结构我在多个项目里复用,稳定可靠。
最后,如果你要把这套东西用到实际产品里,记得在ADC输入端加RC滤波,软件上做滑动平均或者中值滤波。片内信号干净,不代表外部信号也干净。硬件滤波加软件滤波双管齐下,才能得到稳定的采样值。