前言

上一篇暂时结束了定时器的章节,还有一个高级定时器的部分,这个缓一缓吧,先换个片上外设来玩一玩,本文开始介绍STM32F407的又一个片上外设,ADC转换器,细细想来,GPIO的输入、输出、复用、通用都用过了,唯独一个模拟功能还没用过,而本片主角——ADC,一个模拟量数字量转换的片上外设,它的作用就是将GPIO口输入的模拟量转换成CPU内核可以识别的数字量。还是之前的模式,先简单做个概述,然后分析框图,介绍寄存器,最后编程实现一两个小需求。

ADC介绍

ADC概述

概述这个东西我一向是不擅长的,这个活儿还是交给AI来完成。

通过GPT的介绍,只需要知道一下几点即可:
1.ADC是片上外设;2.ADC是模拟数字转换器;3.有很多功能可以配置。

ADC的数量

知道ADC是片上外设后,按照以往的思路,肯定是要知道他的数量的,这里还是去到数据手册的对应位置,如下图所示,STM32F407一共有3个ADC一共16个外部通道;说起通道,联想一下之前的通用定时器里面也有通道,那么ADC的通道和定时器的通道有哪些异同呢。

来看下图,这就是ADC的通道,可以发现其中大部分和定时器的通道一样,也是借助GPIO口实现的,但是也有例外,下面的三个,分别是温度传感器(片内温度)、内部参考电压 VREFINT 、温度传感器或 VBAT这里的三个通道都是片内的不需要使用GPIO,所以在配置过程中可以少一步。

除了上面这个不同点,ADC的通道与定时器的通道还有一些不同的地方,其一就是定时器的通道都是在引脚映射表查询的,但是ADC的通道在引脚映射表是差不到的,原因在于,引脚复用表记录的是GPIO的复用功能,而ADC需要使用的是GPIO的模拟功能;那么问题来了,ADC的通道与GPIO的关系怎么查询呢?
在之前的复用表之前,有一个表7,里面记录所有GPIO管脚的所有功能。需要使用时在此处来查询即可。
注意下图中的写法ADC12_IN4的含义是指ADC1的通道4和ADC2的通道4都是这个引脚。

ADC的特性

  1. STM32F407的ADC最高支持12bit的分辨率,也叫做精度;通俗来说就是可以将3.3V的电压等分成2^12=4096份,外部的模拟量通过各种传感器转换成电压值,最后ADC根据这个电压值和对应公式来计算外界的温度、湿度、光强、声音大小、磁场大小、雨量等等模拟量。STM32的ADC可配置 12 位、 10 位、 8 位或 6 位分辨率,切可独立设置各通道采样时间。
  2. 这些通道的 A/D 转换可在单次、连续、扫描或不连续采样模式下进行;也就是说,ADC的转换模式有多种,可单次转换,也可以连续一直转换,还可以按照列表扫描转换,也可以在转换列表里面跳着转换,这个在后面的框图和寄存器中细说,这里做个了解即可。
  3. ADC 电源要求:全速运行时为 2.4 V 到 3.6 V,慢速运行时为 1.8 V,这个我们来看一下数据手册的具体描述,如下图示,当电源电压在1.8-2.4V时,最大的时钟频率是18MHZ;当电源电压在2.4-3.6V时,最大的时钟频率可以到36Mhz。需要注意,这里的最大时钟频率不等于最大转换频率,STM32F407的 ADC 最大的转换速率为 2.4Mhz,也就是转换时间为 0.41us(在 ADCCLK=36M,采样周期为 3 个 ADC 时钟下得到),不要让 ADC 的时钟超过 36M,否则将导致结果准确度下降。

4.ADC可以采集的电压范围 VREF- <= VIN <= VREF+ ,也正是由于电源电压的限制,STM32的内置ADC是无法直接采集负电压的值,需要做对应的辅助电路才可以。

5.STM32的ADC采用的是逐次趋近型模数转换器;在ADC的转换过程中,最常用的两种转换方式就是并联比较型和逐次逼近型,关于二者的描述如下:

关于这两者的具体转换过程,大家可以去B站搜一下或者找到自己的微机原理,模数转换那一章应该是有介绍的。

6.ADC 的结果存储在一个左对齐或右对齐的 16 位数据寄存器中。
ADC的结果范围:0~4095 ——12bit
可以存储在高12位,高12位需要注意,有符号位,需要处理。

也可以存储在低12位,选择右对齐,一般使用这个存储方式。

ADC框图

框图如下图所示,一眼看去,这个框图也是十分复杂的。还是拆开来看吧。

芯片外部框图

先来看框图的上半部分,如下图,先从芯片外开始看,上半部分的外部引脚一共有4+16个;16个就是前面提到的16个外部输入通道,对应着不同的管脚,需要在模拟功能中去查找;

剩4下个是上图左边红框中的Vref+、Vref-、VDDA以及VSSA;上面提到过一嘴,VREF+和VREF-是ADC模块的参考电压,它们的作用是确定ADC模块的转换范围。VDDA和VSSA则是ADC模块的电源和地电压,它们的作用是保证ADC模块的稳定工作。因此,在使用STM32的ADC模块进行模数转换时,需要同时关注这四个参考电压的作用,以保证ADC模块的工作正常和精度高。

芯片内部框图

再之后就是绿色框对应的3个内部输入通道,具体的对应关系如下:

再就是ADC的时钟,前面提到了,STM32F407的ADC最大的时钟是36MHZ,通过数据手册,可以看出ADC挂接在APB2上,APB2的时钟默认是84MHZ,这显然超过了ADC的最大速率,所以再ADC的时钟配置中肯定会有分频的配置,具体的配置方式留到后面寄存器部分再说。

转换部分框图

搞明白了外部框图构成后,到了ADC的核心了,转换部分,将框图进一步简化如下:

首先,输入通道进来后会经过红框位置的选择器,出来有两个选择,分别进入了注入通道和规则通道;这明显是两个模式,而且对应通道还标着数量,注入通道是4个,规则通道是16个;
先看注入通道,信号进来后,经过转换器的转换,直接输出到了注入数据寄存器,而且下面写着(4x16位)也就是说,这个组别可以一次性转换四个输入通道,四个转换结果会分别存放在四个不同的寄存器中,再使用中直接读取不同寄存器的值就可以了。
而规则通道,信号进来后,经过转换是去到了一个16位的规则数据寄存器,也就是说,虽然规则组有16个通道,但是每次只能转换其中一个通道的数据,至于具体的先转换谁,再转换谁这个肯定是可以程序配置的。
一对比就可以发现,注入组的转换速度明显会比规则组快,但是它只有四个通道,当需要转换数量较多的数据时,注入组显然不太适合。实际使用中注入组通常是在常规组转换过程中插入一些额外的转换,以实现对某些通道的快速采样。而且需要注意的是:注入组的转换由软件触发,需要通过软件配置触发源、采样时间和采样周期等参数。注入组的转换顺序是由通道的优先级决定的,优先级越高的通道越先进行转换。
而规则组是最常用的转换方式,它可以根据硬件触发信号进行转换,并按照通道的顺序依次进行转换。规则组的转换由硬件触发信号触发,可以通过软件配置触发源、采样时间和采样周期等参数。规则组的转换顺序是按照通道的顺序依次进行的,可以通过软件配置通道的顺序和转换方式来实现不同的转换需求。
总的来说,注入组适用于需要快速采样某些通道的场景,而规则组适用于常规的转换需求。在使用时,需要根据具体的应用场景来选择使用注入组或规则组,并根据需求进行软件配置。

状态输出部分

按照以前的片上外设的套路,再完成转换、转换失败等等这些特殊位置肯定是会有对应的标志位用来判断或者作为中断输入信号的,如下图,也是上半部分框图的最后一部分,就是各种状态标志位,首先第一种标志就是来自两个数据寄存器的转换结束标志,这个标志位可以用于编程时读取数据的时刻到来的标志,或者直接进入中断获取数据,除此之外,注入转换结束也可以直接产生一个标志位。
而最上面的DMA溢出与下面紫色框的DMA请求是配对,也是一个标志,关于DMA的内容,咱们放到下一趴中分析。
最后还有一个标志位是模拟看门狗事件的,具体用法大致是,可以设置转换结果的上限和下限值,当数据不在这个范围内的时候就会产生对应的标志,执行一定的操作。

条件触发框图

注意到刚刚的框图还有两个来自下方的箭头,分别指向了注入通道和规则通道,如下图的红色线所示,在介绍规则组和注入组的时候,提到过一嘴,注入组是需要软件触发的,触发条件也可以由软件配置,这部分框图就是告诉我们可以配置为触发ADC的条件有哪些;可以看见了啊,触发信号还是很多的,定时器的、外部中断的很多都可以作为触发信号。
规则通道和注入通道都分别有一个与门(EXTEN、JEXTEN),猜一下这个与门是干啥的,很简单哈名字中都包含了一个EN,这两个与门就是使能位,用来控制是否需要借助这些软件触发条件来实现转换;实际使用中,规则通道由于有硬件触发的机制,所以一般都不会使用这个条件触发的。

好,关于框图的介绍就到这,接下来来看看寄存器部分。

寄存器介绍

规则转换需要使用到的寄存器大致如下,详细的寄存器描述大家自己去手册的11.13 ADC 寄存器查看。
1.ADC 状态寄存器 (ADC_SR)
写法:ADCX->SR
X:具体到哪一个ADC
功能:获取ADC的状态
位 1 EOC:规则通道转换结束 (Regular channel end of conversion)
为1:表示通道转换结束
为0:表示通道转换没有结束

2.ADC 控制寄存器 1 (ADC_CR1)
写法:ADCX->CR1
功能:
位 5 EOCIE:EOC 中断使能 (Interrupt enable for EOC)
位 8 SCAN:扫描模式 (Scan mode)
位 25:24 RES[1:0]:分辨率 (Resolution)

3.ADC 控制寄存器 2 (ADC_CR2)
写法:ADCX->CR1
功能:
位 0 ADON:A/D 转换器开启 / 关闭 (A/D Converter ON / OFF)
位 1 CONT:连续转换 (Continuous conversion)
在这里插入图片描述

位 11 ALIGN:数据对齐 (Data alignment)
位 10 EOCS: 结束转换选择 (End of conversion selection)
0:在每个规则转换序列结束时将 EOC 位置 1。溢出检测仅在 DMA=1 时使能。
1:在每个规则转换结束时将 EOC 位置 1。使能溢出检测
位 29:28 EXTEN:规则通道的外部触发使能 (External trigger enable for regular channels)
位 30 SWSTART:开始转换规则通道 (Start conversion of regular channels)

3.ADC 采样时间寄存器 1 (ADC_SMPR1)
ADC 采样时间寄存器 2 (ADC_SMPR2)

写法:ADCX->SMPR1
功能:设置采样时间
000:3 个周期
001:15 个周期
010:28 个周期
011:56 个周期
100:84 个周期
101:112 个周期
110:144 个周期
111:480 个周期
一般选择最慢的采样频率

4.ADC 规则序列寄存器 1 (ADC_SQR1)
ADC 规则序列寄存器 2 (ADC_SQR2)
ADC 规则序列寄存器 3 (ADC_SQR3)

写法:ADCX->SQR1
功能:写入转换序列,给定转换通道数
位 23:20 L[3:0]:规则通道序列长度 (Regular channel sequence length)
位 4:0 SQ1[4:0]:规则序列中的第一次转换 (1st conversion in regular sequence)
转换的通道,按顺序放入

5.ADC 规则数据寄存器 (ADC_DR)
写法:ADC1->DR
功能:获取adc转换完成的数字量
u16 data= ADC1->DR;

6.ADC 通用状态寄存器 (ADC_CSR)
写法:ADC->CSR
作用:去读ADC的状态
特点:3个ADC都可以表示

7.ADC 通用控制寄存器 (ADC_CCR)
写法:ADC->CCR
位 17:16 ADCPRE:ADC 预分频器 (ADC prescaler)
位 23 TSVREFE:温度传感器和 VREFINT 使能 (Temperature sensor and VREFINT enable)

编程思路

前面的片上外设看了框图都有一个比较清晰的编程思路,但是ADC的看完似乎还差点意思,在ADC特性的介绍的第2点中,提到了一个模式选择的问题,有单次、连续、扫描模式还有不连续采样模式,这个部分在框图中没有体现出来啊,还有关于规则组的16个通道到底先转换谁后转换谁也没有一个具体的说法,你这你这,水文。
我知道你很迷但你先别迷,听我狡辩。

模式选择

关于每个模式的具体工作模式这里先做一个大致的介绍:

其中单次转换和连续转换是由ADC 控制寄存器 2 (ADC_CR2)的位1控制的。

单次转换模式
特点:同一个通道转换一次
CONT为0时选择的时单次转换模式

连续转换模式
特点:同一个通道连续转换
CONT为1时选择的时连续转换模式

而扫描模式 通过将 ADC_CR1 寄存器中的 SCAN 位置 1 来选择扫描模式
扫描转换通道中的所有序列,可以按顺序往下执行
扫描模式和单次转换或者连续转换连用
单次扫描模式 //只扫描一次转换序列内的通道,如下图,从片上温度扫描到光敏后就不会继续再扫描了。

连续扫描模式 / /连续赛秒转换序列内的通道。而连续扫描模式,就是从片上温度依次转换到光敏后,又从头开始从片上温度转换到光敏,一直转换,这个过程中需要设计好程序,保证读取的数据是对应通道的,不然很有可能会造成数据紊乱。

至于不连续采样模式使用的较少,用于多次转换,序列比较长的时候,可以理解为在上面的这个序列表内,每次只转换一部分,多次转换后才会把整个序列转换完毕,也就是说不是每次扫描都会转换整个序列表的全部成员。

规则通道的序列表

关于规则转换的16个通道,其实可以把他理解为一个表格,我们在需要转换的时候,把对应的通道写入表格的位置就可以了,如下图

具体的写入过程的寄存器就是ADC_SQR1它的第20到23位就是用来告诉系统序列内一共有多少个要转换的通道的。

而下面的三个同名的寄存器ADC 规则序列寄存器 1、2、3 (ADC_SQR1、2、3)就是用来写入对应的通道值的,例如假设我们第一个要转换的是片内温度对应的通道数是16;那么就应该在ADC_SAR3的SQ1也就是0-4位写入“0x100”(16)。
依次类推,第二次要转换参考电压IN17,就需要将17(0x101)写入ADC_SAR3的SQ2

伪代码

好了,搞清楚了这几点,差不多可以总结出对应的ADC配置流程了,伪代码如下:

ADC的初始化部分
{
   //打开时钟  IO口   ADC的时钟

   IO控制器
   //模拟功能

   ADC控制器
   //分辨率
   //扫描模式
   //单次或连续
   //数据对齐
   //禁止外部触发
   //设置采样时间
   //设置转换数量
   //设置转换序列
   //ADC分频
   //使能ADC
}
获取ADC值函数
{
   //开启转换通道
   //等待标志位置1
   //读取数据寄存器
}

实际需求

需求1

使用STM32的片上温度计,测出片内的温度,
需求分析,由于是使用内部的通道,所以不需要使用到GPIO,直接查找对应的ADC以及模块即可,之前的介绍中提到过,内部温度是只能使用ADC1的,因此在做配置的时候不能选择ADC2或者ADC3。
1.分辨率,还是选择12bit,当然也可以降低点,一个温度检测确实也用不了这么高的精度;
2.扫描模式,由于只开了一个温度通道就不需要使用扫描模式了;
3.然后是连续模式和单次模式,这里其实无所谓的,选谁都行;
4.再然后就是选择右对齐模式;
5.再然后就是禁止外部触发;
6.然后是采样时间最短的采样周期是3个周期,但是温度传感器用不了这么高的采样速率;

7.将模拟通道的16写入转换通道的0;
8.设置时钟分频,由于ADC的最高支持速度是36MHZ,而时钟线的频率是84MHZ,这里最少也需要四分频;
9.记得使能温度传感器;
//以下是接收部分
10.使用的单次模式,所以每次转换都需要使用软件触发来开启ADC的转换,开启后就可以判断转换完成,进而读出数据了。
代码如下


/*******************************************
*函数名    :ADC1_IN16_Init()
*函数功能  :ADC1通道16初始化配置(内部温度传感器)
*函数参数  :无
*函数返回值:无
*函数描述  :片上温度传感器
*********************************************/
void ADC1_IN16_Init(void)
{

/*-------------打开对应时钟-----------------------------------------------------------------*/
    RCC->APB2ENR |=(1<<8);//打开ADC1的时钟
/*--------------初始化GPIO为复用模式--------------------------------------------------------*/
    //无
/*--------------ADC控制器-------------------------------------------------------------------*/    
    ADC1->CR1 &=~(3<<24);//设置分辨率为12位
    ADC1->CR1 &=~(1<<8);//关闭扫描模式
    //CR2
    ADC1->CR2 &=~(1<<1);//单次转换模式
    ADC1->CR2 &=~(1<<11);//右对齐
    ADC1->CR2 &=~(3<<28);//禁止外部触发
    //SMPR1
    ADC1->SMPR1 &=~(7<<18);//清零
    ADC1->SMPR1 |=(7<<18);//480个周期
    //SQR
    ADC1->SQR1 &=~(0xf<<20);//设置为一次转换
    ADC1->SQR3 &=~(0x1f<<0);
    ADC1->SQR3 |=(16<<0);//将模拟通道的16写入转换通道的0


    //CCR注意是ADC不是ADCX
    ADC->CCR &= ~(3<<16);
    ADC->CCR |= (1<<16);         //4分频84/4=21MHZ
    ADC->CCR|=1<<23; //使能内部温度传感器
    //ADC模块使能
    ADC1->CR2 |= (1<<0);
}

/*******************************************
*函数名    :Get_Temprate()
*函数功能  :获取温度值
*函数参数  :无
*函数返回值:short 温度
*函数描述  ://得到温度值
//返回值:温度值(扩大了 100 倍,单位:℃.)
*********************************************/
short Get_Temprate(void)
{
    u32 adcx;
    short result;
    double temperate;
    ADC1->CR2 |=(1<<30);//开启ADC转换
    //等待转换完成
    while(!(ADC1->SR & (1<<1)));
    //获取转换数据
    adcx = ADC1->DR;
    temperate=(float)adcx*(3.3/4096); //电压值
    temperate=(temperate-0.76)/0.0025+25; //转换为温度值
    result=temperate*=100; //扩大 100 倍.
    return result;
}

现象

需求2

使用光敏电阻获取外界的光照强度,要求和温度传感器一起,都使用ADC1,100ms采集一次
需求分析:
由于使用到了同一个ADC的两个通道,这时就需要使用到扫描模式了,而要求是100ms采集一次,这里就有很多种处理方案了,可以使用注入组或者规则组,配合定时器触发;也可以使用规则组一直连续触发,这时需要处理好读取的顺序,不然会造成数据紊乱,这里笔者选用的是规则组的单次转换,使用软件触发,然后放入100ms的时间片。
具体的代码如下:

typedef struct 
{
    u16 Light;
    double Temp;
}ADC1_Get;

/*******************************************
*函数名    :ADC1_IN12_IN16_Init
*函数功能  :ADC1通道3初始化配置
*函数参数  :无
*函数返回值:无
*函数描述  :PC2----ADC1_IN12
*********************************************/
void ADC1_IN12_IN16_Init(void)
{
/*-------------打开对应时钟-----------------------------------------------------------------*/
    RCC->AHB1ENR |=(1<<2);//打开GPIOC的时钟
    RCC->APB2ENR |=(1<<8);//打开ADC1的时钟
/*--------------初始化GPIO为复用模式--------------------------------------------------------*/
    GPIOC->MODER &=~(3<<4);//清零
    GPIOC->MODER |=(3<<4);//设置为模拟模式
/*--------------ADC控制器-------------------------------------------------------------------*/    
    ADC1->CR1 &=~(3<<24);//设置分辨率为12位
    ADC1->CR1 |=(1<<8);//开启扫描模式
    //CR2
    //ADC1->CR2 |=(1<<1);//连续转换模式
    ADC1->CR2 &=~(1<<1);//单次转换模式
    ADC1->CR2 &=~(1<<11);//右对齐
    ADC1->CR2 |=(1<<10);//每个规则转换结束时将EOC置1
    ADC1->CR2 &=~(3<<28);//禁止外部触发
    //SMPR1
    ADC1->SMPR1 &=~(7<<6);//清零
    ADC1->SMPR1 |=(7<<6);//480个周期
    ADC1->SMPR1 &=~(7<<18);//清零
    ADC1->SMPR1 |=(7<<18);//480个周期
    //SQR
    ADC1->SQR1 &=~(0xf<<20);//清零
    ADC1->SQR1 |=(0x1<<20);//设置为2次转换
    //转换序列
    ADC1->SQR3 &=~(0x1f<<0);
    ADC1->SQR3 |=(12<<0);//将模拟通道的12写入转换通道的1
    ADC1->SQR3 &=~(0x1f<<5);
    ADC1->SQR3 |=(16<<5);//将模拟通道的16写入转换通道的2

    //CCR注意是ADC不是ADCX
    ADC->CCR &= ~(3<<16);
    ADC->CCR |= (1<<16);         //4分频
    ADC->CCR|=1<<23; //使能内部温度传感器
    //ADC模块使能
    ADC1->CR2 |= (1<<0);
}

/*******************************************
*函数名    :Get_Tem_Lux
*函数功能  :获取ADC1通道12、16
*函数参数  :无
*函数返回值:data
*函数描述  :PC2----ADC1_IN12,片上温度——ADC1_IN16
*********************************************/
ADC1_Get Get_Tem_Lux(void)
{
    ADC1_Get data;
    u16 adcx;
    ADC1->CR2 |=(1<<30);//开启ADC转换
    //等待转换完成
    while(!(ADC1->SR & (1<<1)));//注意获取顺序
    //获取转换数据
    data.Light = ADC1->DR;//第一次获取光照值
    //等待转换完成
    while(!(ADC1->SR & (1<<1)));
    //获取转换数据
    adcx = ADC1->DR;//第二次获取的温度值
    data.Temp=(float)adcx*(3.3/4096); //电压值
    data.Temp=(data.Temp-0.76)/0.0025+25; //转换为温度值
    data.Temp=data.Temp*=100; //扩大 100 倍.
    return data;
}

完整代码就不贴了,剩下就需要在100ms的中断调用就可以了。

效果

emm,笔者的这个板子应该是遇到翻新芯片了,片上温度直接起飞,通样的代码,另一个板子就是25℃左右。

注:
实际ADC使用过程中,很少会直接单次采样的,一般都是会采用采用多次后,进行滑动平均滤波或者平均值滤波后再使用。

总结

关于ADC的介绍就记录到此,文中如有不足和错误欢迎批评指正。

M4系列目录

1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信