STM32F4 ADC 内部基准电压校准在高速采样下的漂移补偿策略

1. 漂移问题的根源

STM32F4 系列(如 STM32F407)内置 12 位 ADC,支持最高 2.4 MSPS 采样率。其内部基准电压 VREFINT 是一个带隙参考源,标称值为 1.21V,但实际值随温度(典型 ±5mV/°C)和 VDDA 电压(线性调整率约 0.1%/V)漂移。在高速采样时,ADC 内部开关电容网络动态功耗增大,导致芯片局部温升,进一步加剧 VREFINT 漂移。若不补偿,ADC 转换结果将产生系统性误差,尤其在测量小信号或需要高精度比例测量时,误差可达 ±10 LSB 以上。

2. 补偿原理:比例测量与实时校准

2.1 比例测量基础

ADC 转换结果 D 与输入电压 Vin 的关系为:

D = (Vin / VREFINT) * 4095

若 VREFINT 漂移,D 随之变化。但若同时测量 VREFINT 自身(通过内部通道),则可反推实际 VREFINT 值:

VREFINT_actual = (VREFINT_CAL / D_VREFINT) * 4095

其中 VREFINT_CAL 是出厂校准值(存储在地址 0x1FFF7A2A,12 位)。

2.2 动态补偿策略

在高速采样中,我们无法频繁插入 VREFINT 测量(会中断采样流)。因此采用分段补偿

  • 在采样序列中周期性插入 VREFINT 测量(例如每 1000 次采样插入一次)。
  • 利用插值算法估算当前 VREFINT 值,并修正所有采样结果。
  • 结合过采样技术(硬件或软件)降低噪声,提高修正精度。

3. 硬件与软件配置

3.1 硬件连接

  • 使用 STM32F407 开发板,ADC1 通道 0(PA0)接模拟信号。
  • 确保 VDDA 与 VREF+ 连接稳定,VREF- 接地。
  • 建议在 VDDA 与 GND 间加 1μF 和 100nF 去耦电容。

3.2 寄存器配置步骤

  1. 使能时钟:ADC1、GPIOA、DMA1 时钟。
  2. 配置 GPIO:PA0 为模拟输入。
  3. 配置 ADC1
    • 分辨率 12 位,右对齐。
    • 扫描模式关闭,连续转换模式开启。
    • 采样时间设为 84 周期(高速采样下保证精度)。
    • 使能 DMA 循环模式。
  4. 配置 DMA:传输数据到内存缓冲区。
  5. 校准 ADC:上电后执行一次标准校准。
  6. 读取 VREFINT 校准值:从系统存储器读取。

4. 完整代码示例

以下代码基于 STM32 HAL 库,实现带漂移补偿的 ADC 高速采样。

#include "stm32f4xx_hal.h"

// 缓冲区定义
#define ADC_BUFFER_SIZE 1024
uint16_t adc_buffer[ADC_BUFFER_SIZE];
uint16_t vrefint_buffer[8];  // 存储 VREFINT 采样值

// 出厂校准值(12位)
#define VREFINT_CAL_ADDR 0x1FFF7A2A
uint16_t vrefint_cal = *(uint16_t*)VREFINT_CAL_ADDR;

// 补偿系数
float vrefint_actual;
float compensation_factor;

// ADC 句柄
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_Init(void) {
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();

    // GPIO 配置
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // ADC 配置
    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 21MHz
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = DISABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 1;
    hadc1.Init.DMAContinuousRequests = ENABLE;
    hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
    HAL_ADC_Init(&hadc1);

    // 配置通道0(PA0)
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // DMA 配置
    hdma_adc1.Instance = DMA1_Stream0;
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;
    hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_adc1);

    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

    // ADC 校准
    HAL_ADCEx_Calibration_Start(&hadc1);

    // 启动 ADC 和 DMA
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
}

// 读取 VREFINT 通道(内部通道17)
void Read_VREFINT(void) {
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_VREFINT;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 停止当前 DMA 转换
    HAL_ADC_Stop_DMA(&hadc1);

    // 连续采样 VREFINT 多次取平均
    for (int i = 0; i < 8; i++) {
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 10);
        vrefint_buffer[i] = HAL_ADC_GetValue(&hadc1);
        HAL_ADC_Stop(&hadc1);
    }

    // 计算平均值
    uint32_t sum = 0;
    for (int i = 0; i < 8; i++) sum += vrefint_buffer[i];
    uint16_t vrefint_avg = sum / 8;

    // 计算实际 VREFINT 电压(假设 VDDA=3.3V,但这里用校准值反推)
    // 注意:VREFINT_CAL 是在 VDDA=3.3V 时测得的,因此实际 VDDA 可由下式得到
    vrefint_actual = 3.3f * vrefint_cal / vrefint_avg;

    // 补偿因子:实际 VREFINT 与标称 1.21V 的比值
    compensation_factor = 1.21f / vrefint_actual;

    // 恢复通道0配置
    sConfig.Channel = ADC_CHANNEL_0;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 重新启动 DMA
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
}

// 补偿采样值
float Get_Compensated_Value(uint16_t raw) {
    // 将原始值转换为电压(基于实际 VREFINT)
    float voltage = (float)raw * vrefint_actual / 4095.0f;
    // 应用补偿因子(可选,若需标称电压)
    return voltage * compensation_factor;
}

int main(void) {
    HAL_Init();
    ADC_Init();

    // 初始读取 VREFINT
    Read_VREFINT();

    while (1) {
        // 每 1000 次采样重新校准(简单计数)
        static uint32_t counter = 0;
        if (counter++ >= 1000) {
            counter = 0;
            Read_VREFINT();
        }

        // 处理最新 DMA 数据(例如取平均值)
        // 注意:DMA 循环写入,需防止数据竞争,可使用双缓冲或临界区
        uint32_t sum = 0;
        for (int i = 0; i < ADC_BUFFER_SIZE; i++) {
            sum += adc_buffer[i];
        }
        float avg_raw = (float)sum / ADC_BUFFER_SIZE;
        float compensated = Get_Compensated_Value((uint16_t)avg_raw);

        // 此处可输出或使用 compensated
        HAL_Delay(10);
    }
}

5. 注意事项

  • 校准时机:VREFINT 测量会中断采样流,建议在采样间隙或低优先级任务中执行。若需连续高速采样,可增加 DMA 缓冲并采用双缓冲机制,避免数据覆盖。
  • 滤波:VREFINT 采样值应多次平均(如 8 次)以降低噪声。
  • 温度影响:若环境温度变化剧烈,可提高校准频率;反之可降低。
  • VDDA 稳定性:确保 VDDA 电源纹波小于 10mV,否则补偿效果有限。
  • 过采样:对于更高精度,可开启硬件过采样(若支持)或软件累加平均,但需注意采样速率下降。
  • 校准值有效性:出厂校准值仅适用于 3.3V 供电,若 VDDA 不同,需重新标定。

6. 总结

通过周期性测量 VREFINT 并动态计算补偿因子,可有效抑制温度与电源波动引起的 ADC 漂移,尤其在高速采样场景下显著提升测量一致性。本策略实现简单、开销小,适用于工业控制、传感器采集等对精度要求较高的嵌入式系统。开发者可根据实际需求调整校准周期与滤波深度,达到性能与精度的平衡。