学习参考:
1.江苏科协
2STM32】HAL库 STM32CubeMX教程九---ADC
1.ADC简介
- ADC(Analog-Digital Converter)模拟-数字转换器。
- 分辨率12bit,1us转换时间(从AD转换开始到产生结果需要的最快时间,对应的AD转换频率就是1MHz)
- 输入电压范围0--3.3V,转换结果范围0--4095
- 每个ADC有18个输入通道,可测量16个外部通道(16个GPIO口),2个内部信号源(内部温度传感器、内部参考电压1.2V左右)
- 规则组和注入组两个转换单元
- 模拟看门狗自动监测输入电压范围(使用中断进行程序设计)
- STM32F103C8T6的ADC资源:ADC1、ADC2,10个外部输入通道。
START:开始转换
寄存器:EOC(End of Convert,转换结束信号)
1.1 规则通道和注入通道
-
规则通道(Regular Channels):
- 规则通道是ADC用于常规模数转换的通道。
- 通过规则通道,可以配置ADC以按照预定义的顺序对多个通道进行连续或单次转换。
- 规则通道通常用于采集主要的模拟输入信号。
-
注入通道(Injected Channels):
- 注入通道是ADC中用于特殊应用的通道。
- 注入通道允许在规则通道的基础上,通过插入一些额外的转换来优先处理某些模拟信号。
- 注入通道通常用于对某些紧急或优先级较高的事件进行快速响应,例如监测一个特定的故障条件。
- 注入组使用的不多,常用规则组
1.1.1 规则组的四种转换模式
- 非扫描模式:使用ADC单通道。
- 扫描模式:使用多个ADC通道
- 单次转换:触发一次ADC,只转换一次
- 连续模式:仅需触发一次,在下一次转换完成后立刻进入下一次转换,实现不断地自动进行转换。
-
单次转换,非扫描模式:
只使用ADC的一个通道,触发一次后,需要等待EOC标志位置1,然后从数据寄存器读取结果。如果需要再进行ADC转换,需要再次触发转换。 -
连续转换,非扫描模式(常用):
仅使用一个ADC通道,相比于上一个模式,仅需要触发一次,即可实现自动进行转换。
优点:开始转换之后,不需要等待一段时间,不需要判断是否转换结束。 -
单次转换,扫描模式:
需要指定通道的数量,为了避免数据被覆盖,需要使用DMA模式将数据及时挪走。
相比于第一种模式,可以一次性转换多个通道,不过还是触发一次、所有通道只转换一次。
4.连续转换,扫描模式:
需要指定通道的数量,为了避免数据被覆盖,需要使用DMA模式将数据及时挪走。
不仅可以一次性转换多个通道,还可以实现触发一次、自动不间断转换。
- 间断模式(不常用):在扫描模式下,每隔几个转换就暂停一次,需要再次触发才能继续转换。
1.2 触发ADC转换
对于STM32的ADC,触发ADC开始转换的信号有两种:
- 软件触发源:在程序中手动调用START_ADC语句,就可以启动转换。
- 硬件触发源:主要来自于定时器,定时器可以通向ADC、DAC这些外设,用于触发转换。
- 定时器触发ADC转换示例:给TIM3设置1ms的时间,把TIM3的更新事件选择为TRGO输出,在ADC配置里,选择开始触发信号为TIM3的TRGO信号。这样TIM3的更新事件就能通过硬件自动触发ADC转换了,整个过程不需要进中断,节省了中断资源。
- VDDA:接3.3V
- VSSA:接GND
1.3 ADC预分频器
ADC预分频器来源于RCC,以STM32F103C8T6为例,APB2时钟72MHz,通过ADC预分频器进行分频,得到ADCCLK,ADCCLK最大值为14MHz。如果选择2分频,72M/2 = 36M,就超出范围了,对与ADC预分频器,只能选择6分频,结果是12M。不同的芯片,最大频率不一样,需要查看数据手册。
1.3.1 ADC转换时间
- AD转换的步骤主要为:采样,保持,量化,编码。
- 低速采样可以忽略转换频率,高速采样必须考虑转换时间 的损耗。
- 采样时间:即采样保持的时间,“采样”时间越长,越可以消除一些毛刺信号的干扰。是ADC采样周期的倍数,其值可以设置为1.5、7.5、13.5,...
- 12.5个ADC周期:是量化、编码的时间,因为是12位的ADC,所以需要花费至少12个周期。
ADC 的转换时间跟 ADC 的输入时钟和采样时间有关。
公式为:Tconv = 采样时间 + 12.5 个周期。
当 ADCLK = 14MHZ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)Tconv = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。
一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us,这个才是最常用的。
1.4 数据对齐问题
因为ADC是12位的,而寄存器宽度为16位的,所以便有了数据对齐的选择,常使用数据右对齐。
- 数据右对齐(常用):读出的值就是实际值,高位补零。
- 左对齐:有时候不需要太大的分辨率,便将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配置:连续模式,只需要触发一次。
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通道为例。
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)。在注入组中,每次启动注入模式的转换时,只进行一次转换,而不是连续进行。 即注入组只能配置为单次转换模式。
注入组相比于规则组的优势:
- 规则组仅有一个数据寄存器,因此多通道采集还需要DMA配合。
- 注入组的四个通道都有独立的数据寄存器,使用的时候直接从寄存器里取值即可。且注入组可以打断规则组,因此可以配置规则组进行一些不重要的数据采样(温度、母线电压),配置注入组进行电流采样。
- 所以在ADC支持注入组的情况下,强烈建议使用注入组进行电流采样。
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注入组+多通道
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驱动器的相电流为例。
ADC1的配置:
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的配置:
ADC1配置:
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转换值!
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]); // 电位器不会影响,母线电压的输出值
}