08 ADC模数转换
前言
这一节终于到模数转换了,我在做那个项目的时候忘记如何配置模数转换器了,所以这一章是在我学完后并且在项目中实现后写的文章,这里我会结合项目来介绍如何用adc采集外部模拟量后转换为数字量和用stm32内部的温度传感器通过adc采集后显示出来。
一、什么是ADC
ADC又叫做模数转换,即模拟量转换为数字量,什么是模拟量什么是数字量呢?
模拟量就是一种没有规律的量,比如说声音,声音就是一个模拟量。
数字量是用数字表达的一个量,就像上上节输出比较那说的PWM波就是一个数字量。
而ADC这个设备就是将这些模拟量转换为用数字表达的数字量。
二、stm中的ADC
在stm32f103中,有3个ADC,这3个都是12位分辨率逐次逼近型的ADC,每个ADC都有很多个通道,下图是stm32f103ADC资源:
可以看到ADC1有18个通道,包括16个外部ADC采集和两个内部ADC,而ADC2和ADC3都是外部ADC采集通道。
三、adc内部结构
我们要写代码还是需要先了解一下adc其中的内部结构,这样写代码才好写,下图就是adc的内部结构:
可以看到有一个AD转换器,我们需要配置它才可以进行AD转换,从左到右可以看到,是由输入到一个选择器中来进行选择输入的通道到AD转换器中的,这里可以选择GPIO口和内部的温度和参考电压。
选择通道后就进去到AD转换器中,在AD转换器中有两个组来存放需要转换的AD信息,分别是规则组和注入组,这里是可以在配置AD转换器那配置使用哪个组的。
在配置AD转换器中还需要配置一个触发控制和RCC,触发控制选择的可多了,我们可以使用软件直接触发,也可以使用定时器来进行触发。
而RCC这里就需要注意了,ADC虽然是接到APB2的,但是在进入APB2后还要经过一个分频,因为ADC的时钟最大不能超过12MHz,如果超过了ADC的运行就是超频运行,对于ADC的伤害还是比较大的。
然后AD转换器会将注入组或者规则组中转换后的数据发送个AD数据寄存器中,在AD寄存器中我们就可以把转换的数据读取出来了。
这个就是adc的内部结构,了解了这个adc的内部结构后我们就可以写代码了。
四、adc代码实现
上面了解了adc的内部结构后我们知道adc的使用步骤,首先是如果使用外部输入,就需要配置外部输入端口,然后打开ADC的时钟,并且配置ADC的分频,然后就可以配置ADC,配置完成后将要转换的数据塞到注入组或者是规则组中进行转换,最后从AD数据寄存器中进行读取转换后的结构即可。
1.配置外部引脚
这里是对于外部模拟信号输入,只有从外部读取模拟需要才需要配置外部引脚,如果是直接使用内部来读取温度值,这一步就可以直接省略。
首先配置外部引脚还是一样的,需要打开时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE);
打开时钟后就可以配置GPIO和引脚了,这里的引脚的模式是需要注意一下的,我们这里不能选择什么上拉下拉什么什么的了,而是要选择模拟输入GPIO_Mode_AIN
。
我之前的代码为什么读取不了ADC的值,就是因为我这设置成浮空输入了,浮空、上拉、下拉输入都是输入的是数字量,而我们这里需要让它输入个模拟量来进行转换,所以这里一定要注意。
那么代码就如下:
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 这里是模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_x;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOx, &GPIO_InitStruct);
这样GPIO口就配置完成了。
2.打开ADC的时钟和分频
首先要使用ADC也要打开时钟,ADC是在APB2总线下的器件,所以打开时钟使用的函数也是RCC_APB2PeriphClockCmd
:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADCx, ENABLE);
这里ADCx是具体哪个ADC设备,比如ADC1那这里就是:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
当这个时钟打开后,ADC的时钟频率就为72MHz,而上面我们提到,ADC的最大时钟频率为12MHz,这里如果直接使用就超频了,所以我们需要进行一下分频,分频使用的函数如下:
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);
这里的参数可以选择下面这几个:
参数 | 解释 |
---|---|
RCC_PCLK2_Div2 | ADC clock = PCLK2/2 2分频 |
RCC_PCLK2_Div4 | ADC clock = PCLK2/4 4分频 |
RCC_PCLK2_Div6 | ADC clock = PCLK2/6 6分频 |
RCC_PCLK2_Div8 | ADC clock = PCLK2/8 8分频 |
这里的PCLK2就是APB2总线的频率,这里经过分频我们的PCLK2就是72MHz,我们要得到12MHz只能进行6分频,当然也可以选择8分频,那adc的频率就是9MHz。
所以代码就如下:
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
3.配置ADC
设置完时钟后就可以开始配置ADC了,配置ADC的方式和配置GPIO一样,创建个结构体,然后用Init函数进行初始化,这样就可以了。
首先创建一个结构体,结构体的类型为:
typedef struct
{
uint32_t ADC_Mode; /*!< 将ADC配置为在独立或双模式下运行。
此参数的值可以是 @ref ADC_mode */
FunctionalState ADC_ScanConvMode; /*!< 指定是否在以下位置执行转换
扫描(多通道)或单通道(单通道)模式。
此参数可以设置为 ENABLE 或 DISABLE */
FunctionalState ADC_ContinuousConvMode; /*!< 指定是否在以下位置执行转换
连续或单一模式。
此参数可以设置为 ENABLE 或 DISABLE。 */
uint32_t ADC_ExternalTrigConv; /*!< 定义用于启动常规通道的模数转换的外部触发器。此参数
的值可以是 @ref ADC_external_trigger_sources_for_regular_channels_conversion */
uint32_t ADC_DataAlign; /*!< 指定ADC数据对齐是左对齐还是右对齐。
此参数的值可以是 @ref ADC_data_align*/
uint8_t ADC_NbrOfChannel; /*!< 指定要转换的ADC通道数
将音序器用于常规频道组。
此参数的范围必须介于 1 到 16 之间。 */
}ADC_InitTypeDef;
第一个参数ADC_Mode
,这个是选择adc的运行模式的,单模式是只有一个ADC运行,而双模式是两个ADC一起运行。
第二个参数ADC_ScanConvMode
是决定这个组中可以有多少个转换的adc信息,这里如果是DISABLE
,就是非扫描模式,只能放一个ADC数据进行转换,如果是ENABLE
就是扫描模式,可以放多个ADC数据
第三个参数ADC_ContinuousConvMode
是选择单通道还是扫描模式,当是单次扫描,就每次需要我们手动的将ADC中的数据手动的拿出来,而扫描模式的话就是在转换完成后自动的将数据放在寄存器中。
第四个参数ADC_ExternalTrigConv
可以选择触发方式,有软件触发,TIM触发等等。
第五个参数ADC_DataAlign
是选择对其模式,如果需要直接读取后就能使用的数据,那这里就选择右对齐。
第六个参数ADC_NbrOfChannel
是指定要转换的ADC通道的个数。
这里我就选择单一模式非扫描模式,并且是软件触发,右对齐,只有一个通道需要转换,那代码就可以这样写:
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单一模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
然后初始化:
ADC_Init(ADC1, &ADC_InitStruct);
初始化完成后就使能ADC:
ADC_Cmd(ADC1, ENABLE);
4.校准ADC
为了使ADC采集的数据是精准的,这里就需要让ADC进行一下校准,校准只需要使用函数来控制ADC进行校准就可以了,校准的过程主要是先将校准位清除,然后再为校准位置1即可。
这里使用的代码就这四个:
void ADC_ResetCalibration(ADC_TypeDef* ADCx); // 将校准位复位
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx); // 判断校准位是否复位
void ADC_StartCalibration(ADC_TypeDef* ADCx); // 将校准位置位
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx); // 判断校准位是否置位
代码就可以这样写:
ADC_ResetCalibration(ADC1); // 将校准位复位
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 判断校准位是否复位
ADC_StartCalibration(ADC1); // 将校准位置位
while (ADC_GetCalibrationStatus(ADC1) == RESET); // 判断校准位是否置位
这里主要是判断一个寄存器的位是否被置位和复位。
5.触发ADC转换
校准完成后就可以配置需要转换的ADC到AD转换器中,然后用软件触发进行转换后读取即可,这里的代码也是比较简单的,首先将要转换的ADC通道放到规则组中,使用的函数是:
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
第一个参数ADCx
是ADC的设备号,第二个参数ADC_Channel
是ADC的通道号,第三个Rank
是这个转换的在这个组中是第几个,第四个ADC_SampleTime
是采样时间,采样时间越长,精度越高。
这里我就将通道0塞进去:
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
这里的采样时间用的是239.5,比较长,为了精度。
然后将需要转换的通道塞入组中后就可以使用软件进行软件触发转换了,使用的函数为:
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState)
直接上代码:
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
这样就可以使用软件触发来进行转换了,如果知道是否转换完成呢?
其实在转换完成时,adc会产生一个EOF标志,我们可以判断这个标志来知道adc是否转换完成了,我们使用ADC_GetFlagStatus
来判断这个标志:
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
当产生这个信号后,这个循环就会跳出,我们就可以从AD数据寄存器中读取转换值了,读取方式也是很简单,使用ADC_GetConversionValue
函数就可以读取了,当值取出后,ADC会自动将EOF标志清空,为下一次转换做准备
return ADC_GetConversionValue(ADC1);
五、读取电压值
这个需要来配合这电路图来进行分析了
这里可以看到有两个电阻,比如我这VBIN输入的是3.3,那ADC读取到的值就为VBIN/20*10。这样就很明白读取到的值和转换公式了,直接上代码:
ADC_InitTypeDef ADC_InitStruct = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStruct.GPIO_Pin = ADC;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(ADC_GPIO, &GPIO_InitStruct);
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单一模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_Init(ADC1, &ADC_InitStruct);
ADC_Cmd(ADC1, ENABLE);
// ADC校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == RESET);
上面的是初始化代码,下面的是读取代码加转换:
uint32_t ave = 0;
uint8_t i;
float v = 0;
for (i = 0; i < 50; i++)
{
// ADC规则组通道配置
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
ave = ADC_GetConversionValue(ADC1);
Delay_Ms(10);
}
v = (ave / 50.0) * 3.3 / 4096;
return ave * 20.0 / 10.0;
这里我采集了50次后取平均值,这样采集的数据要准确有点,测试到的电压如下:
六、读取内部温度
在介绍ADC通道的时候介绍到了,内部温度测试的是在ADC1中的通道16,这种内部设置的不需要配置外部GPIO口,直接可以通过配置ADC即可:
ADC_InitTypeDef ADC_InitStruct = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单一模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 单独模式
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_Init(ADC1, &ADC_InitStruct);
ADC_Cmd(ADC1, ENABLE);
// ADC校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == RESET);
配置的代码和前面的一样,主要是塞入组的时候要塞入通道16:
uint32_t ave = 0;
uint8_t i;
for (i = 0; i < 50; i++)
{
// ADC规则组通道配置
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
ave = ADC_GetConversionValue(ADC1);
Delay_Ms(10);
}
这样就可以得到温度了,我看其它大佬写的文章需要进行一个转换,但是我感觉都不需要进行转换就可以得到真实的温度,可能不行,后面我会进行验证的。
总结
对于ADC的介绍就先介绍到这了,后面会结合DMA进行多通道的AD转换,这里先只介绍了最简单的,对于我现在的项目够用,追求速度的话我后面再改为DMA。