ADC(Analog-to-Digital Converter),即模拟-数字转换器,可以将连续变化的模拟信号转换为离散的数字信号,进而使用数字电路进行处理,称之为数字信号处理。
STM32f103 系列有 3 个 ADC,精度为 12 位,每个 ADC 最多有 16 个外部通道。其中 ADC1 和 ADC2 都有 16 个外部通道,ADC3 根据 CPU 引脚的不同通道数也不同,一般都有 8 个外部通道。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。ADC 的输入时钟不得超过14MHz,它是由PCLK2经分频产生。
STM32 的 ADC 多达 18 个通道,其中外部的 16 个通道就是框图中的 ADCx_IN0、ADCx_IN1…ADCx_IN5。这 16 个通道对应着不同的 IO 口,具体是哪一个 IO 口可以从手册查询到。其中 ADC1/2/3 还有内部通道:ADC1 的通道 16 连接到了芯片内部的温度传感器,Vrefint 连接到了通道 17。ADC2 的模拟通道 16 和 17 连接到了内部的 VSS。ADC3 的模拟通道 9、14、15、16 和 17 连接到了内部的 VSS。
开发板板载一个贴片滑动变阻器,引脚为 PC1,对应 ADC1 的通道 11
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
在 Analog
中选择 ADC1
设置,并选择 IN11
通道11
或者在右边图找到 PC1
引脚,选择 ADC1_IN11
具体配置参数如下。
Independent mod
独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选,具体配置 ADC_CR1:DUALMOD 位。Right alignment
转换结果数据右对齐,一般我们选择右对齐模式。Left alignment
转换结果数据左对齐。Disabled
禁止扫描模式。如果是单通道 AD 转换使用 DISABLE。Enabled
开启扫描模式。如果是多通道 AD 转换使用 ENABLE。Disabled
单次转换。转换一次后停止需要手动控制才重新启动转换。Enabled
自动连续转换。Disabled
禁止间断模式。这个在需要考虑功耗问题的产品中很有必要,也就是在某个事件触发下,开启转换。Enabled
开启间断模式。Enable Regular Conversions
是否使能规则转换。Number Of Conversion
ADC转换通道数目,有几个写几个就行。External Trigger Conversion Source
外部触发选择。这个有多个选择,一般采用软件触发方式。Channel
ADC转换通道Sampling Time
采样周期选择,采样周期越短,ADC 转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC 转换数据输出周期就越长同时数据精度越高。Enable Injected Conversions
是否使能注入转换。注入通道只有在规则通道存在时才会出现。Enable Analog WatchDog Mode
是否使能模拟看门狗中断。当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。ADC 的转换时间跟 ADC 的输入时钟和采样时间有关。
公式为:Tconv = 采样时间 + 12.5 个周期。当 ADCLK = 14MHZ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)Tconv = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。
一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M
,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us,这个才是最常用的。
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的 ’.c/.h’
文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
单通道采集适用 AD 转换完成中断,在中断服务函数中读取数据,不使用 DMA 传输,在多通道采集时才使用 DMA 传输。
打开 stm32f1xx_it.c
中断服务函数文件,找到 ADC1 中断的服务函数 ADC1_2_IRQHandler()
中断服务函数里面就调用了 ADC 中断处理函数 HAL_ADC_IRQHandler()
打开 stm32f1xx_hal_adc.c
文件,找到 ADC 中断处理函数原型 HAL_ADC_IRQHandler()
,其主要作用就是判断是哪个 ADC 产生中断,清除中断标识位,然后调用中断回调函数 HAL_ADC_ConvCpltCallback()
。
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
HAL_ADC_ConvCpltCallback()
按照官方提示我们应该再次定义该函数,__weak
是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而 UNUSED(hadc)
,这就是一个防报错的定义,当传进来的ADC号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的定时器号即可。
接下来我们就在 stm32f1xx_it.c
这个文件的最下面添加 HAL_ADC_ConvCpltCallback()
/* USER CODE BEGIN EV */
extern __IO uint32_t ADC_ConvertedValue;
/* USER CODE END EV */
/* USER CODE BEGIN 1 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
ADC_ConvertedValue = HAL_ADC_GetValue(hadc);
}
/* USER CODE END 1 */
在中断回调函数中进行读取数据,将数据存放在变量 ADC_ConvertedValue
中。
在 main.c 定义相关变量。
// ADC转换值
__IO uint32_t ADC_ConvertedValue;
// 用于保存转换计算后的电压值
float ADC_Vol;
在 main.c 中,while 循环前,ADC初始化后,添加ADC中断开启函数,这样在第一次接收到数据的时候才会触发中断。
/**
* @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_USART1_UART_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1); //AD校准
HAL_ADC_Start_IT(&hadc1); //开启ADC中断转换
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
模拟电压经过 ADC 转换后,是一个 12 位的数字值,如果通过串口以 16 进制打印出来的话,可读性比较差,那么有时候我们就需要把数字电压转换成模拟电压,也可以跟实际的模拟电压(用万用表测)对比,看看转换是否准确。
我们一般在设计原理图的时候会把 ADC 的输入电压范围设定在:0~3.3v
,因为 ADC 是 12 位的,那么 12 位满量程对应的就是 3.3V,12 位满量程对应的数字值是:2^12。数值 0 对应的就是 0V。如果转换后的数值为 X ,X 对应的模拟电压为 Y,那么会有这么一个等式成立: 2^12 / 3.3 = X / Y,=> Y = (3.3 * X ) / 2^12。
串口打印功能查看 STM32CubeMX学习笔记(6)——USART串口使用
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
ADC_Vol =(float) ADC_ConvertedValue/4096*3.3; // 读取转换的AD倿
printf("The current AD value = 0x%04X \r\n", ADC_ConvertedValue);
printf("The current AD value = %f V \r\n\r\n",ADC_Vol); //实际电压倿
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
STM32CubeMX 使用 HAL 库生成的代码:
__IO uint32_t ADC_ConvertedValue;
/**
* @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_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();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_11;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC1_Init 2 */
/* USER CODE END ADC1_Init 2 */
}
/**
* @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 */
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
ADC_ConvertedValue = HAL_ADC_GetValue(hadc);
}
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
使用 STM32 标准库的代码:
__IO uint16_t ADC_ConvertedValue;
/**
* @brief ADC GPIO 初始化
* @param 无
* @retval 无
*/
static void ADCx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 打开 ADC IO端口时钟
ADC_GPIO_APBxClock_FUN ( ADC_GPIO_CLK, ENABLE );
// 配置 ADC IO 引脚模式
// 必须为模拟输入
GPIO_InitStructure.GPIO_Pin = ADC_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
// 初始化 ADC IO
GPIO_Init(ADC_PORT, &GPIO_InitStructure);
}
/**
* @brief 配置ADC工作模式
* @param 无
* @retval 无
*/
static void ADCx_Mode_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
// 打开ADC时钟
ADC_APBxClock_FUN ( ADC_CLK, ENABLE );
// ADC 模式配置
// 只使用一个ADC,属于独立模式
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
// 禁止扫描模式,多通道才要,单通道不需要
ADC_InitStructure.ADC_ScanConvMode = DISABLE ;
// 连续转换模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
// 不用外部触发转换,软件开启即可
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
// 转换结果右对齐
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// 转换通道1个
ADC_InitStructure.ADC_NbrOfChannel = 1;
// 初始化ADC
ADC_Init(ADCx, &ADC_InitStructure);
// 配置ADC时钟为PCLK2的8分频,即9MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
// 配置 ADC 通道转换顺序和采样时间
ADC_RegularChannelConfig(ADCx, ADC_CHANNEL, 1,
ADC_SampleTime_55Cycles5);
// ADC 转换结束产生中断,在中断服务程序中读取转换值
ADC_ITConfig(ADCx, ADC_IT_EOC, ENABLE);
// 开启ADC ,并开始转换
ADC_Cmd(ADCx, ENABLE);
// 初始化ADC 校准寄存器
ADC_ResetCalibration(ADCx);
// 等待校准寄存器初始化完成
while(ADC_GetResetCalibrationStatus(ADCx));
// ADC开始校准
ADC_StartCalibration(ADCx);
// 等待校准完成
while(ADC_GetCalibrationStatus(ADCx));
// 由于没有采用外部触发,所以使用软件触发ADC转换
ADC_SoftwareStartConvCmd(ADCx, ENABLE);
}
static void ADC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
// 配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief ADC初始化
* @param 无
* @retval 无
*/
void ADCx_Init(void)
{
ADCx_GPIO_Config();
ADCx_Mode_Config();
ADC_NVIC_Config();
}
void ADC_IRQHandler(void)
{
if (ADC_GetITStatus(ADCx,ADC_IT_EOC)==SET)
{
// 读取ADC的转换值
ADC_ConvertedValue = ADC_GetConversionValue(ADCx);
}
ADC_ClearITPendingBit(ADCx,ADC_IT_EOC);
}
MX_ADC1_Init();
对应 ADCx_GPIO_Config();ADCx_Mode_Config();ADC_NVIC_Config();
HAL_ADC_Init(&hadc1)
对应 ADC_Init(ADCx, &ADC_InitStructure)
HAL_ADCEx_Calibration_Start(&hadc1);
对应 ADC_StartCalibration(ADCx);
HAL_ADC_Start_IT(&hadc1);
对应 ADC_ITConfig(ADCx, ADC_IT_EOC, ENABLE);
HAL_ADC_GetValue(hadc);
对应 ADC_GetConversionValue(ADCx);
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2021 年 1 月 19 日
• 参考:STM32CubeMX系列教程7:模数转换(ADC)
《嵌入式-STM32开发指南》第二部分 基础篇 - 第8章 模拟输入输出-ADC(HAL库)