【自学嵌入式:stm32单片机】DMA+AD多通道
DMA+AD多通道
基本原理

左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面,那我们要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了,所以在这里,DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址,存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址,之后数据宽度,因为ADCDR和SRAM数组,我们要的都是uint16_t的数据,所以数据宽度都是16位的半字传输,然后是地址是否自增,那从这个图里,显然是外设地址不自增,存储器地址自增,传输方向,是外设站点到存储器站点,传输计数器,这里通道有7个,所以计数7次,计数器是否自动重装,这里可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作,最后是触发选择,这里ADCDR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发,ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以我们程序不太好判断,某一个通道转换完成的时机是什么时候,虽然单个通道转换完成后,不产生任何标志位和中断,但是它立该会产生DMA请求,去触发DMA转运,一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使ADC和DMA成为了最常见的伙伴,像其他的一些外设,使用DMA可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多是损失一些性能,但是这个ADC的扫描模式,如果不使用DMA,功能都会收到很大的限制,所以ADC和DMA的结合最为常见。
接线图

代码实现
我最终采用的ADC循环扫描+DMA循环转运的模式
标准库实现
已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32
AD.h
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
//void AD_GetValue(void);
#endif
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
//初始化AD
void AD_Init(void)
{
//DMA是AHB的设备
//RCC_AHBPeriph_DMA1对应stm32f103系列
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
//基本步骤
//开启RCC时钟,包括ADC和GPIO的时钟
//ADCCLK的分频器也需要配置一下
//配置GPIO,把需要用的GPIO配置成模拟输入模式
//配置多路开关,把左边的通道接入到右边的规则列表里
//配置ADC转换器
//需要看门狗就配置,需要中断就在中断输出控制里用ITConfig函数开启对应的中断输出
//然后再在NVIC里,配置一下优先级,这样就能触发中断
//开关控制,调用ADC_Cmd函数,开启ADC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟,ADC都是APB2上的设备
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟,准备开启PA0口
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择6分频,上一篇文章说,只能开启6和8分频,最大14MHz
//配置GPIO,让PA0口变为模拟输入的引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; //PA0,1,2,3
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//4个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
//结构体初始化ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式,不是双ADC
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发源选择,不使用外部触发,也就是内部软件触发的意思
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续模式
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数目
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式
ADC_Init(ADC1, &ADC_InitStructure);
DMA_InitTypeDef DMA_InitStructure; //DMA结构体
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(ADC1->DR)); //外设站点起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设站点数据宽度,传输的是16位半字
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设站点是否自增,不自增,始终转运同一个位置的数据
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器站点起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器站点数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器站点是否自增,存储器的地址是自增的
//DMA_DIR_PeripheralDST外设站点作为目的地
//DMA_DIR_PeripheralSRC外设站点作为源端
//把DataA放到外设站点,DataB放到存储器站点
//传输方向就是外设站点到存储器站点
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,指定外设站点是源端还是目的地
DMA_InitStructure.DMA_BufferSize = 4; //缓存区大小,其实就是传输计数器,4个ADC通道
//自动重装不能应用在存储器到存储器的情况下
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //传输模式,是否使用自动重装,是
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,只有一个DMA通道,选个中等由新阿基
//DMA1_Channel1 - DMA1通道1(ADC的硬件触发只接在了DMA的通道1)
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
//直接使能,还差ADC信号传过来
DMA_Cmd(DMA1_Channel1, ENABLE);
//开启ADC1的DMA
ADC_DMACmd(ADC1, ENABLE);
//开启ADC电源
ADC_Cmd(ADC1, ENABLE);
//校准ADC
ADC_ResetCalibration(ADC1); //复位
while(ADC_GetResetCalibrationStatus(ADC1) == SET); //返回复位校准的状态,等待复位完成,还需要加一个while循环
//ADC_GetCal...获取的是CR2寄存器的RSTCAL标志位,该位由软件设置并由硬件清除
//在校准寄存器被初始化后,该位将被清除,该位被软件置1,开始复位校准,完成后,该位变0
ADC_StartCalibration(ADC1); //开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); //获取校准状态
//最后ADC触发放在初始化的最后一行
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//当ADC触发之后,ADC连续转换,DMA循环转运
//两者一直在工作,始终把最新的转换结果,刷新到SRAM数组中
//当我们想要数据的时候,随时去数组里取就行了
//GetValue函数完全就不需要了
}
//返回AD转换的值,指定对应通道
//void AD_GetValue(void)
//{
// //因为DMA也是正常的单次模式,所以在触发ADC之前,需要再重新写入一下传输计数器
// DMA_Cmd(DMA1_Channel1, DISABLE);
// DMA_SetCurrDataCounter(DMA1_Channel1, 4);
// DMA_Cmd(DMA1_Channel1, ENABLE);
// //ADC还是单次模式,还需要软件触发一下ADC开始,其他则不需要了
// ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //没完成就一直循环等待
// DMA_ClearFlag(DMA1_FLAG_TC1); //清除标志位
//}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "MYOLED.h"
#include "AD.h"
int main(void)
{
MYOLED_Init();
AD_Init();
MYOLED_ShowString(0,0,"AD0:");
MYOLED_ShowString(0,1,"AD1:");
MYOLED_ShowString(0,2,"AD2:");
MYOLED_ShowString(0,3,"AD3:");
while(1)
{
//单次转换非扫描模式
// AD_GetValue();
MYOLED_ShowNum(4,0,AD_Value[0],4);
MYOLED_ShowNum(4,1,AD_Value[1],4);
MYOLED_ShowNum(4,2,AD_Value[2],4);
MYOLED_ShowNum(4,3,AD_Value[3],4);
Delay_ms(100);
}
}
HAL库实现
已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32-hal
具体IDE设置请参考开源的ioc配置文件
AD.h
/*
* AD.h
*
* Created on: Aug 14, 2025
* Author: Administrator
*/
#ifndef HARDWARE_AD_H_
#define HARDWARE_AD_H_
typedef struct AD
{
ADC_HandleTypeDef hadc; //当前用的是哪个ADC转换器
DMA_HandleTypeDef dhtd; //启动哪个DMA及其通道
uint16_t AD_Value[4]; //读取模拟量的数组
}AD;
void AD_Init(AD* ad, ADC_HandleTypeDef hadc, DMA_HandleTypeDef dhtd);
//uint16_t AD_GetValue(AD* ad, uint32_t ADC_Channel);
#endif /* HARDWARE_AD_H_ */
AD.c
/*
* AD.c
*
* Created on: Aug 14, 2025
* Author: Administrator
*/
#include "stm32f1xx_hal.h"
#include "AD.h"
//初始化AD
void AD_Init(AD* ad, ADC_HandleTypeDef hadc, DMA_HandleTypeDef dhtd)
{
ad->hadc = hadc;
ad->dhtd = dhtd;
//在 HAL 库中,ADC 的校准操作被封装成了一个专门的函数,
//无需像标准库那样分步调用复位校准、等待复位完成、
//开始校准、等待校准完成等多个步骤。
if (HAL_ADCEx_Calibration_Start(&(ad->hadc)) != HAL_OK)
{
// 校准失败处理,没有处理,不写
}
ad->AD_Value[0] = 0;ad->AD_Value[1] = 0;ad->AD_Value[2] = 0;
ad->AD_Value[3] = 0;
HAL_ADC_Start_DMA(&(ad->hadc), (uint32_t*)(ad->AD_Value), 4);
}
////获取AD的值
//uint16_t AD_GetValue(AD* ad, uint32_t ADC_Channel)
//{
// ADC_ChannelConfTypeDef sConfig = {0};
// // 配置当前需要转换的通道
// sConfig.Channel = ADC_Channel; // 指定通道(如ADC_CHANNEL_0~3)
// sConfig.Rank = ADC_REGULAR_RANK_1; // 规则组第1位
// sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; // 采样时间55.5周期
// HAL_ADC_ConfigChannel(&(ad->hadc), &sConfig); //设置通道等
// HAL_ADC_Start(&ad->hadc); //软件开启ADC
// HAL_ADC_PollForConversion(&(ad->hadc), HAL_MAX_DELAY); //等待转换
// uint16_t value = (uint16_t)HAL_ADC_GetValue(&(ad->hadc)); // 获取转换结果(自动清除EOC标志位)
// return value;
//}
main.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "AD.h"
#include "MYOLED.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
MYOLED_Init();
AD ad;
AD_Init(&ad, hadc1, hdma_adc1);
MYOLED_ShowString(0,0,"AD0:");
MYOLED_ShowString(0,1,"AD1:");
MYOLED_ShowString(0,2,"AD2:");
MYOLED_ShowString(0,3,"AD3:");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
MYOLED_ShowNum(4,0,ad.AD_Value[0],4);
MYOLED_ShowNum(4,1,ad.AD_Value[1],4);
MYOLED_ShowNum(4,2,ad.AD_Value[2],4);
MYOLED_ShowNum(4,3,ad.AD_Value[3],4);
HAL_Delay(100);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
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;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
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;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief ADC1 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC1_Init(void)
{
/* USER CODE BEGIN ADC1_Init 0 */
/* USER CODE END ADC1_Init 0 */
ADC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN ADC1_Init 1 */
/* USER CODE END ADC1_Init 1 */
/** Common config
*/
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 = 4;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = ADC_REGULAR_RANK_3;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = ADC_REGULAR_RANK_4;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC1_Init 2 */
/* USER CODE END ADC1_Init 2 */
}
/**
* Enable DMA controller clock
*/
static void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA interrupt init */
/* DMA1_Channel1_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_SET);
/*Configure GPIO pins : PB8 PB9 */
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
实现效果

浙公网安备 33010602011771号