STM32 ADC采样详解(标准库版):普通模式与DMA模式,附完整可用代码

前言

ADC(模数转换器)是嵌入式开发中测量模拟信号的核心外设,从简单的电压读取到复杂的传感器数据采集都离不开它。STM32F103 内置 12 位逐次逼近型 ADC,最多支持 18 个通道,在 72MHz 主频下最高采样率达 1Msps,性能非常实用。

在实际项目中,我们最常接触两种采集方式:

  • 普通查询模式:简单直接,适合低频率、单通道的采集。
  • DMA 传输模式:配合 DMA 进行多通道连续采集,CPU 零负担,是工程应用的主流选择。

本文使用 标准外设库(SPL),以 STM32F103C8T6 为核心,先讲清 ADC 时基与规则通道原理,再分别给出普通模式和 DMA 模式的 完整可运行代码,所有配置均经过验证,复制到你的工程里即可点亮功能。


一、STM32 ADC 基础原理

1.1 逐次逼近型 ADC 如何工作

STM32 的 ADC 采用逐次逼近架构,内部通过二分法比较输入电压与 DAC 输出,经过 12 个时钟周期(12 位分辨率)逼近真实值,转换结果范围为 0 ~ 4095

1.2 规则通道与注入通道

  • 规则通道组:可安排 16 个通道的转换序列,按顺序依次转换,结果只存到一个 共用的 16 位数据寄存器 ADC_DR 中。如果 CPU 来不及读取,新的结果会覆盖旧值,这就是多通道采集必须依赖 DMA 的根本原因。
  • 注入通道组:最多 4 个通道,拥有 独立的数据寄存器,可打断规则通道的转换序列,常用于紧急采样(如电流环控制)。

本文将专注于使用最广泛的 规则通道组

1.3 时钟与采样时间

ADC 挂载在 APB2 总线上,时钟通过分频器供给,最大不超过 14MHz
标准配置:72MHz ÷ 6 = 12MHz
单次转换所需时间 = 采样时间 + 12.5 个 ADC 时钟周期。例如采样时间选 55.5 周期,则总时间 ≈ 68 / 12M ≈ 5.67μs,采样率约 176kHz。

1.4 关键配置参数一览

参数含义普通单通道DMA 多通道
ADC_Mode工作模式ADC_Mode_IndependentADC_Mode_Independent
ADC_ScanConvMode扫描模式DISABLEENABLE
ADC_ContinuousConvMode连续转换按需ENABLE
ADC_DataAlign数据对齐ADC_DataAlign_RightADC_DataAlign_Right
ADC_NbrOfChannel规则通道数量1实际通道数

二、普通模式(查询法)

特点:CPU 主动查询 EOC 标志,手动读取 ADC_DR。代码清晰,适用于低频单通道场景。

2.1 单通道单次转换(PA0)

/* adc_single.c */
#include "adc_single.h"

/**
 * @brief  ADC1 单通道初始化(PA0, 通道0)
 */
void ADC1_Single_Init(void)
{
    ADC_InitTypeDef       ADC_InitStructure;
    GPIO_InitTypeDef      GPIO_InitStructure;

    /* 1. 开启时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);               // ADC 时钟 = 72M/6 = 12MHz

    /* 2. PA0 模拟输入 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 3. ADC 基础配置 */
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = DISABLE;          // 单通道不扫描
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;          // 单次转换
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None; // 软件触发
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = 1;               // 通道数
    ADC_Init(ADC1, &ADC_InitStructure);

    /* 4. 配置规则通道:通道0,排序第1,采样时间 55.5 周期 */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    /* 5. 使能 ADC,并校准(必须先使能再校准) */
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

/**
 * @brief  触发一次软件转换,返回 ADC 值
 */
uint16_t ADC1_GetValue(void)
{
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);                 // 启动转换
    while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
    return ADC_GetConversionValue(ADC1);                    // 读取结果(同时清EOC)
}
/* adc_single.h */
#ifndef __ADC_SINGLE_H__
#define __ADC_SINGLE_H__

#include "stm32f10x.h"

void     ADC1_Single_Init(void);
uint16_t ADC1_GetValue(void);

#endif

使用示例(main.c)

int main(void)
{
    uint16_t adc_val;
    float    vol;
    ADC1_Single_Init();

    while(1) {
        adc_val = ADC1_GetValue();
        vol = adc_val * 3.3f / 4095;   // 参考电压 3.3V
        // 可通过串口打印 vol
    }
}

2.2 多通道连续扫描(仅演示,不推荐用于实际项目)

开启扫描 + 连续转换后,ADC 会按顺序循环转换各个通道,但 读取时无法确认当前数据属于哪个通道,极易覆盖。此模式仅供学习了解流程,正式项目请直接跳到 DMA 模式

/* 不推荐!仅作演示 */
void ADC1_Multi_NoDMA_Init(void)
{
    // ... 时钟、GPIO 同前 ...
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;  // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_NbrOfChannel       = 2;
    // ... 其余相同 ...
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    // 启动一次连续转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

警告:这种方式读到的数据顺序不可靠,请不要在项目中采用。


三、DMA 模式(多通道自动采集)

3.1 DMA 如何解决多通道采集难题

DMA(直接存储器访问)可以不经过 CPU,直接将 ADC_DR 中的结果搬运到内存数组里。配合 扫描模式 + 连续转换 + DMA 循环模式,可实现:

  • ADC 自动扫描所有通道 → 转换结果由 DMA 按序存入数组 → 数组满后自动从头覆盖。
  • CPU 完全被解放,只需在需要时读取数组即可。

3.2 双通道 DMA 循环传输(PA0、PA1)

我们以最常见的双通道电压采集为例,数据存放于 ADC_ConvertedValue[2] 中,索引 0 对应通道 0(PA0),索引 1 对应通道 1(PA1)。

/* adc_dma.c */
#include "adc_dma.h"

#define ADC_CH_NUM  2
uint16_t ADC_ConvertedValue[ADC_CH_NUM] = {0};

/**
 * @brief  ADC1 + DMA 初始化(PA0/通道0, PA1/通道1)
 */
void ADC1_DMA_Init(void)
{
    ADC_InitTypeDef     ADC_InitStructure;
    GPIO_InitTypeDef    GPIO_InitStructure;
    DMA_InitTypeDef     DMA_InitStructure;

    /*========== 1. 使能时钟 ==========*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);      // DMA1 时钟
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);                      // ADC 时钟 12MHz

    /*========== 2. 配置 PA0, PA1 为模拟输入 ==========*/
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /*========== 3. 配置 DMA1 通道1 ==========*/
    DMA_DeInit(DMA1_Channel1);                              // 复位 DMA 通道

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;              // 外设地址
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)ADC_ConvertedValue;     // 内存地址
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;            // 外设到内存
    DMA_InitStructure.DMA_BufferSize         = ADC_CH_NUM;                       // 传输个数(两个通道)
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;        // 外设地址固定
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;             // 内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  // 半字(16位)
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;                // 循环模式
    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);                         // 使能 DMA 通道

    /*========== 4. 配置 ADC1 ==========*/
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;                     // 多通道必须扫描
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;                     // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CH_NUM;                 // 通道总数
    ADC_Init(ADC1, &ADC_InitStructure);

    /* 配置规则通道:序列1 - 通道0, 序列2 - 通道1 */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);

    /* 使能 ADC 的 DMA 请求(极易遗漏!) */
    ADC_DMACmd(ADC1, ENABLE);

    /* 使能 ADC 并校准 */
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    /* 启动连续转换(只需触发一次,之后自动循环) */
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
/* adc_dma.h */
#ifndef __ADC_DMA_H__
#define __ADC_DMA_H__

#include "stm32f10x.h"

extern uint16_t ADC_ConvertedValue[2];

void ADC1_DMA_Init(void);

#endif

主程序使用示例(main.c)

#include "adc_dma.h"

int main(void)
{
    float vol_ch0, vol_ch1;
    ADC1_DMA_Init();   // 启动后 DMA 自动循环搬运

    while(1) {
        /* ADC_ConvertedValue[0] 始终存放通道0(PA0)的最新值 */
        /* ADC_ConvertedValue[1] 始终存放通道1(PA1)的最新值 */
        vol_ch0 = ADC_ConvertedValue[0] * 3.3f / 4095;
        vol_ch1 = ADC_ConvertedValue[1] * 3.3f / 4095;
        // 延时或处理数据……
    }
}

3.3 代码要点批注

  • DMA_Mode_Circular:搬运完两个通道后自动跳回数组开头,与 ADC 连续转换完美配合,永不停止。
  • DMA_MemoryInc = Enable:每搬运一次地址递增,保证 [0] 存通道 0,[1] 存通道 1。
  • ADC_DMACmd(ADC1, ENABLE):老手也偶尔忘记这行,没有它 DMA 不会收到任何请求。
  • 校准顺序:必须 ADC_Cmd(ENABLE) 再校准,否则校准无效,结果会有偏差且很难排查。

四、常见避坑指南

  1. 校准顺序错误
    ADC_Cmd 一定要在校准函数之前调用,这是标准库用户最容易踩的坑。

  2. 忘记设置 ADC 时钟分频
    不调用 RCC_ADCCLKConfig(RCC_PCLK2_Div6) 的话,ADC 时钟默认为 72MHz,远超 14MHz 上限,会导致 ADC 工作异常。

  3. DMA 通道对应错误
    ADC1 固定使用 DMA1_Channel1,其他外设不同通道,切勿混淆。

  4. 数据错位(通道对应不上)
    检查 ADC_RegularChannelConfig 的排序参数:第 1 个通道对应数组 [0],第 2 个对应 [1],以此类推。

  5. 查询模式下忘记等待 EOC
    若不等转换完成直接读 ADC_DR,得到的是旧值或无效值。使用 ADC_GetConversionValue 会同时清除 EOC 标志。


五、总结

模式适用场景优缺点
普通单次查询低频单通道(如温度巡检)简单,但阻塞 CPU
多通道查询不建议实际使用数据极易覆盖,仅作学习
DMA 循环多通道连续采集(推荐)CPU 零负担,数据准确可靠

掌握了普通模式和 DMA 模式的标准库写法后,应对绝大多数 STM32 模拟信号采集需求都将游刃有余。建议先在普通模式下调通单通道,确认硬件没问题,再切换到 DMA 模式,这样可以排除很多基础配置错误。

文中代码均可直接复制到 KEIL 标准库工程中编译运行,若遇到问题,欢迎在评论区讨论交流。


参考资料

  • STM32F103x8/xB 数据手册
  • RM0008 Reference Manual
  • STM32F10x Standard Peripherals Library 使用手册
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值