11-ADC模数转换.md 22 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=自动注入)
CR2 0x08 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐,DMA=DMA 使能)
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 时钟)

ADC1->CR2 |= ADC_CR2_CAL;             // 开始校准
while (ADC1->CR2 & ADC_CR2_CAL)       // 等待校准完成(CAL 位硬件自动清 0)
{
}
// 校准完成,ADC 准备就绪

HAL 库版本(CubeMX 自动在 MX_ADC1_Init() 后由用户手动调用):

HAL_ADCEx_Calibration_Start(&hadc1);

校准原理:ADC 内部测量一次 VREFINT,根据误差调整内部电容阵列。


实验对比总览

本笔记包含 4 个实验项目,覆盖单/双通道 × 寄存器/HAL 的全部组合:

项目 通道数 实现方式 DMA 关键文件
32_adc_single_register 单通道(PC0/CH10) 寄存器 Hardware/ADC/adc.c/.h, User/main.c
33_adc_single_hal 单通道(PC0/CH10) HAL Core/Src/adc.c, Core/Src/main.c
34_adc_double_register 双通道(PC0/CH10, PC2/CH12) 寄存器 DMA1 CH1 Hardware/ADC/adc.c/.h, User/main.c
35_adc_double_hal 双通道(PC0/CH10, PC2/CH12) HAL DMA1 CH1 Core/Src/adc.c, Core/Src/dma.c, Core/Src/main.c

实验一:单通道 ADC 采集(寄存器版)

项目路径stm32/32_adc_single_register

需求:使用 ADC1 通道 10(PC0)采集模拟电压,通过串口打印原始值和电压值。

配置摘要

  • 单通道、连续转换、非扫描模式
  • ADC 时钟 = APB2/6 = 12MHz
  • 采样时间 = 7.5 周期

文件:32_adc_single_register/Hardware/ADC/adc.h

#ifndef __ADC_H
#define __ADC_H

#include "stm32f10x.h"

void ADC1_Init(void);
void ADC1_StartConvert(void);
double ADC1_ReadV(void);

#endif

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

#include "adc.h"

void ADC1_Init(void)
{
    // 1. 时钟配置
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;

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

    // 3. ADC配置
    // 3.1 关闭扫描模式
    ADC1->CR1 &= ~ADC_CR1_SCAN;

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

    // 3.3 右对齐
    ADC1->CR2 &= ~ADC_CR2_ALIGN;

    // 3.4 采样时间:001 - 7.5周期
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;

    // 3.5 通道序列
    // 3.5.1 L = 0,1个通道
    ADC1->SQR1 &= ~ADC_SQR1_L;

    // 3.5.2 SQ1 = 通道10
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= 10 << 0;
}

void ADC1_StartConvert(void)
{
    // 1. 上电
    ADC1->CR2 |= ADC_CR2_ADON;

    // 2. 校准
    ADC1->CR2 |= ADC_CR2_CAL;
    while (ADC1->CR2 & ADC_CR2_CAL)
    {}

    // 3. 启动转换(连续模式)
    ADC1->CR2 |= ADC_CR2_ADON;

    // 4. 等待首次转换结束
    while ((ADC1->SR & ADC_SR_EOC) == 0)
    {}
}

double ADC1_ReadV(void)
{
    return ADC1->DR * 3.3 / 4095;
}

文件: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");

    ADC1_StartConvert();

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

说明:连续转换模式下,只需在 ADC1_StartConvert() 中等待首次 EOC,后续硬件自动持续转换,ADC1_ReadV() 直接读取 DR 寄存器获得最新结果。


实验二:单通道 ADC 采集(HAL 版)

项目路径stm32/33_adc_single_hal

需求:与实验一相同,使用 HAL 库实现 ADC1 通道 10(PC0)单通道连续采集。

文件:33_adc_single_hal/Core/Src/adc.c

#include "adc.h"

ADC_HandleTypeDef hadc1;

void MX_ADC1_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    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;
    if (HAL_ADC_Init(&hadc1) != HAL_OK)
    {
        Error_Handler();
    }

    sConfig.Channel = ADC_CHANNEL_10;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (adcHandle->Instance == ADC1)
    {
        __HAL_RCC_ADC1_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();

        GPIO_InitStruct.Pin = GPIO_PIN_0;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
    }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{
    if (adcHandle->Instance == ADC1)
    {
        __HAL_RCC_ADC1_CLK_DISABLE();
        HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);
    }
}

文件:33_adc_single_hal/Core/Src/main.c

#include "main.h"
#include "adc.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    printf("Hello, world\n");

    HAL_ADCEx_Calibration_Start(&hadc1);

    HAL_ADC_Start(&hadc1);

    while (1)
    {
        double v = HAL_ADC_GetValue(&hadc1) * 3.3 / 4095;
        printf("V = %.2f\n", v);
        HAL_Delay(1000);
    }
}

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);

    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
    PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
    HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
}

说明

  • CubeMX 生成 adc.cMX_ADC1_Init + MSP 回调)和 gpio.cMX_GPIO_Init,使能 GPIOC 时钟)
  • 时钟配置在 SystemClock_Config() 中用 RCC_ADCPCLK2_DIV6 设置 ADC 预分频
  • 校准需用户手动调用 HAL_ADCEx_Calibration_Start(CubeMX 不自动执行)
  • 阻塞式读取:HAL_ADC_Start → 循环中 HAL_ADC_GetValue(连续模式下自动更新 DR)

实验三:双通道 ADC + DMA 采集(寄存器版)

项目路径stm32/34_adc_double_register

需求:使用 ADC1 通道 10(PC0)和通道 12(PC2)采集两路模拟电压,通过 DMA1 通道 1 自动搬运转换结果到内存数组。

配置摘要

  • 双通道、连续转换、扫描模式
  • DMA1 通道 1:外设→内存,16 位,内存地址递增,循环模式
  • 规则序列长度 L=1(2 个通道),SQ1=CH10, SQ2=CH12

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

#ifndef __ADC_H
#define __ADC_H

#include "stm32f10x.h"

void ADC1_Init(void);
void ADC1_DMA_Init(void);
void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len);

#endif

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

#include "adc.h"

void ADC1_Init(void)
{
    // 1. 时钟配置
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;

    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. GPIO配置:PC0、PC2 - 模拟输入
    GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
    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 右对齐
    ADC1->CR2 &= ~ADC_CR2_ALIGN;

    // 3.4 采样时间:001 - 7.5周期(通道10 和 通道12)
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
    ADC1->SMPR1 &= ~ADC_SMPR1_SMP12;
    ADC1->SMPR1 |= ADC_SMPR1_SMP12_0;

    // 3.5 通道序列
    // 3.5.1 L = 1,2个通道
    ADC1->SQR1 &= ~ADC_SQR1_L;
    ADC1->SQR1 |= ADC_SQR1_L_0;

    // 3.5.2 SQ1 = 通道10, SQ2 = 通道12
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= 10 << 0;
    ADC1->SQR3 &= ~ADC_SQR3_SQ2;
    ADC1->SQR3 |= 12 << 5;
}

void ADC1_DMA_Init(void)
{
    // 1. DMA时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    // 2. 方向:从外设读取
    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;

    // 3. 数据宽度: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. ADC使能DMA请求
    ADC1->CR2 |= ADC_CR2_DMA;
}

void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len)
{
    // 0. DMA参数配置
    DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);
    DMA1_Channel1->CMAR = destAddr;
    DMA1_Channel1->CNDTR = len;
    DMA1_Channel1->CCR |= DMA_CCR1_EN;

    // 1. 上电
    ADC1->CR2 |= ADC_CR2_ADON;

    // 2. 校准
    ADC1->CR2 |= ADC_CR2_CAL;
    while (ADC1->CR2 & ADC_CR2_CAL)
    {}

    // 3. 启动转换
    ADC1->CR2 |= ADC_CR2_ADON;

    // 4. 等待首次转换结束
    while ((ADC1->SR & ADC_SR_EOC) == 0)
    {}
}

文件:34_adc_double_register/User/main.c

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

uint16_t data[2] = {0};

int main(void)
{
    USART_Init();
    ADC1_Init();
    ADC1_DMA_Init();

    printf("Hello, world!\n");

    ADC1_DMA_StartConvert((uint32_t)data, 2);

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

关键要点

  1. 扫描模式CR1_SCAN)必须开启,否则只能转换第一个通道
  2. 序列长度 L = 通道数 - 1,即 ADC_SQR1_L_0(=1)表示 2 个通道
  3. DMA 的 外设地址不增(CPAR 固定指向 ADC1->DR)、内存地址递增(CMAR 指向数组,自动偏移)
  4. 循环模式(CIRC)使 DMA 持续搬运,与 ADC 连续转换匹配
  5. CNDTR 初始值为 2,每完成一次 DMA 传输自动减 1,循环模式下到达 0 时自动重载

实验四:双通道 ADC + DMA 采集(HAL 版)

项目路径stm32/35_adc_double_hal

需求:与实验三相同,使用 HAL 库 + CubeMX 实现双通道 DMA 采集。

文件:35_adc_double_hal/Core/Src/adc.c

#include "adc.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void MX_ADC1_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 2;
    if (HAL_ADC_Init(&hadc1) != HAL_OK)
    {
        Error_Handler();
    }

    sConfig.Channel = ADC_CHANNEL_10;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }

    sConfig.Channel = ADC_CHANNEL_12;
    sConfig.Rank = ADC_REGULAR_RANK_2;
    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (adcHandle->Instance == ADC1)
    {
        __HAL_RCC_ADC1_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();

        GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_2;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

        // ADC1 DMA Init
        hdma_adc1.Instance = DMA1_Channel1;
        hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
        hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
        hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
        hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
        hdma_adc1.Init.Mode = DMA_CIRCULAR;
        hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
        HAL_DMA_Init(&hdma_adc1);

        __HAL_LINKDMA(adcHandle, DMA_Handle, hdma_adc1);
    }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{
    if (adcHandle->Instance == ADC1)
    {
        __HAL_RCC_ADC1_CLK_DISABLE();
        HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0 | GPIO_PIN_2);
        HAL_DMA_DeInit(adcHandle->DMA_Handle);
    }
}

文件:35_adc_double_hal/Core/Src/dma.c

#include "dma.h"

void MX_DMA_Init(void)
{
    __HAL_RCC_DMA1_CLK_ENABLE();
}

文件:35_adc_double_hal/Core/Src/gpio.c

#include "gpio.h"

void MX_GPIO_Init(void)
{
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
}

文件:35_adc_double_hal/Core/Src/main.c

#include "main.h"
#include "adc.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    printf("Hello, world\n");

    uint16_t data[2] = {0};

    HAL_ADCEx_Calibration_Start(&hadc1);

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)data, 2);

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

关键要点

  1. 初始化顺序很重要MX_GPIO_Init()MX_DMA_Init()MX_ADC1_Init()。DMA 必须在 ADC 之前初始化,因为 HAL_ADC_Init() 内部会调用 HAL_ADC_MspInit(),而 MSP 中配置了 DMA(__HAL_LINKDMA)。
  2. MX_DMA_Init() 仅使能 DMA1 时钟,实际的 DMA 通道配置在 HAL_ADC_MspInit() 中完成。
  3. CubeMX 生成代码时,采样时间可能为 ADC_SAMPLETIME_1CYCLE_5(默认),实际项目中需根据信号源阻抗调整。
  4. DMA 完成回调 HAL_ADC_ConvCpltCallback 可通过重载实现每轮转换完成后的处理。

核心函数速查表

操作 寄存器版(单通道) HAL 版(单通道) 寄存器版(双通道+DMA) HAL 版(双通道+DMA)
开启 ADC `CR2 = ADON` HAL_ADC_Init() `CR2
校准 `CR2 = CAL; while(CR2 & CAL)` HAL_ADCEx_Calibration_Start() `CR2
配置通道 SQR3.SQ1 = ch ADC_ChannelConfTypeDef + HAL_ADC_ConfigChannel SQR3.SQ1/SQ2 = ch 同上(多通道重复调用)
启动转换 `CR2 = ADON` HAL_ADC_Start() `CR2
查询完成 while(!(SR & EOC)) HAL_ADC_PollForConversion() while(!(SR & EOC))(首次等待) 回调 HAL_ADC_ConvCpltCallback
读取结果 val = DR HAL_ADC_GetValue() DMA 自动搬运到 data[] DMA 自动搬运到 data[]
DMA 配置 N/A N/A 手动配置 DMA1 寄存器 MX_DMA_Init() + MSP 中 HAL_DMA_Init()
电压换算 val × 3.3 / 4095 同上 data[n] × 3.3 / 4095 同上
停止 ADC CR2 &= ~ADON HAL_ADC_Stop() CR2 &= ~ADON + CCR &= ~EN HAL_ADC_Stop_DMA()

常见问题与避坑

  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
  8. HAL 双通道 DMA 数据错位 → 确认 MX_DMA_Init()MX_ADC1_Init() 之前调用,否则 MSP 中的 __HAL_LINKDMA 不会生效