4、ADC采样与滤波:模数转换原理、Marlin中的ADC驱动、过采样技术、滑动平均滤波算法

聊到温度控制,第一步就是要把物理世界的温度信号,变成单片机认识的数字信号。这个环节,就是ADC(模数转换器)的活儿。说白了,ADC就是给单片机装上一双“电子眼睛”,让它能看到电压的变化。

4.1 模数转换原理:从模拟到数字的桥梁

ADC的核心任务很简单:测量一个模拟电压,然后输出一个对应的数字值。Marlin常用的AVR单片机(比如Mega2560)内部集成了10位ADC。10位是什么意思?就是它能输出0到1023之间的整数,一共1024个等级。

举个例子,如果参考电压是5V,那么ADC读到的数字值N和实际电压V的关系就是:

V = N × (5V / 1024)

读数为512时,电压就是2.5V。这个分辨率大约是4.88mV。嗯,对于热敏电阻测温来说,这个精度其实够用了。但为什么实际打印时温度还是会跳来跳去?问题出在噪声上。

关键点:ADC的精度取决于参考电压的稳定性和噪声水平。我见过不少机器,就是因为电源纹波太大,导致温度读数像心电图一样上下乱窜。

4.2 Marlin中的ADC驱动:底层是怎么跑的

Marlin的ADC驱动藏在 HAL 层里。不同平台(AVR、ARM、ESP32)的ADC硬件差异很大,但Marlin通过硬件抽象层把它们统一了。

以AVR为例,ADC的初始化代码大概长这样:

void setup_adc() {
    ADMUX = (1 << REFS0);  // 选择AVCC作为参考电压
    ADCSRA = (1 << ADEN)   // 使能ADC
           | (1 << ADPS2)  // 预分频128
           | (1 << ADPS1)
           | (1 << ADPS0);
}

这里有个细节:预分频128。为什么选这么大?因为ADC需要一个稳定的时钟源,太快了反而不准。AVR的ADC时钟推荐在50kHz到200kHz之间,16MHz主频除以128,得到125kHz,刚好落在最佳区间。

启动一次转换的代码更简单:

uint16_t read_adc(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    ADCSRA |= (1 << ADSC);  // 开始转换
    while (ADCSRA & (1 << ADSC));  // 等待完成
    return ADC;
}

你可能会问:为什么不用中断?我个人习惯在温度采样这种周期性任务里用轮询,因为中断太多反而容易打架。Marlin的PID循环是固定频率触发的,在循环里同步读ADC,时序更可控。

4.3 过采样技术:用时间换精度

10位ADC不够用怎么办?换硬件?别急,有个技巧叫“过采样”。

原理其实很简单:如果你对同一个信号采样4次,把结果加起来,就相当于多了2位精度。为什么?因为噪声是随机的,多次采样取平均,能有效抑制噪声,等效于提高了分辨率。

公式是这样的:

要增加n位分辨率,需要采样 4^n 次

比如从10位提升到12位,需要采样16次。Marlin里确实用了这个技巧,但用得比较克制。我记得在某个版本的Marlin中,热床的ADC采样就做了4次过采样,等效到了12位。

我的经验:过采样不是万能的。如果信号本身噪声太小,过采样的效果会打折扣。你想想看,如果每次读到的值都一样,采再多也没用。所以有些主板会故意在ADC引脚上加一点噪声,反而有助于过采样。

4.4 滑动平均滤波算法:让温度曲线平滑下来

好了,ADC读回来了,但数值还在跳。这时候就需要滤波了。Marlin用的是滑动平均滤波,也叫移动平均滤波。

它的思路是维护一个固定长度的缓冲区,每次新数据进来,就计算所有数据的平均值。比如缓冲区长度是8:

// 伪代码示意
#define FILTER_SIZE 8
uint16_t buffer[FILTER_SIZE];
uint8_t index = 0;

uint16_t filter(uint16_t new_value) {
    buffer[index] = new_value;
    index = (index + 1) % FILTER_SIZE;
    
    uint32_t sum = 0;
    for (uint8_t i = 0; i < FILTER_SIZE; i++) {
        sum += buffer[i];
    }
    return sum / FILTER_SIZE;
}

这个算法好处很明显:实现简单,计算量小,对周期性噪声抑制效果好。但缺点也有——它会有延迟。缓冲区越长,延迟越大。

缓冲区长度 平滑效果 延迟(采样周期) 适用场景
4 一般 2个周期 快速响应的喷嘴
8 较好 4个周期 热床
16 很好 8个周期 环境温度监测

注意:我曾经遇到过一个问题:缓冲区长度设得太长,导致PID控制器看到的温度变化总是慢半拍。结果就是温度过冲严重,打印件表面都起泡了。后来我把缓冲区从16改到8,问题就解决了。滤波不是越平滑越好,得看实际需求。

Marlin里还有一个优化:它没有用除法,而是用移位操作。因为缓冲区长度通常是2的幂次,比如8、16、32。这样平均值计算就可以用右移代替除法,快很多。

// Marlin实际代码风格
return sum >> 3;  // 除以8

这种细节,就是嵌入式工程师该抠的地方。你想想看,PID循环每200ms跑一次,每次都要算滤波,如果这里用除法,累积下来也是不小的开销。

知识体系总览

下面这张图,把ADC采样到滤波的完整链路串起来了:

ADC采样与滤波知识链路 热敏电阻 模拟电压信号 ADC转换 10位数字值 (0-1023) 过采样 4^n次采样累加 滑动平均滤波 缓冲区长度N 平滑温度值 关键参数 • 参考电压:5V / 3.3V • 预分频:128 (AVR) • 过采样次数:4^n • 滤波窗口:8/16/32

从热敏电阻的模拟电压,到ADC转换出数字值,再到过采样提升分辨率,最后用滑动平均滤掉噪声——每一步都有它的道理。Marlin把这套流程跑得很成熟,但理解背后的原理,才能在你自己的项目里灵活调整。

嗯,ADC这部分就聊到这儿。记住一句话:采样是基础,滤波是手段,稳定才是目的。