STM32入门教程(ADC模数转换器篇)

重要的内容写在前面:

  1. 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
  2. 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
  3. 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
  4. 如有错漏欢迎指出。

视频链接:[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);  //更新显示的时间间隔
	}
}
  • 25
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值