ADC_案例练习:独立模式多通道采集

引言

前面介绍了单通道采集电压的案例,在理论基础上进一步对ADC转换有了更深的理解,但这还不够,考虑到一般不可能总是只处理单个通道,因此还需要了解多通道转换如何处理更加合适。因此,本次我们继续练习一个案例------独立模式多通道转换

因此,建议初学的先看看单通道采集案例,再来学习本文内容,参见如下文章:

ADC_案例练习:独立模式单通道转换-CSDN博客https://blog.csdn.net/2301_79475128/article/details/156756159?spm=1001.2014.3001.5502


一、需求描述

基于寄存器操作,用一个ADC同时采集多个通道模拟电压。PC0是10通道,采集的是可变电阻器的电压。PC2对应的是12通道,使用杜邦线连接到电源或地,测试他们的电压。

二、硬件电路设计

对于采集可变电阻电压的部分的电路:

对于利用杜邦线接电源或地的引脚:

可见,对于采集可变电阻,使用到的引脚是PC0,其次可使用的ADC通道为ADC123的通道10之一即可,然后引脚另一边是一个可调电阻接VCC,显然当改变电阻阻值时,接入PC0的电压也会改变,也方便观察ADC转换采集到的电压情况。

对于采集电源和地的电压,使用的引脚是PC2,其可用的ADC通道为ADC123的通道12,一般使用ADC1的通道12即可,然后利用杜邦线将该引脚与电源VCC或者GND连接即可检测。


三、需求分析

根据前面的需求描述和硬件电路,做一下简单的需求分析:

硬件上,通过可变电阻配合 3V3 电源,让 PC0 引脚能输出可变电压,给 ADC 提供不同的采样输入;利用杜邦线连接PC2和VCC或GND,给另一个通道通过不同的电压输入。

软件上,通过寄存器配置ADC外设相关内容,实现PC0和PC2对输入的模拟电压进行采样和转换,此时为双通道的电压采集,需要考虑转换速度的问题,然后借助串口打印采集的电压数据。


四、软件设计

同样,在开始软件部分之前,建议各位优先学习和掌握ADC基础知识和常用寄存器配置项,这样更顺利一些,相关文章参见如下:ADC_基础知识-CSDN博客https://blog.csdn.net/2301_79475128/article/details/156691644?spm=1001.2014.3001.5502ADC_常用寄存器介绍-CSDN博客https://blog.csdn.net/2301_79475128/article/details/156728203?spm=1001.2014.3001.5502

4.1 思路分析

在已有前面单通道采集电压的经验后,这里在单纯的ADC配置上其实已经很简单了,但是考虑到多通道处理时的排队和速度问题,当多个通道同时采集时,一般就需要使用DMA来传输数据,否则数据如果来不及取出,则会导致数据被覆盖。

多通道使用DMA的原因介绍

来不及取出的主要原因其实前面介绍过,因为多通道处理时,规则通道组存储转换结果只有一个数据寄存器,如果不及时取出的话会导致数据覆盖,这是其一。

其二,我们单通道采集时,判断转换完成的标志位EOC,如果仔细查阅手册上关于该位的描述就会发现,EOC位是在规则通道组转换结束时才会置位,如下图所示。

这意味着,当出现多个通道需要处理时,单个通道转换完成后会因为规则组其他通道还没转换完就无法根据EOC标志位去判断数据寄存器中是否已经存在数据,进而导致只有拿到排在最后的通道的转换数据,也就是数据覆盖的问题。

因此,为了避免该情况的发生,我们干脆就不等数据寄存器装完后再获取了,而是直接利用DMA通道,在存储器与ADC1外设之间搭建一条高速通道,ADC1转换的结果除了会存储到规则通道数据寄存器外,还会直接经过DMA通道传输给存储器。这样的话,不仅转换速度快,而且无需依靠EOC来判断单通道转换情况,换句话说,此时的EOC直接判断多通道是否全部转换完成。因此,在多通道ADC转换时,我们一般就会借助DMA传输完成电压采集。

所以本次实现的一个大致思路即:在原来单通道转换的基础上增加一个通道 的配置,并且添加对DMA的配置 ,以及在ADC1启动转换中融入DMA模块帮助完成多通道的转换。

-- 融入DMA思路分析 --

考虑到ADC转换本身已经基本不变,因此这里主要分析一下融入的DMA配置的思路。根据前面对DMA的学习,我们知道一般配置流程即:首先确定好对应的DMA通道后,一是DMA初始化 部分:开启DMA时钟、设置传输方向、传输模式、数据宽度、外设存储器地址自增模式;二启动DMA传输:源目的地址配置、传输数据数量以及开启DMA通道使能。

所以,这里同样地,首先明确要使用的DMA通道,查阅手册可知,ADC1使用的DMA通道为DMA_Channel1,如下图所示。

其次就是进行DMA初始化操作

由于本案例是对ADC外设利用DMA通道,因此传输模式就是默认的存储器与外设,所以默认不用配置;

由于我们是要将ADC转换结果经过DMA通道获取,因此相当于ADC1这边是源,存储器那边是目的,换句话说就是ADC1到存储器即外设到存储器,因此传输方向为从外设读

由于ADC1这边数据寄存器均为16位,因此DMA通道传输的外设数据宽度为16位 即2字节,一般存储器数据宽度与外设数据宽度保持一致,因此存储器数据宽度也设置为16位,即2字节;

由于ADC1规则通道数据寄存器只有一个,利用DMA通道传输时,寄存器地址不需要自增,然后另一边存储器地址是需要自增的,因此对于外设存储器地址自增模式的设置为外设地址不执行自增操作、存储器地址执行自增操作

然后是启动DMA传输的设置。该部分在本案例中属于ADC1启动转换时融入的操作。

前面分析,此时ADC1外设属于源、存储器属于目的,因此直接将ADC1的规则通道数据寄存器DR的地址作为源地址、然后存储器的地址作为目的地址即可,而一般存储器对应程序中其实就是我们定义的变量,因此笔者打算直接定义数组,这样数组名就是地址,也方便传入;

然后就是设置好传输的数据数量,因为本次案例是双通道,也就是说经过DMA传输的ADC1转换结果为两个16位数据,即传输的数据量为2

最后就是开启DMA通道1的使能即可。

当然了,为了提高ADC1转换的灵活性,启动DMA通道传输的逻辑中,源地址、目的地址以及数据量都是通过传参的形式传入。

最后,综合ADC1配置以及融入的DMA配置做一个流程上的整理:

  1. **时钟配置:**开启 ADC1、GPIOC、DMA1 时钟;配置 ADC 时钟 6 分频至 12MHz(满足≤14MHz 要求);
  2. GPIO 配置:将 PC0(通道 10)、PC2(通道 12)配置为模拟输入模式(MODE=00、CNF=00);
  3. ADC 基础参数配置:开启扫描模式 + 连续转换模式,设置数据右对齐,配置通道 10/12 采样时间为 7.5 周期;
  4. 规则通道配置:设置序列长度为 2,序列 1 = 通道 10、序列 2 = 通道 12;
  5. DMA 初始化:配置 DMA1 通道 1 为 "外设→存储器" 传输,数据宽度 16 位,外设地址不增量、存储器地址增量,开启循环模式;
  6. ADC 关联 DMA:开启 ADC 的 DMA 模式,让转换结果自动通过 DMA 传输;
  7. DMA 参数设置:指定 DMA 传输源地址(ADC1_DR)、目标地址(用户指定)和传输长度;
  8. ADC 启动与校准:使能 DMA 通道,ADC 上电后执行校准,等待校准完成;
  9. 启动转换:二次置位 ADON 启动 ADC 转换,轮询 EOC 位等待首次全部转换完成,后续由 DMA + 连续转换自动循环采集。

注:这里还涉及到一个循环模式的设置,原因是ADC多通道转换时设置的连续转换模式,因此ADC转换结果会循环产生,而默认DMA通道传输只会传输1次,因此为了保证DMA传输连续性,也需要开启DMA的循环模式;再者,ADC外设要使用DMA传输,除了DMA模块本身的配置和全局使能外,其ADC自身也需要开启DMA模式,否则仍旧无法正常使用DMA。


4.2 程序实现(寄存器方式)

根据前面思路分析,接下来就依据上述流程完成代码编写。

4.2.1 ADC初始化

首先是对ADC进行初始化,根据上述分析,本案例ADC初始化就是在前面单通道基础上增加一个通道的配置即可。需要注意的是,多通道以后,就需要开启扫描模式,避免只对第一个通道进行转换,然后连续转换模式同样开启。参考代码如下:

cpp 复制代码
// 初始化
void ADC1_Init(void)
{
    // 1. 开启时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    // 分频:6分频 12MHz
    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;

    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. GPIO工作模式:PC0 PC2模拟输入 MODE-00 CNF-00
    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 通道采样时间:通道10 12 - 7.5周期
    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
    ADC1->SMPR1 &= ~(ADC_SMPR1_SMP10_2 | ADC_SMPR1_SMP10_1);

    ADC1->SMPR1 |= ADC_SMPR1_SMP12_0;
    ADC1->SMPR1 &= ~(ADC_SMPR1_SMP12_2 | ADC_SMPR1_SMP12_1);

    // 3.5 规则通道组序列配置
    // 3.5.1 序列长度:2 L-0001
    ADC1->SQR1 &= ~ADC_SQR1_L;
    ADC1->SQR1 |= ADC_SQR1_L_0;

    // 3.5.2 序列转换位置:sq1 通道为10
    ADC1->SQR3 &= ~ADC_SQR3_SQ1;
    ADC1->SQR3 |= (10 << 0);
    // 通道12
    ADC1->SQR3 &= ~ADC_SQR3_SQ2;
    ADC1->SQR3 |= (12 << 5);
}

:这里相比前面,还多了对GPIOC时钟的开启,原因是后续DMA启动前面需要先上电,而这里将用到的外设的时钟均开启基本能够解决该问题,因此便开启了。当然也可以不这样,而是先让ADC上电唤醒后在启动DMA也行。

4.2.2 DMA初始化

接着是配置DMA启动前的相关参数,按照前面思路分析的流程来即可,主要就注意一下外设使用DMA时需要开启DMA模式,否则仍无法启动DMA通道。参考代码如下:

cpp 复制代码
// DMA的初始化
void ADC1_DMA_Init(void)
{
    // 1. 开启时钟
    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. 开启DMA模式
    ADC1->CR2 |= ADC_CR2_DMA;
}

4.2.3 带DMA的ADC转换启动

最后就是实现一下启动带DMA的ADC转换函数。根据前面的分析,这里需要先设置DMA通道上对应的地址和数据量,接着是原来ADC转换的一般启动步骤。参考代码如下:

cpp 复制代码
// 开启转换(带DMA)
void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len)
{
    // 1. 设置源和目标地址:外设->存储器
    DMA1_Channel1->CPAR = (uint32_t)(&(ADC1->DR));
    DMA1_Channel1->CMAR = destAddr;

    // 2. 传输数量
    DMA1_Channel1->CNDTR = len;

    // 3. 使能DMA1通道
    DMA1_Channel1->CCR |= DMA_CCR1_EN;

    // 4. 上电唤醒
    ADC1->CR2 |= ADC_CR2_ADON;

    // 5. AD校准
    ADC1->CR2 |= ADC_CR2_CAL;

    // 轮询判断校准是否完毕,完成则清除
    while (ADC1->CR2 & ADC_CR2_CAL)
    {
    }

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

    // 4. 判断是否全部转换完成 EOC置位
    while (ADC1->SR & ADC_SR_EOC == 0)
    {
    }
}

:可以发现,这里使用的启动转换方式不是之前单通道案例中的方式,而是直接利用ADON位的二次置位实现,大家可根据实际情况使用不同方式,因为考虑到一般不会用到注入通道组,因此两种方式兼可。

4.2.4 主程序实现

最后完成main.c的实现。由于使用DMA通道,因此需要定义存储器中的量用于存放规则通道数据寄存器中经过DMA通道传输过来的数据,且存储器在STM32中一般表示SRAM,程序中的变量就存储在SRAM中,因此直接定义一个数组存储即可。然后其余逻辑与单通道类似,先对相关外设进行初始化,然后启动ADC转换,循环中间隔打印获取的数据。

cpp 复制代码
/*
 * @Description: 
 * @version: 
 * @Author: BreezeJuvenile
 * @Date: 2025-03-16 12:34:00
 * @LastEditors: BreezeJuvenile
 * @LastEditTime: 2025-03-16 13:37:01
 */
#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("PC0 = %.2f v\tPC2 = %.2f v \n", data[0] * 3.3 / 4095, data[1] * 3.3 / 4095);
		Delay_ms(1000);
	}
}

:因为经过DMA通道传输来的数据未经过电压转换,因此在打印数据时需要再进行一下转换,具体公式可参考前面单通道案例代码或者ADC基础知识文章中的介绍,这里不再赘述。

还要说明的是,使用DMA模式确实能够保证多通道转换时避免因为寄存器单一而覆盖数据,但是不太能误认为采样时间短了也能去避免这种问题。采样时间其实决定的是ADC处理在采样阶段从时间上划分模拟电压的间隔时间,也就是划分频率的意思,毕竟划分是一瞬间的事情,故采样时间应该是每一次划分瞬间间隔的时间周期。总结一句话,多通道采集时要效率最好采取DMA模式,采样时间影响的只是采集样本点数量,影响的主要是精度方面的,而对转换效率的影响应该主要得靠DMA进行提升。(个人理解,如有错误可评论区指正哈)


4.3 程序实现(HAL库方式)

接下来,我们在使用HAL库方式实现一下本案例。同理,这里主要介绍与本案例练习内容强相关的ADC配置部分的配置。

4.3.1 图形化配置

首先进入STM32CubeMX进行图形化配置。这里主要展示ADC部分的配置,串口和基础配置可参考前面的案例介绍的文章。

取消不用ADC1中的中断使能

DMA模式的配置如下图。

使用的俩引脚配置会自动配置好,如下图。

图形化配置完成后,笔者使用的MDK工具链,因此还会进入keil中打开工程做一些基础的配置,如调试器以及串口需要添加micro lib等,最后就使用keil或VSCode补充代码。


4.3.2 代码补充与完善

接着,使用keil或者VSCode打开进行代码补充,值得注意的是,笔者这里不赘述串口重定向printf的代码补充,如果确实不清楚怎么弄,可参见笔者之前的文章,如下:

STM32调试手段:重定向printf串口_stm32 printf重定向-CSDN博客https://blog.csdn.net/2301_79475128/article/details/145305160 由于图形化配置后生成的代码基本已经把需要的初始化配置都做完了,而本案例在寄存器方式实现的时候其实主要就是初始化配置相关内容,因此这里只需要在主程序调用一下即可。根据单通道使用HAL库实现案例的经验,这里我们需要在转换前手动进行AD校准,不让转换得到的数据并不准确。

main函数中while循环前补充的代码参考如下:

cpp 复制代码
  /* USER CODE BEGIN 2 */

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

  uint16_t data[2] = {0};
  HAL_ADCEx_Calibration_Start(&hadc1);

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

  /* USER CODE END 2 */

循环中补充的就是打印数据的代码,参考如下:

cpp 复制代码
// 打印转换值
printf("PC0 = %.2f v\tPC2 = %.2f v \n", data[0] * 3.3 / 4095, data[1] * 3.3 / 4095);
HAL_Delay(1000);

至此,HAL库方式就实现完成了。


4.4 效果测试

4.4.1 寄存器方式测试

如下图可看出,当我将PC2使用杜邦线连接到VCC时,考虑损耗后确实能够采集到大概3.3V左右的电压,且PC0仍旧可以正常采集可变电阻电压值。

当我们将PC2引脚浮空时,效果如下动画所示。

可见,当调节可变电阻改变采集电压时,PC2浮空的电压会随着PC0输入的电压值改变而发生变化,只不过不会达到3.3V或者降到0V。这是由于PC2与PC0挨得比较近,PC0浮空状态下极易受到附近引脚电平的影响,这也是我们一般不推荐将引脚置为浮空输入状态的原因之一。

4.4.2 HAL库方式测试

这是PC2引脚浮空时的测试如下:

也没啥毛病。


五、总结

本文介绍了基于STM32的ADC多通道电压采集实现方法。通过可变电阻和杜邦线连接两种方式,在PC0(通道10)和PC2(通道12)引脚输入模拟电压信号。重点分析了多通道采集时需要使用DMA传输的必要性,详细阐述了寄存器配置流程,包括ADC扫描模式、连续转换、DMA通道设置等关键步骤。


以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!

鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!

相关推荐
国科安芯2 小时前
高轨航天器抗辐照MCU选型约束分析
单片机·嵌入式硬件·性能优化·机器人·安全性测试
CS Beginner2 小时前
【单片机】嵌入式显示屏开发框架:QT、SDL、LVGL 深度解析
单片机·嵌入式硬件·qt
YouEmbedded2 小时前
解码从架构到嵌套向量中断控制器(NVIC)
stm32·软件架构·mcu中断·exti外设·启动文件分析
亿道电子Emdoor3 小时前
【Altium】原理图中网络标签作用范围的设置
单片机·嵌入式硬件
风行男孩3 小时前
stm32基础学习——串口(USART)的基本使用
stm32·嵌入式硬件·学习
点灯小铭3 小时前
基于单片机的多模式档位调节与过热保护风扇设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
星源~3 小时前
Zephyr - MCU 开发快速入门指南
单片机·嵌入式硬件·物联网·嵌入式开发·zephyr
星源~3 小时前
zephyr-开发环境配置疑难问题解决
单片机·嵌入式硬件·物联网·项目开发
BMS小旭4 小时前
CubeMx-DMA
单片机·学习·cubemx·dma