电机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)
%#eml

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)
%#eml

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 ==> 2rotor

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)
%#eml
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)
%#eml
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
/* =================================================================================
File name: CLARKE.H
===================================================================================*/


#ifndef __CLARKE_H__
#define __CLARKE_H__

typedef struct { _iq As; // Input: phase-a stator variable
_iq Bs; // Input: phase-b stator variable
_iq Cs; // Input: phase-c stator variable
_iq Alpha; // Output: stationary d-axis stator variable
_iq Beta; // Output: stationary q-axis stator variable
} CLARKE;

/*-----------------------------------------------------------------------------
Default initalizer for the CLARKE object.
-----------------------------------------------------------------------------*/
#define CLARKE_DEFAULTS { 0, \
0, \
0, \
0, \
0, \
}

/*------------------------------------------------------------------------------
CLARKE Transformation Macro Definition
------------------------------------------------------------------------------*/

// 1/sqrt(3) = 0.57735026918963
#define ONEbySQRT3 0.57735026918963 /* 1/sqrt(3) */


// Clarke transform macro (with 2 currents)
// 只采用两相电流,另一相Ia+Ib+Ic=0得到
//==========================================
#define CLARKE_MACRO(v) \
v.Alpha = v.As; \
v.Beta = _IQmpy((v.As +_IQmpy2(v.Bs)),_IQ(ONEbySQRT3));


// Clarke transform macro (with 3 currents)
// 三相电流时beta=(b-c)/sqrt(3)
//==========================================
#define CLARKE1_MACRO(v) \
v.Alpha = v.As; \
v.Beta = _IQmpy((v.Bs - v.Cs),_IQ(ONEbySQRT3));

#endif // __CLARKE_H__

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 /* 1/sqrt(3) in q1.15 format=0.5773315*/

/**
* @brief This function transforms stator values a and b (which are
* directed along axes each displaced by 120 degrees) into values
* alpha and beta in a stationary qd reference frame.
* alpha = a
* beta = -(2*b+a)/sqrt(3)
* @param Input: stator values a and b in ab_t format
* @retval Stator values alpha and beta in alphabeta_t format
*/
__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;

/* qIalpha = qIas*/
Output.alpha = Input.a;

a_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.a;

b_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.b;

/*qIbeta = -(2*qIbs+qIas)/sqrt(3)*/
// 这里的右移15位,除以32768。
// divSQRT_3 = 0x49E6 / 32768 = 0.5773315
#ifdef FULL_MISRA_C_COMPLIANCY
wbeta_tmp = ( -( a_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) -
( b_divSQRT3_tmp ) ) / 32768;
#else
/* WARNING: the below instruction is not MISRA compliant, user should verify
that Cortex-M3 assembly instruction ASR (arithmetic shift right) is used by
the compiler to perform the shift (instead of LSR logical shift right) */

wbeta_tmp = ( -( a_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) -
( b_divSQRT3_tmp ) ) >> 15;
#endif

/* Check saturation of Ibeta */
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
/* =================================================================================
File name: PARK.H
===================================================================================*/

#ifndef __PARK_H__
#define __PARK_H__

typedef struct { _iq Alpha; // Input: stationary d-axis stator variable
_iq Beta; // Input: stationary q-axis stator variable
_iq Angle; // Input: rotating angle (pu)
_iq Ds; // Output: rotating d-axis stator variable
_iq Qs; // Output: rotating q-axis stator variable
_iq Sine;
_iq Cosine;
} PARK;

/*-----------------------------------------------------------------------------
Default initalizer for the PARK object.
-----------------------------------------------------------------------------*/
#define PARK_DEFAULTS { 0, \
0, \
0, \
0, \
0, \
0, \
0, \
}

/*------------------------------------------------------------------------------
PARK Transformation Macro Definition
------------------------------------------------------------------------------*/


#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 // __PARK_H__

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;

// 传入theta 查表计算得到 cos和sin的值
Local_Vector_Components = MCM_Trig_Functions( Theta );

// 不保证溢出,先计算一次,然后各种限幅判断,最后再做IQ的赋值
/*No overflow guaranteed*/
// 计算 alpha*cos(theta)
q_tmp_1 = Input.alpha * ( int32_t )Local_Vector_Components.hCos;

/*No overflow guaranteed*/
// 计算 beta*sin(theta)
q_tmp_2 = Input.beta * ( int32_t )Local_Vector_Components.hSin;

/*Iq component in Q1.15 Format */
#ifdef FULL_MISRA_C_COMPLIANCY
wqd_tmp = ( q_tmp_1 - q_tmp_2 ) / 32768;
#else
/* WARNING: the below instruction is not MISRA compliant, user should verify
that Cortex-M3 assembly instruction ASR (arithmetic shift right) is used by
the compiler to perform the shift (instead of LSR logical shift right) */

// IQ的计算,计算完了,去各种限幅。 右移15是sin和cos/32768得到真正的值,又回到16位以内
wqd_tmp = ( q_tmp_1 - q_tmp_2 ) >> 15;
#endif

/* Check saturation of Iq */
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;
}

/*No overflow guaranteed*/
d_tmp_1 = Input.alpha * ( int32_t )Local_Vector_Components.hSin;

/*No overflow guaranteed*/
d_tmp_2 = Input.beta * ( int32_t )Local_Vector_Components.hCos;

/*Id component in Q1.15 Format */
#ifdef FULL_MISRA_C_COMPLIANCY
wqd_tmp = ( d_tmp_1 + d_tmp_2 ) / 32768;
#else
/* WARNING: the below instruction is not MISRA compliant, user should verify
that Cortex-M3 assembly instruction ASR (arithmetic shift right) is used by
the compiler to perform the shift (instead of LSR logical shift right) */
wqd_tmp = ( d_tmp_1 + d_tmp_2 ) >> 15;
#endif

/* Check saturation of Id */
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
/* =================================================================================
File name: IPARK.H
===================================================================================*/

#ifndef __IPARK_H__
#define __IPARK_H__

typedef struct { _iq Alpha; // Output: stationary d-axis stator variable
_iq Beta; // Output: stationary q-axis stator variable
_iq Angle; // Input: rotating angle (pu)
_iq Ds; // Input: rotating d-axis stator variable
_iq Qs; // Input: rotating q-axis stator variable
_iq Sine; // Input: Sine term
_iq Cosine; // Input: Cosine term
} IPARK;

/*-----------------------------------------------------------------------------
Default initalizer for the IPARK object.
-----------------------------------------------------------------------------*/
#define IPARK_DEFAULTS { 0, \
0, \
0, \
0, \
0, \
0, \
0, \
}

/*------------------------------------------------------------------------------
Inverse PARK Transformation Macro Definition
------------------------------------------------------------------------------*/


#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 // __IPARK_H__

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;

// 查表得到cos(theta)和sin(theta)
Local_Vector_Components = MCM_Trig_Functions( Theta );

/*No overflow guaranteed*/
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
/* WARNING: the below instruction is not MISRA compliant, user should verify
that Cortex-M3 assembly instruction ASR (arithmetic shift right) is used by
the compiler to perform the shift (instead of LSR logical shift right) */
// 计算转换后的输出alpha
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
/* WARNING: the below instruction is not MISRA compliant, user should verify
that Cortex-M3 assembly instruction ASR (arithmetic shift right) is used by
the compiler to perform the shift (instead of LSR logical shift right) */
// 计算转换后的输出beta
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; // 存放电网电压变换到alpha,beta轴上的值
float32 d,q; // 存放电网电压变换到d,q轴上的值
float32 theta; // 存放电网电压当前相位角度
float32 sin_sita,cos_sita; // 存放根据theta计算出来的正弦余弦值
void (*Clarke)(); // 坐标变换有四个函数,每个函数在内存都有一块区域,定义四个函数指针变量,指向这四个函数的首地址
void (*Park)(); // 注意函数指针的定义的第二个括号里面是最好不要写东西。因为函数指针的意思是这个指针变量指向任意函数,我们并不知道指向的函数的参数情况,所以在定义函数指针的时候,不能指定函数参数。
void (*InvPark)();
void (*InvClarke)();
} CORDI_TRANSFER;

// 上面的数据类型中,有四个函数指针,那么我们还要定义四个坐标变换函数。

// Clarke
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));
}

// park
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;
}

// inv-clarke
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;
}

// inv-park
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,//这四个函数名代表函数的首地址,而定义CORDI_TRANSFER的时候,
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中。后面的几个变换按照上面的类推就行。