3. ADC参考电压校准

ADC参考电压校准 学习卡片

任务定义

本案例针对STM32F103系列MCU内置ADC的参考电压(VREFINT)通道,实现一种无需外部精密基准源即可校准ADC精度的方案。任务目标是:读取芯片内部固定的1.20V(典型值)参考电压通道,反推当前实际VDDA供电电压,进而修正ADC转换结果,使ADC读数在VDDA偏离标称值3.3V时仍能保持准确。完成本案例后,你将获得一个可直接移植的ADC校准函数,以及验证该校准逻辑正确性的方法。

校准原理

STM32F103的ADC内置一个连接到通道17(ADC_IN17)的内部参考电压VREFINT,其典型值为1.20V(实际值因芯片批次略有差异,存储在芯片出厂时烧录的地址0x1FFFF7E2处,为12位ADC原始值)。由于VREFINT是稳定的带隙基准,不随VDDA变化,因此通过测量VREFINT通道的ADC值,可以反推当前VDDA:

VDDA = 1.20V × 4095 / ADC_VREFINT_measured

得到实际VDDA后,任意通道的修正电压为:

V_channel = ADC_channel × VDDA / 4095

本案例使用芯片出厂校准值(而非典型值1.20V)以提高精度,校准值地址为0x1FFFF7E2,存储格式为uint16_t,表示在VDDA=3.3V时VREFINT通道的ADC读数。

硬件连接

本案例仅需STM32F103最小系统板(如Blue Pill),无需外部器件。将待测电压(0~3.3V)连接到PA1(ADC1通道1),同时确保VDDA引脚连接稳定的3.3V电源。若需验证校准效果,可准备一个可调稳压源或精密分压器产生已知电压。

代码实现


#include "stm32f10x.h"
#include <stdio.h>

// 芯片出厂VREFINT校准值地址(12位ADC原始值,对应VDDA=3.3V时测得)
#define VREFINT_CAL_ADDR    ((uint16_t*)0x1FFFF7E2)

static uint16_t vrefint_cal;   // 出厂校准值
static float vdda_actual;      // 反推的实际VDDA

// 初始化ADC1:通道1(PA1) + 通道17(VREFINT)
void ADC_Calibration_Init(void) {
    GPIO_InitTypeDef gpio;
    ADC_InitTypeDef adc;
    
    // 读取出厂校准值
    vrefint_cal = *VREFINT_CAL_ADDR;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  // ADC时钟=12MHz
    
    // PA1模拟输入
    gpio.GPIO_Pin = GPIO_Pin_1;
    gpio.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &gpio);
    
    // ADC1配置:独立模式,右对齐,单次转换
    adc.ADC_Mode = ADC_Mode_Independent;
    adc.ADC_ScanConvMode = DISABLE;
    adc.ADC_ContinuousConvMode = DISABLE;
    adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    adc.ADC_DataAlign = ADC_DataAlign_Right;
    adc.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &adc);
    
    // 使能ADC并复位校准寄存器
    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1));
}

// 读取指定通道的ADC原始值(单次转换)
uint16_t ADC_ReadChannel(uint8_t channel) {
    ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5);
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
    return ADC_GetConversionValue(ADC1);
}

// 更新实际VDDA(每次VDDA可能变化时调用)
void ADC_UpdateVDDA(void) {
    uint16_t vrefint_measured = ADC_ReadChannel(ADC_CHANNEL_17);
    // 使用出厂校准值反推:VDDA = 3.3 * vrefint_cal / vrefint_measured
    vdda_actual = 3.3f * (float)vrefint_cal / (float)vrefint_measured;
}

// 读取通道电压(已校准,单位:V)
float ADC_ReadVoltage(uint8_t channel) {
    uint16_t raw = ADC_ReadChannel(channel);
    return (float)raw * vdda_actual / 4095.0f;
}

// 简单测试:读取PA1电压并打印
int main(void) {
    ADC_Calibration_Init();
    ADC_UpdateVDDA();  // 首次更新VDDA
    
    while (1) {
        float v_ch1 = ADC_ReadVoltage(ADC_Channel_1);
        printf("VDDA=%.3fV, PA1=%.3fV\r\n", vdda_actual, v_ch1);
        for (volatile uint32_t i = 0; i < 7200000; i++);  // 约100ms延时
    }
}

关键实现说明

代码首先从固定地址0x1FFFF7E2读取出厂校准值,该值是在出厂测试时以VDDA=3.3V为基准测得的VREFINT通道ADC读数。每次需要测量电压前,先调用ADC_UpdateVDDA()读取当前VREFINT通道值,按比例关系反推实际VDDA。随后ADC_ReadVoltage()将任意通道的原始ADC值乘以实际VDDA再除以4095,得到修正后的电压值。若VDDA恰好为3.3V,则校准结果与直接使用3.3V计算一致;若VDDA漂移(例如降至3.0V),未校准的读数将偏低约9%,而校准后仍能输出正确电压。

验证方法

验证步骤:

  1. 编译并烧录代码,打开串口终端(波特率115200),观察输出。
  2. 将PA1接地,预期输出PA1≈0.000V。
  3. 将PA1接至VDDA(3.3V),预期输出PA1≈3.300V(误差±20mV内)。
  4. 使用可调电源将VDDA从3.3V缓慢调低至3.0V,观察输出:VDDA=字段应跟随变化(约3.0V),且PA1接VDDA时读数仍应接近3.0V(而非错误的2.7V)。

判定标准:当VDDA从3.3V变化到3.0V时,PA1接VDDA的读数误差小于30mV,且VDDA显示值与万用表实测值偏差小于50mV,即判定校准功能有效。若VDDA不变时读数偏差较大,可检查供电稳定性或校准值读取是否正确。

ADC参考电压校准数据流与验证拓扑 STM32F103 ADC1 通道1 (PA1) ADC1 通道17 (VREFINT) 校准计算单元 0x1FFFF7E2 出厂校准值 待测电压 0~3.3V → PA1 VDDA (3.0~3.6V) 串口输出 VDDA=3.312V PA1=1.650V ① 输入已知电压 ② 观察校准后读数 校准公式:VDDA = 3.3 × 出厂校准值 / VREFINT实测值;V_ch = ADC_raw × VDDA / 4095

编译与运行

使用Keil MDK或STM32CubeIDE创建工程,选择STM32F103C8T6芯片。需包含标准外设库(SPL)或使用寄存器方式。代码中使用了printf重定向,需在工程中实现fputc函数将输出重定向到USART1(PA9/PA10,115200-8-N-1)。编译下载后,打开串口助手即可观察数据。