F
(
−
)
V_{REF(-)}
VREF(−)是DAC的参考电压,定义数据对应的电压范围(255对应3.3V或5V)。通常芯片的工作电压正极
V
c
c
V_{cc}
Vcc和
V
R
E
F
(
)
V_{REF(+)}
VREF(+)相同,接在一起;通常芯片的工作电压负极
G
N
D
GND
GND和
V
R
E
F
(
−
)
V_{REF(-)}
VREF(−)相同,接在一起。
1.3 STM32中的ADC基本结构
STM32中ADC的结构框图如上图所示。其“模拟至数字转换器”模块的工作模式与ADC0809在原理上完全相同。不同点有以下几点:
- 普通的ADC多路开关一般只选中一个,STM32的ADC可以同时选中多个通道进行转换,规则组最多同时选中16个通道,注入组一次最多可以选中4个通道。(以餐厅点菜模型为例,普通模式为每次点一个菜,做好菜后上菜;STM32可以做到每次列出一个菜单,规则组一次最多可以列16个菜,注入组一次最多可以列4个菜,做好后依次上菜)
- STM32中的ADC的转换结果会被存储在对应的数据寄存器中。对于规则组通道,其只有一个数据寄存器(餐桌上只能摆一个菜),后转换的数据会将之前转换的数据覆盖,之前转换的数据就会丢失。对于规则组通道,要想实现同时转换的功能,最好配合DMA来将转换后的数据及时转运,就可以保证转换的数据不会丢失了。对于注入组通道,它拥有4个数据寄存器(餐厅的VIP坐席,餐桌上一次可以摆四个菜)。对于注入组而言,就不用担心数据覆盖的问题了。一般情况下,使用规则组和DMA就可以满足大部分的使用需求。(这里只讲解规则组使用,注入组自行了解即可)
- 结构图的左下角为触发转换信号,对应ADC0809的START信号。STM32的触发转换信号来源有两种:软件触发和硬件触发。硬件触发信号可以来自于定时器的各个通道、定时器TRGO主模式的输出,外部中断EXTI。下表列出了ADC1和ADC2的触发源。(其中EXTI线11/TIM8_YRGO事件的选择需要使用AFIO端口重映射来配置)
定时器可以通过主模式输出TRGO控制ADC、DAC等外设,用于通过硬件电路自动触发转换。ADC经常需要过一个固定的时间段转换一次,例如可以每隔1ms转换一次。通过定时器,每隔1ms申请一次中断,在中断函数中手动通过软件触发开启一次转换也可以实现功能,但是频繁进中断会对主程序造成一定的影响。并且在不同的中断之间,由于优先级的不同,有可能会使某些中断不能及时得到响应。如果触发ADC的中断没有及时得到响应,那么ADC的转换频率就肯定会受影响了。所以对于这种需要频繁进中断,但是在中断函数中只完成了简单工作的情况,一般都会有硬件电路的支持。
-
在STM32中,
V
R
E
F
(
)
V_{REF(+)}
VREF(+)一般和
V
D
D
A
V_{DDA}
VDDA(ADC模块的正极供电引脚)接在一起,
V
R
E
F
(
−
)
V_{REF(-)}
VREF(−)一般和
V
S
S
A
V_{SSA}
VSSA(ADC模块的负极供电引脚)接在一起。本课程使用的芯片没有单独的
V
R
E
F
(
)
V_{REF(+)}
VREF(+)和
V
R
E
F
(
−
)
V_{REF(-)}
VREF(−)的引脚,它在芯片内部就已经和对应引脚连接在一起了。(
V
D
D
A
V_{DDA}
VDDA和
V
S
S
A
V_{SSA}
VSSA是STM32模拟部分的电源,例如ADC、RC振荡器、锁相环等,在套件中的最小系统板中已经将
V
D
D
A
V_{DDA}
VDDA与3.3V、
V
S
S
A
V_{SSA}
VSSA与GND相连接了)。 5. 这里ADC的时钟ADCCLK是来自于RCC的APB2时钟。由原理图可得,ADCCLK最大为14MHz,所以ADC预分频器只能选择6分频(得到12MHz)和8分频(得到9MHz)两个值。 6. ADC可以通过DMA请求信号触发DMA转运数据。(这部分内容会在下一小节STM32学习笔记(九)中涉及) 7. 模拟看门狗的功能是监测指定的通道。可以设置模拟看门狗的阈值高限(12位)、阈值底限(12位)和指定“看门”的通道。只要通道的电压值超过阈值范围,模拟看门狗就会“乱叫”,申请一个模拟看门狗的中断,之后通向NVIC。 8. 规则组和注入组在转换完成后会生成一个转换完成的信号。EOC为规则组转换完成的信号,JEOC为注入组转换完成的信号。这两个信号会在状态寄存器中置一个标志位,我们通过读取状态寄存器,就可以知道转换是否完成了。同时这两个标志位也可以通过配置通向NVIC申请中断。
1.4 STM32中ADC的输入通道
由ADC的内部结构可知,STM32的ADC对应16个输入通道。这16个输入通道对应的GPIO端口如下表所示:
上表展示了ADC通道和引脚复用之间的连接关系,这个对应关系也可以参考引脚定义表。可以看到,只有ADC1拥有温度传感器和内部参考电压的采样通道。ADC1和ADC2的引脚完全相同,ADC3有些是存在变化的。本节课程使用的STM32F103C8T6没有PC0到PC5的引脚,故也就不存在通道10到通道15。
参考引脚定义表可以看到,STM32的ADC1和ADC2的引脚是相同的。这样的设计是为双ADC模式服务的。关于双ADC模式的内容比较复杂,这里仅作简单了解即可。双ADC模式,即ADC1和ADC2同时工作,二者可以配合为同步模式、交叉模式等多种不同的工作模式。以交叉模式为例,ADC1和ADC2交叉对同一个通道进行采样,这样就可以进一步提高采样率。
1.5 STM32中的ADC的四种转换模式
STM32中的ADC的转换模式有以下四种:
- 单次转换非扫描模式
- 连续转换非扫描模式
- 单次转换扫描模式
- 连续转换扫描模式
ADC的16个序列就相当于一个“菜单”,在序列中可以填入不同的通道。在非扫描模式下,这个“菜单”就只对存放在序列1的通道起作用, 这时“菜单”中可以选中一组的方式就退化为简单地选中一个的模式了。 在每次转换开始前可以对序列中的通道进行更改。在扫描模式下,“菜单”将发挥作用。每次转换开始后,依次对定义的通道进行转换,并且将转换结果放在数据寄存器中。这里为了方式数据被覆盖导致丢失,就需要使用DMA及时将数据移走。
在单次转换模式中,需要手动触发转换。 ADC就会对序列中的通道进行转换,转换完成后,将转换结果储存在数据寄存器中,同时将EOC标志位置1,转换过程结束。在连续转换模式中,一次转换完成后不会停止,而是立刻开始下一轮的转换,并持续下去。这样就可以在最开始时触发一次,之后就可以一直转换了。这个模式的好处在于:开始转换后不需要等待,因为其一直都在转换,所以也不需要手动开启转换了,也不用判断转换是否结束。需要读取AD值时,直接在数据寄存器中读取即可。
在扫描模式的情况下,还可以使用间断模式。它的作用是在扫描的过程中,每隔几次转换就暂停,需要再次触发才能继续。该模式仅作了解即可。
1.6 使用ADC时的一些细节讨论
1.6.1 数据对齐
STM32中的ADC是12位的,但是数据寄存器拥有16位,故存在数据对齐的问题。数据右对齐,即作为转换结果的12位数据向右靠,高位补0;数据左对齐,即作为转换结果的12位数据向左靠,低位补0。在使用时通常使用数据右对齐,这样在读取时直接读取寄存器即可。如果选择左对齐直接读取,得到的数据会比实际的数据大16倍。当对分辨率的要求不高时(对电压仅作大概的判断即可)可以采用左对齐,将数据寄存器的高8位取出,就相当于舍弃了转换结果的4位的精度,12位的ADC退化位为8位的ADC。
1.6.2 转换时间
AD的转换时间是一个很短的时间,如果不需要极高的转换频率,那么转换时间是可以忽略的。那么转换时间具体是多少呢?
AD转换的步骤分别是:采样、保持、量化、编码。其中采样和保持可以看作一个过程,量化和编码可以看作一个过程。量化和编码实际上就是ADC逐次比较的过程,一般ADC的位数越多,所花费的时间就越长。采样和保持是为了保证在量化和编码的过程中输入电压的变化不会过大。在量化和编码之前,需要添加采样-保持电路,即需要设置一个采样开关,打开开关一段时间来收集电压(可以用一个小容量的电容来存储这个电压),存储完成之后断开开关,再进行之后的AD转换。这样就可以保证在量化和编码器件始终保持电压基本不变。这个采样时间是比较长的。所以AD转换所花费的时间可以分为:采样-保持电路的采样时间 + 量化和编码花费的时间。
- STM32 ADC总转换时间 = 采样时间 + 12.5个ADC周期
ADC的采样时间可以在程序中进行配置。之后花费12个ADC周期进行量化和编码,多余的0.5个周期完成了其他的工作。
最短的转换时间:当ADCCLK = 14MHz,采样时间为1.5个ADC周期时:
T
C
O
N
V
=
(
1.5
12.5
)
⋅
T
A
D
C
=
14
T
A
D
C
=
1
μ
s
T_{CONV}=(1.5 + 12.5)\cdot T_{ADC} = 14T_{ADC}=1\mu s
TCONV=(1.5+12.5)⋅TADC=14TADC=1μs
当然,可以通过设置将ADC的转换频率超过14MHz,这样ADC就会工作在超频状态下。超频时转换时间可能会更短,不过电路的稳定性将无法保证。
1.6.1 ADC校准
ADC有一个固定的内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。建议在每次上电后执行依次校准,且启动校准前,ADC必须处于关电状态超过至少两个ADC周期。
ADC的校准详细过程不需要掌握,在使用时在ADC初始化最后加几行固定的代码即可。
二、ADC 实际应用示例
2.1 ADC 常用库函数
- ADC的RCC时钟配置函数
该配置函数定义存放在stm32f10x_rcc.h文件中,用来配置ADCCLK分频器。它可以对APB2的72MHz时钟选择2、4、6、8分频,输出到ADCCLK。
void RCC\_ADCCLKConfig(uint32\_t RCC_PCLK2)
- ADC设置
// 恢复ADC缺省配置
void ADC\_DeInit(ADC_TypeDef\* ADCx);
// ADC初始化
void ADC\_Init(ADC_TypeDef\* ADCx, ADC_InitTypeDef\* ADC_InitStruct);
// ADC配置结构体初始化
void ADC\_StructInit(ADC_InitTypeDef\* ADC_InitStruct);
// ADC上电工作函数,即开关控制函数
void ADC\_Cmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
// ADC开启DMA输出信号
void ADC\_DMACmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
// ADC中断输出控制函数
void ADC\_ITConfig(ADC_TypeDef\* ADCx, uint16\_t ADC_IT, FunctionalState NewState);
// 下面4个函数用于ADC工作前的校准操作,在ADC初始化完成后依次调用即可
// ADC复位校准
void ADC\_ResetCalibration(ADC_TypeDef\* ADCx);
// ADC获取复位校准状态
FlagStatus ADC\_GetResetCalibrationStatus(ADC_TypeDef\* ADCx);
// ADC开始校准
void ADC\_StartCalibration(ADC_TypeDef\* ADCx);
// ADC获取开始校准状态
FlagStatus ADC\_GetCalibrationStatus(ADC_TypeDef\* ADCx);
// ADC软件触发转换,给CR2的SWSTART置1(开始转换后立即自动清0)
void ADC\_SoftwareStartConvCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
// ADC获取软件触发状态,获取CR2的SWSTART(开始转换规则通道)位
// 不能用它判断转换是否结束,一般不用,了解即可
FlagStatus ADC\_GetSoftwareStartConvStatus(ADC_TypeDef\* ADCx);
// 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获取转换值,获取AD转换的数据寄存器
uint16\_t ADC\_GetConversionValue(ADC_TypeDef\* ADCx);
// ADC获取双模式转换值,读取双ADC模式下ADC的转换结果
uint32\_t ADC\_GetDualModeConversionValue(void);
// ADC温度传感器、内部参考电压控制,开启内部的两个转换通道
void ADC\_TempSensorVrefintCmd(FunctionalState NewState);
// 下面的函数与操作标志位寄存器状态有关
// ADC获取标志位状态,可通过获取EOC标志位判断转换是否结束
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);
- 模拟看门狗配置(本节暂不涉及,需要可以了解)
// 对模拟看门狗进行配置
// 是否启动模拟看门狗
void ADC\_AnalogWatchdogCmd(ADC_TypeDef\* ADCx, uint32\_t ADC_AnalogWatchdog);
// 配置模拟看门狗高低阈值
void ADC\_AnalogWatchdogThresholdsConfig(ADC_TypeDef\* ADCx, uint16\_t HighThreshold, uint16\_t LowThreshold);
// 配置看门通道
void ADC\_AnalogWatchdogSingleChannelConfig(ADC_TypeDef\* ADCx, uint8\_t ADC_Channel);
- 注入组相关配置函数(本节暂不涉及,需要可以了解)
void ADC\_AutoInjectedConvCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
void ADC\_InjectedDiscModeCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
void ADC\_ExternalTrigInjectedConvConfig(ADC_TypeDef\* ADCx, uint32\_t ADC_ExternalTrigInjecConv);
void ADC\_ExternalTrigInjectedConvCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
void ADC\_SoftwareStartInjectedConvCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
FlagStatus ADC\_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef\* ADCx);
void ADC\_InjectedChannelConfig(ADC_TypeDef\* ADCx, uint8\_t ADC_Channel, uint8\_t Rank, uint8\_t ADC_SampleTime);
void ADC\_InjectedSequencerLengthConfig(ADC_TypeDef\* ADCx, uint8\_t Length);
void ADC\_SetInjectedOffset(ADC_TypeDef\* ADCx, uint8\_t ADC_InjectedChannel, uint16\_t Offset);
uint16\_t ADC\_GetInjectedConversionValue(ADC_TypeDef\* ADCx, uint8\_t ADC_InjectedChannel);
- ADC间断模式配置
// 下面两个函数用来配置STM32中ADC的间断模式
// 配置每隔几个通道间断依次
void ADC\_DiscModeChannelCountConfig(ADC_TypeDef\* ADCx, uint8\_t Number);
// 开启间断模式
void ADC\_DiscModeCmd(ADC_TypeDef\* ADCx, FunctionalState NewState);
2.2 ADC 单通道转换
AD.c
#include "stm32f10x.h" // Device header
/\*\*
\* @brief ADC初始化函数(软件触发,且这里不使用模拟看门狗和中断)
\* @param 无
\* @retval 无
\*/
void AD\_Init(void)
{
// 1. RCC开启时钟
RCC\_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC\_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC\_ADCCLKConfig(RCC_PCLK2_Div6); // ADCCLK = 72MHz / 6 = 12MHz
// 2. 配置GPIO
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);
// 3. 将指定的GPIO端口接入规则组列表中
ADC\_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // 把通道0填入序列1中,通道的采样周期是55.5个ADCCLK的周期
// 4. 配置ADC
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // ADC模式(独立模式或双ADC模式):独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // ADC数据对齐:右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // ADC外部触发源选择:不使用外部源触发(这里使用软件触发)
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // ADC连续转换模式:单次转换
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // ADC扫描模式:非扫描
ADC_InitStruct.ADC_NbrOfChannel = 1; // 扫描模式下通道的数量
ADC\_Init(ADC1, &ADC_InitStruct);
/\* 中断和模拟看门狗在此配置 \*/
// 5. 开关控制
ADC\_Cmd(ADC1, ENABLE);
// 6. 对ADC进行校准
ADC\_ResetCalibration(ADC1); // 复位校准
while (ADC\_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成
ADC\_StartCalibration(ADC1); // 开始校准
while (ADC\_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
/\*\*
\* @brief ADC结果读取函数(软件触发)
\* @param 无
\* @retval 转换之后的结果
\*/
uint16\_t AD\_GetValue(void)
{
// 1. 软件触发开启转换
ADC\_SoftwareStartConvCmd(ADC1, ENABLE);
// 2. 等待转换完成(获取标志位状态,等待EOC标志位置1)
while (ADC\_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 转换未完成则等待(55.5T + 12.5T = 68T,结果大概为5.6us)
// 3. 读取ADC数据寄存器并返回
return ADC\_GetConversionValue(ADC1); // 读取之后会自动清除EOC标志位
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16\_t AD_Value;
float Voltage;
int main()
{
OLED\_Init();
AD\_Init();
OLED\_ShowString(1, 1, "AD\_Value:");
OLED\_ShowString(2, 1, "Voltage:0.00V");
while(1)
{
AD_Value = AD\_GetValue();
Voltage = (float)AD_Value / 4095 \* 3.3; // 整数除以小数会舍弃小数部分
OLED\_ShowNum(1, 10, AD_Value, 4);
OLED\_ShowNum(2, 9, Voltage, 1); // 显示整数部分
OLED\_ShowNum(2, 11, (uint16\_t)(Voltage \* 100) % 100, 2); // 显示小数部分
Delay\_ms(100);
}
}
2.3 ADC 多通道转换
AD.c
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新