小李不想当小白头像
关注
ADC模数转换器(STM32标准库学习笔记)封面图

ADC模数转换器(STM32标准库学习笔记)

在上一篇文章中,我们学习了 TIM 编码器接口,掌握了如何通过定时器的编码器模式来读取正交编码器的位置与速度信息,从而实现对电机等旋转部件的精确测量。而在实际嵌入式项目中,除了数字量的测量,我们往往还需要采集来自传感器的模拟信号,例如温度、光照、电位器电压等。这些模拟量在自然界中是连续变化的,而单片机只能处理数字信号,因此就需要一种能够将模拟信号转换为数字信号的器件——ADC(模数转换器)。

本文作为 STM32 标准库学习笔记的延续,将围绕 STM32F103C8T6 的 ADC 模数转换器展开,从基本原理、内部结构、转换模式,到硬件电路与库函数配置,再到单通道与多通道的完整实战实例,系统梳理 ADC 的使用方法,并分享实践过程中遇到的典型问题与解决方案。

如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!

一、ADC模数转换器

1.1 ADC简介

ADC(Analog-Digital Converter)模拟-数字转换器

ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁

12位逐次逼近型ADC,1us转换时间

输入电压范围:0~3.3V,转换结果范围:0~4095

18个输入通道,可测量16个外部和2个内部信号源

规则组和注入组两个转换单元

模拟看门狗自动监测输入电压范围

STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道

1.2 逐次逼近型ADC

1.3 ADC框图

其中VREF+、VREF-、VDDA和VSSA:

VREF+和VREF-是ADC的参考电压,决定了ADC输入电压的范围

VDDA和VSSA是ADC的供电引脚

ADC预分频器:

其中需要注意!!!

这个ADC的预分频器很尴尬,ADCCLK能输入最大14MHz的频率,但是APB2时钟72MHzAPB2时钟通过ADC预分频器进行分频,得到ADCCLK,ADC的预分频器如果选择2分频,72M / 2 = 36M超出允许范围;如果选择4分频,72M / 4 = 18M还是超出允许范围;所以只能选择6分频,72M / 6 = 12M;和选择8分频,72M / 8 = 9M;

1.4 ADC基本结构

1.5 输入通道

1.6 转换模式

(1)单次转换,非扫描模式

(2)连续转换,非扫描模式

(3)单次转换,扫描模式

(4)连续转换,扫描模式

1.7 触发控制

1.8 数据对齐

1.9 转换时间

AD转换的步骤:采样,保持,量化,编码

STM32 ADC的总转换时间为:

TCONV = 采样时间 + 12.5个ADC周期

例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期

TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs

1.10 校准

ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差

建议在每次上电后执行一次校准

启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期

1.11 硬件电路

分压电阻:

传感器:

我们这节需要用到AO这个引脚↓↓

传感器模块大概电路:

1.12 AD通道的配置

第一步,开启RCC时钟,包括ADC和GPIO的时钟,同时配置ADCCLK的分频器;

第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式

第三步,配置这里的多路开关,把左边的通道接入到右边的规则组列表里

第四步,配置ADC转换器了,在库函数里,是用结构体来配置的,可以配置这一大块电路的参数,包括ADC是单次转换还是连续转换、扫描还是非扫描、有几个通道,触发源是什么,数据对齐是左对齐还是右对齐;

如果需要模拟看门狗,那会有几个函数用来配置阈值和监测通道的;

如果想要开启中断,那就在中断输出控制里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置一下优先级,这样就能触发中断了;

第五步,开关控制,调用一下ADC_Cmd函数,开启ADC,当然在开启ADC之后,根据手册里的建议,我们还可以对ADC进行一下校准,这样可以减小误差;

最后,在ADC工作时,如果想要软件触发转换,有函数可以触发,如果想读取转换结果,也有函数可以读取结果;

1.13 实例一 7.1.1 AD单通道

用到的库函数:

//配置ADC分频器,对APB2的72MHz的时钟进行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);

//ADC供电使能

void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);

//DMA输出信号使能

void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

//ADC的中断输出控制

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);

//ADC获取软件开始转换状态(但和获取软件控制转换是否结束毫无关系!!只能查看开始标志位,但开始标志位在开始转换时清除,不是在结束转换时清除)

FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);

//间断模式相关,每隔几个通道间断一次

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_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

//ADC外部触发转换控制,就是是否允许外部触发转换

void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

//ADC获取转换值

uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

//ADC获取双模式转换值,这个是双ADC模式读取转换结果的函数

uint32_t ADC_GetDualModeConversionValue(void);

//模拟看门狗使能

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);

//ADC 温度传感器、内部参考电压控制使能,用来开启内部的两个通道,不然读不到ADC 温度传感器、内部参考电压的结果

void ADC_TempSensorVrefintCmd(FunctionalState NewState);

//获取标志位状态(真正能获取结束转换标志,然后参数给EOC的标志位,判断EOC标志位是不是置1了,如果转换结束,EOC标志位置1,调用本函数判断标志位)

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);

注意!!配置要测量ADC的电压的GPIO口需要配置成GPIO_Mode_AIN模拟输入模式,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰,所以AIN模式就是ADC的专属模式

AD.c

-----------------------------

#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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);

	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);
	
	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);
	
}

-----------------------------

main.c

-----------------------------

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;
float Voltage;

int main(void)
{
	OLED_Init();
	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_ShowChar(2,10,'.');
		OLED_ShowNum(2,11,(uint16_t)(Voltage * 100) % 100,2);
		
		Delay_ms(100);
	}
}

-----------------------------

1.14 实例二 7.2.1 AD多通道

使用AD多通道时,在扫描模式下,最好使用DMA配合转运数据,手动转运数据是比较困难的;扫描模式中启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的中断位,也不会触发中断,只有在整个列表都转换完成后,才会产生一次EOC标志位,才能触发中断

不使用DMA实现多通道采集的方法:

1.可以使用单次转换、非扫描的模式,来实现多通道;

2.只需要在每次触发转换之前,手动更改一下列表第一个位置的通道就行了,比如第一次转换,先写入通道0,之后触发、等待、读值;第二次转换,再先把通道0改成通道1,之后触发、等待、读值等等;

AD.c

-----------------------------

#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

	
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1,ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while( ADC_GetResetCalibrationStatus(ADC1) == SET );
	ADC_StartCalibration(ADC1);
	while( ADC_GetCalibrationStatus(ADC1) == SET);

//	ADC_SoftwareStartConvCmd(ADC1,ENABLE);	
}
	
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	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);
	
}

-----------------------------

main.c

-----------------------------

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0=0,AD1,AD2,AD3;


int main(void)
{
	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);
	}
}

-----------------------------

1.15 问题

出现的问题:ADC1,2,3数据均正常,但电位器的ADC0数据保持在3955几乎不变,无论这么修改电位器的阻值,ADC0的数据均不变

问题产生的原因:连续转换模式 + 软件触发冲突,我现在是手动切换通道的单通道单次采集,必须关掉连续模式

解决方法:ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;改为ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 修改为DISABLE,单次转换

随后读取正常

二、总结

本文围绕 STM32F103C8T6 的 ADC 模数转换器,系统梳理了从基本原理到实际应用的全过程。ADC 作为连接模拟世界与数字世界的桥梁,其核心价值在于将连续变化的模拟电压转换为数字变量,为后续的数据处理与控制逻辑提供基础。

在原理层面,本文介绍了 12 位逐次逼近型 ADC 的工作机制、输入电压范围(0~3.3V)与转换结果范围(0~4095),并详细解析了 ADC 框图、基本结构、输入通道、转换模式、触发控制、数据对齐、转换时间与校准等关键知识点。其中,ADC 预分频器的配置需要特别注意:由于 ADCCLK 最大允许 14MHz,而 APB2 时钟为 72MHz,因此只能选择 6 分频(12MHz)或 8 分频(9MHz),2 分频与 4 分频均会超出允许范围。

在实践层面,本文通过两个实例演示了 ADC 的完整配置流程。单通道采集实例展示了从开启 RCC 时钟、配置 GPIO 为模拟输入、设置规则组通道、初始化 ADC 结构体,到校准、软件触发与读取转换结果的完整步骤;多通道采集实例则介绍了在不使用 DMA 的情况下,通过单次转换、非扫描模式配合手动切换通道来实现多通道采集的方法。

针对实践过程中遇到的典型问题——电位器 ADC0 数据保持不变,本文也给出了分析与解决方案:该问题源于连续转换模式与软件触发的冲突,将 ADC_ContinuousConvMode 由 ENABLE 改为 DISABLE 后即可恢复正常读取。这一排查过程也提醒我们,在手动切换通道的单通道单次采集场景下,务必关闭连续转换模式。

总体而言,掌握 ADC 的配置要点与常见问题排查方法,是进行模拟信号采集相关开发的重要基础。希望本文能够帮助读者快速上手 STM32 的 ADC 应用,并在实际项目中灵活运用。

都看到这了点点赞吧!

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/xiaolitongxue222/article/details/165492841

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--