天天看点

STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结

STM32 CubeMX学习:7. ADC模数转化

系列文章目录

  1. 前期的准备
  2. 点亮 LED
  3. 闪烁 LED
  4. 定时器闪烁LED
  5. PWM控制LED亮度
  6. 常见的PWM设备
  7. 按键的外部中断
  8. ADC模数转换
  9. 串口收发
  10. 串口打印遥控器数据
  11. 未完待续…

文章目录

  • STM32 CubeMX学习:7. ADC模数转化
  • 0 前言
  • 1. 基础知识学习
    • 1.1 ADC原理介绍
    • 1.2 STM32F4x ADC介绍
  • 2. 程序学习
    • 2.1 ADC在CubeMX的配置
    • 2.2 内部VREFINT电压的使用
    • 2.3 相关函数介绍
    • 2.4 程序执行流程
  • 3. 进阶学习
  • 总结

0 前言

我们已经知道,在单片机中传输的信号均为数字信号,通过离散的高低电平表示数字逻辑的1和0,但是在现实的物理世界中,只存在模拟信号,即连续变化的信号,将这些连续变化的信号——比如热,光,声音,速度通过各种传感器转化成连续的电信号,再通过ADC功能将连续的模拟信号转化成离散的数字信号给单片机进行处理。

1. 基础知识学习

1.1 ADC原理介绍

一般ADC的工作流程为采样,比较,转换。

  • 采样:是指对某一时刻的模拟电压进行采集,
  • 比较:是指将采样的电压在比较电路中进行比较,
  • 转换:是指将比较电路中结果转换成数字量。

    在我们这里,使用的STM32f4采用12位逐次逼近型ADC(SAR-ADC)。现在,以下图为例,介绍3位ADC的比较过程:

    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结

    不同的位数分别赋予1/2,1/4,1/8的权值,模拟信号的采样值为Vin,

    1)与1/2Vref进行比较,Vin大于1/2Vref,则将第一位标记为1,

    2)与3/4Vref进行比较,Vin小于3/4Vref,则将第二位标记为0,

    3)与5/8Vref进行比较,Vin小于5/8Vref,则将第三位标记为0。

    图中的Vin通过这个三位的ADC后输出的结果为100。转换的结果为1/2Vref,通过这样逐次比较过程,将采样取得的模拟电压和内部参考电压Vref的加权值进行比较,不同的位数赋予不同的权值。

    但是,如果我们输入为一个未知的电压,完整的比较流程图如下:

    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结

    STM32支持最高12位ADC,一般ADC的位数越多则转换精度越高,但与此同时转换的速度也会变慢。

    此外,STM32内部有一个校准电压VREFINT,电压为1.2V,当供电电压不为3.3V,可以使用内部的vrefint通道采集1.2V电压作为Vref,以提高精度。

1.2 STM32F4x ADC介绍

  1. STM32F4x ADC特点
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
  2. STM32F40x系列ADC外部通道和引脚对应关系
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结

2. 程序学习

2.1 ADC在CubeMX的配置

这次我们使用引脚PF10作为电源ADC引脚,使用ADC3的通道8,STM32内部的1.2V校准电压Vrefint在ADC1中。所以我们需要按如下方式在CubeMX里进行配置。

  1. 开启ADC1和ADC3分别用于内部1.2V的Vrefint通道读取和电池电压ADC3的通道8读取。
  2. 在cubeMX中开启ADC1,在设置中将Vrefint Channel勾选,用于读取内部参考电压。ADC在cubeMX中的设置如图采样频率设置为PCLK2/4,采样位数为12位,数据设置为右对齐,其余均保持默认。其中Vrefine在stm32内部完成,没有对应引脚。
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
    最终ADC配置如图所示。
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
    《补充》:但是,这里还是把其中参数的具体含义给大家科普一下吧
名称 功能
Clock Prescaler 设置采样时钟频率
Resolution 设置采样精度
Data Alignment 设置数据对齐方式
Scan Conversion Mode 扫描转换模式开启/关闭
Continuous Conversion Mode 连续转换模式开启/关闭
Discontinuous Conversion Mode 非连续转换模式开启/关闭
DMA Continuous Requests DMA连续启动开启/关闭
End of Conversion Selection 每个通道转换结束后发送EOC标志/所有通道转换结束后发送EOC标志
  1. 在CubeMX中开启ADC3,并打开其IN8用于电池电压的读取,其设置和ADC1一致。可以看到引脚图像中ADC3对应的PF10变绿。
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
  2. 这里我们需要把PC0,PC1,PC2引脚设置为输入模式,且这三个引脚拉为高电平,如图所示
    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
  3. 点击GENERATE CODE生成代码

2.2 内部VREFINT电压的使用

VREFINT 即 ADC的内部参照电压1.2V。通过将采样内部参照电压1.2V的 ADC 值和 Vref 的加权值进行比较,进而得到 ADC 的输出值。一般来说STM32的 ADC 采用 Vcc 作为 Vref ,但为了防止 Vcc 存在波动较大导致Vref不稳定,进而导致采样值的比较结果不准确,STM32可以通过内部已有的参照电压VREFINT来进行校准,接着以VREFINT为参照来比较ADC的采样值,从而获得比较高的精度,VREFINT的电压为1.2V。

通过一个函数对1.2V的电压进行多次采样,并计算其平均值,接着将其与ADC采出的数据值做对比,得到单位数字电压对应的模拟电压值voltage_vrefint_proportion,其计算公式如下,设采样得到的数字值为average_adc:

a v e r a g e _ a d c = t o t a l _ d a c 200 average\_adc=\frac{total\_dac}{200} average_adc=200total_dac​

v o l t a g e _ v r e f i n t _ p r o p o r t i o n = 1.2 v a v e r a g e _ a d c = 200 ∗ 1.2 t o t a l _ a d c voltage\_vrefint\_proportion=\frac{1.2v}{average\_adc}=\frac{200 ∗ 1.2}{total\_adc} voltage_vrefint_proportion=average_adc1.2v​=total_adc200∗1.2​

其代码如下所示

void init_vrefint_reciprocal(void) 
{ 
	uint8_t i = 0; 
	uint32_t total_adc = 0; 
	for(i = 0; i < 200; i++)
	 {
	 	 total_adc += adcx_get_chx_value(&hadc1, ADC_CHANNEL_VREFINT);
	  } 
	  voltage_vrefint_proportion = 200 * 1.2f / total_adc;
}
           

2.3 相关函数介绍

Hal库提供了以下很多有关ADC的函数

  1. ADC通道设置函数
函数作用 设置ADC通道的各个属性值,包括转换通道,序列排序,采样时间等
返回值 HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK
参数1 TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2
参数2 ADC_ChannelConfTypeDef* sConfig即指向ADC设置的结构体指针。我们先对sConfig结构体进行赋值,然后再将其指针作为参数输入函数
  1. ADC开启采样函数
函数作用 开启ADC的采样
返回值 HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK
参数 TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2
  1. 等待ADC转换结束函数
函数作用 等待ADC转换结束
返回值 HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK
参数1 TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2
参数2 HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK
  1. 获取ADC函数
函数作用 获取ADC值
返回值 HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK
参数 TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2

2.4 程序执行流程

本程序中,首先对内部参考电压电压VREFINT进行adc采样将其作为校准值,在init_vrefint_reciprocal中,对VREFINT电压进行200次的采样,接着求其均值后使用VREFINT的电压值1.2V去除以该ADC采样得到的均值,算出voltage_vrefint_proportion,后续ADC中采样到的电压值与voltage_vrefint_proportion相乘就可以计算出以内部参考电压做过校准的ADC值了。

void init_vrefint_reciprocal(void) 
{ 
	uint8_t i = 0; uint32_t total_adc = 0; 
	
	for(i = 0; i < 200; i++) 
	{ 
		total_adc += adcx_get_chx_value(&hadc1, ADC_CHANNEL_VREFINT); 
	} 
	
	voltage_vrefint_proportion = 200 * 1.2f / total_adc; 
}
           

接着通过ADC对经过了分压电路的电池电压值进行采样,将该采样结果与voltage_vrefint_proportion相乘,就得到了取值范围在0-3.3V之间的ADC采样值,由于这个采样值是分压后的结果,需要反向计算出电压的值。分压的电阻值为200KΩ和22KΩ,由于 (22K Ω + 200K Ω) / 22K Ω = 10.09,乘以这个值之后就可以得到电池的电压值。

(注意,这里我们是根据一个具体的分压电路给出的计算公式,当我们实际使用时,要根据自己所使用的具体电路修改对应的计算方法,)

fp32 get_battery_voltage(void) 
{ 
	fp32 voltage; 
	uint16_t adcx = 0; 
	adcx = adcx_get_chx_value(&hadc3, ADC_CHANNEL_8);
	
	//(22K Ω + 200K Ω) / 22K Ω = 10.090909090909090909090909090909 
	voltage = (fp32)adcx * voltage_vrefint_proportion * 10.090909090909090909090909090909f; 
	return voltage; 
}
           

其实,我们还可以通过ADC获得板载的温度传感器的温度值,同样是先经过ADC值进行采样,采样结束后,将ADC采样结果adc带入公式temperate = (adc - 0.76f) * 400.0f + 25.0f,从而计算出温度值。

fp32 get_temprate(void) 
{ 
	uint16_t adcx = 0; 
	fp32 temperate; 
	
	adcx = adcx_get_chx_value(&hadc1, ADC_CHANNEL_TEMPSENSOR);
	
	temperate = (fp32)adcx * voltage_vrefint_proportion;
	temperate = (temperate - 0.76f) * 400.0f + 25.0f;
	 
	return temperate; 
}
           

在我们完成代码时,还需要像以前一样自己新建一个Boards目录,在里面新建一个bsp_adc.c文件

#include "bsp_adc.h"
#include "main.h"
extern ADC_HandleTypeDef hadc1;
extern ADC_HandleTypeDef hadc3;


volatile fp32 voltage_vrefint_proportion = 8.0586080586080586080586080586081e-4f;

static uint16_t adcx_get_chx_value(ADC_HandleTypeDef *ADCx, uint32_t ch)
{
    static ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ch;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;//ADC_SAMPLETIME_3CYCLES;

    if (HAL_ADC_ConfigChannel(ADCx, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }

    HAL_ADC_Start(ADCx);

    HAL_ADC_PollForConversion(ADCx, 10);
    return (uint16_t)HAL_ADC_GetValue(ADCx);

}
void init_vrefint_reciprocal(void)
{
    uint8_t i = 0;
    uint32_t total_adc = 0;
    for(i = 0; i < 200; i++)
    {
        total_adc += adcx_get_chx_value(&hadc1, ADC_CHANNEL_VREFINT);
    }

    voltage_vrefint_proportion = 200 * 1.2f / total_adc;
}
fp32 get_temprate(void)
{
    uint16_t adcx = 0;
    fp32 temperate;

    adcx = adcx_get_chx_value(&hadc1, ADC_CHANNEL_TEMPSENSOR);
    temperate = (fp32)adcx * voltage_vrefint_proportion;
    temperate = (temperate - 0.76f) * 400.0f + 25.0f;

    return temperate;
}


fp32 get_battery_voltage(void)
{
    fp32 voltage;
    uint16_t adcx = 0;

    adcx = adcx_get_chx_value(&hadc3, ADC_CHANNEL_8);
    //(22K Ω + 200K Ω)  / 22K Ω = 10.090909090909090909090909090909
    voltage =  (fp32)adcx * voltage_vrefint_proportion * 10.090909090909090909090909090909f;

    return voltage;
}

uint8_t get_hardware_version(void)
{
    uint8_t hardware_version;
    hardware_version = HAL_GPIO_ReadPin(HW0_GPIO_Port, HW0_Pin)
                                | (HAL_GPIO_ReadPin(HW1_GPIO_Port, HW1_Pin)<<1)
                                | (HAL_GPIO_ReadPin(HW2_GPIO_Port, HW2_Pin)<<2);



    return hardware_version;
}


           

接着新建一个对应的bsp_adc.h文件放置于对应目录之下

#ifndef BSP_ADC_H
#define BSP_ADC_H
#include "#ifndef BSP_ADC_H
#define BSP_ADC_H
#include "struct_typedef.h"

extern void init_vrefint_reciprocal(void);
extern fp32 get_temprate(void);
extern fp32 get_battery_voltage(void);
extern uint8_t get_hardware_version(void);
#endif
"

extern void init_vrefint_reciprocal(void);
extern fp32 get_temprate(void);
extern fp32 get_battery_voltage(void);
extern uint8_t get_hardware_version(void);
#endif

           

在这个文件中,我们还用到了一个定义结构体的头文件:struct_typedef.h

#ifndef STRUCT_TYPEDEF_H
#define STRUCT_TYPEDEF_H


typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int64_t;

/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef unsigned char bool_t;
typedef float fp32;
typedef double fp64;


#endif




           

最终的main.c文件如下所示

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "bsp_adc.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 ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
fp32 voltage;
fp32 temperature;
uint8_t handware_version;
/* 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_ADC1_Init();
  MX_ADC3_Init();
  /* USER CODE BEGIN 2 */
	
	//use vrefint voltage to calibrate
  //使用基准电压来校准
  init_vrefint_reciprocal();
		
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		
				//get battery voltage
        //获取电源电压
        voltage = get_battery_voltage();
        //get chip temperate
        //获取片内温度
        temperature = get_temprate();
        //get handware version
        //获取硬件版本
        handware_version = get_hardware_version();
        HAL_Delay(100);
		
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage 
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
  /** Initializes the CPU, AHB and APB busses clocks 
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 6;
  RCC_OscInitStruct.PLL.PLLN = 168;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 4;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB busses 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_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/* 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 */

  /* 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,
     tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

           

3. 进阶学习

逐次型ADC是通过一位位进行比较得出转换值,其原理图如下所示。

STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结

如图所示,我们发现整个电路由比较器、D/A转换器、缓冲寄存器和若干控制逻辑电路构成。作用如下所示:

  • 比较器:用于输入电压值D/A转换器输出电压进行比较,当输入电压大于D/A转换器电压时,输出为1,反之输出为0;
  • D/A转换器:ADC的逆向过程,将缓冲寄存器的记录数字量转换成模拟量。
  • 缓冲寄存器:记录当前转换的数字量。

整个过程如下:

  1. 将缓冲寄存器清零;
  2. 将逐次逼近寄存器 最高位 置1;
  3. 把数字值送入D/A转换器,经D/A转换后的模拟量送入比较器,称为 Vo;
  4. Vo与比较器的待转换的模拟量Vi 比较,若Vo<Vi,该位被保留,否则被清0。
  5. 再置 寄存器 次高位为1,将寄存器中新的数字量送D/A转换器,
  6. 输出的 Vo再与Vi比较,若Vo<Vi,该位被保留,否则被清0。
  7. 循环此过程,直到寄存器最低位,得到数字量的输出。

    在这个过程中,D/A转换器使用的电压为STM32的电源电压Vref+和Vref-,如图所示:

    STM32 CubeMX学习:7. ADC模数转化STM32 CubeMX学习:7. ADC模数转化0 前言1. 基础知识学习2. 程序学习3. 进阶学习总结
    STM32的标准电压为 3.3V。例如第一次转换时,输出为 1/2Vre电压,即1.65V。但由于外部供电电压不一定为 3.3V。故而为了在这种情况提高ADC精度,STM32内部有VERFINT1.2V稳定电压,可以使用ADC采样该电压来提高ADC的精度。

代码我已经放到了我的GitHub仓库,如有需要可以下载使用:

CubeMX学习

总结

这次的博客,我们重点学习了ADC的相关知识,ADC是模拟量变成数字量的过程,单片机获取电压,电流传感器等模拟量的方法。通过ADC功能,我们能够获取各种传感器的模拟值,并将其转化为单片机可以处理的数字量,也可以获取电池的电压值,并将其通过电量形式显示出来。