11-ADC模数转换.md 14 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 通道 10(PC0)采集模拟电压,通过串口打印原始值和电压值。

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

#include "adc.h"

// 初始化
void ADC1_Init(void)
{
    // 1. 时钟配置
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    // ADC的时钟频率不能超过14MHz,需预分频 10 - 6分频
    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;

    // RCC->APB2ENR | RCC_APB2ENR_IOPCEN;

    // 2. GPIO配置模式:PC0 - 模拟输入,CNF = 00, MODE = 00
    GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);

    // 3. ADC配置
    // 3.1 关闭扫描模式,只有1个转换通道
    ADC1->CR1 &= ~ADC_CR1_SCAN;

    // 3.2 开启连续转换模式
    ADC1->CR2 |= ADC_CR2_CONT;

    // 3.3 数据对齐方式:0 - 右对齐
    ADC1->CR2 &= ~ADC_CR2_ALIGN;

    // 3.4 配置采样时间,010 - 13.5个ADC时钟周期
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_1;

    // 3.5 配置通道序列
    // 3.5.1 设置规则组通道数目L = 0000,1个通道
    ADC1->SQR1 &= ~ADC_SQR1_L;

    // 3.5.2 将通道号10写入 SQ1(在SQR3里)
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= 10 << 0;      // 低5位,第一个通道号,设为10

    // 3.6 选择外部触发方式(暂时注释)
    // ADC1->CR2 |= ADC_CR2_EXTTRIG;
    // ADC1->CR2 |= ADC_CR2_EXTSEL;
}

// 启动ADC开始转换
void ADC1_StartConvert(void)
{
    // 1. 上电,将ADC从断电模式唤醒
    ADC1->CR2 |= ADC_CR2_ADON;

    // 2. 执行校准,等待结束
    ADC1->CR2 |= ADC_CR2_CAL;
    while (ADC1->CR2 & ADC_CR2_CAL)
    {}

    // 3. 连续方式启动AD转换
    // ADC1->CR2 |= ADC_CR2_SWSTART;
    ADC1->CR2 |= ADC_CR2_ADON;
    
    // 4. 等待转换结束
    while ((ADC1->SR & ADC_SR_EOC) == 0)
    {}
}

// 获取转换后的电压值
double ADC1_ReadV(void)
{
    return ADC1->DR * 3.3 / 4095;
}

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

#include "usart.h"
#include "adc.h"
#include "delay.h"

int main(void)
{
	// 初始化
	USART_Init();
	ADC1_Init();

	printf("Hello world!\n");

	// 启动AD转换
	ADC1_StartConvert();

	while (1)
	{
		printf("V = %.2f\n", ADC1_ReadV());

		Delay_ms(1000);
	}
}

实验:双通道 ADC + DMA 采集

项目路径stm32_base/34_adc_double_register

多通道 + DMA 是 ADC 的典型高效用法。实际项目中使用 ADC1 的通道10(PC0)通道12(PC2),通过 DMA1 通道1自动搬运转换结果:

文件:stm32_base/34_adc_double_register/Hardware/ADC/adc.h

#ifndef __ADC_H
#define __ADC_H

#include "stm32f10x.h"

// 初始化
void ADC1_Init(void);

// DMA相关的初始化
void ADC1_DMA_Init(void);

// 启动ADC开始转换,DMA配置完成(源地址固定为ADC1->DR)
void ADC1_StartConvert_DMA(uint32_t destAddr, uint16_t len);

#endif

文件:stm32_base/34_adc_double_register/Hardware/ADC/adc.c

#include "adc.h"

// 初始化
void ADC1_Init(void)
{
    // 1. 时钟配置
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    // ADC的时钟频率不能超过14MHz,需预分频 10 - 6分频
    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;

    // RCC->APB2ENR | RCC_APB2ENR_IOPCEN;

    // 2. GPIO配置模式:PC0 - 模拟输入,CNF = 00, MODE = 00
    GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);

    // PC2 - 模拟输入,CNF = 00, MODE = 00
    GPIOC->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2);

    // 3. ADC配置
    // 3.1 开启扫描模式
    ADC1->CR1 |= ADC_CR1_SCAN;

    // 3.2 开启连续转换模式
    ADC1->CR2 |= ADC_CR2_CONT;

    // 3.3 数据对齐方式:0 - 右对齐
    ADC1->CR2 &= ~ADC_CR2_ALIGN;

    // 3.4 配置采样时间,010 - 13.5个ADC时钟周期
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_1;
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP12;

    // 3.5 配置通道序列
    // 3.5.1 设置规则组通道数目L = 0001,2个通道
    ADC1->SQR1 &= ~ADC_SQR1_L;
    ADC1->SQR1 |= ADC_SQR1_L_0;

    // 3.5.2 将通道号10写入 SQ1(在SQR3里)
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= 10 << 0;      // 低5位,第一个通道号,设为10

    // 将通道号12写入 SQ2(在SQR3里)
    ADC1->SQR3 &= ~ADC_SQR3_SQ2;
    ADC1->SQR3 |= 12 << 5;      // 中间5位,第二个通道号,设为12

    // 3.6 选择外部触发方式(暂时注释)
    // ADC1->CR2 |= ADC_CR2_EXTTRIG;
    // ADC1->CR2 |= ADC_CR2_EXTSEL;
}

// DMA相关的初始化
void ADC1_DMA_Init(void)
{
    // 1. 开启时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 2. DMA传输方向DIR = 0,从外设读取
    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;

    // 3. 数据宽度:01 - 16位
    DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE_1;
    DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0;

    DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE_1;
    DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0;

    // 4. 地址是否增量:外设不增、存储器增
    DMA1_Channel1->CCR &= ~DMA_CCR1_PINC;
    DMA1_Channel1->CCR |= DMA_CCR1_MINC;

    // 5. 开启循环模式
    DMA1_Channel1->CCR |= DMA_CCR1_CIRC;

    // 6. ADC1使能DMA请求
    ADC1->CR2 |= ADC_CR2_DMA;
}

// 启动ADC开始转换,DMA配置完成(源地址固定为ADC1->DR)
void ADC1_StartConvert_DMA(uint32_t destAddr, uint16_t len)
{
    // 0. DMA配置参数:通道1
    DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);
    DMA1_Channel1->CMAR = destAddr;
    DMA1_Channel1->CNDTR = len;

    DMA1_Channel1->CCR |= DMA_CCR1_EN;

    // 1. 上电,将ADC从断电模式唤醒
    ADC1->CR2 |= ADC_CR2_ADON;

    // 2. 执行校准,等待结束
    ADC1->CR2 |= ADC_CR2_CAL;
    while (ADC1->CR2 & ADC_CR2_CAL)
    {}

    // 3. 连续方式启动AD转换
    // ADC1->CR2 |= ADC_CR2_SWSTART;
    ADC1->CR2 |= ADC_CR2_ADON;
    
    // 4. 等待转换结束
    while ((ADC1->SR & ADC_SR_EOC) == 0)
    {}
}

文件:stm32_base/34_adc_double_register/User/main.c

#include "usart.h"
#include "adc.h"
#include "delay.h"

// 定义一个大小为2的数组,保存转换结果
uint16_t data[2];

int main(void)
{
	// 初始化
	USART_Init();
	ADC1_Init();
	ADC1_DMA_Init();

	printf("Hello world!\n");

	// 启动AD转换
	ADC1_StartConvert_DMA((uint32_t)data, 2);

	while (1)
	{
		double v1 = data[0] * 3.3 / 4095;
		double v2 = data[1] * 3.3 / 4095;
		printf("V_PC0 = %.2f, V_PC2 = %.2f\n", v1, v2);

		Delay_ms(1000);
	}
}

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