重要的内容写在前面:
- 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
- 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
- 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
- 如有错漏欢迎指出。
视频链接:[7-1] ADC模数转换器_哔哩哔哩_bilibili
一、ADC简介
1、ADC(Analog-Digital Converter)模拟-数字转换器概述
(1)ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。
(2)STM32中的ADC是12位(分辨率)逐次逼近型ADC,1us转换时间(对应AD转换频率就是1MHz)。
(3)输入电压范围:0~3.3V,转换结果范围:0~4095。
(4)ADC最多有18个输入通道,可测量16个外部和2个内部信号源。
(5)规则组和注入组两个转换单元。
(6)模拟看门狗自动监测输入电压范围。
(7)STM32F103C8T6的ADC资源:ADC1、ADC2,10个外部输入通道。
2、逐次逼近型ADC
(1)IN0~IN7是8路输入通道,通过通道选择开关选择其中1路进入比较器,“地址锁存和译码”则是负责选择通道,需要选择哪路通道,就将通道号置于ADDA~ADDC三个引脚上(3位二进制数表示0~7)。
(2)如果想转换多路信号,不必设计多个ADC,只需要一个AD转换器加一个多路选择开关即可。
(3)比较器可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小,两个输入分别为待测电压和DAC(数模转换器)输出电压,如果DAC输出的电压比待测电压大,DAC输出需要降低,如果DAC输出的电压比待测电压小,DAC输出需要增加,直到DAC输出的电压和外部通道输出的电压近似相等,这样DAC输入的数据就是外部电压的编码数据(待测电压的电压值DAC一般使用二分法进行查找)。
(4)START代表开始转换信号,CLOCK是DAC的时钟,EOC代表转换结束信号,VREF是ADC(DAC)的参考电压,通常和供电电压VCC相等。
(5)AD转换结束后输出到三态锁存缓冲器,8位ADC有8根输出线。
二、STM32中的ADC
1、STM32中ADC的结构
(1)STM32中的ADC有16个输入端口(ADCx_IN0~ADCx_IN15,16个GPIO口)和两个内部通道(一个是内部温度传感器,一个是VREFINT——内部参考电压),18个通道经过模拟多路开关进行选择。
(2)经过多路开关的选择,信号进入模数转换器,模数转换器对待测信号进行逐次比较,转换结果会放在数据寄存器中,用户读取数据寄存器的值就能获取转换结果。
(3)对于普通ADC,多路开关一般只能选择一个通道,不过在STM32中,ADC的多路开关可以同时选择多个,并且分为规则通道组(一次性选择16个通道)和注入通道组(一次性选择4个通道)。
①对于规则通道组,它的数据寄存器只有16位,如果选择了多个通道,那么只有一个通道的数据能被读出,其它通道的数据会被前一个通道的数据覆盖,使用该组最好配合DMA记录被覆盖的数据。
②对于注入通道组,它的数据寄存器有64位,可以同时记录4个通道的数据,不必担心数据被覆盖的问题。
(5)触发ADC开始转换的信号有两种,一种是软件触发,另一种是硬件触发。注入组和规则组都有不同的硬件触发源(主要来自定时器,见下图模拟多路开关下面的两个模块),比如设置TIM3的定时时间为1ms,并将TIM3的更新事件选择为TRGO输出,然后选择规则组触发信号为TIM3_TRGO,这样TIM3的更新事件就能通关硬件自动触发ADC转换,不需要转入中断函数进行处理;当然也可以选择外部中断引脚来触发转换。
(6)VREF是ADC的参考电压,决定了ADC输入电压的范围,VDDA、VSSA是ADC的供电引脚,一般VREF+要接VDDA、VREF-要接VSSA。
(7)ADCCLK是ADC的时钟,用于驱动模数转换器对待测信号进行逐次比较。
(8)模拟看门狗中可以存一个阈值高限/阈值低限,如果启动模拟看门狗并指定看门通道,看门狗会持续关注该通道,一旦通道电压不在阈值范围内,模拟看门狗会申请中断通向NVIC。
(9)规则组或注入组转换完成之后会产生一个EOC信号(标志位由软件清除或读取数据寄存器时清除),注入组转换完成之后会产生一个JEOC信号(标志位由软件清除),这两个信号都会在状态寄存器中置一个标志位,用户读取标志位就能得知转换是否结束,同时这两个标志位可以去到NVIC申请中断,如果开启了NVIC对应的通道,它们就会触发中断。
2、输入通道对应引脚
通道 | ADC1 | ADC2 | ADC3 |
通道0 | PA0 | PA0 | PA0 |
通道1 | PA1 | PA1 | PA1 |
通道2 | PA2 | PA2 | PA2 |
通道3 | PA3 | PA3 | PA3 |
通道4 | PA4 | PA4 | PF6 |
通道5 | PA5 | PA5 | PF7 |
通道6 | PA6 | PA6 | PF8 |
通道7 | PA7 | PA7 | PF9 |
通道8 | PB0 | PB0 | PF10 |
通道9 | PB1 | PB1 | |
通道10 | PC0 | PC0 | PC0 |
通道11 | PC1 | PC1 | PC1 |
通道12 | PC2 | PC2 | PC2 |
通道13 | PC3 | PC3 | PC3 |
通道14 | PC4 | PC4 | |
通道15 | PC5 | PC5 | |
通道16 | 温度传感器 | ||
通道17 | 内部参考电压 |
注:本教程使用的开发板只有ADC1和ADC2
3、转换模式
(1)单次转换,非扫描模式:在非扫描模式下,每次触发转换后只能有一个通道的电压被转换,转换结果存放在数据寄存器中,同时EOC标志位置1,转换过程结束,如果想再启动一次转换,那就需要再触发一次,在触发转换之前可以更换待测通道。
(2)连续转换,非扫描模式:与单次转换不同,在完成一次转换后转换不会停止,不需要触发就能直接进行下一轮转换,也就是说只需要触发一次转换就可以实现对某个通道持续测量,用户只需要读取数据寄存器的值即可。
(3)单次转换,扫描模式:每触发一次转换,ADC就会按照序列对各个通道进行转换,转换结果写入数据寄存器中(对于规则通道组,为了防止数据被覆盖,需要使用DMA及时将数据挪移),所有通道转换完成后产生EOC信号,一轮转换结束。在配置时需要指定需要转换的通道数目。
(4)连续转换,扫描模式:单次转换扫描模式和连续转换非扫描模式的结合版,只需要触发一次转换就可以实现对多个通道持续测量。
4、触发控制
5、数据对齐
ADC的转换结果为12位,但是数据寄存器有16位,为了填满数据寄存器,剩下的4位用0填充,如果数据采取的是左对齐方式,那么程序在获取数据寄存器的数据后需要将数据右移4位将0清掉,然后才能将二进制数据转换为十进制数据。
①数据右对齐:(常用)
②数据左对齐:
6、转换时间
(1)AD转换的步骤:采样,保持,量化,编码。
(2)STM32 ADC的总转换时间为:TCONV = 采样时间 + 12.5个ADC周期
例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
7、自校准模式
(1)ADC有一个内置自校准模式,校准可大幅减小因内部电容器组的变化而造成的准精度误差。
(2)校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。
(3)建议在每次上电后执行一次校准。
(4)启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期。
三、示例程序
1、串联分压常见电路
(1)左一可以通过调节RP1的阻值来控制PA0的电压。
(2)左二中N1是一个传感器,传感器的阻值产生变化,对应PA1的输出电压也会产生变化。
(3)左三是一个电压转换电路,可以将5V的电压转换为3.3V输出。
2、AD单通道
(1)按照下图所示接好线路,并将OLED显示屏的工程文件夹作为模板复制一份使用。
(2)在项目的Hardware组中添加AD.h文件和AD.c文件用于封装模数转换器模块的代码。
①AD.h文件:
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(void);
#endif
②AD.c文件:
#include "stm32f10x.h" // Device header
void AD_Init(void)
{
//开启ADC和GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置ADCCLK的分频器
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADCCLK = 72MHz / 6 = 12MHz
//配置PA0为模拟输入模式
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);
//配置多路开关,将PA0接入规则组
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
//ADC_Channel_0:ADC1的通道0对应PA0
//1:序列中排第一
//ADC_SampleTime_55Cycles5:速度和稳定性相关参数(本例中无要求)
//配置AD转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
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; //序列中的通道数为1
ADC_Init(ADC1, &ADC_InitStructure);
//开关控制(开启ADC)和校准
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1); //复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成(复位完成后该位自动置为0)
ADC_StartCalibration(ADC1); //开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成(校准完成后该位自动置为0)
}
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC进行一次转换
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //判断规则组是否转换完成
return ADC_GetConversionValue(ADC1); //返回数据寄存器的二进制数据,EOC位自动清零
}
/*
如果ADC选择连续转换模式,那么在初始化函数中校准完成后可以直接软件触发ADC,
这样AD_GetValue函数中就不需要判断转换是否完成,直接返回数据寄存器的值即可
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换
*/
(3)在stm32f10x_rcc.h文件中有配置ADCCLK的函数,在stm32f10x_adc.h文件中则有ADC相关的函数,以下先简单介绍比较常用的几个。
[1]RCC_ADCCLKConfig函数:配置ADCCLK分频器,可以对APB2的72MHz时钟选择2/4/6/8分频。
[2]ADC_DeInit函数:恢复ADC缺省配置。
[3]ADC_Init函数:使用结构体中的参数初始化ADC。
[4]ADC_StructInit函数:给结构体中的参数赋一个默认值。
[5]ADC_Cmd函数:给ADC上电,即开关控制。
[6]ADC_DMACmd函数:开启DMA输出信号,如果需要使用DMA转运数据,就需要调用该函数。
[7]ADC_ITConfig函数:中断输出控制,用于控制某个中断能不能通往NVIC。
[8]ADC_ResetCalibration函数:复位校准。
[9]ADC_GetResetCalibrationStatus函数:获取复位校准状态。
[10]ADC_StartCalibration函数:开始校准。
[11]ADC_GetCalibrationStatus函数:获取开始校准状态。
[12]ADC_SoftwareStartConvCmd函数:调用该函数实现软件触发。
[13]ADC_GetSoftwareStartConvStatus函数:获取软件开始转换状态。(不可以借助该函数判断转换是否结束,SWSTART位在转换开始后就立刻清零了)
[14]ADC_GetFlagStatus函数:获取标志位状态(参数传入EOC,判断EOC标志位是否置1)。
[15]ADC_DiscModeChannelCountConfig函数:配置间断模式每转换几个通道间断一次。
[16]ADC_DiscModeCmd函数:开启间断模式。
[17]ADC_RegularChannelConfig函数:规则组通道配置,给转换模式序列的每个位置填写指定的通道。
[18]ADC_ExternalTrigConvCmd函数:设置是否允许外部触发转换。
[19]ADC_GetConversionValue函数:获取AD转换的数据寄存器内容(读取转换结果)。
[20]ADC_GetDualModeConversionValue双ADC模式下读取转换结果。
[21]ADC_AnalogWatchdogCmd函数:设置是否启动模拟看门狗。
[22]ADC_AnalogWatchdogThresholdsConfig函数:配置看门狗的高低阈值。
[23]ADC_AnalogWatchdogSingleChannelConfig函数:配置看门狗看守的通道。
[24]ADC_TempSensorVrefintCmd函数:开启两个内部通道(一个是内部温度传感器,一个是VREFINT——内部参考电压)。
[25]ADC_GetFlagStatus函数:获取标志位状态。
[26]ADC_ClearFlag函数:清除标志位。
[27]ADC_GetITStatus函数:获取中断状态。
[28]ADC_ClearITPendingBit函数:清除中断挂起位。
(4)在main.c文件中粘贴以下代码,然后进行编译,将程序下载到开发板中,用螺丝刀拧动电位器,观察OLED屏显示的数据。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "AD.h"
#include "Delay.h"
uint16_t ADValue;
float Volatage;
int main()
{
OLED_Init();
AD_Init();
OLED_ShowString(1,1,"ADValue:");
OLED_ShowString(2,1,"Volatage:");
while(1)
{
ADValue = AD_GetValue(); //获取ADC数据寄存器的值
Volatage = (float)ADValue / 4095 * 3.3; //线性转换为电压值
OLED_ShowNum(1,9,ADValue,4);
OLED_ShowNum(2,10,Volatage,1); //电压的整数部分
OLED_ShowNum(2,11,(int)(Volatage * 100) % 100,3); //电压的小数部分
Delay_ms(100); //更新显示的时间间隔
}
}
3、AD多通道
(1)按照下图所示接好线路,并将AD单通道的工程文件夹作为模板复制一份使用。(这次传感器使用的是AO引脚,因为要测量的是它们输出的模拟电压而不是经过二值化的数字电压)
(2)修改AD.h文件和AD.c文件:
①AD.h文件:
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif
②AD.c文件:
#include "stm32f10x.h" // Device header
void AD_Init(void)
{
//开启ADC和GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置ADCCLK的分频器
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADCCLK = 72MHz / 6 = 12MHz
//配置PA0~PA3为模拟输入模式
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);
//配置多路开关,将PA0接入规则组
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
//ADC_Channel_0:ADC1的通道0对应PA0
//1:序列中排第一
//ADC_SampleTime_55Cycles5:速度和稳定性相关参数(本例中无要求)
//配置AD转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
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; //序列中的通道数为1
ADC_Init(ADC1, &ADC_InitStructure);
//开关控制(开启ADC)和校准
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1); //复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成(复位完成后该位自动置为0)
ADC_StartCalibration(ADC1); //开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成(校准完成后该位自动置为0)
}
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); //判断规则组是否转换完成
return ADC_GetConversionValue(ADC1); //返回数据寄存器的二进制数据,EOC位自动清零
}
(3)在main.c文件中粘贴以下代码,然后进行编译,将程序下载到开发板中,根据主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "AD.h"
#include "Delay.h"
uint16_t AD0, AD1, AD2, AD3;
float Volatage;
int main()
{
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); //将通道0接入规则组并获取测得的数据
AD1 = AD_GetValue(ADC_Channel_1); //将通道1接入规则组并获取测得的数据
AD2 = AD_GetValue(ADC_Channel_2); //将通道2接入规则组并获取测得的数据
AD3 = AD_GetValue(ADC_Channel_3); //将通道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); //更新显示的时间间隔
}
}