STM-32:ADC模数转换器—ADC单通道转换/ADC多通道转换

一、ADC 模数转换器

1.1ADC简介

ADC(Analog-Digital Converter),意即模拟-数字转换器,简称模数转换器。ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。与ADC相对应,从数字电路到模拟电路的桥梁即DAC(Digital-Analog Convertor),数模转换器。

DAC不是唯一可以实现将数字量转换为模拟量功能的外设,PWM波形同样实现了用数字量对模拟量进行编码的操作。PWM只有完全导通和完全断开两种状态,故其不存在静态功耗。所以在直流电机调速等大功率的引用场景,使用PWM来对模拟量进行编码是比使用DAC的更好的选择。目前DAC的应用场景通常在波形发生领域,例如信号发生器、音频解码芯片等。

STM32中的ADC是一个12位的逐次逼近型的ADC,最快转换时间1us。“逐次逼近”是ADC的一种工作模式;“12位”指ADC的分辨率,12位AD值的表示范围即0∼2的12次方-1,量化值为0 ∼ 4095 ,位数越高,量化结果约精细,对应ADC的分辨率就越高;转换时间1us对应的频率就是1MHz,这就是STM32中ADC的最快转换频率。
  STM32中的ADC的输入电压范围为0 ∼ 3.3 V ,对应的转换结果就是0 ∼ 4095。ADC的输入电压一般要求都是要在芯片供电的正负极之间变化的。且电压和转换结果都是一一对应的线性关系。
  STM32中的ADC有18个输入通道,可以测量16个外部信号和2个内部信号源。外部的16个信号源即16个GPIO口(可能来自不同的GPIOx,具体要参考引脚定义),在引脚上直接输入模拟信号即可,不需要额外的测量电路;2个内部信号源分别是内部温度传感器和内部参考电压,温度传感器可以测量CPU的温度,内部参考电压是一个1.2V左右的内部基准电压,且这个内部基准电压是不随外部供电电压变化的。当芯片的供电电压不是准确的3.3V时,就可以通过这个内部的基准电压进行校准来得到正确的电压值。
  STM32还拥有两个增强功能:拥有规则组和注入组两个转换单元。普通的AD转换流程为:先启动一次转换,之后读值,依次循环。STM32的ADC可以将要转换的通道列为一组,每一次连续转换多个值。规则组用于常规使用,注入组一般用于突发事件。
  STM32中还拥有一个模拟看门狗(AWD),可以自动监测输入电压范围。STM32的ADC一般可以用于测量光线强度、温度。这里的模拟看门狗实现的功能是:监测温度(光强),当其高于某个阈值,或低于某个阈值时,申请中断,之后可以在中断函数中执行相应的操作。

1.2 逐次逼近型ADC工作原理

下图所示的即为逐次逼近型ADC的工作原理图(ADC0809)。STM32中的ADC的结构与此类似,学习下面的结构可以帮助我们理解STM32中ADC的工作原理。
在这里插入图片描述
ADC0809是一个独立的8位逐次逼近型ADC芯片。它拥有IN0~IN7,8个输入通道,通过地址锁存器和译码器电路实现对通道的选择,且每一次转换通道选择开关只能转换一个通道的信号。ADC转换的速度非常快,从信号转换开始到结束只需要几us的时间,如果想转换多路信号,不必设计多个ADC,只需要在每一次转换前通过多路选择开关选择要转换的通路即可。 STM32的ADC拥有18个输入通道,与这里的8个输入通道的结构相对应。
  ADC内部拥有一个DAC模块,其内部是通过加权电阻网络实现模数转换,可以将逐次逼近寄存器SAR的值转换为对应的模拟电压值,将其电压值再与待测电压相比较,比较结果控制SAR中存储的值,直到DAC输出的电压与外部通道输入的电压近似相等,DAC输入的数据就是外部电压的编码数据了。为了最快找到未知编码的电压,通常使用二分法进行查找。且使用二分法查找未知电压的编码的好处在于:每次选择比较的值恰好为对应二进制数字的每一位的权数,判断过程相当于,从高位到低位依次判断为1还是为0的过程。要找到未知编码的电压,8位ADC需要判断8次,12位ADC需要判断12次。转换结束后,ADC的输入数据就是未知电压的编码,通过8位数字输出端口(D0~D7)进行输出。
  结构图上方的EOC(End Of Convert)是转换结束信号。该芯片通过START端口控制转换开始,CLOCK控制ADC内部的转换工作频率。V R E F ( + ) 和V R E F ( − ) 是DAC的参考电压,定义数据对应的电压范围(255对应3.3V或5V)。通常芯片的工作电压正极V c c 和V R E F ( + ) 相同,接在一起;通常芯片的工作电压负极GND和V R E F ( − ) 相同,接在一起。

1.3STM32中的ADC基本结构

在这里插入图片描述
STM32中ADC的结构框图如上图所示。其“模拟至数字转换器”模块的工作模式与ADC0809在原理上完全相同。不同点有以下几点:

普通的ADC多路开关一般只选中一个,STM32的ADC可以同时选中多个通道进行转换,规则组最多同时选中16个通道,注入组一次最多可以选中4个通道。(以餐厅点菜模型为例,普通模式为每次点一个菜,做好菜后上菜;STM32可以做到每次列出一个菜单,规则组一次最多可以列16个菜,注入组一次最多可以列4个菜,做好后依次上菜)

STM32中的ADC的转换结果会被存储在对应的数据寄存器中。对于规则组通道,其只有一个数据寄存器(餐桌上只能摆一个菜),后转换的数据会将之前转换的数据覆盖,之前转换的数据就会丢失。对于规则组通道,要想实现同时转换的功能,最好配合DMA来将转换后的数据及时转运,就可以保证转换的数据不会丢失了。对于注入组通道,它拥有4个数据寄存器(餐厅的VIP坐席,餐桌上一次可以摆四个菜)。对于注入组而言,就不用担心数据覆盖的问题了。一般情况下,使用规则组和DMA就可以满足大部分的使用需求。(这里只讲解规则组使用,注入组自行了解即可)

结构图的右下角为触发转换信号,对应ADC0809的START信号。STM32的触发转换信号来源有两种:软件触发和硬件触发。硬件触发信号可以来自于定时器的各个通道、定时器TRGO主模式的输出,外部中断EXTI。下表列出了ADC1和ADC2的触发源。(其中EXTI线11/TIM8_YRGO事件的选择需要使用AFIO端口重映射来配置)
在这里插入图片描述
定时器可以通过主模式输出TRGO控制ADC、DAC等外设,用于通过硬件电路自动触发转换。ADC经常需要过一个固定的时间段转换一次,例如可以每隔1ms转换一次。通过定时器,每隔1ms申请一次中断,在中断函数中手动通过软件触发开启一次转换也可以实现功能,但是频繁进中断会对主程序造成一定的影响。并且在不同的中断之间,由于优先级的不同,有可能会使某些中断不能及时得到响应。如果触发ADC的中断没有及时得到响应,那么ADC的转换频率就肯定会受影响了。所以对于这种需要频繁进中断,但是在中断函数中只完成了简单工作的情况,一般都会有硬件电路的支持。

这里ADC的时钟ADCCLK是来自于RCC的APB2时钟。由原理图可得,ADCCLK最大为14MHz,所以ADC预分频器只能选择6分频(得到12MHz)和8分频(得到9MHz)两个值。

ADC可以通过DMA请求信号触发DMA转运数据。

模拟看门狗的功能是监测指定的通道。可以设置模拟看门狗的阈值高限(12位)、阈值底限(12位)和指定“看门”的通道。只要通道的电压值超过阈值范围,模拟看门狗就会“乱叫”,申请一个模拟看门狗的中断,之后通向NVIC。

规则组和注入组在转换完成后会生成一个转换完成的信号。EOC为规则组转换完成的信号,JEOC为注入组转换完成的信号。这两个信号会在状态寄存器中置一个标志位,我们通过读取状态寄存器,就可以知道转换是否完成了。同时这两个标志位也可以通过配置通向NVIC申请中断。
在这里插入图片描述

1.4STM32中ADC的输入通道

由ADC的内部结构可知,STM32的ADC对应16个输入通道。这16个输入通道对应的GPIO端口如下表所示:
在这里插入图片描述
上表展示了ADC通道和引脚复用之间的连接关系,这个对应关系也可以参考引脚定义表。可以看到,只有ADC1拥有温度传感器和内部参考电压的采样通道。ADC1和ADC2的引脚完全相同,ADC3有些是存在变化的。本节课程使用的STM32F103C8T6没有PC0到PC5的引脚,故也就不存在通道10到通道15。

参考引脚定义表可以看到,STM32的ADC1和ADC2的引脚是相同的。这样的设计是为双ADC模式服务的。关于双ADC模式的内容比较复杂,这里仅作简单了解即可。双ADC模式,即ADC1和ADC2同时工作,二者可以配合为同步模式、交叉模式等多种不同的工作模式。以交叉模式为例,ADC1和ADC2交叉对同一个通道进行采样,这样就可以进一步提高采样率。

1.5STM32中的ADC的四种转换模式

1.5.1单次转换非扫描模式

在这里插入图片描述

1.5.2连续转换非扫描模式

在这里插入图片描述

1.5.3单次转换扫描模式

在这里插入图片描述

1.5.4连续转换扫描模式

在这里插入图片描述

ADC的16个序列就相当于一个“菜单”,在序列中可以填入不同的通道。在非扫描模式下,这个“菜单”就只对存放在序列1的通道起作用, 这时“菜单”中可以选中一组的方式就退化为简单地选中一个的模式了。 在每次转换开始前可以对序列中的通道进行更改。在扫描模式下,“菜单”将发挥作用。每次转换开始后,依次对定义的通道进行转换,并且将转换结果放在数据寄存器中。这里为了方式数据被覆盖导致丢失,就需要使用DMA及时将数据移走。
  在单次转换模式中,需要手动触发转换。 ADC就会对序列中的通道进行转换,转换完成后,将转换结果储存在数据寄存器中,同时将EOC标志位置1,转换过程结束。在连续转换模式中,一次转换完成后不会停止,而是立刻开始下一轮的转换,并持续下去。这样就可以在最开始时触发一次,之后就可以一直转换了。这个模式的好处在于:开始转换后不需要等待,因为其一直都在转换,所以也不需要手动开启转换了,也不用判断转换是否结束。需要读取AD值时,直接在数据寄存器中读取即可。
  在扫描模式的情况下,还可以使用间断模式。它的作用是在扫描的过程中,每隔几次转换就暂停,需要再次触发才能继续。该模式仅作了解即可。

1.6使用ADC时的一些细节

1.6.1数据对齐

在这里插入图片描述
STM32中的ADC是12位的,但是数据寄存器拥有16位,故存在数据对齐的问题。数据右对齐,即作为转换结果的12位数据向右靠,高位补0;数据左对齐,即作为转换结果的12位数据向左靠,低位补0。在使用时通常使用数据右对齐,这样在读取时直接读取寄存器即可。如果选择左对齐直接读取,得到的数据会比实际的数据大16倍。当对分辨率的要求不高时(对电压仅作大概的判断即可)可以采用左对齐,将数据寄存器的高8位取出,就相当于舍弃了转换结果的4位的精度,12位的ADC退化位为8位的ADC。

1.6.2转换时间

AD的转换时间是一个很短的时间,如果不需要极高的转换频率,那么转换时间是可以忽略的。那么转换时间具体是多少呢?
  AD转换的步骤分别是:采样、保持、量化、编码。其中采样和保持可以看作一个过程,量化和编码可以看作一个过程。量化和编码实际上就是ADC逐次比较的过程,一般ADC的位数越多,所花费的时间就越长。采样和保持是为了保证在量化和编码的过程中输入电压的变化不会过大。在量化和编码之前,需要添加采样-保持电路,即需要设置一个采样开关,打开开关一段时间来收集电压(可以用一个小容量的电容来存储这个电压),存储完成之后断开开关,再进行之后的AD转换。这样就可以保证在量化和编码器件始终保持电压基本不变。这个采样时间是比较长的。所以AD转换所花费的时间可以分为:采样-保持电路的采样时间 + 量化和编码花费的时间。
  
STM32 ADC总转换时间 = 采样时间 + 12.5个ADC周期

ADC的采样时间可以在程序中进行配置。之后花费12个ADC周期进行量化和编码,多余的0.5个周期完成了其他的工作。
 最短的转换时间:当ADCCLK = 14MHz,采样时间为1.5个ADC周期时:
在这里插入图片描述
当然,可以通过设置将ADC的转换频率超过14MHz,这样ADC就会工作在超频状态下。超频时转换时间可能会更短,不过电路的稳定性将无法保证。

1.6.3 ADC校准

ADC有一个固定的内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。建议在每次上电后执行依次校准,且启动校准前,ADC必须处于关电状态超过至少两个ADC周期。
  ADC的校准详细过程不需要掌握,在使用时在ADC初始化最后加几行固定的代码即可。

二、实例代码

2.1ADC单通道转换

2.1.1接线图

在这里插入图片描述

2.1.2程序代码

AD.c

#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
    
    
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

uint16_t AD_GetValue(void)
{
    
    
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
	return ADC_GetConversionValue(ADC1);
}

AD.h

#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(void);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;
float Voltage;

int main(void)
{
    
    
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Volatge:0.00V");
	
	while (1)
	{
    
    
		ADValue = AD_GetValue();
		Voltage = (float)ADValue / 4095 * 3.3;
		
		OLED_ShowNum(1, 9, ADValue, 4);
		OLED_ShowNum(2, 9, Voltage, 1);
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
		
		Delay_ms(100);
	}
}

2.2ADC多通道转换

2.2.1接线图

在这里插入图片描述

2.2.2程序代码

AD.c

#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
    
    
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
		
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

uint16_t AD_GetValue(uint8_t ADC_Channel)
{
    
    
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
	return ADC_GetConversionValue(ADC1);
}

AD.h

#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0, AD1, AD2, AD3;

int main(void)
{
    
    
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while (1)
	{
    
    
		AD0 = AD_GetValue(ADC_Channel_0);
		AD1 = AD_GetValue(ADC_Channel_1);
		AD2 = AD_GetValue(ADC_Channel_2);
		AD3 = AD_GetValue(ADC_Channel_3);
		
		OLED_ShowNum(1, 5, AD0, 4);
		OLED_ShowNum(2, 5, AD1, 4);
		OLED_ShowNum(3, 5, AD2, 4);
		OLED_ShowNum(4, 5, AD3, 4);
		
		Delay_ms(100);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_27928443/article/details/129974897
今日推荐