引言:ADC比较复杂,本文先给大家HAL库调用所用到的函数,然后介绍原理和配置的参数,最后配置stm32cubemx软件,实现ADC读取三相电机的母线电压(读电流然后转换为电压)和U、V、W三相电流,内容较多较难。
1.HAL库ADC的API调用
序号 | HAL库函数 | 说明 |
---|---|---|
1 | HAL_ADC_Start() | 启动 ADC 转换(单次或连续转换) |
2 | HAL_ADC_Stop() | 停止 ADC 转换。 |
3 | HAL_ADC_PollForConversion() | 轮询等待 ADC 转换完成。 |
4 | HAL_ADC_GetValue() | 获取当前 ADC 转换的数值 |
5 | HAL_ADC_GetState() | 获取 ADC 当前的状态(空闲、忙碌、转换完成等) |
6 | HAL_ADC_ConvCpltCallback() | 当 ADC 转换完成时会触发此回调函数 |
7 | HAL_ADC_ConvHalfCpltCallback() | 当使用 DMA 模式时,半传输完成会触发此回调函数 |
8 | HAL_ADC_LevelOutOfWindowCallback() | 当 ADC 的输入超出设定的模拟窗口值时触发此回调函数 |
9 | HAL_ADCEx_Calibration_Start( &hadc1, ADC_SINGLE_ENDED); | ADC自校验 |
10 | __HAL_ADC_CLEAR_FLAG( &hadc1, ADC_FLAG_JEOC); __HAL_ADC_CLEAR_FLAG( &hadc1, ADC_FLAG_EOC); | 清除标志位 |
2.ADC原理介绍
2.1定义
将电信号转换为数字信号,又称模数转换器
2.2类型
分为并联比较器和逐次逼近比较器,其中并联比较器转换快但成本高,还不准确;而逐次逼近型分辨率高,但读取速度慢,因为需要反复比较数值大小,保证分辨率。
2.3框图简介
来自正点原子hal库教程,第三节会按照这个流程介绍个部分组成、类型、作用。
3.ADC配置选项介绍
3.1参考电压
VDDA既是模拟电压,也是ADC的参考电压,通常是将+3.3V电源滤波再给个电感,使VDDA稳定在3.3V,保证ADC读取模拟量的准确性,注意VDDA不能给其他外设使用,会影响其稳定性的。
3.2转换序列
一般情况,规则组有16个,注入组有4个。
涉及到使用的话,只需要知道注入组的优先级高,在规则组运行过程中,注入组被触发可以打断规则组运行,如图所示(来源正点原子hal库教程)
3.3触发源
分为:软件触发和硬件触发
(1)软件触发,比如先使能ADC,然后直接用程序触发即可
HAL_ADC_Start(&hadc2);
adc_vbus = HAL_ADC_GetValue(&hadc2);
(2)硬件触发,则可以通过时钟触发或者外部事件触发,比如用时钟1的通道4,不输出PWM形式,当其TIM1_IN4输出高电平,有上升沿时,触发ADC中断;外部事件,比如GPIO输入模式接受到一个上升沿触发ADC、或者芯片接受到其他外设的上升沿。如下既是ADC注入组中断回调函数
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
3.4转换时间
(1)首先计算ADC周期时间
ADC的时钟源,根据下图的芯片手册说明,system时钟是ADC的异步时钟源
我们选择异步时钟4分频,系统时钟是160MHZ,那么ADC的时钟频率是40MHZ
(2)最后计算转换时间
根据芯片手册,ADC采样时间是图中的第一个公式
我们的ADC频率是40MHZ,SMP选择6.5周期的,那么ADC采样时间计算如下
3.5数据寄存器(如何读取数据)
(1)寄存器方式(建议)
adc1_in1 = hadc1.Instance->JDR1 ;
adc1_in2 = hadc1.Instance->JDR2 ;
adc1_in3 = hadc1.Instance->JDR3 ;
(2)Hal库函数
规则组只有一个
软件触发模式下,调用 HAL_ADC_Start()
,规则组的转换结果就会更新,再调用HAL_ADC_GetValue()
读取 ADCx->DR
寄存器中的值,只能用于读取规则组的值
HAL_ADC_Start(&hadc2); // 启动规则组转换
HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); // 轮询等待转换完成
uint32_t adc_value = HAL_ADC_GetValue(&hadc2); // 获取规则组的值
规则组有两个
需要设置为扫描模式、DMA储存,其中DMA只需要启动一次,就能连续转换并存储DMA中
uint32_t adc_values[2]; // 存储两个通道的结果
// 启动规则组转换,并使用 DMA
HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adc_values, 2);
// adc_values[0] 为通道 1 的值
// adc_values[1] 为通道 2 的值
3.6中断(ADC中断回调、DMA回调、溢出回调)
中断回调、DMA回调、溢出回调
HAL_ADC_ConvCpltCallback();//中断回调
HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc);//注入组中断回调
HAL_ADC_ConvHalfCpltCallback();//DMA中断回调
HAL_ADC_LevelOutOfWindowCallback();//溢出回调
3.7转换模式(转换手动?自动?)
单次转换:只触发一次转换
连续转换:自动触发下一次转换
3.扫描模式(一次转换多少个ADC)
扫描关闭:仅规则组(若触发)和注入组(若触发)的第一次转换
扫描开启:规则组(若触发)和注入组(若触发)全部转换
3.不同模式组合及应用场合
当你需要读取多个ADC,那你就选多通道,扫描模式;反之,你只需要读一个ADC,就单通道,不扫描模式
当你不想用DMA,那就是单次扫描,每次想读ADC时,需要触发ADC去读一次;反之就是连续扫描,系统自动读,不再需要触发ADC,再读ADC
至于规则组和注入组,重要的数据给注入组,不重要的数据给规则组,那些不常变化、变化小的、对系统影响相对小的数据,比如:母线电压,就是规则组数据
转换模式 | 扫描模式 | |
---|---|---|
单次 | 不扫描 | ADC单通道,对规则组第1个或注入组第1个一次转换 |
单次 | 扫描 | ADC多通道,所有通道都转换一次 |
连续 | 不扫描 | ADC单通道,对规则组第1个或注入组第1个连续转换 |
连续 | 扫描 | ADC多通道,对所有通道连续转换 |
4.stm32cubemx配置
实验目标:读取母线电压和三相电流,配置为母线电压为规则组,三相电流为注入组,ADC多通道扫描模式,单次转换(也可以开启DMA,配置为连续转换)。
先开启4个ADC
配置母线电压,软件触发,数据不是太重要,采样时间可以短一点
配置三相电流,硬件触发,TIM1_IN4作为触发源,数据重要,采样时间可以长点
最后打开中断(如果你选择连续转换,还需要打开DMA)
5.keil代码解析
首先在main初始化ADC,在主循环那读取母线电压
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_ADC1_Init();
MX_TIM1_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
//ADC
HAL_ADCEx_Calibration_Start( &hadc1, ADC_SINGLE_ENDED);
__HAL_ADC_CLEAR_FLAG( &hadc1, ADC_FLAG_JEOC);
__HAL_ADC_CLEAR_FLAG( &hadc1, ADC_FLAG_EOC);
HAL_ADCEx_InjectedStart_IT(&hadc1);
//TIM4编码器模式
HAL_TIM_Base_Start(&htim3);
HAL_TIM_Encoder_Start_DMA(&htim3,TIM_CHANNEL_1|TIM_CHANNEL_2,&encoderValue[0],&encoderValue[1],2);
//TIM1高级定时器
TIM1->ARR = 8000 - 1;
TIM1->CCR4 = 8000 - 2;
HAL_TIM_Base_Start( &htim1);
HAL_TIM_PWM_Start( &htim1, TIM_CHANNEL_4);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//ADC规则组软件触发读取母线电压
HAL_ADC_Start(&hadc1);
adc_vbus = HAL_ADC_GetValue(&hadc1);
rtU.v_bus = adc_vbus*0.0887694f;
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
最后是在ADC注入组中断回调函数里,读取三相电流,其他的代码讲解就不罗列在此了,可以看后续的外设篇讲解或在生成式AI里询问。
adc1_in1 = hadc1.Instance->JDR1 ;
adc1_in2 = hadc1.Instance->JDR2 ;
adc1_in3 = hadc1.Instance->JDR3 ;
/**************************** 4.ADC+3.ABZ编码器+5.高级定时器+2.串口****************************/
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
static uint8_t cnt;
static uint8_t prev_state;
/* Prevent unused argument(s) compilation warning */
UNUSED(hadc);
if(hadc == &hadc1)
{
if(ADC_offset == 0)
{
cnt++;
adc1_in1 = hadc1.Instance->JDR1;//读取注入组三相电流
adc1_in2 = hadc1.Instance->JDR2;
adc1_in3 = hadc1.Instance->JDR3;
IA_Offset += adc1_in1;//10次累计再做平均,实习滤波,算出电流0点偏移量
IB_Offset += adc1_in2;
IC_Offset += adc1_in3;
if(cnt >= 10)
{
ADC_offset = 1;
IA_Offset = IA_Offset/10;
IB_Offset = IB_Offset/10;
IC_Offset = IC_Offset/10;
}
}
else
{
adc1_in1 = hadc1.Instance->JDR1 ;
adc1_in2 = hadc1.Instance->JDR2 ;
adc1_in3 = hadc1.Instance->JDR3 ;
rtU.ia = (adc1_in1 - IA_Offset)*0.00149025f;
rtU.ib = (adc1_in2 - IB_Offset)*0.00149025f;
rtU.ic = (adc1_in3 - IC_Offset)*0.00149025f;
//编码器读取ABZ
Enc_cnt = __HAL_TIM_GET_COUNTER(&htim3) ;
m_Theta = (Enc_cnt + amend_Theta)*2*PI/16384;
while(m_Theta >= 2*PI){m_Theta -= 2*PI;}
while(m_Theta < 0){m_Theta += 2*PI;}//角度归一化
//转速计算
m_speed = (m_Theta - m_Theta_delay)/0.0001;
m_Theta_delay = m_Theta;
//电角度、机械角度计算
e_Theta = m_Theta *pole_pairs;
while(e_Theta >= 2*PI){e_Theta -= 2*PI;}
while(e_Theta < 0){e_Theta += 2*PI;}//角度归一化
// 电角度0对齐
if(prev_state == 2 && state == 3) {
__HAL_TIM_SET_COUNTER(&htim3, 0);
e_Theta = 0;
}
prev_state = state;
//位置计算
m_positionfd = Position_Cal(m_prevTheta, m_Theta);
m_prevTheta = m_Theta;
//赋值给Simulink模型
rtU.FOC_state = 1;
rtU.PositionRef = 0;
rtU.PositionFd = m_positionfd;
rtU.theta = e_Theta;
rtU.torque_ref = 1;
FOC_Model_step();
if(state == 2){
TIM1->CCR1 = 0;
TIM1->CCR2 = 30;
TIM1->CCR3 = 100;
}
else{
TIM1->CCR1 = rtY.tABC[0];
TIM1->CCR2 = rtY.tABC[1];
TIM1->CCR3 = rtY.tABC[2];
}
// TIM1->CCR1 = rtY.tABC[0];
// TIM1->CCR2 = rtY.tABC[1];
// TIM1->CCR3 = rtY.tABC[2];
load_data[0] = rtU.ia;
load_data[1] = adc1_in1;
load_data[2] =IA_Offset;
load_data[3] = state;
load_data[4] = e_Theta;
memcpy(tempData, (uint8_t *)&load_data, sizeof(load_data));
HAL_UART_Transmit_DMA(&huart2,(uint8_t *)tempData,6*4);
}
}
/* NOTE : This function should not be modified. When the callback is needed,
function HAL_ADCEx_InjectedConvCpltCallback must be implemented in the user file.
*/
}
/**************************** 4.ADC+3.ABZ编码器+5.高级定时器+2.串口****************************/
结果:三相电流处于中间值4096的一半,是因为硬件上有个1.65V的直流偏执,下章TIM里改为时钟触发ADC采样
结语:本文内容十分难懂,建议结合着正点原子hal库视频观看
内容较多,难免出错,如有错误,感谢指正。