返回博客
STM32 ADC 多通道采集与分压电路校准

STM32 ADC 多通道采集与分压电路校准

2025年4月8日STM32, ADC, 模拟电路

ADC 基本原理

STM32F103 的 ADC(Analog-to-Digital Converter,模数转换器)为 12 位分辨率,最大采样率 1Msps。ADC 将模拟电压转换为数字量,转换结果为 0-4095(0xFFF),对应 0-VREF+ 电压。

ADC 关键参数:

  • 分辨率:12 位,4096 级
  • 参考电压:VREF+(通常 3.3V)
  • 输入范围:0 至 VREF+
  • 转换公式:ADC_Value = (Vin / VREF+) × 4095

电压计算:

#define VREF 3.3f

float ADC_To_Voltage(uint16_t adc_value) {
    return (adc_value * VREF) / 4095.0f;
}

采样时间选择

采样时间(Sampling Time)决定 ADC 输入电容充电时间,影响转换精度和速度。采样时间越长,精度越高但速度越慢。

STM32F103 采样时间选项:

  • 1.5 周期:最快,适合低阻抗信号源
  • 7.5 周期:平衡速度与精度
  • 13.5 周期:中等精度
  • 28.5 周期:高精度
  • 41.5 周期、55.5 周期、71.5 周期、239.5 周期:更高精度

采样时间计算:

总转换时间 = 采样时间 + 12.5 周期(12 位转换)

72MHz ADC 时钟下,7.5 周期采样时间转换时间约为 1μs。

hadc1.Init.SamplingTimeCommon = ADC_SAMPLETIME_7CYCLES5;

规则组与注入组

STM32 ADC 分为规则通道(Regular Channels)和注入通道(Injected Channels)。

规则组

规则通道按顺序转换,最多 16 个通道。适用于常规采样。

ADC_ChannelConfTypeDef sConfig = {0};

sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

注入组

注入通道可中断规则组转换,优先级高,最多 4 个通道。适用于紧急采样(如过流保护)。

sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_INJECTED_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

单次/连续/扫描模式

单次模式

每次触发转换一个通道,转换完成后停止。

hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;

连续模式

转换完成后自动开始下一次转换,无需重新触发。

hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;

扫描模式

按顺序转换多个通道,配合 DMA 使用。

hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;

DMA 搬运多通道数据

多通道采集使用 DMA 自动搬运数据,避免 CPU 干预。

#include "stm32f1xx_hal.h"

#define ADC_CHANNEL_COUNT 4

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint32_t adc_values[ADC_CHANNEL_COUNT];  // DMA 缓冲区

void MX_ADC1_Init(void) {
    ADC_ChannelConfTypeDef sConfig = {0};
    
    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = ADC_CHANNEL_COUNT;
    HAL_ADC_Init(&hadc1);
    
    // 配置通道 0-3
    sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES5;
    
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = ADC_REGULAR_RANK_2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_2;
    sConfig.Rank = ADC_REGULAR_RANK_3;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_3;
    sConfig.Rank = ADC_REGULAR_RANK_4;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

void MX_DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    if (hadc->Instance == ADC1) {
        __HAL_RCC_ADC1_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        
        // PA0-PA3 作为 ADC 输入
        GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
        
        // ADC1 DMA 配置
        hdma_adc1.Instance = DMA1_Channel1;
        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_WORD;
        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
        hdma_adc1.Init.Mode = DMA_CIRCULAR;
        hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
        HAL_DMA_Init(&hdma_adc1);
        
        __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1);
    }
}

void ADC_Start(void) {
    HAL_ADC_Start_DMA(&hadc1, adc_values, ADC_CHANNEL_COUNT);
}

// 读取各通道电压
void ADC_Read_All_Channels(float *voltages) {
    for (int i = 0; i < ADC_CHANNEL_COUNT; i++) {
        voltages[i] = (adc_values[i] * 3.3f) / 4095.0f;
    }
}

分压电阻计算

工业设备常用 24V 供电,需通过分压电阻将 0-24V 映射到 ADC 输入范围 0-3.3V。

分压电路:

24V --- R1 ---+--- ADC --- GND
              |
             R2
              |
             GND

分压公式:

Vadc = Vin × R2 / (R1 + R2)

选择 R1 = 68kΩ,R2 = 10kΩ:

Vadc = 24V × 10k / (68k + 10k) = 3.16V < 3.3V(安全)

反向计算实际电压:

#define R1 68000.0f
#define R2 10000.0f

float ADC_To_Actual_Voltage(uint16_t adc_value) {
    float v_adc = (adc_value * 3.3f) / 4095.0f;
    return v_adc * (R1 + R2) / R2;
}

分压电阻功耗计算:

最大功耗 = (24V)^2 / (R1 + R2) = 576 / 78000 = 7.4mW
选用 1/8W 或 1/4W 电阻

ADC 校准流程

STM32 ADC 存在偏移误差,需校准以提高精度。HAL 库提供校准函数。

void ADC_Calibrate(void) {
    HAL_ADCEx_Calibration_Start(&hadc1);
}

校准注意事项:

  • 校准前 ADC 必须停止
  • 校准期间禁止中断
  • 每次上电或参考电压变化后需重新校准

完整初始化流程:

void ADC_Init_With_Calibration(void) {
    HAL_ADC_DeInit(&hadc1);
    MX_ADC1_Init();
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, adc_values, ADC_CHANNEL_COUNT);
}

软件滤波算法

ADC 采样存在噪声,需通过软件滤波提高稳定性。

均值滤波

连续采样 N 次取平均值,适合消除随机噪声。

#define FILTER_SAMPLES 10

float ADC_Mean_Filter(uint8_t channel) {
    uint32_t sum = 0;
    
    for (int i = 0; i < FILTER_SAMPLES; i++) {
        sum += adc_values[channel];
        HAL_Delay(1);
    }
    
    return (sum * 3.3f) / (FILTER_SAMPLES * 4095.0f);
}

中值滤波

连续采样 N 次,取中位数,适合消除脉冲干扰。

#define MEDIAN_SAMPLES 5

uint16_t ADC_Median_Filter(uint8_t channel) {
    uint16_t samples[MEDIAN_SAMPLES];
    
    for (int i = 0; i < MEDIAN_SAMPLES; i++) {
        samples[i] = adc_values[channel];
        HAL_Delay(1);
    }
    
    // 冒泡排序
    for (int i = 0; i < MEDIAN_SAMPLES - 1; i++) {
        for (int j = 0; j < MEDIAN_SAMPLES - i - 1; j++) {
            if (samples[j] > samples[j + 1]) {
                uint16_t temp = samples[j];
                samples[j] = samples[j + 1];
                samples[j + 1] = temp;
            }
        }
    }
    
    return samples[MEDIAN_SAMPLES / 2];
}

滑动平均滤波

维护固定长度队列,新数据替换旧数据后计算平均值。

#define SLIDING_WINDOW_SIZE 8

typedef struct {
    uint16_t buffer[SLIDING_WINDOW_SIZE];
    uint8_t index;
    uint32_t sum;
} SlidingAverage_t;

SlidingAverage_t sliding_avg;

void Sliding_Average_Init(void) {
    memset(&sliding_avg, 0, sizeof(sliding_avg));
}

float Sliding_Average_Update(uint16_t new_value) {
    sliding_avg.sum -= sliding_avg.buffer[sliding_avg.index];
    sliding_avg.buffer[sliding_avg.index] = new_value;
    sliding_avg.sum += new_value;
    sliding_avg.index = (sliding_avg.index + 1) % SLIDING_WINDOW_SIZE;
    
    return (sliding_avg.sum * 3.3f) / (SLIDING_WINDOW_SIZE * 4095.0f);
}

实际应用:读取拨码开关地址

通过 ADC 读取拨码开关设置的地址,避免占用多个 GPIO。

拨码开关电路:

3.3V --- 拨码开关(4位) --- 分压网络 --- ADC

不同组合产生不同电压,通过查表确定地址。

#define DIP_ADC_CHANNEL ADC_CHANNEL_4

typedef struct {
    uint16_t adc_min;
    uint16_t adc_max;
    uint8_t address;
} DipSwitch_Map_t;

const DipSwitch_Map_t dip_map[] = {
    {0, 100, 0},
    {300, 500, 1},
    {600, 800, 2},
    {900, 1100, 3},
    {1200, 1400, 4},
    {1500, 1700, 5},
    {1800, 2000, 6},
    {2100, 2300, 7},
    {2400, 2600, 8},
    {2700, 2900, 9},
    {3000, 3200, 10},
    {3300, 3500, 11},
    {3600, 3800, 12},
    {3900, 4100, 13},
    {4200, 4400, 14},
    {4500, 4095, 15}
};

uint8_t Read_Dip_Switch_Address(void) {
    uint16_t adc_value = adc_values[DIP_ADC_CHANNEL];
    
    for (int i = 0; i < sizeof(dip_map) / sizeof(dip_map[0]); i++) {
        if (adc_value >= dip_map[i].adc_min && adc_value <= dip_map[i].adc_max) {
            return dip_map[i].address;
        }
    }
    
    return 0;  // 默认地址
}

完整应用示例

以下代码实现多通道 ADC 采集、滤波和拨码开关读取:

#include "stm32f1xx_hal.h"
#include <string.h>

#define ADC_CHANNEL_COUNT 5
#define FILTER_SAMPLES 10

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint32_t adc_raw_values[ADC_CHANNEL_COUNT];
float adc_filtered_values[ADC_CHANNEL_COUNT];

volatile uint8_t adc_conversion_complete = 0;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_ADC1_Init();
    
    ADC_Calibrate();
    HAL_ADC_Start_DMA(&hadc1, adc_raw_values, ADC_CHANNEL_COUNT);
    
    while (1) {
        if (adc_conversion_complete) {
            adc_conversion_complete = 0;
            
            // 滤波处理
            for (int i = 0; i < ADC_CHANNEL_COUNT - 1; i++) {
                adc_filtered_values[i] = ADC_Mean_Filter(i);
            }
            
            // 读取拨码开关地址
            uint8_t device_addr = Read_Dip_Switch_Address();
            
            // 处理数据
            float voltage_ch0 = ADC_To_Actual_Voltage(adc_filtered_values[0]);
            float voltage_ch1 = ADC_To_Actual_Voltage(adc_filtered_values[1]);
        }
        
        HAL_Delay(100);
    }
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        adc_conversion_complete = 1;
    }
}

void ADC_Calibrate(void) {
    HAL_ADC_Stop_DMA(&hadc1);
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, adc_raw_values, ADC_CHANNEL_COUNT);
}

float ADC_Mean_Filter(uint8_t channel) {
    uint32_t sum = 0;
    
    for (int i = 0; i < FILTER_SAMPLES; i++) {
        sum += adc_raw_values[channel];
        HAL_Delay(1);
    }
    
    return (sum * 3.3f) / (FILTER_SAMPLES * 4095.0f);
}

float ADC_To_Actual_Voltage(float adc_voltage) {
    return adc_voltage * 7.8f;  // 分压比 (68k+10k)/10k = 7.8
}

static void MX_ADC1_Init(void) {
    ADC_ChannelConfTypeDef sConfig = {0};
    
    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = ADC_CHANNEL_COUNT;
    HAL_ADC_Init(&hadc1);
    
    sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES5;
    
    // 通道 0-3:电压采集
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = ADC_REGULAR_RANK_2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_2;
    sConfig.Rank = ADC_REGULAR_RANK_3;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    sConfig.Channel = ADC_CHANNEL_3;
    sConfig.Rank = ADC_REGULAR_RANK_4;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
    // 通道 4:拨码开关
    sConfig.Channel = ADC_CHANNEL_4;
    sConfig.Rank = ADC_REGULAR_RANK_5;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

static void MX_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | 
                          GPIO_PIN_3 | GPIO_PIN_4;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

static void MX_DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        __HAL_RCC_ADC1_CLK_ENABLE();
        
        hdma_adc1.Instance = DMA1_Channel1;
        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_WORD;
        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
        hdma_adc1.Init.Mode = DMA_CIRCULAR;
        hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
        HAL_DMA_Init(&hdma_adc1);
        
        __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc1);
    }
}

常见问题

ADC 读数不稳定

原因:采样时间过短、输入阻抗过高、电源噪声。

解决:增加采样时间、降低输入阻抗、加去耦电容。

sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES5;  // 最长采样时间

多通道数据错位

原因:DMA 配置错误或通道顺序不对。

解决:检查 DMA 数据对齐和通道 Rank 配置。

hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;

校准失败

原因:ADC 未停止或参考电压不稳定。

解决:校准前确保 ADC 停止,等待电源稳定。

HAL_ADC_Stop(&hadc1);
HAL_ADCEx_Calibration_Start(&hadc1);