电机FOC中的坐标变换CLARK_PARK 1 前言 电机FOC包含了SVPWM、坐标转换、信号采集反馈、PID闭环控制等,这个控制策略,统称为FOC控制。一般SVPWM算法的实现是在静止的αβ坐标系上实现。而PID控制器由于是对直流参考信号的跟踪效果较好,因此三相交流电会经过坐标变换,在旋转的dq坐标轴上,可以用直流量描述电枢绕组的合成矢量。
FOC控制中,有两种坐标转换需要注意的,分别是clark变换和park变换。clark变换将abc坐标系转换为αβ坐标系,而park变换将静止的αβ坐标系转换为dq坐标系。
2 自然坐标系ABC 三相永磁同步电机的驱动电路如下图所示。
根据图示电路可以发现在三相永磁同步电机的驱动电路中,三相逆变输出的三相电压为𝑈𝐴,𝑈𝐵,𝑈𝐶将作用于电机,那么在三相平面静止坐标系ABC中,电压方程满足以下公式:
𝜃𝑒为电角度 𝑈𝑚为相电压基波峰值
所以根据上述公式可以发现,三相电压的大小是随时间变化的正弦波形,相位依次相差120°,具体如下图所示;
3 αβ坐标系 由静止三相坐标系ABC变换到静止坐标系αβ的过程称为Clarke变换;在αβ静止坐标系中,α轴和β轴的相位差为90°,且αβ的大小是随时间变化的正弦波,具体如下图所示。
从自然坐标系𝐴𝐵𝐶变换到静止坐标系 𝛼𝛽,满足以下条件:
其中𝑇3𝑆/2𝑆为变换矩阵:
注意:N为系数,做等幅值转换和等功率转换N系数不同。
当 N=2 / 3,是等幅值转换。当 N=√(2/3),是等功率转换。
4 clark变换
Clarke变换的目的是将平衡的三相量转换为平衡的两相正交量。
4.1 公式
其实直接可以把转换公式列出。
clark变换 写成转换矩阵,就是:
使用三相正弦波的模型输出三相电压波形,然后使用clarke变换模块,将三相波形转换为两相波形。转换后alpha的波形和a相的波形相同。转换公式使用等幅值变换。
将两个转换矩阵相乘,应该是一个单位矩阵,系数K的作用是可以将转换变为等幅值转换或者等功率转换。
当 K=2 / 3,是等幅值转换。当 K=√(2/3),是等功率转换。
既然alpha和beta的值都知道了,那么角度theta也可以顺便计算一下。使用theta = atan2(beta, alpha)。alpha为实轴,beta为虚轴,所以这里参数y对应beta,参数x对应alpha。
clark变换的逆变换:
写成转换矩阵,就是(采用振幅不变变换):
采用功率不变变换:
4.2 matlab仿真 在matlab/simulink中搭建仿真模型
matlab中ABCToAlphaBeta的代码:
1 2 3 4 5 6 function y = fcn (a,b,c) alpha = a - b/2 - c/2 ; beta = sqrt (3 )/2 * (b - c);y = (2 /3 )*[alpha;beta ]; # 乘以2 /3 是为了让alpha波形和a相波形幅值相等
AlphaBetaToABC中的代码:
1 2 3 4 5 6 7 8 function y = fcn (alpha,beta) a = alpha; b = -1 /2 * alpha + sqrt (3 )/2 * beta ; c = -1 /2 * alpha - sqrt (3 )/2 * beta ; y = [a;b;c];
仿真波形:
我在这里使用的是等幅值变换。因为这里调制系数为1,各个正弦波的幅值都是1。假如使用等功率变换,alphabeta坐标系上的幅值会超过1,此时若直接经过SVPWM算法,会变成过调制。
5 dq坐标系 dq坐标系相对于定子来说是旋转的坐标系,转速的角速度和转子旋转的角速度相同,所以,相当于转子来说,dq坐标系就是静止的坐标系,而id和iq则是恒定不变的两个值,具体如下图所示。
根据物理结构,我们发现,d轴方向与转子磁链方向重合,又叫直轴。q轴方向与转子磁链方向垂直,又叫交轴。d轴和q轴如下图所示。
6 park变换
park变换将平衡两相正交平稳系统中的矢量变换为正交旋转坐标系。
park变换的本质是静止坐标系αβ乘以一个旋转矩阵,从而得到dq坐标系,其中满足以下条件。
其中𝑇2𝑠/2𝑟为旋转矩阵,所以,park变换和反park变换其根本就是旋转矩阵不同。𝑇2𝑠/2𝑟可以表示为:
𝑇2𝑠/2𝑟含义为 2stator ==> 2 rotor
2轴定子坐标系转换到2轴转子坐标系
6.1 公式
通过几何变换,可以直接得到:
图中Θ就是d轴和α轴之间的夹角。我们也可以用q轴和α轴之间的夹角进行转换,但是会影响矩阵的参数。故还是采用d轴和α轴之间的夹角。因为我参考了wikipedia的alphabeta transformation和TI的controlSuite里面的资料,都是使用这个夹角去推转换矩阵的。因此不钻牛角尖了。
写成矩阵形式:
那么逆变换就是:
Park反变换是将正交旋转坐标系中的矢量投影到两相正交固定框架。
①经过clarke变换将三相电流变换为固定的alpha-beta直角坐标系下。
②因转子是旋转的,又经过Park变换将固定的直角坐标系下的alpha-beta轴,变换为旋转坐标D-Q轴。
③将三相相差120度的正弦信号变换为了线性的ID、IQ信号。
④但是我们得到反馈,计算完后。要将信号变换回去,输出对应的三相电压。
⑤又会经过反Park变换,而反clarke变换可以使用另一种方式svpwm去完成输出电压。
θ角度是由位置传感器得到的已知变量。(锁相环)
跟着转子旋转的d-q坐标系成功把cos,sin正余弦信号转换为线性的了。
6.2 仿真 搭建仿真模型:
matlab中AlphaBetaToDQ代码:
1 2 3 4 5 6 function y = fcn (alpha,beta, c) d = cos (c)* alpha + sin (c)*beta ; q = -sin (c)* alpha + cos (c)*beta ; y=[d;q];
DQToAlphaBeta代码:
1 2 3 4 5 6 function y = fcn (d,q, c) alpha = cos (c) * d - sin (c)*q; beta = sin (c) * d + cos (c)*q; y=[alpha;beta ];
波形:
可以看到,Ud和Uq是恒定值,所以Park变换也叫做交直变换,由输入的交流量,最终变换到相对于转子坐标的直流量。
此时注意到,现在三相abc的波形函数是:
假如需要反转,那么把输入改为:
同时把仿真模型中的constant改为 -2*pi*10
可以得到:
4 C语言(Ti或ST) 以下代码来自TI的controlSUITE。在此特别鸣谢TI。
4.1 clarke.h TI的Digital Motor Control库中的实现:使用了IQmath的库,把运算浮点数速度提高,定点DSP实现精确的浮点运算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #ifndef __CLARKE_H__ #define __CLARKE_H__ typedef struct { _iq As; _iq Bs; _iq Cs; _iq Alpha; _iq Beta; } CLARKE; #define CLARKE_DEFAULTS { 0, \ 0, \ 0, \ 0, \ 0, \ } #define ONEbySQRT3 0.57735026918963 #define CLARKE_MACRO(v) \ v.Alpha = v.As; \ v.Beta = _IQmpy((v.As +_IQmpy2(v.Bs)),_IQ(ONEbySQRT3)); #define CLARKE1_MACRO(v) \ v.Alpha = v.As; \ v.Beta = _IQmpy((v.Bs - v.Cs),_IQ(ONEbySQRT3)); #endif
ST的电机库的实现函数:st库只使用了两相电流,一堆限幅检测猛如虎。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 typedef struct { int16_t a; int16_t b; } ab_t ; #define divSQRT_3 (int32_t)0x49E6 __weak alphabeta_t MCM_Clarke ( ab_t Input ) { alphabeta_t Output; int32_t a_divSQRT3_tmp, b_divSQRT3_tmp ; int32_t wbeta_tmp; int16_t hbeta_tmp; Output.alpha = Input.a; a_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.a; b_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.b; #ifdef FULL_MISRA_C_COMPLIANCY wbeta_tmp = ( -( a_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) ) / 32768 ; #else wbeta_tmp = ( -( a_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) ) >> 15 ; #endif if ( wbeta_tmp > INT16_MAX ) { hbeta_tmp = INT16_MAX; } else if ( wbeta_tmp < ( -32768 ) ) { hbeta_tmp = ( -32768 ); } else { hbeta_tmp = ( int16_t )( wbeta_tmp ); } Output.beta = hbeta_tmp; if ( Output.beta == ( int16_t )( -32768 ) ) { Output.beta = -32767 ; } return ( Output ); }
用__weak前缀的函数称这个函数为“弱函数”。
若两个或两个以上全局符号(函数或变量名)名字一样,而其中之一声明为weak属性,则这些全局符号不会引发定义错误。链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。
当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。
4.2 park.h TI也是很简单的两句计算,cos和sin是外部计算传入参数。对于三角函数,TI有自己的库去计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #ifndef __PARK_H__ #define __PARK_H__ typedef struct { _iq Alpha; _iq Beta; _iq Angle; _iq Ds; _iq Qs; _iq Sine; _iq Cosine; } PARK; #define PARK_DEFAULTS { 0, \ 0, \ 0, \ 0, \ 0, \ 0, \ 0, \ } #define PARK_MACRO(v) \ \ v.Ds = _IQmpy(v.Alpha,v.Cosine) + _IQmpy(v.Beta,v.Sine); \ v.Qs = _IQmpy(v.Beta,v.Cosine) - _IQmpy(v.Alpha,v.Sine); #endif
ST的库:ST的代码写的很“规矩”,一步一步的。有很多限幅检测。
在对theta求角度的查表法也绕了一下。解决浮点计算,统一后,全使用int型的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 typedef struct { int16_t hCos; int16_t hSin; } Trig_Components; typedef struct { int16_t alpha; int16_t beta; } alphabeta_t ; typedef struct { int16_t q; int16_t d; } qd_t ; __weak qd_t MCM_Park ( alphabeta_t Input, int16_t Theta ) { qd_t Output; int32_t d_tmp_1, d_tmp_2, q_tmp_1, q_tmp_2; Trig_Components Local_Vector_Components; int32_t wqd_tmp; int16_t hqd_tmp; Local_Vector_Components = MCM_Trig_Functions( Theta ); q_tmp_1 = Input.alpha * ( int32_t )Local_Vector_Components.hCos; q_tmp_2 = Input.beta * ( int32_t )Local_Vector_Components.hSin; #ifdef FULL_MISRA_C_COMPLIANCY wqd_tmp = ( q_tmp_1 - q_tmp_2 ) / 32768 ; #else wqd_tmp = ( q_tmp_1 - q_tmp_2 ) >> 15 ; #endif if ( wqd_tmp > INT16_MAX ) hqd_tmp = INT16_MAX; else if ( wqd_tmp < ( -32768 ) ) hqd_tmp = ( -32768 ); else hqd_tmp = ( int16_t )( wqd_tmp ); Output.q = hqd_tmp; if ( Output.q == ( int16_t )( -32768 ) ) { Output.q = -32767 ; } d_tmp_1 = Input.alpha * ( int32_t )Local_Vector_Components.hSin; d_tmp_2 = Input.beta * ( int32_t )Local_Vector_Components.hCos; #ifdef FULL_MISRA_C_COMPLIANCY wqd_tmp = ( d_tmp_1 + d_tmp_2 ) / 32768 ; #else wqd_tmp = ( d_tmp_1 + d_tmp_2 ) >> 15 ; #endif if ( wqd_tmp > INT16_MAX ) { hqd_tmp = INT16_MAX; } else if ( wqd_tmp < ( -32768 ) ) { hqd_tmp = ( -32768 ); } else { hqd_tmp = ( int16_t )( wqd_tmp ); } Output.d = hqd_tmp; if ( Output.d == ( int16_t )( -32768 ) ) { Output.d = -32767 ; } return ( Output ); }
4.3 ipark.h TI程序:和park程序一样,cos和sin是参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #ifndef __IPARK_H__ #define __IPARK_H__ typedef struct { _iq Alpha; _iq Beta; _iq Angle; _iq Ds; _iq Qs; _iq Sine; _iq Cosine; } IPARK; #define IPARK_DEFAULTS { 0, \ 0, \ 0, \ 0, \ 0, \ 0, \ 0, \ } #define IPARK_MACRO(v) \ \ v.Alpha = _IQmpy(v.Ds,v.Cosine) - _IQmpy(v.Qs,v.Sine); \ v.Beta = _IQmpy(v.Qs,v.Cosine) + _IQmpy(v.Ds,v.Sine); #endif
ST程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 __weak alphabeta_t MCM_Rev_Park ( qd_t Input, int16_t Theta ) { int32_t alpha_tmp1, alpha_tmp2, beta_tmp1, beta_tmp2; Trig_Components Local_Vector_Components; alphabeta_t Output; Local_Vector_Components = MCM_Trig_Functions( Theta ); alpha_tmp1 = Input.q * ( int32_t )Local_Vector_Components.hCos; alpha_tmp2 = Input.d * ( int32_t )Local_Vector_Components.hSin; #ifdef FULL_MISRA_C_COMPLIANCY Output.alpha = ( int16_t )( ( ( alpha_tmp1 ) + ( alpha_tmp2 ) ) / 32768 ); #else Output.alpha = ( int16_t )( ( ( alpha_tmp1 ) + ( alpha_tmp2 ) ) >> 15 ); #endif beta_tmp1 = Input.q * ( int32_t )Local_Vector_Components.hSin; beta_tmp2 = Input.d * ( int32_t )Local_Vector_Components.hCos; #ifdef FULL_MISRA_C_COMPLIANCY Output.beta = ( int16_t )( ( beta_tmp2 - beta_tmp1 ) / 32768 ); #else Output.beta = ( int16_t )( ( beta_tmp2 - beta_tmp1 ) >> 15 ); #endif return ( Output ); }
5 C语言自实现 此处用到了C语言在嵌入式领域非常常用的一个语法:函数指针。
指针是个变量,那么函数指针也是个变量。函数指针变量装的内容是一个函数的首地址。
函数指针的定义为:函数返回值类型 (* 指针变量名) (函数参数列表);
函数指针放入结构体,提供了C++的面向对象的思想,便于模块的封装和层次化。
坐标变换过程中有许多变量,同时有数个函数,如果在C++中,我们可以定义一个类,但是在C语言中我们只能定义一个结构体组织这些紧密联系的成员和算法实现。
以三相电网电压为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 typedef struct { float32 a,b,c; float32 alpha,beta; float32 d,q; float32 theta; float32 sin_sita,cos_sita; void (*Clarke)(); void (*Park)(); void (*InvPark)(); void (*InvClarke)(); } CORDI_TRANSFER; void clarke (CORDI_TRANSFER *p) { p->alpha = (2.0 /3.0 ) * (p->a - 0.5 *p->b - 0.5 *p->c); p->beta = (2.0 /3.0 ) * (sqrt (3 )/2 * (p->b - p->c)); } void park (CORDI_TRANSFER *p) { p->d = p->alpha * p->cos_sita + p->beta * p->sin_sita; p->q = -p->alpha * p->sin_sita + p->beta * p->cos_sita; } void inv_clarke (CORDI_TRANSFER *p) { p->a = p->alpha; p->b = -0.5 *p->alpha + sqrt (3 )/2 *p->beta; p->c = -0.5 *p->alpha - sqrt (3 )/2 *p->beta; } void inv_park () { p->alpha = p->d*p->cos_sita - p->q*p->sin_sita; p->beta = p->d*p->sin_sita + p->q*p->cos_sita; } void main () { CORDI_TRANSFER ClarkeParkSwitch = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , clarke, park, inv_park, inv_clarke }
下面开始坐标变换啦,以clark变换为例。
把三相电压变换成两相电压。
1、输入采样滤波后的三相电Ua,Ub,Uc。存放在ClarkeParkSwitch.a, ClarkeParkSwitch.b, ClarkeParkSwitch.c :
1 ClarkeParkSwitch.a=Ua, ClarkeParkSwitch.b=Ub, ClarkeParkSwitch.c
2、调用void clarke(CORDI_TRANSFER *p)函数
进行clark变换:ClarkeParkSwitch.Clarke(&ClarkeParkSwitch)。这里需要解释,clark变换函数的调用有点特殊,是利用函数指针变量调用的,因为初始化变量ClarkeParkSwitch时,四个函数的指针都初始化给了结构体里面的四个函数指针。
ClarkeParkSwitch.Clarke(&ClarkeParkSwitch)这句话完成的的功能和clarke(&ClarkeParkSwitch)是等价的。执行之后,
计算出Ualpha和Ubelta的值,存放于ClarkeParkSwitch. alpha, ClarkeParkSwitch.beta中。后面的几个变换按照上面的类推就行。