咳咳,这一篇来玩一下STM32的ADC(Analog to Digital Converter),也就是可以把输入的模拟量转换为数字量,这样就可以做个电压表了,再加上一些辅助电路,就能够自己做一个万用表了,非常完美。(嗯,这篇我们只做数字电压表~就是这么懒)
从这一篇开始,对STM32内部结构和寄存器的介绍会更加详细一点,要开始深入了解了,感兴趣的朋友还可以对照前几篇自个儿深入了解一下,嘿嘿~ . ~
打开STM32F103C8T6的数据手册,第一页就对其拥有的外设进行了简单的列举介绍,找到ADC的相关介绍:
从介绍中可以知道:
在STM32手册中,找到ADC章节,其内部结构图如下(看不清的话请点击放大):
有几个需要关注的部分:
了解了ADC的内部结构之后,就要开始对其寄存器进行分析,那么就会问了,我们用的是库开发,为什么还要去看寄存器呢;这里简单说明一下,库开发是为了方便程序的开发,也是建立在寄存器控制之上的,如果不了解相关的寄存器的话,可能会不知道用哪些库函数,并且作为单片机开发,了解其寄存器对后续程序移植等等都有很大的好处。当然,在刚开始看寄存器的时候难免会晕,熟悉了就好了(づ ̄ v ̄)づ
建议大家在开发单片机程序时,用这么个流程:确定相关外设后,先看板子的电路图,确定硬件电路的关系,然后在手册中找对应的外设章节进行了解,然后查看相关寄存器的配置说明,并结合手册中的功能说明进行分析,最后确定程序设计思路,然后就是写程序,最后就到了最重要的Debug,也就是改bug……
ADC_SR为ADC的状态标志寄存器,提供AD转换中的一些标志:
这里Bit 1 EOC可以用于判断AD转换是否完成,若完成就可以读取AD转换的数据了。
ADC_CR1是ADC的控制寄存器1,用于对ADC进行设置:
由于我们只进行一路ADC的采集,只需要用到一个ADC,因此设置成独立模式即可。
ADC_CR2是ADC的控制寄存器2,用于对ADC进行设置:
STM32的ADC模块自带内部校准功能,手册建议是每次上电之后都进行一次校准,以保证AD结果的准确;并且设置AD数据右对齐(因此ADC为12位,但ADC结果数据寄存器为16位),这样直接读取就是AD转换结果(不需要进行移位),方便计算;设置ADC为连续采样模式,这样不需要每次都触发AD转换;
Bit 23:0 SMPx[2:0]:通道X的采样周期设置:
总的采样时间计算为:
T c o n v = 采 样 周 期 + 12.5 个 周 期 T_{conv}=采样周期+12.5个周期 Tconv=采样周期+12.5个周期
这里我们可以根据需要进行设置。
同上
ADC_SQR1与ADC_SQR2、ADC_SQR3都是用于在多通道采集时,设置通道采集顺序的寄存器。这里我们设置成1就好,因为没有多通道,不涉及顺序。
ADC普通通道采样数据寄存器:
可以看到,一个ADC功能虽然提供了很多寄存器(14个,上面没涉及的没有列出)进行设置,但是如果只考虑相关功能的寄存器,其实还是不多的,有了寄存器的基础,再使用库函数开发就更快了(当然也可能就抛弃库开发转向寄存器开发了,嘻嘻嘻)
综合上述寄存器说明,结合STM32F103C8T6引脚图,可以选择PA2(ADC_IN2)作为ADC1采样电压输入引脚:
这里笔者用了一个很久很久之前买的按键键盘作为ADC电压输入,将电压信号接到PA2引脚上:
这个小键盘通过电阻对供电电压进行分压,当按下不同按键时,其输出端口OUT的电压不同,笔者正好趁这个机会,把每个按键按下的输出电压测一下,后续可以用这个键盘配合其他小模块写更有意思的程序。
根据上面寄存器分析记过,在官方库中查找相应的库函数,编写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);
}