Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the wp-statistics domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /www/wwwroot/robotattractor/wp-includes/functions.php on line 6114

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the jetpack domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /www/wwwroot/robotattractor/wp-includes/functions.php on line 6114

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the updraftplus domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /www/wwwroot/robotattractor/wp-includes/functions.php on line 6114
Codesys PLC中实现卡尔曼滤波器 – 机器人家园

1 卡尔曼滤波基本步骤

  卡尔曼滤波包含两个步骤:
  1 预测:

  2 更新:

  其中,$K_k$是卡尔曼增益,它是卡尔曼滤波器的精华,定义如下

  以上公式采用的符号与维基百科—卡尔曼滤波保持一致。
  如果你理解上面的公式有困难,那么下面我给出一种简单的解释。
  以机器人定位为例,状态$x$代表机器人的位置和姿态(x,y平面坐标和朝向角度),控制$u$代表机器人的控制输入量(例如左右两车轮的转速)。直观地看,第一步的预测其实就是使用机器人的运动方程进行状态的递推。我们都知道,模型都是有误差的,采集的控制量也不可能完全与实际的一样,这就造成了理论计算值与机器人真实状态的偏差。我们用$x$表示理论计算值,用$x_t$(下标t表示true)表示机器人真实的状态。短时间内,这个偏差是可以接受的,但问题是这个偏差会一直累积,导致我们的理论推测值与真实值越差越多,最后完全不可相信了。所以卡尔曼滤波的第一步就是单纯的里程定位或者航迹推测,这没什么新鲜的。
  那么你可能会问,协方差矩阵$P$代表了什么呢?协方差矩阵$P$代表了我们对估计值$x$相信的程度,我们用它对信任程度进行量化。$P$越小说明我们对估计$x$越有信心,反之如果$P$越大我们就越怀疑$x$。这就和人一样,有些人位高权重我们就越倾向于不假思索地相信他们说的话,有些人经常胡说八道我们就会怀疑他观点的正确性。在后面的计算中,我们会根据$P$的大小来分配权重,权重和$P$的大小成反比。
  既然只依靠第一步不靠谱,我们就引入了传感器来弥补。这就是第二步的更新,用传感器的测量结果($z$)来矫正偏差。可是我们知道,传感器测量值也不是百分之百准确的,它也有不确定度($R$),我们也不能完全相信它。我们面对两个都没有完全说真话的人,这个时候到底该怎么办呢?是只信任其中一个还是综合考虑两个人的信息呢?肯定不能完全信任一个人,但是怎么综合考虑最好却是门艺术。卡尔曼滤波巧妙的地方就在于,它没有简单粗暴地直接把二者加起来,而是给其中一项前面乘了个系数,这就是卡尔曼增益$K$。卡尔曼证明了这样设计的增益$K$是最优的。没错,卡尔曼增益就是前面提到的权重,它综合了两个信息来源,一个是状态估计,一个是传感器的测量。根据卡尔曼增益$K$的构造方法我们可以猜出来,$P$越大$R$越小,那么$K$就越接近1,这意味着我们就越相信传感器测量,而不信状态的估计值;反之,$P$越小$R$越大,那么$K$就越接近0,这意味着我们就越相信状态的估计值,而不信传感器的测量。
  这样我们就解决了到底该相信谁的问题。  

2 数学基础

  如果你要估计的状态是向量,那么卡尔曼滤波就需要矩阵计算。而且它的计算过程基本上把常用的矩阵计算都占了,包括矩阵的加、减、乘、求逆、转置,矩阵与向量的乘,向量的加、减、内积、外积、标量与向量乘等等。这非常有意思,有点像我们练毛笔字中的“永”字,所有常用笔画集于一身。如果把卡尔曼滤波实现了,那么实现其它的算法就触类旁通了。好了,大学里的线性代数课本该拿出来了。
  所有的矩阵运算中,求逆是最复杂的。文献$[1]$使用了伴随矩阵计算逆矩阵,这种方法适合小规模的矩阵(维数<4),对于大规模矩阵效率较低。目前,矩阵求逆最常用而且比较高效的方法是LU分解法,思路是首先将矩阵分解成两个三角矩阵(的乘积),然后再计算两个三角矩阵的逆。由于计算三角矩阵的逆比较容易,所以这样我们能够高效地求矩阵的逆。但是在实际应用LU分解法时有一点要注意,如果矩阵的对角线上出现了0,就会使分解的三角矩阵不可逆,这可以通过对矩阵的行重新排列来解决,这样就多了一个置换矩阵,细节可以看维基百科。
  维基百科提供了LU分解的C语言代码,我直接将C代码翻译成ST代码。在C语言程序中,直接在输入矩阵上做了计算,即将分解结果保存在了输入矩阵$A$中。如果你想提取出两个三角矩阵(L和U),可以按照下式这样分解,其中$I$是单位矩阵。注意这里的矩阵$A$是被更新过的矩阵,不再是原始的输入$A$了哦。当然,从分解的三角矩阵L和U可以还原得到最开始的输入$A$,只需要再乘以一个置换矩阵就行了。

$$
\begin{aligned}
A=(L-I)+U
\end{aligned}
$$

3 PLC具体实现

3.1 PLC编程语言

  我们用PLC做做数字的加减乘除很容易,但是要实现矩阵计算就有点复杂了,有过PLC编程经验的同志都知道,PLC一般不提供矩阵计算所需的函数。原因也很简单,早期的PLC主要被设计用于逻辑控制和过程控制,不是用来实现复杂的运动控制和数据处理算法的。这也是在PLC中实现的卡尔曼滤波主要的难点所在,基础支持太少。常用的PLC编程语言有梯形图和ST。其中,梯形图适合实现逻辑流程控制,不适合数值计算,所以我们采用ST语言这种与高级语言比较类似的PLC语言来实现卡尔曼滤波。由于现在的PLC语言基本都遵守IEC 61131-3国际标准,所以我们只在一种PLC环境中实现,移植到其它厂家的PLC产品将非常容易。我们选择codesys软PLC,它的编程软件免费而且使用方便,还支持本地仿真调试,不像西门子一样还要安装庞大而缓慢的博图软件。
  codesys官方网店提供矩阵计算库(Matrix Library),可实现常用的矩阵操作,例如相加、乘、逆、行列式,但是你要花250欧元(人民币1932元)购买授权才能使用。

3.2 数据类型

  既然我们用PLC语言实现,那就要熟悉PLC语言的特点。首先面对的问题就是如何定义一个矩阵或者向量。codesys提供了ARRAY数组这种数据类型,可以用来实现矩阵或向量的表示。ARRAY最多支持三维,我们只使用一维和二维就够了。定义二维数组的方法如下,其中,0..2表示数组元素索引的上下界,从A[0,0]开始,最后一位是A[2,2]。可以在定义数组时就初始化,注意对于高维数组,在初始化时采用一维拉直的写法,这就需要约定好是按行拉直还是按列拉直。我们规定一下,本文都采用行主序的存储方式,也就是说矩阵按照行拉直。注意MATLAB采用了Column major order(列主序),而C语言一般是Row major order(行主序),我们与C语言一致。
  

VAR
    A:ARRAY[0..2,0..2] OF REAL:=[1,2,3,4,5,6,7,8,9];
END_VAR

  如何操作ARRAY数组呢?如果我们想引用数组中的元素,例如读写,使用A[i,j]即可,其中i和j就是矩阵中元素的行列。
  如果我们想传递数组该怎么办呢,例如将数组作为一个参数传递给函数块?数组名就像C语言中的指针,可以直接传递数组名,例如A:=FUN(B)。也可以直接用数组名给另一个数组赋值,例如A:=B。
  数组的大小可以在定义时设为固定值,在程序执行过程中始终保持不变。这样所有的矩阵运算函数都只接收返回固定大小的数组。但是这样编写的程序没有通用性,遇到一个新的问题或者加入新的条件我们就要修改数组大小,很繁琐。有一个方法是使用可变长度数组$^{[2,3]}$(Arrays of variable length)。但是注意,可变长度数组只能用于VAR_IN_OUT声明中,不能用在其它声明中(例如VAR或者VAR_IN),下面我给出两个例子。
  第一个例子是计算两个向量的内积,将其定义为函数(FUN),声明部分如下。Inner 函数的返回值是REAL类型,输入是两个可变长度的一维数组:v1和v2,二者长度必须相等,注意v1和v2的定义放在了VAR_IN_OUT中,而不是VAR中。
  

FUNCTION Inner : REAL;
VAR
    i:LINT;
END_VAR
VAR_IN_OUT
   v1: ARRAY[*] OF REAL;    //输入向量1
   v2: ARRAY[*] OF REAL;    //输入向量2
END_VAR

  函数主体部分如下。在遍历数组元素时使用了系统自带的LOWER_BOUND和UPPER_BOUND函数来获取数组的上下界。
  

Inner:=0;
FOR i:=LOWER_BOUND(v1,1) TO UPPER_BOUND(v1,1) BY 1 DO 
    Inner:=Inner+v1[i]*v2[i];
END_FOR;

  函数使用的方式如下。该函数由于使用了可变长度的数组,所以不限制输入向量的维数,对于任意维数的数组都可以给出正确的结果。

result:=Inner(v1,v2);

  第二个例子是两个矩阵相加,声明部分代码如下:

FUNCTION MatAdd: BOOL;
VAR_IN_OUT
   A: ARRAY[*,*] OF REAL;   //输入矩阵
   B: ARRAY[*,*] OF REAL;   //输入矩阵
   C: ARRAY[*,*] OF REAL;   //输出矩阵
END_VAR
VAR
    i:LINT;
    j:LINT;
END_VAR

  函数主体部分如下:

FOR i:=LOWER_BOUND(A,1) TO UPPER_BOUND(A,1) BY 1 DO 
    FOR j:=LOWER_BOUND(A,2) TO UPPER_BOUND(A,2) BY 1 DO 
        C[i,j]:=A[i,j]+B[i,j];
    END_FOR;
END_FOR;

  该函数的使用方式如下,注意MatAdd前面没有赋值符号(:=),而是直接把想要赋值的数组作为参数传递给了这个函数(的第三个输入参数C),C数组里保存着我们想要的结果。

MatAdd(Mat1,Mat2,Mat3);

3.3 矩阵求逆

  LU分解法的具体ST代码如下,基本与C语言版本一样,只有一个地方不同:C语言可以使用指针,但是ST语言的指针不太好用,所以我们不得不挨个操作矩阵中的每个元素,导致代码略长。

    FOR i:=0 TO n-1 BY 1 DO 
        maxA:=0.0;
        imax:=i;
        FOR k:=i TO n-1 BY 1 DO 
            absA:=ABS(A[k,i]);
            IF absA>maxA THEN
                maxA:=absA;
                imax:=k;
            END_IF
        END_FOR;

        IF imax<>i THEN
            //pivoting P
            j:=P[i];
            P[i]:=P[imax];
            P[imax]:=j;
            //pivoting rows of A
            FOR k:=0 TO n-1 BY 1 DO 
                tmp[k]:=A[i,k]; 
            END_FOR;
            FOR k:=0 TO n-1 BY 1 DO 
                A[i,k]:=A[imax,k]; 
            END_FOR;
            FOR k:=0 TO n-1 BY 1 DO 
                A[imax,k]:=tmp[k]; 
            END_FOR;
        END_IF

        FOR j:=i+1 TO n-1 BY 1 DO 
            A[j,i]:=A[j,i]/A[i,i]; 
            FOR k:=i+1 TO n-1 BY 1 DO 
                A[j,k]:=A[j,k]-A[j,i]*A[i,k]; 
            END_FOR;
        END_FOR;
    END_FOR;

  完成LU分解后,下面再求逆就容易了,代码如下。

    FOR j:=0 TO n-1 BY 1 DO 
        FOR i:=0 TO n-1 BY 1 DO 
            IF P[i]=j THEN
                IA[i,j]:=1;
            ELSE
                IA[i,j]:=0; 
            END_IF; 
            FOR k:=0 TO i-1 BY 1 DO 
                IA[i,j]:=IA[i,j]-A[i,k]*IA[k,j];
            END_FOR;
        END_FOR;
        FOR i:=n-1 TO 0 BY -1 DO        
            FOR k:=i+1 TO n-1 BY 1 DO 
                IA[i,j]:=IA[i,j]-A[i,k]*IA[k,j];
            END_FOR;
            IA[i,j]:=IA[i,j]/A[i,i];
        END_FOR;
    END_FOR;

  完整的卡尔曼滤波迭代过程代码如下:

FOR i:=0 TO 100 BY 1 DO
        //Prediction
        x:=VecAdd(VecAdd(MatVecMul(F,x),u)); 
        P:=MatAdd(MatMul(MatMul(F,P),MatTranspose(F)),Q);
        //Update
        K:=VecMul(MatVecMul(P,H),MatInv(KroneckerProduct(H,P),MatTranspose(H))+Rc);
        x:=VecAdd(x,VecMul(K,z-Inner(H,x)));
        P:=MatMul(MatSub(IdentityMatrix(n),KroneckerProduct(K,H)),P);
    END_FOR;

4 具体案例

  这里我们用一个例子来检验代码的正确性,例子来自于[4],其与文献[1]差不多,是一个小球做自由落体运动,估计它的高度和速度。
  
  文献[4]给出了MATLAB代码,如下。当然,这里只是仿真结果,噪声用randn函数生成。因为是仿真,为了对比和计算传感器观测值,还定义了真实的系统状态xt。仿真中,真实状态xt是没有噪声的(完美的),估计值是有噪音的。

% Define the system
N = 100;  % number of time steps
dt = 0.01;  % Sampling time (s)
t = dt*(1:N);  % time vector (s)
F = [1, dt; 0, 1];  % system matrix - state
G = [-1/2*dt^2; -dt];  % system matrix - input
H = [1 0];  % observation matrix
Q = [0, 0; 0, 0];  % process noise covariance
u = 9.80665;  % input = acceleration due to gravity (m/s^2)
I = eye(2);  % identity matrix
% Define the initial position and velocity
y0 = 100;  % m
v0 = 0;  % m/s
% Initialize the state vector (true state)
xt = zeros(2, N);  % True state vector
xt(:, 1) = [y0; v0];  % True intial state
% Loop through and calculate the true state
for k = 2:N 
xt(:, k) = F*xt(:, k-1) + G*u;  % Propagate the states through the prediction equations
end
% Generate the noisy measurement from the true state
R = 4;  % m^2/s^2
v = sqrt(R)*randn(1, N);  % measurement noise
z = H*xt + v;  % noisy measurement
%% Perform the Kalman filter estimation
% Initialize the state vector (estimated state)
x = zeros(2, N);  % Estimated state vector
x(:, 1) = [105; 0];  % Guess for initial state
% Initialize the covariance matrix
P = [10, 0; 0, 0.01];  % Covariance for initial state error
% Loop through and perform the Kalman filter equations recursively
for k = 2:N 
x(:, k) = F*x(:, k-1) + G*u;  % Predict the state vector
P = F*P*F' + Q;  % Predict the covariance
K = P*H'/(H*P*H' + R); % Calculate the Kalman gain matrix
x(:,k) = x(:,k) + K*(z(k) - H*x(:,k)); % Update the state vector
P = (I - K*H)*P; % Update the covariance
end
%% Plot the results
plot(t, x(1,:), 'b--', 'LineWidth', 2); 
hold on; plot(t, xt(1,:), 'r:', 'LineWidth', 1.5) 
xlabel('t (s)'); ylabel('x_1 = h (m)'); grid on; 
legend('Measured','Estimated','True'); 

参考资料

[1] 卡尔曼滤波器在PLC系统中的实现,徐承爱,自动化技术与应用, 2014.
[2] https://help.codesys.com/api-content/2/codesys/3.5.12.0/en/_cds_datatype_array/
[3] IEC 61131-3: Arrays with variable length
[4] A Kalman Filtering Tutorial for Undergraduate Students

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注