tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — ADC章节 + 配套代码32~35" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:ADC 就像一把精密的电压尺子——输入 0~3.3V 的模拟电压,输出 0~4095 的数字(12 位)。就像用一把刻有 4096 个刻度的尺子量高度,输入 1.65V 时输出 2048(中间值)。采样时间就是"尺子读数需要的时间",采样时间越长读数越准。规则组和注入组就像"普通队列"和"VIP 通道"——VIP 可以插队。
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) |
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
特点:
ADC 有两种转换序列:
| 组 | 最大通道数 | 特点 | 用途 |
|---|---|---|---|
| 规则组 | 16 通道 | 常规采集,按顺序转换 | 周期性采集传感器数据 |
| 注入组 | 4 通道 | 可打断规则组的转换 | 紧急事件触发采集 |
类比:规则组是普通队列(一个个来),注入组是 VIP 通道——来了就插队。
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 单次转换 | 每触发一次转换一个通道 | 按键触发采集 |
| 连续转换 | 转换完自动开始下一次 | 持续监测电压 |
| 扫描模式 | 按序列依次转换所有通道 | 多通道循环采集 |
| 间断模式 | 每触发一次转换一部分通道 | 超多通道分批次 |
| 触发源 | 说明 |
|---|---|
| 软件触发 | CR2.SWSTART=1 启动转换 |
| 定时器触发 | TIM1/2/3/4 的 TRGO 事件 |
| 外部引脚触发 | EXTI 线上升沿/下降沿 |
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(最准) |
采样时间选择原则:
| 寄存器 | 地址偏移 | 功能 |
|---|---|---|
| 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 上电后需要校准以提高精度:
// 校准流程
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,根据误差调整内部电容阵列。
项目路径: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 采集一次
}
}
项目路径: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 初始化(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() |