ADC基础知识参考:
阅读参考手册
自行查阅手册。
理论部分可直接参考:STM32—ADC详解_Aspirant-GQ的博客-CSDN博客_stm32adc
以下引用部分内容:
12位ADC是一种逐次逼近型模拟数字转换器。它有多达18个通道,可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。
ADC的输入时钟不得超过14MHz,其时钟频率由PCLK2分频产生。
功能框图可以大体分为7部分,下面一一讲解:
1.电压输入范围
ADC所能测量的电压范围就是VREF- ≤ VIN ≤ VREF+,把 VSSA 和 VREF-接地,把 VREF+和 VDDA 接 3V3,得到ADC 的输入电压范围为: 0~3.3V。为了提高转换的精确度,ADC使用一个独立的电源供电,过滤和屏蔽来自印刷电路板上的毛刺干扰。
关于参考电压:ADC参考电压有多重要?
2.输入通道
ADC的信号输入就是通过通道来实现的,信号通过通道输入到单片机中,单片机经过转换后,将模拟信号输出为数字信号。STM32中的ADC有着18个通道,其中16个外部通道,2个内部通道。
ADCx_IN0 ~ ADCx_IN15是外部GPIO端口复用。
温度传感器和内部参考电压是内部输入通道。
3.规则通道和注入通道
外部的16个通道在转换时又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路(注入通道貌似使用不多),下面简单介绍一下俩种通道:
规则通道
规则通道顾名思义就是,最平常的通道、也是最常用的通道,规规矩矩的按照我们设定的转换顺序进行转换的通道。平时的ADC转换都是用规则通道实现的。
注入通道
注入通道是相对于规则通道的,注入通道可以在规则通道转换时,强行插入转换,相当于一个“中断通道”吧。当有注入通道需要转换时,规则通道的转换会停止,优先执行注入通道的转换,当注入通道的转换执行完毕后,再回到之前规则通道进行转换。换个角度说,注入通道只有在规则通道存在的情况下才会存在。举个简单的例子:
假设你在家里的宅院内放了5个温度探头,室内放了3个温度探头;你需求时刻监督室外温度,但偶尔你也想看看室内的温度。此时你能够运用规则通道组循环扫描室外的5个探头并显现AD转化成果,当你想看室内温度时,经过一个按钮发动注入转化组(3个室内探头)并暂时显现室内温度,当你放开这个按钮后,体系又会回到规矩通道组持续检测室外温度。
显然,测量并显现室内温度的进程中止了测量并显现室外温度的进程。
在程序规划上,能够在初始化阶段就设置好不同的转化组,体系运转中不用再改变转化的设置,然后实现两类任务互不干扰和快速切换的目的。
上述例子由于速度较慢,不能彻底体现区分规矩组和注入组带来的优点,但在工业应用领域中有许多检测探头需要快速处理,这样对AD转化进行分组将简化处理程序并加快处理速度。
4.触发源
ADC转换的输入、通道、转换顺序都已经说明了,但ADC转换是怎么触发的呢?就像通信协议一样,都要规定一个起始信号才能传输信息,ADC也需要一个触发信号来实行模/数转换。
其一就是通过直接配置寄存器触发,通过配置控制寄存器CR2的ADON位,写1时开始转换,写0时停止转换。在程序运行过程中只要调用库函数,将CR2寄存器的ADON位置1就可以进行转换,比较好理解。
另外,还可以通过内部定时器或者外部IO触发转换,也就是说可以利用内部时钟让ADC进行周期性的转换,也可以利用外部IO使ADC在需要时转换,具体的触发由控制寄存器CR2决定。
5.转换时间
还有一点,就是转换时间的问题,ADC的每一次信号转换都要时间,这个时间就是转换时间,转换时间由输入时钟和采样周期来决定。
输入时钟
由于ADC在STM32中是挂载在APB2总线上的,所以ADC得时钟是由PCLK2(72MHz)经过分频得到的,分频因子由 RCC 时钟配置寄存器RCC_CFGR 的位 15:14 ADCPRE[1:0]设置,可以是 2/4/6/8 分频,一般配置分频因子为8,即8分频得到ADC的输入时钟频率为9MHz。
采样周期
采样周期是确立在输入时钟上的,配置采样周期可以确定使用多少个ADC时钟周期来对电压进行采样,采样的周期数可通过 ADC采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0]位设置,ADC_SMPR2 控制的是通道 0~9, ADC_SMPR1 控制的是通道 10~17。每个通道可以配置不同的采样周期,但最小的采样周期是1.5个周期,也就是说如果想最快时间采样就设置采样周期为1.5.
转换时间
转换时间=采样时间+12.5个周期
12.5个周期是固定的,一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。6.数据寄存器
转换完成后的数据就存放在数据寄存器中,但数据的存放也分为规则通道转换数据和注入通道转换数据的。
规则数据寄存器
规则数据寄存器负责存放规则通道转换的数据,通过32位寄存器ADC_DR来存放。
当使用ADC独立模式(也就是只使用一个ADC,可以使用多个通道)时,数据存放在低16位中,当使用ADC多模式时高16位存放ADC2的数据。
需要注意的是ADC转换的精度是12位,而寄存器中有16个位来存放数据,所以要规定数据存放是左对齐还是右对齐。
当同一个ADC但是使用多个通道转换数据时,会产生多个转换数据,然鹅数据寄存器只有一个,多个数据存放在一个寄存器中会覆盖数据导致ADC转换错误,所以我们经常在一个通道转换完成之后就立刻将数据取出来,方便下一个数据存放。一般开启DMA模式将转换的数据,传输在一个数组中,程序对数组读操作就可以得到转换的结果。
注入数据寄存器
注入通道转换的数据寄存器有4个,由于注入通道最多有4个,所以注入通道转换的数据都有固定的存放位置,不会跟规则寄存器那样产生数据覆盖的问题。
ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由ADC_CR2 的 11 位 ALIGN 设置。
7.中断从框图中可以知道数据转换完成之后可以产生中断,有三种情况:
规则通道转换完成中断
规则通道数据转换完成之后,可以产生一个中断,可以在中断函数中读取规则数据寄存器的值。这也是单通道时读取数据的一种方法。
注入通道转换完成中断
注入通道数据转换完成之后,可以产生一个中断,并且也可以在中断中读取注入数据寄存器的值,达到读取数据的作用。
模拟看门狗事件
当输入的模拟量(电压)不在阈值范围内就会产生看门狗事件,就是用来监视输入的模拟量是否正常。
当然,在转换完成之后也可以产生DMA请求,从而将转换好的数据从数据寄存器中读取到内存中。
注意:
同时只能测一路通道,通过扫描的形式来实现多路输入;
同一个通道在同一时间只能属于一个ADC,即三个ADC不能同时作用于同一个通道;
规则通道就是常规通道用法,注入通道类似于抢占;
连续模式一般要配合DMA,要不然CPU一直处在中断模式;
ADC开关控制
通过设置 ADC_CR2 寄存器的 ADON 位可给 ADC 上电。当第一次设置 ADON 位时,它将 ADC 从断电状态下唤醒。ADC 上电延迟一段时间后 (t STAB ) ,再次设置 ADON 位时开始进行转换。![]()
通过清除ADON位可以停止转换,并将ADC置于断电模式。在这个模式中,ADC几乎不耗电(仅几个μA)。
硬件原理图
这里用了个NTC电阻RT1,温度的改变会引起电阻值的改变,从而改变A点的电位,R11是一个分压电阻。当温度变高(可用手触摸使得温度升高),RT1阻值降低,分压也相应变小,从而使得A点电位变低。
则根据以上电路可知,当温度升高,PC3端口读到的电压会降低。
配置MX
这里选择用ADC1的通道13,即IN13。
注:因为规则转换共用一个数据寄存器,虽然连续转换更接近我们的要求,不过连续转换可能来不及读数据,所以采用默认的单次转换。把数据读出来后,再开启读取。
配置时钟,不能超过14MHz
确认无误后,点击生成代码。
程序编写
ADC相关的库函数:
/* Blocking mode: Polling */ HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc); HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc); HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout); HAL_StatusTypeDef HAL_ADC_PollForEvent(ADC_HandleTypeDef* hadc, uint32_t EventType, uint32_t Timeout); /* Non-blocking mode: Interruption */ HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc); HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc); /* Non-blocking mode: DMA */ HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length); HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc); /* ADC retrieve conversion value intended to be used with polling or interruption */ uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc); /* ADC IRQHandler and Callbacks used in non-blocking modes (Interruption and DMA) */ void HAL_ADC_IRQHandler(ADC_HandleTypeDef* hadc); void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc); void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc); void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef* hadc); void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc);
ADC的三种⼯作⽅式及优缺点
1.查询模式:查询模式下,占⽤CUP时间较多,cup效率较低。
2.中断模式:相⽐查询模式⼤⼤释放了cup,提⾼了cup的利⽤率。
3.DMA模式:该模式下基本不占⽤cup,能直接将ADC采集的数据存储到存储器。
这里,使用查询模式,流程如下:
①开启ADC:调⽤HAL_ADC_Start(),开启ADC。
②等待EOC标志位:调⽤查询函数HAL_ADC_PollForConversion(),等待ADC转化结束,CUP在这段时间内不能⼲其他事,所以查询 ⽅式降低了CUP的使⽤率。
③读取寄存器数据:调⽤HAL_ADC_GetValue()。
添加两个文件,useradc.c和useradc.h
useradc.h
#ifndef _USERADC_H_ #define _USERADC_H_ #include <stdint.h> //确定要实现的led功能 typedef struct { float (*getVol)(void); } useradc_handler; //将结构体声明出去 extern useradc_handler useradcHandler; #endif
useradc.c
#include "myapplication.h" static float GetVol(void); useradc_handler useradcHandler = { GetVol }; static float GetVol(void) { uint32_t adcResultNum = 0; HAL_ADC_Start(&hadc1); if(HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { adcResultNum = HAL_ADC_GetValue(&hadc1); } return 3.3/4096*adcResultNum; }
调用
static void Run(void) { HAL_Delay(1000); printf("voltage is %f\n\r", useradcHandler.getVol()); }
可以看到,在串口中输出了电压值
当用吹风机给NTC加热时,电压持续下降
补充
单片机ADC只能对电压进行直接采集,并且无法直接采集负电压。 负电压,可以采用电压抬升的方法,先抬高到正电压,再输入到单片机的AD引脚。 抬升的方法,可以采用运放,搭建一个加法器来实现。
ADS8331/8332的使用
引脚图如下:
驱动程序如下:
这里是用蓝牙spi实现的。
重点看时序,spi部分可以替换成STM32等单片机的。
static void SPI_Init(void) { nrf_gpio_cfg_output(SPI_CS_PIN); //片选引脚 nrf_gpio_pin_set(SPI_CS_PIN); //初始置高电平 nrf_gpio_cfg_output(AD_CONVEST); //转换开始引脚 nrf_gpio_pin_set(AD_CONVEST); //初始置高电平 nrf_gpio_cfg_output(AD_RST); //复位引脚 nrf_gpio_pin_clear(AD_RST); //初始为低电平 nrf_gpio_cfg_input(AD_EOC, NRF_GPIO_PIN_NOPULL); //转换完成引脚 //使用默认配置参数初始化SPI配置结构体 nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG; //重写SPI信号连接的引脚配置 spi_config.ss_pin = NRF_DRV_SPI_PIN_NOT_USED; spi_config.miso_pin = SPI_MISO_PIN; //MISO引脚 spi_config.mosi_pin = SPI_MOSI_PIN; //MOSI引脚 spi_config.sck_pin = SPI_SCK_PIN; //时钟引脚 //初始化SPI APP_ERROR_CHECK(nrf_drv_spi_init(&spi1,&spi_config,NULL,NULL)); } //配置ADS8331为人工转换模式 static void ADS8331CfgManMode(void) { //要发送的数据 spi_tx_buf[0] = ((ADSCMF_CONFIG >> 8) & 0xff); spi_tx_buf[1] = (ADSCMF_CONFIG & 0xff); /* Reset ADS8331 */ AD_RST_HIGH; nrf_delay_us(10); AD_RST_LOW; nrf_delay_us(10); AD_RST_HIGH; nrf_delay_us(10); /* 拉低片选,使能芯片 */ SPIFlash_CS_LOW; nrf_delay_us(10); /* 启动数据传输,配置为人工转换模式 */ APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi1, spi_tx_buf, 2, spi_rx_buf, 2)); /* 拉高片选,释放芯片 */ SPIFlash_CS_HIGH; nrf_delay_us(10); } //读取通道数据 //参数1是要读取的通道 //参数2是要存放读取数据的指针 static void ReadAds8332Data(uint8_t ChannelNumIn, uint16_t *pDataBuffIn) { uint8_t ChannelNum = ChannelNumIn;//指定采集通道号 uint16_t *pEegDataBuffer = NULL; uint32_t usWaitTime = 0; uint16_t ADCValue = 0; pEegDataBuffer = pDataBuffIn; /* 拉低片选,使能芯片 */ SPIFlash_CS_LOW; /* 发送通道号 */ memset(spi_tx_buf,0,sizeof(spi_tx_buf)); memset(spi_rx_buf,0,sizeof(spi_rx_buf)); spi_tx_buf[0] = (ChannelNum << 4); spi_tx_buf[1] = 0x00; /* 启动数据传输,发送通道号*/ APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi1, spi_tx_buf, 2, spi_rx_buf, 2)); /* 拉高片选,释放芯片 */ SPIFlash_CS_HIGH; nrf_delay_us(1); /* 启动转换,要求低电平保持至少40ns */ nrf_gpio_pin_clear(AD_CONVEST); nrf_delay_us(1); nrf_gpio_pin_set(AD_CONVEST); /* 等待EOC管脚变为高电平 */ usWaitTime = 0; while(0 == nrf_gpio_pin_read(AD_EOC)) { usWaitTime++; if(usWaitTime > 10000) { return; } } /* 检测到EOC管脚高电平时,至少延时20ns,再拉低CS,此处延时1us */ nrf_delay_us(5); /* 拉低片选,使能芯片 */ SPIFlash_CS_LOW; /* 发送读取转换的数据指令 */ spi_tx_buf[0] = ((R_DATA>>8) & 0xff); spi_tx_buf[1] = R_DATA & 0xff; APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi1, spi_tx_buf, 2, spi_rx_buf, 2)); /* 拉高片选,释放芯片 */ SPIFlash_CS_HIGH; ADCValue = spi_rx_buf[0]; ADCValue <<= 8; ADCValue += spi_rx_buf[1]; *pEegDataBuffer = ADCValue; }
对应头文件部分内容如下:
//SPI引脚定义 #define SPI_CS_PIN 12 #define SPI_SCK_PIN 11 #define SPI_MISO_PIN 6 #define SPI_MOSI_PIN 13 #define AD_EOC 8 #define AD_CONVEST 2 #define AD_RST 7 //采集使能信号引脚定义 #define EEG_PWR_ON_PIN 14 #define SPIFlash_CS_LOW nrf_gpio_pin_clear(SPI_CS_PIN) //片选输出低电平:使能芯片 #define SPIFlash_CS_HIGH nrf_gpio_pin_set(SPI_CS_PIN) //片选输出高电平:取消片选 #define AD_RST_HIGH nrf_gpio_pin_set(AD_RST) //AD复位引脚输出高电平 #define AD_RST_LOW nrf_gpio_pin_clear(AD_RST) //AD复位引脚输出低电平 #define EEG_DATA_AVR_COUNT 5 #define R_DATA ( 0xD000 ) #define W_CFR ( 0xE000 ) #define MANUALCHANNEL ( 0x0000 ) #define AUTOCHANNEL ( 0x0001 << 11 ) #define USESCLK ( 0x0000 ) #define USEOSC ( 0x0001 << 10 ) #define AUTOTRIGGER ( 0x0000) #define MANUALTRIGGER ( 0x0001 << 9 ) #define RATE500K ( 0x0000 ) #define RATE250K ( 0x0001 << 8 ) #define EOCINTHIGH ( 0x0000 ) #define EOCINTLOW ( 0x0001 << 7 ) #define PIN10ASINT ( 0x0000 ) #define PIN10ASEOC ( 0x0001 << 6 ) #define PIN10ASCDI ( 0x0000 ) #define PIN10ASINTEOC ( 0x0001 << 5 ) #define AUTONAPENABLED ( 0x0000 ) #define AUTONAPDISABLED ( 0x0001 << 4 ) #define NAPENABLED ( 0x0000 ) #define NAPDISABLED ( 0x0001 << 3 ) #define DEEPENABLED ( 0x0000 ) #define DEEPDISABLED ( 0x0001 << 2 ) #define TAGDISABLED ( 0x0000 ) #define TAGENABLED ( 0x0001 << 1 ) #define SYSRESET ( 0x0000 ) #define SYSOPERATION ( 0x0001 << 0 ) /* 自动转化模式 */ #define AUTOMODE ( 0x00) #if AUTOMODE #define ADSCMF_CONFIG (W_CFR \ |AUTOCHANNEL \ |USEOSC \ |AUTOTRIGGER \ |RATE250K \ |EOCINTLOW \ |PIN10ASEOC \ |PIN10ASINTEOC \ |AUTONAPDISABLED \ |NAPDISABLED \ |DEEPDISABLED \ |TAGDISABLED \ |SYSOPERATION ) #endif /* 人工转化模式 */ #define MANMODE ( 0x01) #if MANMODE #define ADSCMF_CONFIG (W_CFR \ |MANUALCHANNEL \ |USEOSC \ |MANUALTRIGGER \ |RATE250K \ |EOCINTLOW \ |PIN10ASEOC \ |PIN10ASINTEOC \ |AUTONAPDISABLED \ |NAPDISABLED \ |DEEPDISABLED \ |TAGDISABLED \ |SYSOPERATION ) #endif #define SPI_SCK_1 nrf_gpio_pin_set(SPI_SCK_PIN) /* SCK = 1 */ #define SPI_SCK_0 nrf_gpio_pin_clear(SPI_SCK_PIN) /* SCK = 0 */ #define SPI_MOSI_1 nrf_gpio_pin_set(SPI_MOSI_PIN) /* MOSI = 1 */ #define SPI_MOSI_0 nrf_gpio_pin_clear(SPI_MOSI_PIN) /* MOSI = 0 */ #define SPI_READ_MISO nrf_gpio_pin_read(SPI_MISO_PIN) /* 读MISO口线状态 */ #define Dummy_Byte 0xFF //读取时MISO发送的数据,可以为任意数据
如何使用AD采集弱信号?
考虑AD的分辨率是多少
假如AD的位数是16位,参考电压范围是0—3.3V,那么分辨率是3.3/65536,即0.00005035400390625V,也就是说,最小能采集到的电压值是51uV,当AD里面的值为1的时候,就表示最低的51uV电压。
可是有一种需求,那就是生活中有很多信号很微弱,比如人体的肌电信号,甚至能低到1uV。
难道这种信号就采集不到了?
当然不会。
这时候,就首先需要用硬件放大电路将微弱信号放大,比如放大100倍,那么,实际1uV的肌电信号,到AD输入口的时候,就是100uV了,这样在AD的分辨率识别能力之内,就能采集到了。
当我AD里的数据为2时,我们就知道表示100uV。
之后,要在软件里要将硬件的放大给还原到原始数据,硬件放大了100倍,那么软件就要再缩小100倍,就能还原到原来的1uV电压了。问题:原始信号——AD分辨率达不到X;
解决办法:原始信号——硬件放大到AD的分辨能力之内√——AD采集——软件缩小还原到原始值;信号如果太弱,其实很容易收到干扰。
所以,放大倍数也不要太小。再延伸一下,如果我需要采集电压值的范围在5uV—100000uV之内,而且,上位机显示的时候带两位小数点,能实现吗?
首先想一想,如果上位机需要显示两位小数点,也就是说,分辨率要达到0.01uV才行。
16位AD分辨率为51uV;
24位AD分辨率为0.20uV;
32位AD的分辨率为0.0008uV;
可见,只有32位的AD可以直接满足。那么,如果条件限制只能使用16位的AD呢?
也就是说,需要放大51/0.01=5100倍才能刚刚好够用。
如果都放大了5100倍,最低的是能达到,但是最大值就到了100000uV*5100,即510V,可见,肯定不行。
所以,只能看看能不能硬件实现分段放大。
软件实现肯定不行,因为软件首先就要把值读出来才能判断,但是软件根本就读不到,也就谈不上判断了。如果是5uV—1000uV并且带两位小数呢?
还是一样,分辨率要达到0.01,需要放大5100倍才能被AD采集到。
看最大值1000uV*5100=5100000uV,也就是5.1V,也够呛。