STM32CubeMX学习笔记(4)--ADC模数转换器应用

901 阅读12分钟

学习参考:
1.江苏科协
2STM32】HAL库 STM32CubeMX教程九---ADC

1.ADC简介

  1. ADC(Analog-Digital Converter)模拟-数字转换器。
  2. 分辨率12bit,1us转换时间(从AD转换开始到产生结果需要的最快时间,对应的AD转换频率就是1MHz)
  3. 输入电压范围0--3.3V,转换结果范围0--4095
  4. 每个ADC有18个输入通道,可测量16个外部通道(16个GPIO口),2个内部信号源(内部温度传感器、内部参考电压1.2V左右)
  5. 规则组和注入组两个转换单元
  6. 模拟看门狗自动监测输入电压范围(使用中断进行程序设计)
  7. STM32F103C8T6的ADC资源:ADC1、ADC2,10个外部输入通道。

START:开始转换
寄存器:EOC(End of Convert,转换结束信号)

1.1 规则通道和注入通道

  1. 规则通道(Regular Channels):

    • 规则通道是ADC用于常规模数转换的通道。
    • 通过规则通道,可以配置ADC以按照预定义的顺序对多个通道进行连续或单次转换。
    • 规则通道通常用于采集主要的模拟输入信号。
  2. 注入通道(Injected Channels):

    • 注入通道是ADC中用于特殊应用的通道。
    • 注入通道允许在规则通道的基础上,通过插入一些额外的转换来优先处理某些模拟信号。
    • 注入通道通常用于对某些紧急或优先级较高的事件进行快速响应,例如监测一个特定的故障条件。
    • 注入组使用的不多,常用规则组

1.1.1 规则组的四种转换模式

  • 非扫描模式:使用ADC单通道。
  • 扫描模式:使用多个ADC通道
  • 单次转换:触发一次ADC,只转换一次
  • 连续模式:仅需触发一次,在下一次转换完成后立刻进入下一次转换,实现不断地自动进行转换。
  1. 单次转换,非扫描模式:
    只使用ADC的一个通道,触发一次后,需要等待EOC标志位置1,然后从数据寄存器读取结果。如果需要再进行ADC转换,需要再次触发转换。 image.png

  2. 连续转换,非扫描模式(常用):
    仅使用一个ADC通道,相比于上一个模式,仅需要触发一次,即可实现自动进行转换。
    优点:开始转换之后,不需要等待一段时间,不需要判断是否转换结束。 image.png

  3. 单次转换,扫描模式:
    需要指定通道的数量,为了避免数据被覆盖,需要使用DMA模式将数据及时挪走。
    相比于第一种模式,可以一次性转换多个通道,不过还是触发一次、所有通道只转换一次。
    image.png

4.连续转换,扫描模式:
需要指定通道的数量,为了避免数据被覆盖,需要使用DMA模式将数据及时挪走。
不仅可以一次性转换多个通道,还可以实现触发一次、自动不间断转换。
image.png

  1. 间断模式(不常用):在扫描模式下,每隔几个转换就暂停一次,需要再次触发才能继续转换。

1.2 触发ADC转换

对于STM32的ADC,触发ADC开始转换的信号有两种:

  1. 软件触发源:在程序中手动调用START_ADC语句,就可以启动转换。
  2. 硬件触发源:主要来自于定时器,定时器可以通向ADC、DAC这些外设,用于触发转换。
  3. 定时器触发ADC转换示例:给TIM3设置1ms的时间,把TIM3的更新事件选择为TRGO输出,在ADC配置里,选择开始触发信号为TIM3的TRGO信号。这样TIM3的更新事件就能通过硬件自动触发ADC转换了,整个过程不需要进中断,节省了中断资源。

image.png

  • VDDA:接3.3V
  • VSSA:接GND

1.3 ADC预分频器

image.png
image.png
ADC预分频器来源于RCC,以STM32F103C8T6为例,APB2时钟72MHz,通过ADC预分频器进行分频,得到ADCCLK,ADCCLK最大值为14MHz。如果选择2分频,72M/2 = 36M,就超出范围了,对与ADC预分频器,只能选择6分频,结果是12M。不同的芯片,最大频率不一样,需要查看数据手册。

1.3.1 ADC转换时间

  1. AD转换的步骤主要为:采样,保持,量化,编码。
  2. 低速采样可以忽略转换频率,高速采样必须考虑转换时间 的损耗。
  3. 采样时间:即采样保持的时间,“采样”时间越长,越可以消除一些毛刺信号的干扰。是ADC采样周期的倍数,其值可以设置为1.5、7.5、13.5,...
  4. 12.5个ADC周期:是量化、编码的时间,因为是12位的ADC,所以需要花费至少12个周期。 image.png

ADC 的转换时间跟 ADC 的输入时钟和采样时间有关。
公式为:Tconv = 采样时间 + 12.5 个周期
当 ADCLK = 14MHZ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)Tconv = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。
一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us,这个才是最常用的。

image.png

1.4 数据对齐问题

因为ADC是12位的,而寄存器宽度为16位的,所以便有了数据对齐的选择,常使用数据右对齐

image.png

  • 数据右对齐(常用):读出的值就是实际值,高位补零。
  • 左对齐:有时候不需要太大的分辨率,便将12位ADC的转换数据左对齐,然后只取高8位。数据左移1次,数据乘2,相当于把结果放大了16倍。
  • 左对齐应用:低分辨率读取,把数据的高8位取出来,舍弃掉后面4位的精度,这个12位的ADC就退化为了8位的ADC了。

2.ADC校准问题

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

  • 建议在每次上电后执行一次校准。
  • 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期。
  • 校准过程的代码是固定的,只需要在ADC初始化之后加几句代码即可。

3.ADC应用

注:配置USART的TX为DMA模式时,一定要打开USART的中断!!!

3.1 规则组+单通道+DMA

ADC1配置:连续模式,只需要触发一次。
image.png

uint16_t value[1];

void adc_user_Init(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)value, 1); // 在连续转换模式下,只需触发一次
}

void adc_user_log(void)
{
    float Ua = (float)value[0] / 4095 * 3.3f;
    FOC_log("[value]:%f\r\n", Ua);
}

测试单次转换模式:需要在while(1)中使用HAL_ADC_Start(&hadc1); // 开始ADC规则组的转换进行软件触发ADC转换。

3.2规则组+多通道+DMA

使用连续转换模式进行测试:以ADC1的IN5、IN6通道为例。

image.png

image.png

uint16_t value[2];

void adc_user_Init(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)value, 2); // 在连续转换模式下,只需触发一次
}

void adc_user_log(void)
{
    float Ua = (float)value[0] / 4095 * 3.3f;
    float Ub = (float)value[1] / 4095 * 3.3f;
    FOC_log("[value]:%f,%f\r\n", Ua, Ub);
}

3.3注入组+单通道

在STM32的ADC模块中,连续转换模式通常是在常规组(Regular group)中配置的,而不是在注入组(Injected group)。在注入组中,每次启动注入模式的转换时,只进行一次转换,而不是连续进行。 即注入组只能配置为单次转换模式。

注入组相比于规则组的优势:

  1. 规则组仅有一个数据寄存器,因此多通道采集还需要DMA配合。
  2. 注入组的四个通道都有独立的数据寄存器,使用的时候直接从寄存器里取值即可。且注入组可以打断规则组,因此可以配置规则组进行一些不重要的数据采样(温度、母线电压),配置注入组进行电流采样。
  3. 所以在ADC支持注入组的情况下,强烈建议使用注入组进行电流采样。

image.png

uint16_t value[1];

void adc_user_Init(void)
{
   
}

void adc_user_log(void)
{
     HAL_ADCEx_InjectedStart(&hadc1);                            // 手动启动注入模式的ADC转换
    // 获取注入模式下的ADC转换值
    value[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 获取指定通道的值
 
    // 将ADC转换值,换算为电压值
    float Ua = (float)value[0] / 4095 * 3.3f;
    FOC_log("[value]:%f\r\n", Ua);
}

3.4注入组+多通道

image.png

image.png

uint16_t value[2];

void adc_user_Init(void)
{
    
}

void adc_user_log(void)
{
    HAL_ADCEx_InjectedStart(&hadc1);                            // 手动启动注入模式的ADC转换
    // 获取注入模式下的ADC转换值
    value[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 获取指定通道的值
    value[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2); // 获取指定通道的值

    // 将ADC转换值,换算为电压值
    float Ua = (float)value[0] / 4095 * 3.3f;
    float Ub = (float)value[1] / 4095 * 3.3f;
    FOC_log("[value]:%f,%f\r\n", Ua, Ub);
}

3.5定时器触发ADC规则组+多通道+DMA

不需要打开定时器的中断!!!
使用定时器触发ADC转换,就不需要把ADC配置为连续转换模式了!!!

定时器TIM1配置:以采集FOC驱动器的相电流为例。

image.png

image.png

image.png

ADC1的配置:

image.png

image.png

uint16_t value[2];

void adc_user_Init(void)
{
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)value, 2); // 在连续转换模式下,只需触发一次
}

void adc_user_log(void)
{
    float Ua = (float)value[0] / 4095 * 3.3f;
    float Ub = (float)value[1] / 4095 * 3.3f;
    FOC_log("[value]:%f,%f\r\n", Ua, Ub);
}

3.6定时器触发ADC注入组+多通道

定时器TIM1的配置:

image.png

image.png

image.png

ADC1配置:

image.png

uint16_t value[2];

void adc_uesr_Init(void)
{
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);                   // 开启TIM1_CH4,用于触发ADC转换
    HAL_ADCEx_InjectedStart(&hadc1);                            // 启动注入模式的ADC转换
}

void adc_get_value(void)
{

    // 获取注入模式下的ADC转换值
    value[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 获取指定通道的值
    value[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2); // 获取指定通道的值

    // 将ADC转换值,换算为电压值
    float Ua = (float)value[0] / 4095 * 3.3f;
    float Ub = (float)value[1] / 4095 * 3.3f;
    FOC_log("[value]:%f,%f\r\n", Ua, Ub);
}

3.6.1 定时器触发+注入组+多通道+ADC中断

最优方案:在ADC中断里,获取ADC转换值!

image.png

ADC初始化函数:写在while(1)之前

void adc_user_Init(void)
{
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4); // 开启TIM1_CH4,用于触发ADC转换
    HAL_ADCEx_InjectedStart(&hadc1);          // 启动注入模式的ADC转换
    HAL_ADCEx_InjectedStart_IT(&hadc1);       // 开启ADC注入组中断
}

获取ADC转换值方法1:重写注入组完成后的中断回调函数

void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /* Prevent unused argument(s) compilation warning */
    UNUSED(hadc);
    /* NOTE : This function Should not be modified, when the callback is needed,
              the HAL_ADC_InjectedConvCpltCallback could be implemented in the user file
     */

    if (hadc == &hadc1)
    {
        value[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 获取指定通道的值
        value[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2); // 获取指定通道的值
    }
}

获取ADC转换值方法2:在STM32CubeMX自动生成的中断回调函数里,获取ADC转换值,在stm32f4xx_it.c文件中。

/**
 * @brief This function handles ADC1 global interrupt.
 */
void ADC_IRQHandler(void)
{
  /* USER CODE BEGIN ADC_IRQn 0 */

  /* USER CODE END ADC_IRQn 0 */
  HAL_ADC_IRQHandler(&hadc1);
  /* USER CODE BEGIN ADC_IRQn 1 */

  a1 = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1); // 获取ADC转换值
  /* USER CODE END ADC_IRQn 1 */
}

20231225更新

#include "adc.h"
#include "Serial.h"
#include "usart.h"
#include "ADC_user.h"
#include "string.h"

/**********************  注入组+多通道+单次转换  ************************/
// void init_ADC(void)
// {
//     HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED); // 启动ADC自校准
// }

// void adc_log(void)
// {
//     const uint16_t arrSize = 12;
//     // static uint8_t tempData[arrSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x80, 0x7f};
//     static uint8_t tempData[arrSize] = {0};
//     tempData[arrSize - 2] = 0x80;
//     tempData[arrSize - 1] = 0x7f;

//     float temp[2];
//     HAL_ADCEx_InjectedStart(&hadc2); // 软件触发注入组转换

//     temp[0] = (float)HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);                           // 电位器
//     temp[1] = (float)HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_2) * 3.32f * 11.0f / 4095.0f; // 母线电压

//     memcpy(tempData, (uint8_t *)temp, sizeof(temp));
//     HAL_UART_Transmit_DMA(&huart1, tempData, arrSize); // 使用JustFloat协议,旋转电位器,母线电压的输出值会受影响
//     // log_dma("%f,%f\r\n", temp[0], temp[1]); // 电位器不会影响,母线电压的输出值
// }

/********************** 规则组+多通道+DMA  ************************/
uint16_t adc2_value[2];
const float VDDA = 3.32f;
float gain_Udc; // 母线电压增益系数 = VDDA * 分压电阻系数 / 4095

void init_ADC(void)
{
    HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED); // 启动ADC自校准
    HAL_ADC_Start_DMA(&hadc2, (uint32_t *)adc2_value, 2);  // 在DMA连续转换模式下,只需要触发一次
    gain_Udc = VDDA * 11.0f / 4095.0f;
}

// void adc_log(void)
// {
//     const uint16_t arrSize = 12;
//     // static uint8_t tempData[arrSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x80, 0x7f};
//     static uint8_t tempData[arrSize] = {0};
//     tempData[arrSize - 2] = 0x80;
//     tempData[arrSize - 1] = 0x7f;

//     float temp[2];

//     temp[0] = (float)adc2_value[0]; // 电位器的原始值
//     temp[1] = (float)adc2_value[1]*gain_Udc; // 母线电压

//     memcpy(tempData, (uint8_t *)temp, sizeof(temp));
//     HAL_UART_Transmit_DMA(&huart1, tempData, arrSize); // 使用JustFloat协议,旋转电位器,母线电压的输出值会受影响
//     // log_dma("%f,%f\r\n", temp[0], temp[1]); // 电位器不会影响,母线电压的输出值
// }

void adc_log(void)
{
    HAL_ADC_Start_DMA(&hadc2, (uint32_t *)adc2_value, 2);
    const uint16_t arrSize = 12;
    // static uint8_t tempData[arrSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x80, 0x7f};
    static uint8_t tempData[arrSize] = {0};
    tempData[arrSize - 2] = 0x80;
    tempData[arrSize - 1] = 0x7f;

    float temp[2];

    temp[0] = (float)adc2_value[0];            // 电位器的原始值
    temp[1] = (float)adc2_value[1] * gain_Udc; // 母线电压

    memcpy(tempData, (uint8_t *)temp, sizeof(temp));
    HAL_UART_Transmit_DMA(&huart1, tempData, arrSize); // 使用JustFloat协议,旋转电位器,母线电压的输出值会受影响
    // log_dma("%f,%f\r\n", temp[0], temp[1]); // 电位器不会影响,母线电压的输出值
}