STM32的模拟数字转换器

ADC(Analog to Digital Converter)。即模拟/数字转换器,在模拟信号需要以数字形式处理、存储或传输时,就需要模/数转换器将模拟量转换为数字量。

STM32的ADC外设

STM32 内置了 ADC 外设,不需要辅助 IC 就可以直接将模拟信号转换为数字信号,以便内核处理数字量。STM32 的 ADC 外设非常强大,STM32F103x8 等中容量产品具有 2 个 ADC 外设,STM32F103xC 等大容量产品具有 3 个 ADC 。每个 ADC 都可以采集一个或多个外部输入的模拟量,各个 ADC 还可以通过采集同一个通道而提高对输入信号的采样速率。

ADC 的类型决定了其精度和性能的极限,STM32 的 ADC 是 12 位逐次逼近型模拟数字转换器,因此它的转换结果是一个介于 0~4095 的整数值,最小量化单位 LSB 为输入电压范围的 1/212

STM32的ADC结构

在使用标准外设库操作 STM32 的 ADC 时,第一步就是配置外设的一些通用参数(也就是初始化该外设)。和其它外设的初始化方式一样,ADC 也通过结构体的形式提供初始化参数,它的初始化结构体具有以下成员:

typedef struct {
    uint32_t ADC_Mode;
    FunctionalState ADC_ScanConvMode;
    FunctionalState ADC_ContinuousConvMode;
    uint32_t ADC_ExternalTrigConv;
    uint32_t ADC_DataAlign;
    uint8_t ADC_NbrOfChannel;
} ADC_InitTypeDef;

为了明白每个成员的作用以及可以设置的值,同样需要结合 STM32 的 ADC 结构框图来理解。以下展示了 ADC 的结构框图的主要部分:

未展示的部分是 ADC 的触发源结构,这部分结构将在下文介绍

这个结构可以拆分为以下几个部分理解:

参考电压范围

STM32F103 芯片都有两个引脚 VREF+ 和 VREF- ,表示 STM32 的模拟参考电压,只有这个范围内的电压才能被正确读取。VDDA 和 VVSSA 在介绍 STM32 最小系统时介绍过,它们是为 ADC 外设供电的模拟电源和模拟地引脚。对于 F103C8T6 这种小型设备,VREF+ 和 VDDA 一般直接连接、VREF- 和 VSSA 一般也直接连接,因此 STM32 ADC 的电压输入范围为 0~3.3V 。

如果需要使输入的电压范围变宽,从而可以测量负电压或者更高的正电压,可以在外部加一个电压转换电路,把需要转换的电压抬升或者降压到 0~3.3V 。

通道

当确定了参考电压以后,ADC 就可以读取模拟信号了。但下一个问题是,ADC 从哪里读取这些模拟信号。通常,将可供 ADC 读取的模拟信号称为通道(channel),这些通道既可以是由 GPIO 读取的外部电压,也可以是内部一些元件产生的电压信号。

STM32F1 的每个 ADC 最多有 18 个输入通道,可测量 16 个外部和 2 个内部信号源。STM32F103x8 只有 ADC1 和 ADC2 两个 ADC 外设,以下是它每个通道对应的输入:

STM32F103x8 的 ADC 输入通道
ADC1 通道对应 IOADC2 通道对应 IO
通道 0PA0通道 0PA0
通道 1PA1通道 1PA1
通道 2PA2通道 2PA2
通道 3PA3通道 3PA3
通道 4PA4通道 4PA4
通道 5PA5通道 5PA5
通道 6PA6通道 6PA6
通道 7PA7通道 7PA7
通道 8PB0通道 8PB0
通道 9PB1通道 9PB1
通道 10PC0通道 10PC0
通道 11PC1通道 11PC1
通道 12PC2通道 12PC2
通道 13PC3通道 13PC3
通道 14PC4通道 14PC4
通道 15PC5通道 15PC5
通道 16连接内部温度传感器通道 16连接内部 VSS
通道 17连接内部 VREFINT通道 17连接内部 VSS

VREFINT 是 STM32 内部的参考电压(Reference Internal),值恒定为 1.2V ,不随外部供电电压而变化。如果不确定模拟输入的参考正电压是否为 3.3V ,就可以读取该内部参考电压并用于校准。

如果使用的是 STM32F103C8 的芯片,由于 PC0 及后续的引脚都没有引出,所以外部真正能用到的只有 10 个通道。

看起来 ADC1 和 ADC2 的通道基本相同,那么 STM32 为何要提供两个 ADC 外设呢?这是由于某些时候一个 ADC 的采样效率不能满足需求,这时候就可以让两个 ADC 一起对同一个通道采用,从而提升采样效率。

ADC 初始化结构体的成员 .ADC_Mode 配置的 ADC 模式就和 ADC 的使用数量有关,当使用一个 ADC 时可以配置为独立模式 ADC_Mode_Independent ,使用两个 ADC 时可以配置为双模式,在双模式下还有很多细分模式可选。如果不是特别关注采样效率,一般使用一个 ADC 的独立模式即可。

转换组

STM32 的 ADC 还有一个特点:它允许用户配置一组要转换的 ADC 通道,每轮转换自动按照顺序转换组里的每个通道,这样在读取的模拟量不止一个时,就无需手动逐个切换要转换的通道了。这就是 ADC 的扫描模式(Scan mode),如果需要使用扫描模式,需要在初始化结构体中将 .ADC_ScanConvMode 设置为 ENABLE

STM32 的转换组可以分为规则组(Regular Group)和注入组(Injected Group),规则组是最常用的 ADC 转换组,用于执行常规的 ADC 转换。注入组是相对规则组而言的,它可以在规则组转换的时候强行插入转换:如果在规则组转换过程中,有注入组插队,那么就要先转换完注入组,再回到规则组的转换流程。这点跟中断程序很像,注入组通常用于快速响应某些事件的转换需求。

STM32F1 的规则组最多拥有 16 个待转换通道,注入组最多拥有 4 个待转换通道。在 ADC 的初始化结构体中,需要通过 .ADC_NbrOfChannel 成员指明规则组的通道数量。注入组的通道数配置不在初始化结构体中,除此之外转换组的配置也不在初始化结构体中,它们都是单独调用其它函数,这部分内容将在接下来应用部分说明。

转换组的底层原理是,ADC 中存在若干规则序列寄存器 SQRx 和注入序列寄存器 JSQR ,这些寄存器在初始化时将依次填入序列的长度和序列每个位置的通道号,转换时硬件自动扫描这些寄存器中的每个通道号并依次转换。

转换组的通道是可以重复的,如果想提高转换组中某个通道的采样率,可以将该通道在组内适当重复几个,在读入转换数据时注意对应上即可。

转换组还允许使用者选择单次转换或连续转换(Continuous conversion)模式,如果设置为连续转换模式,则 ADC 完成上一个通道的转换后会马上自动地启动下一个通道的转换,不需要再次触发;而单次转换模式每次转换都需要触发才能开始。是否需要连续转换可由 ADC 初始化结构体的 .ADC_ContinuousConvMode 成员配置。

扫描还是非扫描、单次转换还是连续转换,这两个组合起来一共有四种模式,下图对比了这四种模式的差别:

ADC序列相关的四种工作方式

触发源

为了开始 ADC 的转换,需要提供一个触发源。比较常规的使 ADC 开始转换的方式是软件开启,即通过软件向控制寄存器的某个位写入特定值,那么 ADC 就开始转换了。

除此之外,STM32 的 ADC 也提供了其它触发转换的触发源,包括外部引脚上的信号(例如定时器输出、外部中断等)和内部事件(例如定时器溢出、比较器输出等)。通过硬件的触发可以不经过内核处理,从而减轻内核压力并提高响应速度。

数据存储

ADC 转换完成后,得到的结果将会存放在寄存器中供读取。不同转换组数据存放的位置也不同,规则组的数据存放在规则数据寄存器,注入组的数据存放在注入数据寄存器。

不过规则组只有一个规则数据寄存器 DR ,这就意味着如果规则序列包含两个或以上的通道的话,转换过程中后面通道的转换结果都会覆盖前面通道的转换结果,因此需要及时从寄存器中读出数据。而注入组的每个通道都拥有一个注入数据寄存器 JDRx ,就没有这样的问题了。

ADC 的数据寄存器都是 32 位的寄存器,其中规则数据寄存器的低 16 位保存 ADC1 转换的数据,高 16 位在双模式下保存 ADC2 转换的数据;而注入组的四个注入数据寄存器都只用到低 16 位保存注入转换的数据。

既然 ADC 的数据寄存器使用 16 位保存转换的数据,而 ADC 的精度只有 12 位,无法放满这 16 位空间,这时就涉及到数据对齐的概念了:ADC 的转换结果可以右对齐(即高位对齐),也可以左对齐(即低位对齐),但左对齐与通常整数的表示方式不符,使用时需要再加以移位处理;左对齐一般适用于需要将数据传输给其它也使用左对齐处理的元器件的情况。

除此之外,规则数据寄存器和注入数据寄存器对其它四个空位的处理方式也不同:规则数据寄存器直接将空位填 0 ,而注入数据寄存器会在高位添加额外的符号位,这样使得注入组可以测量比负参考电压还低的负值电压数据,如下所示:

ADC 初始化结构体的成员 .ADC_DataAlign 控制结果数据的对齐方式。一般来说只需要使用右对齐 ADC_DataAlign_Right 即可。

转换效率

在使用 ADC 时,转换速度也是一个值得考虑的指标。一些高频的模拟输入源要求 ADC 结构的转换速度足够短,才能准确地反映输入的信号。

STM32F1 的转换时间是可以通过编程调整的,但转换一次至少需要 14 个 ADC 时钟周期,而 ADC 外设的时钟频率最高不能超过 14 MHz ,因此 STM32F1 的 ADC 转换时间最短为 1μs ,足以胜任中低频示波器的采样工作。

不过这里要注意的是,如果 STM32 的系统频率为 72MHz 的话,不管如何分频都无法达到 14 MHz 的最大频率,当 ADC 时钟为 PCLK2 的 6 分频时,ADC 的最大频率只能达到 12MHz ,因此最低转换时间为 1.17μs ,可以参见时钟树:

只有在 STM32 使用的系统频率为 56MHz 时,从 PCLK2 再取 4 分频才能达到 14 MHz 的最大频率。


制约 ADC 转换效率的另一个因素是 ADC 的采样时间。STM32 模拟量的读取由采样和转换两部分组成,ADC 在进行转换之前需要对模拟信号进行采样和保持,以确保在转换过程中模拟信号的稳定性。采样时间是可以通过编程调节的,采样时间越长,输入信号的测量稳定性也就越高,但相应地转换速度也就越慢。STM32 提供的转换时间可以参见 ADC_SampleTime_* 一系列宏,从最短的 1.5 个时钟周期到 239.5 周期不等。因此总的转换时间 TCONV = 采样时间 + 12.5 个 ADC 时钟周期(固定时间),最快 14 个周期。

ADC转换程序

单通道ADC转换

接下来的程序说明了实现单通道 ADC 转换所需要编写的最基本的代码,编写的思路和步骤为:

  1. 开始外设时钟

在外设初始化前,需要启用 ADC 外设和 GPIO 端口的时钟。STM32F103 系列的 ADC 和 GPIO 外设都挂载在 APB2 总线上:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);

除此之外,如果还需要配置 ADC 的时钟分频,可以调用以下函数完成:

RCC_ADCCLKConfig(RCC_PCLK2_Div6);
  1. 初始化 GPIO 引脚

在通过 GPIO 读取模拟输入时,需要将输入引脚配置为模拟输入模式 GPIO_Mode_AIN

// 2. Initialize GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
  1. 初始化 ADC 外设

ADC 外设的初始化和其它外设是一样的。在该程序中,需要提供初始化结构体如下的值:

ADC_InitTypeDef ADC_InitStructure = {
    .ADC_Mode = ADC_Mode_Independent,
    .ADC_NbrOfChannel = 1,
    .ADC_ScanConvMode = DISABLE,
    .ADC_ContinuousConvMode = DISABLE,
    .ADC_ExternalTrigConv = ADC_ExternalTrigConv_None,
    .ADC_DataAlign = ADC_DataAlign_Right
};
ADC_Init(ADC1, &ADC_InitStructure);

该初始化结构体做了如下配置:

  • 只使用一个 ADC ,属于独立模式
  • 转换通道 1 个
  • 单通道情况下,不需要扫描模式
  • 单次转换模式,需要数据时再启动转换即可
  • 不用外部触发转换,由软件开启即可
  • 转换结果右对齐
  1. 配置 ADC 各通道

初始化了 ADC 一些通用的参数后,接下来可以配置 ADC 的转换组的细节,主要包括转换组各个序列位置对应的通道,以及每个通道的采样时间。这一过程主要由以下函数完成:

  • void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);

其中 Rank 表示转换序列的位号,ADC_SampleTime 是通道的采样时间。由于本次只需要转换 1 个通道的数据,因此只需要配置位号为 1 的通道即可:

ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1,
                         ADC_SampleTime_55Cycles5);

类似地,如果要配置注入组,可以调用 ADC_InjectedChannelConfig() 函数,它们的用法类似。

  1. 启用 ADC

这一步没有什么好说的,和其它外设类似,启用 ADC 调用的是 ADC_Cmd() 函数:

ADC_Cmd(ADC1, ENABLE);
  1. 自校准

在使用 STM32 的 ADC 前,还有一个可选的步骤是校准 ADC ,校准可以大幅减少因内部电容器组的变化而产生的精度误差。STM32 的 ADC 具有内置的自校准功能,在校准期间,每个电容器上都会计算出一个误差修正码(数字值),可以用于消除在随后的转换中电容器产生的误差。

ADC 的校准可以通过以下代码完成,建议在每次上电时执行一次校准即可:

// 初始化 ADC 校准寄存器  
ADC_ResetCalibration(ADC1);
// 等待校准寄存器初始化完成
while (ADC_GetResetCalibrationStatus(ADC1));
// 开始校准 ADC
ADC_StartCalibration(ADC1);
// 等待校准完成
while (ADC_GetCalibrationStatus(ADC1));
  1. 触发转换并读取转换数据

经过以上步骤,ADC 已经配置完毕,随时可以读取外部模拟输入。由于以上配置 ADC 为软件触发中断,因此需要使用以下函数开始 ADC 的转换:

ADC_SoftwareStartConvCmd(ADC1, ENABLE);

如果以上配置 ADC 为连续转换模式,那么该函数只需要在配置时调用一次即可;如果配置 ADC 为单次转换模式,那么每次转换前都需要调用该函数。

ADC 在转换结束时,状态寄存器的转换结束(End of Conversion, EOT)标志位会被挂起,可以通过读取该标志位判断是否转换完毕:

while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) != SET);

转换结束后,就可以从数据寄存器中读取转换结果。在标准外设库中,可以通过以下函数实现这一点:

uint16_t ADC_Value = ADC_GetConversionValue(ADC1);

ADC_GetConversionValue() 函数中会读取数据寄存器,读取数据寄存器同时会自动清除 EOC 标志位,无需再手动清除标志位了。

static inline float ToLocalVoltage(uint16_t RawValue) {
    return ((float)(RawValue * 3.3 / 4095));
}

以上触发转换并读取转换数据的程序可以合并为一个函数,在需要采集外部输入时只需要通过调用该函数就可以获取结果。

ADC转换中断

以上使用同步方式等待读取的实现有一个问题,那就是 ADC 的转换时间并不算短,程序会在此处暂停一段时间,特别是当采用连续转换模式时,程序会因为暂停而发生堵塞。此时可以采用中断的形式等待读取数据。

从 ADC 的结构图中可以看出,当数据转换结束、保存到数据寄存器后,可以产生三种类型的中断:规则通道转换结束中断、注入转换通道转换结束中断、模拟看门狗中断。其中转换结束中断比较简单:在转换结束时发出中断信号,可以在中断服务程序中读取转换的数据。

为了使用 ADC 转换中断功能,需要在 NVIC 中启用中断并配置中断优先级。注意,ADC 的所有类型中断、以及 ADC1 和 ADC2 的中断都共用一个中断源 ADC1_2_IRQn

NVIC_InitTypeDef NVIC_InitStructure = {
    .NVIC_IRQChannel = ADC1_2_IRQn,
    .NVIC_IRQChannelPreemptionPriority = 1,
    .NVIC_IRQChannelSubPriority = 1,
    .NVIC_IRQChannelCmd = ENABLE,
};
NVIC_Init(&NVIC_InitStructure);

和 STM32 的其它中断一样,ADC 的每种类型的中断都有各自的中断使能位和中断标志位,因此启用 ADC 转换中断功能还需要使能对应的标志位:

ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

在中断服务程序中,可以根据中断类型写相应配套的数据处理代码(为了在中断服务函数和其它函数之间共享转换结果,这里 ADC_Value 被实现为全局变量):

uint16_t ADC_Value;

void ADC1_2_IRQHandler(void) {
    if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) {
        ADC_Value = ADC_GetConversionValue(ADC1);
        ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
    }
}

一旦编写完成这些程序,在主函数中调用 ADC_SoftwareStartConvCmd() 后,在转换结束前程序就可以执行其它任务了,中断服务程序会自动处理数据。

多通道ADC采集

STM32 的 ADC 支持采集多个通道的模拟量,但是由于规则数据寄存器只有一个,仅将以上代码改为多通道会带来数据丢失的问题:不管是同步读取还是中断读取,EOC 只会发生在所有通道的数据全部采集结束之后,而此时规则数据寄存器只保留着最后一个通道的数据。

有一种迂回的方法解决这个问题:只使用单通道模式,但在每次转换前,修改序号为 1 的通道号为后一个准备读取的通道号。这样虽然一次只采集一个通道的数据,但是由于每次采集时通道都在改变,就间接地实现了多通道的数据采集。

如果要采集的通道数据不超过 4 个,另一种解决方法是作为注入通道采集。注入序列具有 4 个数据寄存器,不会发生通道间数据互相覆盖的问题。

在下一节中将介绍 STM32 的 DMA ,它提供了规则通道的多通道采集方案。

接下来的程序将采集以下 4 个通道的模拟量:

  • PC0:外部模拟输入量
  • PC1:连接到 VCC 上,采集电源电压值
  • 内部温度传感器
  • 内部 VREFINT

在读取内部温度传感器和 VREFINT 的值前,需要先使能这两个通道,使能的方式为:

ADC_TempSensorVrefintCmd(ENABLE);

由于不使用规则通道,因此在初始化 ADC 时,不需要配置规则通道数,但仍然需要开启扫描模式:

ADC_InitTypeDef ADC_InitStructure = {
    .ADC_Mode = ADC_Mode_Independent,
    .ADC_ScanConvMode = ENABLE,
    .ADC_ContinuousConvMode = ENABLE,
    .ADC_ExternalTrigConv = ADC_ExternalTrigConv_None,
    .ADC_DataAlign = ADC_DataAlign_Right
};
ADC_Init(ADC1, &ADC_InitStructure);

注入通道的配置则需要通过一些单独的函数来完成,包括注入序列的长度、是否需要外部触发、是否夹杂在规则序列中自动转换:

ADC_InjectedSequencerLengthConfig(ADC1, 4);
ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_None);
ADC_AutoInjectedConvCmd(ADC1, ENABLE);

每个注入通道的安排通过 ADC_InjectedChannelConfig() 函数实现,这点和规则序列倒是非常相似的:

ADC_InjectedChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_55Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_10, 2, ADC_SampleTime_55Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_16, 3, ADC_SampleTime_55Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_17, 4, ADC_SampleTime_55Cycles5);

配置完成之后,就可以编写中断服务函数了。注入序列数据寄存器的读取可以借助 ADC_GetInjectedConversionValue() 函数完成,不过由于注入序列数据寄存器不止一个,所以该函数还需要一个额外的参数指明读取的是哪个注入通道的数据:

void ADC1_2_IRQHandler(void) {
    if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) {
        ADC_Values[0] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
        ADC_Values[1] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2);
        ADC_Values[2] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_3);
        ADC_Values[3] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_4);
        ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
    }
}

ADC 的结构框图表明,当注入序列转换完成后,既会产生 JEOC 标志/中断,也会产生 EOC 标志/中断。由于当前程序不需要区分是规则序列还是注入序列完成,所以判断哪个标志位都可以。

京ICP备2021034974号
contact me by hello@frozencandles.fun