零电角度标定是FOC(磁场定向控制)系统中一个关键步骤。简单来说,就是找到电机转子位置与定子绕组之间的对应关系。想象一下,你手里拿着一块磁铁靠近另一个磁铁,当两个磁铁的N极和S极完全对齐时,这个位置就是零电角度位置。
在实际电机控制中,我们需要把这个理论上的零电角度位置转化为编码器的具体读数。为什么要这么做呢?因为编码器是实时反馈转子位置的传感器,只有知道零电角度对应的编码器值,后续的FOC控制才能准确工作。
这个方法特别适合已经搭建好STM32硬件平台,使用编码器作为位置反馈的开发者。你可能已经完成了电机驱动电路、编码器接口等硬件设计,现在需要的就是这个关键的软件标定步骤。
开环电流注入法的核心思想很简单:我们强制让电机产生一个固定方向的磁场,然后读取此时编码器的值。具体来说:
这相当于人为制造了一个"基准点"。因为iq=0,电机不会转动,但会有电流流过绕组产生固定磁场。这个磁场会吸引转子到一个固定位置,这个位置就是零电角度位置。
你可能会问:为什么不用闭环控制?主要有几个原因:
不过要注意,这种方法会让电机发热,所以标定完成后要尽快断电。我在实际项目中就遇到过因为标定时间过长导致电机烫手的情况。
在STM32CubeMX中配置PWM时,有几个关键点需要注意:
以TIM1为例,假设主频是168MHz,通常我会这样配置:
c复制htim1.Instance = TIM1;
htim1.Init.Prescaler = 1; // 2分频
htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1;
htim1.Init.Period = 500; // ARR值
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
编码器接口通常使用通用定时器(如TIM2/TIM3)的编码器模式。配置时要注意:
c复制htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFF;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
sConfig.EncoderMode = TIM_ENCODERMODE_TI12;
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 0;
让我们看看具体的代码实现。首先是Park逆变换部分:
c复制void Get_EncoderOffset_Of_Zero_Electric_Angle_Method_1(void){
float TempVar1 = 0.0f;
float TempVar2 = 0.0f;
id = 1; // 设定id值
theta = 0.0f; // 假设电角度为0
// 计算sin/cos值
sin_theta = arm_sin_f32(theta);
cos_theta = arm_cos_f32(theta);
// Park逆变换
V_Alpha = (id*cos_theta - iq*sin_theta);
V_Beta = (id*sin_theta + iq*cos_theta);
// 后续SVPWM生成代码...
}
这段代码的关键点:
接下来是SVPWM的生成逻辑,这是整个标定过程的核心:
c复制// 确定扇区
TempVar1 = V_Alpha * 0.86602540378444f; // √3/2
TempVar2 = V_Beta * 0.5f;
SectorJudgmentFactor1 = TempVar1 - TempVar2;
SectorJudgmentFactor2 = - TempVar1 - TempVar2;
SectorNum = 0;
if( V_Beta >= 0) SectorNum = SectorNum + 1;
if( SectorJudgmentFactor1 >= 0 ) SectorNum = SectorNum + 2;
if( SectorJudgmentFactor2 >= 0 ) SectorNum = SectorNum + 4;
// 计算中间变量
MiddleTerm_X = V_Beta;
MiddleTerm_Y = TempVar1 + TempVar2;
MiddleTerm_Z = -TempVar1 + TempVar2;
// 扇区处理逻辑
switch(SectorNum){
case 1:
t_first = MiddleTerm_Z;
t_second = MiddleTerm_Y;
// 过调制处理
if(t_first + t_second > 1){
t_firstaddsecond = t_first + t_second;
t_first = t_first / t_firstaddsecond;
t_second = t_second / t_firstaddsecond;
}
t_a = (1 - t_first - t_second)*0.5f;
t_b = t_a + t_first;
t_c = t_b + t_second;
t_cm1 = t_b;
t_cm2 = t_a;
t_cm3 = t_c;
break;
// 其他扇区处理...
}
// 设置PWM比较值
TIM1_CH1_CMP_VAL = (uint16_t)((t_cm1)*500.0f);
TIM1_CH2_CMP_VAL = (uint16_t)((t_cm2)*500.0f);
TIM1_CH3_CMP_VAL = (uint16_t)((t_cm3)*500.0f);
TIM1->CCR1 = TIM1_CH1_CMP_VAL;
TIM1->CCR2 = TIM1_CH2_CMP_VAL;
TIM1->CCR3 = TIM1_CH3_CMP_VAL;
这段代码做了以下几件事:
当电机被固定在一个位置后,我们需要读取编码器的值:
c复制uint16_t Get_EncoderValue(void){
return TIM2->CNT; // 假设编码器接在TIM2上
}
void Calibrate_Zero_Angle(void){
motor_start(); // 使能电机驱动
Get_EncoderOffset_Of_Zero_Electric_Angle_Method_1();
HAL_Delay(100); // 等待稳定
uint16_t encoder_zero = Get_EncoderValue();
motor_stop(); // 立即断电
// 保存encoder_zero到Flash或其他存储介质
Save_Zero_Offset(encoder_zero);
}
这里有几个注意事项:
在实际项目中,我发现电机发热是个常见问题。通过以下方法可以缓解:
我曾经在一个项目中因为标定时间设置过长(1秒),导致电机温度快速上升,后来调整到200ms就解决了问题。
要提高标定精度,可以考虑:
c复制#define CALIBRATION_TIMES 5
uint16_t Do_Calibration(void){
uint16_t values[CALIBRATION_TIMES];
for(int i=0; i<CALIBRATION_TIMES; i++){
Calibrate_Zero_Angle();
values[i] = Get_EncoderValue();
HAL_Delay(500); // 让电机冷却
}
// 简单的排序取中值
Bubble_Sort(values, CALIBRATION_TIMES);
return values[CALIBRATION_TIMES/2];
}
在实际应用中,可能会遇到各种异常情况:
我遇到过最棘手的问题是编码器读数不稳定,最后发现是电源噪声导致的,增加滤波电容后就解决了。