赞
踩
ADC 是一种常见的外设,你可以在STM32 上见到它,在I.MX 6ULL 上依然能看到它的存在。通过读取GPIO 引脚的高低电平我们可以知道输入的是1 还0,但是我们并不能知道它实际的电压是多少。ADC 的存在就是让你知道的更加清楚,ADC 可以让你知道某个IO 的具体电压值。有很多传感器都是模拟信号输出的,也就是输出电压值,我们需要测量到其具体的电压值,然后在使用对应的公式进行计算,得到最终的数字值。本章我们就来学习一下I.MX6ULL的ADC 外设。
ADC,Analog to Digital Converter 的缩写,中文名称模数转换器。它可以将外部的模拟信号转化成数字信号。对于GPIO 口来说高于某个电压值,它读出来的只有高电平,低于就是低电平。假如我想知道具体的电压数值就要借助于ADC 的帮助,它可以将一个范围内的电压精确的读取出来。假设我们的GPIO 口只要高于1.7V 的都认为是高电平,例如,比如某个IO 口上外接了一个设备它能提供0-2V 的电压变化,我们在这个IO 口上使用GPIO 模式去读取的话我们只能获得0 和1 两个数据,但是我们使用ADC 模式去读取就可以获得0-2V 之间连续变化的数值。
ADC 有几个比较重要的参数:
测量范围:测量范围对于ADC 来说就好比尺子的量程,ADC 测量范围决定了你外接的设备其信号输出电压范围,不能超过ADC 的测量范围。如果所使用的外部传感器输出的电压信号范围和所使用的ADC 测量范围不符合,那么就需要自行设计相关电压转换电路。
分辨率:就是尺子上的能量出来的最小测量刻度,例如我们常用的厘米尺它的最小刻度就是1 毫米,表示最小测量精度就是1 毫米。假如ADC 的测量范围为0-5V,分辨率设置为12位,那么我们能测出来的最小电压就是5V 除以2 的12 次方,也就是5/4096=0.00122V。很明显,分辨率越高,采集到的信号越精确,所以分辨率是衡量ADC 的一个重要指标。
精度:是影响结果准确度的因素之一,比如在厘米尺上我们能测量出大概多少毫米的尺度但是毫米后一点点我们却不能准确的量出。经过计算我们ADC 在12 位分辨率下的最小测量值是0.00122V 但是我们ADC 的精度最高只能到11 位也就是0.00244V。也就是ADC 测量出0.00244V 的结果是要比0.00122V 要可靠,也更准确。
采样时间:当ADC 在某时刻采集外部电压信号的时候,此时外部的信号应该保持不变,但实际上外部的信号是不停变化的。所以在ADC 内部有一个保持电路,保持某一时刻的外部信号,这样ADC 就可以稳定采集了,保持这个信号的时间就是采样时间。
采样率:也就是在一秒的时间内采集多少次。很明显,采样率越高越好,当采样率不够的时候可能会丢失部分信息,所以ADC 采样率是衡量ADC 性能的另一个重要指标总之,只要是需要模拟信号转为数字信号的场合,那么肯定要用到ADC。很多数字传感器内部会集成ADC,传感器内部使用ADC 来处理原始的模拟信号,最终给用户输出数字信号。
I.MX6ULL 提供了两个12 位ADC 通道和10 个输入接口给我们使用。I.MX6ULL 的ADC外设特性如下:
1)、线性连续逼近算法,分辨率高达12 位。
2)、多达10 个通道可以选择。
3)、最高采样率1MS/s。
4)、多达8 个单端外部模拟输入。
5)、单次或连续转换(单次转换后自动返回空闲状态)。
6)、可以配置为12/10/8 位。
7)、可配置的采样时间和转换速度/功率
8)、支持转换完成、硬件平均完成标志和中断。
9)、自我校准模式
ADC 有三种工作状态:禁止状态(Disabled)、闲置状态(Idle)、工作状态(Performing conversions)
禁止状态:ADC 模块被禁止工作。
闲置状态:当前转换已经完成,下次转换尚未准备时的状态,当异步时钟输出被关闭,ADC进入该状态时,ADC 此时处于最低功耗状态。
工作状态:当ADC 初始化完成后,并设置好输入通道后,将进入的状态。转换过程中也一直保持在工作状态。
我们来介绍一下ADC 对应的寄存器。让我们了解如何使用它。这里我们拿ADC1 来进行一个介绍,ADC2 和ADC1有一点不同但在总体上来说是差不多的,想要深入了解的详细去看参考手册。
我们接下来看一下ADC 的几个重要的寄存器,首先看一下ADCx_CFG(x=1~2)寄存器,这是ADC1 的配置寄存器,此寄存器结构如图C1.1.1.1 所示:
寄存器ADC1_CFG 用到的重要位如下:
OVWREN (bit16):数据复写使能位,为1 的时候使能复写功能,为0 的时候关闭复写功能。
AVGS(bit15:14):硬件平均次数,只有当ADC1_GC 寄存器的AVGE 位为1 的时候才有效。
可选值表C1.1.1.1 所示:
AVGS(bit15:bit14) | 含义 |
---|---|
00 | 4 次样本求平均。 |
01 | 8 次样本求平均。 |
10 | 16 次样本求平均。 |
11 | 32 次样本求平均。 |
ADTRG(bit13):转换触发选择。为0 的时候选择软件触发,为1 的时候,不选择软件触发。
REFSEL(bit12:11):参考电压选择,为00 时选择VREFH/VREFL 这两个引脚上的电压为参考电压,正点原子ALPHA 开发板上VREFH 为3.3V,VREFL 为0V。
ADHSC(bit10):高速转换使能位,当为0 时为正常模式,为1 时为高速模式。
ADSTS(bit9:8):设置ADC 的采样周期,与ADLSMP 位一起决定采样周期,如表C1.1.1.2所示:
值 | 含义 |
---|---|
00 | 当ADLSMP=0 时采样一次需要2 个ADC clocks,ADLSMP=1 时需要12 个。 |
01 | 当ADLSMP=0 时采样一次需要4 个ADC clocks,ADLSMP=1 时需要16 个。 |
10 | 当ADLSMP=0 时采样一次需要6 个ADC clocks,ADLSMP=1 时需要20 个。 |
11 | 当ADLSMP=0 时采样一次需要8 个ADC clocks,ADLSMP=1 时需要24 个。 |
ADIV(bit6:5):时钟分频选择,为00 的时候不分频,为01 的时候2 分频,为10 的时候4分频,为11 的时候8 分频。
ADLSMP(bit4):长采样周期使能位,当值为0 时为短采样周期模式,为1 时为长采样周期模式。搭配ADSTS 位一起控制ADC 的采样周期,见表C1.1.2。
MODE(bit3:2):选择转换精度,设置如表C1.1.3:
值 | 含义 |
---|---|
00 | 8 位精度 |
01 | 10 位精度 |
10 | 12 位精度 |
11 | 无效 |
ADICLK(bit1:0):输入时钟源选择,为00 的时候选择IPG Clock,为01 的时候选择IPG Clock/2,为10 的时候无效,为11 的时候选择呢ADACK。本教程我们设置为11,也就是选择ADACK 为ADC 的时钟源。
接下来看一下通用控制寄存器ADCx_GC,寄存器结构如图C1.1.1.2 所示:
此寄存器对应的位含义如下:
CAL(bit7):当该位写入1 时,硬件校准功能将会启动,校准过程中该位会一直保持1,校准完成后会清0,校准完成后需要检查一下ADC_GS[CALF]位,确认校准结果。
ADCO(bit6):连续转换使能位,只有在开启了硬件平均功能时有效,为0 时只能转换一次或一组,当ADCO 为1 时可以连续转换或多组。
AVGE(bit5):硬件平均使能位。为0 时关闭,为1 时使能。
ACFE(bit4):比较功能使能位。为0 时关闭,为1 时使能。
ACFGT(bit3):配置比较方法,如果为0 的话就比较转换结果是否小于ADC_CV 寄存器值,如果为1 的话就比较装换结果是否大于或等于ADC_CV 寄存器值。
ACREN(bit2):范围比较功能使能位。为0 的话仅和ADC_CV 里的CV1 比较,为1 的话和ADC_CV 里的CV1、CV2 比较。
ACREN(bit2):范围比较功能使能位。为0 的话仅和ADC_CV 里的CV1 比较,为1 的话和ADC_CV 里的CV1、CV2 比较。
DMAEN(bit1):DMA 功能使能位,为0 是关闭,为1 是开启。
ADACKEN(bit0):异步时钟输出使能位,为0 是关闭,为1 时开启。
接下来看一下通用状态寄存器ADCx_GS,寄存器结构如图C1.1.1.3 所示:
此寄存器对应的位含义如下:
AWKST(bit2):异步唤醒中断状态,为1 时表示发生了异步唤醒中断。为0 时没有发生异步中断。
CALF(bit1):校准失败标志位,为0 的时候表示校准正常完成,为1 的时候表示校准失败。
ADACT(bit0):转换活动标志,为0 的时候表示转换没有进行,为1 的时候表示正在进行转换。
接下来看一下状态寄存器ADCx_HS,此寄存器结构如图C1.1.1.4 所示:
此寄存器只有一个位COCO0,这是转换完成标志位,此位为只读位,当关闭比较功能和硬件平均以后每次转换完整此位就会被置1。使能硬件平均以后,只有在设置的转换次数达到以后此位才置1。
再来看一下控制寄存器ADCx_HC0,此寄存器结构如图C1.1.1.5 所示:
来看一下此寄存器对应的位:
AIEN(bit7):转换完成中断控制位,为1 的时候打开转换完成中断,为0 的时候关闭。
ADCH(bit4:0):转换通道选择,可以设置为00000~01111 分别对应通道0~15。11001 为内部通道,用于ADC 自测。
最后看一下数据结果寄存器ADCx_R0,顾名思义,此寄存器保存ADC 数据结果,也就是转换值,寄存器结构如图C1.1.1.6 所示:
从图C1.1.1.6 可以看出,只有bit11:0 这12 位有效,此12 位用来保存ADC 转换结果。
关于ADC 有关的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL参考手册》第13.6 小节。本章我们使用I.MX6ULL 的ADC1 通道1,ADC1 通道1 的引脚为GPIO1_IO01,配置步骤如下:
1、初始化ADC1_CH1
初始化ADC1_CH1,配置ADC 位数,时钟源,采样时间等。
2、校准ADC
ADC 在使用之前需要校准一次。
4、使能ADC
配置好ADC 以后就可以开启了。
5、读取ADC 值
ADC 正常工作以后就可以读取ADC 值。
本试验用到的资源如下:
1)、指示灯LED0。
2)、RGB LCD 接口。
3)、GPIO1_IO01 引脚
本实验主要用到I.MX6ULL 的GPIO1_IO01 引脚,将其作为ADC1 的通道1引脚,正点原子ALPHA 开发板上引出了GPIO1_IO01 引脚,如图C1.2.1.1 所示:
图C1.2.1.1 中的GPIO1 就是GPIO1_IO01 引脚,此引脚作为ADC1_CH1,我们可以使用杜邦线在此引脚上引入一个0~3.3V 的电压,然后使用内部ADC 进行测量。
本实验对应的例程路径为:开发板光盘-> 1、裸机例程-> 21_adc。
本章实验在上一章例程的基础上完成,更改工程名字为“adc”,然后在bsp 文件夹下创建名为“adc”的文件夹。在bsp/adc 中新建bsp_adc.c 和bsp_adc.h 这两个文件。在bsp_adc.h 中输入如下内容:
#ifndef __ADC_H #define __ADC_H /*************************************************************** Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved. 文件名 : bsp_adc.h 作者 : 左忠凯 版本 : V1.0 描述 : ADC驱动头文件。 其他 : 无 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/22 左忠凯创建 ***************************************************************/ #include "imx6ul.h" int adc1ch1_init(void); status_t adc1_autocalibration(void); uint32_t getadc_value(void); unsigned short getadc_average(unsigned char times); unsigned short getadc_volt(void); #endif
文件bsp_adc.h 内容很简单,都是一些函数声明。接下来在文件bsp_backlight.c 中输入如下内容:
#include "bsp_adc.h" #include "bsp_delay.h" #include "stdio.h" /* * @description : 初始化ADC1_CH1,使用GPIO1_IO01这个引脚。 * @param : 无 * @return : 0 成功,其他值 错误代码 */ int adc1ch1_init(void) { int ret = 0; /* 1、初始化ADC1 CH1 */ /* CFG寄存器 * bit16 0 关闭复写功能 * bit15:14 00 硬件平均设置为默认值,00的时候4次平均, * 但是得ADC_GC寄存器的AVGE位置1来使能硬件平均 * bit13 0 软件触发 * bit12:1 00 参考电压为VREFH/VREFL,也就是3.3V/0V * bit10 0 正常转换速度 * bit9:8 00 采样时间2/12,ADLSMP=0(短采样)的时候为2个周期 * ADLSMP=1(长采样)的时候为12个周期 * bit7 0 非低功耗模式 * bit6:5 00 ADC时钟源1分频 * bit4 0 短采样 * bit3:2 10 12位ADC * bit1:0 11 ADC时钟源选择ADACK */ ADC1->CFG = 0; ADC1->CFG |= (2 << 2) | (3 << 0); /* GC寄存器 * bit7 0 先关闭校准功能,后面会校准 * bit6 0 关闭持续转换 * bit5 0 关闭硬件平均功能 * bit4 0 关闭比较功能 * bit3 0 关闭比较的Greater Than功能 * bit2 0 关闭比较的Range功能 * bit1 0 关闭DMA * bit0 1 使能ADACK */ ADC1->GC = 0; ADC1->GC |= 1 << 0; /* 2、校准ADC */ if(adc1_autocalibration() != kStatus_Success) ret = -1; return ret; } /* * @description : 初始化ADC1校准 * @param : 无 * @return : kStatus_Success 成功,kStatus_Fail 失败 */ status_t adc1_autocalibration(void) { status_t ret = kStatus_Success; ADC1->GS |= (1 << 2); /* 清除CALF位,写1清零 */ ADC1->GC |= (1 << 7); /* 使能校准功能 */ /* 校准完成之前GC寄存器的CAL位会一直为1,直到校准完成此位自动清零 */ while((ADC1->GC & (1 << 7)) != 0) { /* 如果GS寄存器的CALF位为1的话表示校准失败 */ if((ADC1->GS & (1 << 2)) != 0) { ret = kStatus_Fail; break; } } /* 校准成功以后HS寄存器的COCO0位会置1 */ if((ADC1->HS & (1 << 0)) == 0) ret = kStatus_Fail; /* 如果GS寄存器的CALF位为1的话表示校准失败 */ if((ADC1->GS & (1 << 2)) != 0) ret = kStatus_Fail; return ret; } /* * @description : 获取ADC原始值 * @param : 无 * @return : 获取到的ADC原始值 */ unsigned int getadc_value(void) { /* 配置ADC通道1 */ ADC1->HC[0] = 0; /* 关闭转换结束中断 */ ADC1->HC[0] |= (1 << 0); /* 通道1 */ while((ADC1->HS & (1 << 0)) == 0); /* 等待转换完成 */ return ADC1->R[0]; /* 返回ADC值 */ } /* * @description : 获取ADC平均值 * @param times : 获取次数 * @return : times次转换结果平均值 */ unsigned short getadc_average(unsigned char times) { unsigned int temp_val = 0; unsigned char t; for(t = 0; t < times; t++){ temp_val += getadc_value(); delayms(5); } return temp_val / times; } /* * @description : 获取ADC对应的电压值 * @param : 无 * @return : 获取到的电压值,单位为mV */ unsigned short getadc_volt(void) { unsigned int adcvalue=0; unsigned int ret = 0; adcvalue = getadc_average(5); ret = (float)adcvalue * (3300.0f / 4096.0f); /* 获取计算后的带小数的实际电压值 */ return ret; }
文件bsp_blacklight.c 一共有5 个函数,首先是函数adc1ch1_init,这个是ADC1 通道1 的初始化函数,在此函数里面会初始化ADC,比如设置ADC 时钟源、设置参考电压、ADC 位数等,初始化完成以后会调用adc1_autocalibration 函数校准一次ADC。第2 个函数就是
adc1_autocalibration,这个是ADC 校准函数,在使用ADC 之前最好校准一次。第3 个函数是getadc_value,这个函数用于获取ADC 转换值,也就是读取ADCx_R0 寄存器。第4 个函数为getadc_average,这是软件平均值,也就是软件读取多次ADC 值,然后进行平均,大家也可以直接使用ADC 自带的硬件平均。最后一个函数就是getadc_volt,此函数用于将获取到的原始
ADC 值转换为对应的电压值。
最后在上一章实验的main.c 文件基础上,将main 函数改为如下内容:
#include "bsp_clk.h" #include "bsp_delay.h" #include "bsp_led.h" #include "bsp_beep.h" #include "bsp_key.h" #include "bsp_int.h" #include "bsp_uart.h" #include "bsp_lcd.h" #include "bsp_lcdapi.h" #include "bsp_rtc.h" #include "bsp_backlight.h" #include "bsp_adc.h" #include "stdio.h" /* * @description : 使能I.MX6U的硬件NEON和FPU * @param : 无 * @return : 无 */ void imx6ul_hardfpu_enable(void) { uint32_t cpacr; uint32_t fpexc; /* 使能NEON和FPU */ cpacr = __get_CPACR(); cpacr = (cpacr & ~(CPACR_ASEDIS_Msk | CPACR_D32DIS_Msk)) | (3UL << CPACR_cp10_Pos) | (3UL << CPACR_cp11_Pos); __set_CPACR(cpacr); fpexc = __get_FPEXC(); fpexc |= 0x40000000UL; __set_FPEXC(fpexc); } volatile /* * @description : main函数 * @param : 无 * @return : 无 */ int main(void) { unsigned char i = 0; unsigned int adcvalue; unsigned char state = OFF; signed int integ; /* 整数部分 */ signed int fract; /* 小数部分 */ imx6ul_hardfpu_enable(); /* 使能I.MX6U的硬件浮点 */ int_init(); /* 初始化中断(一定要最先调用!) */ imx6u_clkinit(); /* 初始化系统时钟 */ delay_init(); /* 初始化延时 */ clk_enable(); /* 使能所有的时钟 */ led_init(); /* 初始化led */ beep_init(); /* 初始化beep */ uart_init(); /* 初始化串口,波特率115200 */ lcd_init(); /* 初始化LCD */ adc1ch1_init(); /* ADC1_CH1 */ tftlcd_dev.forecolor = LCD_RED; lcd_show_string(50, 10, 400, 24, 24, (char*)"ALPHA-IMX6U ADC TEST"); lcd_show_string(50, 40, 200, 16, 16, (char*)"ATOM@ALIENTEK"); lcd_show_string(50, 60, 200, 16, 16, (char*)"2019/12/16"); lcd_show_string(50, 90, 400, 16, 16, (char*)"ADC Ori Value:0000"); lcd_show_string(50, 110, 400, 16, 16,(char*)"ADC Val Value:0.00 V"); tftlcd_dev.forecolor = LCD_BLUE; while(1) { adcvalue = getadc_average(5); lcd_showxnum(162, 90, adcvalue, 4, 16, 0); /* ADC原始数据值 */ printf("ADC orig value = %d\r\n", adcvalue); adcvalue = getadc_volt(); integ = adcvalue / 1000; fract = adcvalue % 1000; lcd_showxnum(162, 110, integ, 1, 16, 0); /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */ lcd_showxnum(178, 110, fract, 3, 16, 0X80); /* 显示电压值小数部分(前面转换为了整形显示),这里显示的就是111. */ printf("ADC vola = %d.%dV\r\n", integ, fract); delayms(50); i++; if(i == 10) { i = 0; state = !state; led_switch(LED0,state); } } return 0; }
第18 行调用函数adc1ch1_init 初始化ADC1_CH1。第30 行调用getadc_average 函数获取ADC 原始值,这里读取5 次数据然后求平均。第34 行调用getadc_volt 函数获取ADC 对应的电压值。最后将原始值和电压值都显示在LCD 上。
修改Makefile 中的TARGET 为adc,然后在在INCDIRS 和SRCDIRS 中加入“bsp/adc”,修改后的Makefile 如下:
CROSS_COMPILE ?= arm-linux-gnueabihf- TARGET ?= adc CC := $(CROSS_COMPILE)gcc LD := $(CROSS_COMPILE)ld OBJCOPY := $(CROSS_COMPILE)objcopy OBJDUMP := $(CROSS_COMPILE)objdump LIBPATH := -lgcc -L /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4 INCDIRS := imx6ul \ stdio/include \ bsp/clk \ bsp/led \ bsp/delay \ bsp/beep \ bsp/gpio \ bsp/key \ bsp/exit \ bsp/int \ bsp/epittimer \ bsp/keyfilter \ bsp/uart \ bsp/lcd \ bsp/rtc \ bsp/i2c \ bsp/ap3216c \ bsp/spi \ bsp/icm20608 \ bsp/touchscreen \ bsp/backlight \ bsp/adc SRCDIRS := project \ stdio/lib \ bsp/clk \ bsp/led \ bsp/delay \ bsp/beep \ bsp/gpio \ bsp/key \ bsp/exit \ bsp/int \ bsp/epittimer \ bsp/keyfilter \ bsp/uart \ bsp/lcd \ bsp/rtc \ bsp/i2c \ bsp/ap3216c \ bsp/spi \ bsp/icm20608 \ bsp/touchscreen \ bsp/backlight \ bsp/adc INCLUDE := $(patsubst %, -I %, $(INCDIRS)) SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S)) CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c)) SFILENDIR := $(notdir $(SFILES)) CFILENDIR := $(notdir $(CFILES)) SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o)) COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o)) OBJS := $(SOBJS) $(COBJS) VPATH := $(SRCDIRS) .PHONY: clean $(TARGET).bin : $(OBJS) $(LD) -Timx6ul.lds -o $(TARGET).elf $^ $(LIBPATH) $(OBJCOPY) -O binary -S $(TARGET).elf $@ $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis $(SOBJS) : obj/%.o : %.S $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< $(COBJS) : obj/%.o : %.c $(CC) -Wall -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< clean: rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)
第2 行修改变量TARGET 为“adc”,也就是目标名称为“adc”。
第33 行在变量INCDIRS 中添加ADC 驱动头文件(.h)路径。
第57 行在变量SRCDIRS 中添加ADC 驱动驱动文件(.c)路径。
链接脚本保持不变。
使用Make 命令编译代码,编译成功以后使用软件imxdownload 将编译完成的adc.bin 文件下载到SD 卡中,命令如下:
chmod 777 imxdownload //给予imxdownload 可执行权限,一次即可
./imxdownload adc.bin /dev/sdd //烧写到SD 卡中,不能烧写到/dev/sda 或sda1 里面!
烧写成功以后将SD 卡插到开发板的SD 卡槽中,然后复位开发板。用杜邦线将图C1.2.1中的GPIO1 引脚接到GND 上,那么此时测量到的电压肯定就是0V,如图C1.4.2.1 所示:
从图C1.4.2.1 可以看出,当GPIO1_IO01 接到GND 的时候,此时ADC 原始数值为50,换算出来的实际电压为0.045V,考虑到误差已经电源的抖动,可以认为是0V。接下来大家可以将GPIO1_IO01 引脚接到3.3V 电源引脚上,此时测量值如图C1.4.2.2 所示:
从图C1.4.2.2 可以看出,此时的ADC 原始值为3945,对应的电压值为3.18V。至此,I.MX6ULL 的ADC 就讲解完成。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。