STM32之ADC

1. ADC介绍

1.1 什么是ADC(可以理解为一个"电压表")

• 全称:Analog-to-Digital Converter,指模拟/数字转换器。

• ADC可以将引脚上连续变化的模拟电压 转换为内存中存储的数字变量,是建立模拟电路到数字电路的桥梁。

• 简单理解ADC就是一个转换器。如图:

12 位 ADC 是一种逐次逼近型模拟数字转换器 (0~4095)。它有多达18 个通道,可测量 16 个外部和 2 个内部信号源。各通道的 A/D 转换可以单次、连续、扫描或间断 模式执行。 ADC 的结果可以左对齐或右对齐 方式存储在16 位数据寄存器 **(ADC_DR)**中。

• ADC 的输入时钟不得超过 14MHz ,它是由 PCLK2经分频产生。

• STM32F103C8T6 ADC资源:ADC1 ADC2 10 个外部输入通道。

1.2 ADC工作原理(逐次逼近型),如图:

• 内部的"控制与定时"让"逐次逼近寄存器"在"D/A转换器(DAC)"产生一个模拟量与外界输入的模拟量进行比较,如果比外界的小了,那么重新生成比之前大的模拟量再次进行比较;如果比外界的大了,那么生成比之前小的模拟量进行比较,直到接近或者相等为止,最后将最接近的模拟量放进输出缓冲器中。

1.3 ADC特性参数

• ADC的性能指标:

量程 :能测量的电压范围(0-4095)(12 位的 ADC)

分辨率 :ADC能辨别最小模拟量 ,通常以输出二进制数的位数 表示,比如:8、10、12、16位等;位数越多,分辨率越高,一般来说分辨率越高,转化时间越长

转化时间 :从转换开始 到获得稳定的数字量 输出所需要的时间称为转换时间 。转换时间越长,转换结果相对越准确,但是转换速度就越慢。

1.4 ADC特性

• 12 位精度下转换速度可高达1MHZ

• ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中 ( 通常还是使用右对齐多 )

• 供电电压:VSSA :0V,VDDA :2.4V~3.6V。

• ADC输入范围:VREF- ≤ VIN ≤ VREF+,0~3.3V。

2. ADC的一些细节

2.1 输入通道

• 总共 2 个 ADC(ADC1,ADC2),每个 ADC 有 18 个转换通道: 16 个外部通道、 2 个内部通道 (温度传感器、内部参考电压),如图:

• 虽然两个ADC的IO基本一样,但是可以采用分时复用来使用,使用的好的话,甚至测量值也会更好。

• 外部的16个通道在转换时又分为规则通道和注入通道其中规则通道最多有 16 路,注入通道最多有 4

2.2 规则组/注入组

• 如图,规则组(规则通道)

• 类似于"流水线",把要测的通道放在规则组里面,按照转换顺序,一个一个进行转换。注意(通道可以重复,例如,在第一个转换中是通道1,在后面也可以再次出现)。

2.3 注入组(注入通道)

• 类似于"中断",在正常按照顺序转换时,突然插入了几个要转换的通道。注意(插入的注入组通道也可以重复)。

2.4 转换顺序

• 每个 ADC 规则通道只有一个数据寄存器16 个规则通道一起共用这个寄存器 ,所以需要指定规则转换通道的转换顺序

• 规则通道中的转换顺序 由三个寄存器控制:SQR1 SQR2 SQR3 ,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序 ,只要在对应的寄存器位SQx 中写入相应的通道,这个通道就是第x个转换。

• 理解:在三个寄存器设置在那个 通道是第几个 转换。例如SQ1 中设置通道 5第一个转换。

SQL[3:0]就是需要转换多少个通道。

• 和规则通道转换顺序的控制一样,注入通道的转换也是通过注入寄存器来控制 ,只不过只有一个 JSQR 寄存器来控制转换的顺序,控制关系如下:

• 注入序列的转换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。

• 如果设置转换通道数目小于4,例如设置为3,则转换顺序是JSQR2→JSQR3→JSQR4,而不是从JSQR1开始。

2.5 触发转换的方法

• 通过向控制寄存器ADC-CR2ADON 位写 1 来开启 ADC ,再将 SWSTART 位置 1 ,启动规则通道转换(软件)

• 也可以通过外部事件(如定时器)进行转换(硬件),如图:

2.6 转换时间

• ADC 是挂载在 APB2 总线PCLK2 )上的,经过分频器得到 ADC 时钟( ADCCLK 最高 14 MHz

• 转换时间 = 采样时间+12.5周期。(周期等于频率的倒数)。

12.5个周期是固定的 ,一般我们设置 PCLK2=72M ,经过 ADC 预分频器能分频到最大的时钟只能是12M,如果采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。但是也有些芯片可以算出最短为1us,因为最大时钟可以到14M。

2.7 中断及事件,如图:

• 注意:DMA 只适用于规则通道。

2.8 校准

ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。在校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。

• 通过设置ADC_CR2 寄存器的 CAL 位启动校准 。一旦校准结束, CAL位被硬件 复位,可以开始正常转换。建议在上电时执行一次 ADC 校准 。校准阶段结束后,校准码储存在 ADC_DR 中。

2.9 单次转换和连续转换

• 单次转换:只转换一次。

• 连续转换:转换一次之后,立马进行下一次转换。

2.10 扫描模式

• 关闭扫描模式:只转换ADC_SQRx或ADC_JSQR选中的第一个通道。

• 打开扫描模式:扫描所有被ADC_SQRx或ADC_JSQR选中的所有通道。

3. 实战单通道采集

• 配置ADC工作参数 HAL_ADC_Init()

• ADC校准 HAL_ADCEx_Calibration_Start()

• Msp初始化(NVIC,CLOCK,GPIO),HAL_ADC_MspInit()

• 配置ADC通道(转换位置和采样时间)HAL_ADC_ConfigChannel()

• 启动ADC转换HAL_ADC_Start()

• 等待规则组转换完成HAL_ADC_PollForConversion()//阻塞等待

• 获取规则通道AD转换结果 HAL_ADC_GetValue()

• adc.c

cpp 复制代码
#include "adc.h"

ADC_HandleTypeDef adc_handle = {0};
void adc_init(){
    adc_handle.Instance = ADC1;//选择的是ADC1来进行转换
    adc_handle.Init.ContinuousConvMode = DISABLE;//不连续
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;//右对齐
    adc_handle.Init.DiscontinuousConvMode = DISABLE;//间断也不开
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;//软件触发
    adc_handle.Init.NbrOfConversion = 1;//转换数量
    adc_handle.Init.NbrOfDiscConversion = 0;//间断模式下的转换数量
    adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;//不扫描
    HAL_ADC_Init(&adc_handle);
    HAL_ADCEx_Calibration_Start(&adc_handle);//校准

}

void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
    if(hadc->Instance == ADC1){
        GPIO_InitTypeDef gpio_init = {0};
        RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
        __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
        __HAL_RCC_ADC1_CLK_ENABLE();
        gpio_init.Mode = GPIO_MODE_ANALOG;//模式选择模拟输入
        gpio_init.Pin = GPIO_PIN_1;//推挽输出
        HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
        
        rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
        rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
        
        HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
        
    
    }
}

void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){//配置通道
    ADC_ChannelConfTypeDef adc_config = {0};
    adc_config.Channel =ch;
    adc_config.Rank = rank;
    adc_config.SamplingTime = stime;//转换时间
    HAL_ADC_ConfigChannel(hadc,&adc_config);//配置通道
}

uint32_t get_adc_value(uint32_t ch){
   adc_config_channel(&adc_handle,ch,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
   HAL_ADC_Start(&adc_handle);//开始转换。
   HAL_ADC_PollForConversion(&adc_handle,10);//用于在轮询模式(阻塞模式)下实现模数转换。
    //该函数会阻塞当前线程,
    //直到模数转换完成并且数据准备就绪,然后将转换结果存储在给定的变量中并返回。
   return  (uint16_t)HAL_ADC_GetValue(&adc_handle);
}

• main.c

cpp 复制代码
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"

int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    led_init();                         /* LED初始化 */
    uart1_init(115200);
    adc_init();
    printf("hello world\r\n");
    while(1)//流水灯实验
    { 
        printf("adc recv : %.2f\r\n",(float)get_adc_value(ADC_CHANNEL_1) / 4096 * 3.3);//通过轮询得方式获得转化后得结果
        delay_ms(500);
    }
}

4. ADC单通道采集 + DMA读取

• 配置ADC工作参数 HAL_ADC_Init() ADC校准 HAL_ADCEx_Calibration_Start()

• 配置DMA,将DMA与ADC句柄联系起来HAL_DMA_Init(),__HAL_LINKDMA()

• Msp初始化(NVIC,CLOCK,GPIO),HAL_ADC_MspInit()

• 配置ADC通道(转换位置和采样时间)HAL_ADC_ConfigChannel()

• 触发ADC转换,DMA传输数据HAL_ADC_Start_DMA()

• adc.c

cpp 复制代码
#include "adc.h"

ADC_HandleTypeDef adc_handle = {0};
DMA_HandleTypeDef dma_handle = {0};
void adc_init(){
    adc_handle.Instance = ADC1;
    adc_handle.Init.ContinuousConvMode = ENABLE;
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    adc_handle.Init.DiscontinuousConvMode = DISABLE;
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    adc_handle.Init.NbrOfConversion = 1;
    adc_handle.Init.NbrOfDiscConversion = 0;
    adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;
    HAL_ADC_Init(&adc_handle);
    HAL_ADCEx_Calibration_Start(&adc_handle);
    
}

void dma_init(){

    __HAL_RCC_DMA1_CLK_ENABLE();
    dma_handle.Instance = DMA1_Channel1;
    
    dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
    dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;//为什么是半字?,因为12位的ADC
    dma_handle.Init.MemInc = DMA_MINC_ENABLE;
    dma_handle.Init.Mode = DMA_CIRCULAR;//连续转化
    dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;
    dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
    HAL_DMA_Init(&dma_handle);
    
    __HAL_LINKDMA(&adc_handle,DMA_Handle,dma_handle);//ADC要和DMA链接
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
    if(hadc->Instance == ADC1){
        GPIO_InitTypeDef gpio_init = {0};
        RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
        __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
        __HAL_RCC_ADC1_CLK_ENABLE();
        gpio_init.Mode = GPIO_MODE_ANALOG;
        gpio_init.Pin = GPIO_PIN_1;//推挽输出
        HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
        
        rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
        rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
        HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
    
    }
}

void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){
    ADC_ChannelConfTypeDef adc_config = {0};
    adc_config.Channel =ch;
    adc_config.Rank = rank;
    adc_config.SamplingTime = stime;
    HAL_ADC_ConfigChannel(hadc,&adc_config);
}

void adc_dma_init(uint32_t *pdata){
    adc_init();
    dma_init();
    adc_config_channel(&adc_handle,ADC_CHANNEL_1,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
    HAL_ADC_Start_DMA(&adc_handle,pdata,1);
}

• mian.c

cpp 复制代码
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"


uint16_t pdata = 0;
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    led_init();                         /* LED初始化 */
    uart1_init(115200);
    adc_dma_init((uint32_t *)&pdata);//不用while轮询,用了DMA_CIRCULAR模式
    printf("hello world\r\n");
    while(1)//流水灯实验
    { 
        printf("adc recv : %.2f\r\n",(float)pdata / 4096 * 3.3);
        delay_ms(500);
    }
}

5. ADC多通道采集 + DMA读取

• 基于上述修改就行了

• adc.c

cpp 复制代码
#include "adc.h"

ADC_HandleTypeDef adc_handle = {0};
DMA_HandleTypeDef dma_handle = {0};
void adc_init(){
    adc_handle.Instance = ADC1;
    adc_handle.Init.ContinuousConvMode = ENABLE;
    adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    adc_handle.Init.DiscontinuousConvMode = DISABLE;
    adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    adc_handle.Init.NbrOfConversion = 4;
    adc_handle.Init.NbrOfDiscConversion = 0;
    adc_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;
    HAL_ADC_Init(&adc_handle);
    HAL_ADCEx_Calibration_Start(&adc_handle);
    
}

void dma_init(){

    __HAL_RCC_DMA1_CLK_ENABLE();
    dma_handle.Instance = DMA1_Channel1;
    
    dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
    dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    dma_handle.Init.MemInc = DMA_MINC_ENABLE;
    dma_handle.Init.Mode = DMA_CIRCULAR;
    dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;
    dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
    HAL_DMA_Init(&dma_handle);
    
    __HAL_LINKDMA(&adc_handle,DMA_Handle,dma_handle);
}

void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc){
    if(hadc->Instance == ADC1){
        GPIO_InitTypeDef gpio_init = {0};
        RCC_PeriphCLKInitTypeDef rcc_pct_init = {0};
        __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
        __HAL_RCC_ADC1_CLK_ENABLE();
        gpio_init.Mode = GPIO_MODE_ANALOG;
        gpio_init.Pin = GPIO_PIN_0 | GPIO_PIN_1 |GPIO_PIN_2 |GPIO_PIN_3 ;//推挽输出
        HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
        
        rcc_pct_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
        rcc_pct_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
        HAL_RCCEx_PeriphCLKConfig(&rcc_pct_init);
    
    }
}

void adc_config_channel(ADC_HandleTypeDef* hadc,uint32_t ch,uint32_t rank,uint32_t stime){
    ADC_ChannelConfTypeDef adc_config = {0};
    adc_config.Channel =ch;
    adc_config.Rank = rank;
    adc_config.SamplingTime = stime;
    HAL_ADC_ConfigChannel(hadc,&adc_config);
}

void adc_dma_init(uint32_t *pdata){
    adc_init();
    dma_init();
    adc_config_channel(&adc_handle,ADC_CHANNEL_0,ADC_REGULAR_RANK_1,ADC_SAMPLETIME_239CYCLES_5);
    adc_config_channel(&adc_handle,ADC_CHANNEL_1,ADC_REGULAR_RANK_2,ADC_SAMPLETIME_239CYCLES_5);
    adc_config_channel(&adc_handle,ADC_CHANNEL_2,ADC_REGULAR_RANK_3,ADC_SAMPLETIME_239CYCLES_5);
    adc_config_channel(&adc_handle,ADC_CHANNEL_3,ADC_REGULAR_RANK_4,ADC_SAMPLETIME_239CYCLES_5);
    HAL_ADC_Start_DMA(&adc_handle,pdata,4);
}

• main.c

cpp 复制代码
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "adc.h"


uint16_t pdata[4] = {0};
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    led_init();                         /* LED初始化 */
    uart1_init(115200);
    adc_dma_init((uint32_t *)pdata);
    printf("hello world\r\n");
    while(1)//流水灯实验
    { 
        printf("adc recv1 : %.2f\r\n",(float)pdata[0] / 4096 * 3.3);
        printf("adc recv2 : %.2f\r\n",(float)pdata[1] / 4096 * 3.3);
        printf("adc recv3 : %.2f\r\n",(float)pdata[2] / 4096 * 3.3);
        printf("adc recv4 : %.2f\r\n\r\n",(float)pdata[3] / 4096 * 3.3);
        delay_ms(500);
    }
}
相关推荐
蓬荜生灰2 小时前
STM32(8)-- 自己创建库函数
stm32·单片机·嵌入式硬件
qq_401700412 小时前
基于CN3762 PWM 降压模式双节18650锂电池充电管理芯片
单片机·嵌入式硬件
仰泳之鹅2 小时前
【杂谈】针对Cortex M4内核使用Systick进行延时
单片机·嵌入式硬件
济6172 小时前
linux 系统移植(第十五期)---Linux 内核移植(4)-- 修改 EMMC 驱动--- Ubuntu20.04
linux·嵌入式硬件
松涛和鸣3 小时前
62、IIC通信解析
服务器·arm开发·单片机·嵌入式硬件·html
2501_927773073 小时前
嵌入式——I.MX6ULL裸机环境配置
c语言·嵌入式硬件
yuan199973 小时前
STM32F103CBT6驱动AW9523B实现呼吸灯实例
stm32·单片机·嵌入式硬件
三伏5223 小时前
Cortex-M3权威指南Cn第八章——笔记
笔记·单片机·嵌入式硬件·cortex-m3
学工科的皮皮志^_^3 小时前
以太网PHY芯片学习RTF8211
经验分享·嵌入式硬件·学习·以太网·phy