1. 项目概述:为什么需要关注STM32的内部参照电压?

在嵌入式开发,尤其是基于STM32这类MCU进行精密测量时,我们常常会遇到一个看似简单却容易踩坑的问题:如何确保ADC(模数转换器)读数的准确性?很多工程师在调试传感器,比如读取一个分压电阻的电压来反推温度或压力时,会发现测量值会随着系统供电电压的波动而“飘”。你可能会反复校准代码、检查电路,甚至怀疑ADC本身出了问题,但根源可能在于那个容易被忽略的“参考基准”。

STM32芯片内部其实藏着一个硬件“定海神针”——内部参照电压(Internal Reference Voltage)。它不是一个给ADC供电的电源,而是一个高精度、高稳定性的电压基准源,在芯片内部直接连接到了ADC1的通道17。它的典型值在1.20V左右,并且最关键的特性是,其电压值几乎不随外部VCC电压的变化而变化。这就好比在波涛汹涌的海面上(变化的VCC),有一个固定在深海海床上的灯塔(稳定的内部参照电压),为我们提供了判断位置的绝对坐标。

不少人会把它和ADC的参考电压Vref+搞混。简单来说,Vref+是ADC进行转换时所用的“尺子”的总长度,而内部参照电压是刻在这把尺子上的一个“标准刻度”。在Vref+这把“尺子”本身会伸缩(即VCC波动或外部参考不稳)的情况下,通过读取内部参照电压这个“标准刻度”的ADC值,我们就能反向推算出此刻“尺子”的实际长度,从而对所有其他通道的测量值进行校准,得到真实的电压值。这个方法在电池供电设备(电压逐渐下降)、对成本敏感无法使用外部基准源的场合,或者利用内置温度传感器进行系统补偿时,尤其有效。接下来,我将拆解其原理,并分享从理论到实战,再到避坑的完整经验。

2. 核心原理深度解析:参照电压与ADC参考电压的区分

要正确使用内部参照电压,必须从根本上厘清几个关键概念,这是避免后续一切混乱和错误的基础。

2.1 ADC的参考电压(Vref+):测量的“尺子”

ADC的工作本质是将一个连续的模拟电压值,映射到一个离散的数字值上。这个映射过程需要一个参考基准,这就是Vref+。对于STM32,ADC的输出数字值N与输入电压Vin的关系由以下公式决定: N = (Vin / Vref+) * (2^分辨率 - 1) 对于12位ADC, 2^12 - 1 = 4095 。因此, Vin = (N / 4095) * Vref+

这里的关键在于 Vref+的稳定性直接决定了测量的绝对精度 。如果Vref+是波动的3.3V,那么同一个1.0V的输入信号,在Vref+为3.3V时和3.2V时,计算出来的Vin会不同。STM32的Vref+引脚设计因封装而异:

  • 对于100脚及以上封装 :Vref+是一个独立的引脚。你可以选择连接一个外部高精度、低温漂的基准电压源(如REF3030,3.0V),从而获得极高的测量稳定性。
  • 对于64脚及以下封装 :为了节省引脚,Vref+在芯片内部与VCC(即你的MCU供电电压)直接相连。这意味着,你的ADC参考电压就是你的板载3.3V电源。任何来自LDO的纹波、负载变化引起的压降、电池电量衰减,都会直接导致你的ADC“尺子”长度发生变化,测量值自然就会“飘”。

注意 :即使对于有独立Vref+引脚的大封装型号,如果为了省成本或简化设计而将Vref+直接连接到VCC,那么其效果就和内置连接的小封装型号一样,参考电压会随VCC波动。

2.2 内部参照电压(Vrefint):尺子上的“标准刻度”

内部参照电压是一个完全不同的概念。它是芯片在生产时,通过半导体工艺制造出的一个带隙基准电压源(Bandgap Reference)。其物理特性决定了它能在很宽的电压和温度范围内(当然是在芯片工作条件下)输出一个极其稳定的电压值,典型值为1.20V。它不负责给ADC供电,而是作为ADC可以测量的一个 输入信号 ,固定连接在ADC1的通道17上。

你可以把它想象成一把尺子(Vref+)上被精密刻下的一个1.20厘米的标记(Vrefint)。正常情况下,我们用尺子量东西,默认尺子本身是准的。但如果尺子是橡胶做的(Vref+会变),今天拉得长一点,明天缩得短一点,那么用它量出的所有长度都不准了。这时,如果我们每次测量前,先用这把橡胶尺子去量一下那个永恒的1.20厘米标记(读取Vrefint的ADC值),发现今天量出来是1.15厘米,明天量出来是1.25厘米,我们就知道尺子今天被拉长了还是缩短了,从而可以修正所有其他测量结果。

2.3 校准公式的推导与理解

官方给出的公式 Vchx = Vrefint * (ADchx / ADrefint) 看似简单,但其背后是比例关系的巧妙运用。我们来一步步推导:

  1. 理想情况(Vref+稳定) :若Vref+恒定,测量任意通道电压 Vchx ,其ADC值为 ADchx ,则有: Vchx_ideal = (ADchx / 4095) * Vref+_ideal 同样,测量内部参照电压 Vrefint (恒定1.20V),其ADC值为 ADrefint_ideal ,则有: Vrefint = (ADrefint_ideal / 4095) * Vref+_ideal

  2. 实际情况(Vref+波动) :当Vref+变为 Vref+_real 时,我们实际读到的 ADrefint_real ADchx_real 是基于这个变化的参考电压得到的: Vrefint = (ADrefint_real / 4095) * Vref+_real ... (1) Vchx_real = (ADchx_real / 4095) * Vref+_real ... (2)

  3. 消除Vref+_real :我们的目标是求出 Vchx_real ,但 Vref+_real 未知。将公式(1)变形: Vref+_real = Vrefint * 4095 / ADrefint_real 。 将此式代入公式(2): Vchx_real = (ADchx_real / 4095) * (Vrefint * 4095 / ADrefint_real) 化简后得到: Vchx_real = Vrefint * (ADchx_real / ADrefint_real)

推导结论 :这个公式的精妙之处在于,它完全消去了未知且波动的 Vref+_real 和ADC的满量程数字 4095 。最终计算只依赖于三个量:已知的 Vrefint 理论值(1.20V)、实时测得的参照电压ADC值 ADrefint_real 、以及实时测得的目标通道ADC值 ADchx_real 。这意味着,只要 Vrefint 本身足够稳定,我们就能在Vref+(或VCC)大幅波动的情况下,依然获得准确的绝对电压测量值。这本质是一种 比例测量法 ,用内部基准来校准外部参考。

3. 实战步骤:从配置到计算的完整代码实现

理解了原理,我们进入实战环节。我将以STM32CubeIDE/HAL库环境为例,展示如何一步步实现基于内部参照电压的ADC校准测量。

3.1 硬件与软件环境准备

  • 硬件 :任意一款STM32开发板(如STM32F103C8T6,属于64脚以下封装,Vref+内部连接VCC,是演示此技术必要性的绝佳平台)。
  • 软件 :STM32CubeMX, STM32CubeIDE 或 Keil MDK。
  • 目标 :测量一个连接到PA0(ADC1_IN0)的外部可调电压(例如通过电位器分压得到),并利用内部参照电压校准,在串口打印出准确的电压值。

3.2 使用STM32CubeMX进行基础配置

  1. ADC配置

    • Analog -> ADC1 下,启用 IN0 通道(对应你的输入引脚)。
    • 关键一步:在 ADC1 的配置参数中,找到 Internal Channels ,勾选 Vrefint 。这一步至关重要,它使能了内部参照电压到ADC的输入通路。
    • 设置ADC参数:分辨率(12位)、扫描转换模式(Disable,单通道非扫描)、连续转换模式(Disable,使用单次触发)、外部触发源(软件触发)。时钟预分频根据系统时钟调整,保证ADC时钟不超过手册规定(通常14MHz)。
    • DMA Settings 标签页,可以添加DMA以提高效率(可选,本例为简化用轮询)。
  2. 串口配置 :启用一个USART(如USART1)用于打印数据,配置为异步模式,波特率115200。

  3. 生成代码 :配置好时钟树(确保系统时钟正确)后,生成工程代码。

3.3 核心代码实现与详解

打开生成的工程,我们在 main.c 的用户代码区添加功能。

/* 私有变量定义 */
uint32_t adc_value_ch0 = 0;
uint32_t adc_value_vrefint = 0;
float measured_voltage = 0.0f;
const float VREFINT_CALIB = 1.20f; // 理论典型值,可根据实际校准调整

/* 私有函数声明 */
static uint32_t Read_ADC_Channel(ADC_HandleTypeDef* hadc, uint32_t channel);

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();

  printf("STM32 Internal Vrefint Calibration Demo Started.\r\n");

  while (1) {
    // 步骤1:读取内部参照电压通道(ADC1_CH17)的原始值
    adc_value_vrefint = Read_ADC_Channel(&hadc1, ADC_CHANNEL_VREFINT);
    HAL_Delay(10); // 短暂延时,避免ADC连续转换过热影响精度

    // 步骤2:读取外部待测通道(ADC1_CH0)的原始值
    adc_value_ch0 = Read_ADC_Channel(&hadc1, ADC_CHANNEL_0);
    HAL_Delay(10);

    // 步骤3:应用校准公式计算真实电压
    // 公式:V_ch0 = VREFINT_CALIB * (adc_value_ch0 / adc_value_vrefint)
    if (adc_value_vrefint != 0) { // 避免除零错误
      measured_voltage = VREFINT_CALIB * ((float)adc_value_ch0 / (float)adc_value_vrefint);
    } else {
      measured_voltage = 0.0f;
    }

    // 步骤4:输出结果
    printf("ADC_Ch0: %4lu, ADC_Vrefint: %4lu, Calculated Voltage: %.3f V\r\n",
           adc_value_ch0, adc_value_vrefint, measured_voltage);

    HAL_Delay(500); // 每500ms测量一次
  }
}

/**
  * @brief  读取指定ADC通道的单次转换值(轮询方式)
  * @param  hadc: ADC句柄指针
  * @param  channel: 要读取的ADC通道
  * @retval 12位ADC原始值
  */
static uint32_t Read_ADC_Channel(ADC_HandleTypeDef* hadc, uint32_t channel) {
  ADC_ChannelConfTypeDef sConfig = {0};
  uint32_t raw_value = 0;

  // 配置要转换的通道
  sConfig.Channel = channel;
  sConfig.Rank = 1; // 单通道,Rank设为1
  sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 采样时间,可根据信号源阻抗调整
  if (HAL_ADC_ConfigChannel(hadc, &sConfig) != HAL_OK) {
    Error_Handler();
  }

  // 启动ADC转换
  if (HAL_ADC_Start(hadc) != HAL_OK) {
    Error_Handler();
  }

  // 等待转换完成,超时时间10ms
  if (HAL_ADC_PollForConversion(hadc, 10) == HAL_OK) {
    // 读取转换值
    raw_value = HAL_ADC_GetValue(hadc);
  }

  // 停止ADC转换
  HAL_ADC_Stop(hadc);

  return raw_value;
}

代码关键点解析

  1. 通道配置 :每次读取前都需要用 HAL_ADC_ConfigChannel 重新配置通道。因为我们的ADC工作在单次、单通道模式,每次测量的目标通道可能不同(Vrefint或Ch0)。
  2. 采样时间 ADC_SAMPLETIME_239CYCLES_5 是一个相对较长的采样时间,能保证对高阻抗信号源(如电位器)的采样充分,提高精度。对于内部Vrefint通道,这个时间也足够了。
  3. 转换顺序 :在每次计算前,都先读取 Vrefint ,再读取目标通道。这是为了确保两个读数是在尽可能接近的时间点获取的,此时VCC(即Vref+)的波动最小,校准效果最好。
  4. 浮点运算 :公式中使用了浮点数除法。对于没有FPU的M0/M3内核,频繁计算会有性能开销。在实际产品中,如果MCU负载重,可以考虑将 VREFINT_CALIB 放大一定倍数(如1000),用整数运算,最后再缩放。

4. 精度提升与出厂校准值的使用

直接使用1.20V这个典型值进行计算,虽然能消除Vref+波动的影响,但精度受限于 Vrefint 本身的个体误差(数据手册给出最小1.16V,最大1.24V)。为了达到更高精度,ST在芯片出厂时,对每个芯片的 Vrefint 在特定条件(通常为3.3V Vref+, 30°C)下进行了测量,并将测量得到的ADC参考值存储在了芯片的系统存储区(System Memory)中。

4.1 获取与使用出厂校准值

对于STM32F1系列,这个值存储在地址 0x1FFFF7BA 0x1FFFF7C0 (两个字节,实际是 0x1FFFF7BA 0x1FFFF7BB 组成一个16位整数)。其他系列地址不同,需查阅对应芯片的参考手册(Reference Manual)中“Device electronic signature”或“ADC calibration values”章节。

这个存储值 VREFINT_CAL 代表的是,当Vref+为3.3V(或芯片指定的校准电压Vref+_cal,通常是3.0V或3.3V,需查手册)时,测量内部 Vrefint (理论值1.20V)所得到的ADC原始值。我们可以利用它来反推出当前Vref+下的 Vrefint 实际ADC值应该是什么,或者直接计算出一个更精确的 Vrefint 实际电压值。

方法一:直接计算精确的Vrefint实际电压 假设已知校准条件:Vref+_cal = 3.3V, 读取到的出厂校准值 VREFINT_CAL = 1650 (举例)。 在理想情况下, Vrefint_actual = (VREFINT_CAL / 4095) * Vref+_cal 。 但我们的Vref+_real可能不是3.3V。更通用的方法是,利用当前测得的 ADrefint_real 和出厂校准值 VREFINT_CAL 的比例关系,来修正当前Vref+下的测量。

方法二:修正计算(更常用,更准确) 校准公式可以演进为: Vchx = (Vrefint_typical * Vref+_cal) * (ADchx / VREFINT_CAL) * (VREFINT_CAL / ADrefint_real) 简化后,一个更实用的公式是: Vchx = 3.3V * (ADchx / VREFINT_CAL) * (VREFINT_CAL / ADrefint_real) 但更常见的做法是,将其融入到一个系数中:

实际上,HAL库提供了一个更简洁的方式。我们可以直接使用 __HAL_ADC_CALC_VREFANALOG_VOLTAGE __HAL_ADC_CALC_DATA_TO_VOLTAGE 这些宏(具体宏名可能因系列略有差异),它们内部已经封装了读取校准值和计算的逻辑。但理解其手动实现更有助于调试:

// 以STM32F103为例,读取出厂校准值
#define VREFINT_CAL_ADDR ((uint16_t*) (0x1FFFF7BA)) // F1系列地址
uint16_t vrefint_calibration_value = *VREFINT_CAL_ADDR;

// 在计算电压时,使用校准值进行比例修正
float measured_voltage_calibrated = 0.0f;
if (adc_value_vrefint != 0) {
    // 核心修正公式:V_chx = 3.3V * (AD_chx / VREFINT_CAL) * (VREFINT_CAL / AD_vrefint)
    // 化简后:V_chx = 3.3V * (AD_chx / AD_vrefint)
    // 但注意,这个3.3V是校准时的Vref+_cal。如果实际Vref+_real也是3.3V,且稳定,那直接用这个公式。
    // 如果Vref+_real不稳定,我们仍然需要AD_vrefint来反映实时比例。
    // 更严谨的、同时消除Vrefint个体误差和Vref+实时波动的公式是:
    // V_chx = (Vrefint_typical * Vref+_cal / VREFINT_CAL) * (AD_chx / AD_vrefint) * (VREFINT_CAL / AD_vrefint)? 这里需要仔细推导。
    // 实际上,常见的工程简化做法是:
    // 1. 认为 Vrefint_actual ≈ (VREFINT_CAL / 4095) * Vref+_cal
    // 2. 代入原始公式 Vchx = Vrefint_actual * (ADchx / ADrefint_real)
    // 得到: Vchx = [(VREFINT_CAL / 4095) * Vref+_cal] * (ADchx / ADrefint_real)
    // 其中 Vref+_cal 是已知的校准电压(如3.3V)。
    measured_voltage_calibrated = ((float)vrefint_calibration_value / 4095.0f) * 3.3f;
    measured_voltage_calibrated = measured_voltage_calibrated * ((float)adc_value_ch0 / (float)adc_value_vrefint);
}

操作心得 :对于精度要求不高于1%的应用,使用1.20V典型值进行比例计算已经能极大改善VCC波动带来的影响。对于精度要求更高的场合(如0.5%以内),务必使用出厂校准值。同时要注意,出厂校准是在特定温度和Vref+下进行的,温度变化会引入新的误差,对于宽温范围应用,可能需要额外的温度补偿算法。

5. 典型应用场景与高级技巧

内部参照电压不仅仅用于校准普通ADC通道,它在一些特定场景下发挥着不可替代的作用。

5.1 为内部温度传感器测量提供基准

STM32芯片内部集成了一个温度传感器,它连接在ADC1的通道16(或特定通道,依型号而定)。其输出电压与温度成线性关系,公式通常为: Temperature (℃) = ((V_sense - V_25) / Avg_Slope) + 25 其中, V_25 是芯片在25°C时的传感器电压, Avg_Slope 是温度系数(mV/°C),这两个值在数据手册中给出,但通常是基于Vref+ = 3.3V的假设。

如果Vref+波动,直接使用上述公式和ADC原始值计算温度会不准。此时,必须先用内部参照电压 Vrefint 校准ADC的参考电压,得到准确的 V_sense 电压值后,再代入温度公式计算。HAL库中的 HAL_ADCEx_GetTemperature 函数内部其实就使用了类似机制(如果使能了 Vrefint 通道)。

5.2 监测系统供电电压(VCC/Battery)

这是一个非常实用的技巧,尤其对于电池供电设备。既然小封装STM32的Vref+等于VCC,而我们又能通过 Vrefint 测出实时的Vref+比例关系,那么反过来,我们就能计算出当前的VCC电压!

推导过程如下:

  1. 已知: Vrefint = (ADrefint_real / 4095) * VCC_real
  2. 因此, VCC_real = Vrefint * 4095 / ADrefint_real

其中 Vrefint 使用出厂校准值计算出的实际电压更准。这样,你只需要读取一个ADC通道(通道17),就能实时监控MCU的供电电压,实现低电量检测,而无需任何外部电阻分压电路!注意,测量VCC时,输入给ADC的电压不能超过Vref+(即VCC),所以这个方法在理论上是自洽的。

5.3 多通道扫描模式下的集成

在实际项目中,我们可能需要周期性测量多个外部通道和Vrefint通道。这时可以使用ADC的扫描模式+DMA。

  1. CubeMX配置 :在ADC配置中,启用扫描转换模式(Scan Conversion Mode)和连续转换模式(Continuous Conversion Mode)。在 Rank 序列中,依次添加 VREFINT 通道和所有需要的外部通道(如IN0, IN1...)。设置合理的采样顺序和采样时间。
  2. 启用DMA :在DMA Settings中添加一个DMA请求(模式设为循环模式,数据宽度为半字),让ADC转换完成后自动将数据搬运到内存数组。
  3. 代码处理 :启动ADC后,DMA会自动填充数组。假设数组 adc_values[0] 是Vrefint通道的值, adc_values[1] 是IN0的值,以此类推。然后在主循环或定时中断中,直接使用公式 Vx = 1.20 * (adc_values[n] / adc_values[0]) 进行计算即可。这种方法效率极高,且能保证所有通道的读数几乎是在同一时刻完成的,校准同步性最好。

6. 常见问题、误差分析与排查实录

即使理解了原理,实际调试中也可能遇到各种问题。下面是我在多个项目中总结的常见坑点和解决方案。

6.1 测量值跳动大,不稳定

  • 可能原因1:电源噪声 。这是最常见的原因。VCC上的纹波会直接作为噪声被ADC测量到。
    • 排查与解决 :用示波器测量MCU的VCC引脚和地之间的波形。确保电源电路有足够的滤波电容(如10uF电解电容并联0.1uF陶瓷电容,尽量靠近MCU电源引脚)。在ADC的VDDA引脚(如果独立)和VSSA引脚之间增加一个1uF+100nF的退耦电容。如果使用开关电源,考虑增加LC滤波。
  • 可能原因2:采样时间不足 。对于高阻抗信号源(如电位器),ADC的采样电容没有足够时间充电到稳定电压。
    • 排查与解决 :增加ADC通道的采样周期(Sampling Time)。在CubeMX中尝试 ADC_SAMPLETIME_239CYCLES_5 或更高。对于内部Vrefint通道,其输出阻抗较低,但较长的采样时间也无害。
  • 可能原因3:数字电路干扰 。当GPIO高速翻转、PWM输出时,会产生高频噪声耦合到模拟部分。
    • 排查与解决 :在ADC转换期间,尽量避免高速数字IO活动。可以尝试在读取ADC前,暂时关闭不必要的定时器、PWM输出。确保模拟地(VSSA)和数字地(VSS)通过单点良好连接。

6.2 校准后电压值仍有固定偏差

  • 可能原因1:未使用出厂校准值 。1.20V只是典型值,你的芯片实际值可能在1.16V到1.24V之间,这会导致约±3%的系统性偏差。
    • 排查与解决 :务必读取并使用存储在系统存储区的出厂校准值 VREFINT_CAL 。计算时使用 Vrefint_actual = 3.3 * (VREFINT_CAL / 4095) 来替代固定的1.20V。
  • 可能原因2:参考电压Vref+_cal不准确 。出厂校准是在特定的Vref+(如3.3V精确值)下进行的。如果你的实际Vref+(即使是稳定的)与这个校准电压有偏差,也会引入误差。
    • 排查与解决 :对于有独立Vref+引脚的型号,为其连接一个高精度基准源。对于Vref+连VCC的型号,确保给MCU供电的LDO或电源本身精度足够(如1%精度)。可以使用一个校准过的万用表测量实际VCC电压,并在代码中替换掉公式里的3.3V理论值。
  • 可能原因3:PCB布局与接地问题 。模拟信号走线过长,靠近数字信号线,或地线回路设计不当,会引入噪声和偏移。
    • 排查与解决 :遵循模拟电路布局原则:将ADC相关元件(滤波电容)尽量靠近MCU;模拟信号走线短而粗,避免与数字线平行走线;使用完整的接地平面,并确保模拟部分接地干净。

6.3 读取内部参照电压通道返回0或固定值

  • 可能原因1:未使能内部通道 。在CubeMX中忘记勾选 Vrefint ,或者在代码中未正确配置该通道。
    • 排查与解决 :检查CubeMX配置和生成的 ADC1_Init 函数,确认 Vrefint 通道被添加到了ADC的初始化序列中。在手动配置通道的函数调用中,确保通道参数是 ADC_CHANNEL_VREFINT
  • 可能原因2:采样序列配置错误 。在多通道扫描模式下, Rank 编号冲突或未正确设置。
    • 排查与解决 :检查每个通道的 Rank 值是否唯一且连续。确保 NumberOfConversion 设置正确,包含了所有通道(含Vrefint)。
  • 可能原因3:代码逻辑错误 。在单次转换模式下,每次读取前没有重新配置通道到 ADC_CHANNEL_VREFINT
    • 排查与解决 :参考本文3.3节的 Read_ADC_Channel 函数,确保每次调用都传入了正确的通道参数。

6.4 使用DMA时数据错位

  • 可能原因 :DMA缓冲区数组定义的数据类型或长度与ADC配置不匹配,或者通道顺序与DMA填充顺序理解有误。
    • 排查与解决 :确保DMA内存缓冲区(如 uint16_t adc_buffer[N] )的大小 N 大于等于ADC转换的通道数。在扫描模式下,DMA会按照你在CubeMX中配置的 Rank 顺序依次填充缓冲区。 adc_buffer[0] 对应Rank1的通道, adc_buffer[1] 对应Rank2的通道,以此类推。确认Vrefint通道所在的Rank,然后从数组对应位置取值。

最后一点实操心得 :在进行精密测量前,最好让ADC“热身”。上电后,先进行几次冗余的ADC转换并丢弃结果,让ADC内部电路稳定。同时,软件上可以对连续多次的测量结果进行简单的数字滤波,例如取连续10次读数的平均值或中位值,能有效抑制随机噪声,使读数更平滑稳定。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐