3. 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%,而校准后仍能输出正确电压。
验证方法
验证步骤:
- 编译并烧录代码,打开串口终端(波特率115200),观察输出。
- 将PA1接地,预期输出PA1≈0.000V。
- 将PA1接至VDDA(3.3V),预期输出PA1≈3.300V(误差±20mV内)。
- 使用可调电源将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不变时读数偏差较大,可检查供电稳定性或校准值读取是否正确。
编译与运行
使用Keil MDK或STM32CubeIDE创建工程,选择STM32F103C8T6芯片。需包含标准外设库(SPL)或使用寄存器方式。代码中使用了printf重定向,需在工程中实现fputc函数将输出重定向到USART1(PA9/PA10,115200-8-N-1)。编译下载后,打开串口助手即可观察数据。