4. ADC采样与校准:从原理到实战

ADC,模数转换器,说白了就是把模拟电压变成数字。你想想看,传感器输出的都是连续变化的电压,而单片机只能认0和1。没有ADC,物联网设备就是个睁眼瞎。我做了这么多年嵌入式,ADC这块踩过的坑比走过的路还多,今天咱们就把这事聊透。

4.1 ADC工作原理:量化与编码

ADC的核心思想很简单——比较。它内部有个参考电压,比如3.3V。输入电压和参考电压一比,就能算出对应的数字值。常见的逐次逼近型(SAR)ADC,就像用天平称东西,先放个大砝码,大了就换小的,直到平衡。

举个例子,一个12位ADC,参考电压3.3V。输入1.65V,那数字值就是:

数字值 = (输入电压 / 参考电压) × 2^分辨率
       = (1.65 / 3.3) × 4096
       = 2048

嗯,这里要注意,这个计算是理想情况。实际芯片里还有偏移误差、增益误差、非线性误差。我在一个工业项目里遇到过,ADC读出来总是偏大0.5%,查了半天才发现是参考电压芯片的温漂问题。

核心公式: 数字值 = (Vin / Vref) × 2N,其中N为ADC分辨率位数。

4.2 采样率与分辨率:鱼和熊掌

采样率决定你能采多快的信号,分辨率决定你能看多细的电压。这两者往往是矛盾的。

分辨率 量化台阶(3.3V参考) 典型采样率 适用场景
8位 12.9 mV 1 MSPS+ 音频、快速信号
10位 3.22 mV 200 kSPS 一般传感器
12位 0.806 mV 100 kSPS 精密测量
16位 50.3 μV 10 kSPS 高精度仪表

我建议你根据信号频率来选。采样率至少要是信号最高频率的2倍,这是奈奎斯特定理。但实际工程中,我一般留5-10倍余量。比如一个温度传感器,变化很慢,1kHz采样率就绰绰有余了。

避坑指南: 我曾经在一个项目中用了16位ADC,但采样率只有100Hz,结果50Hz工频干扰混叠进了信号,怎么滤波都滤不干净。后来加了抗混叠滤波器才解决。记住,采样率不够,分辨率再高也是白搭。

4.3 参考电压选择:精度之源

参考电压是ADC的尺子。这把尺子不准,量出来的数据全是错的。我见过太多人忽略这一点,结果产品量产时一致性差得一塌糊涂。

参考电压有三种常见方案:

  • 内部参考: 芯片自带的,方便但精度一般,温漂大。适合对精度要求不高的场景。
  • 电源VDD: 直接用供电电压,成本最低。但电源有纹波,精度最差。
  • 外部精密参考: 比如TL431、REF3033,精度高、温漂小。我推荐在工业产品中使用。

我个人习惯,只要产品不是玩具级别的,一律用外部参考。多花几毛钱,省去后面一堆校准的麻烦。记得在参考电压输出端加一个0.1μF和10μF的退耦电容,位置要尽量靠近ADC的参考引脚。

小技巧: 如果板子上有多个ADC通道,尽量共用一个参考电压。不同参考源之间的差异,会引入额外的通道间误差。

4.4 软件滤波算法:让数据更干净

硬件搞定了,软件也不能含糊。ADC原始数据里往往混着噪声,不滤波直接使用,系统会抽风。我常用的两种滤波算法,今天分享给你。

4.4.1 中值滤波:干掉毛刺

中值滤波对付脉冲噪声特别有效。比如传感器偶尔被干扰跳一下,中值滤波能直接把它扔掉。原理很简单:连续采N个值,排序后取中间那个。

// 中值滤波,窗口大小5
uint16_t median_filter(uint16_t *buf, uint8_t len) {
    uint16_t temp[5];
    uint8_t i, j;
    
    // 复制数据
    for(i = 0; i < len; i++) {
        temp[i] = buf[i];
    }
    
    // 冒泡排序(数据量小,够用)
    for(i = 0; i < len - 1; i++) {
        for(j = 0; j < len - 1 - i; j++) {
            if(temp[j] > temp[j + 1]) {
                uint16_t t = temp[j];
                temp[j] = temp[j + 1];
                temp[j + 1] = t;
            }
        }
    }
    
    return temp[len / 2];  // 取中间值
}

窗口大小我一般取3、5或7。越大滤得越干净,但响应也越慢。温度传感器我取5,压力传感器取3,这个要根据实际信号调。

4.4.2 滑动平均:平滑曲线

滑动平均适合滤除高频噪声,让数据曲线变得平滑。它维护一个固定长度的队列,每次新数据进来,去掉最老的,加上最新的,然后求平均。

// 滑动平均滤波
#define WINDOW_SIZE 8

uint16_t moving_average(uint16_t new_value) {
    static uint16_t buffer[WINDOW_SIZE] = {0};
    static uint8_t index = 0;
    static uint8_t count = 0;
    uint32_t sum = 0;
    uint8_t i;
    
    buffer[index] = new_value;
    index = (index + 1) % WINDOW_SIZE;
    if(count < WINDOW_SIZE) count++;
    
    for(i = 0; i < count; i++) {
        sum += buffer[i];
    }
    
    return (uint16_t)(sum / count);
}

窗口大小选2的幂次,比如4、8、16,这样除法可以用移位操作,速度快很多。我在一个电池管理项目中,用16点滑动平均,把ADC的噪声从±5mV降到了±1mV以内。

实战建议: 中值滤波和滑动平均可以组合使用。先中值去毛刺,再滑动平均平滑。我常用的组合是:5点中值 + 8点滑动平均,效果相当不错。

4.5 ADC校准:消除系统误差

再好的ADC也有误差。校准就是把这些固定误差找出来,在软件里补偿掉。校准分两步:偏移校准和增益校准。

偏移校准: 把ADC输入短接到GND,读到的值就是偏移量。正常应该是0,实际可能是几到几十个LSB。把这个值记下来,以后每次读数都减去它。

增益校准: 给ADC输入一个已知的精密电压(比如2.5V),读到的值和理论值一比,算出增益系数。以后每次读数都乘以这个系数。

// ADC校准示例
typedef struct {
    int16_t offset;     // 偏移量
    float gain;         // 增益系数
} adc_cal_t;

adc_cal_t adc_cal;

void adc_calibrate(void) {
    uint32_t sum = 0;
    
    // 偏移校准:输入短接GND
    for(int i = 0; i < 100; i++) {
        sum += adc_read();
    }
    adc_cal.offset = sum / 100;
    
    // 增益校准:输入精密2.5V
    sum = 0;
    for(int i = 0; i < 100; i++) {
        sum += adc_read();
    }
    uint16_t raw = sum / 100;
    uint16_t ideal = (uint16_t)(2.5 / 3.3 * 4096);  // 12位ADC
    adc_cal.gain = (float)ideal / (float)(raw - adc_cal.offset);
}

uint16_t adc_get_calibrated(void) {
    uint16_t raw = adc_read();
    int32_t corrected = (int32_t)(raw - adc_cal.offset) * adc_cal.gain;
    if(corrected < 0) corrected = 0;
    if(corrected > 4095) corrected = 4095;
    return (uint16_t)corrected;
}

注意: 校准要在产品工作温度下进行。我曾经在25℃校准好,结果产品在-10℃环境下偏移量变了3倍。后来加了温度补偿才搞定。

4.6 知识体系总览

下面这张图把ADC采样与校准的核心知识点串起来了,你可以对照着梳理自己的项目。

ADC采样与校准知识体系 ADC核心原理 采样率与分辨率 参考电压选择 软件滤波算法 校准与补偿 奈奎斯特定理 抗混叠滤波 内部参考 / 外部参考 退耦电容布局 中值滤波(去毛刺) 滑动平均(平滑) 偏移校准 / 增益校准 温度补偿 硬件选型 + 软件滤波 + 校准补偿 = 稳定可靠的ADC数据

ADC这块,说难不难,说简单也不简单。关键是把原理搞懂,选型时留足余量,软件上做好滤波和校准。做到这几点,你的传感器数据基本就稳了。下次遇到ADC读数不准,别急着换芯片,先检查参考电压和滤波算法,八成问题出在这两个地方。