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

DMA+AD多通道

基本原理

image
左边是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的结合最为常见。

接线图

image

代码实现

我最终采用的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 */

实现效果

image

posted @ 2025-08-18 21:04  秦瑞迁  阅读(103)  评论(0)    收藏  举报