8、ADC模拟-数字转换器(STM32)

一、原理

(1)总体版

关于ADC的原理,这里的直接用大佬的笔记,他是对STM32F407VET6型号的32来写的,型号不同但ADC原理知识不会有太大差异,拿来参考加深对ADC的印象即可。

http://t.csdnimg.cn/NsRwTicon-default.png?t=N7T8http://t.csdnimg.cn/NsRwT

(2)搭配STM32江协版

下面这个作者的ADC笔记是跟江协的STM32-ADC课程编写的,这个适合搭配网课去了解理解,前期先知道怎么用即可,不要研究的太深入。

http://t.csdnimg.cn/K3TGAicon-default.png?t=N7T8http://t.csdnimg.cn/K3TGA

二、认识ADC库函数名与作用

附:ADC库函数里各函数的功能,简单的备注多看多用就好,不需要记,作为前期认识

​
void ADC_DeInit(ADC_TypeDef* ADCx);
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);

void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);//ADC上电(开关控制)
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);//开启DMA输出信号(DMA转运数据)
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);//中断输出控制(控制中断通往NVIC)
void ADC_ResetCalibration(ADC_TypeDef* ADCx);//复位校准
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);//获取复位校准状态
void ADC_StartCalibration(ADC_TypeDef* ADCx);//开始校准
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);//获取开始校准状态

void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//ADC软件转换控制(软件触发)
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);//ADC获取软件开始转换状态(没啥用)
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);//配置间断模式:每隔几个通道间断一次
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//是不是启动间断模式
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);//ADC规则组通道配置
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);//获取AD转换的数据寄存器
uint32_t ADC_GetDualModeConversionValue(void);//获取双模式转换值
//下面都是对ADC注入组的配置(inject)
void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv);
void ADC_ExternalTrigInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_SoftwareStartInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
FlagStatus ADC_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef* ADCx);
void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length);
void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset);
uint16_t ADC_GetInjectedConversionValue(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel);

//配置模拟看门狗
void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog);//是否启动模拟看门狗
void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold);//配置高低阈值
void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel);//配置看门的通道

//开启内部两个通道
void ADC_TempSensorVrefintCmd(FunctionalState NewState);//ADC温度传感器、内部参考电压控制
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//获取标志位状态
void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//清除标志位
ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT);//获取中断状态
void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);//清除中断挂起位

​

三、代码实现部分(注释很重要)

我们关注的是先会怎么用,以下是代码实现部分:

(1)AD单通道

新建AD.c与AD.h

AD.c

#include "stm32f10x.h"                  // Device header
//ADC初始化
void AD_Init(void)
{
	//第一步开启时钟,包括ADC、RCC(ADCCLK的分频器)、GPIO口
	//第二步从左到右,配置GPIO,把需要的GPIO配置为模拟输入的模式
	//第三步,配置这里的多路开关,把这边的通道接入到右边的规则组列表里
	//第四步,配置ADC转换器,在库函数里用结构体来配置(AD转换器+AD数据寄存器)的参数
	//第五步,开关控制调用ADC_Cmd开启ADC
	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,指定通道,规则组序列器的次序,通道的采样时间)
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	//现在规则组菜单列表的第一个位置,写入通道0通道(即在这个序列1的位置写入通道0)如果还想继续填充,
	//就再复制一次,改想要的通道即可
	
	//记得是先写ADC_Init(即Init),再从他调定义把ADC_InitStructure复制过来,再开始配置结构体
	ADC_InitTypeDef ADC_InitStructure;
	
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//工作模式:单ADC通道模式,参数选择独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐:右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源:None不使用外部触发(即内部触发)
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转换模式:连续模式,即DISABLE
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;//扫描转换模式:DISABLE,即非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;//通道数目:1//这个参数仅在扫描模式下才需要使用,
	                                       //如果是非扫描模式,那整个列表就只有第一个序列有效
										   //无论写多少数目,最终只有序列1的位置有效
	
	ADC_Init(ADC1, &ADC_InitStructure);
	
	
	//开启ADC电源	
	ADC_Cmd(ADC1, ENABLE);
	
	//ADC校准
	ADC_ResetCalibration(ADC1);//复位校准
	
	//这个函数是返回复位校准的状态,所以要等待复位完成的话,还需要加一个while循环
	//如果没校准完成,就在这个while空循环里一直等待
	
	//获取标志位和校准完成的对应关系,这个函数获取的是CR2寄存器里的RSTCAL标志位,
	//该位由软件设置并由硬件清除,再校准寄存器被初始化后该位将被清除
	//所以用法是,软件置该位为1,硬件开始复位校准,当复位校准完成后,该位就会由硬件自动清零
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//如果他是1,就需要一直空循环等待
													   //如果它变为0了,就说明复位完成,可以跳出等待了
	//启动校准,参数给ADC1
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

//启动转换获取结果
//软件触发转换,等待转换完成(即等待EOC标志位置1),最后读取ADC数据寄存器,
uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换
	//获取标志位状态的函数,和上面一样的道理
	//参数(ADC1,选规则组EOC的)EOC是规则组或注入组完成时都会置1(手册寄存器查找)
	//当它为1时,转换完成,0时未完成
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//当未完成时,执行空循环,
	            //EOC由硬件自动置1,那while就自动跳出,表示转换完成,
	//整个周期等于采样周期55.5+转换周期12.5=68个周期
	//配置的ADCCLK是72MHz的6分频,就是12MHz,12MHz进行68个周期,转换才能完成
	//最终时间为1/12,再乘以68,等于5.6us	
	
	return ADC_GetConversionValue(ADC1);//ADC获取转换值,直接读取ADC的DR数据寄存器
	//因为读取DR寄存器会自动清除EOC标志位,所以不需要再手动加while循环等待置01清除标志位
	
}

//要用单通道的连续转换模式,修改三个地方即可
//1、ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;转换模式该为ENABLE

//2、连续转换只需要在最开始触发一次就行了,所以软件触发函数可以挪到初始化最后
//即(ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换),触发一次就行了,不要放在函数每次调用,
//这时内部的ADC就会一次接着一次地连续不断对指定的通道0进行转换,转换结果放在数据寄存器里

//此时寄存器会不断刷新最新的转换结果,所以在AD_GetValue函数里不需要判断标志位了,直接return数据寄存器的值就行了

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();
		
		//因为ADValue为整数,在除4095之后会舍弃小数部分,这样会导致计算错误,所以要对其强转为float类型
		//实际上AD值等于4096时才对应3.3V,会有一个数的偏差,所以AD的最大值4095实际对应的是3.3小一点点
		//没有办法达到3.3V,这个是受限于ADC的结构
		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);//小数部分,由于浮点数不能取余,所以要对(Voltage*100)转换为整形
		
		Delay_ms(100);//让它算慢点
	}
}

(2)AD多通道

注释都是精华

#include "stm32f10x.h"                  // Device header
//ADC初始化
void AD_Init(void)
{
	//第一步开启时钟,包括ADC、RCC(ADCCLK的分频器)、GPIO口
	//第二步从左到右,配置GPIO,把需要的GPIO配置为模拟输入的模式
	//第三步,配置这里的多路开关,把这边的通道接入到右边的规则组列表里
	//第四步,配置ADC转换器,在库函数里用结构体来配置(AD转换器+AD数据寄存器)的参数
	//第五步,开关控制调用ADC_Cmd开启ADC
	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);
	
	
	//现在规则组菜单列表的第一个位置,写入通道0通道(即在这个序列1的位置写入通道0)如果还想继续填充,
	//就再复制一次,改想要的通道即可
	
	//记得是先写ADC_Init(即Init),再从他调定义把ADC_InitStructure复制过来,再开始配置结构体
	ADC_InitTypeDef ADC_InitStructure;
	
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//工作模式:单ADC通道模式,参数选择独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐:右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源:None不使用外部触发(即内部触发)
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转换模式:连续模式,即DISABLE
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;//扫描转换模式:DISABLE,即非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;//通道数目:1//这个参数仅在扫描模式下才需要使用,
	                                       //如果是非扫描模式,那整个列表就只有第一个序列有效
										   //无论写多少数目,最终只有序列1的位置有效
	
	ADC_Init(ADC1, &ADC_InitStructure);
	
	
	//开启ADC电源	
	ADC_Cmd(ADC1, ENABLE);
	
	//ADC校准
	ADC_ResetCalibration(ADC1);//复位校准
	
	//这个函数是返回复位校准的状态,所以要等待复位完成的话,还需要加一个while循环
	//如果没校准完成,就在这个while空循环里一直等待
	
	//获取标志位和校准完成的对应关系,这个函数获取的是CR2寄存器里的RSTCAL标志位,
	//该位由软件设置并由硬件清除,再校准寄存器被初始化后该位将被清除
	//所以用法是,软件置该位为1,硬件开始复位校准,当复位校准完成后,该位就会由硬件自动清零
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//如果他是1,就需要一直空循环等待
													   //如果它变为0了,就说明复位完成,可以跳出等待了
	//启动校准,参数给ADC1
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

//启动转换获取结果
//软件触发转换,等待转换完成(即等待EOC标志位置1),最后读取ADC数据寄存器,
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	//(ADC,指定通道,规则组序列器的次序,通道的采样时间)
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换
	//获取标志位状态的函数,和上面一样的道理
	//参数(ADC1,选规则组EOC的)EOC是规则组或注入组完成时都会置1(手册寄存器查找)
	//当它为1时,转换完成,0时未完成
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//当未完成时,执行空循环,
	            //EOC由硬件自动置1,那while就自动跳出,表示转换完成,
	//整个周期等于采样周期55.5+转换周期12.5=68个周期
	//配置的ADCCLK是72MHz的6分频,就是12MHz,12MHz进行68个周期,转换才能完成
	//最终时间为1/12,再乘以68,等于5.6us	
	
	return ADC_GetConversionValue(ADC1);//ADC获取转换值,直接读取ADC的DR数据寄存器
	//因为读取DR寄存器会自动清除EOC标志位,所以不需要再手动加while循环等待置01清除标志位
	
}

//要用单通道的连续转换模式,修改三个地方即可
//1、ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;转换模式该为ENABLE

//2、连续转换只需要在最开始触发一次就行了,所以软件触发函数可以挪到初始化最后
//即(ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换),触发一次就行了,不要放在函数每次调用,
//这时内部的ADC就会一次接着一次地连续不断对指定的通道0进行转换,转换结果放在数据寄存器里

//此时寄存器会不断刷新最新的转换结果,所以在AD_GetValue函数里不需要判断标志位了,直接return数据寄存器的值就行了

//多通道的设置
//首先我们想到的是后面的两种扫描模式,利用ppt里面的两个列表,把四个通道都填进去,然后触发转换,就能实现多通道了,
//这样确实是一种不错的方法,但是由于数据覆盖的问题,如果想要用扫描模式实现多通道,最好还是要配DMA来实现
//那我们一个通道转换完成后,手动把数据运出来可以吗?为啥非要用DMA来转运呢?
//看似非常简单,实际操作起来会有一些问题

//第一个问题是,在扫描模式下,你启动列表之后,它里面每一个单独的通道转换完成之后,
//不会产生如何的标志位,也不会触发如何中断,你不知道某一个通道是不是转换完了,他只有在整个列表都转换完成之后,
//才会产生一次EOC标志位,才能触发中断
//而这时前面的数据就已经覆盖丢失了;

//第二个问题是,AD转换是非常快的,转换一个通道大概只有几us
//也就是说你如果不能在几us的时间内把数据转运走
//那数据就会丢失,这对我们程序手动转运数据要求就比较高了
//所以在扫描模式下,手动转运数据是比较困难的

//不过也不是说手动转运不可行,我们可以使用间断模式,在扫描的时候,每转换一个通道就会暂停一次,
//等我们手动把数据转运走之后,再继续触发,继续下一次转换,这样就可以实现手动转运数据的功能
//但是由于单个通道转换完成之后,没有标志位,所以启动转换之后,只能通过Delay延时的方式,
//延时足够长的时间,才能保证转换完成

//可以看得出,这种方法不顺心且不能提高效率,所以不推荐使用

//这种方法不行,我们可以用上面的单次转换、非扫描的模式,来实现多通道
//只需要在每次触发前,手动更改一下列表第一个位置的通道就可以了

//(ADC,指定通道,规则组序列器的次序,通道的采样时间)
//先把这个(ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);)函数放置
//AD_GetValue函数中,然后我们想指定的通道,可以提取成AD_GetValue的参数,ADC_Channel
//只需要指定一个通道,返回值就是我们指定通道的结果了

//GPIO口也更改一下
//h头函数文件内函数声明也更新一下

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;
float Voltage;

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:");
	//依次启动四次转换,,并且在转换前,指定了转换的通道,每次转换完成后,把结果费别存在四个数据里
	//最后显示一下,这就是使用单次非扫描的模式,实现AD多通道的方法
	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);//让它算慢点
	}
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值