stm32F103——ADC与DMA(按键与光敏电阻)

ADC的基本介绍

ADC的基本定义

Analog-to-Digital Converter的缩写。指模/数转换器或者模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件。

典型的模拟数字转换器将模拟信号转换为表示一定比例电压值的数字信号。

我们先来看ADC的工作原理:

 

        通道选择:1个ADC模块具有多个通道,不同的通道在不同的引脚上。所以,我们要采集哪个引脚上面的模拟量,需要我们去选择对应ADC通道。

        采保电容:采样保持电容。当开关闭合的时候,给采保电容进行充电,然后在规定的时间内断开开关,然后将采保电容上面的模拟量电压进行转换为数字量。这个就是ADC的采集原理。

        采样时间:当开关闭合的时候,给采保电容进行充电,然后在规定的时间内断开开关,然后将采保电容上面的电压进行转换。这个规定时间就是采样时间。采样时间是会变化的。

        转换时间:对采保电容中的存的电压进行转换,转换模块会花费一定的时间,这个就是转换时间。然后把转换的结果存入缓冲器,供程序员读取使用。转换时间是固定不可变的。

        注意:采样时间并不是固定不变的,采样时间需要做实验,根据实验结果来调整采样时间。

        采样时间不能过长,也不能过短。当采样数据过长,由于输入量是一直在变化的,所以就会导致采保电容上面的电压也是一直在变化的,如果电压值一直在变化我们采集出来的值也不是一个真实反映情况的值。我们采集出来的值,是需要有代表性作用的。

        当采样时间过短,假如输入电压是3V,那么当给采保电容充电到1.5V的时候,开关就断开了,此时就不能正确的反应输入的电压了。而且还有采样信号源内阻的问题,说白了就是对这个采保电容充电的电流够不够大,只有充电的电流大了,才能在最短的时间内充满,由于电容充满电后相当于开路,此时ADC内阻无穷大。如果充电的电流非常小,那么充电时间长,电容在充电的时候阻抗非常小,此时ADC内阻就非常小。如果此时恰恰配置的采样时间又很小,结果只有一个,采样不准确。我们经常看到,有的设计方案,做电压采样,分压电阻设计的阻值非常大,当这样的大阻抗遇到高速采样的时候,小电阻钳位大电阻,此时R1、R2电阻的分压就不准确了,那么采样就有可能会不准确。如下图:

        大家看上面的R1、R2设置大了,那么充电电流就小了,那么R1、R2的阻值设计小了,充电电流大了,那么带来的问题功耗就大了,当需要高速采样的时候,就需要注意这个阻值分配的问题,对功耗要求比较高的产品,如可穿戴产品,蓝牙耳机,智能手表等,这个就需要大的阻值了,一般情况下功耗要求不高的场合,我们基本上设计在1-2mA,所以从此可以得出一个结论,采样时间不是越快越好,也不是越慢越好,恰到好处即可,需要你去实际调试。

        如何从带有干扰杂波的信号中提取到有用的信号呢,一般情况下,我们会根据系统的特性进行定点采样,即避开干扰区来采样提取有用的信号,再配软件或者硬件滤波手段来提取有用信号。

        所以采集一个信号就有2个时间了:采集时间 + 转换时间,这两个时间也就反应了采集速度有多快,能不能快。这两个时间决定了ADC采样的速度问题,当需要高速AD采样的时候,这两个时间尤为重要,因为它决定了采样速度。

采保电容得到了准确的采样信号,就一定意味着转换结果准确吗?

        答案肯定是NO,这个和ADC模块的参考电压也有关系,参考电压就像一标准样品一样,举例来讲:老板说照着这个样品给我做100个,那首先前提是你的样品得准确,如果样品都不准确,那做出来的东西也必然会有偏差。

ADC的转换原理

逐次比较型ADC
        一般单片机里面都是用逐次比较型ADC转换。其基本原理是从高位到低位逐位试探比较,好像用天平称物体,从重到轻逐级增减砝码进行试探。逐次逼近法转换过程是:初始化时将逐次逼近寄存器各位清零;转换开始时,先将逐次逼近寄存器最高位置1,送入D/A转换器,经D/A转换后生成的模拟量送入比较器,称为 Vo,与送入比较器的待转换的模拟量Vi进行比较,若Vo<Vi,该位1被保留,否则被清除。然后再置逐次逼近寄存器次高位为1,将寄存器中新的数字量送D/A转换器,输出的 Vo再与Vi比较,若Vo<Vi,该位1被保留,否则被清除。重复此过程,直至逼近寄存器最低位。转换结束后,将逐次逼近寄存器中的数字量送入缓冲寄存器,得到数字量的输出。

      

        上图是一个8位逐次逼近型ADC的框图,“输入的模拟量”是输入电压信号,“START”用来控制ADC启动转换,“CLOCK”是ADC模块的输入时钟,“EOC”是ADC转换结束信号,“OE”是ADC转换结果输出允许信号,“VREF”是参考电压。

        随着时钟信号的输入,启动信号的开始,控制模块会逐次控制逐次比较寄存器产生不同的数据,数据产生后会送给D/A转换器(即DAC),D/A转换器会依据参考电压,把这个数字量转化为模拟量送给比较器,比较器比较D/A转换器送出来的模拟量和输入模拟量的大小,产生的结果给控制单元电路,控制单元电路根据上一次的结果再次控制产生不同的数据,让D/A变成模拟量,再去比较,以此这样循环,每次比较,比较器会得出一个结果高或者低,根据这个结果决定当前产生的数字量是大了还是小了,一次一次的比较,找到那个和输入模拟量最接近的数字量,最后把这个数字量控制送到输出缓冲器,并且控制送出EOC输出转换完成信号,这就是一个大致的逐次逼近工作原理。

        详细过程,请查看视频:【张飞实战电子】剖析影响ADC结果关键因素-第二集_哔哩哔哩_bilibili

        从上面的描述中,我们抓住一个重点是:D/A转换器会依据参考电,把生成的数字量变为模拟量,在转换的时候必须需要有一个参考电压,这个电压就是我们AD模块的参考电压,那么大家试想,如果参考电压都不稳定的话,转出来的模拟量是不是也不会稳定,那么和输入模拟量比较的时候,比较的结果也就可能会发生偏差,造成错误的比较结果。

       

         那怎么来保证这个参考电压比较稳定呢?

        1.我们可以在参考电压引脚附近就近放置电容(一大一小,大的储能,小的滤波);

        2.可以在参考电源前端串一个小电感再加电容。如图所示,这两种方法比较常见,也比较便宜,大家可以参考.

 总结,ADC的参考电压是非常重要的,所以参考电压精确度不容忽略,要尽可能地使参考电压稳定,不受干扰。

分辨率

         分辨率有8位、10位、12位、16位。那么这到底是什么意思呢?

        假设是12位的分辨率,其对应3.3V。那么就是将3.3V分割成2^12份,最小的电压就是\frac{3.3V}{2^1^2}(即:0.0008057V),它用0000 0000 0000 0001来表示(因为是12位)。\frac{3.3V}{2^1^2} *  2  = \frac{3.3V}{2^1^1},它用0000 0000 0000 0010来表示(因为左移1位相当于乘以2)。所以1000 0000 0000 0000就表示电压值,\frac{3.3V}{2^1^2}  *  2^11 = \frac{3.3V}{2},即表示1.65V电压。110 0000 0000 0001 就表示电压值 \frac{3.3V}{2} +  \frac{3.3V}{2^2}。由此可知,12位的分辨率可以表示0 ~ 3.3V之间的很多电压值。所以,分辨率越高,表达的电压就越精确。

        单片机中通常是10位、12位分辨率的比较多,如果我们需要更精确的ADC,那么就要使用外挂的ADC芯片了。

电路:

        按下不同按键后,由于分压电阻并联,导致分压改变,ADkey的电压改变,经过ADC转换后,将模拟电压值转换成数字量。如下图

ADkey引脚的模拟电压值转换为数字量(十进制)的计算方法

                                                (ADkey电压值 / 3.3v) *  4095

例如:AD_K2的引脚电压值为0.396v(模拟量),那么(0.396 / 3.3) * 4095 = 491.4(数字量)

           如果引脚电压值为3.3v(模拟量),那么(3.3 / 3.3) * 4095 = 4095(数字量)

 注意:表格中只是理论计算值,实际上电阻阻值存在1%~5%的误差。所以,实际上分得的电压与计算值也就存在误差。

单次扫描:只转换一次模拟量。

连续扫描:连续循环的转换各个通道的模拟量。

左右对齐:ADC是12位的,但是由于内存是以字节(8位)来存储的,所以保持ADC数值的寄存器是16个位。ADC的数值只有12个位,所以还剩下4个位空着。右对齐:从寄存器的第0位开始放数据。最后一个数据放在第11位。左对齐:从寄存器的第4位开始放数据。最后一个数据放在第15位。如下图:

 所以,在左对齐中把寄存器中的值右移四位就得到右对齐的数据了。我们一般用的是右对齐数据

通道之间的采样间隔(可编程):我们可以自己去设置,采样的间隔时间。

采样时间和转换时间的计算:

         我们可以看到,ADC时钟频率为56Mhz的时候,是1us,但是,ADC时钟频率为72Mhz的时候,却是1.17us。频率增加了,转换时间反而变成了。这是为什么?

         

         实际上,ADC模块的最大频率只有14Mhz,当ADC外设的时钟线的频率为72Mhz的时候,只能6分频,频率才能小于14Mhz(即:72/6 = 12Mhz),此时ADC模块才能正常工作。但是,ADC模块的却只在12Mhz下工作。而当ADC外设的时钟线的频率为56Mhz的时候,56/4 = 14Mhz。此时ADC模块的在14Mhz下工作,因为14Mhz > 12Mhz,所以,转换时间比较快。

 当ADC外设时钟为56Mhz的时候:(ADC模块工作频率为14Mhz)

        采样时间为:\frac{1}{14M}

        转换时间为:1.5 * \frac{1}{14M}  +  12.5 * \frac{1}{14M}   =   14 * \frac{1}{14M}  = 1us

 当ADC外设时钟为72Mhz的时候:(ADC模块工作频率为12Mhz)

        采样时间为:\frac{1}{12M}

        转换时间为:1.5* \frac{1}{12M}  +  12.5 * \frac{1}{12M}   =   14 * \frac{1}{12M}  = 1.17us

注意:采样时间系数1.5,是可以通过编程更改的

 

         ADC模块采样,是不需要CPU参与的(除非设置了ADC采样时产生中断)。我们在ADC采样到数据后,一般是使用DMA将数据存储到内存中,因为,如果每次都用中断来进行ADC采样数据和数据存储的话,那么CPU资源就会被ADC采样和存储数据操作给霸占,这样会浪费大量的CPU资源。(重点)

         注意:ADC是不断循环的对通道进行采样。但是,DMA是来一个数据就搬运一次,而不是不管有没有数据来,它都在哪里循环搬运。

自校准:ADC在使能之后是需要进行校准的。否则,采样的数值不准确。

模拟数字转换规则


    1>规则转换:按照程序设定好的依次进行转换(即:几秒钟转换一次)
    2>注入转换:类似于中断,当注入转换触发时,先进行注入转换,再继续规则转换(即:当注入转换触发时时,先转换注入转换的数据,转换完注入转换触发的数据后,在来执行程序中已经规定好的规则转换。也就是说,注入转换会抢占规则转换)

注意:规则转换和注入转换都有外部触发选项

 
 

ADC怎样对读取到的数据进行处理:

我们现在需要读取按键产生的分压值和光敏电阻产生的分压值,如果对这两个通道进行循环扫描(即:读取一次ADCkey,下一次读取光敏电阻,然后反复循环)。但是数据该怎么处理呢?

我们可以建立一个二维数组data[key][guan_ming],如下:

         每个外设都读取50个数值,把它们放在二维数组data[key][guan_ming]中,然后将这50个数求平均值,此时这个平均值才是较为接近的正确的值。

ADC内部框图

         我们可以看到注入转换数据寄存器(4x16bit)而规则转换数据寄存器(16bit)说明注入转换数据寄存器有4个16bit的寄存器,而规则转换数据寄存器只有1个16bit的寄存器。

        

 

 ​​​​​​模拟看门狗事件:我们设置一个数值范围   低门限~高门限,如果ADC读到的值不在这个数值范围内,那么模拟看门狗就会触发一个中断/事件。然后程序进行相应的处理。

编程步骤:

ADC采集AD按键和光敏电阻的电压值,编程步骤:
1,打开时钟--------ADC1, GPIOC, AFIO, DMA1
2.,ADC预分频器-------6分频-----ADCCLK = 12MHZ
3,初始化GPIOC
-------GPIO_Pin_0 | GPIO_Pin_1
-------模拟输入
4.初始化ADC1
-------模式-----ADC独立模式
-------是否扫描-----是
-------是否连续转换----是
-------对齐方式---右对齐
-------是否使用外部触发------否
-------通道数目-----2
5,使能ADC的DMA
6,DMA初始化
-------外设基地址--------&ADC 1->DR(即:规则数据寄存器ADC_DR)
-------内存基地址--------二维数组 ADC_DATA[50][2]  (即:二维数组)(50 * 2 = 100个数据)
-------方式----外设作为数据来源
-------外设地址是否递增------否(即:ADC地址----不自增)
-------内存地址是否递增------是(即:二维数组地址----自增 )
-------外设数据传输的宽度-----半字(每次搬运两个字节宽度)(因为2^8 = 256,而ADC采样的数值在0~4095,所以只能使用两个字节(即半字)来表示,2^16 = 65535)
-------内存数据传输的宽度-----半字
-------传输的数目-----100(100个字节)
-------模式-----循环模式
-------内存到内存是否使能----否(如果设置了外设到内存的话,那么内存到内存就要失能)
-------优先级---高

7,使能DMA1通道1(只有DMA1通道1可以搬运ADC1的数据)
8,配置通道10和11的采样顺序和采样时间
9,使能ADC1

10,校准
    ------复位校准寄存器
    ------等待复位成功
    ------开始校准
    ------等待校准成功
11,软件触发ADC(有外部触发ADC和软件触发ADC。外部触发ADC需要发生中断)

库函数

         按键与光敏电阻的引脚分别为PC0和PC1,这两个对于的ADC通道为通道10和通道11

 

 

         设置采样通道、采样通道的顺序Rank与采样时间ADC_SampleTime。采样顺序就是,比如通道11设置为1,通道10设置为2,那么先采集通道11的数据,再采集通道10的数据。采样时间,如下图:

         注意:采样时间越长,采样出来的数据越准确。在12Mhz频率下,最长的239.5个周期就是21us,采样50个数据总共也就花费21* 50 = 1050us的时间(即1.05ms),对于整个按键动作来说,这个时间非常短。

         注意:获取ADC校准寄存器的状态标志位与以往的不一样。它是正在执行校准初始化的过程中时,标志位置1。已经完成校准初始化时,标志位置0。

        注意:ADC要在使能之后,才能开始进行校准。

编写代码


#define ADC_CH 2
#define ADC_data_size 50
uint16_t ADC_DATA[ADC_data_size][ADC_CH];        //用来存储数据的二维数组

void ADC1_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    ADC_InitTypeDef ADC_InitStruct;
    DMA_InitTypeDef DMA_InitStruct;
    // 1,打开时钟----ADC1,GPIOC,AFIO,DMA1 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOC|RCC_APB2Periph_AFIO,ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

    // 2,ADC预分频器---6分频---ADCCLK=12MHZ
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    // 3,初始化GPIOC
    GPIO_InitStruct.GPIO_Pin    =GPIO_Pin_0|GPIO_Pin_1;
    GPIO_InitStruct.GPIO_Mode   =GPIO_Mode_AIN;
    GPIO_Init(GPIOC,&GPIO_InitStruct);

    // 4,初始化ADC1
    ADC_InitStruct.ADC_ContinuousConvMode   =ENABLE;
    ADC_InitStruct.ADC_DataAlign            =ADC_DataAlign_Right;
    ADC_InitStruct.ADC_ExternalTrigConv     =ADC_ExternalTrigConv_None;
    ADC_InitStruct.ADC_Mode                 =ADC_Mode_Independent;
    ADC_InitStruct.ADC_NbrOfChannel         =ADC_CH;        //宏定义ADC_CH = 2
    ADC_InitStruct.ADC_ScanConvMode         =ENABLE;
    ADC_Init(ADC1,&ADC_InitStruct);

    // 5,使能ADC的DMA
    ADC_DMACmd(ADC1,ENABLE);

    // 6,DMA初始化
    DMA_InitStruct.DMA_PeripheralBaseAddr   =(uint32_t)&ADC1->DR;       
    DMA_InitStruct.DMA_MemoryBaseAddr       =(uint32_t)ADC_DATA;  /*这里强转为(uint32_t)类型,是因为在结构体DMA_InitStruct里面,成员DMA_MemoryBaseAddr是uint32_t类型的*/
    DMA_InitStruct.DMA_DIR                  =DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_PeripheralInc        =DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc            =DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize   =DMA_PeripheralDataSize_HalfWord;
    DMA_InitStruct.DMA_MemoryDataSize       =DMA_MemoryDataSize_HalfWord;
    DMA_InitStruct.DMA_BufferSize           =ADC_CH*ADC_data_size;
    DMA_InitStruct.DMA_M2M                  =DMA_M2M_Disable;
    DMA_InitStruct.DMA_Mode                 =DMA_Mode_Circular;
    DMA_InitStruct.DMA_Priority             =DMA_Priority_High;
    DMA_Init(DMA1_Channel1,&DMA_InitStruct);

    // 7,使能DMA1通道1
    DMA_Cmd(DMA1_Channel1, ENABLE);

    // 8,配置通道10和通道11的采样顺序和采样时间
    ADC_RegularChannelConfig(ADC1,ADC_Channel_10, 1,ADC_SampleTime_239Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_11, 2,ADC_SampleTime_239Cycles5);

    // 9,使能ADC1
    ADC_Cmd(ADC1,ENABLE);

    // 10,校准
    ADC_ResetCalibration(ADC1);
    while(SET==ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(SET==ADC_GetCalibrationStatus(ADC1));

    // 11,软件触发ADC
    ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}


uint16_t ADC_RG_Value(void)        //将采集到的50个光敏电阻电压的数据累加起来,然后求平均值
{
    uint32_t SUM=0;
    uint8_t i;
    for(i=0;i<ADC_data_size;i++){
        SUM=SUM+ADC_DATA[i][0];
    }
    return (uint16_t)(SUM/ADC_data_size);
}

uint16_t ADC_KEY_Value(void)        //将采集到的50个按键电压的数据累加起来,然后求平均值
{
    uint32_t SUM=0;
    uint8_t i;
    for(i=0;i<ADC_data_size;i++){
        SUM=SUM+ADC_DATA[i][1];
    }
    return (uint16_t)(SUM/ADC_data_size);
}


int main(void)
{
   
    RCC_ConfigTo72M();
    Systick_Config(72);
    DMA1CH2_Config(SRC,DRC,data_size);
    ADC1_Config();
    while(1){
        printf("ADC_RG_Value=%d\n",ADC_RG_Value());
        printf("ADC_KEY_Value=%d\n",ADC_KEY_Value());

   }
}

注意:DMA搬运数据,是来一个数据搬运一次,而不是,不管来不来数据它都独自一人,在一个孤独的角落默默的搬运。

  • 6
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
在STM32单片机上,可以通过ADC(模数转换器)外部通道实现一个IO多个按键功能的程序。以下是一个实现示例: 首先,我们需要配置ADC的外部通道。使用STM32的外设库函数,将需要用作按键输入的IO引脚配置为模拟输入,并选择一个合适的ADC外部通道进行映射。 接下来,我们需要初始化ADC模块。设置ADC的工作模式、采样时间、转换分辨率等参数,并使能ADC模块。 在主循环中,我们可以使用ADC的外设库函数来进行模数转换。通过读取ADC的转换结果,我们可以得到IO引脚的模拟值。根据不同的模拟值,我们可以判断按键的当前状态。 例如,如果ADC转换结果为0~1000之间的值,我们可以将其定义为按键1按下的状态。如果转换结果为1000~2000之间的值,我们可以将其定义为按键2按下的状态,以此类推。 根据实际情况,我们可以通过增加额外的条件判断,来实现更复杂的按键功能。例如,可以设置一个超时计时器,当某个按键持续按下一段时间后,执行特定的操作。 最后,将按键状态对应的控制动作写入程序中,例如控制LED灯的开关、触发其他外设等。 需要注意的是,由于ADC是模数转换器,其转换精度和稳定性可能受到一些干扰。为了提高系统的稳定性和准确性,我们可以采取一些措施,如加入外部电源滤波电路、校准ADC输入电压等。 综上所述,通过配置ADC的外部通道,并利用ADC的转换结果来判断IO引脚的模拟值,我们可以实现单片机上的一个IO多个按键功能的程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值