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在实际板子上往往就是VDDAVDDA又可能不是精确的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这是硬性上限。如果你的系统主频是72MHzPCLK2也是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但如果你用了DMAEOC行为会变。第二连续转换模式和单次转换模式的选择单次模式下每次都要手动置位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. 那些让我熬夜的ADCDMAUART联调问题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或1DMA设为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 - 当前电压) / 斜率 25V25和斜率从手册里查。浮点运算在F103上没有硬件FPU用软件浮点会慢。如果对速度有要求可以用定点数运算把电压放大1000倍用整数算最后显示的时候再插小数点。这个技巧在采样率高的场景下能省不少CPU时间。注意所有换算公式里的常数都要用你手上那颗芯片的手册值不同系列、不同批次的参数可能有差异。不要抄网上的代码直接用。7. 我个人在实际操作中的几点体会片内信号做ADC演示这件事看起来简单但它是理解整个模拟采集链路的钥匙。我自己的经验是每次换新芯片或者新板子第一件事就是用内部温度和VREFINT跑一遍ADC确认时钟、校准、DMA、串口这条链路全通。这个过程花不了半小时但能帮你排除掉后面90%的玄学问题。另外DMA加空闲中断接收串口数据这个组合在调试阶段特别有用。你可以用串口助手发命令来切换采样通道、调整采样率不用反复烧录程序。空闲中断的判断逻辑是串口接收中断里填缓冲区总线空闲中断里置完成标志主循环处理命令。这个结构我在多个项目里复用稳定可靠。最后如果你要把这套东西用到实际产品里记得在ADC输入端加RC滤波软件上做滑动平均或者中值滤波。片内信号干净不代表外部信号也干净。硬件滤波加软件滤波双管齐下才能得到稳定的采样值。