[003] [蓝桥杯物联网] ADC硬件框架与HAL库轮询/中断/DMA方式编程

蓝桥杯
Contents
ADC硬件框架
通道选择
ADC转换模式
ADC硬件框图分析
主要寄存器
HAL库ADC编程
轮询方式
中断方式
DMA方式
总结

MCU型号:STM32L071KBU
SDK:HAL库
工具:CubeMX + MDK

1 ADC硬件框架

下面为STM32F1/4系列的ADC硬件框架(STM32L0系列ADC框架相对比较简单)

12位ADC是一种逐次逼近型模拟数字转换器(successive approximation analog-to-digital converter),它有多达18个通道,可测量16个外部和2个内部信号源。(L0系列有19个通道,其中3个内部信号源),各通道的A/D转换可以单次连续扫描间断模式执行。ADC的结果可以左对齐右对齐方式存储在16位数据寄存器中。

STM32F10x系列芯片ADC通道和引脚对应关系 :

image-20220401020011718

1.1 通道选择

有16个复用通道,可以将转换分为两组:规则通道组和注入通道组。规则通道至多16个,注入通道至多4个。

  • 规则通道组:相当于正常运行的程序

  • 注入通道组:注入通道可以打断规则通道,如果在规则通道转换过程中,有注入通道进行转换,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。(类似中断打断正常运行的程序)

image-20220401021650141

1.2 ADC转换模式

  • 单次转换模式:ADC只执行一次转换(适用于规则通道和注入通道)

  • 连续转换模式:当前面的ADC转换结束后,立马启动下一次转换

  • 扫描模式(多通道使用):ADC扫描所有选中的规则通道和注入通道,在每个组的每个通道上执行单次转换。在每个转换结束时,这一组的下一个通道被自动转换。如果设置了CONT位(开启了连续转换模式),转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。(所有通道转换完毕才会产生相应中断,可通过DMA读取)

  • 间断(不连续)模式:每触发一次,转换一个通道。在所选转换通道循环,由触发信号启动新一轮的转换,直到转换完成为止。

扫描模式是一次对所有选中的通道进行转换,比如开了ch0,ch1,ch4,ch5。 ch0转换完以后就会自动转换通道1,4,5直到转换完这个过程不能被打断。如果开启了连续转换模式,则会在转换完ch5之后开始新一轮的转换。

间断模式是对扫描模式的一种补充,它可以把0,1,4,5这四个通道进行分组。可以分成0,1一组,4,5一组。也可以每个通道单独配置为一组。这样每一组转换之前都需要先触发一次。

ADC单通道:

只进行一次ADC转换:配置为“单次转换模式”,扫描模式关闭。ADC通道转换一次后,就停止转换。等待再次使能后才会重新转换

进行连续ADC转换:配置为“连续转换模式”,扫描模式关闭。ADC通道转换一次后,接着进行下一次转换,不断连续。

ADC多通道:

只进行一次ADC转换:配置为“单次转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,就停止转换。等待再次使能后才会重新转换

进行连续ADC转换:配置为“连续转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,接着进行下一次转换,不断连续。

因此,多通道必须使能扫描模式

1.3 ADC硬件框图分析

STM32F4系列:

  1. ADC电压输入范围

image-20220401025107859

ADC一般用于采集小电压,其输入值不能超过VDDA,即ADC输入范围:VREF- ≤ VIN ≤ VREF+

一般把VSSA和VREF- 接地, VREF+ 和 VDDA接3V3,那么ADC的输入范围是0~3.3V。

  1. ADC输入通道

ADCx_INT0-ADCx_INT15 对应三个ADC的16个外部通道,进行模拟信号转换。此外,还有两个内部通道:温度检测或者内部电压检测

  1. 注入通道与规则通道

参看【通道选择】。

  1. ADC时钟

STM32L0系列ADC时钟为HSI时钟(16M),STM32F1/4系列时钟为经可编程预分频器分频的 APB2 时钟, 分频因子由RCC_CFGR的ADCPRE[1:0]配置,可配置2/4/6/8分频,对F1/4ADC的时钟最好不超过14M。

ADC采样时间的计算公式:T = 采样时间 + 12.5个周期,其中1周期为1/ADCCLK

例如,ADC_CLK = 16 MHz 且采用时间为 1.5 ADC时钟周期:采样时间= 1.5 + 12.5 = 14 ADC 时钟周期 = 0.875 μs

  1. 外部触发转换

ADC 转换可以由ADC 控制寄存器2: ADC_CR2ADON 位来控制,写1 的时候开始转换,写0 的时候停止转换。(L0系列由ADC_CRADSTART位控制)

除了ADC_CR2寄存器的ADON位控制转换的开始与停止,还可以支持外部事件触发转换(比如定时器捕捉、EXTI线),包括内部定时器触发外部IO触发。具体的触发源由ADC_CR2EXTSEL[2:0]位(规则通道触发源 )和 JEXTSEL[2:0]位(注入通道触发源)控制。

  1. 中断

STM32F1:

image-20220401031222857

STM32F4:

image-20220401031349341

STM32L0:

image-20220401024222963

分别为:校准结束、ADC准备就绪、转换结束、转换序列结束、模拟看门狗被置位、采样结束、溢出

  1. 模拟看门狗

当被ADC转换的模拟电压值低于低阈值或高于高阈值时,便会产生中断。阈值的高低值由ADC_LTRADC_HTR寄存器配置,防止读取到的电压值超量程或者低于量程。

image-20220401031650681

  1. DMA

规则和注入通道转换结束后会产生DMA请求,用于将转换好的数据传输到内存。

  • DMA与单次转换模式:在这种模式下,每当有新的转换数据字可用时,ADC都会生成一个DMA传输请求,并且一旦DMA传输到最后一个数据时,ADC就会停止生成DMA请求,即使转换已经再次启动。当DMA传输完成时:

    • ADC数据寄存器内容被冻结

    • 任何正在进行的转换将被中止,其部分结果将被丢弃

    • 没有向DMA发送新的请求,若仍启动了转换,这将避免溢出错误(overrun error)

    • 扫描序列停止和复位

    • DMA复位

  • DMA与连续转换模式:在这种模式下,每当数据寄存器中有一个新的转换数据字可用时,ADC就会生成一个DMA传输请求,即使DMA已经到达了最后一次DMA传输。 这允许DMA以循环模式配置,以处理连续模拟输入数据流。

: 只有ADC1和ADC3拥有DMA功能。由ADC2转化的数据可以通过双ADC模式,利用ADC1的DMA功能传输。(L0系列只有ADC1)

1.4 主要寄存器

STM32L0系列的主要寄存器:
在这里插入图片描述

2 HAL库ADC编程

MCU:STM32L071KB

2.1 轮询方式

2.1.1 ADC轮询函数

// 轮询方式开启ADC (单次转换模式每次读完都需要重新开启)
HAL_ADC_Start(ADC_HandleTypeDef* hadc);
// 轮询方式关闭ADC
HAL_ADC_Stop(ADC_HandleTypeDef* hadc);
// 轮询方式等待规则组转换结束
HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);
// 轮询方式等待外部事件触发
HAL_ADC_PollForEvent(ADC_HandleTypeDef* hadc, uint32_t EventType, uint32_t Timeout);

ADC数值读取(适用于轮询和中断

uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc);

ADC校准函数

AL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc, uint32_t SingleDiff)

SingleDiff:单端或差分输入选择;可选参数只有1个:ADC_SINGLE_ENDED单端

2.1.2 单通道 单次

在这里插入图片描述
ADC设置(ADC_Settings

  • Clock Prescaler时钟分频系数:同步时钟 / 2
  • Resolution 分辨率:12位
  • Data Alignment 数据对齐方式:右对齐
  • Scan Direction扫描方向:向前
  • Continuous Conversion Mode 连续转换模式:不使用
  • Discontinuous Conversion Mode间断(不连续)模式:不使用
  • DMA Continuous Requests DMA连续请求:不使用
  • End Of Conversion Selection结束转换选择:结束单次转换
  • Overrun behaviour溢出行为:保留溢出数据
  • Low Power Auto Wait低电压自动等待:不使用
  • Low Frequency Mode低频率模式:不使用
  • Auto off自动关闭:不使用
  • Oversampling Mode过采样模式:不使用

ADC规则转换模式(ADC_Regular_ConversionMode

  • Sampling Time采样时间:1.5个周期
  • External Trigge rConversion Source 外部触发转换源:由软件启动定时器转换
  • External Trigge rConversion Edge 外部转换信号触发边沿:无

WatchDog 模拟看门狗

  • Enable Analog WatchDog Mode使能模拟看门狗:不使能
uint16_t adc_read(ADC_HandleTypeDef* hadc) 
{
    
    
    uint32_t adc_value = 0;

    for (int i = 0; i < 8; i++)
    {
    
    
        HAL_ADC_Start(hadc);
        HAL_ADC_PollForConversion(hadc, 50);
        adc_value += HAL_ADC_GetValue(hadc);
    }

    return adc_value / 8;
}

单次转换模式,每次启动ADC转换一次数据,启动转换后等待转换完成,完成后读取数值,ADC转换会自动停止,下次采集需再次手动开启。

2.1.3 单通道 连续

image-20220401161315096

hadc.Init.ContinuousConvMode = ENABLE;
uint16_t adc_read(ADC_HandleTypeDef* hadc) 
{
    
    
    uint32_t adc_value = 0;
    HAL_ADC_Start(hadc);
    for (int i = 0; i < 8; i++)
    {
    
    
        HAL_ADC_PollForConversion(hadc, 50);
        adc_value += HAL_ADC_GetValue(hadc);
    }
    HAL_ADC_Stop(hadc);
    return adc_value / 8;
}

连续转换模式,只要启动ADC后就会不停的工作,读取完数值可采用HAL_ADC_Stop函数手动停止ADC。(当然也可以不停止,不过这不满足低功耗设计要求)

2.1.4 多通道 扫描

在这里插入图片描述

多通道扫描模式是默认开启的,同时使用连续转换。在扫描模式下,会先采集CH8,然后采集CH9(前向扫描);又因为使能了连续转换模式,所以在本轮序列采集完后,会自动从CH8继续开始采集。

// PB0-RP2    ------> ADC_IN8
// PB1-RP1    ------> ADC_IN9
uint16_t adc_value[2];
void adc_read(ADC_HandleTypeDef* hadc) 
{
    
    
    HAL_ADC_Start(hadc);
    if(HAL_ADC_PollForConversion(hadc, 20) == HAL_OK)
        adc_value[0] = HAL_ADC_GetValue(hadc);    // ADC_IN8

    if(HAL_ADC_PollForConversion(hadc, 20) == HAL_OK)
        adc_value[1]  = HAL_ADC_GetValue(hadc);   // ADC_IN9
    HAL_ADC_Stop(hadc);
}

但这是有问题的,最后OLED显示结果(两数值均为RP1采集值):
在这里插入图片描述

RP2 =  adc_value[0] * 3.3f / 4095.f;
RP1 =  adc_value[1] * 3.3f / 4095.f;

下面分析源码:

  • HAL_ADC_PollForConversionimage-20220401174647378
    在没有使能DMA情况下(CFGR1DMAEN位没置位),则tmp_Flag_EOC = (ADC_FLAG_EOC | ADC_FLAG_EOS),其EOC为单次转换结束标志,EOS为转换序列结束标志,即需要等所有通道转换结束后才会结束等待,返回HAL_OK

  • HAL_ADC_GetValue

uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc)
{
    
     
	return hadc->Instance->DR;
}

DR寄存器为最后一个转换通道的转换结果。即此处ADC_CH9通道的数值,所以最后采集的数值均为RP1电位器的ADC转换数值。

【结论】STM32L0系列HAL中ADC轮询方式无法实现多通道采集

那么直接操作寄存器吧:

void adc_read(ADC_HandleTypeDef* hadc) 
{
    
    
    hadc->Instance->CR |= ADC_CR_ADSTART;
    while ((hadc->Instance->ISR & ADC_FLAG_EOC) == 0);
    adc_value[0] = hadc->Instance->DR;    // ADC_IN8
    while ((hadc->Instance->ISR & ADC_FLAG_EOS) == 0);
    adc_value[1] = hadc->Instance->DR;    // ADC_IN9
    hadc->Instance->CR |= ADC_CR_ADSTP;         // 仅在ADC_CR_ADSTART=1且ADDIS=0时写入有效
}

在这里插入图片描述
虽然可以正常读取,但有时RP1的数值会跳变为RP2的数值,原因就是ADC转换太快了,可能执行到adc_value[0] = hadc->Instance->DR时,ADC单次扫描已结束了,DR寄存器为ADC_IN9的值。

后面采用DMA+连续扫描模式解决

2.2 中断方式

2.2.1 ADC中断函数

// 中断方式开启ADC
HAL_StatusTypeDef    HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
// 中断方式关闭ADC
HAL_StatusTypeDef    HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);

ADC中断和DMA方式触发中断的回调函数:

// 转换完成中断触发/DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc);

2.2.2 单通道

使能中断:
在这里插入图片描述
开启ADC转换:

int main(void){
    
    
	[...]
    
    HAL_ADC_Start_IT(&hadc);
    
    while (1){
    
    
        [...]
    }
}

ADC转换完成回调函数

ADC_HandleTypeDef* hadc1 = &hadc;		// 全局变量与局部变量同名...
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    
    
    if (hadc == hadc1)
    {
    
    
        adc_value[0] = HAL_ADC_GetValue(hadc);
    }
}

但最后发现板子卡死了,在ADC回调函数和ADC中断来回跳转,原因是使能了连续模式,单次转换完成后,又继续开始转换,导致ADC中断不断触发、回调,而ADC单次转换周期us级别,CPU主频也才30M,根本来不及跳转到main中执行,造成了“卡死”现象。

关闭连续模式后,也不能在回调函数中再次启动ADC转换:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    
    
    if (hadc == hadc1)
    {
    
    
        adc_value[0] = HAL_ADC_GetValue(hadc);
        HAL_ADC_Start_IT(hadc);
    }
}

这样跟连续模式没区别,刚触发又开始转换,一样“卡死”。

解决方法

uint8_t adc_flag = false;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    
    
    if (hadc == hadc1)
    {
    
    
        adc_value[0] = HAL_ADC_GetValue(hadc);
        adc_flag = true;
    }
}

ADC回调函数中将标志置位后,在后台系统(main函数)中运行,根据系统tick时间每50ms判断一次:

void adc_test(void)
{
    
    
    float adc1_vol, adc2_vol;
    if (HAL_GetTick() % 50 == 0 && adc_flag)
    {
    
    
        adc_flag = false;

        adc2_vol =  adc_value[0] * 3.3f / 4095.f;
        adc1_vol =  adc_value[1] * 3.3f / 4095.f;

        snprintf(oled_buf, sizeof(oled_buf), "RP1: %.2fV  ", adc1_vol);
        OLED_ShowString(0, 0, (uint8_t *)oled_buf, 16);
        snprintf(oled_buf, sizeof(oled_buf), "RP2: %.2fV  ", adc2_vol);
        OLED_ShowString(0, 2, (uint8_t *)oled_buf, 16);

        HAL_ADC_Start_IT(&hadc);
    }
}

adc_test在main中调用。

2.2.3 多通道

中断方式多通道读取问题与轮询方式一样,但是每次读取的是CH8通道的值(RP2),应该是扫描时CH8转换完成后,置位了EOC标志,触发了中断。这次换个方法解决吧。

利用ADC_CHSELRADC通道选择寄存器**(只有当ADSTART=0才能写入**,该标志每次转换完成硬件会清除):

image-20220401210105538

uint8_t adc_flag = false;
uint8_t channel_switch_flag = false;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    
    
    if (hadc == hadc1)
    {
    
    
        if (channel_switch_flag)
        {
    
    
            adc_value[1] = HAL_ADC_GetValue(hadc);  // ADC_IN9
            hadc->Instance->CHSELR = 1 << 8;
        }
        else
        {
    
    
            adc_value[0] = HAL_ADC_GetValue(hadc);  // ADC_IN8
		    hadc->Instance->CHSELR = 1 << 9;
        }
        channel_switch_flag = ~channel_switch_flag;
        adc_flag = true;
    }
}

每次进入回调函数时,手动切换一下通道即可。
在这里插入图片描述
这次还挺稳定

注意:同样需要关闭连续转换模式,不然会一直触发中断,呈现“卡死”现象。

2.3 DMA方式

2.3.1 ADC_DMA函数

// DMA方式开启ADC和DMA传输
HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
// DMA方式关闭ADC和DMA传输
HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);
  • pData指向buffer缓冲区
  • Length为总采集次数(对于多通道,每个通道计一次采集次数)

DMA方式触发中断的回调函数:

// 转换完成中断触发/DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc);
// DMA半传输完成回调
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc);

2.3.2 单通道 单次

配置DMA为普通模式、存储器自增、半字传输(12bit分辨率ADC):
在这里插入图片描述
首先启动DMA传输:

int main(void){
    
    
	[...]
    
    HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 1);
    
    while (1){
    
    
        [...]
    }
}

DMA传输完成回调函数:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) 
{
    
    
    if (hadc == hadc1)
    {
    
    
        adc_flag = 1;
    } 
}

标志置位后需要再次启动DMA:

void adc_test(void)
{
    
    
    float adc1_vol, adc2_vol;

    if (adc_flag)
    {
    
    
        adc_flag = false;

        adc2_vol =  adc_value[0] * 3.3f / 4095.f;
        adc1_vol =  adc_value[1] * 3.3f / 4095.f;
		[...]
        HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 1);
    }
}

注意单次转换length参数只能为1,否则会导致DMA没有传输完成,不会进入回调函数。

2.3.3 单通道 连续

在这里插入图片描述
将length改为10:

HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);

均值滤波:

uint16_t get_adc_average(uint16_t *pbuf, uint16_t num)
{
    
    
    uint32_t sum = 0;
    for (int i = 0; i < num; i++)
        sum += pbuf[i];
        
    return sum / num;
}

转换成电压值:

void adc_test(void)
{
    
    
    float adc1_vol, adc2_vol;

    if (adc_flag)
    {
    
    
        adc_flag = false;
        adc2_vol =  get_adc_average(adc_value, 10) * 3.3f / 4095.f;
        [...]
        HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);
    }
}

2.3.4 多通道 扫描

扫描模式是默认开启的,同时必须开启连续模式,否则采集一轮结束后不会继续采集,这样就不会触发DMA中断进入回调函数了。连续扫描模式下,多通道会根据length数量循环采集各通道,如ch8和ch9两通道,length为10的话,则adc_value[0][2][4][6][8]存放的就是ch8通道的采样值,adc_value[1][3][5][7][9]数组位置存放的是ch9的值:

image-20220401230917880

修改一下滤波函数:

void get_adc_average(uint16_t *pbuf, uint16_t num)
{
    
    
    uint32_t odd_sum = 0, even_sum = 0;
    for (int i = 0; i < num; i++)
    {
    
    
        if (i & 0x01)            // 奇数
            odd_sum += pbuf[i];
        else
            even_sum += pbuf[i];
    }

    adc1_val = 2 * odd_sum  / num;    
    adc2_val = 2 * even_sum / num;   
}

转换成电压值:

void adc_test(void)
{
    
    
    float adc1_vol, adc2_vol;

    if (adc_flag)
    {
    
    
        adc_flag = false;
        adc1_vol = adc1_val * 3.3f / 4095.f;
        adc2_vol = adc2_val * 3.3f / 4095.f;
        [...]
        HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);
    }
}

实验发现用DMA方式采集ch8和ch9通道,两通道会相互干扰,比如ch8在0~3.3V变化,则ch9会随之在0~0.26V间浮动。

查询资料发现:采样周期越长通道间的相互干扰就越小,反之则越大

因此,将采样周期改为最大:
在这里插入图片描述
最后经过测试,ch8在0~3.3V变化时,ch9不再受影响,成功解决该问题。

3 总结

  • ADC一般为12bit分辨率,数据可左对齐和右对齐(一般右对齐),同时具有单次、连续、扫描、间隔模式,其中扫描模式一般用于多通道的采集(多通道默认使能)。
  • 对于F1/4通道分为规则通道组与注入通道组,注入通道类似中断,可打断正在转换的规则通道组,优先转换注入通道;L0中没有注入通道,均为规则通道。
  • F1/4的ADC时钟来自APB2经过ADC分配器分频后的时钟,L0的ADC时钟来自HSI(16M)。
  • ADC的触发源除了内部通道外,还有外部事件触发源,比如定时器捕获,EXTI事件触发。
  • ADC中断触发标志中EOC(End of any conversion)为当前通道转换结束标志,EOS(End of a sequence of conversions)为当前转换序列全部转换结束标志,即所有通道均转换完成。
  • 轮询模式下,对于单通道,单次转换时每次都要调用HAL_ADC_Start函数开始转换,而对于连续转换,只要初始化调用一次HAL_ADC_Start函数,之后就会自动转换;对于多通道,HAL库要等待转换序列结束标志EOS置位,因此每次读取的都是最后一个通道的数值,最后直接使用寄存器编程解决了此问题,但是数值有跳变现象。
  • 中断模式下,对于单通道,如果使用了连续模式或在单次转换模式下在ADC回调函数中调用了HAL_ADC_Start_IT(&hadc);,导致ADC中断不断触发,并且触发周期是us级别的,产生“卡死”假象,最后只能选择使用单次转换模式,同时在回调函数中设置转换完成标志,在后台系统main中再次开启ADC中断转换;对于多通道采集与轮询模式问题一致,但每次读取的都是第一个通道的数据,最后在ADC回调函数中用来回切换通道的方式解决了该问题,且ADC采集数值比较稳定,也不存在两通道干扰的现象。
  • DMA模式下,对于单通道,采用单次转换时length参数只能为1,否则DMA传输到第2个数据时,ADC已关闭,传输未完成不会进入回调函数;采用连续转换时length参数可以设置任意值,且可使用均值滤波等方式减小噪声;对于多通道(必须开启连续模式),DMA会从第一个通道开始采集,采集到最后一个通道再从头开始,但是多通道同时采集时会相互干扰,最后增大ADC采样周期,解决了此问题。

参考:

END

猜你喜欢

转载自blog.csdn.net/kouxi1/article/details/123911525
003
今日推荐