STM32实战总结:HAL之ADC

ADC基础知识参考:

51单片机外设篇:ADC_路溪非溪的博客-CSDN博客_51单片机 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,也够呛。

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值