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+(Vc0Vin)et/τ采样误差(还没充到位的差值): ▽ 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=VinVc(t)=VinVc0et/τ  这就是“采样电容抓住瞬时电压”的本质:在给定 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​} VinVc0Vref可得到: 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τ Vc0et/τ2131tsampleτ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} tsample9(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} VholdVDAC则 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) */
}

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐