【STM32F103笔记】8、数据采集之ADC——做个数字电压表吧

咳咳,这一篇来玩一下STM32的ADC(Analog to Digital Converter),也就是可以把输入的模拟量转换为数字量,这样就可以做个电压表了,再加上一些辅助电路,就能够自己做一个万用表了,非常完美。(嗯,这篇我们只做数字电压表~就是这么懒)

从这一篇开始,对STM32内部结构和寄存器的介绍会更加详细一点,要开始深入了解了,感兴趣的朋友还可以对照前几篇自个儿深入了解一下,嘿嘿~ . ~

ADC介绍

打开STM32F103C8T6的数据手册,第一页就对其拥有的外设进行了简单的列举介绍,找到ADC的相关介绍:
在这里插入图片描述
从介绍中可以知道:

  • STM32F103C8T6有2个12位的A/D转换器,支持16个通道,转换时间最小可达1us;
  • A/D转换电压范围是0到3.6V,这个非常重要,不要把超过范围的电压接入电压采集引脚,轻则导致转换结果超出范围,读数不准,严重可能会烧坏引脚;因此在使用ADC进行电压转换的时候,要根据采集的电压范围进行相应的分压设计;
  • 2个ADC可以同步采集,并且可由定时器控制,还具有内部温度传感器;
  • 其它说明等用到的时候再看。

ADC内部结构

在STM32手册中,找到ADC章节,其内部结构图如下(看不清的话请点击放大):
在这里插入图片描述
有几个需要关注的部分:

  • 正中间就是STM32的A/D模块的核心部分——ADC,模数转换器;
  • 左侧上面的红框中:
    • VREF+与VREF-是ADC的参考电压,这个电压的要求是精度高且稳定,由于笔者的最小系统板用的是STM32F103C8T6,引脚数是48,而64引脚及以下的芯片VREF在内部连接到VDDA,仅有100脚以上的芯片才会将VREF引到外部引脚;
    • VDDA和VSSA是ADC的供电电源,在笔者的最小系统板上通过电感电容组成LC滤波电路,与供电VDD连接,提供一个较为稳定的3.3V电源:
      在这里插入图片描述
  • 左侧下面的红框中就是外部电压的输入引脚了(常规的输入引脚),而VDD为3.3V供电电压,因此VREF电压为3.3V,即AD采样转换的电压范围为0~3.3V,而ADC为12位,故采样结果 0 - 4095 对应电压 0 - 3.3V;
  • 在右侧的红框中是ADC所使用的时钟,在进入ADC之前有一个预分频器,而在第二篇中的时钟分析里可知,ADC挂载在APB2总线上,因此APB2总线时钟信号(72MHz)经过这个预分频器最后进入ADC,提供时钟信号,这里注意进入ADC的时钟不能超过14MHz,因此在72MHz的APB2总线时钟下,预分频系数只能设置为8分频或6分频;
  • 在图中还可以看出,ADC还可以用定时器进行控制,并可以触发中断,在这里暂时不用这些功能。

ADC寄存器

了解了ADC的内部结构之后,就要开始对其寄存器进行分析,那么就会问了,我们用的是库开发,为什么还要去看寄存器呢;这里简单说明一下,库开发是为了方便程序的开发,也是建立在寄存器控制之上的,如果不了解相关的寄存器的话,可能会不知道用哪些库函数,并且作为单片机开发,了解其寄存器对后续程序移植等等都有很大的好处。当然,在刚开始看寄存器的时候难免会晕,熟悉了就好了(づ ̄ v ̄)づ

建议大家在开发单片机程序时,用这么个流程:确定相关外设后,先看板子的电路图,确定硬件电路的关系,然后在手册中找对应的外设章节进行了解,然后查看相关寄存器的配置说明,并结合手册中的功能说明进行分析,最后确定程序设计思路,然后就是写程序,最后就到了最重要的Debug,也就是改bug……

1、ADC status register (ADC_SR)

ADC_SR为ADC的状态标志寄存器,提供AD转换中的一些标志:
在这里插入图片描述

  • Bit 31:15:保留位,不需要进行设置,但是必须保持清0;
  • Bit 4 STRT:普通通道AD转换开始标志位,转换开始后由硬件置1,结束后由软件清0;
  • Bit 3 JSTRT:注入通道AD转换开始标志位,同上;
  • Bit 2 JEOC:注入通道AD转换完成标志位,转换完成时硬件置1,由软件清0;
  • Bit 1 EOC:通道AD转换完成标志位,转换完成时硬件置1,由软件清0;
  • Bit 0 AWD:模拟看门狗功能,当AD转换值超过设定的电压范围时置1,由软件清0;
    所谓由软件清0就是在程序中要向该位写入0。

这里Bit 1 EOC可以用于判断AD转换是否完成,若完成就可以读取AD转换的数据了。

2、ADC control register 1 (ADC_CR1)

ADC_CR1是ADC的控制寄存器1,用于对ADC进行设置:
在这里插入图片描述

  • Bit 31:24:保留位,不需要进行设置,但是必须保持清0;
  • Bit 23 AWDEN:普通通道模拟看门狗使能,置1使能,清0禁用;
  • ……
  • Bit 19:16 DUALMOD[3:0]:设置2个ADC的使用方式:
    • 0000 - 独立模式,2个ADC不同时工作;
  • ……
  • Bit 11 DISCEN:普通通道非连续模式设置,置1使能非连续模式,清0禁用;
  • Bit 8 SCAN:当需要采集多路电压数据时,可以使用SCAN模式,扫描模式,置1使能,清0禁用,这里我们不需要使用禁用就可以;
  • ……
  • Bit 5 EOCIE:转换完成触发中断使能位,置1使能,清0禁用,这里我们不需要使用禁用就可以;
  • ……

由于我们只进行一路ADC的采集,只需要用到一个ADC,因此设置成独立模式即可。

3、ADC control register 2 (ADC_CR2)

ADC_CR2是ADC的控制寄存器2,用于对ADC进行设置:
在这里插入图片描述

  • Bit 31:24:保留位,不需要进行设置,但是必须保持清0;
  • Bit 23 TSVREFE:温度传感器使能;
  • Bit 22 SWSTART:普通通道转换开始触发位,置1则ADC开始转换;
  • ……
  • Bit 11 ALIGN:数据对齐方式,置1左对齐,清0右对齐,为了计算方便,这里清0选择右对齐;
  • Bit 3 RSTCAL:复位校准,软件置1开始复位,硬件清0表示复位完成;
  • Bit 2 CAL:AD校准,软件置1开始校准,硬件清0表示校准完成;
  • Bit 1 CONT:连续转换模式,置1设置连续转换,清0设置单次转换;
  • Bit 0 ADON:置1使能并启动AD转换;

STM32的ADC模块自带内部校准功能,手册建议是每次上电之后都进行一次校准,以保证AD结果的准确;并且设置AD数据右对齐(因此ADC为12位,但ADC结果数据寄存器为16位),这样直接读取就是AD转换结果(不需要进行移位),方便计算;设置ADC为连续采样模式,这样不需要每次都触发AD转换;

4、ADC sample time register 1 (ADC_SMPR1)

ADC_SMPR1是ADC的采样周期设置寄存器:
在这里插入图片描述

  • Bit 23:0 SMPx[2:0]:通道X的采样周期设置:

    • 000:1.5个周期
    • 001:7.5个周期
    • ……具体设置可以参阅手册。

    总的采样时间计算为:
    T c o n v = + 12.5 T_{conv}=采样周期+12.5个周期
    这里我们可以根据需要进行设置。

5、ADC sample time register 2 (ADC_SMPR2)

同上

6、ADC regular sequence register 1 (ADC_SQR1)

ADC_SQR1与ADC_SQR2、ADC_SQR3都是用于在多通道采集时,设置通道采集顺序的寄存器。这里我们设置成1就好,因为没有多通道,不涉及顺序。

7、ADC regular data register (ADC_DR)

ADC普通通道采样数据寄存器:
在这里插入图片描述
可以看到,一个ADC功能虽然提供了很多寄存器(14个,上面没涉及的没有列出)进行设置,但是如果只考虑相关功能的寄存器,其实还是不多的,有了寄存器的基础,再使用库函数开发就更快了(当然也可能就抛弃库开发转向寄存器开发了,嘻嘻嘻)

程序设计

综合上述寄存器说明,结合STM32F103C8T6引脚图,可以选择PA2(ADC_IN2)作为ADC1采样电压输入引脚:

  • 配置ADC1采样的引脚;
  • 设置ADC1预分频;
  • 设置ADC1为独立转换模式,不使用扫描功能,使能连续转换功能,并且设置转换数据为右对齐;
  • 使能ADC1,并且进行校准;
  • 最后开始转换,并读取AD采样数据,通过串口进行显示。

硬件连接

这里笔者用了一个很久很久之前买的按键键盘作为ADC电压输入,将电压信号接到PA2引脚上:
在这里插入图片描述
这个小键盘通过电阻对供电电压进行分压,当按下不同按键时,其输出端口OUT的电压不同,笔者正好趁这个机会,把每个按键按下的输出电压测一下,后续可以用这个键盘配合其他小模块写更有意思的程序。

ADC初始化程序

根据上面寄存器分析记过,在官方库中查找相应的库函数,编写ADC初始化程序,具体程序说明请看注释:

void ADC1_PA2_Config(void)
{
	GPIO_InitTypeDef PA2InitStruct;
	// 同样,官方库为ADC初始化提供了对应的结构体,可以先在帮助手册中看看结构体的内容
	ADC_InitTypeDef ADC1InitStruct;
	
	// ADC1、GPIOA都挂载在APB2总线上,开启它们的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
	
	// 设置GPIOA2为模拟输入
	PA2InitStruct.GPIO_Pin = GPIO_Pin_2;
	PA2InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(GPIOA, &PA2InitStruct);
	
	// ADC1设置为独立模式
	ADC1InitStruct.ADC_Mode = ADC_Mode_Independent;
	// 不使用扫描模式(就一个通道)
	ADC1InitStruct.ADC_ScanConvMode = DISABLE;
	// 设置为连续转换模式
	ADC1InitStruct.ADC_ContinuousConvMode = ENABLE;
	// 不使用外部触发ADC采样转换
	ADC1InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	// 设置AD转换数据右对齐
	ADC1InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
	// 一个通道-PA2对应ADC_IN2
	ADC1InitStruct.ADC_NbrOfChannel = 1;
	// 调用ADC_Init函数进行初始化,这里其实主要设置的是ADC_CR1和ADC_CR2寄存器,可以去函数定义里看看,体会一下
	ADC_Init(ADC1, &ADC1InitStruct);
	
	// 设置ADC1的预分频系数,APB2时钟信号为72MHz,6分频,即12MHz
	// 这里是设置RCC的RCC_CFGR寄存器的Bit 15:14,即ADC预分频器
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	// 设置ADC1的通道2(对应PA2),转换顺序设置为1(就一个通道)
	// 采样周期设置为41.5个,这样总的转换时间就是54个时钟周期,为4.5us
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_41Cycles5);
	
	// 使能ADC1,即设置ADC_CR2寄存器Bit0 ADON为1
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准,即将ADC_CR2寄存器Bit3 RSTCAL置1
	ADC_ResetCalibration(ADC1);
	// 等待复位校准完成,即不断读取ADC_CR2寄存器Bit3 RSTCAL,若为0则表示已完成
	while(ADC_GetResetCalibrationStatus(ADC1));
	// 开始校准,即将ADC_CR2寄存器Bit2 CAL置1
	ADC_StartCalibration(ADC1);
	// 同样等待校准完成,即不断读取ADC_CR2寄存器Bit2 CAL,若为0则表示已完成
	while(ADC_GetResetCalibrationStatus(ADC1));
	
	// 由于没有设置ADC外部触发,所以这里需要软件触发一次,这样在连续模式下,ADC就会一直采用转换了
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

其他程序说明

这里还使用了前几篇的delay与usart相关程序,笔者推荐将其放在单独的文件夹中,并包含进工程,这样不同外设的程序比较整齐,也方便后续扩展:
在这里插入图片描述

完整程序

这里只给出main.c中程序,其它程序请在博客的其它文章中参考:

#include "stm32f10x.h"
#include "usart.h"
#include "delay.h"

void ADC1_PA2_Config(void);

int main(void)
{
	uint16_t adcResult;
	double vol;
	
	USART1Config();
	ADC1_PA2_Config();
	
	printf("Hello\r\n");
	printf("Getting ADC1 ch2:\r\n");
	
	while(1)
	{
		while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
		adcResult = ADC_GetConversionValue(ADC1);
		vol = (double)adcResult / 4096.0 * 3.3;
		printf("%d - %lf \r\n", adcResult, vol);
		delay_ms(500);
	}
	
}

void ADC1_PA2_Config(void)
{
	GPIO_InitTypeDef PA2InitStruct;
	ADC_InitTypeDef ADC1InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
	
	PA2InitStruct.GPIO_Pin = GPIO_Pin_2;
	PA2InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(GPIOA, &PA2InitStruct);
	
	ADC1InitStruct.ADC_Mode = ADC_Mode_Independent;
	ADC1InitStruct.ADC_ScanConvMode = DISABLE;
	ADC1InitStruct.ADC_ContinuousConvMode = ENABLE;
	ADC1InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC1InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
	ADC1InitStruct.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC1InitStruct);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_41Cycles5);
	
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1));
	ADC_StartCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1));
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

运行结果

将程序编译下载运行:
在这里插入图片描述

完结撒花✿✿ヽ(°▽°)ノ✿

发布了10 篇原创文章 · 获赞 24 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Keep_moving_tzw/article/details/104709910