1 ADC简介
ADC(Analog-Digital Converter)模拟-数字转换器
ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
STM32是数字电路,只有高低电平,没有几V电压的概念,想读取电压值,就需要借助ADC模数转换器了。DAC则是相反的功能。
12位逐次逼近型ADC,1us转换时间
分辨率:一般用多少位来表示,0 ~ 2 ^ 12 - 1(量化结果0~4095)
转换时间:也是转换频率,从转换开始到产生结果需要花1us时间,即转换频率是1MHz
输入电压范围:0~3.3V,转换结果范围:0~4095
线性对应
18个输入通道,可测量16个外部和2个内部信号源
16个GPIO口、内部温度传感器(测量CPU的温度)和内部参考电压(1.2V的基准电压)
规则组和注入组两个转换单元
模拟看门狗自动监测输入电压范围
STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
1.1 逐次逼近型ADC
ADC0809的内部结构图,独立的8位逐次逼近型ADC芯片。
左边是8路输入通道,通过通道选择开关,选中一路,输入到比较器前进行转换;下面是地址锁存和译码(想选中哪个通道,就把通道号放在这三个引脚上,再给一个锁存信号,上面对应的通路开关就可以自动拨好了,相当于38译码器)
比较器有两个输入端,一个是待测电压,另一个是DAC(数模转换器)的电压输出端;如果DAC输出的电压比较大,就调小DAC数据;如果DAC输出的电压比较小,就增大DAC数据;直到DAC输出的电压和外部通道输入的电压近似相等,这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理。一般使用二分法调节。
1.2 ADC框图
具体的
有温度传感器、内部参考电压,总共18个输入通道,接到模拟多路开关,其输出接到模数转换器,即逐次比较,转换结果放在数据寄存器里。
普通的的流程是:多路开关选中某一个通道,开始转换,等待转换完成,取出结果。
这里比较高级,可以同时选中多个,而且在转换的时候,还分成了两个组,规则通道组和注入通道组,其中规则组可以一次性最多选择16个通道,注入组一次最多选择4个通道。规则组的数据寄存器只能存储一个结果,如果不想之前的结果被覆盖,那在转换完成之后,要尽快把结果拿走(使用DMA转运数据);注入组可以存4个结果,不用担心数据覆盖。
左下角是触发转换部分,对于STM32的ADC,触发ADC开始转换的信号有两种:一种是软件触发,程序中调用一条代码;另一种是硬件触发,就是这些触发源。上面是注入组触发源,下面是规则组触发源。这些触发源主要来自定时器,有定时器的各个通道,还有TRGO定时器主模式的输出。定时器可以通向ADC、DAC这些外设,用于触发转换。因为ADC经常需要过一个固定的时间段转换一次,正常的思路是:用定时器,每隔1ms申请一次中断,在中断里手动开始一次转换;但是频繁进中断对程序是有影响的,所以对这种需要频繁进中断,并且在中断里只完成简单的工作的情况,一般都会有硬件的支持。还可以选择外部中断引脚来触发转换。
VREF+、VREF-是ADC的参考电压,决定了ADC的输入电压范围,VDDA、VSSA是ADC的供电引脚,一般情况下VREF+接VDDA,VREF-接VSSA。
右边这里是ADCCLK是ADC的时钟,是用于驱动内部逐次比较的时钟,这个时钟来自ADC的预分频器,而ADC预分频器来自RCC的。
模拟看门狗里面可以存储阈值高限和阈值底限,如果指定了模拟看门狗,并且指定了看门的通道,越限之后,它就会乱叫,在上面申请一个模拟看门狗中断,最后通向NVIC。
对于规则组和注入组,转换完成之后,也会有一个EOC转换完成的信号,EOC是规则组的完成信号,JEOC是注入组完成的信号,这两个信号会在状态寄存器里置一个标志位,读取这个标志位就可以知道是不是转换完成了;同时这两个标志位也可以去到NVIC,申请中断,如果开启了NVIC对应的通道,它们就会触发中断。
1.3 ADC基本结构
左边是输入通道,16个GPIO口外加两个内部通道,然后进入AD转换器。AD转换器里有两个组,一个是规则组,一个是注入组,规则组最多选择16个通道,注入组最多选择4个通道,然后转换的结果存在AD数据寄存器里,其中规则组只有一个数据寄存器,而注入组有4个数据寄存器。下面有触发控制,触发控制可以选择软件触发和硬件触发,硬件触发主要是来自定时器,当然也可以选择外部中断的引脚;右边是来自RCC的ADC时钟CLOCK,ADC逐次比较的功能就是这个时钟推动的。然后上面可以布置一个模拟看门狗用于检测转换结果的范围,如果超出设定的阈值,就通过中断输出控制,向NVIC申请中断。另外规则组和注入组完成之后会有个EOC信号,它会置一个标志位,当然也可以通向NVIC。右下角有个开关控制,在库函数中就是ADC_Cmd,用于给ADC上电的。
1.4 输入通道
通道 | 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 | 内部参考电压 |
ADC通道和引脚复用的关系。
ADC12_IN0的意思是ADC1和ADC2的IN0都是在PA0上的。以此类推。双ADC模式。
1.5 规则组的转换模式
在ADC初始化中会有两个参数,一个是选择单次转换还是连续转换;另一个是扫描模式还是非扫描
1.5.1 单次转换,非扫描模式
这个列表就是规则组里面的菜单,有16个空位。可以在这里写入要转换的通道,比如通道2,在非扫描的模式下,这个菜单就只有第一个序列1的位置有效。这时菜单同时选中一组的方式就退化成简单地选中一个的方式了,在这里可以在序列1的位置指定想转换的通道,比如通道2,然后触发转换,ADC就会对通道2进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给EOC标志位置1,整个转换过程就结束了。判断EOC标志位来确定转换是否完成。如果想再启动一次转换,那就需要再触发一次,转换结束,置EOC标志位,读结果。如果想换一个通道转换,那就在转换之前把第一个位置的通道2改成其他通道,再启动转换就可以了。这就是单次转换、非扫描的转换模式。
1.5.2 连续转换,非扫描模式
首先还是非扫描模式。所以菜单列表就只用第一个,然后与上一个单次转换不同的是它在一次转换结束后不会停止,而是立刻开始下一轮的转换,然后一直持续下去。只触发一次就可以转换了。
好处是:开始转换之后不需要等待一段时间,因为一直在转换,所以也不需要手动开始转换,也不用判断是否结束,想要读AD值的时候,直接从寄存器取就是了。
1.5.3 单次转换,扫描模式
单次转换,每触发一次,转换结束后,就会停下来。扫描模式会用到菜单列表,选择通道,可以任意指定,可以重复,初始化结构体中有个参数指定通道数目。这里为了防止数据被覆盖,就需要用DMA及时将数据挪走,7个通道转换完成之后,产生EOC信号,转换结束。再触发下一次,开始新的转换。
1.5.4 连续转换,扫描模式
在上一个模式的基础上变了,就是一次转换完成后,立刻开始下一次转换。
还有间断模式,每隔几个转换,暂停一次需要再次触发才能继续。
1.6 触发控制
这个表是规则组的触发源,有来自定时器的信号,也有来自外部引脚/片上外设的信号,具体需要AFIO重映射。还有软件触发。这些触发信号通过右边寄存器的位来完成。
1.7 数据对齐
ADC是12位的,但是数据寄存器是16位的,因此存在一个数据对齐的问题
数据右对齐(一般是这个)
数据左对齐
有点像大端模式,小端模式
1.8 转换时间
AD转换的步骤:采样,保持,量化,编码
STM32 ADC的总转换时间为:TCONV = 采样时间 + 12.5个ADC周期(12位)
例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
1.9 校准
ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
建议在每次上电后执行一次校准
启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
1.10 硬件电路
第一个是电位器产生一个可调的电压,这里电位器的两个固定端,一个接3.3,一个接GND,这样中间的滑动端就可以输出一个0~3.3的可调电压输出了。这里可以接ADC的输入通道,比如PA0口;滑动端往上滑时,电压增大,往下滑时,电压减小。阻值不宜太小。
第二个是传感器输出电压的电路,一般是光敏电阻、热敏电阻、红外接收管、麦克风等。串联分压,当传感器阻值变小时,下拉作用变强,输出端电压就下降;反之,输出端电压增大。
第三个是电压转换电路,比如想测0~5V的VIN电压,但是ADC只能接收0~3.3V的电压,分压。
手册
2 AD单通道
2.1 接线图
2.2 模块封装
按这个初始化
相关库函数
// 在rcc.h中
// 配置ADCCLK分频器,2/4/6/8分频
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);
void ADC_DeInit(ADC_TypeDef* ADCx); // 恢复缺省设置
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct); // 初始化
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct); // 结构体初始化
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState); // 给ADC上电的
// 中断输出控制,用于某个中断,能不能通往NVIC
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);
// 复位校准/获取复位状态/开始校准/获取开始校准状态
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软件开始转换控制,软件触发
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
// 配置间断模式
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number); // 每隔几个通道间断一次
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState); // 是否启用间断模式
// ADC规则组通道配置
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
// ADC外部触发转换控制
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
// ADC获取转换值
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);
// ADC获取双模式转换值
uint32_t ADC_GetDualModeConversionValue(void);
// 获取标志位状态/清除标志位状态/获取中断状态/清除中断挂起位
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT);
void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);
// 带Injected是注入组的函数
版本1:单次转换非扫描
AD.c
#include "stm32f10x.h" // Device header
// AD初始化函数
void AD_Init(void)
{
// 1开启RCC时钟,ADC、GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72M / 6 = 12M
// 2配置GPIO模拟输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3配置多路开关,选择规则组的输入通道,指定通道/规则组序列器的次序/通道采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
// 4配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 外部触发选择,不使用外部触发
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 工作模式(独立/双ADC)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次/连续转换
ADC_InitStructure.ADC_NbrOfChannel = 1; // 通道数目
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 扫描/非扫描转换
// 单次转换非扫描模式
// 单次转换/连续转换 扫描/非扫描
ADC_Init(ADC1, &ADC_InitStructure);
// 5开关控制
ADC_Cmd(ADC1, ENABLE);
// 6校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
// 获取转换结果
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 返回转换值
}
版本2:连续转换非扫描
#include "stm32f10x.h" // Device header
// AD初始化函数
void AD_Init(void)
{
// 1开启RCC时钟,ADC、GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72M / 6 = 12M
// 2配置GPIO模拟输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3配置多路开关,选择规则组的输入通道,指定通道/规则组序列器的次序/通道采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
// 4配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 外部触发选择,不使用外部触发
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 工作模式(独立/双ADC)
// ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次/连续转换
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 单次/连续转换
ADC_InitStructure.ADC_NbrOfChannel = 1; // 通道数目
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 扫描/非扫描转换
// 单次转换非扫描模式
// 单次转换/连续转换 扫描/非扫描
ADC_Init(ADC1, &ADC_InitStructure);
// 5开关控制
ADC_Cmd(ADC1, ENABLE);
// 6校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
// 加到这里触发一次
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换
}
// 获取转换结果
uint16_t AD_GetValue(void)
{
// ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换
// while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 返回转换值
}
2.3 主函数
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t ADValue;
float voltage;
int main()
{
OLED_Init(); // 初始化OLED
AD_Init();
OLED_ShowString(1, 1, "ADValue:");
OLED_ShowString(2, 1, "Voltage:0.00v"); // 将转换结果换算成电压值
while (1)
{
ADValue = AD_GetValue();
voltage = (float)ADValue / 4095 * 3.3; // 实际电压值
OLED_ShowNum(1, 9, ADValue, 4);
OLED_ShowNum(2, 9, voltage, 1); // 显示整数部分
OLED_ShowNum(2, 11, (uint16_t)(voltage * 100) % 100, 2); // 显示小数
Delay_ms(100);
}
}
3 AD多通道
3.1 接线图
使用了4个通道
3.2 模块封装
单次转换非扫描
#include "stm32f10x.h" // Device header
// AD初始化函数
void AD_Init(void)
{
// 1开启RCC时钟,ADC、GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72M / 6 = 12M
// 2配置GPIO模拟输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3配置多路开关,选择规则组的输入通道,指定通道/规则组序列器的次序/通道采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
// 4配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 外部触发选择,不使用外部触发
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 工作模式(独立/双ADC)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStructure.ADC_NbrOfChannel = 1; // 通道数目
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描转换
// 单次转换非扫描模式
// 单次转换/连续转换 扫描/非扫描
ADC_Init(ADC1, &ADC_InitStructure);
// 5开关控制
ADC_Cmd(ADC1, ENABLE);
// 6校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
// 获取转换结果
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
// 3配置多路开关,选择规则组的输入通道,指定通道/规则组序列器的次序/通道采样时间
// 填充通道
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 返回转换值
}
3.3 主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t AD0, AD1, AD2, AD3;
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);
AD1 = AD_GetValue(ADC_Channel_1);
AD2 = AD_GetValue(ADC_Channel_2);
AD3 = AD_GetValue(ADC_Channel_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);
}
}