【STM32】ADC数模转换器

【STM32】ADC数模转换器

  • 一、ADC数模---模数转换器
  • [二、逐次逼近型 ADC](#二、逐次逼近型 ADC)
    • [2.1 ADC0809芯片](#2.1 ADC0809芯片)
    • [2.2 STM32里的ADC](#2.2 STM32里的ADC)
  • 三、ADC基本结构
    • [3.1 基本结构图](#3.1 基本结构图)
    • [3.2 ADC通道和引脚复用关系](#3.2 ADC通道和引脚复用关系)
    • [3.3 规则组的4种转换模式](#3.3 规则组的4种转换模式)
    • [3.4 触发控制(规则组的触发源)](#3.4 触发控制(规则组的触发源))
    • [3.5 其它有关细节](#3.5 其它有关细节)
  • 四、ADC程序
    • [4.1 ADC+单通道(单次转换非扫描)](#4.1 ADC+单通道(单次转换非扫描))
    • [4.2 ADC+多通道](#4.2 ADC+多通道)

一、ADC数模---模数转换器

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

  2. 12位(0~2¹²⁻¹)逐次逼近型ADC,1μs转换时间 1MHz,32ADC的最快转换频率

  3. 输入电压范围0 - 3.3V,转换结果0 - 4095

  4. 18个输入通道,可测量16个外部(GPIO口)和2个内部信号源(内部温度传感器,内部参考电压 1.2V)

  5. 规则组(常规使用)和注入组(突发事件)两个转换单元

  6. 模拟看门狗自动监测输入电压范围(高于阈值/低于时,看门狗会触发中断)
    C8T6的资源:ADC1,ADC2,10个外部输入通道

二、逐次逼近型 ADC

2.1 ADC0809芯片

以ADC0809芯片为例

ADC(模数转换器)的作用是把模拟信号(像连续变化的电压)转换成数字信号。以逐次逼近型ADC为例,转换过程大致如下:

  1. 启动转换:当START信号到来时,启动ADC转换。
  2. 逐次逼近比较 :逐次逼近寄存器(SAR)从最高位开始,依次假设每一位为"1",并将这个假设的数字量送入DAC(数模转换器)。DAC会把数字量转换成对应的模拟电压。然后,比较器将这个模拟电压与输入的模拟电压进行比较。
    • 如果DAC输出的模拟电压小于输入的模拟电压,说明假设的这一位"1"是合理的,就保留这一位为"1"。
    • 如果DAC输出的模拟电压大于输入的模拟电压,说明假设的这一位"1"不合理,就将这一位置为"0"。
    • 按照这样的方式,从最高位到最低位依次进行判断,直到确定所有位的值。
  3. 转换结果输出:当所有位都判断完毕后,逐次逼近寄存器中的数字量就是输入模拟电压对应的数字量。此时,EOC(转换结束信号)会发出提示,当OE(输出使能)有效时,8位三态锁存缓冲器就会把转换得到的数字量输出(如D0 - D7)。

2.2 STM32里的ADC

  1. 有 18 个输入通道(如 ADCx_IN0 - ADCx_IN15 等)。
  2. 模拟多路开关,可选择最多 4 个注入通道、最多 16 个规则通道。
  3. 包含注入通道数据寄存器(4×16 位,无需担心数据覆盖)、规则通道数据寄存器(16 位,可配合 DMA)。
  • 规则组:可以把它想象成日常的 "常规工作流程",按照预先设定好的顺序,对多个指定的模拟输入通道依次进行转换。转换后的数据存储在规则通道数据寄存器中,并且可以配合 DMA(直接内存访问)进行数据传输,这样在进行大量数据转换时,不需要 CPU 频繁介入,提高了系统效率,适合连续、大量的常规数据采集场景。
  • 注入组:注入组类似于 "突发任务" 或者 "优先任务" 组。当系统有一些紧急或者需要优先处理的模拟信号转换需求时,就可以使用注入组。它可以在规则组转换的过程中插入进来,优先进行转换。转换后的数据存储在注入通道数据寄存器中,这些寄存器通常有多个(比如 4 个 16 位的寄存器),可以存储多组注入转换的数据,而且由于是优先处理的 "突发" 任务,不需要像规则组那样依赖 DMA 进行大规模数据传输,适合小批量、高优先级的转换场景。
  • 两者关系:规则通道负责常规、连续的数据采集,而注入通道可中断规则通道的转换过程,优先执行自身的转换任务,完成后再回到规则通道继续之前的转换。这样的设计能灵活管理不同优先级的模拟信号转换,既保障了常规数据的采集,又能及时响应特殊、紧急的转换需求,提升了 ADC 对复杂场景的适应能力。
  • PS:规则通道数据寄存器只有一个16位的寄存器,需要配合DMA及时将数据转运,否则会出现数据覆盖丢失的问题。
  1. 时钟部分(ADCCLK,最大 14MHz,属于 RCC,只能选择 6/8 分频,来源于 RCC)。
  2. 触发转换部分,涉及多种触发源(如 TIM1、TIM2 等定时器相关触发,以及外部触发等)
  3. 模拟看门狗等模块,用于监测输入电压范围,可触发中断(至 NVIC 的 ADC 中断)。

三、ADC基本结构

3.1 基本结构图


按照此图来编写程序

3.2 ADC通道和引脚复用关系

3.3 规则组的4种转换模式

  1. 单次转换 + 非扫描模式
    当 ADC 接收到触发信号后,仅对一个预设通道进行一次转换,转换完成后便停止工作。若需要再次转换,必须重新施加触发信号。
  2. 连续转换 + 非扫描模式
    一次触发后,ADC 会对同一个通道进行连续不断的转换,无需再次触发,直到手动停止转换或系统复位。
  3. 单次转换 + 扫描模式
    触发后,ADC 会按预设的通道序列(如通道 1→通道 2→通道 3)依次对多个通道各进行一次转换,完成所有通道的一轮扫描后自动停止。
  4. 连续转换 + 扫描模式
    一次触发后,ADC 会按预设通道序列循环连续扫描(如通道 1→通道 2→通道 3→通道 1→通道 2...),不断重复转换过程,直到手动停止。

3.4 触发控制(规则组的触发源)

3.5 其它有关细节

  1. 采用数据右对齐
  2. 转换时间
    AD转换的步骤:采样,保持,量化,编码(ADC逐次比较的过程)
    STM32 ADC的总转换时间(T_{CONV}) = 采样时间 + 12.5个ADC周期
    例如:当ADCCLK = 14MHz,采样时间为1.5个ADC周期
    (T_{CONV}) = 1.5 + 12.5 = 14个ADC周期 = 1μs
  3. 校准
    ADC有个内置自校准模式,校准可大幅度减小因内部电容器组的变化而造成的精准度误差,建议每次上电后执行一次校准,启动校准前,ADC必须处于关电状态超过至少两个ADC时钟周期(STM32会自动完成)
  4. 硬件电路

四、ADC程序

基本步骤
①开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK
②配置GPIO成模拟输入的模式
③配置多路开关,把左边通道接到右边的规则组里
④配置ADC转换器,用结构体来配置
⑤调用ADC_cmd()函数

4.1 ADC+单通道(单次转换非扫描)

接线图

  • AD.h 代码模块
c 复制代码
#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(void);

#endif
  • AD.c 代码模块
c 复制代码
#include "stm32f10x.h"                  // Device header


void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*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);					//将PA0引脚初始化为模拟输入
	
	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		//规则组序列1的位置,配置为通道0
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:无
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}
  • main.c 代码模块
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;			//定义AD值变量
float Voltage;				//定义电压变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();			//OLED初始化
	AD_Init();				//AD初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Voltage:0.00V");
	
	while (1)
	{
		ADValue = AD_GetValue();					//获取AD转换的值
		Voltage = (float)ADValue / 4095 * 3.3;		//将AD值线性变换到0~3.3的范围,表示电压
		
		OLED_ShowNum(1, 9, ADValue, 4);				//显示AD值
		OLED_ShowNum(2, 9, Voltage, 1);				//显示电压值的整数部分
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);	//显示电压值的小数部分
		
		Delay_ms(100);			//延时100ms,手动增加一些转换的间隔时间
	}
}

4.2 ADC+多通道

采用扫描模式实现多通道,最好配合DMA防止数据覆盖
本次利用单次转换非扫描模式,只要在每次触发转换之前,手动更改一下列表第一个位置通道即可

接线图

  • AD.h 代码模块
c 复制代码
#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);

#endif
  • AD.c 代码模块
c 复制代码
#include "stm32f10x.h"                  // Device header


void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	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);					//将PA0、PA1、PA2和PA3引脚初始化为模拟输入
	
	/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);	//在每次转换前,根据函数形参灵活更改规则组的通道1
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}
  • main.c
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0, AD1, AD2, AD3;	//定义AD值变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	
	/*显示静态字符串*/
	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);		//单次启动ADC,转换通道0
		AD1 = AD_GetValue(ADC_Channel_1);		//单次启动ADC,转换通道1
		AD2 = AD_GetValue(ADC_Channel_2);		//单次启动ADC,转换通道2
		AD3 = AD_GetValue(ADC_Channel_3);		//单次启动ADC,转换通道3
		
		OLED_ShowNum(1, 5, AD0, 4);				//显示通道0的转换结果AD0
		OLED_ShowNum(2, 5, AD1, 4);				//显示通道1的转换结果AD1
		OLED_ShowNum(3, 5, AD2, 4);				//显示通道2的转换结果AD2
		OLED_ShowNum(4, 5, AD3, 4);				//显示通道3的转换结果AD3
		
		Delay_ms(100);			//延时100ms,手动增加一些转换的间隔时间
	}
}
相关推荐
充哥单片机设计2 小时前
【STM32项目开源】基于STM32的智能点滴输液系统
stm32·单片机·嵌入式硬件
啊?啊?5 小时前
C/C++练手小项目之倒计时与下载进度条模拟
c语言·开发语言·c++
正点原子5 小时前
正点原子 x STM32:智能加速边缘AI应用开发!
人工智能·stm32·嵌入式硬件
田甲5 小时前
【STM32】基于串口的bootloader
stm32·单片机·嵌入式硬件
杰尼君6 小时前
STM32CubeMX笔记(11)-- AD模块使用
笔记·stm32·嵌入式硬件
lingzhilab9 小时前
零知IDE——STM32F407VET6与ADS1115模数转换器实现多通道数据采集显示系统
stm32·单片机·开源
xxy.c12 小时前
基于IMX6ULL的时钟,定时器(EPIT,GPT)
单片机·嵌入式硬件·fpga开发
happygrilclh13 小时前
stm32L496 flash 分配
stm32·单片机·嵌入式硬件
古译汉书13 小时前
嵌入式铁头山羊STM32-各章节详细笔记-查阅传送门
数据结构·笔记·stm32·单片机·嵌入式硬件·个人开发