嵌入式系统中STM32ADC学习笔记
目录
1. 背景:模拟量为什么要ADC
在生活中,很多物理量(压力、加速度、温度、位移)都以电压或电流的形式呈现,这类信号是连续的(模拟量)。而 MCU(如STM32)只能处理离散的数字数据,因此需要 ADC(Analog-to-Digital Converter)将模拟电压转换成数字码值,并由软件算法进一步换算得到工程单位(kPa、g、℃等)。
2. STM32F103 的 ADC I/O 口
2.1 ADC I/O 的本质
STM32F103 的“ADC脚”不是普通数字IO的变体,而是 ADC 模块的模拟输入入口。外部模拟电压通过该引脚进入芯片内部的:
- 模拟多路复用器(Analog MUX):选择当前采样的通道(哪个引脚/内部通道)。
- 采样保持网络(Sample & Hold):用采样电容“抓住瞬时电压”。
- SAR转换核心:逐次逼近输出12位码值。
2.2 为什么必须把GPIO配置为 Analog
GPIO配置为模拟输入(Analog mode)的目的:
- 关闭数字输入缓冲(降低漏电、减少数字噪声耦合)。
- 提升测量一致性和稳定性。
3. ADC 原理
下图为STM32 ADC的工作原理图。
3.1参考与供电
图左上角的 VREF+、VREF- 定义了 ADC 的量化范围:
- 输入电压被映射到 [𝑉𝑅𝐸𝐹−,𝑉𝑅𝐸𝐹+]对应的数字码值范围(F103通常是 12bit:0~4095)。
- 工程上常见𝑉𝑅𝐸𝐹−=VSSA≈0V,𝑉𝑅𝐸𝐹+=VDDA≈3.3V。
VDDA/VSSA 是 ADC 模拟部分的供电与地,决定底噪/稳定性。
如果 VDDA 纹波大或模拟地回流不干净,即使数字逻辑正确,ADC 结果也会抖。
3.2 Analog MUX(模拟多路复用器)
用“模拟开关阵列”选择当前被采样的通道。外部通道:ADCx_IN0 ~ ADCx_IN15(来自 GPIO 端口的模拟输入脚);内部通道:温度传感器、VREFINT。
(1)MUX 的“具体实现方式”
本质是 多路 MOS 传输门/开关阵列:1.每个通道对应一只(或一组)模拟开关;2.控制逻辑保证同一时刻只闭合目标通道,其他断开;3.被选通道的电压被送到同一个“内部采样节点”(后面接 S/H)。
(2)为什么 MUX 会带来“通道串扰/首样本污染”
通道切换时,内部采样节点上往往还残留“上一通道”的电压(来自采样电容的电荷),再叠加开关注入电荷,会造成:多通道扫描时,切换后的第一个样本更容易偏解决手段就是:增大采样时间、降低源阻抗、或在ADC脚边加合适的 RC/缓冲(你之前提的方案A正是为这个问题服务)。
3.3 电容采样原理
Sample & Hold(采样保持网络):采样开关 + 采样电容如何“抓住瞬时电压”。虽然把S/H 画在“模拟至数字转换器”内部,但它实际位于 MUX 后、SAR 核心前,是 ADC 精度的关键。
(1)两个阶段
采样阶段 vs 保持阶段。采样阶段(Sample):采样开关闭合一段时间 t s a m p l e t_{sample} tsample,输入信号给采样电容 C s h C_{sh} Csh充电。保持阶段(Hold):采样开关断开, C s h C_{sh} Csh上的电压被“冻结”,SAR 在这段时间内完成逐次逼近比较。
(2)用公式描述“抓住瞬时电压”
把输入源等效为 V i n V_{in} Vin,串联输出阻抗 R s o u r c e R_{source} Rsource,再加 ADC 采样开关等效电阻 R s w R_{sw} Rsw。令 R e q = R s o u r c e + R s w , τ = R e q C s h R_{eq}=R_{source}+R_{sw}, τ=R_{eq}C_{sh} Req=Rsource+Rsw,τ=ReqCsh如果采样开始时采样电容初值为 V c 0 V_{c0} Vc0,(多通道时常等于“上一个通道电压”),则采样电容电压为: V t = V i n + ( V c 0 − V i n ) e − t / τ V_{t}=V_{in}+(V_{c0}-V_{in})e^{-t / τ } Vt=Vin+(Vc0−Vin)e−t/τ采样误差(还没充到位的差值): ▽ V t = V i n − V c ( t ) = V i n − V c 0 e − t / τ \bigtriangledown V_{t}=V_{in}-V_{c}(t)=V_{in}-V_{c0}e^{-t / τ } ▽Vt=Vin−Vc(t)=Vin−Vc0e−t/τ 这就是“采样电容抓住瞬时电压”的本质:在给定 t s a m p l e t_{sample} tsample内,让指数项足够小。
(3)12bit 精度下的采样时间下限
&emsp要求采样误差小于 0.5 LSB(避免量化边界跳码)。对 12bit、满量程 V r e f V_{ref} Vref: 0.5 L S B = V r e f 2 13 0.5LSB=\frac{V_{ref}}{2^{13} } 0.5LSB=213Vref &emsp在最坏情况下(通道切换导致 ∣ V i n − V c 0 ∣ ≈ V r e f \left | V_{in}- V_{c0}\right | \approx V_{ref} ∣Vin−Vc0∣≈Vref可得到: V c 0 e − t / τ ≤ 1 2 13 ⇒ t s a m p l e ≥ τ I n ( 2 13 ) ≈ 9 τ V_{c0}e^{-t / τ } \le \frac{1}{2^{13}} \Rightarrow t_{sample}\ge τ In(2^{13})\approx 9τ Vc0e−t/τ≤2131⇒tsample≥τIn(213)≈9τ
即: t s a m p l e ≥ 9 ( R s o u r c e + R s w ) C s h t_{sample}\ge 9(R_{source}+R_{sw})C_{sh} tsample≥9(Rsource+Rsw)Csh &emsp在 STM32F103 里把采样周期设为 71.5/239.5 cycles,本质是在给C_{sh}足够时间“追上”输入电压。
3.4 SAR 转换
SAR 转换核心为:比较器 + 内部DAC + SAR逻辑,如何把保持电压变成 12bit 码。框图中“模拟至数字转换器”就是 SAR ADC 的主体。其典型实现包含:1.比较器:比较 V h o l d V_{hold} Vhold与内部 DAC 输出 V D A C V_{DAC} VDAC;2.内部 DAC(常见为电容阵列DAC):快速生成试探电压;3.SAR 控制逻辑/寄存器:按 MSB→LSB 逐位决策.
(1)逐次逼近的“具体步骤”(12次比较)
对于 12bit:
1.先试探 MSB=1 → V D A C V_{DAC} VDAC= V r e f / 2 V_{ref}/2 Vref/2,若 V h o l d ≥ V D A C V_{hold}≥V_{DAC} Vhold≥VDAC则 MSB 保留1,否则置0。
2.再试探次高位 → V h o l d = (已确定的位组合) V_{hold}=(已确定的位组合) Vhold=(已确定的位组合)
3.重复到 LSB
最终输出一个 12bit Code。
(2)为什么转换时间里有“固定开销”
转换完成会产生 EOC/JEOC,并且 ADC 需要一个与 ADCCLK相关的固定时间来完成逐次比较(在F1上常见写法是:总转换时间 = 采样时间 + 固定转换周期)。这正对应框图右侧的 ADCCLK(来自 ADC 预分频器) 驱动整个时序。
3.5 规则组与注入组
两条路径:1.规则通道(Regular):MUX最多 16 通道序列;结果进入 规则通道数据寄存器 DR(16位容器装12位结果);可产生 DMA 请求(图中从DR指向“DMA请求”)。2.注入通道(Injected):MUX最多 4 通道序列;结果进入 注入通道数据寄存器(4×16位):JDR1~JDR4;更偏“插队/高优先级测量”(例如希望在某事件发生时立即采几路关键量)。
3.6 触发系统
EXTSEL/JEXTSEL + EXTTRIG/JEXTTRIG 决定“何时开始转换”。1.规则组触发:由 EXTSEL[2:0] 选择触发源(如 TIM1_CH1/CH2/CH3、TIM2_CH2、TIM3_TRGO、TIM4_CH4、EXTI_11 等);由 EXTTRIG 使能外部触发还有 ADCx_ETRGREG_REMAP 之类重映射控制位(把触发源映射到 TIM8_TRGO 等);2.注入组触发:JEXTSEL[2:0] 选择触发源(如 TIM1_TRGO、TIM1_CH4、TIM2_TRGO、TIM2_CH1、TIM3_CH4、TIM4_TRGO、EXTI_15 等);由 JEXTTRIG 使能外部触发;还有 ADCx_ETRGINJ_REMAP 重映射控制位。
3.7 标志位/中断/DMA
数据出来后怎么通知CPU或自动搬运,主要是通过三类事件源:EOC(规则转换结束)、JEOC(注入转换结束)、AWD(模拟看门狗事件)。对应还有使能位:EOCIE、JEOCIE、AWDIE。使能后事件会“或”到一起送到 NVIC 的 ADC 中断。同时,规则组 DR 旁边画了 DMA 请求:通常是规则组转换完成触发 DMA,把 DR 自动搬到内存,形成稳定采样流。
模拟看门狗(Analog Watchdog)
3.7 标志位/中断/DMA
“模拟看门狗”块包含:比较结果、阈值高限(12位)、阈值低限(12位)。工作方式:硬件对转换结果做上下限比较,越界则置位 AWD,可中断通知。
4. 配置ADC的主要C程序代码
#include "stm32f10x.h" // 包含F1寄存器定义与外设库声明
/******************** 用户可配置参数区 **************************/
#define ADC_CLK_DIV RCC_PCLK2_Div6 // ADC时钟分频:72MHz/6=12MHz(常用稳妥)
#define ADC_CHANNEL_PRESS ADC_Channel_0 // 压力通道:PA0 -> ADC1_IN0
#define ADC_CHANNEL_AX ADC_Channel_1 // 加速度X:PA1 -> ADC1_IN1
#define ADC_CHANNEL_AY ADC_Channel_2 // 加速度Y:PA2 -> ADC1_IN2
#define ADC_CHANNEL_AZ ADC_Channel_3 // 加速度Z:PA3 -> ADC1_IN3
#define ADC_SAMPLE_PRESS ADC_SampleTime_71Cycles5 // 压力采样时间(可按源阻抗调整)
#define ADC_SAMPLE_ACC ADC_SampleTime_239Cycles5 // XYZ采样时间(更稳,抗通道切换污染)
/******************** DMA采样缓冲区 ****************************/
/* 说明:每帧4点:[P, X, Y, Z];frames决定缓冲帧数 */
volatile uint16_t g_adc_buf[4 * 64]; // 示例:64帧环形缓冲(共256个采样点)
/******************** 内部函数声明 ******************************/
static void RCC_Config_For_ADC_GPIO_DMA_TIM3(void); // 统一打开时钟
static void GPIO_Config_Analog_PA0_PA3(void); // PA0~PA3 配模拟输入
static void GPIO_Config_Digital_Example(void); // 普通GPIO示例
static void DMA_Config_For_ADC1(uint16_t *buf, uint32_t count); // DMA:ADC1->内存
static void TIM3_TRGO_Config(uint32_t sample_hz); // TIM3更新事件作为TRGO触发
static void ADC1_Config_4CH_Scan_ExternalTrig_DMA(void); // ADC1:4通道扫描+外部触发+DMA
/******************** 对外初始化接口 ******************************/
/**
* @brief 初始化:GPIO(模拟)+ADC1(4通道扫描)+DMA+TIM3_TRGO触发
* @param sample_hz 采样频率(每秒采“帧”的次数;每帧4通道)
* @note 例如 sample_hz=1000 => 每秒1000帧 => 每通道1000SPS
*/
void ADC1_4CH_DMA_TIM3_Init(uint32_t sample_hz)
{
RCC_Config_For_ADC_GPIO_DMA_TIM3(); // 1) 打开外设时钟(GPIO/ADC/DMA/TIM)
GPIO_Config_Analog_PA0_PA3(); // 2) 配置PA0~PA3为模拟输入(ADC通道口)
/* 如果你还需要普通GPIO,这里给一个示例初始化(可选) */
GPIO_Config_Digital_Example(); // 2.1) 普通GPIO示例(不影响ADC)
DMA_Config_For_ADC1((uint16_t*)g_adc_buf, // 3) 配置DMA:ADC1_DR -> g_adc_buf
(uint32_t)(sizeof(g_adc_buf)/sizeof(g_adc_buf[0]))); // 传输总点数(半字个数)
TIM3_TRGO_Config(sample_hz); // 4) 配置TIM3产生TRGO(更新事件)作为采样节拍
ADC1_Config_4CH_Scan_ExternalTrig_DMA(); // 5) 配置ADC1:4通道扫描 + 外部触发 + DMA
/* 注意:先开DMA,再开ADC,再开TIM(已按上述顺序),采样更稳定 */
}
/******************** 时钟配置 ******************************/
static void RCC_Config_For_ADC_GPIO_DMA_TIM3(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟(PA0~PA3)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能GPIOB时钟(普通GPIO示例用)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC时钟(普通GPIO示例用)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能ADC1外设时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA1时钟(ADC1默认用DMA1_Channel1)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使能TIM3时钟(用于TRGO触发)
RCC_ADCCLKConfig(ADC_CLK_DIV); // 配置ADC时钟分频(决定ADCCLK)
}
/******************** GPIO:ADC模拟输入 ******************************/
static void GPIO_Config_Analog_PA0_PA3(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义GPIO初始化结构体
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; // 选择PA0~PA3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式(关闭数字输入缓冲,降低干扰)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度字段对模拟输入无意义,但库要求填
GPIO_Init(GPIOA, &GPIO_InitStructure); // 应用到GPIOA
}
/******************** GPIO:普通数字口示例 ******************************/
/* 示例:PC13 推挽输出(LED),PB0 上拉输入(按键) */
static void GPIO_Config_Digital_Example(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义GPIO初始化结构体
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // 选择PC13
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 输出速度2MHz(够用且EMI更低)
GPIO_Init(GPIOC, &GPIO_InitStructure); // 应用到GPIOC
GPIO_SetBits(GPIOC, GPIO_Pin_13); // 默认输出高电平(根据你的板子逻辑可改)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择PB0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 输入速度字段同样必须填
GPIO_Init(GPIOB, &GPIO_InitStructure); // 应用到GPIOB
}
/******************** DMA:ADC1_DR -> 内存 ******************************/
static void DMA_Config_For_ADC1(uint16_t *buf, uint32_t count)
{
DMA_InitTypeDef DMA_InitStructure; // 定义DMA初始化结构体
DMA_DeInit(DMA1_Channel1); // 复位DMA1通道1到默认状态(ADC1通常用Ch1)
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址:ADC1数据寄存器DR
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buf; // 内存地址:采样缓冲区首地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;// 方向:外设->内存
DMA_InitStructure.DMA_BufferSize = count; // 传输数据个数(单位:半字,因为后面配16位)
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增(始终读DR)
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增(按数组顺序写入)
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 环形模式:循环写缓冲区,适合连续采样
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 高优先级,避免被其他DMA抢占
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止内存到内存(这里只是外设到内存)
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 写入DMA配置
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA通道
}
/******************** TIM3:TRGO触发源 ******************************/
/* 用TIM3更新事件(Update)作为 TRGO;sample_hz 为“帧频率”(每秒触发多少次序列转换) */
static void TIM3_TRGO_Config(uint32_t sample_hz)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 定义定时器时基结构体
TIM_DeInit(TIM3); // 复位TIM3寄存器
/* TIM3 在APB1上:若APB1分频≠1,定时器时钟会自动×2;这里不给你猜,直接用 SystemCoreClock 推导常用情况 */
/* 常见配置:SYSCLK=72MHz,APB1=36MHz,则 TIM3CLK=72MHz(因为APB1分频为2时定时器时钟×2) */
uint32_t tim_clk_hz = 72000000UL; // 假定TIM3计数时钟72MHz(典型F103配置)
uint16_t prescaler = (uint16_t)(tim_clk_hz / 1000000UL - 1); // 预分频到1MHz计数(1us/计数),便于整除
uint32_t period = (1000000UL / sample_hz) - 1; // 自动重装载值:1MHz/sample_hz = 周期(us)
TIM_TimeBaseStructure.TIM_Prescaler = prescaler; // 设置预分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseStructure.TIM_Period = (uint16_t)period; // 设置周期(ARR)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频1
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // TIM3无重复计数需求
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 应用时基配置
/* TRGO选择:用更新事件作为触发输出(MMS=010) */
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // 选择TRGO源为Update事件
TIM_Cmd(TIM3, ENABLE); // 使能TIM3开始跑(开始输出TRGO节拍)
}
/******************** ADC1:4通道扫描 + 外部触发 + DMA ******************************/
static void ADC1_Config_4CH_Scan_ExternalTrig_DMA(void)
{
ADC_InitTypeDef ADC_InitStructure; // 定义ADC初始化结构体
ADC_DeInit(ADC1); // 复位ADC1到默认状态
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式(不做双ADC同步)
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式:按序列转换多个通道
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 非连续:每次触发转换“一帧”(序列长度=4)
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // 规则组外部触发源:TIM3_TRGO
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐:结果0~4095直接使用
ADC_InitStructure.ADC_NbrOfChannel = 4; // 规则组通道数=4
ADC_Init(ADC1, &ADC_InitStructure); // 写入ADC配置
/* 规则组序列配置:Rank决定每次DMA写入的顺序 */
ADC_RegularChannelConfig(ADC1, ADC_CHANNEL_PRESS, 1, ADC_SAMPLE_PRESS); // Rank1=压力,采样时间PRESS
ADC_RegularChannelConfig(ADC1, ADC_CHANNEL_AX, 2, ADC_SAMPLE_ACC); // Rank2=X,采样时间ACC
ADC_RegularChannelConfig(ADC1, ADC_CHANNEL_AY, 3, ADC_SAMPLE_ACC); // Rank3=Y,采样时间ACC
ADC_RegularChannelConfig(ADC1, ADC_CHANNEL_AZ, 4, ADC_SAMPLE_ACC); // Rank4=Z,采样时间ACC
ADC_DMACmd(ADC1, ENABLE); // 使能ADC发出DMA请求(规则组DR更新时)
ADC_Cmd(ADC1, ENABLE); // 使能ADC1(上电/开始工作)
/* 复位校准寄存器 */
ADC_ResetCalibration(ADC1); // 触发复位校准
while(ADC_GetResetCalibrationStatus(ADC1)) { } // 等待复位校准完成(硬件清零标志)
/* 开始校准 */
ADC_StartCalibration(ADC1); // 触发校准
while(ADC_GetCalibrationStatus(ADC1)) { } // 等待校准完成
/* 重要:外部触发工作前,必须允许外部触发 */
ADC_ExternalTrigConvCmd(ADC1, ENABLE); // 使能规则组外部触发(允许TIM3_TRGO启动转换)
/* 注意:这里不调用 ADC_SoftwareStartConvCmd,因为我们用的是外部触发(TIM3_TRGO) */
}
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)