STM32之ADC详解
一、ADC概述
ADC(模拟量转数字量转换器),在 STM32 开发中,利用 ADC 端口的电压数据,转换为对应的具体数字量数据内容。可通过 ADC 方式获取常用数据内容有:
- 光敏电阻、电池电量、油箱油量
ADC 转换的数据,可用于执行器控制行为,如低电量警告、油箱油量不足警告(即【阈值警告】 【阈值处理】 ,通过设置阈值,数据达到或超出时触发相应警示或控制逻辑 ) 。
二、ADC 工作原理
1.ADC 的主要特征
2. ADC 内部架构框图
一、整体架构与核心组件
- 模拟输入部分
- 外部输入通道:
ADCx_IN0 ~ ADCx_IN15
是 16 组外部模拟信号输入引脚,可连接光敏电阻、电压传感器等外设,采集外部模拟电压。- 内部传感器:集成温度传感器(需结合
V_REFINT
参考电压工作 ),用于检测芯片内部温度,方便实现温度补偿、过热保护等功能。- 模拟多路开关:最多支持 16 通道规则通道、4 通道注入通道切换,按需选通一路模拟信号送入转换器,实现多通道分时复用。
- 转换核心
- 模拟至数字转换器:采用逐次逼近法,以
ADCCLK
(来自 ADC 预分频器,需配置分频保证时钟稳定 )为基准时钟,将模拟电压与内部基准电压比较,逐位确定数字量,完成0~3.3V
(Vref+ = 3.3V
、Vref- = 0V
时 )模拟信号到 12 位数字量(范围0000 0000 0000 ~ 1111 1111 1111
)的转换。- 参考电压:
V_REF+
、V_REF-
是 ADC 转换基准,决定量程。3.3V
输入时,电压分辨率为3.3V/4096 ≈ 0.0008V
,即 1 个数字量对应约0.8mV
电压变化。
二、通道与转换模式
- 规则通道
- 通道特点:最多 16 通道,用于常规、连续的多通道采样,像循环采集电池电压、多路传感器数据。
- 触发控制:通过
EXTSEL[2:0]
选择定时器触发源(如TIM1_CH1
、TIM2_CH2
等 ),实现定时采样;也可由外部中断触发,灵活适配不同场景需求。- 数据存储:转换结果存入
规则通道数据寄存器(16 位)
,支持 DMA 请求,转换完成后直接通过 DMA 传输数据到内存,减轻 CPU 负担。- 注入通道
- 通道特点:最多 4 通道,优先级高于规则通道,用于紧急、需快速响应的采样(如安全阈值监测 )。
- 触发控制:由
JEXTSEL[2:0]
选定时器触发源(如TIM1_TRGO
、TIM4_CH3
等 ),或JEXTRIG
控制位手动触发。- 数据存储:结果存入
注入通道数据寄存器(4×16 位)
,可独立处理,快速响应特殊需求。
三、中断与阈值监测(模拟看门狗)
- 中断机制
- 转换结束中断:规则通道(
EOC
)、注入通道(JEOC
)转换完成时,置标志位并可使能中断,触发ADC中断
到 NVIC,通知 CPU 读取数据,实现实时处理。- 阈值中断:模拟看门狗比较转换结果与
阈值高限(12 位)
、阈值低限(12 位)
,超出范围置AWD
标志位,使能中断(AWDIE
)后触发中断,用于电压超限报警(如电池过压 / 欠压、传感器异常 )。- 模拟看门狗:实时监控 ADC 转换结果,一旦超出设定阈值,立即触发中断或标志位,快速响应异常,保障系统安全。
四、工作流程总结
- 信号输入:外部 / 内部模拟信号经
ADCx_IN
或内部传感器进入,模拟多路开关选通通道。- 触发转换:规则 / 注入通道通过定时器、外部中断等触发,启动
模拟至数字转换器
工作。- 数据转换:逐次逼近法转换模拟电压为 12 位数字量,存入对应数据寄存器。
- 结果处理:可触发中断通知 CPU 读取,或通过 DMA 传输数据;模拟看门狗实时监测,超限触发报警,实现从模拟信号采集到数字信号处理、异常响应的完整流程,支撑 STM32 对模拟量的精准采集与智能控制 。
五、举例讲解
一、场景与需求
- 常规任务:每隔 100ms 采集 3 路信号
- 电池电压(
ADC1_IN0
,规则通道)- 车外温度(
ADC1_IN1
,规则通道,接温度传感器)- 光照强度(
ADC1_IN2
,规则通道,接光敏电阻)- 紧急任务:实时监测电池电压,一旦超出
2.8V~3.6V
范围,立即触发报警
- 复用电池电压信号到
ADC1_IN8
(注入通道,优先级更高)二、工作流程拆解(多转换触发逻辑)
1. 硬件连接与通道准备
- 外部信号接入:
- 电池电压、温度传感器、光敏电阻的模拟信号,分别接到
ADC1_IN0
/IN1
/IN2
(规则通道);- 电池电压同时接到
ADC1_IN8
(注入通道,用于紧急阈值监测)。- 多路开关配置:
- 规则通道:使能
IN0
/IN1
/IN2
,共 3 路,用于循环采样;- 注入通道:使能
IN8
,共 1 路,用于紧急监测。2. 触发转换的两种方式
(1) 规则通道触发(常规采集)
- 触发源:定时器触发(如
TIM3_TRGO
,配置为 100ms 触发一次)。- 流程:
① 定时器每 100ms 产生一个触发信号 → 触发规则通道转换;
② 模拟多路开关按顺序选通IN0
→IN1
→IN2
;
③ ADC 依次对 3 路信号进行转换(逐次逼近法),结果存入规则数据寄存器。(2) 注入通道触发(紧急监测)
- 触发源:软件触发 + 模拟看门狗(双重保障)。
- 流程:
① 初始触发:系统启动时,手动触发一次注入通道转换(读取初始电池电压);
② 持续监测:ADC 转换后,模拟看门狗自动比较结果与阈值(2.8V~3.6V
):
- 若在范围内:不触发中断,等待下一次规则通道触发时,顺带重新触发注入转换(或定时触发);
- 若超出范围:立即置
AWD
标志位 → 触发注入中断 → CPU 跳转到中断函数处理(如点亮故障灯、记录日志)。3. 数据处理与响应
规则通道数据:
转换完成后,通过 DMA 自动将 3 路结果搬运到内存数组 → 程序读取数组,计算电池电量、温度值、光照强度,更新仪表盘显示。注入通道数据:
若触发中断(电压超限):
① 中断函数中读取注入数据寄存器 → 获取实时电池电压;
② 执行紧急逻辑(如:点亮红色故障灯、发送 CAN 报警帧、限制非关键用电设备)。4. 多转换并行的关键逻辑
- 优先级:注入通道优先级 > 规则通道。若规则通道转换中触发注入中断,ADC 会暂停规则转换,优先处理注入通道,保障紧急任务响应。
- 资源复用:同一模拟信号(如电池电压)可接入多个通道(规则 + 注入),实现 “常规轮询 + 紧急监测” 的差异化需求。
3. ADC 数据转换规则
ADC 转换核心信息:
- 转换方法:ADC 转换器采用逐次逼近法进行数据转换
- 采样精度:12 位,数值范围
0000 0000 0000 ~ 1111 1111 1111
(对应十进制0 ~ 4095
) - 参考电压:
Vref+ = 3.3V
,Vref- = 0V
- 电压分辨率:
3.3V / 4096 ≈ 0.0008V
(即 1 个 ADC 数值对应约 0.8mV 电压 ) - 示例场景:假设 ADC 读取到电压为
1.83V
,可基于上述参数换算数字量
三、ADC 编程实现和相关寄存器
1. ADC 时钟问题
- 分析依据:根据原理图进行分析
- 引脚与通道对应:当前引脚为 PF8 引脚,其对应的 ADC 通道是 ADC3_IN6
- 时钟归属:ADC3 所在的时钟为 APB2 总线时钟,即 ADC3 的时钟由 APB2 总线提供 ,在 STM32 中,不同的外设会挂载在不同的总线(如 APB1、APB2 等 )上,其时钟由对应总线时钟源分频等配置后提供,这里明确了 ADC3 依赖 APB2 总线时钟来进行工作时序的驱动 。
2. ADC_CR1 寄存器
3. ADC_CR2 寄存器
当前寄存器主要控制,ADC 采用数据通道,通信触发规则,数据对齐方式,校验和 ADC开启。
ADC_CR2寄存器的配置内容:
- SWSTART(位 22):置 1,用于开始规则通道的转换
- EXTTRIG(位 20):置 1,使能规则通道的外部触发转换模式
- EXTSEL(位 19 - 17):设为 111,选择启动规则通道组转换的外部事件
- ALIGN(位 11):置 0,配置数据右对齐
- RSTCAL(位 3):置 1,执行复位校准操作
- CAL(位 2):置 1,进行 A/D 校准
- CONT(位 1):置 1,使能连续转换模式
- ADON(位 0):置 1,开启 A/D 转换器 ,各配置项共同定义了 ADC 的转换启动、触发、校准、数据对齐及运行模式等关键参数 。
4. ADC_SMPR 寄存器
5. ADC_SQR 寄存器
在 ADC 规则通道配置中,需两个寄存器配合实现:
- ADC_SQR1:用于配置规则通道开启的数量(即决定要转换的规则通道总数 )。
- ADC_SQR3:用于配置 SQ1(规则通道序列中的第 1 个转换通道 )寄存器位对应的具体通道,此处为
ADC3_IN6
(指定第 1 个转换的规则通道是ADC3
的IN6
通道 ) 。
四、示例代码:
adc.c:
#include "adc.h"void LSEN_Init(void)
{// 1. RCC 时钟使能,需要提供 GPIOF 和 ADC3 RCC->APB2ENR |= (0x01 << 7) | (0x01 << 15);// 2. GPIOF --> PF8 模拟输入模式 ==> 0000GPIOF->CRH &= ~(0x0F);/*3. ADC 配置*//*3.1 ADC 预分配倍数配置因为当前 STM32F103ZET6 对应 72 MHz,ADCCLK 不得大于 14 MHz,预分频倍数最小可以选择 6*/RCC->CFGR &= ~(0x03 << 14);RCC->CFGR |= (0x02 << 14);/*3.2 配置 ADC 的工作通道选择工作通道为 ADC3_IN6,规则通道打开一个,配置 SQ1*/ADC3->SQR1 &= ~(0x0F << 20);ADC3->SQR3 &= ~(0x1F);ADC3->SQR3 |= 0x06;/*3.3 ADC 采用周期采样周期选择 239.5 + 12.5 最大 ADC 采样周期,可以获取到更大的数据精度。*/ADC3->SMPR2 |= (0x07 << 18);/*3.4. 配置 ADC CR 寄存器相关内容CR1- DUALMOD位 [19:16] : ADC 独立模式 ==> 0000- SCAN [位8] : 扫描模式关闭 ==> 0CR2- SWSTART [位22] : 开始转换规则通道 ==> 1- EXTTRIG [位20]:规则通道的外部触发转换模式 ==> 1- EXTSEL [位19:17]: 选择启动规则通道组转换的外部事件 ==> 111- ALIGN [位11]::数据对齐(Data alignment) ==> 0 右对齐- CONT [位1]::连续转换(Continuous conversion) ==> 1*/ADC3->CR1 &= ~(0x0F << 16);ADC3->CR1 &= ~(0x01 << 8);ADC3->CR2 &= ~(0xFFFFFFFF);ADC3->CR2 |= (0x01 << 22); // SWSTART [位22] : 开始转换规则通道 ==> 1ADC3->CR2 |= (0x01 << 20); // EXTTRIG [位20]:规则通道的外部触发转换模式 ==> 1ADC3->CR2 |= (0x07 << 17); // EXTSEL [位19:17]: 选择启动规则通道组转换的外部事件 ==> 111ADC3->CR2 &= ~(0x01 << 11); // ALIGN [位11]::数据对齐(Data alignment) ==> 0 右对齐ADC3->CR2 |= (0x01 << 1);/*3.5 ADC 复位 + 校准ADC 自校准 + 重启过程- RSTCAL [位3]::复位校准(Reset calibration) ==> 1 - CAL [位2]::A/D校准(A/D Calibration) ==> 1- ADON [位0]::开/关A/D转换器(A/D converter ON/ OFF) ==> 1*/ADC3->CR2 &= ~(0x01); // 关闭 ADCDelay_ms(10); // 延时 10 msADC3->CR2 |= 0x01; // 打开 ADC/*开始复位校准,给予对应寄存器标志位 1,如果 ADC 复位校准结束对应寄存器位置硬件清除为 0*/ADC3->CR2 |= (0x01 << 3); // while 循环是等待当前复位校准结束 while ((ADC3->CR2 & (0x01 << 3)));Delay_ms(10); /*开始 A/D 校准,给予对应寄存器标志位 1,ADC A/D 校准之后对应寄存器位置硬件清除为 0*/ADC3->CR2 |= (0x01 << 2);// while 循环是等待当前 A/D 校准结束 while ((ADC3->CR2 & (0x01 << 2)));ADC3->CR2 |= 0x01; // 打开 ADC
}u16 LSEN_GetValue(void)
{/*ADC->SR 状态寄存器 EOC [位1] 位置,如果数据未转换完成EOC 为 0 ,转换完成 EOC 为 1*/while (!(ADC3->SR & (0x01 << 1)));return ADC3->DR;}
adc.h:
#ifndef _ADC_H
#define _ADC_H#include "stm32f10x.h"#include "delay.h"/*** @brief 光敏电阻初始化函数*/
void LSEN_Init(void);u16 LSEN_GetValue(void);#endif
delay.c:
#include "delay.h"void Delay_us(u32 us)
{while (us--) {// 利用 __NOP() 操作占用 MCU 一次执行周期特征,调用 72 个 __NOP// 不建议使用 for 循环或者 while 循环,循环判断都需要占用一个 MCU 执行周期__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();}
}void Delay_ms(u32 ms)
{Delay_us(ms * 1000);
}
delay.h:
#ifndef _DELAY_H
#define _DELAY_H#include "stm32f10x.h"/*** @brief 延时微秒控制函数,延时单位是 us** @param us 延时微秒时间*/
void Delay_us(u32 us);/*** @brief 延时毫秒控制函数,延时单位是 ms** @param ms 延时毫秒时间*/
void Delay_ms(u32 ms);#endif
usart.c:
#include "usart1.h"USART1_Data usart1_val = {0};void USART1_Init(u32 brr)
{/*1. 时钟使能 GPIOA 和 USART1,两者都在 APB2 时钟控制USART1 对应位 14,GPIOA 对应位2*/RCC->APB2ENR |= (0x01 << 2) | (0x01 << 14);/*2. PA9 和 PA10 GPIO 配置PA9 是 MCU 的 TX 数据发送端,GPIO 工作模式选择【复用推挽输出模式】PA10 是 MCU 的 RX 数据发送端,GPIO 工作模式选择【浮空输入模式】*/GPIOA->CRH &= ~(0x00FF << 4);GPIOA->CRH |= 0x0B << 4; // PA9 --> TX【复用推挽输出模式】GPIOA->CRH |= 0x04 << 8; // PA10 --> RX【浮空输入模式】/*3. USART1 串口配置3.1 8n1 配置,NRZ 数据格式配置,8个数据位,0 个校验位,1 个停止位3.2 USART1 对应 TE 和 RE 开启,打开 USART1 的发送数据和读取数据能力3.3 USART1 BRR 波特率配置*/// 3.1 8n1 配置,如果仅使用寄存器方式配置当前代码,可以省略一下过程// 当前代码是为了后续的 【标准库】和【Hal库】,也是代码逻辑的一部分USART1->CR1 &= ~(0x01 << 12); // 【8】USART1->CR1 控制寄存器对应 M (位12) 明确当前数据字长为 8 数据位USART1->CR1 &= ~(0x01 << 10); // 【n】USART1->CR1 控制寄存器对应 PCE (位10), 明确当前不使用校验位USART1->CR2 &= ~(0x03 << 12); // 【1】USART1->CR2 控制寄存器对应 STOP (位13,12), 限制当前数据停止位为 1// 3.2 USART1 对应 TE 和 RE 开启USART1->CR1 |= (0x03 << 2); // TE(位3) RE(位2) 进行赋值 1 开始操作// 3.3 USART1 BRR 波特率配置// 假设波特率是 115200 ==> USARTDIV 数据float usart_div = 72 * 1000 * 1000 / (16 * brr);// usart_div == 39.0625/*将 usart_div 进行拆解,分别对应整数部分和小数部分内容,提供给当前 USART1 中用于计算波特率对应寄存器位。*/int usart_div_Mantissa = (u32)usart_div;int usart_div_fraction = (u32)((usart_div - usart_div_Mantissa) * 16);// 两个数据进行组合 提供给 USART1 波特率寄存器的数据为 USART1->BRR |= (usart_div_Mantissa << 4) | usart_div_fraction;// 4. 启动 USART1 USART1->CR1 |= (0x01 << 13);
}void USART1_SendByte(u8 byte)
{/*利用 USART1_SR 寄存器,判断之前的数据内容是否发送完成,如果没有发送完成,本次发送操作进入【阻塞状态】如果 USART1_SR TC ==> 0 表示之前的数据发送未完成如果 USART1_SR TC ==> 1 表示之前的数据发送完毕TC Transmission Complete*/while (0 == (USART1->SR & (0x01 << 6)));/*将需要发送的数据存储到 USART1->DR 数据寄存器中,DR 会将数据直接提供给 TDR 寄存器,TDR 寄存器会将数据提供给移位寄存器,SR 寄存器 TC TC 寄存器位置 0发送完毕会将 SR 寄存器的中,TC 寄存器位置修改为 1*/USART1->DR = byte;
}void USART1_SendBuffer(u8 *buffer, u16 count)
{while (count--){USART1_SendByte(*buffer);buffer++;}
}void USART1_SendString(const char * str)
{while (*str){USART1_SendByte(*str);str++;}
}u8 USART1_ReceiveByte(void)
{u8 data = 0;/*判断在 USART1->SR 寄存器中,对应的 RXNE (Read data register not empty) 标志位如果没有数据可以收到,RXNE 为 0如果有数据可以读取,RXNE 为 1while 进行 RXNE 标志位判断,如果没有数据当前循环【阻塞后续代码】*/while (0 == (USART1->SR & (0x01 << 5)));data = (u8)USART1->DR; return data;
}/*
非重要知识点,仅实现 printf 函数功能重定向,可以实现
printf 打印操作数据 USART1 发送到 PC
*/
int fputc(int c, FILE *stream)
{USART1_SendByte(c);return c;
}void USART1_Interrupt_Enable(void)
{/*当前 USART1 的控制寄存器中,打开 IDLEIE 数据总线空闲中断使能打开 RXNEIE 数据总线空闲中断使能*/USART1->CR1 |= (0x01 << 4) | (0x01 << 5);/*设置当前 USART1 对应的中断优先级为 0001 在全局优先级设置为 2 的情况下 占先 0 次级 1*/NVIC_SetPriority(USART1_IRQn, 1); // 0001 占先 0 次级 1/*告知当前 MCU 使能对应的 USART1_IRQn 中断*/NVIC_EnableIRQ(USART1_IRQn);
}/*
完成 USART1 对应的 USART1_IRQn 对应的中断处理函数
当前中断处理函数是用于接收的数据内容进行处置操作,将接收的数据
存储到 USART1_Data 结构体中,对应的 u8 data[DATA_SIZE] 数组
*/
void USART1_IRQHandler(void)
{u32 val = 0;/*usart1_val.flag ==> 1 表示当前数据接收完毕,同时已经回显到PC 端 USART 工具*/if (usart1_val.flag){// 对当前数据空间进行擦除,memset(&usart1_val, 0, sizeof(USART1_Data));}/*如果当前触发的中断为 【RXNE 中断】,表示数据在通过串口传递到 MCU 中*/if (USART1->SR & (0x01 << 5)){usart1_val.data[usart1_val.count++] = USART1->DR;/*当前接收到的有效字节个数 == DATA_SIZE,当前数据缓冲区数组已满*/if (DATA_SIZE == usart1_val.count){USART1_SendBuffer(usart1_val.data, usart1_val.count);usart1_val.flag = 1;}}/*如果当前数据总线空闲 【IDLE 中断】,表示数据传递完毕*/if (USART1->SR & (0x01 << 4)){/*表示当前数据接收已完成*/usart1_val.flag = 1;/*需要完成对于当前 USART1->SR IDLE 数据总线空闲中断标志位进行清除操作。【官方要求】1. 读取 USART1->SR 寄存器2. 读取 USART1->DR 寄存器*/val = USART1->SR;val = USART1->DR;// 将数据回显到 PC 端 USART 调试工具USART1_SendBuffer(usart1_val.data, usart1_val.count);}
}
usart.h:
#ifndef _USART1_H
#define _USART1_H#include "stm32f10x.h"#include "stdio.h"
#include "stdlib.h"
#include "string.h"#define DATA_SIZE (256)typedef struct usart1_data
{u8 data[DATA_SIZE]; // 接受数据缓冲区u8 flag; // 数据处理标志位u16 count; // 读取到的有效字节个数
} USART1_Data;extern USART1_Data usart1_val;/*** @brief USART1 初始化函数,需要完成* 1. PA9 和 PA10 GPIO 配置* 2. USART1 配置** @param brr 用户提供的对应当前 USART1 的波特率*/
void USART1_Init(u32 brr);/*** @brief USART1 发送一个字节数据到其他设备** @param byte 发送的字节数据。*/
void USART1_SendByte(u8 byte);void USART1_SendBuffer(u8 *buffer, u16 count);void USART1_SendString(const char * str);/*** @brief USART1 接受外部输入的数据内容,当前函数是接收一个字节数据** @return 返回值是接收到的数据内容*/
u8 USART1_ReceiveByte(void);/*
后续代码中需要利用【中断】对代码内容进行优化1. 接收数据终止条件2. 提供外部可以持续使用数据内容3. 数据发送中断判断
*//*** @brief USART1 串口中断使能函数*/
void USART1_Interrupt_Enable(void);#endif
main.c:
#include "stm32f10x.h"//#include "led.h"
//#include "key.h"
#include "delay.h"
//#include "beep.h"
#include "usart1.h"
#include "adc.h"int main(void)
{//Led_Init();LSEN_Init();USART1_Init(115200);USART1_Interrupt_Enable();//Led1_Ctrl(1);while (1){u16 adc_data = LSEN_GetValue();printf("adc_data : %d\r\n", adc_data);printf("U_LSEN : %f\r\n", 3.3 / 4096 * adc_data);Delay_ms(500);}
}
https://github.com/0voice