STM32 ADC模数转换器学习指南

文章目录

  • 前言
  • 一、ADC简介
  • 1.概述
  • 2.图示详解
  • 1.外挂式逐次逼近型ADC
  • 2.STM32的逐次逼近型ADC
  • 二、细节之处
  • 1.输入通道
  • 2.四种转换模式(规则组)
  • 3.触发控制
  • 4.数据对齐
  • 5.转换时间
  • 6.校准
  • 7.硬件电路
  • 三、实操案例
  • 1.AD单通道
  • 2.AD多通道
  • 总结

  • 声明:学习笔记根据b站江科大自化协stm32入门教程编辑,仅供学习交流使用!
    注意:本文9920字,阅读大约需要15分钟,请耐心会收获满满!

    前言

    本次学习有两个实操程序,第一个程序为AD单通道,第二个为AD多通道
    STM32的ADC为12位,AD最大值是4095,对应最大电压3.3V,可对0-3.3v之间的任意电压量化,所以ADC相当于一个电压表。而对于GPIO而言只能读取引脚的高低电平两个值。


    一、ADC简介

    1.概述


    注:开关控制对应ADC_Cmd库函数,用于给ADC上电
    ADC(Analog-Digital Converter)模拟-数字转换器,ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。
    12位逐次逼近型ADC,1us转换时间。12位于1us涉及到ADC的两个关键参数,第一个位分辨率,一般用多少位表示,表示可量化的精细度。第二个是转换时间也即转换频率,AD转换需要一定时间,1us表示从AD转换开始到产生结果的时间,对应频率为1MHz.
    输入电压范围:0-3.3V,转换结果范围:0~4095。一一对应的线性关系
    18个输入通道,可测量16个外部和2个内部信号源。外部时钟源信号就是16个GPIO口,在引脚直接接模拟信号就可。2个内部信号源是内部温度传感器和内部参考电压,温度传感器可测CPU的温度;内部参考电压是一个1.2v左右的基准电压,它不随外部供电电压变化而变化,如果芯片的供电电压不是标准3.3v,那么测量外部引脚的电压可能就不对,此时可读取基准电压进行校准得到正确的外部引脚电压值。
    规则组和注入组两个转换单元。属于STM32 ADC的增强功能,普通的AD转换流程为启动一次转换读一次值,然后再启动再读值的流程。增强后,可以列一个组,一次性启动一个组,连续转换多个值;并且有两个组,规则组用于常规使用,注入组用于突发事件。
    模拟看门狗自动监测输入电压范围。ADC一般可用于测量光线强度、温度这些值且经常会有个需求:如果光线或温度高于某个阈值、低于某个阈值时,执行一些操作。这个高于/低于某个阈值的判断就可用模拟看门狗自动执行,模拟看门狗可监测指定的某些通道,当AD值高于它设定的上阈值或低于下阈值就会申请中断,在中断函数执行相应操作。这样就不用手动(软件资源)读值,再用if进行判断了。
    STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道,上面所说的16个是这个系列最多的通道数,本型号只有10个。
    拓展:
    DAC与之相反位数模转换器,是数字到模拟的桥梁!不过学习定时器输出比较讲的PWM,也可实现数字到模拟的转换,同时PWM只有完全导通和完全断开两种状态,无功率损耗,所以在直流电机调速这种大功率应用场景,使用PWM来等效模拟量是比DAC更好的选择,且PWM电路更加简单更常用。而DAC的应用主要是在波形生成领域,比如信号发生器、音频解码芯片等,这些领域PWM不好替代!!实操用的SYM32没有DAC外设。


    2.图示详解

    1.外挂式逐次逼近型ADC

    》外挂式逐次逼近型ADC:(与STM32里的ADC原理基本一样)
    它是一款外挂的(独立出来的)ADC芯片,随着单片机集成度的提升现在很多单片机内部就已经集成了ADC外设,不用这种外挂芯片了,引脚可直接测电压非常方便!

    1、首先左边IN0-IN7是8路输入通道,通过通道选择开关选中一路输入到下一步进行转换,地址锁存和译码就是想选中哪个通道(8路)就把通道号放在ADDA-ADDC三个引脚上,ALE给一个锁存信号上面对应的通路开关就可自动拨好。这部分就相当于一个可以通过模拟信号的数据选择器,当然STM32的ADC有18个输入通道!
    2、逐次逼近方法:信号到一个电压比较器,可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小;两个输入端一个为待测电压,另一个是DAC的电压输出端(DAC内部是使用加权电阻网络来实现转换,可参考51教程),两者比较并不断调整DAC电压的输入编码值使两者不断逼近,这样DAC的输入数据就是外部电压的编码数据了。电压调节的过程由图中的逐次逼近SAR完成,为了快速常使用二分法查找,8位的ADC从高位到地位需要依次判断8次,12位的需要12次。
    3、AD转换结束后,DAC的输入数据(SAR向下的双箭头)就是位置待测电压的编码,通过朝右的双箭头输出到三态锁存缓冲器,8位8根线,12位12根线。
    4、引脚:EOC为End of Convert即转换结束信号;START是开始转换,给一个输入脉冲;CLOCK是ADC时钟,因为ADC内部是一步一步进行判断的,需要时钟来推动这个过程;下面的VREF+和VREF-是DAC的参考电压,比如给DAC个数据255,是对应5V还是3.3V就由参考电压决定,DAC的参考电压也决定了ADC的输入范围,所以也是ADC的参考电压;Vcc与GND为供电,通常参考电压+和Vcc一样,会接在一起,负极和GND也是一样接在一起!


    2.STM32的逐次逼近型ADC

    》STM32的逐次逼近型ADC: (芯片手册第11章11.3)

    下面截取上图局部,一部分一部分分析:

    相比上面的外挂ADC的高级之处:可同时选中多个通道,且转换的时候分成了两组规则通道和注入通道。比喻成餐厅菜单介绍:
    (1)规则组菜单可同时存16个菜,但是存在一个不足之处:规则组只有一个数据寄存器,就是桌子比较小最多只能放一个菜,如果上16个菜那么前15个菜都会被挤掉,只能得到第16个菜。所以对于规则组转换来说如果使用这个菜单,最好配合DMA来实现,DMA是一个数据转运小助手,它可以每上一个菜之后把这个菜挪到其他地方,防止被覆盖,下节笔记我们一起学习DMA。上图DMA请求就是用来触发DMA进行数据转运的。
    (2)注入组就比较高级,比作餐厅的VIP座位,在这个座位上一次最多可以点4个菜,且数据寄存器有4个(可以同时上这4个菜),不必担心数据覆盖。
    一般使用规则组就足够,下面也主要涉及规则组,注入组可参考芯片手册!
    触发STM32的ADC开始转换的信号有两种:

    (1)软件触发。即在程序中手动调用一条代码,就可启动转换
    (2)硬件触发。即上图的这些触发源,上面为注入组的触发源,下面为规则组的触发源,这些触发源主要来自定时器,有定时器的各个通道,还有TRGO定时器主模式的输出。定时器可以通向ADC、DAC这些外设用于触发转换,因为ADC经常需要过一个固定时间段转换一次,正常实现思路为用定时器每隔一定时间申请一次中断,在中断里手动进行一次转换,但就像之前一直强调的频繁进中断会阻塞主程序。一般这样的问题都会有硬件支持,比如以上图举个例子:给TIM3定个1ms的时间,并且把TIM3的更新事件选择为TRGO输出,然后在ADC选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换,整个过程不需要进中断节省了中断资源。当然也可选择外部中断引脚EXTI_11来触发转换。
    STM32ADC的时钟ADCCLK来自ADC预分频器,而ADC预分频器来源于RCC。如图RCC时钟树:
    因为ADCCLK最大14MHz,虽然ADC预分频器有2、4、6、8分频,但实际上只有6分频12MHz,8分频9MHz满足要求。

    模拟看门狗与EOC信号
    模拟看门狗用于监测转换结果的范围,可以存放一个阈值高限和阈值低限,如果启动了模拟看门狗,并且指定了看门的通道,那这个看门狗就会关注此通道,一旦超过这个阈值范围就会乱叫,在上面申请一个模拟看门狗的中断最后通向NVIC。
    然后对于规则组和注入组而言,它们转换完成后,也会有一个EOC或JEOC转换完成的信号,这两个信号会在状态寄存器里分别置一个标志位,读取这个标志位就能知道是不是转换结束了;同时这两个标志位也可以去NVIC申请中断(如果开启了NVIC对应的通道它们就会触发中断)


    二、细节之处

    1.输入通道

    下图为ADC通道和引脚复用的关系:(也可对照引脚定义表)

    只有ADC1有通道16和17,ADC2和ADC3没有。通道0-15GPIO口的引脚,ADC1和ADC2的引脚完全相同,ADC3中间有些变化,不过本实操所用芯片无ADC3。标绿部分引脚本芯片也无。(STM32F10xxx)


    2.四种转换模式(规则组)

    用库函数初始化ADC的结构体里,会有两个参数,一个选择单次还是连续转换,另一个选择扫描还是非扫描模式。
    1、单次转换,非扫描模式

    最简单的一种模式(没用到“菜单”列表),此列表就是规则组里的“菜单”,序列1-16,可在此处“点菜”,就是写入你要转换的通道,在非扫描模式下,菜单只有序列1的位置有效,这时“菜单”同时选中一组的方式就退化为简单选中一个的方式。可在序列1处写入我们想转换的通道,比如上图中的通道2,就可触发转换,转换结果放在数据寄存器里,同时给EOC标志位置1,转换过程结束。判断EOC标志位,如果转换完了就可在数据寄存器里读取结果了,如果想再启动一次转换,就需要再触发一次,以此循环。如果想换一个通道转换,在转换前把序列1位置的通道2改成其他通道。
    2、连续转换,非扫描模式

    还是非扫描模式,“菜单”只用序列1位置,与上一种单次转换不同之处是它在一次转换结束后不会停止,而是立刻开始下一轮转换,一直持续下去。即只需要最开始触发一次,之后就可以一直转换了,好处是开始转换后不需要等待一段时间,不需要手动开始转换。

    3、单次转换,扫描模式

    也是单次转换,每触发一次转换结束后就会停下;扫描模式就会用到“菜单”列表,可在菜单“点菜”,比如上图中的几个通道,每个位置是通道几可以任意指定并且可以重复。在ADC初始化结构体里还有个参数——通道数目,如图选了7。

    4、连续转换,扫描模式

    同理,可知此模式。在扫描模式下还可有一种模式——间断模式,作用是在扫描过程中每隔几个转换,就暂停一次,需要再次触发才能继续,此模式没有列出要不分类就太多。


    3.触发控制


    之前讲过触发控制,此表为规则组的触发源,也就是下图绿圈部分,有来自定时器的信号、引脚或定时器的信号(具体是引脚和定时器,需要用AFIO重映射来确定)、最后的软件控制位就是软件触发。这些触发信号的选择,可以通过设置表右边的寄存器完成,使用库函数的话只需一个函数给个参数即可。


    4.数据对齐

    我们这里的ADC是12位的,它的转换结果就是一个12位的数据,但是数据寄存器是16位,所以存在数据对齐的问题。高位补0
    1、数据右对齐

    2、数据左对齐

    一般选择右对齐,这样读取16位寄存器直接就是转换结果,如果是左对齐直接读的话得到的数据会比实际的大,左移四位等于乘16倍。
    左对齐的用途是:如果不想要这么高的分辨率,觉得0~4095太大了,就做个简单的判断,不需要这么高分辨率,就可选择左对齐,然后把这个数据的高8位取出,这样就舍弃了后面4位精度,退化为8位ADC。这样也可使用右对齐把12位取出来再做处理,只不过多了一步。


    5.转换时间

    对于这个参数我们一般不太敏感,因为AD转换速度很快,如果不需要非常高速的转换频率,那么转换时间就可忽略。
    AD转换的步骤:采样,保持,量化,编码。
    采样保持可放一起,量化编码可放一起,总共两大步。量化编码是ADC逐次比较的过程,这个需要时间,位数越多时间越长;为什么要采样保持?因为AD转换也就是量化编码需要时间,如果在这段时间输入电压还在不断变化,就没法定位输入电压到底在哪里。所以在量化编码之前,需要设置一个采样开关,先打开采样开关收集一下外部电压,比如可用一个小容量的电容存储一下这个电压,存储好之后断开采样开关再进行AD转换,这样在量化编码期间,电压时钟保持不变,这样才能精确定位未知电压的位置,这就是所谓的采样保持电路。这里会产生采样时间,所以可得:
    2、
    采样保持花费的时间可以在程序里配置,采样时间越大,越能避免一些毛刺信号的干扰,不过转换时间相应被延长。12.5个ADC周期是量化编码所需时间,ADC位12位所以需要花费12个周期,多了0.5个周期可能是要做其他事情。ADC周期就是从RCC分频过来的ADCCLK,ADCCLK最大14MHz,例子就是最快转换时间。其实可以超频转换(>14MHz),不过稳定性不强。


    6.校准

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


    7.硬件电路

    对于ADC外围电路的设计给出以下三个电路图:

    图一为电位器产生一个可调的电压,电位器两个固定端一端接3.3V,另一端接GND,这样中间滑动端就可输出一个0~3.3V可调的电压输出,这里可接ADC输入通道如图上PA0口。电阻阻值不可太小,若太小电阻会比较费电,再小就有可能发热冒烟,一般为KΩ级的。
    图二为传感器输出电压的电路,一般如光敏电阻、热敏电阻、红外接收管、麦克风等都可等效为一个可变电阻N1,与一个固定电阻串联分压,得到一个反应电阻值的电压电路。固定电阻一般可选与传感器阻值相近的电阻,这样可得到位于中间电压区域比较好的输出。
    图三为一个简单的电压转换电路,比如想测一个0~5V的VIN电压,但ADC只可接收0-3.3V的电压,用电阻分压,如PA2输入点的电压为VIN/50Kx33K。如果电压过高就不建议使用这种电路了,可能比较危险,高电压采集最好使用专用芯片,比如隔离放大器等,做到高低电压隔离保证电路安全。
    更多内容在芯片手册第11章!


    三、实操案例

    1.AD单通道

    xxxxx
    在这里根据引脚定义表,PA0到PB1十个引脚是ADC的10个通道,这10个任意选一个,其他的不可接模拟电压。此代码为单通道单次转换非扫描模式。

    //AD.c
    #include "stm32f10x.h"                  // Device header
    //根据ADC基本结构图,打通它即可:
    //1、开启RCC时钟,包括ADC和GPIO的时钟;ADCCLK的分频器也需要配置下
    //2、配置GPIO,把需要用的GPIO配置为模拟输入模式
    //3、配置多路开关,把左边的通道接入到右边的规则组列表里(“点菜”)
    //4、配置ADC转换器,利用库函数里的结构体,可配置包括AD转换器和AD数据寄存器在内的一大块的电路
       //(包括ADC是单次还是连续、扫描还是非扫描、几个通道、触发源是什么、数据对齐是左or右对齐)
    //其他的,如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道。
    //如果想开启中断,可在中断输出函数控制里用ITConfig函数开启对应的中断输出,然后在NVIC里配置下优先级就可触发中断。
    //实例代码中暂未用到模拟看门狗和中断。
    //5、开关控制,ADC_Cmd()函数开启ADC。
    //6、根据手册建议,开启后还可对其进行下校准,可减小误差。
    void AD_Init(void){
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//6分频
    	
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN ;//选择模拟输入模式即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);//一个口非扫描模式,点一个菜。“1”表示序列1的位置,加上Channel1表示在序列1处写入通道1,第四个参数与采样时间有关根据需要。
    	//ADC_RegularChannelConfig(ADC1,ADC_Channel_3,2,ADC_SampleTime_55Cycles5);//多个序列可在后面加,此处就一个输入口PA0
    	
    	ADC_InitTypeDef ADC_InitStructure;
    	ADC_InitStructure.ADC_Mode= ADC_Mode_Independent;//独立模式,ADC1和ADC2各转换各的。其他为双ADC模式较复杂
    	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_GetCalibrationStatus(ADC1) == SET);
    	ADC_StartCalibration(ADC1);
    	while (ADC_GetCalibrationStatus(ADC1) == SET);
    }
    
    uint16_t AD_GetValue(void){
    	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发,ADC开始转换
    	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);//ADC转换需要一定时间
    	return ADC_GetConversionValue(ADC1);//获取转换值
    }
    
    //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(3,1,"Voltage:0.00V");
    	while(1){
    		ADValue = AD_GetValue();
    		Voltage = (float)ADValue/4095*3.3;//数字与电压映射关系,ADValue为uint16类型除以4095会舍弃小数部分,先强转为float
    		OLED_ShowNum(1,9,ADValue,4);
    		//目前的OLED没有显示浮点数的功能,但可用显示整数的借用
    		OLED_ShowNum(2,9,Voltage,1);//显示整数部分
    		OLED_ShowNum(2,11,(uint16_t)(Voltage*100)%100,2);//显示小数部分,浮点数不可%取余所以先强转为整型
    		Delay_ms(20);//限制OLED刷新速度
    	}
    }
    
    

    若需要单通道连续转换非扫描模式,则:
    改动1:AD_ContinuousConvMode=ENABLE;
    改动2:连续转换只需要在最开始触发一次,软件触发ADC_SoftwareStartConvCmd(ADC1,ENABLE);挪到初始化函数里面。while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);也不需要判断标志位了。


    2.AD多通道

    xxxxx
    该实操使用4个AD转换通道,第一个通道(绿色)还是电位器接在PA0,其他三个(黄色)为三个传感器模块:光敏、热敏、反射式红外传感器PA1、PA2、PA3。注意传感器AO为模拟量输出,DO为数字输出。
    》多通道首先想到了之前的扫描模式,方法不错,但要用到DMA来解决数据覆盖的问题,这里还没学DMA。
    》还有一种想法是一个通道转换完成后手动转运出数据不就行了,为啥要DMA?看似简单但实际操作会遇到问题,问题一:扫描模式下,启动列表后,里面的每一个单独的通道转换完成之后不会产生任何标志位,也不会触发中断,无法知道某个通道是否转换完毕,只有在整个列表转换完成后才会产生一次EOC标志位,才可触发中断;问题二:AD转换十分快速(几us/通道),手动转运困难。不过也不代表一定不行,可使用间断模式,扫描时每转换一个通道暂停一次,不推荐效率低。
    》演示代码的方法:在上面的单次转换、非扫描模式基础上,在每次触发转换之前,手动更改下列表第一个位置的通道,比如第一次转换先写入通道哦,之后触发、等待、读值;第二次转换把通道0改为通道1…。
    更改1:

    //删除AD_Init()里的
    ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
    

    更改2:

    //下面的AD_GetValue()函数变化为可改变通道的:
    uint16_t AD_GetValue(uint8_t ADC_Channel){
    	ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);//通道可改
    	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发,ADC开始转换
    	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);//ADC转换需要一定时间
    	return ADC_GetConversionValue(ADC1);//获取转换值
    }
    

    更改3:

    //main.c
    #include "stm32f10x.h"   // Device header
    #include "Delay.h"   
    #include "OLED.h"
    #include "AD.h"
    
    uint16_t AD0,AD1,AD2,AD3;//表示四个ADC输入通道的转换结果的接收变量
    
    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,AD3,4);
    		OLED_ShowNum(4,5,AD3,4);
    		
    		Delay_ms(100);
    	}
    }
    

    总结

    遇到挫折,要有勇往直前的信念,马上行动,坚持到底,决不放弃,成功者决不放弃,放弃者绝不会成功。成功的道路上,肯定会有失败;对于失败,我们要正确地看待和对待,不怕失败者,则必成功;怕失败者,则一无是处,会更失败。
    今天的学习分享到此就结束了,我们下次再见!!
    往期精彩:
    STM32定时中断
    STM32外部中断
    STM32GPIO精讲

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 ADC模数转换器学习指南

    发表评论