基于SVPWM的霍尔传感器FOC实现过程

FOC算法笔记

起源:使用ST WorkBench配置的HALL BLDC控制算法抖动严重。

ST的电机控制算法,代码非常高端大气,值得学习。

HAL库与LL库混用,效率很高。很多中断回调都直接重写了,没有使用HAL库那一套。

只是好多地方不知道干嘛的,改动也无从下手。

为了加深理解,这里选择自己敲一遍。谨此记录。

1、硬件配置

stm32G474RET6 + X-NUCLEO-IHM07M1 ;

请添加图片描述

一对极BLDC;
请添加图片描述

12V直流电源;

逻辑分析仪/示波器;

红外测速仪器;

2、软件配置

这里使用keil5+cubeMX; 电机控制板和驱动板的原理图必须搞到;

  • 配置定时器1,四路PWM输出。1-3路为IHM07M1的mos管开关输入,该驱动不需要互补输出,第四路PWM用做触发ADC采样,不需要输出。具体配置如下图:

请添加图片描述

stm32g474主频为170M,定时器1主频也为170M,这里配置PWM中心对齐加6分频,实际的PWM频率为:
C L K P W M = 定时器主频 ( P r e s c a l e r + 1 ) ∗ 2 = 12.14 K CLK_{PWM} = \frac{定时器主频}{(Prescaler+1)*2} = 12.14K CLKPWM=(Prescaler+1)2定时器主频=12.14K
计数值这里暂定1K吧。

Trigger Output(TRGO)配置为 OC4REF,即为第四通道的比较输出功能。作为下文ADC采集的硬件触发功能。这样可以根据实际情况通过改变PWM通道的占空比灵活配置采样时刻。

  • 生成代码后,还需要手动开启PWM输出:

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
    
  • 还需要开启UART功能,通过上位机Vofa+ 可以观察数据波形。开启功能简单叙述:

    请添加图片描述
    请添加图片描述

由于需要借助VOFA+软件观察波形,所以还需要简单的通信协议;
定义发送缓冲区:
	char buff[3*4+4] = {0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, 
						0x00, 0x80, 0x7f};
填充数据:
	*(float*)buff = MotorM1.CurVelocity;
	*((float*)buff+1) = MotorM1.CurPosition;
	*((float*)buff+2) = 0;
发送数据:
	HAL_UART_Transmit_DMA(&huart2,(uint8_t *)&buff,sizeof(buff));

2、开环运行

请添加图片描述

开环运行原理图如上;只需要左侧三个参数即可让电机动起来;其中 V_d = 0; θ 为自增变量;V_q为一个较小的任意值;下面贴出关键函数的实现:

SVPWM:
void Motor::SvpwmTransform(Ualpha_beta_t vol, Dabc_t* duty)
{
    uint32_t timelimit = sqrt3 * 1000 / 2;
    uint32_t t0 = 0, t1 = 0, t2 = 0;
    uint8_t sectiontable[] = { 0, 2, 6, 1, 4, 3, 5, 0 };
    int32_t U1 = vol.beta;
    int32_t U2 = (sqrt3 * vol.alpha - vol.beta) / 2;
    int32_t U3 = -(sqrt3 * vol.alpha + vol.beta) / 2;
    int16_t ta = 0, tb = 0, tc = 0;
    uint8_t a = U1 > 0 ? 1 : 0;
    uint8_t b = U2 > 0 ? 1 : 0;
    uint8_t c = U3 > 0 ? 1 : 0;
    uint8_t n = 4 * c + 2 * b + a;
    //CurSector = sectiontable[n];
    switch (sectiontable[n]) {
    case 1:
        t1 = U2;//4
        t2 = U1;//6
        break;
    case 2:
        t1 = -U3;//6
        t2 = -U2;//2
        break;
    case 3:
        t1 = U1;//2
        t2 = U3;//3
        break;
    case 4:
        t1 = -U2;//3
        t2 = -U1;//1
        break;
    case 5:
        t1 = U3;//1
        t2 = U2;//5
        break;
    case 6:
        t1 = -U1;//5
        t2 = -U3;//4
        break;
    default:
        break;
    }
    //最大到六边形的内切圆,
    if (t1 + t2 > timelimit) {
        uint32_t temp = t1 + t2;
        t1 = t1 * (timelimit) / temp;
        t2 = t2 * (timelimit) / temp;
    }

    t0 = (timelimit - t1 - t2) >> 1;

    switch (sectiontable[n]) {
    case 1:
        ta = t0 >> 1;
        tb = (t0 + t1) >> 1;
        tc = (t0 + t1 + t2) >> 1;
        break;
    case 2:
        ta = (t0 + t2) >> 1;
        tb = t0 >> 1;
        tc = (t0 + t1 + t2) >> 1;
        break;
    case 3:
        ta = (t0 + t1 + t2) >> 1;
        tb = t0 >> 1;
        tc = (t0 + t1) >> 1;
        break;
    case 4:
        ta = (t0 + t1 + t2) >> 1;
        tb = (t0 + t2) >> 1;
        tc = t0 >> 1;
        break;
    case 5:
        ta = (t0 + t1) >> 1;
        tb = (t0 + t1 + t2) >> 1;
        tc = t0 >> 1;
        break;
    case 6:
        ta = t0 >> 1;
        tb = (t0 + t1 + t2) >> 1;
        tc = (t0 + t2) >> 1;
        break;
    default:
        break;
    }
    duty->a = ta;
    duty->b = tb;
    duty->c = tc;
}
更新占空比:
Dabc_t Motor::UpdateDuty(void)
{
	int ccr_limit = 1000;
	
    Dabc_t duty = { 0,0,0 };

    duty.a = MotorM1.Duty.a / 1000;
    duty.b = MotorM1.Duty.b / 1000;
    duty.c = MotorM1.Duty.c / 1000;
	if(MotorM1.Enable)
	{
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,duty.a * ccr_limit);
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,duty.b * ccr_limit);
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_3,duty.c * ccr_limit);
	}
	else
	{
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,duty.a * 0);
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_2,duty.b * 0);
		__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_3,duty.c * 0);
	}
	
    return duty;
}

连接好驱动板、控制板、电机,如果硬件没有问题应该就可以缓慢的转动了。可以适当调节Vq和θ的值。同时配合示波器观察实际输出。逐步排查问题,常见的问题如:驱动没有使能,没有供电,相关硬件保护被触发,跳线帽不正确,导线连接问题等。

实际占空比的波形如下:(vd = 200, θ += 0.001)

请添加图片描述

开环转动过程中可能会伴随着电机的噪声、抖动,驱动的发热等,均为正常现象,该步骤只是暂时的过渡阶段,仅用于验证代码和硬件。下面着手实现闭环:

3、电流检测

硬件原理

IHM07M1驱动采样电路简图:

一种三相电机的三相电流采样方法与流程

其中 Ra、Rb、Rc为采样电阻。由原理图知:只有对应相的下桥臂打开时才能测量该相的电流。否则没有电流经过测量无意义。同时该驱动板支持三电阻采样。

请添加图片描述

每个采样电阻阻值为0.33欧姆。

请添加图片描述

三路电流采样电路相同,放大倍数已经标注为1.53倍。电流采样非常简单,即使用采样电阻串联在电机某相回路中,通过测量单片机ADC采样电阻两端电压,由于采样电阻阻值已知,可以通过欧姆定律反推实际电流:
I 相电流 = U 采样电阻两端电压 R 采样电阻阻值 ∗ 放大倍数 I_{相电流} = \frac{U_{采样电阻两端电压}}{R_{采样电阻阻值}* 放大倍数} I相电流=R采样电阻阻值放大倍数U采样电阻两端电压

软件原理

​ 由于ADC采样在每个PWM周期,速度很快(12.14K),为节约CPU资源一般是使用DMA的,在DMA完成中断回调中进行计算和处理。

其中ADC配置如下图示,配置仅做参考。

通道1和通道7对应PA0和PC1,这两个GPIO连接了采样电流运放的输出。所以只能配置这两个通道。

请添加图片描述

其中外部触发源一定选择 Timer 1 Trigger Out Event,该触发源对应前文叙述的PWM通道4。

请添加图片描述

DMA功能也非常重要,12位ADC只能开启半字传输,也就是16位传输,同时开启循环模式,这样会自动更新索引并覆盖之前的数据。

请添加图片描述

配置完成后还需要手动开启:

uint16_t ADC_result[3] ; // ADC缓冲区
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_result, 2); //开启传输

同时设置定时器1第四通道的比较值:

__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_4,950);

在DMA中断回调中计算电机相电流:

void  HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{

	if(hadc->Instance==ADC1)
	{		
        MotorM1.PhaseCurr.a = (ADC_result[0] - 1910) * 3.3 / 4096 / 1.528 / 0.33;
        MotorM1.PhaseCurr.b = (ADC_result[1] - 1910) * 3.3 / 4096 / 1.528 / 0.33;
        MotorM1.PhaseCurr.c = -MotorM1.PhaseCurr.a - MotorM1.PhaseCurr.b;
		HAL_GPIO_TogglePin(USER_LED_GPIO_Port, USER_LED_Pin); //翻转LED观察测量时刻是否正常
	}
}

采样电阻两端的电压计算公式为:
U 采样电阻 = ( 采样值 − 校零值 ) 3.3 4096 U_{采样电阻} = (采样值 - 校零值) \frac{3.3}{4096} U采样电阻=(采样值校零值)40963.3
将采样电阻的电压带入上文的相电流计算公式即得到电机相电流,计算前两相电流后,可以通过基尔霍夫定律得到第三相电流。

通过逻辑分析仪观察实际电流采样时刻:
请添加图片描述
请添加图片描述

可以看到;周期结束和开始时进行LED翻转,而LED翻转在DMA中断回调中进行,该配置与预期相符。

通过Vofa上位机观察实际的三相电流:

请添加图片描述

可以看到电流三相对称,与预期相符。

实际的电机电流可能有抖动、震荡,如下图:

请添加图片描述

这样需要根据实际情况修改采样时刻,在电流稳定的时刻进行采样。

4、电流闭环运行

将采样电流与目标电流进行误差控制,实现电流环控制,将电流环控制程序放在定时器1的溢出中断。每个PWM周期进行一次电流采样,采样后进行PI控制,同时调整下一周期的PWM占空比。θ依然为自增变量,目标电流与前文V_d、V_q相同。部分代码如下:

motor_t theta = 0;
void FOC_ControllerM1()
{
	Ialpha_beta_t cur_two;
	Ialpha_beta_t vol_two;
	Idq_t cur_rotate;
	Idq_t vol_rotate;
	theta += 0.001;
    if (theta > 2 * Pi)
    {
        theta -= 2 * Pi;
    }
	if(theta < - 2 * Pi)
	{
		theta += 2 * Pi;
	}
    cur_two = Clark_Transform(MotorM1.PhaseCurr);
    cur_rotate = Park_Transform(cur_two, theta);
    vol_rotate.d = PIDCtrlBlock(MotorM1.TargetCurr.d, cur_rotate.d*1000, &pid_Id);
    vol_rotate.q = PIDCtrlBlock(MotorM1.TargetCurr.q, cur_rotate.q*1000, &pid_Iq);
    vol_two = Inverse_Park_Transform(vol_rotate, theta);
    MotorM1.SvpwmTransform(vol_two, &MotorM1.Duty);
    MotorM1.UpdateDuty();
}

PI控制器的参数需要调整到比较稳定的数值,因为接下来是速度环,需要先将电流环稳定才行。

5、速度闭环运行

霍尔信号检测速度和位置,其实就是根据三个霍尔信号来判断的。根据三个信号的高低电平,霍尔信号分为 5->1->3->2->6->4 六个状态,并按照这样的顺序依次变化,反转就是逆序的变化。如图,120度摆放的霍尔传感器,每60度就会变化一次状态。

请添加图片描述

HALL捕获配置

将定时器配置为HALL Sensor Mode 模式。三个通道就会连接在一起,当三个通道状态变化时,就会发生中断。

软件中有两个点需要注意:

  • HALL Sensor Ratio 2分频后,只有上升沿触发中断。不分频则是上升沿下降沿都会中断。

  • HALL开启溢出中断,可以设置中断源,设置为只有计数器上/下溢才中断,否则会在每次HALL中断都触发溢出中断。
    请添加图片描述

    HAL_TIMEx_HallSensor_Start_IT(&htim2);  //开启HALL中断
    __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);//开启更新中断
    __HAL_TIM_URS_ENABLE(&htim2);				//设置中断源为溢出中断
    
    重写输入捕获中断回调;
    void  HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
    {
    // 中断中,读取HALL的三个信号的状态;
    // 通过判断HALL状态的顺序,判断正反转。
    // HALL状态对应了转子的位置;因为每次中断电机转动60度,所以实际位置 = 上一次位置 +(反转-) 60度;
    // 每次中断都需要读取通道1的捕获值;中断会中断清空计数值;
    // 捕获值记录了该次中断与上次中断的时间间隔;由此可以计算速度;因为每次中断都代表电机转动了60度,可以计算出瞬时速度;
    
    }
    
速度闭环

上面已经得到了离散的六个霍尔位置,并且计算得到了速度;

此时,开环运行的θ就不需要自加了。该位置信息为 六个离散的位置加对瞬时速度的积分,积分时间为FOC速度环的控制周期;

inter = ((float)(MotorM1.CurVelocity)* 2.0f * Pi ) / 60000.0f ;
//速度单位为转每分,转化为弧度,除60000后变为 1ms 经过的弧度。
theta = (float)MotorM1.CurPosition/(65536*2π) + inter;
//65535为霍尔定时器的计算器装载值;

速度环的执行频率暂定为1K。
至此,只需要调节PID的参数,即可实现正常运行。
请添加图片描述

实测轻松达到万转;

但是PID参数还是需要细调,速度不是一直很平稳。
请添加图片描述

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值