一、ADC模数转换简介
ADC(Analog-to-Digital Converter,模数转换器) 是将连续变化的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。模数转换器可以实现这个功能,在各种不同的产品中都可以找到它的身影。与之相对应的 DAC(Digital-to-Analog Converter),它是 ADC 模数转换的逆向过程。
我们在采集外界信号转换为电气(电平)信号时,通常有两种输入方式:逻辑电平输入和ADC输入。例如现有输入电压值采集,前者只能读取高、低电平(0或1)的二态信号,该态势信号是与某阀值比较输出的真假数字信号,而ADC输入方式可以读取出电压值大小的离散信号,这个将输入的连续模拟量电压转换为数字离散电压的过程就叫模数转换,模数转换一般要经过采样、保持和量化、编码这几个步骤。
【1】采样,将时间上连续变化的模拟信号转换为时间上离散的模拟信号。
【2】保持,模拟信号转换为数字信号都需要一定时间,为了给后续处理过程提供一个稳定的值,在采样电路后要求将所采样的模拟信号保持一段时间。
【3】量化,采样保持电路的输出电压还需要按照某种近似方式归化到与之相应的离散电平上,任何数字量只能是某个最小数量单位的整数倍。
【4】编码,量化后的数值其后还需要编码过程,也就是 A/D 转换器输出的数字量。
二、ADC工程创建
【1】本文采用STM32L496VGTx的ali联合上海诺行的开发板,其支持ADC功能的原理框图及引脚描述如下:
引脚描述:
本文不按开发板资料设定来配置ADC,只采用到PC2、PC3两个引脚。
【2】本文基于原有的已经实现串口lpusart调试输出和LCD屏幕输出的就工程为基础新建一个adc工程,并将旧工程已经实现的功能迁移到新功能工程。
完成新工程创建后,进入cubeMX配置界面,配置ADC功能
开启ADC1的IN3:
开启ADC2的IN4
前两部配置开启ADC功能时,时钟会自动开启ADC时钟源
【3】点击保存输出
在ICore目录下新建adc文件夹,并在该目录下创建adc.h和adc.c源文件,项目整体目录结构如下:
三、轮询读取单路ADC数值代码设计
目前只配置了ADC轮询模式读取数据,在stm32l4xx_hal_adc.c中关于轮询模式读取ADC数据说明如下:
(++) ADC conversion by polling:
(+++) Activate the ADC peripheral and start conversions
using function HAL_ADC_Start()
(+++) Wait for ADC conversion completion
using function HAL_ADC_PollForConversion()
(+++) Retrieve conversion results
using function HAL_ADC_GetValue()
(+++) Stop conversion and disable the ADC peripheral
using function HAL_ADC_Stop()
因此现读取adc数据驱动: adc.h
#ifndef ADC_ADC_H_
#define ADC_ADC_H_
#include "stm32l4xx_hal.h" //HAL库文件声明
extern ADC_HandleTypeDef hadc1;
extern ADC_HandleTypeDef hadc2;
uint16_t ADC_IN_1(void);
uint16_t ADC_IN_2(void);
#endif /* ADC_ADC_H_ */
adc.c
#include "adc.h"
uint16_t ADC_IN_1(void) //ADC采集程序
{
HAL_ADC_Start(&hadc1);//开始ADC采集
HAL_ADC_PollForConversion(&hadc1,500);//等待采集结束
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC))//读取ADC完成标志位
{
return HAL_ADC_GetValue(&hadc1);//读出ADC数值
}
return 0;
}
uint16_t ADC_IN_2(void) //ADC采集程序
{
HAL_ADC_Start(&hadc2);//开始ADC采集
HAL_ADC_PollForConversion(&hadc2,500);//等待采集结束
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc2), HAL_ADC_STATE_REG_EOC))//读取ADC完成标志位
{
return HAL_ADC_GetValue(&hadc2);//读出ADC数值
}
return 0;
}
在main.c文件中,添加acd.h驱动文件
/* USER CODE BEGIN Includes */
#include "../../ICore/key/key.h"
#include "../../ICore/led/led.h"
#include "../../ICore/print/print.h"
#include "../../ICore/usart/usart.h"
#include "../../ICore/oled/oled.h"
#include "../../ICore/adc/adc.h"
/* USER CODE END Includes */
在主函数内初始化两个独立ADC引脚
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
//ADC相关,ADC_SINGLE_ENDED=单端模式,ADC_DIFFERENTIAL_ENDED=差分模式,CubeMX配置了单端
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);//ADC采样校准
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);//ADC采样校准
//LCD
OLED_init();
//设置OLED蓝色背景显示
BSP_LCD_Clear_DMA(LCD_DISP_BLUE);
printf("OLED_Clear_DMA\r\n");
/* USER CODE END 2 */
在主函数循环体内,通过按键2获取两个ADC数值
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
//printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
OLED_printf(10,10,"%.*s",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(KEY_0())
{
BSP_LCD_login(24,108);
}
if(KEY_1())
{
BSP_LCD_img_DMA();
}
if(KEY_2())
{
printf("ADC1=%04X ADC2=%04X \r\n",ADC_IN_1(),ADC_IN_2());
OLED_printf(10,108,"ADC1=%04X ADC2=%04X",ADC_IN_1(),ADC_IN_2());//向lcd发送字符串
}
/* USER CODE END WHILE */
编译及下载效果如下,目前两个ADC引脚没有外接设备,每次取得数值是一定范围随机的:
四、DMA方式读取单路ADC代码设计
回到cubeMX配置界面,开启ADC的连续转换功能
开启ADC的DMA功能
确认ADC的DMA的终端功能是否开启。
确保DMA初始化排序在ADC前面
点击保存输出生成代码,设置ADC1支持DMA模式,ADC2依然是轮询模式。
关于ADC的DMA读取数据,HAL标准库描述如下:
(++) ADC conversion with transfer by DMA:
(+++) Activate the ADC peripheral and start conversions
using function HAL_ADC_Start_DMA()
(+++) Wait for ADC conversion completion by call of function
HAL_ADC_ConvCpltCallback() or HAL_ADC_ConvHalfCpltCallback()
(these functions must be implemented in user program)
(+++) Conversion results are automatically transferred by DMA into
destination variable address.
(+++) Stop conversion and disable the ADC peripheral
using function HAL_ADC_Stop_DMA()
现在来实现ADC的DMA读取数据,在main.c主函数内,调整如下:
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
//ADC相关,ADC_SINGLE_ENDED=单端模式,ADC_DIFFERENTIAL_ENDED=差分模式,CubeMX配置了单端
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);//ADC采样校准
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);//ADC采样校准
//本次新调整
uint16_t a1 = 0, a2 = 0; //缓存ADC1数值
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&a1,1);//启动DMA,采集数据存入的变量地址,长度1
//LCD
OLED_init();
//设置OLED蓝色背景显示
BSP_LCD_Clear_DMA(LCD_DISP_BLUE);
printf("OLED_Clear_DMA\r\n");
/* USER CODE END 2 */
在主函数循环体内调整如下:
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
//printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
OLED_printf(10,10,"%.*s",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(KEY_0())
{
BSP_LCD_login(24,108);
}
if(KEY_1())
{
BSP_LCD_img_DMA();
}
if(KEY_2())
{
//本次调整
a2 = ADC_IN_2();
printf("ADC1=%04X ADC2=%04X \r\n",a1,a2);
OLED_printf(10,108,"ADC1=%04X ADC2=%04X",a1,a2);//向lcd发送字符串
}
/* USER CODE END WHILE */
编译及下载,也能实现数据读取:
五、DMA模式读取ADC多路数据
再回到cubeMX配置界面,关闭ADC2
再开启ADC1的4通道对PC3的支持,参数设置页面,设置通道数量为2,rank1支持3通道,rank2支持4通道
确保扫描模式已经自动开启(设置多通道会自动开启)
点击保存生成输出代码
DMA读取多路ADC数据和单路数据步骤几乎一致,只是给出不同大缓存空间而已,调整代码如下:
在main.c主函数调整ADC初始化
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
//ADC相关,ADC_SINGLE_ENDED=单端模式,ADC_DIFFERENTIAL_ENDED=差分模式,CubeMX配置了单端
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);//ADC采样校准
//本次调整,注释hadc2相关,给hadc1的DMA读取2个缓存空间
// HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);//ADC采样校准
// uint16_t a1 = 0, a2 = 0; //缓存ADC1数值
// HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&a1,1);//启动DMA,采集数据存入的变量地址,长度1
uint16_t adc_val[2] = {0};
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&adc_val,2);//启动DMA,采集数据存入的变量地址,长度2
//LCD
OLED_init();
//设置OLED蓝色背景显示
BSP_LCD_Clear_DMA(LCD_DISP_BLUE);
printf("OLED_Clear_DMA\r\n");
/* USER CODE END 2 */
在main.c的主函数循环体中,调整如下:
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
//printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
OLED_printf(10,10,"%.*s",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(KEY_0())
{
BSP_LCD_login(24,108);
}
if(KEY_1())
{
BSP_LCD_img_DMA();
}
if(KEY_2())
{
//本次调整,直接读取数组数据打印输出
// a2 = ADC_IN_2();
printf("ADC1=%04X ADC2=%04X \r\n",adc_val[0],adc_val[1]);
OLED_printf(10,108,"ADC1=%04X ADC2=%04X",adc_val[0],adc_val[1]);//向lcd发送字符串
}
/* USER CODE END WHILE */
编译及下载,同样能顺利读取到数据:
六、ADC中断读取模式
再次回到cubeMX配置界面,再次开启ADC2支持PC3引脚,参数保持默认并开启中断功能。
保存输出生成代码
在自定义的驱动文件adc.c(非图形配置生成输出代码),添加对回调函数的处理:
extern uint16_t Adc_Value;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)//回调函数
{
if(&hadc2==hadc){
Adc_Value = HAL_ADC_GetValue(&hadc2);
}
}
在main.c文件中,加入全局变量声明:
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint16_t Adc_Value = 0;
/* USER CODE END 0 */
在mian.c文件的主函数中,改写ADC初始化
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
//ADC相关,ADC_SINGLE_ENDED=单端模式,ADC_DIFFERENTIAL_ENDED=差分模式,CubeMX配置了单端
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);//ADC采样校准
// HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);//ADC采样校准
// uint16_t a1 = 0, a2 = 0; //缓存ADC1数值
// HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&a1,1);//启动DMA,采集数据存入的变量地址,长度1
uint16_t adc_val[2] = {0};
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&adc_val,1);//启动DMA,采集数据存入的变量地址,长度1
//本次改写
Adc_Value = 0;
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);
HAL_ADC_Start_IT(&hadc2);
//LCD
OLED_init();
//设置OLED蓝色背景显示
BSP_LCD_Clear_DMA(LCD_DISP_BLUE);
printf("OLED_Clear_DMA\r\n");
/* USER CODE END 2 */
在主函数循环体内,调整输出显示
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
//printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
OLED_printf(10,10,"%.*s",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(KEY_0())
{
BSP_LCD_login(24,108);
}
if(KEY_1())
{
BSP_LCD_img_DMA();
}
if(KEY_2())
{
// a2 = ADC_IN_2();
HAL_ADC_Start_IT(&hadc2);//无外接设备,需master端自驱动刷新数据
printf("ADC1=%04X ADC2=%04X ADC2=%04X \r\n",adc_val[0],adc_val[1],Adc_Value);
OLED_printf(10,108,"ADC1=%04X ADC2=%04X ADC2=%04X ",adc_val[0],adc_val[1],Adc_Value);//向lcd发送字符串
}
/* USER CODE END WHILE */
编译下载,数据只有中断读取的变更了,而DMA读取的不刷新变化,显然PC3引脚共用带来的问题(先留个疑问,哈哈):