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读数不准,别急着换芯片,先检查参考电压和滤波算法,八成问题出在这两个地方。