STM32学习----ADC模数转换(轮询、中断、定时器、DMA)

什么是模拟数据

        模拟数据是指在一定时间范围内连续变化的信号,该信号的取值可以是任意实数值。例如,声音、温度、压力等物理量在不同时间内的变化可以表示为模拟数据。在数字化处理中,需要将模拟数据转换为数字信号,以便于进行数字化处理和存储。

什么是数字数据

        数字数据是指在一定时间范围内以离散形式表示的信号,该信号的取值只能是一组预定义的数字。例如,计算机中的数据、音频、视频等信号都是以数字形式表示的。在数字化处理中,模拟数据需要通过模数转换器(ADC)转换为数字数据。

什么是模数转换

        模数转换是将模拟信号转换成数字信号的过程。它的基本原理是利用采样定理,将连续时间的模拟信号离散化,转换成一系列离散时间的采样数据。这个过程包括两个步骤:采样和量化。

        采样是指在一定时间间隔内对模拟信号进行采样,将其转换为一系列数字信号。采样的时间间隔必须满足采样定理,以保证采样数据的完整性和准确性。

        量化是指将采样得到的连续模拟信号转换为一系列离散的数字信号。量化的过程中,将模拟信号的幅度划分为若干个等间隔的电平,然后将每个电平对应的数字编码输出,即将模拟信号转换为数字信号。

STM32系列单片机模数转换原理与过程

  1. 采样:ADC的输入是模拟电压信号,为了将其转换为数字信号,需要先将模拟信号进行采样。在STM32F103单片机中,采样由外部模拟信号触发源触发,也可以由软件触发。

  2. 保持:采样后,需要将采样值保持在采样保持电容中,以便于后续的转换。在STM32F103单片机中,采样保持电容由程序配置。

  3. 转换:采样保持电容中的电压值被转换成数字量,同时进行12位的精度转换。在STM32F103单片机中,ADC可以进行单通道、扫描等模式的转换。

  4. 输出:转换完成后,数字量被存储在ADC数据寄存器中,可以由程序读取并进行后续处理

  1. 采样阶段:在ADC转换开始前,采样电路会将模拟信号进行采样,将其转化为数字信号,该阶段持续时间一般为12个时钟周期。

  2. 保持阶段:采样完成后,电荷保持电路会将采样的电荷保持在保持电容中,以保证转换过程中模拟信号的稳定性和准确性。该阶段持续时间一般为1.5个时钟周期。

  3. 转换阶段:保持电容中的电荷会被转化为数字信号,该阶段持续时间一般为12个时钟周期。

  4. 输出阶段:转换完成后,数字信号会被送至ADC数据寄存器中,等待CPU读取。

ADC的采样、保持和转换的时序是由ADC控制器内部的时序控制电路来控制的,这些时序参数可以通过相应的寄存器进行配置。

不同模式的ADC

1、软件触发、轮询转换状态:

这也是最简单的一种ADC工作方式,主要就靠三个函数来完成

HAL_ADC_Start(&hadc1); // 启动ADC转换,假设使用的是ADC1
HAL_ADC_PollForConversion(&hadc1, 100); // 等待转换完成
uint32_t value = HAL_ADC_GetValue(&hadc1); // 获取ADC转换的值

使用STM32CubeMX软件来配置

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第1张图片

自动生成的ADC配置代码

ADC_HandleTypeDef hadc1;

/* ADC1 init function */
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; //通道10
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_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)
  {
    /* ADC1 clock enable */
    __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)
  {
    /* Peripheral clock disable */
    __HAL_RCC_ADC1_CLK_DISABLE();

    HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);

  }
}

int main(void)
{
  uint32_t value = 0; //定义一个value变量来接收AD转换的数据

  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();

  HAL_ADC_Start(&hadc1);//1、开启采集与转换

  while (1)
  {

    HAL_ADC_PollForConversion(&hadc1, 100); ///2、等待转换完成,超时时间100ms,
    value = HAL_ADC_GetValue(&hadc1); ///3、获取转换结果,把结果赋值给value变量
    
    //因为硬件电路就是一个采集电压的电路,电压与ADC的值转换公式value*3.3/4096
    printf("adc_value = %.2f V:\n\r", value*3.3/4096);

    HAL_Delay(1000); //一般1us左右转换就完事了,不加个延时函数,打印数据就太多了

  }
}

2、软件触发,中断转换

        轮询的工作方式,不管是通讯,还是ADC,都是不太好的,在单片机学习的过程中,轮询的工作方式只是方面理解原理,在实际的应用中最好还是用上中断或者DMA。

        前面轮询的工作方式是:

        1、开启采集与转换;

        2、等待转换完成;

        3、读取数据;

在等待转换完成的时候就是一直在那死等,程序控制上不应该这样,用中断,就是ADC转换完成之后,直接跳转到中断服务函数,你可以在中断服务函数中设置一个flag,或者直接在中断服务函数中就把数据给处理了(在数据量不大的情况下)。配置的过程也仅是增加了一个中断,其他的设置与轮询方式一样。

主要参与的函数

HAL_ADC_Start_IT(&hadc1);//开启ADC中断,ADC转换完成就到中断服务函数

//中断服务函数
void ADC1_2_IRQHandler(void)
{
  HAL_ADC_IRQHandler(&hadc1);
}

//中断回调函数,被上面的中断服务函数所引用,在中断回调函数中就把转换完成标志置位
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  conv_flag = 1;
}

//获取结果还是要靠这个函数
HAL_ADC_GetValue(&hadc1);

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第2张图片

 配置中断,优先级就默认 0,0,实际综合性的工程中还是要根据项目实际来配置优先级的。

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第3张图片

自动生成的ADC配置代码

#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_1CYCLE_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);

    /* ADC1 interrupt Init */
    HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_2_IRQn); //使能中断,其他的与轮询没什么区别
  }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{
  if(adcHandle->Instance==ADC1)
  {
    /* Peripheral clock disable */
    __HAL_RCC_ADC1_CLK_DISABLE();
    HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);
    HAL_NVIC_DisableIRQ(ADC1_2_IRQn);
  }
}

 因为使用中断的方式进行ADC工作,转换完成之后就进入中断服务函数

//这个函数是自动生成的,只要ADC有中断就到这里了
void ADC1_2_IRQHandler(void)
{
  HAL_ADC_IRQHandler(&hadc1);
}

//回调函数被上面的中断服务函数所调用,需要自己去写内容;
//可以在回调函数中直接取数据,或者处理数据(可以,单不推荐,中断服务函数要少多点事)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  conv_flag = 1; //转换完成标志置1
  //value = HAL_ADC_GetValue(&hadc1); // 获取转换结果,可以在回调函数中去数据

}

在main函数中取数据,处理数据(就是把数据打印一下,有其他的用途的就自己写代码去)

int main(void)
{
  uint32_t value = 0; //定义一个变量来接收ADC的数据

  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();

  HAL_ADC_Start_IT(&hadc1); //开启ADC采集与转换,中断的方式,转换完成就进入中断服务函数

  while (1)
  {
    if(conv_flag == 1) //转换完成标志为1,就来取数据
    {
      conv_flag = 0; //把标志位清零
      value = HAL_ADC_GetValue(&hadc1); // 获取转换结果
    }
    
    printf("adc_value = %.2f V:\n\r", value*3.3/4096);//打印数据
    HAL_Delay(1000); //加个延时就是怕打印数据太频繁,控制工程中不要随便加延时函数,低效  
  }

}

3、定时器触发,ADC中断转换

STM32单片机的ADC采样支持定时器触发,就是你可以以固定的时间间隔来获得ADC的值,比如每5秒获取一下ADC的值。

ADC的配置如下

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第4张图片

 既然使用定时器来触发ADC采样,那么就需要设置一下定时器,你使用来了那个定时器,就去设置哪个定时器,本例程使用的是定时器3

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第5张图片

使用CubeMX自动生成的代码

#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 = DISABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; //定时器3触发输出时间
  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_1CYCLE_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);

    HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_2_IRQn);
  }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{

  if(adcHandle->Instance==ADC1)
  {
    __HAL_RCC_ADC1_CLK_DISABLE();

    HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);
    HAL_NVIC_DisableIRQ(ADC1_2_IRQn);
  }
}

定时器部分的代码

#include "tim.h"
TIM_HandleTypeDef htim3;

void MX_TIM3_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 7200;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 50000;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }

}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{

  if(tim_baseHandle->Instance==TIM3)
  {
    __HAL_RCC_TIM3_CLK_ENABLE();
  }
}

void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{

  if(tim_baseHandle->Instance==TIM3)
  {
    __HAL_RCC_TIM3_CLK_DISABLE();
  }
}

在main函数中开启定时器和ADC中断,就可以按照设置的时间间隔获取ADC的值,ADC的数据在中断回调函数中去获取。

int main(void)
{
  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();
  MX_TIM3_Init();

  HAL_TIM_Base_Start(&htim3); //开启定时器3
  HAL_ADC_Start_IT(&hadc1);//开启ADC中断

  while (1)
  {
  }

}

在中断回调函数中获取ADC的值

void ADC1_2_IRQHandler(void)
{
  HAL_ADC_IRQHandler(&hadc1);
}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  uint32_t value = 0;
  value = HAL_ADC_GetValue(&hadc1); // 获取转换结果
  printf("adc_value = %.2f V:\n\r", value*3.3/4096);

}

4、DMA模式

使用ADC的DMA模式,就是让ADC一直在哪里采样转换,转换完成之后就把ADC的数据通过DMA直接送到目标位置,目标位置一般是一个数组

ADC的设置如下

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第6张图片

 DMA的设置如下

STM32学习----ADC模数转换(轮询、中断、定时器、DMA)_第7张图片

 对应的配置代码

#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_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_1CYCLE_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);

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

    __HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);

    HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_2_IRQn);

  }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{

  if(adcHandle->Instance==ADC1)
  {
    __HAL_RCC_ADC1_CLK_DISABLE();
    HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);
    HAL_DMA_DeInit(adcHandle->DMA_Handle);
    HAL_NVIC_DisableIRQ(ADC1_2_IRQn);
  }
}

在main文件中添加部分代码

uint16_t adc_data[100]; //定义一个数组来接收DMA传输的数据


void SystemClock_Config(void);

int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  MX_USART1_UART_Init();

  HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_data,100);//把数据传导adc_data,传输100个数据

  while (1)
  {

  }
  /* USER CODE END 3 */
}

多通道的ADC数据传输后续添加,主要是硬件开发板上只有一个通道。

你可能感兴趣的:(STM32学习,学习,stm32,单片机,嵌入式硬件)