1. 项目概述:从模拟世界到数字世界的桥梁
如果你正在用STM32做一个温湿度监测、电池电压检测或者音频信号采集的项目,那么ADC(模数转换器)这个外设就是你绕不开的核心。很多新手朋友一听到“ADC”、“采样率”、“分辨率”这些词就有点发怵,觉得这是模拟电路工程师才需要深究的东西。其实不然,在嵌入式开发中,ADC是我们与真实物理世界交互最直接的窗口。你想想,传感器输出的电压信号、麦克风采集的音频波形、电位器调节的电压值,这些都是连续的模拟量。而我们的STM32微控制器是个数字世界的居民,它只认识0和1。ADC的作用,就是在这两个世界之间担任翻译官,把连续的电压信号,转换成微控制器能理解和处理的数字值。
我刚开始接触STM32的ADC时,也走过不少弯路。比如,采回来的数据跳得厉害,根本没法用;或者配置了半天,DMA就是传不过来数据;又或者采样速度总是不对,达不到项目要求。这些问题,大多不是因为芯片不好,而是对ADC这个外设的工作机制理解不够透彻,配置时忽略了某些关键细节。标准外设库(Standard Peripheral Library)虽然封装了底层寄存器操作,让我们能用更直观的函数来配置,但它也像一把双刃剑。用好了,事半功倍;用不好,你甚至不知道问题出在库函数调用上,还是硬件连接上。
所以,这一章我们不打算一上来就贴大段代码。那样做,你只是照猫画虎,知其然不知其所以然。我们要做的是,先把STM32 ADC的“脾气秉性”摸清楚。它有哪些工作模式?精度和速度如何权衡?触发转换的“扳机”有哪些?只有把这些基础原理吃透了,你再去看库函数里的那些参数,比如ADC_InitTypeDef结构体里的ADC_ContinuousConvMode、ADC_ExternalTrigConv,才会恍然大悟:“哦,原来这个参数是干这个用的!” 接下来,我们就从最核心的概念开始,一步步拆解STM32的ADC。
2. ADC核心概念与STM32的实现解析
2.1 分辨率、量程与参考电压:ADC的“尺子”
你可以把ADC想象成一把用来测量电压的“数字尺子”。这把尺子有几个关键属性决定了它的测量能力。
首先是分辨率,也就是这把尺子的刻度有多密。STM32F1系列最常见的ADC是12位的。什么是12位?这意味着这把尺子有 2^12 = 4096 个刻度。它能输出的数字值范围是0到4095(或者0到4096,取决于你怎么看,通常说0-4095)。如果你的测量电压范围是0-3.3V,那么理论上,每个数字刻度(1 LSB)代表的电压值是 3.3V / 4096 ≈ 0.000806V,也就是0.8mV。分辨率越高,这把尺子就越精密,能区分更微小的电压变化。但要注意,高分辨率不等于高精度,精度还受参考电压稳定性、内部噪声等多种因素影响。
这就引出了第二个关键:量程与参考电压。ADC测量的是输入引脚(ADCx_INy)上的电压相对于它的参考电压的比例。STM32的ADC通常有多个参考电压引脚:VREF+和VREF-(在部分型号上独立引出),以及VDDA和VSSA。对于大多数通用型号,我们通常将VREF+连接到VDDA(模拟电源,通常3.3V),VREF-连接到VSSA(模拟地,0V)。那么,ADC的量程就是0V到VDDA(例如3.3V)。当输入电压为0V时,ADC输出0;当输入电压等于VDDA时,ADC输出满量程值4095。
重要提示:VDDA的供电质量直接影响ADC的测量精度。务必确保VDDA电源干净、稳定。在设计PCB时,VDDA引脚附近必须放置一个10uF的钽电容和一个0.1uF的陶瓷电容进行去耦,并且最好通过磁珠与数字电源VDD隔离。这是很多初学者采样值跳动大的首要原因。
2.2 采样、保持与转换:ADC的“工作流水线”
ADC不是瞬间完成转换的,它遵循一个经典的流程:采样->保持->量化->编码。STM32内部已经为我们集成了这个流水线。
采样:内部的一个采样开关闭合,让外部信号给内部的一个小电容(采样保持电容)充电。这个时间必须足够长,让电容上的电压能跟上外部信号的变化。这个时间就是采样时间,在STM32中可以通过寄存器
ADC_SMPR1和ADC_SMPR2来配置,每个通道可以独立设置。采样时间设置得太短,电容还没充到实际的电压值,开关就断开了,会导致测量误差;设置得太长,又会影响总的转换速度。对于信号源内阻较大的情况(比如用一个大电阻分压),必须延长采样时间。保持:采样开关断开。此时,外部信号的变化不再影响那个小电容,电容上的电压被“保持”住,成为一个稳定的值,供后续电路进行转换。这就好比摄影师按下快门瞬间,定格了一幅画面。
量化与编码:这是ADC核心的“算数”部分,将保持住的模拟电压值,与内部的一系列基准电压进行比较(通过逐次逼近寄存器SAR逻辑),最终产生对应的12位数字代码。这个过程需要一定数量的ADC时钟周期,对于12位分辨率,STM32通常需要12.5或13.5个ADC时钟周期。
总转换时间的计算公式至关重要:Tconv = 采样时间 + 12.5个周期。例如,如果ADC时钟(ADCCLK)配置为12MHz,采样时间设置为239.5个周期(这是STM32F1支持的最长时间),那么总转换时间 Tconv = (239.5 + 12.5) / 12MHz ≈ 21us。这意味着这个ADC通道每秒最多能进行约47,600次转换。理解这个计算,你才能根据项目需求(信号频率、精度要求)合理配置时钟和采样时间。
2.3 触发源与转换模式:谁来按下“开始”按钮?
ADC不会自己无缘无故开始转换,需要有一个“触发信号”来告诉它:“开始干活!”。STM32提供了丰富的触发源:
- 软件触发:最简单直接,在程序中调用库函数
ADC_SoftwareStartConvCmd(ADCx, ENABLE)来启动一次转换。适合手动、单次采集的场景。 - 外部引脚触发:可以通过特定的GPIO引脚(如EXTI线)上的上升沿或下降沿来触发ADC。常用于需要与外部事件严格同步的场合。
- 定时器触发:这是最强大、最常用的自动触发方式。你可以配置一个定时器(如TIM2、TIM3、TIM4)的某个事件(更新事件、比较匹配事件、捕获事件)来产生周期性的触发信号。这是实现固定采样率、进行信号处理(如音频采集)的黄金标准。例如,用TIM2的更新事件每1ms触发一次ADC,你就得到了一个1kHz的采样率。
确定了“扳机”后,还要选择开火模式:
- 单次转换模式:触发一次,只对一个选定的通道进行一次转换,然后ADC就停止,等待下一次触发。
- 连续转换模式:一旦启动,ADC就会马不停蹄地、一个接一个地对选定的通道进行转换,转换完立即开始下一次,形成连续流。通常需要配合DMA来搬运数据,否则数据会被覆盖。
- 扫描模式:当你有多个通道需要测量时(比如同时测温度、电压、光强),可以启用扫描模式。ADC会按照你预先设置好的通道顺序(
ADC_RegularChannelConfig),自动依次对每个通道进行转换。扫描模式可以和单次/连续模式组合使用。
2.4 STM32F1 ADC的特性概览
以最经典的STM32F103系列为例,其ADC模块的主要特性包括:
- 12位分辨率,支持左对齐或右对齐的数据寄存器。
- 多达18个复用通道:包括16个外部GPIO引脚(ADCx_IN0~IN15)和2个内部信号源(温度传感器、内部参考电压VREFINT)。这2个内部通道非常有用,温度传感器可以用于监测芯片结温,VREFINT可以用于计算实际的VDDA电压,实现“测电压”的自我校准。
- 单次或连续转换模式,可配置的扫描模式。
- 多种触发源:软件触发、定时器触发、外部引脚触发。
- 模拟看门狗:可以设置一个电压阈值窗口,当转换结果高于上限或低于下限时,会产生中断。非常适合用于电池电压监控,只在电压异常时才通知CPU。
- DMA支持:这是实现高效数据流的关键。在连续转换或多通道扫描时,必须启用DMA,让ADC转换完成的数据自动搬运到内存数组中,从而解放CPU。
3. 标准外设库驱动框架与关键结构体
3.1 初始化结构体:ADC的“身份证”
标准外设库通过ADC_InitTypeDef这个结构体来统一配置ADC的工作模式。理解每个成员的含义,是正确配置的前提。
typedef struct { uint32_t ADC_Mode; // ADC工作模式(独立/双ADC模式) FunctionalState ADC_ScanConvMode; // 扫描模式使能(ENABLE/DISABLE) FunctionalState ADC_ContinuousConvMode; // 连续转换模式使能 uint32_t ADC_ExternalTrigConv; // 外部触发选择 uint32_t ADC_DataAlign; // 数据对齐方式(左对齐/右对齐) uint8_t ADC_NbrOfChannel; // 规则通道组中要转换的通道数目 } ADC_InitTypeDef;ADC_Mode:对于单个ADC,就选ADC_Mode_Independent(独立模式)。F1还有双ADC模式(同步、交替等),用于提高采样率或实现相位差采集,初期可以先不管。ADC_ScanConvMode:如果你需要按顺序转换多个通道(比如IN0, IN1, IN5),就必须设为ENABLE。如果只用一个通道,可以设为DISABLE。ADC_ContinuousConvMode:如果需要ADC不停地自动转换,就设为ENABLE(通常配合DMA)。如果是单次触发转换一次,就设为DISABLE。ADC_ExternalTrigConv:这是配置触发源的关键。如果你用软件触发,就选ADC_ExternalTrigConv_None。如果用定时器2的通道2事件触发,就选ADC_ExternalTrigConv_T2_CC2。这里一定要和你硬件上连接的定时器事件对应上。ADC_DataAlign:通常选ADC_DataAlign_Right(右对齐)。这样读取到的12位数据就在一个16位变量的低12位,处理起来直观。左对齐有时用于快速判断电压范围。ADC_NbrOfChannel:这个参数不是手动填写的!它在你后续调用ADC_RegularChannelConfig函数配置通道顺序时,库函数内部会自动管理。你只需要在初始化时给它赋一个值(比如1),但最终以实际配置的通道数为准。有些教程这里会让人困惑。
3.2 通道配置与采样时间
初始化完ADC主体后,还需要告诉ADC具体转换哪个或哪些通道,以及每个通道用多长的采样时间。
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)ADC_Channel:指定具体的通道,例如ADC_Channel_0对应GPIO PA0。Rank:规则通道组的转换顺序(1~16)。Rank为1的通道最先转换。在扫描模式下,ADC会从Rank1转换到RankN。ADC_SampleTime:为该通道设置采样时间周期数。可选1.5, 7.5, 13.5, 28.5, 41.5, 55.5, 71.5, 239.5个周期。时间越长,采样越充分,抗噪声能力越强,但转换速度越慢。对于高阻抗信号源,必须选择较长的采样时间(如239.5)。
3.3 校准:消除系统误差
ADC内部电路存在微小的偏移和增益误差。因此,在上电初始化后、开始第一次转换前,必须执行校准流程。这是一个固定的操作序列:
ADC_ResetCalibration(ADC1); // 重置校准寄存器 while(ADC_GetResetCalibrationStatus(ADC1)); // 等待重置完成 ADC_StartCalibration(ADC1); // 开始校准 while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成切记:每次ADC上电或从低功耗模式唤醒后,都需要重新校准。不校准可能导致转换结果存在固定的偏差。
4. 单通道单次转换实战:以电位器电压读取为例
理论说得再多,不如动手调一遍。我们来实现一个最基础的场景:用ADC1的通道0(PA0引脚)读取一个电位器的电压,并通过串口打印出来。采用软件触发、单次转换模式。
4.1 硬件连接与GPIO配置
首先,准备一个10kΩ的电位器,两端分别接3.3V和GND,中间滑动端接STM32的PA0引脚。PA0是ADC1_IN0的复用功能。
在代码中,首先要初始化对应的GPIO为模拟输入模式。这一点非常重要,如果配置成浮空输入或其他模式,会导致采样不准确。
// 1. 开启GPIOA和ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // 2. 配置PA0为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure);4.2 ADC初始化与校准
接下来,按照我们前面分析的步骤,配置ADC1。
// 3. 初始化ADC1 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道,禁用扫描 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道数为1 ADC_Init(ADC1, &ADC_InitStructure); // 4. 配置ADC1通道0的转换顺序和采样时间 // 参数:ADC1, 通道0, 转换顺序Rank为1, 采样时间239.5周期(保证充分采样) ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 5. 使能ADC1 ADC_Cmd(ADC1, ENABLE); // 6. 执行ADC校准(必须步骤) ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));4.3 软件触发与数据读取
初始化完成后,我们就可以在需要的时候启动转换并读取结果了。通常我们会封装一个Get_Adc函数。
uint16_t Get_Adc(uint8_t ch) { // 设置指定通道的转换顺序(单通道时Rank总是1) ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); // 软件启动转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 等待转换结束标志位EOC置位 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 清除EOC标志(读取DR寄存器会自动清除,但显式清除是好习惯) ADC_ClearFlag(ADC1, ADC_FLAG_EOC); // 返回转换结果 return ADC_GetConversionValue(ADC1); }在主循环中,我们可以周期性地调用这个函数,并将得到的12位原始值转换为电压值。
int main(void) { // ... 初始化系统时钟、GPIO、ADC、串口等 ... USART_Config(); // 假设已配置好串口用于打印 while(1) { uint16_t adc_value; float voltage; adc_value = Get_Adc(ADC_Channel_0); // 读取原始值 voltage = (float)adc_value / 4096 * 3.3; // 计算电压,假设VDDA=3.3V printf("ADC Value: %d, Voltage: %.3f V\r\n", adc_value, voltage); Delay_ms(500); // 延时500ms,避免打印太快 } }烧录程序,旋转电位器,你应该能在串口助手上看到变化的ADC值和计算出的电压值。恭喜你,完成了与模拟世界的一次成功对话!
5. 多通道扫描与DMA传输实战:高效采集传感器数据
单次转换模式适合不频繁的采样。但在实际项目中,我们经常需要同时监测多个传感器(比如一个智能花盆需要同时测土壤湿度、环境温度、光照强度),并且需要以固定的频率持续采样。这时,多通道扫描模式 + 连续转换模式 + DMA就是最佳组合拳。
5.1 场景与配置思路
假设我们需要以100Hz的频率(每10ms一次)同时采集三个传感器:
- 通道0(PA0):土壤湿度传感器(模拟电压输出)
- 通道1(PA1):NTC热敏电阻温度传感器(通过分压电路)
- 通道4(PA4):光照传感器(模拟电压输出)
我们希望ADC每10ms自动完成这三个通道的转换,并将数据自动存放到一个数组中,完全不需要CPU干预。等存够一组数据(比如100组),CPU再一次性处理。这需要:
- ADC配置为扫描模式、连续转换模式。
- 使用定时器触发(例如TIM3),精确产生10ms的触发信号。
- 启用DMA,让ADC转换完成的数据自动搬运到内存。
5.2 DMA配置详解
DMA(直接存储器访问)是STM32的“数据搬运工”,可以在外设和内存之间直接传输数据,不占用CPU。对于ADC多通道扫描,我们配置DMA从ADC的DR(数据寄存器)搬运到我们定义的一个数组。
// 定义存储ADC值的数组。数组大小要能容纳所有通道的一次扫描。 #define ADC_CHANNEL_NUM 3 uint16_t ADC_ConvertedValue[ADC_CHANNEL_NUM]; // 存放通道0,1,4的值 // 1. 开启DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置DMA1通道1(ADC1固定使用DMA1通道1) DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel1); // 复位通道1配置 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); // 外设地址:ADC1数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_ConvertedValue; // 内存地址:我们的数组 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:从外设到内存 DMA_InitStructure.DMA_BufferSize = ADC_CHANNEL_NUM; // 缓冲区大小,即一次传输的数据量(3个) DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增(存到数组的不同位置) DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度:16位(半字) DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度:16位 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式:传输完自动从头开始,实现连续搬运 DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级:高 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止内存到内存模式 DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 3. 使能DMA通道 DMA_Cmd(DMA1_Channel1, ENABLE);关键点解析:
DMA_Mode_Circular(循环模式):这是实现连续自动采集的核心。当DMA传输完BufferSize(3个)数据后,会自动将MemoryBaseAddr和BufferSize重置为初始值,然后等待下一次ADC转换完成,继续搬运。这样就形成了一个“环形缓冲区”,ADC的数据流可以源源不断地注入数组。DMA_MemoryInc_Enable:必须使能。这样第一次转换的数据存到ADC_ConvertedValue[0],第二次存到[1],第三次存到[2],符合我们的预期。
5.3 ADC与定时器联动配置
接下来配置ADC工作在扫描、连续、定时器触发模式。
// 4. 配置定时器3产生10ms更新事件作为ADC触发源 // 假设系统时钟72MHz,定时器预分频7200-1,则计数器时钟为10kHz // 自动重装载值设为100-1,则产生100Hz(10ms)的更新事件 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period = 100 - 1; TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // 设置更新事件为触发输出 TIM_Cmd(TIM3, ENABLE); // 启动定时器 // 5. 配置ADC1 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 使能扫描模式! ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 使能连续转换! ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // 定时器3触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = ADC_CHANNEL_NUM; // 这里填3 ADC_Init(ADC1, &ADC_InitStructure); // 6. 配置ADC1的规则通道组(顺序:0 -> 1 -> 4) ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 3, ADC_SampleTime_55Cycles5); // 7. 使能ADC1的DMA请求 ADC_DMACmd(ADC1, ENABLE); // 8. 使能ADC1 ADC_Cmd(ADC1, ENABLE); // 9. 校准ADC1 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 10. 由定时器触发开始转换(对于外部触发,需要先软件启动一次) ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 注意:在使能了外部触发且为连续模式后,第一次可以用软件启动,之后将由定时器自动触发。配置完成后,整个系统就开始自动运行了:TIM3每10ms产生一个触发信号,ADC1收到信号后,依次快速转换通道0、1、4,每转换完一个数据,DMA就立刻将其搬运到ADC_ConvertedValue数组的对应位置。由于是循环模式,这个搬运过程会一直持续。此时,你的主循环while(1)里几乎不需要做任何事情,只需要定期(比如每秒)去读取ADC_ConvertedValue数组里的最新值进行处理即可。CPU占用率极低。
6. 常见问题、调试技巧与进阶优化
6.1 采样值不稳定(跳动大)
这是ADC调试中最常见的问题。别急着怀疑代码,按以下顺序排查硬件和软件:
- 电源与地:这是首要怀疑对象。用示波器测量VDDA和VSSA引脚,看纹波是否过大(应小于50mV)。确保模拟部分和数字部分用电感或磁珠隔离,并在VDDA引脚最近处放置10uF钽电容+0.1uF陶瓷电容。
- 参考电压:如果VREF+是独立引脚,确保其连接了稳定、干净的参考电压源。如果是与VDDA相连,则VDDA的质量就是参考电压的质量。
- 信号源阻抗:ADC输入引脚内部可以等效为一个采样开关和一个小电容。在采样期间,需要信号源在采样时间内对该电容充电到稳定值。如果信号源阻抗太高(比如用了几百kΩ的电阻分压),充电就会很慢,导致采样不充分。解决方法:a) 在ADC输入引脚对地加一个100pF~10nF的小电容(注意:这会形成一个低通滤波器,可能影响高频信号)。b) 显著增加该通道的采样时间(设置为239.5个周期)。c) 在分压电路后加一个电压跟随器(运放)进行缓冲,提供低阻抗输出。
- 采样时间不足:如第3点所述,对于高阻抗源,必须增加
ADC_SampleTime。用公式算一下充电时间是否足够。 - PCB布局与走线:模拟信号线应远离数字信号线(尤其是时钟、PWM线),最好用地线隔离。避免在ADC输入引脚附近走高频数字线。
- 软件滤波:硬件无法完全消除噪声时,软件滤波是最后的手段。最简单的是一阶滞后滤波(低通滤波):
new_value = old_value * 0.9 + sample * 0.1。更复杂的有中值滤波、滑动平均滤波等。
6.2 DMA传输数据错位或溢出
现象:数组里的数据顺序不对,或者数据全是0或固定值。
- 检查DMA缓冲区大小:
DMA_BufferSize必须等于你希望DMA一次传输的数据量。对于多通道扫描,这等于通道数(N)。DMA会在传输完N个数据后,才产生传输完成中断(如果使能了的话)或重置指针(循环模式)。 - 检查内存地址递增:
DMA_MemoryInc必须使能,并且DMA_MemoryDataSize要与数组元素类型匹配(uint16_t对应HalfWord)。 - 检查ADC的DMA是否使能:务必调用
ADC_DMACmd(ADCx, ENABLE)。 - 检查ADC是否真的在转换:可以用
ADC_GetFlagStatus(ADCx, ADC_FLAG_EOC)在循环里检查EOC标志,或者用逻辑分析仪/示波器查看ADC的转换完成信号引脚(如果有引出)。 - DMA通道冲突:确保没有其他外设使用了同一个DMA通道。STM32的DMA通道是多个外设复用的。
6.3 转换速度达不到预期
计算的理论采样率很高,但实际测量发现间隔时间很长。
- 计算总转换时间:务必用公式
Tconv = (采样周期 + 12.5) / ADCCLK计算单个通道的转换时间。多通道扫描时,一次扫描的总时间是Tscan = N * Tconv。你的触发间隔(定时器周期)必须大于等于Tscan,否则ADC还在忙,新的触发会丢失。 - 检查ADCCLK:STM32F1的ADC时钟最大为14MHz,且来自APB2时钟(最大72MHz)的分频。确保
RCC_ADCCLKConfig(RCC_PCLK2_Div6)这样的分频配置正确,使得ADCCLK不超过14MHz。 - 连续模式下的数据覆盖:在连续转换+扫描模式下,如果不用DMA,你必须以快于
Tscan的速度读取ADC_DR寄存器,否则数据会被新转换的数据覆盖。这就是为什么这种模式必须搭配DMA。
6.4 使用内部参考电压校准VDDA
这是一个高级技巧,能显著提高测量精度。STM32内部有一个出厂时校准过的参考电压VREFINT(约1.2V)。在芯片温度变化时,VDDA可能会波动,但VREFINT相对于VDDA的比例是稳定的。我们可以通过测量VREFINT通道的ADC值,反向推算出实际的VDDA电压。
- 在初始化时,除了测量外部通道,也把内部参考电压通道(
ADC_Channel_Vrefint)加入扫描序列,或者单独测量一次。 - 读取
VREFINT的ADC原始值,记为AdcVrefint。 - 从芯片数据手册或参考手册中找到
VREFINT的标准电压值Vrefint_cal(通常在1.20V~1.25V之间,这个值在芯片生产时被校准并存储在系统存储区的一个特定地址,如0x1FFFF7BA)。 - 计算实际VDDA:
Vdda_actual = Vrefint_cal * 4096 / AdcVrefint。 - 用计算出的
Vdda_actual去计算其他外部通道的真实电压:Vchannel_actual = AdcChannel * Vdda_actual / 4096。
这种方法可以有效补偿电源电压波动带来的测量误差,特别适合电池供电等电压不稳定的场景。
6.5 注入通道的使用
我们前面用的都是“规则通道”。STM32还有一组“注入通道”,你可以把它理解为一个可以“插队”的高优先级通道组。当注入通道被触发时(可以是外部事件,如过压信号),它会立即中断当前正在进行的规则通道转换,优先执行注入通道的转换,完成后再回到规则通道。这在处理紧急信号时非常有用。标准外设库也提供了ADC_InjectedChannelConfig等函数来配置注入通道,其使用方式与规则通道类似,但有独立的数据寄存器(JDRx)和触发源。