STM32CubeMX系列05——ADC(轮询、中断、DMA)

文章目录

  • 1. 所用硬件
  • 2. 生成工程
  • 2.1. 创建工程选择主控
  • 2.2. 系统配置
  • 2.3. 配置工程目录
  • 2.4. 配置用到的外设
  • 3. ADC配置(四选一)
  • 3.1. 单通道轮询
  • 3.2. 单通道中断
  • 3.3. 多通道轮询
  • 3.4. DMA模式
  • ====>>> 文章汇总(有代码汇总) <<<====

    1. 所用硬件

    正点原子Mini板,主控 STM32F103RCT6.

    用到的外设:

    1. 串口1(PA9、PA10)
    2. 任意几个GPIO口(这里用PA1、PA2、PA3,对应ADC通道1、2、3)。

    2. 生成工程

    2.1. 创建工程选择主控

    2.2. 系统配置

    配置时钟源

    配置debug模式(如果需要ST-Link下载及调试可以勾选)

    配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)

    注意最后的ADC时钟,时钟频率最大14MHZ,因此这里设置6分频,刚好小于14。

    2.3. 配置工程目录

    2.4. 配置用到的外设

    串口1配置(用于输出结果)

    3. ADC配置(四选一)

    有如下情况:

    1. 单通道轮询
    2. 单通道中断
    3. 多通道轮询
    4. DMA模式(单通道、多通道都能用)

    设置说明:

  • ADC_Settings:
  • Data Alignment:
  • Right alignment :转换结果数据右对齐,一般我们选择右对齐模式。
  • Left alignment 转换结果数据左对齐。
  • Scan Conversion Mode:
  • Disabled 禁止扫描模式。如果是单通道 AD 转换使用 DISABLE。
  • Enabled 开启扫描模式。如果是多通道 AD 转换使用 ENABLE。
  • Continuous Conversion Mode:
  • Disabled 单次转换。转换一次后停止需要手动控制才重新启动转换。
  • Enabled 自动连续转换。
  • DiscontinuousConvMode:
  • Disabled 禁止间断模式。这个在需要考虑功耗问题的产品中很有必要,也就是在某个事件触发下,开启转换。
  • Enabled 开启间断模式。
  • ADC_Regular_ConversionMode:
  • Enable Regular Conversions 是否使能规则转换。
  • Number Of Conversion ADC转换通道数目,有几个写几个就行。
  • External Trigger Conversion Source 外部触发选择。这个有多个选择,一般采用软件触发方式。
  • Rank:
  • Channel ADC 转换通道
  • Sampling Time 采样周期选择,采样周期越短,ADC 转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC 转换数据输出周期就越长同时数据精度越高。
  • ADC_Injected_ConversionMode:
    Enable Injected Conversions 是否使能注入转换。注入通道只有在规则通道存在时才会出现。
  • WatchDog:Enable Analog WatchDog Mode 是否使能模拟看门狗中断。当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。
  • 3.1. 单通道轮询

    第一步:配置ADC

    第二步:点击生成代码

    第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用

    // 需要调用stdio.h文件
    #include <stdio.h>
    //取消ARM的半主机工作模式
    #pragma import(__use_no_semihosting)//标准库需要的支持函数                 
    struct __FILE 
    { 
    	int handle; 
    }; 
    FILE __stdout;       
    void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
    { 
    	x = x;
    } 
    
    int fputc(int ch, FILE *f)
    {  
    	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
    	return ch;
    }
    

    第四步:编写 main.c 代码 其他的什么都不用改

      while (1)
      {
    		// 开启ADC
    		HAL_ADC_Start(&hadc1);
    		// 开始轮询转换
    		HAL_ADC_PollForConversion(&hadc1,100);
    		// 存储转换的值
    		float value = 0;
    		// 查询ADC状态
    		uint32_t state = HAL_ADC_GetState(&hadc1);
    		if (( state & HAL_ADC_STATE_REG_EOC) == HAL_ADC_STATE_REG_EOC)
    		{
    			// 获取ADC转换结果
    			value = HAL_ADC_GetValue(&hadc1);
    			printf("adc value:%f \r\n",value/4096.0*3.3);
    		}
    		else
    		{
    			printf("adc state %d \r\n",state);
    		}
    		// 关闭ADC
    		HAL_ADC_Stop(&hadc1);
    		HAL_Delay(1000);
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
    

    效果验证

    单通道轮询 在转换时会阻塞直到转换完成。

    3.2. 单通道中断

    第一步:配置上:在“单通道轮询”实现配置基础上再打开ADC全局中断。

    第二步:点击生成代码

    第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用

    // 需要调用stdio.h文件
    #include <stdio.h>
    //取消ARM的半主机工作模式
    #pragma import(__use_no_semihosting)//标准库需要的支持函数                 
    struct __FILE 
    { 
    	int handle; 
    }; 
    FILE __stdout;       
    void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
    { 
    	x = x;
    } 
    
    int fputc(int ch, FILE *f)
    {  
    	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
    	return ch;
    }
    

    第四步:编写 main.c 代码

    生成后查看代码,在stm32f1xx_it.c文件中有 ADC1通道2的中断函数ADC1_2_IRQHandler,这个中断函数又调用了HAL_ADC_IRQHandler(&hadc1);

    /**
      * @brief This function handles ADC1 and ADC2 global interrupts.
      */
    void ADC1_2_IRQHandler(void)
    {
      /* USER CODE BEGIN ADC1_2_IRQn 0 */
    
      /* USER CODE END ADC1_2_IRQn 0 */
      HAL_ADC_IRQHandler(&hadc1);
      /* USER CODE BEGIN ADC1_2_IRQn 1 */
    
      /* USER CODE END ADC1_2_IRQn 1 */
    }
    

    HAL_ADC_IRQHandler(&hadc1);函数在stm32f1xx_hal_adc.c中,这个函数考虑了很多情况,其中调用了HAL_ADC_ConvCpltCallback(hadc);,还是在同一个文件中,这是一个弱函数。根据翻译,很好理解,我们直接重新定义这个方法即可。

    /**
      * @brief  Conversion complete callback in non blocking mode 
      * @param  hadc: ADC handle
      * @retval None
      */
    __weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
    {
      /* Prevent unused argument(s) compilation warning */
      UNUSED(hadc);
      /* NOTE : This function should not be modified. When the callback is needed,
                function HAL_ADC_ConvCpltCallback must be implemented in the user file.
       */
    }
    

    main.c

    /* USER CODE BEGIN PFP */
    // 重定义ADC转换完成回调函数
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
    {
    	if(hadc == &hadc1)
    	{
    		uint16_t adc_value = HAL_ADC_GetValue(hadc);
    		printf("refresh adc value:%f \r\n", adc_value/4096.0*3.3);
    		// 重新开启ADC中断
    		HAL_ADC_Start_IT(&hadc1);
    	}
    }
    /* USER CODE END PFP */
    
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_ADC1_Init();
      MX_USART1_UART_Init();
      
      /* USER CODE BEGIN WHILE */
    	// 开启ADC中断
    	HAL_ADC_Start_IT(&hadc1);
      while (1)
      {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    

    效果和轮询的一样,不过这个会一直执行,而且很快

    实际上,这里设置的是单次转换,所以中断只会触发一次,需要再次使用HAL_ADC_Start_IT开启中断。如果需要实时的转换,可以将转换设为连续模式,这样的话ADC转换器便会实时的持续的进行转换,那将是非常消耗CPU的,以至于main将不能正常执行(采样时间太短的话)。

    开启中断后,一般需要实现HAL_ADC_ConvCpltCallback函数,在callback中GetValue,也可以在程序其他地方像轮询那样先判断ADC状态,再GetValue。

    3.3. 多通道轮询

    第一步:ADC配置
    多通道时扫描模式会自动打开。要开启“Discontinuous Conversion Mode”。

    第二步:点击生成代码

    第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用

    // 需要调用stdio.h文件
    #include <stdio.h>
    //取消ARM的半主机工作模式
    #pragma import(__use_no_semihosting)//标准库需要的支持函数                 
    struct __FILE 
    { 
    	int handle; 
    }; 
    FILE __stdout;       
    void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
    { 
    	x = x;
    } 
    
    int fputc(int ch, FILE *f)
    {  
    	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
    	return ch;
    }
    

    第四步:编写 main.c 代码

    HAL_ADCEx_Calibration_Start(&hadc1);
    const uint8_t kNbrOfPin = 3;
      while (1)
      {
    	for(int i = 0; i < kNbrOfPin; i++)
    	{
    		HAL_ADC_Start(&hadc1);
    		HAL_ADC_PollForConversion(&hadc1, 100);
    		float value = 0;
    		uint32_t state = HAL_ADC_GetState(&hadc1);
    		if (( state & HAL_ADC_STATE_REG_EOC) == HAL_ADC_STATE_REG_EOC)
    		{
    			value = HAL_ADC_GetValue(&hadc1);
    			printf("adc value [%d]:%f\r\n", i,value/4096.0*3.3);
    		}
    		else
    		{
    			printf("adc state[%d]:%d\r\n", i, state);
    		}
    	}
    	HAL_ADC_Stop(&hadc1);
    	HAL_Delay(200);
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
    

    效果验证

    3.4. DMA模式

    第一步:ADC配置


    第二步:点击生成代码

    第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用

    // 需要调用stdio.h文件
    #include <stdio.h>
    //取消ARM的半主机工作模式
    #pragma import(__use_no_semihosting)//标准库需要的支持函数                 
    struct __FILE 
    { 
    	int handle; 
    }; 
    FILE __stdout;       
    void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
    { 
    	x = x;
    } 
    
    int fputc(int ch, FILE *f)
    {  
    	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
    	return ch;
    }
    

    第四步:编写 main.c 代码

    /* USER CODE BEGIN PFP */
    void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
    {
    	if(hadc == &hadc1)
    	{
    		// 使用DMA其实也会运行到这里,也可以将结果在这里输出。
    		// 当然此函数也可以不写。
    	}
    }
    /* USER CODE END PFP */
    
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    	uint16_t adc_value[3] = {0};
      /* USER CODE END 1 */
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
      
      /* Configure the system clock */
      SystemClock_Config();
      
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_ADC1_Init();
      MX_USART1_UART_Init();
      
      /* USER CODE BEGIN WHILE */
    	HAL_ADCEx_Calibration_Start(&hadc1);
    	// enable DMA通道
    	// 参数:ADC1、目标缓冲区地址、从ADC外围设备传输到内存的数据长度
    	/*
    	 * 此处有个大坑,经过测试,DMA中断非常容易进(具体的不知道)
    	 *
    	 * 如果ADC采样周期短的话,一直在执行中断,
    	 * 导致无法执行主程序,因此会卡死在这个函数里面出不去。
    	 *
    	 * 因此,ADC的采用周期需要长一点。
    	 */
    	HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_value, 3);
    	
      while (1)
      {
    	printf("-------------------- \r\n");
    	printf("adc value[0]:%f \r\n", adc_value[0]/4096.0*3.3);
    	printf("adc value[1]:%f \r\n", adc_value[1]/4096.0*3.3);
    	printf("adc value[2]:%f \r\n", adc_value[2]/4096.0*3.3);
    	HAL_Delay(1000);
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    

    注意
    用到 DMA 的外设,MX_DMA_Init(); 一定要在外设初始化前面,比如这里的 MX_ADC1_Init(); 。

    效果验证

    多通道DMA和单通道DMA配置基本相同,只需注意存储AD转换结果的数组,如果有两个通道,数组长度为2,则每个通道的值分别对应数组的每一位;如果数组长度为2的整数倍,如10,则数组内[0] [2] [4] [6] [8]的值对应其中一个通道的AD值,即存储了连续采集5次的AD值,这样可以用多个AD值求平均值。

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32CubeMX系列05——ADC(轮询、中断、DMA)

    发表评论