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/32_adc_single_register
需求:使用 ADC1 通道 10(PC0)采集模拟电压,通过串口打印原始值和电压值。
文件:stm32/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/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);
}
}
项目路径:stm32/34_adc_double_register
多通道 + DMA 是 ADC 的典型高效用法。实际项目中使用 ADC1 的通道10(PC0)和通道12(PC2),通过 DMA1 通道1自动搬运转换结果:
文件:stm32/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/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/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 初始化(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() |