ADC/DMA多通道采集例程 - 基于STM32F411

1. 为什么要用 ADC + DMA?

核心原理:解放 CPU!
正常情况下,CPU 需要死等 ADC 转换完成,再把数据读出到内存。如果 ADC 通道多、采样频率高,CPU 就会被耗死在“等待-读取”的枯燥操作中。
引入 DMA(直接内存访问,硬件搬运工) 后,ADC 每转换完一个通道的数据,会直接触发 DMA 硬件,由 DMA 自动将外设数据搬运到我们指定的内存数组里,整个过程 CPU 完全不需要插手。

2. STM32CubeMX 配置指南 (基于 F411)

假设我们需要连续采集两个模拟传感器的数据(例如接入 PA0PA1)。

第一步:配置 ADC 引脚

  1. 侧边栏打开 Analog -> ADC1
  2. 勾选 IN0IN1,对应的 PA0PA1 引脚会自动配置为模拟模式。

第二步:配置 ADC 参数

打开 Parameter Settings 面板:

  • Scan Conversion Mode (扫描模式)Enable(多通道采集必须开启)。
  • Continuous Conversion Mode (连续转换)Enable(开启后 ADC 会自动一直采,不需要 CPU 每次去触发)。
  • Number of Conversion (转换数量):改为 2(因为我们用了两个输入通道)。
  • 在下方的 Rank 中,将 Rank 1 和 Rank 2 分别绑定为 Channel 0 和 Channel 1(这决定了数据存进数组的先后顺序)。

第三步:配置 DMA

打开 DMA Settings 面板,点击 Add 添加 ADC1 的 DMA 请求:

  • Mode (模式):改为 Circular (循环模式,搬运满一轮后自动回到数组开头继续搬,实现自动刷新)。
  • Data Width (数据宽度)
    • Peripheral (外设) 选择 Half Word (16位,因为 STM32F411 的 ADC 结果是12位)。
    • Memory (内存) 也选择 Half Word
  • Increment Address (地址自增)
    • Peripheral 保留不勾选 (ADC永远只有那一个数据寄存器,地址固定)。
    • Memory 必须 勾选 (每次搬运自动放到数组的下一个位置,否则后采的数据会覆盖前面)。

3. 代码实战 (HAL 库)

CubeMX 配置并生成代码后,我们只需要手动添加不超过 10 行代码就能让它跑起来。

1. 定义存储数组

main.c 的全局变量定义区域(/* USER CODE BEGIN PV */),预留出接收数据的数组:

1
2
3
/* USER CODE BEGIN PV */
uint16_t adc_values[2]; // 存放两个通道的 ADC 结果,类型必须与 DMA 配的 Half Word 一致
/* USER CODE END PV */

2. 启动 ADC 与 DMA

main() 函数中,各种外设初始化完成后(/* USER CODE BEGIN 2 */),开启自动采集:

1
2
3
4
/* USER CODE BEGIN 2 */
// 一条语句:同时启动外设ADC并开启DMA搬运
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, 2);
/* USER CODE END 2 */

注:代码执行到此处后,adc_values[0]adc_values[1] 里的值就会被硬件在后台悄悄且源源不断地更新,主循环里的代码直接当成普通变量读取即可,再也不需要去管底层的调用。

3. (进阶) 处理转换回调

如果想在 DMA 每完整搬运完一轮(集齐2个数据)时立即做点什么运算,可以在 main.c 的最底下写回调函数:

1
2
3
4
5
6
7
8
9
10
11
/* USER CODE BEGIN 4 */
// DMA 中断回调:搬运完成指定数量的数据后自动触发
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1)
{
// 此时一轮数据采集已经完成
// 你可以通过标志位通知主函数去处理 adc_values 里的最新数据
}
}
/* USER CODE END 4 */

4. 核心函数剖析

HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length)

  1. hadc:ADC 句柄指针,传入 &hadc1 即可。
  2. pData:目标内存的首地址。底层参数类型设定的是32位指针,为了不报警告,用 (uint32_t *)adc_values 强转一下。
  3. Length传输的数据长度。DMA 搬运多少次判定为“完成一整轮任务”。我们有 2 个通道,所以填 2。此数值通常等同于你的数组大小和开启的通道数。

避坑指南
在多通道 ADC 配合 DMA 时,初学者最容易遇到的情况是读取的数据错位(通道乱套)。如果你发现 adc_values[0] 里的数值似乎是 PA1 的,那一定是因为在 CubeMX 排列 Rank 顺序时排反了,记住:数组索引永远严格对应 Rank 序列配置的先后顺序

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
/* USER CODE BEGIN PV */
uint16_t adc_values[2]; // 存放两个通道的 ADC 结果
/* USER CODE END PV */

int main(void)
{

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/* Enable the CPU Cache */

/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();

/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();

/* MCU Configuration--------------------------------------------------------*/

/* Update SystemCoreClock variable according to RCC registers values. */
SystemCoreClockUpdate();
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Update SystemCoreClock variable */
SystemCoreClockUpdate();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
SystemIsolation_Config();
MX_GPIO_Init();
MX_DMA_Init(); // 必须先初始化 DMA
MX_ADC1_Init(); // 再初始化 ADC1

/* USER CODE BEGIN 2 */
// 启动 ADC 和 DMA,指定目标缓冲区地址和长度
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, 2);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 在这里可以直接读取 adc_values[0] 和 adc_values[1] 的最新值
// 硬件会自动在后台刷新这些值,CPU 零负载

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

ADC/DMA多通道采集例程 - 基于STM32F411
https://rubbishbro.github.io/2026/03/24/ADC-DMA_example/
Author
John Doe
Posted on
March 24, 2026
Licensed under