3. STM32 ADC外设配置:时钟配置、采样时间、扫描模式,一步步教你初始化

好,咱们接着往下走。上一章我们把ADC的硬件原理和引脚搞清楚了,这一章就动真格的了——配置STM32的ADC外设。

说实话,ADC初始化这块,我当年刚入行时也踩过不少坑。尤其是时钟配置,稍不注意,采样出来的数据就是一堆乱码。你想想看,一个制氧机要是氧浓度算错了,那可是要出大事的。

所以这一章,我会手把手带你走一遍初始化流程。咱们以STM32F103系列为例,其他型号大同小异。

3.1 时钟配置——ADC的“心跳”

ADC要工作,首先得有时钟。就像人要有心跳一样。STM32的ADC时钟来自APB2总线,但有个关键点:ADC最大时钟不能超过14MHz

为什么?因为ADC内部是逐次逼近型结构,时钟太快,比较器来不及稳定。我在项目中遇到过,有人直接把APB2的72MHz分给ADC,结果采样值跳得像心电图——嗯,那肯定是不行的。

⚠️ 重要警告: ADC时钟频率超过14MHz,转换结果精度会显著下降。这是芯片手册里明确写的,别挑战它。

正确的做法是:先配置APB2时钟(通常是72MHz),然后通过ADC预分频器分频。分频系数可选2、4、6、8。72MHz分频后,只有选8才能得到9MHz,安全落在14MHz以内。

我个人习惯用6分频,得到12MHz。这样既满足最大频率限制,又能保证采样速度。代码里这样写:

// 使能ADC1时钟,挂在APB2上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

// ADC预分频:6分频 -> 72MHz / 6 = 12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);

你看,就两行。但这两行决定了后面所有数据的质量。

3.2 采样时间——别让信号“饿着”

时钟配好了,接下来是采样时间。这个参数很多人忽略,但它直接影响采样值的准确性。

采样时间,说白了就是ADC内部采样电容充电的时间。如果时间太短,电容没充满,采到的电压就偏低。你想想看,制氧机里氧浓度传感器输出的是微弱信号,本来就小,再被“饿着”采样,那误差能不大吗?

STM32的采样时间可以配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。我一般给传感器信号配55.5或71.5个周期,给电源电压这种稳定信号配13.5就够了。

💡 我的经验: 对于氧浓度传感器(通常是电化学或超声波),建议采样时间不低于55.5个周期。我曾经试过用13.5,结果数据波动大到滤波都救不回来。

配置代码示例:

// 配置ADC1的采样时间
// 通道0(PA0)接氧浓度传感器,用55.5个周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

// 通道1(PA1)接温度补偿,用13.5个周期就够了
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);

这里有个细节:第二个参数是通道号,第三个参数是规则组里的序列位置。我习惯把最重要的传感器放在序列1,这样心里踏实。

3.3 扫描模式——一次搞定多通道

制氧机里,我们通常要采集多个信号:氧浓度、温度、压力、流量……一个个手动触发太傻了。这时候就要用扫描模式。

扫描模式的意思是:ADC自动把规则组里配置的所有通道依次转换一遍。你只需要启动一次,它自己跑完所有通道,然后产生一个DMA请求或中断。

配置扫描模式很简单:

// 开启扫描模式
ADC_ScanModeCmd(ADC1, ENABLE);

// 开启连续转换模式(可选,看需求)
ADC_ContinuousModeCmd(ADC1, ENABLE);

注意:扫描模式必须配合DMA使用,否则你来不及读数据,下一个转换结果就把上一个覆盖了。我刚开始做的时候忘了开DMA,结果读到的全是最后一个通道的值——排查了半天才发现。

🔑 关键配置总结:
  • 扫描模式 + 连续转换 + DMA = 自动采集多通道
  • 扫描模式 + 单次转换 + 查询 = 手动触发多通道
  • 不开启扫描模式 = 只转换一个通道

3.4 完整初始化代码——一步到位

好了,把上面所有东西串起来,就是一套完整的ADC初始化。我直接贴代码,注释写得很清楚:

void ADC_Init_For_Oxygen(void)
{
    // 1. 时钟配置
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  // 12MHz

    // 2. GPIO配置:PA0、PA1为模拟输入
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. ADC基本配置
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;        // 独立模式
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;              // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;        // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;    // 右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 2;                   // 2个通道
    ADC_Init(ADC1, &ADC_InitStructure);

    // 4. 配置采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);

    // 5. 使能ADC并校准
    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    // 6. 启动转换(配合DMA使用)
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

这段代码我用了很多项目,基本没出过问题。唯一要注意的是校准那一步——必须等校准完成再启动转换,否则第一次采到的数据是错的。

3.5 避坑指南——我踩过的雷

最后,分享几个我实际项目中遇到的坑,你遇到了可以直接绕过去:

  • 时钟分频选错: 我曾经用2分频,ADC跑到36MHz,结果数据全是0xFFFF。查了半天才发现是时钟超了。记住:不超过14MHz。
  • 采样时间太短: 有个项目用7.5个周期采温度传感器,结果温度每跳1度,ADC值跳20个LSB。后来改成55.5,稳如老狗。
  • 忘记开DMA: 扫描模式下不开DMA,你只能读到最后一个通道的值。这个坑我至少见过3个新手踩过。
  • 校准没等完成: 校准标志位没检查就直接启动转换,第一次数据偏差很大。加个while循环等一等就好。

嗯,这一章的内容就这些。配置ADC其实不难,关键是每个参数都要理解它为什么这么设。下一章我们会讲DMA传输,把数据从ADC搬到内存里,那才是真正的高效玩法。

你先把初始化代码跑通,有问题随时翻回来看看。