4、ADC采样与量化:采样定理、分辨率与精度、STM32内部ADC配置、DMA传输

各位同学,大家好。今天我们聊一个非常实在的话题——ADC采样与量化。

做消防报警系统,传感器信号进来,第一关就是ADC。不管是烟雾浓度、温度变化,还是一氧化碳浓度,这些模拟量都得先变成数字量,才能让单片机处理。这一步要是没做好,后面算法再牛也白搭。

我个人习惯把ADC比作「翻译官」。它把自然界连续的模拟信号,翻译成单片机认识的0和1。翻译得好不好,就看采样定理、分辨率、精度这几个关键点。

4.1 采样定理:别让信号「说谎」

先问大家一个问题:一个1kHz的正弦波,你用1kHz的采样率去采,能还原出来吗?

答案是——不能。采出来你会看到一个直流电平,或者一个频率完全不对的信号。这就是混叠现象。

采样定理(奈奎斯特定理)说得很清楚:采样频率必须大于信号最高频率的两倍。也就是:

fs > 2 * fmax

举个例子。消防报警中,烟雾传感器的输出信号变化很慢,通常几十赫兹就够用了。但有些场合,比如火焰探测器,需要检测紫外或红外脉冲,频率可能到几百赫兹。这时候采样率就得跟上。

我在项目中遇到过一个坑:某次做吸气式感烟探测器,采样率设得不够高,结果高频噪声混叠进了低频段,导致误报警频发。查了两天才找到原因——不是传感器问题,是采样率不够。

避坑指南: 我曾经吃过这个亏——采样率不是越高越好,但绝对不能低于2倍最高频率。实际工程中,我一般留3~5倍的余量。比如信号最高100Hz,采样率我设500Hz甚至1kHz。

4.2 分辨率与精度:别把概念搞混了

很多初学者容易把分辨率和精度混为一谈。我刚开始做嵌入式时也犯过这个错。

分辨率,说白了就是ADC能分辨的最小电压变化。12位ADC,参考电压3.3V,分辨率就是:

3.3V / 4096 ≈ 0.8mV

也就是说,每变化0.8mV,数字量就跳一个字。

精度呢?它反映的是实际测量值和真实值之间的偏差。分辨率高不代表精度高。你想想看,一个12位ADC,如果内部参考电压漂了,或者有噪声干扰,那测出来的数据可能差好几个字。

参数 含义 影响因素
分辨率 能分辨的最小电压变化 位数、参考电压
精度 测量值与真实值的偏差 参考电压稳定性、噪声、温度漂移

我建议:在消防报警系统中,12位ADC基本够用。但如果你做的是高精度气体检测,比如ppm级别的CO传感器,那可能需要16位甚至更高。不过要注意,位数越高,转换时间越长,这是个取舍。

4.3 STM32内部ADC配置:实战经验分享

STM32的ADC模块功能很丰富。我以F103系列为例,说说常用的配置流程。

嗯,这里要注意:STM32的ADC可以工作在多种模式下——单次转换、连续转换、扫描模式、注入模式。消防报警中,我一般用连续转换+扫描模式,这样能循环采集多个通道。

下面是一个典型的初始化代码:

void ADC_Init(void)
{
    ADC_InitTypeDef ADC_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);

    // 配置PA0为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // ADC配置
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;  // 单通道
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    // 配置采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    // 使能ADC
    ADC_Cmd(ADC1, ENABLE);

    // 校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

这段代码里,我特别想强调两点:

  • 采样时间:55.5个周期,对于消防传感器来说足够了。如果信号源内阻大,可以适当加长采样时间。
  • 校准:每次上电后一定要做校准。我见过有人偷懒没做校准,结果采集的数据偏差很大。

4.4 DMA传输:解放CPU的关键

做消防报警系统,CPU不能一直忙着搬数据。它还要跑算法、处理通信、响应按键。所以,ADC数据用DMA传输几乎是标配。

DMA(直接存储器访问)说白了就是:ADC转换完数据,自动存到内存里,不用CPU插手。CPU该干嘛干嘛,等数据存够了再去处理。

配置DMA的步骤也不复杂:

void DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_DeInit(DMA1_Channel1);

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Value;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = 100;  // 缓存100个数据
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);
}

这里有个关键点:循环模式。我习惯用循环模式,这样DMA会一直往缓冲区写数据,写满了从头开始覆盖。配合双缓冲区或者环形队列,数据不会丢。

个人经验: 我在做多传感器融合时,一般开一个512字节的环形缓冲区。DMA填数据,主循环取数据做滤波和融合。这样CPU几乎零负担,处理100Hz的采样绰绰有余。

4.5 实际应用中的注意事项

最后,分享几个我在消防报警项目中踩过的坑:

  • 参考电压要稳:STM32内部参考电压精度一般,如果要求高,建议用外部基准源。我吃过一次亏,电池供电时电压波动,ADC数据跟着飘。
  • PCB布局要讲究:模拟信号线尽量短,远离数字信号。特别是PWM和开关电源,干扰很大。
  • 软件滤波不能省:ADC原始数据直接拿来用?不行的。我一般先做中值滤波或滑动平均,再去判断报警阈值。

好了,关于ADC采样与量化,今天就聊这么多。下一章我们讲传感器信号调理——怎么把微弱的传感器信号放大、滤波,变成ADC能采的干净信号。