那个初夏的夜晚——与ADC的浪漫邂逅
2019-06-17 23:12:43

注:本文属博主学习时所作笔记,内容源大参考于野火的《零死角玩转STM32F103》以及部分网络资料,笔记内容仅作为自己参考,免去频繁查询参考手册的麻烦,如有错误,还请指出!

ADC简介

STM32f103 系列有 3 个 ADC,精度为 12 位,每个 ADC 最多有 16 个外部通道,可测量16个外部和2个内部信号源。其中ADC1 和 ADC2 都有 16 个外部通道,ADC3 根据 CPU 引脚的不同通道数也不同,一般都有8 个外部通道。

ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。

单个ADC框图

电压输入范围

输入电压:$V_{REF-} \leq V_{IN} \leq V_{REF+}$,决定输入电压的引脚:$V_{REF-}, V_{REF+},VDDA,VSSA$,把$VSSA$和$V_{REF-}$接地,$V_{REF+}$和$VDDA$接$3.3V$,得到ADC的输入电压范围为:==0~3.3V==。

VDD: D即device 表示器件的意思,是器件内部的工作电压。

VDDA(A表示 Analog 【模拟】)是模拟电源,当使用到模拟信号的时候,比如AD(模数)或者DA(数模)的时候,系统会使用VDDA的电压作为参考电压来。不要求精准使用的话,可以直接把VDDA和VDD同时接入$3.3V$就行。如果要求精准,则需要做一个稳压电路,再接入VDDA 。

VSS:S即series 表示公共连接的意思,通常指电路公共接地端电压 ,VSSA同理为模拟部分的电源。

对于数字电路来说,VCC是电路的供电电压,VDD是芯片的工作电压(通常$VCC>VDD$),$VSS$是接地点。
例如,对于ARM单片机来说,其供电电压$VCC$一般为5V,一般经过稳压模块将其转换为单片机工作电压$VDD = 3.3V$。

定制电压范围

例如将电压范围设成$-10V$~ $10V$,可以通过下图的附加扩展电路实现:

附加电路

根据基尔霍夫定律(KCL),即节点流入的电流等于流出的电流可以得到:
$$
{(V_{IN}-V_{OUT})\over 2 } + {(3.3V-V_{OUT})\over R_1 } = {V_{OUT} \over R_3}
$$
解得: $V_{OUT}={(V_{IN}+ 10)\over 6}$

在附加电路的条件下,当$V_{IN}=-10V$时,$V_{OUT}=0$;当$V_{IN}=10V$时,$V_{OUT}=3.3V$。以此达到测量$-10V$~ $10V$电压的目的。

输入通道

输入通道,顾名思义为将电压输入到ADC的通道,STM32 的 ADC 多达 18 个通道,具体参考下图:

输入通道

外部的 16 个通道在转换的时候又分为规则通道和注入通道,其中规则通道最多有 16路,注入通道最多有 4 路。

  • 规则通道:也就是一般情况下用到的通道;
  • 注入通道:可以在规则通道转换的时候强行插入转换,享有转换特权,有点像中断的操作。

转换顺序

  • 规则序列

    转换的优先级并不是根据通道的编号来确定的,需要操作规则序列寄存器SQRx(x=1,2,3),例如将通道六设置为第十个转换,SQ10这个位写6即可。SQL1中SQL位用于设置转换通道的数量,具体的操作都可以库函数代劳。

    规则序列

  • 注入序列

    注入序列

    注入序列的转换顺序稍有不一样,当JL的值小于4时,第一次转的顺序是JSQx(x=4-JL);当JL等于4时,与SQR一致。

触发源

触发源可以看作是系统给ADC这个外设的一个信号,当触发源激活时,ADC就开始进行转换。触发源一般有两种:软件触发和外部事件触发(包括内部定时器和外部IO);

  • 软件触发

    ADC_CR2ADON位写1,开始转换;

  • 外部事件触发

    外部触发源很多,不在此详叙,主要经过EXTSEL[2:0]JEXTSEL[2:0]位来选择,EXTTRIGJEXTTRIG这两位来激活。

转换时间

ADC时钟

ADC 输入时钟 ADC_CLK 由 PCLK2 经过分频产生,最大是 14M,分频因子由 RCC 时钟配置寄存器 RCC_CFGR 的位 15:14 ADCPRE[1:0]设置,可以是 2/4/6/8 分频,注意这里没有 1 分频。一般我们设置 $PCLK2=HCLK=72M$。

采样时间

ADC 使用若干个 ADC_CLK 周期对输入的电压进行采样,采样的周期数可通过 ADC 采样时间寄存器 ADC_SMPR1ADC_SMPR2 中的 SMP[2:0]位设置,ADC_SMPR2 控制的是通道 09,ADC_SMPR1 控制的是通道 1017。每个通道可以分别用不同的时间采样。其中采样周期最小是 1.5 个,即如果我们要达到最快的采样,那么应该设置采样周期为 1.5个周期,这里说的周期就是$ 1\over ADC_{CLK}$。

ADC 的转换时间跟 ADC 的输入时钟和采样时间有关,公式为:
$$
Tconv = 采样时间 + 12.5 个周期。
$$
当 $ADCLK = 14MHZ$ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)$T_{conv}$ = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。一般我们设置 $PCLK2=72M$,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us,这个才是最常用的。

数据寄存器

ADC转换后,规则组数据存放在ADC_DR寄存器中,注入组在JDRx

规则数据寄存器

ADC 规则组数据寄存器 ADC_DR 只有一个,是一个 32 位的寄存器,低 16 位在单 ADC时使用,高 16 位是在 ADC1 中双模式下保存 ADC2 转换的规则数据,双模式就是 ADC1 和ADC2 同时使用。在单模式下,ADC1/2/3 都不使用高 16 位。因为 ADC 的精度是 12 位,
无论 ADC_DR 的高 16 或者低 16 位都放不满,只能左对齐或者右对齐,具体是以哪一种方式存放,由 ADC_CR2 的 11 位 ALIGN 设置。

由于数据寄存器只有一个,而通道却有多个,多通道转换时为了避免数据被覆盖,转换完成时应立刻将数据取走,一般开启DMA传输模式,直接将数据传输给变量。

注入数据寄存器

ADC 注入组最多有 4 个通道,刚好注入数据寄存器也有 4 个,每个通道对应着自己的寄存器,不会跟规则寄存器那样产生数据覆盖的问题。ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由
ADC_CR2 的 11 位 ALIGN 设置。

中断

转换结束中断

数据转换结束后,可以产生中断,中断分为三种:

  • 规则通道转换结束中断,对应中断位:ADC_IT_EOC;

  • 注入转换通道转换结束中断,对应中断位:ADC_IT_JEOC;

  • 模拟看门狗中断,对应中断位:ADC_IT_AWD

模拟看门狗中断

当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是我们开启了模拟看门狗中断,其中低阈值和高阈值由 ADC_LTRADC_HTR 设置。例如我们设置的电压高阈值为3.3V,若接入的模拟电压高于3.3V时,即产生中断。

DMA 请求

规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA 请求,把转换好的数据直接存储在内存里面。要注意的是只有 ADC1 和 ADC3 可以产生 DMA 请求

双 ADC 模式

AD 转换包括采样阶段和转换阶段,在采样阶段才对通道数据进行采集;而在转换阶段只是将采集到的数据进行转换为数字量输出,此刻通道数据变化不会改变转换结果。独立模式的 ADC 采集需要在一个通道采集并且转换完成后才会进行下一个通道的采集。而双重 ADC 的机制就是使用两个 ADC 同时采样一个或者多个通道。双重 ADC 模式较独立模式一个最大的优势就是提高了采样率,弥补了单个 ADC 采样不够快的缺点。

启用双 ADC 模式的时候,通过配置 ADC_CR1 寄存器的 DUALMOD[3:0]位,可以有几种不同的模式:

模式 简介
同步注入模式 ADC1 和 ADC2 同时转换一个注入通道组,其中 ADC1 为主,ADC2 为从。转换的数据存储在每个 ADC 接口的ADC_JDRx寄存器中。
同步规则模式 ADC1 和 ADC2 同时转换一个规则通道组,其中 ADC1 为主,ADC2 为从。ADC1 转换的结果放在 ADC1_DR 的低 16 位,ADC2 转换的结果放在 ADC1_DR 的高十六位。
快速交叉模式 ADC1 和 ADC2 交替采集一个规则通道组(通常为一个通道)。当ADC2 触发之后,ADC1 需要等待 ==7==个 ADCCLK 之后才能触发。
慢速交叉模式 ADC1 和 ADC2 交替采集一个规则通道组(只能为一个通道)。当ADC2 触发之后,ADC1 需要等待 ==14== 个 ADCCLK 之后才能触发。
交替触发模式 ADC1 和 ADC2 轮流采集注入通道组,当 ADC1 所有通道采集完毕之后再采集 ADC2 的通道,如此循环。跟交叉采集不一样。
混合的规则/注入同步模式 规则组同步转换被中断,以启动注入组的同步转换。分开两个模式来理解就可以了,区别就是注入组可以中断规则组的转换。
混 合 的 同 步 规则+交替触发模式 规则组同步转换被中断,以启动注入组交替触发转换。分开两个模式来理解就可以了,区别就是注入组可以中断规则组的转换。
混合同步注入+交叉模式 交叉转换可以被同步注入模式中断。这种情况下,交叉转换被中断,注入转换被启动。

例程设计

STM32 的 ADC 功能繁多,以下通过三个基础例程尽量展示ADC的功能,代码中均没有使用宏定义,是为了看得更直观。

独立模式单通道

**1) **初始 ADC 用到的 GPIO,这里我们以GPIOC1为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void ADCx_GPIO_Config(void){
GPIO_InitTypeDef GPIO_InitStructure;

// 打开 ADC IO 端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

// 配置 ADC IO 引脚模式
// 必须为模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
// 初始化 ADC IO
GPIO_Init(GPIOC, &GPIO_InitStructure);
}

2) 设置 ADC 的工作参数并初始化(以ADC1为例);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void ADCx_Mode_Config(void){
ADC_InitTypeDef ADC_InitStructure;
// 打开 ADC 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
// ADC 模式配置
// 只使用一个 ADC,属于独立模式
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;

// 禁止扫描模式,多通道才要,单通道不需要
ADC_InitStructure.ADC_ScanConvMode = DISABLE;

// 连续转换模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;

// 不用外部触发转换,软件开启即可
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;

// 转换结果右对齐
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;

// 转换通道 1 个
ADC_InitStructure.ADC_NbrOfChannel = 1;
// 初始化 ADC
ADC_Init(ADC1, &ADC_InitStructure);
// 配置ADC时钟为PCLK2的8分频,即9MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div8);

// 配置 ADC 通道转换顺序和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 1, ADC_SampleTime_55Cycles5);
/**
* @param1 ADC 外设
* @param2 通道选择
* @param3 转换顺序 1~16
* @param4 采样周期
*/
// ADC 转换结束产生中断,在中断服务程序中读取转换值
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

// 开启ADC,并开始转换
ADC_Cmd(ADC1, ENABLE);

// 初始化ADC 校准寄存器
ADC_ResetCalibration(ADC1);
// 等待校准寄存器初始化完成
while(ADC_GetResetCalibrationStatus(ADC1));

// ADC开始校准
ADC_StartCalibration(ADC1);
// 等待校准完成
while(ADC_GetCalibrationStatus(ADC1));

// 由于没有采用外部触发,所以使用软件触发ADC转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

**3) **配置中断源和中断优先级;

1
2
3
4
5
6
7
8
9
10
11
12
void ADC_NVIC_Config(void){
NVIC_InitTypeDef NVIC_InitStructure;
// 优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

// 配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}

4) 中断服务函数

1
2
3
4
5
6
7
8
void ADC1_2_IRQHandler(void){	
if (ADC_GetITStatus(ADC1,ADC_IT_EOC)==SET) {
// 读取ADC的转换值
ADC_ConvertedValue = ADC_GetConversionValue(ADC1);
// ADC_ConvertedValue为转换值,定义原型为:__IO uint16_t ADC_ConvertedValue;
}
ADC_ClearITPendingBit(ADC1,ADC_IT_EOC);
}

5) main()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern __IO uint16_t ADC_ConvertedValue;
// 局部变量,用于保存转换计算后的电压值
float ADC_ConvertedValueLocal;

int main(void){
// 默认已经完成了串口配置
USART_Config();
ADCx_GPIO_Config();
ADCx_Mode_Config();
ADC_NVIC_Config();
printf("\t\n ----这是一个串口发送实验----\r\n");
ADC_ConvertedValueLocal =(float) ADC_ConvertedValue/4096*3.3;
printf("\r\n The current AD value = 0x%04X \r\n",ADC_ConvertedValue);
printf("\r\n The current AD value = %f V \r\n",ADC_ConvertedValueLocal);
printf("\r\n\r\n");
}

独立模式多通道

因为与单通道大多配置都相同,因此只列出差异部分。

**1) **初始 ADC 用到的 GPIO;

1
GPIO_InitStructure.GPIO_Pin = 	ADC_PIN1 | ADC_PIN2 | ADC_PIN3 | ADC_PIN4 | ADC_PIN5 | ADC_PIN6;

2) 设置 ADC 的工作参数并初始化,还需要对DMA进行配置;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 打开 DMA 时钟
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&(ADC_x->DR)); // 外设基址为:ADC 数据寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)ADC_ConvertedValue; // 存储器地址,用户自定义变量(数组名)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 数据源来自外设
DMA_InitStructure.DMA_BufferSize = 6; // 缓冲区大小,应该等于数据目的地的数目大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设寄存器只有一个,地址不用递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器地址递增
// 外设数据大小为半字,即两个字节
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据大小也为半字,跟外设数据大小相同
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环传输模式

// DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
DMA_InitStructure.DMA_Priority = DMA_Priority_High;

// 禁止存储器到存储器模式,因为是从外设到存储器
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 初始化DMA
DMA_Cmd(DMA1_Channel1 , ENABLE); // 使能 DMA 通道
/* -------------------------------------------------------------------------------------------------------*/

ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式 多通道
ADC_InitStructure.ADC_NbrOfChannel = 6; // 转换通道个数

// 配置ADC 通道的转换顺序和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 4, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_14, 5, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_15, 6, ADC_SampleTime_55Cycles5);

ADC_DMACmd(ADC1, ENABLE); // 使能ADC DMA 请求

3) main()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ADC1转换的电压值通过DMA方式传到SRAM
extern __IO uint16_t ADC_ConvertedValue[6];

// 局部变量,用于保存转换计算后的电压值
float ADC_ConvertedValueLocal[6];

int main(void){
// 默认已经完成了串口配置
USART_Config();
ADCx_GPIO_Config();
ADCx_Mode_Config();;
printf("\t\n ----这是一个串口发送实验----\r\n");
ADC_ConvertedValueLocal[0] =(float) ADC_ConvertedValue[0]/4096*3.3;
ADC_ConvertedValueLocal[1] =(float) ADC_ConvertedValue[1]/4096*3.3;
ADC_ConvertedValueLocal[2] =(float) ADC_ConvertedValue[2]/4096*3.3;
ADC_ConvertedValueLocal[3] =(float) ADC_ConvertedValue[3]/4096*3.3;
ADC_ConvertedValueLocal[4] =(float) ADC_ConvertedValue[4]/4096*3.3;
ADC_ConvertedValueLocal[5] =(float) ADC_ConvertedValue[5]/4096*3.3;

printf("\r\n CH0 value = %f V \r\n",ADC_ConvertedValueLocal[0]);
printf("\r\n CH1 value = %f V \r\n",ADC_ConvertedValueLocal[1]);
printf("\r\n CH2 value = %f V \r\n",ADC_ConvertedValueLocal[2]);
printf("\r\n CH3 value = %f V \r\n",ADC_ConvertedValueLocal[3]);
printf("\r\n CH4 value = %f V \r\n",ADC_ConvertedValueLocal[4]);
printf("\r\n CH5 value = %f V \r\n",ADC_ConvertedValueLocal[5]);
printf("\r\n\r\n");
}

双ADC同步规则

**1) **初始 ADC 用到的 GPIO;

1
2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;

2) 设置 ADC 的工作参数并初始化,还需要对DMA进行配置;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult;			// 双ADC的规则同步
ADC_InitStructure.ADC_ScanConvMode = ENABLE ; // 扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式
// 不用外部触发转换,软件开启即可
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;

ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 转换结果右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道个数
ADC_Init(ADCx_1, &ADC_InitStructure); // 初始化ADC
RCC_ADCCLKConfig(RCC_PCLK2_Div8); // 配置ADC时钟PCLK2的8分频,即9MHz
// 配置ADC1 通道的转换顺序和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_239Cycles5);
ADC_DMACmd(ADC1, ENABLE); // 使能ADC DMA 请求
// 由于没有采用外部触发,所以使用软件触发ADC转换
ADC_SoftwareStartConvCmd(ADCx_1, ENABLE);

ADC_Init(ADC2, &ADC_InitStructure);
// 配置ADC2 通道的转换顺序和采样时间
ADC_RegularChannelConfig(ADC2, ADC_Channel_14, 1, ADC_SampleTime_239Cycles5);

ADC_ExternalTrigConvCmd(ADC2, ENABLE); // 使能ADC2的外部触发转换

3) main()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ADC1转换的电压值通过MDA方式传到SRAM
extern __IO uint32_t ADC_ConvertedValue[1];

// 局部变量,用于保存转换计算后的电压值
float ADC_ConvertedValueLocal[NOFCHANEL*2];

int main(void){
uint16_t temp0=0 ,temp1=0;
USART_Config(); // 默认已经完成了串口配置
ADCx_GPIO_Config(); // GPIO 初始化
ADCx_Mode_Config(); // ADC 初始化
printf("\r\n ----这是一个双ADC规则同步采集实验----\r\n");

// 取出ADC1数据寄存器的高16位,这个是ADC2的转换数据
temp0 = (ADC_ConvertedValue[0]&0XFFFF0000) >> 16;
// 取出ADC1数据寄存器的低16位,这个是ADC1的转换数据
temp1 = (ADC_ConvertedValue[0]&0XFFFF);

ADC_ConvertedValueLocal[0] =(float) temp0/4096*3.3;
ADC_ConvertedValueLocal[1] =(float) temp1/4096*3.3;

printf("\r\n ADCx_1 value = %f V \r\n", ADC_ConvertedValueLocal[1]);
printf("\r\n ADCx_2 value = %f V \r\n", ADC_ConvertedValueLocal[0]);
printf("\r\n\r\n");
}
}
Prev
2019-06-17 23:12:43
Next