11-ADC模数转换.md 13 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — ADC章节 + 配套代码32~35" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

ADC模数转换

用生活理解:ADC 就像一把精密的电压尺子——输入 0~3.3V 的模拟电压,输出 0~4095 的数字(12 位)。就像用一把刻有 4096 个刻度的尺子量高度,输入 1.65V 时输出 2048(中间值)。采样时间就是"尺子读数需要的时间",采样时间越长读数越准。规则组和注入组就像"普通队列"和"VIP 通道"——VIP 可以插队。


ADC 概述

ADC = Analog-to-Digital Converter(模数转换器),将连续的模拟信号转换为离散的数字信号。

STM32F103ZET6 共有 3 个 ADC 模块(ADC1/ADC2/ADC3):

特性
分辨率 12 位(输出范围 0~4095)
通道数 最多 18 个通道(16 个外部引脚 + 2 个内部)
转换时间 最快 1.17μs(14MHz 时钟下)
输入电压 0 ~ VREF+(通常 = VDDA = 3.3V)
ADC 时钟 来自 APB2,经预分频器,最大 14MHz

内部通道

通道 连接 说明
通道16 内部温度传感器 测量芯片内部温度
通道17 VREFINT 内部参考电压(1.2V)

SAR ADC 工作原理

STM32F103 使用逐次逼近型(SAR)ADC:

输入电压 VIN = 1.8V, VREF = 3.3V

第1步: VIN > 3.3V/2=1.65V ? → YES → Bit11=1, 余量 = 1.8-1.65 = 0.15V
第2步: VIN > 1.65+1.65/2=2.475V ? → NO → Bit10=0
第3步: VIN > 1.65+0.825/2=2.0625V ? → NO → Bit9=0
第4步: VIN > 1.65+0.4125/2=1.85625V ? → NO → Bit8=0
第5步: VIN > 1.65+0.20625/2=1.753125V ? → YES → Bit7=1
...

最终: 12 次比较后得到 12 位结果
数字值 = VIN / VREF × 4095 = 1.8 / 3.3 × 4095 ≈ 2234

特点

  • 优点:转换时间确定(固定周期数),无延迟
  • 缺点:对输入噪声敏感(需要适当的采样时间)
  • 转换时间 = 采样时间 + 12.5 个 ADC 时钟周期

关键概念

规则组 vs 注入组

ADC 有两种转换序列:

最大通道数 特点 用途
规则组 16 通道 常规采集,按顺序转换 周期性采集传感器数据
注入组 4 通道 可打断规则组的转换 紧急事件触发采集

类比:规则组是普通队列(一个个来),注入组是 VIP 通道——来了就插队。

转换模式

模式 说明 适用场景
单次转换 每触发一次转换一个通道 按键触发采集
连续转换 转换完自动开始下一次 持续监测电压
扫描模式 按序列依次转换所有通道 多通道循环采集
间断模式 每触发一次转换一部分通道 超多通道分批次

触发方式

触发源 说明
软件触发 CR2.SWSTART=1 启动转换
定时器触发 TIM1/2/3/4 的 TRGO 事件
外部引脚触发 EXTI 线上升沿/下降沿

ADC 时钟配置

ADC_CLK = APB2_CLK / ADCPRE[1:0]

ADCPRE 分频系数:
  00: /2 (36MHz)   → 超限! ADC_CLK > 14MHz
  01: /4 (18MHz)   → 超限!
  10: /6 (12MHz)   → ✅ 推荐(12MHz < 14MHz)
  11: /8 (9MHz)    → ✅ 较慢但更准

参考:参考手册 §11(ADC 寄存器描述) APB2 时钟 = 72MHz,ADCPRE=Div6 → ADC_CLK=12MHz 是最均衡选择

采样时间

每个通道可独立配置采样时间(SMPR1/SMPR2 寄存器):

SMP[2:0] 采样周期数 总转换时间(12MHz ADC_CLK)
000 1.5 (1.5+12.5) / 12MHz = 1.17μs
001 7.5 (7.5+12.5) / 12MHz = 1.67μs
010 13.5 2.17μs
011 28.5 3.42μs
100 41.5 4.50μs
101 55.5 5.67μs
110 71.5 7.00μs
111 239.5 21.00μs(最准)

采样时间选择原则

  • 低阻抗信号源(<10KΩ):短采样时间(1.5~7.5 周期)
  • 高阻抗信号源(>10KΩ):长采样时间(28.5~239.5 周期)
  • 追求速度选短时间,追求精度选长时间

ADC 关键寄存器

寄存器 地址偏移 功能
SR 0x00 状态寄存器(EOC=转换结束,STRT=开始)
CR1 0x04 控制1(SCAN=扫描,JAUTO=自动注入,RES=分辨率)
CR2 0x08 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐)
SMPR1 0x0C 采样时间寄存器1(通道10~17)
SMPR2 0x10 采样时间寄存器2(通道0~9)
SQR1 0x2C 规则序列寄存器1(L=序列长度,SQ13~16)
SQR2 0x30 规则序列寄存器2(SQ7~12)
SQR3 0x34 规则序列寄存器3(SQ1~6)
DR 0x4C 规则组数据寄存器(12 位结果,右/左对齐)

参考:参考手册 §11 完整寄存器位域定义


ADC 校准

ADC 上电后需要校准以提高精度:

// 校准流程
ADC1->CR2 |= ADC_CR2_ADON;           // 开启 ADC
// 等待稳定(约 2 个 ADC 时钟)
Delay_us(1);

ADC1->CR2 |= ADC_CR2_CAL;             // 开始校准
while (ADC1->CR2 & ADC_CR2_CAL)       // 等待校准完成(CAL 位硬件自动清 0)
{
}
// 校准完成,ADC 准备就绪

校准原理:ADC 内部测量一次 VREFINT,根据误差调整内部电容阵列。


实验:单通道 ADC 采集

软件设计(寄存器版 — 单次采集)

项目路径stm32_base/32_adc_single_register

需求:使用 ADC1 通道 0(PA0)采集模拟电压,通过串口打印原始值和电压值。

文件:stm32_base/32_adc_single_register/Hardware/ADC/adc.c

#include "adc.h"

// ADC1 初始化:PA0(通道0),软件触发,单次转换
void ADC1_Init(void)
{
    /* ========== 1. 开启时钟 ========== */
    // ADC1 挂载在 APB2 总线上
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;     // |=: ADC1时钟使能
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;      // |=: GPIOA时钟使能

    /* ========== 2. 配置 PA0 为模拟输入 ========== */
    // 模拟输入模式: MODE=00, CNF=00
    // 注意: 模拟输入下施密特触发器关闭以降低功耗
    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);  // &=~: 同时清除MODE0和CNF0

    /* ========== 3. 配置 ADC 时钟分频 ========== */
    // APB2=72MHz, ADCPRE=Div6 → ADC_CLK=12MHz(≤14MHz ✅)
    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;      // |=: ADCPRE[1:0]=10

    /* ========== 4. 复位 ADC 配置 ========== */
    // 通过 RSTR 寄存器复位(可选但推荐,确保从干净状态开始)
    RCC->APB2RSTR |= RCC_APB2RSTR_ADC1RST;   // |=: 复位ADC1
    RCC->APB2RSTR &= ~RCC_APB2RSTR_ADC1RST;  // &=~: 解除复位

    /* ========== 5. 开启 ADC + 校准 ========== */
    ADC1->CR2 |= ADC_CR2_ADON;               // |=: ADON=1 开启ADC
    Delay_us(10);                             // 等待稳定

    // 校准
    ADC1->CR2 |= ADC_CR2_CAL;               // |=: 开始校准
    while (ADC1->CR2 & ADC_CR2_CAL);         // 等待CAL硬件清0(校准结束)

    /* ========== 6. 配置规则组序列 ========== */
    // SQ1[4:0] = 通道号 0(PA0)
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;             // &=~: 清除SQ1位域
    ADC1->SQR3 |= (0 << 0);                   // |=: SQ1 = 通道 0(0 是默认值,此行示意)
    // SEQ_LEN[3:0] = 0(表示序列长度为 1)
    ADC1->SQR1 &= ~ADC_SQR1_L;               // &=~: L清0(1个转换)

    /* ========== 7. 配置采样时间 ========== */
    // 通道0的采样时间: SMP0[2:0]=111 → 239.5周期(最准)
    ADC1->SMPR2 |= ADC_SMPR2_SMP0;           // |=: SMP0位全部置1
}

// 读取 ADC 转换结果(软件触发单次转换)
uint16_t ADC1_Read(void)
{
    // 1. 启动转换:再次写 ADON(当 CR2 已有 ADON=1 时,再次写 1 触发转换)
    ADC1->CR2 |= ADC_CR2_ADON;               // |=: 软件触发

    // 2. 等待转换结束
    // SR.EOC = 1 表示转换完成
    while (!(ADC1->SR & ADC_SR_EOC))          // &测试: EOC位=1?
    {
    }

    // 3. 读取 DR 寄存器(读取后 EOC 自动清 0)
    return ADC1->DR;                          // 返回 12 位结果(0~4095)
}

// 将 ADC 原始值转换为电压(mV)
uint16_t ADC_GetVoltage(uint16_t adc_value)
{
    // V = adc_value × 3300mV / 4095
    return (uint16_t)((uint32_t)adc_value * 3300 / 4095);
}

文件:stm32_base/32_adc_single_register/User/main.c

#include "adc.h"
#include "usart.h"
#include <stdio.h>

int main(void)
{
    USART_Init();
    ADC1_Init();

    printf("ADC 单通道采集实验\r\n");

    while (1)
    {
        uint16_t adc_val = ADC1_Read();
        uint16_t volt_mv = ADC_GetVoltage(adc_val);

        printf("ADC: %4d  (0~4095)  |  %.3fV\r\n",
               adc_val, volt_mv / 1000.0f);

        Delay_ms(500);  // 每 500ms 采集一次
    }
}

实验:双通道 ADC + DMA 采集

项目路径stm32_base/34_adc_double_register

多通道 + DMA 是 ADC 的典型高效用法:

// 配置 ADC1 通道 0 和通道 1(PA0, PA1)
// 使用 DMA 自动搬运转换结果,无需 CPU 轮询

#define ADC_CHANNELS  2
uint16_t adc_values[ADC_CHANNELS];  // DMA 自动写入此数组

void ADC_DMA_Init(void)
{
    // ADC 配置(同单通道,但做以下修改)

    // 1. 规则序列: 通道0 → 通道1 依次转换
    ADC1->SQR3 = (1 << 0) | (0 << 5);  // SQ1=通道1, SQ2=通道0(此处仅为示例)
    // SQR1.L = 1 → 序列长度为 2
    ADC1->SQR1 |= (1 << 20);            // L[3:0]=1: 共 2 个转换

    // 2. 开启 DMA 模式
    ADC1->CR2 |= ADC_CR2_DMA;          // DMA=1: 使能DMA传输

    // 3. 开启扫描模式(多通道必须开启)
    ADC1->CR1 |= ADC_CR1_SCAN;         // SCAN=1: 扫描模式

    // 4. 开启连续转换
    ADC1->CR2 |= ADC_CR2_CONT;         // CONT=1: 连续转换
}

// DMA 配置
void DMA_ADC_Init(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // ADC1 使用 DMA1 通道1
    DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);      // 外设: ADC数据寄存器
    DMA1_Channel1->CMAR = (uint32_t)adc_values;         // 内存: 存储数组
    DMA1_Channel1->CNDTR = ADC_CHANNELS;                // 传输次数 = 通道数
    DMA1_Channel1->CCR |= DMA_CCR1_MINC                 // 内存地址递增
                        | DMA_CCR1_CIRC                 // 循环模式
                        | DMA_CCR1_TCIE;                // 完成中断

    DMA1_Channel1->CCR |= DMA_CCR1_EN;                  // 使能

    // 注意: 每次转换完 ADC 会自动发 DMA 请求
    // DMA 将 ADC1->DR 的值搬到 adc_values[x] 然后地址+1
    // 所有通道转换完一轮 → DMA 中断
}

HAL 库版 ADC

// HAL 库 ADC 初始化(CubeMX 生成)
ADC_HandleTypeDef hadc1;

void MX_ADC1_Init(void)
{
    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;     // 非扫描
    hadc1.Init.ContinuousConvMode = ENABLE;          // 连续转换
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;// 软件触发
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;       // 右对齐
    hadc1.Init.NbrOfConversion = 1;                   // 1个转换
    HAL_ADC_Init(&hadc1);
}

// 阻塞式读取
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint16_t val = HAL_ADC_GetValue(&hadc1);

// DMA 方式
HAL_ADC_Start_DMA(&hadc1, buffer, size);
// 完成回调: HAL_ADC_ConvCpltCallback(&hadc1)

核心函数速查表

操作 寄存器版 HAL 库版
开启 ADC `CR2 = ADON`
校准 `CR2 = CAL; while(CR2 & CAL)`
配置通道 SQR3.SQ1 = ch ADC_ChannelConfTypeDef
启动转换 `CR2 = ADON`(或 SWSTART)
查询完成 while(!(SR & EOC)) HAL_ADC_PollForConversion()
读取结果 val = DR HAL_ADC_GetValue()
启动+DMA `CR2 = DMA + CONT+DMA_Init()`
电压换算 val × 3300 / 4095 同上
停止 ADC CR2 &= ~ADON HAL_ADC_Stop()

常见问题与避坑

  1. ADC 值始终为 0 → 检查 GPIO 模式(必须是模拟输入,不是浮空输入)、ADC 是否校准
  2. ADC 值始终为 4095 → 输入引脚悬空(感应噪声)、输入电压超过 VREF+
  3. ADC 值跳动很大 → 采样时间太短(增加采样周期数)、电源去耦不良、PCB 布线噪声
  4. 多通道值错乱 → SQR 序列配置错误、DMA 缓冲区大小与通道数不匹配
  5. ADC 时钟 >14MHz → 转换结果不准确,必须确保 ADCPRE 分频后 ADC_CLK ≤ 14MHz
  6. 电压换算不准 → VREF+ 实际电压可能是 3.28V 而非 3.30V,需用万用表测量后修正
  7. DMA 传输不停止 → CIRC(循环模式)导致持续传输,停止时需要先清 DMA EN 再清 CIRC