4、ADC采样与量化:采样定理、分辨率与精度、STM32内部ADC配置、DMA传输
各位同学,大家好。今天我们聊一个非常实在的话题——ADC采样与量化。
做消防报警系统,传感器信号进来,第一关就是ADC。不管是烟雾浓度、温度变化,还是一氧化碳浓度,这些模拟量都得先变成数字量,才能让单片机处理。这一步要是没做好,后面算法再牛也白搭。
我个人习惯把ADC比作「翻译官」。它把自然界连续的模拟信号,翻译成单片机认识的0和1。翻译得好不好,就看采样定理、分辨率、精度这几个关键点。
4.1 采样定理:别让信号「说谎」
先问大家一个问题:一个1kHz的正弦波,你用1kHz的采样率去采,能还原出来吗?
答案是——不能。采出来你会看到一个直流电平,或者一个频率完全不对的信号。这就是混叠现象。
采样定理(奈奎斯特定理)说得很清楚:采样频率必须大于信号最高频率的两倍。也就是:
fs > 2 * fmax
举个例子。消防报警中,烟雾传感器的输出信号变化很慢,通常几十赫兹就够用了。但有些场合,比如火焰探测器,需要检测紫外或红外脉冲,频率可能到几百赫兹。这时候采样率就得跟上。
我在项目中遇到过一个坑:某次做吸气式感烟探测器,采样率设得不够高,结果高频噪声混叠进了低频段,导致误报警频发。查了两天才找到原因——不是传感器问题,是采样率不够。
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会一直往缓冲区写数据,写满了从头开始覆盖。配合双缓冲区或者环形队列,数据不会丢。
4.5 实际应用中的注意事项
最后,分享几个我在消防报警项目中踩过的坑:
- 参考电压要稳:STM32内部参考电压精度一般,如果要求高,建议用外部基准源。我吃过一次亏,电池供电时电压波动,ADC数据跟着飘。
- PCB布局要讲究:模拟信号线尽量短,远离数字信号。特别是PWM和开关电源,干扰很大。
- 软件滤波不能省:ADC原始数据直接拿来用?不行的。我一般先做中值滤波或滑动平均,再去判断报警阈值。
好了,关于ADC采样与量化,今天就聊这么多。下一章我们讲传感器信号调理——怎么把微弱的传感器信号放大、滤波,变成ADC能采的干净信号。