STM32 CubeMX学习:7. ADC模数转化
系列文章目录
- 前期的准备
- 点亮 LED
- 闪烁 LED
- 定时器闪烁LED
- PWM控制LED亮度
- 常见的PWM设备
- 按键的外部中断
- ADC模数转换
- 串口收发
- 串口打印遥控器数据
- 未完待续…
文章目录
- 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的比较过程:
不同的位数分别赋予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支持最高12位ADC,一般ADC的位数越多则转换精度越高,但与此同时转换的速度也会变慢。
此外,STM32内部有一个校准电压VREFINT,电压为1.2V,当供电电压不为3.3V,可以使用内部的vrefint通道采集1.2V电压作为Vref,以提高精度。
1.2 STM32F4x ADC介绍
- STM32F4x ADC特点
- STM32F40x系列ADC外部通道和引脚对应关系
2. 程序学习
2.1 ADC在CubeMX的配置
这次我们使用引脚PF10作为电源ADC引脚,使用ADC3的通道8,STM32内部的1.2V校准电压Vrefint在ADC1中。所以我们需要按如下方式在CubeMX里进行配置。
- 开启ADC1和ADC3分别用于内部1.2V的Vrefint通道读取和电池电压ADC3的通道8读取。
- 在cubeMX中开启ADC1,在设置中将Vrefint Channel勾选,用于读取内部参考电压。ADC在cubeMX中的设置如图采样频率设置为PCLK2/4,采样位数为12位,数据设置为右对齐,其余均保持默认。其中Vrefine在stm32内部完成,没有对应引脚。 最终ADC配置如图所示。 《补充》:但是,这里还是把其中参数的具体含义给大家科普一下吧
名称 | 功能 |
---|---|
Clock Prescaler | 设置采样时钟频率 |
Resolution | 设置采样精度 |
Data Alignment | 设置数据对齐方式 |
Scan Conversion Mode | 扫描转换模式开启/关闭 |
Continuous Conversion Mode | 连续转换模式开启/关闭 |
Discontinuous Conversion Mode | 非连续转换模式开启/关闭 |
DMA Continuous Requests | DMA连续启动开启/关闭 |
End of Conversion Selection | 每个通道转换结束后发送EOC标志/所有通道转换结束后发送EOC标志 |
- 在CubeMX中开启ADC3,并打开其IN8用于电池电压的读取,其设置和ADC1一致。可以看到引脚图像中ADC3对应的PF10变绿。
- 这里我们需要把PC0,PC1,PC2引脚设置为输入模式,且这三个引脚拉为高电平,如图所示
- 点击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的函数
- ADC通道设置函数
函数作用 | 设置ADC通道的各个属性值,包括转换通道,序列排序,采样时间等 |
---|---|
返回值 | HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK |
参数1 | TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2 |
参数2 | ADC_ChannelConfTypeDef* sConfig即指向ADC设置的结构体指针。我们先对sConfig结构体进行赋值,然后再将其指针作为参数输入函数 |
- ADC开启采样函数
函数作用 | 开启ADC的采样 |
---|---|
返回值 | HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK |
参数 | TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2 |
- 等待ADC转换结束函数
函数作用 | 等待ADC转换结束 |
---|---|
返回值 | HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK |
参数1 | TIM_HandleTypeDef * hadc即ADC的句柄指针,如果是adc1就输入&hadc1,adc2就输入&adc2 |
参数2 | HAL_StatusTypeDef,HAL库定义的几种状态,如果成功使ADC开始工作,则返回HAL_OK |
- 获取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>© 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是通过一位位进行比较得出转换值,其原理图如下所示。
如图所示,我们发现整个电路由比较器、D/A转换器、缓冲寄存器和若干控制逻辑电路构成。作用如下所示:
- 比较器:用于输入电压值D/A转换器输出电压进行比较,当输入电压大于D/A转换器电压时,输出为1,反之输出为0;
- D/A转换器:ADC的逆向过程,将缓冲寄存器的记录数字量转换成模拟量。
- 缓冲寄存器:记录当前转换的数字量。
整个过程如下:
- 将缓冲寄存器清零;
- 将逐次逼近寄存器 最高位 置1;
- 把数字值送入D/A转换器,经D/A转换后的模拟量送入比较器,称为 Vo;
- Vo与比较器的待转换的模拟量Vi 比较,若Vo<Vi,该位被保留,否则被清0。
- 再置 寄存器 次高位为1,将寄存器中新的数字量送D/A转换器,
- 输出的 Vo再与Vi比较,若Vo<Vi,该位被保留,否则被清0。
-
循环此过程,直到寄存器最低位,得到数字量的输出。
在这个过程中,D/A转换器使用的电压为STM32的电源电压Vref+和Vref-,如图所示:
STM32的标准电压为 3.3V。例如第一次转换时,输出为 1/2Vre电压,即1.65V。但由于外部供电电压不一定为 3.3V。故而为了在这种情况提高ADC精度,STM32内部有VERFINT1.2V稳定电压,可以使用ADC采样该电压来提高ADC的精度。
代码我已经放到了我的GitHub仓库,如有需要可以下载使用:
CubeMX学习
总结
这次的博客,我们重点学习了ADC的相关知识,ADC是模拟量变成数字量的过程,单片机获取电压,电流传感器等模拟量的方法。通过ADC功能,我们能够获取各种传感器的模拟值,并将其转化为单片机可以处理的数字量,也可以获取电池的电压值,并将其通过电量形式显示出来。