STM32 ADC — 独立模式单通道 + DMA 连续采集
开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程:ADC—独立模式-单通道-DMA
参考源码:User/main.c、User/adc/bsp_adc.h、User/adc/bsp_adc.c
ADC(Analog-to-Digital Converter,模数转换器)把现实世界的连续模拟电压变成 MCU 能处理的数字量。本文用 ADC1 单通道 + DMA 循环搬运,实现"不占用 CPU"的连续电压采集,是 ADC 最经典、最高效的用法之一。
一、功能简介
把开发板上的电位器(滑变电阻,接 PC1 / ADC_CHANNEL_11)电压送到 ADC1,配置为:
- 独立模式(非双重 ADC 协同);
- 单通道(只采通道 11);
- 连续转换 + 扫描关闭(采完一个通道立刻从头再来);
- DMA 循环模式:每转换完一次,DMA 自动把结果搬进
ADC_ConvertedValue 变量。
主循环只需读取这个变量、套公式算出电压、用 printf 打印即可。因 DMA 持续搬运,CPU 几乎不参与采样本体。
核心知识点:
- ADC 通道、采样时间、对齐方式的概念;
- DMA 如何与 ADC 联动( Peripheral→Memory);
- 电压换算公式:
V = ADC值 / 4096 × 3.3。
二、硬件准备
| 项目 |
说明 |
| 开发板 |
野火 EBF STM32F103 Mini |
| ADC 外设 |
ADC1,通道 11(CH11 → PC1) |
| 信号源 |
板载电位器(滑动变阻器),中间抽头接 PC1 |
| 调试输出 |
USART1(115200)打印电压值 |
STM32F103 的 ADC 为 12 位,转换结果范围 0 ~ 4095,参考电压一般接 3.3V。
三、完整代码(带中文注释)
1. bsp_adc.h(引脚与 DMA 通道定义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#ifndef __BSP_ADC_H
#define __BSP_ADC_H
#include "stm32f1xx.h"
/* ADC GPIO 宏定义:CH11 -> PC1 */
#define RHEOSTAT_ADC_GPIO_PORT GPIOC
#define RHEOSTAT_ADC_GPIO_PIN GPIO_PIN_1
#define RHEOSTAT_ADC_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
/* ADC 序号宏定义 */
#define RHEOSTAT_ADC ADC1
#define RHEOSTAT_ADC_CLK_ENABLE() __HAL_RCC_ADC1_CLK_ENABLE();
#define RHEOSTAT_ADC_CHANNEL ADC_CHANNEL_11
/* ADC 使用的 DMA 通道:ADC1 固定对应 DMA1_Channel1 */
#define RHEOSTAT_ADC_DMA_CLK_ENABLE() __HAL_RCC_DMA1_CLK_ENABLE();
#define RHEOSTAT_ADC_DMA_STREAM DMA1_Channel1
void Rheostat_Init(void);
#endif /* __BSP_ADC_H */
|
2. bsp_adc.c(GPIO、DMA、ADC 配置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include "./adc/bsp_adc.h"
__IO uint32_t ADC_ConvertedValue; // DMA 搬运的目标:转换结果
DMA_HandleTypeDef hdma_adcx;
ADC_HandleTypeDef ADC_Handle;
ADC_ChannelConfTypeDef ADC_Config;
/* 第 1 步:配置 ADC 输入引脚为模拟模式 */
static void Rheostat_ADC_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RHEOSTAT_ADC_CLK_ENABLE();
RHEOSTAT_ADC_GPIO_CLK_ENABLE();
GPIO_InitStructure.Pin = RHEOSTAT_ADC_GPIO_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_ANALOG; // 必须设为模拟输入
HAL_GPIO_Init(RHEOSTAT_ADC_GPIO_PORT, &GPIO_InitStructure);
}
/* 第 2 步:配置 DMA 与 ADC 工作模式 */
static void Rheostat_ADC_Mode_Config(void)
{
/* ---- DMA 结构体参数初始化 ---- */
RHEOSTAT_ADC_DMA_CLK_ENABLE();
hdma_adcx.Instance = RHEOSTAT_ADC_DMA_STREAM; // DMA1_Channel1
hdma_adcx.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设 -> 内存
hdma_adcx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增(固定为 ADC_DR)
hdma_adcx.Init.MemInc = DMA_MINC_DISABLE; // 这里只采 1 个变量,内存也不递增
hdma_adcx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据 16 位(ADC 是 16 位寄存器)
hdma_adcx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 内存数据 16 位
hdma_adcx.Init.Mode = DMA_CIRCULAR; // 循环模式:不断搬
hdma_adcx.Init.Priority = DMA_PRIORITY_MEDIUM; // 中等优先级
HAL_DMA_Init(&hdma_adcx);
/* 把 DMA 句柄绑到 ADC 句柄(HAL 内部用这个关联触发搬运) */
__HAL_LINKDMA(&ADC_Handle, DMA_Handle, hdma_adcx);
/* ---- ADC 时钟分频:PCLK2(72M)/8 = 9MHz,必须在允许范围 ---- */
RCC_PeriphCLKInitTypeDef ADC_CLKInit;
ADC_CLKInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
ADC_CLKInit.AdcClockSelection = RCC_ADCPCLK2_DIV8; // 分频因子 8
HAL_RCCEx_PeriphCLKConfig(&ADC_CLKInit);
/* ---- ADC 基本参数 ---- */
ADC_Handle.Instance = RHEOSTAT_ADC; // ADC1
ADC_Handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐
ADC_Handle.Init.ScanConvMode = DISABLE; // 关闭扫描(单通道不需要)
ADC_Handle.Init.ContinuousConvMode = ENABLE; // 连续转换
ADC_Handle.Init.NbrOfConversion = 1; // 规则序列 1 个转换
ADC_Handle.Init.DiscontinuousConvMode = DISABLE;
ADC_Handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
HAL_ADC_Init(&ADC_Handle); // 会回调 HAL_ADC_MspInit
/* ---- 配置规则通道:通道 11,序列 1,采样时间 55.5 周期 ---- */
ADC_Config.Channel = RHEOSTAT_ADC_CHANNEL;
ADC_Config.Rank = 1;
ADC_Config.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
HAL_ADC_ConfigChannel(&ADC_Handle, &ADC_Config);
/* 启动 ADC 的 DMA 转换:结果自动、循环地写入 ADC_ConvertedValue */
HAL_ADC_Start_DMA(&ADC_Handle, (uint32_t*)&ADC_ConvertedValue, 1);
}
/* 对外初始化入口 */
void Rheostat_Init(void)
{
Rheostat_ADC_GPIO_Config();
Rheostat_ADC_Mode_Config();
}
|
3. main.c(主循环读取并打印)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include "main.h"
#include "stm32f1xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./led/bsp_led.h"
#include <stdio.h>
#include "./adc/bsp_adc.h"
extern __IO uint32_t ADC_ConvertedValue; // DMA 持续更新的转换值
float ADC_Vol; // 换算后的电压值
static void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
int main(void)
{
SystemClock_Config(); // 72MHz
DEBUG_USART_Config(); // 串口用于打印
Rheostat_Init(); // 初始化 ADC + DMA
while (1)
{
/* 把 12 位 ADC 值换算成电压:Vref = 3.3V,满量程 4096 */
ADC_Vol = (float)ADC_ConvertedValue / 4096 * (float)3.3;
printf("\r\n The current AD value = 0x%04X \r\n", ADC_ConvertedValue);
printf("\r\n The current AD value = %f V \r\n", ADC_Vol);
Delay(0x8fffff); // 简单延时,控制台输出别太快
}
}
|
四、代码分段讲解
1. 为什么引脚要设成 GPIO_MODE_ANALOG?
ADC 输入必须走芯片内部的模拟通路。把引脚配成模拟模式,GPIO 的数字输入缓冲会被断开,避免数字电路对微弱模拟信号产生干扰,也能降低功耗。
2. DMA 方向:外设 → 内存
ADC 转换结果固定存放在 数据寄存器 ADC_DR(外设端地址不变,所以 PeriphInc = DISABLE)。DMA 每次转换完成被触发,自动把 DR 里的 16 位值搬到 ADC_ConvertedValue。因为是循环模式,搬完又从头搬,形成"永动机"式采集。
3. __HAL_LINKDMA 做了什么?
它把 DMA 句柄挂到 ADC 句柄的 DMA_Handle 字段上。这样 HAL_ADC_Start_DMA() 内部才知道用哪个 DMA 通道去搬 ADC 的数据——这是 HAL 关联"外设触发 DMA"的关键一步。
4. ADC 时钟为什么分频?
F103 的 ADC 最高工作频率约 14MHz,而 PCLK2 是 72MHz,所以必须分频(RCC_ADCPCLK2_DIV8 → 9MHz)。不分频会超出规格,采样不准甚至出错。
5. 电压换算
12 位分辨率 → 最大值 4095(代码里用 4096 当分母近似满量程)。参考电压 3.3V:
1
|
电压 V = (ADC值 / 4096) × 3.3
|
五、实验现象
- 打开串口助手(115200),复位开发板;
- 串口每隔约 1 秒打印一次:当前 ADC 原始值和换算电压;
- 旋转电位器,电压值随之在 0 ~ 3.3V 之间连续变化,说明 DMA 在后台持续把最新采样搬进了变量。
六、常见问题
Q1:打印的电压一直是 0 或满量程不变?
- 确认电位器中心抽头确实接到了 PC1,且两端分别接 3.3V 和 GND;
- 确认
RHEOSTAT_ADC_CHANNEL 与实际引脚对应(PC1 = ADC_CHANNEL_11)。
Q2:数据剧烈跳变、不稳定?
- 延长采样时间(
ADC_SAMPLETIME_55CYCLES_5 → 更大值);
- 在信号源并联一个小电容滤波,或在软件里做滑动平均。
Q3:想同时采多个通道怎么办?
- 开启
ScanConvMode = ENABLE,把 NbrOfConversion 设为通道数,并让 DMA 内存递增(MemInc=ENABLE),配合一个数组接收。详见 ADC—独立模式-多通道-DMA。
七、小结
- 单通道连续采集 + DMA 循环,是 ADC 的"零 CPU 占用"标配;
- 配置顺序:开时钟 → 引脚模拟模式 → DMA 参数 → ADC 参数 → 规则通道 → Start_DMA;
- DMA 把外设数据自动搬进内存,是后续 SPI、USART、DAC 等高速传输的共同基石。
八、拓展应用
- 多通道扫描:把电位器、温度、光照等多路信号编入规则序列,用 DMA 一次性搬进数组。
- 定时器触发采样:用 TIM 的 TRGO 触发 ADC,实现固定频率(如 1kHz)的等间隔采样,做简易示波器。
- 芯片温度采集:切换通道到内部温度传感器(
ADC_CHANNEL_TEMPSENSOR),免接外部元件测芯片温(见 ADC—芯片温度)。
- DMA 半传输/传输完成中断:在回调里做 FFT、求平均,搭一个低成本信号采集前端。