Open Source
    Microcontroller
    Automatic Control
    Coding Notes

2014年8月23日 星期六

Arduino〈倒單擺 Inverted Pendulum〉
(5) 數學推導與Processing模擬

晚上10:11 Posted by Unknown , , , No comments

數學推導與模擬是在設計控制器前,了解一個系統必經的過程之一,雖然倒單擺作為一經典教材,其運動方程式幾乎已成陳腔濫調一般,各種變化形式,或者更複雜的倒單擺模型亦是層出不窮,幾乎在網路上都找得到。不過本篇文章還是會先回到最原始的倒單擺數學推導,並搭配Processing軟體模擬其動態,作為專題記錄。





得到倒單擺的運動方程式之後,下一步就是透過電腦模擬分析來作為設計控制器前的準備。學校裡大概都是使用MATLAB來執行這樣的工作,先將方程式寫入m-file,再由主程式呼叫ode45進行求解,然而ode45這個經典的數值分析方法並非MATLAB不可,任何可執行計算的程式語言都可以套用。所以若是已經厭倦每次模擬都只能看到醜醜的響應曲線圖,不妨試用Processing,因為其以繪圖見長,直接把模型的動態顯示出來不是更直觀嗎?
那麼ode45很難寫嗎? 其實不然,有興趣可以找找MATLAB或是其他相似軟體的原始碼,在其函數中真正執行RK45(Runge-Kutta-Fehlberg Method)的部分大概也只佔其函數定義三分之一的篇幅。

在Processing中,我把倒單擺的參數設定、模型顯示、數學描述與RK45都包在一個class中,由於計算中會用到向量與矩陣,所以還必須定義一些相關的函數,或是直接import JAVA的矩陣類別也可以,範例如下:

class invPendulum {
  // System Parameters
  final float gravity = 9.8,
              massPendulum = 1.5,
              massCart = 0.3,
              lengthArm = 0.6,
              momentInertia = massPendulum * lengthArm * lengthArm,
              A = lengthArm + momentInertia / (massPendulum * lengthArm),
              B = ( massPendulum + massCart ) / (massPendulum * lengthArm);

  int systemOrder = 4;
  // Initial Condition
  float iniTheta = 0.1, 
        iniOmega, 
        iniDisplacement, 
        iniVelocity;
  
  //  State Variables
  float displacement,
        velocity,
        theta,
        thetaLimit = 0.78,        
        omega,   
        
        z[] = new float[systemOrder],
        zTemp[] = new float[systemOrder],
        z4[] = new float[systemOrder],
        z5[] = new float[systemOrder];
   
  // Calculation Variables
  float torque,
        force,
        alpha,
        acceleration;

  // Adaptive RK45 Method Parameters
  final float tolerance = 0.001,
              maxIteration = 5,
        
              a2 = 0.25,
              a3[] = {3/32f, 9/32f},
              a4[] = {1932/2197f, -7200/2197f, 7296/2197f},
              a5[] = {439/216f, -8.0, 3680/513f, -845/4104f},
              a6[] = {-8/27f, 2.0, -3544/2565f, 1859/4104f, -11/40f},
        
              b1[] = {16/135f, 0.0, 6656/12825f, 28561/56430f, -9/50f, 2/55f},  // Order 5
              b2[] = {25/216f, 0.0, 1408/2565f, 2197/4101f, -0.2, 0.0},       // Order 4
        
              c[] = {0.25, 3/8f, 12/13f, 1.0, 0.5};

  float h = 0.01,        
        k1[] = new float[systemOrder],
        k2[] = new float[systemOrder],
        k3[] = new float[systemOrder],
        k4[] = new float[systemOrder],
        k5[] = new float[systemOrder],
        k6[] = new float[systemOrder];
    
  // Control Signal
  float u = 0;

  // Visualized Model
  PShape redPin;
  final float refX = 729.546, refY = 1839.63,
              scaleFactor = 0.1;
  PVector refPoint;
  
  float groundHeight;
  
  invPendulum(int W, int H) {
    
    redPin = loadShape("redPin.svg");
    redPin.scale(scaleFactor);
    refPoint = new PVector( scaleFactor * refX , scaleFactor * refY);    
    iniDisplacement = W/2;
    groundHeight = 0.5 * H;  
    println( A * B );
    
    displacement = iniDisplacement;
    velocity = iniVelocity;
    theta = iniTheta;
    omega = iniOmega;
    
    z[0] = displacement;
    z[1] = velocity;
    z[2] = theta;
    z[3] = omega;
  }
  
  float[] equationMotion(float x[]) {
    
    float k[] = new float[systemOrder];
    
    force = A * ( u + pow(x[3], 2) * sin(x[2]) ) - 0.5 * gravity * sin(2 * x[2]);
    acceleration = force / ( A * B - pow(cos(x[2]), 2) );
    
    torque = gravity * B * sin(x[2]) - 0.5 * pow(x[3], 2) * sin(2 * x[2]) - u * cos(x[2]);
    alpha = torque / ( A * B - pow(cos(x[2]), 2) );
    
    k[0] = x[1];
    k[1] = acceleration;
    k[2] = x[3];
    k[3] = alpha;
    
    return k;
    
  } // float[] equationMotion(float x[])
  
  float[] equationConstrain(float x[]) {
    
    float k[] = new float[systemOrder];
    
    force = A * u;
    acceleration = force / ( A * B - pow(cos(x[2]), 2) );
    
    torque = - u * cos(x[2]);
    alpha = torque / ( A * B - pow(cos(x[2]), 2) );
    
    k[0] = x[1];
    k[1] = acceleration;
    k[2] = x[3];
    k[3] = alpha;
    
    return k;
    
  } // float[] equationConstrain(float x[])
  
  void RK45() {
    
    float eps[] = new float[systemOrder], maxEps = 1.0;
    float s;
    int iterationCount = 0;
      
    if( abs(z[2]) < thetaLimit ) {
      
      while( (iterationCount < maxIteration) && (maxEps > tolerance) ) {
        k1 = vectorScaling( equationMotion(z), h );
        
        zTemp = vectorAddition( z,
                                vectorScaling(k1, 2) );  
        k2 = vectorScaling( equationMotion(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition( z,
                                vectorScaling(k1, a3[0]) ),
                                vectorScaling(k2, a3[1]) );
        k3 = vectorScaling( equationMotion(zTemp), h );
         
        zTemp = vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a4[0]) ), 
                                vectorScaling(k2, a4[1]) ),
                                vectorScaling(k3, a4[2]) );
        k4 = vectorScaling( equationMotion(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a5[0]) ), 
                                vectorScaling(k2, a5[1]) ),
                                vectorScaling(k3, a5[2]) ),
                                vectorScaling(k4, a5[3]) );
        k5 = vectorScaling( equationMotion(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition(
                vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a6[0]) ), 
                                vectorScaling(k2, a6[1]) ),
                                vectorScaling(k3, a6[2]) ),
                                vectorScaling(k4, a6[3]) ),
                                vectorScaling(k5, a6[4]) );
        k6 = vectorScaling( equationMotion(zTemp), h );

  //      println("k1");
  //      println(k1);
  //      println("k2");
  //      println(k2);
  //      println("k3");
  //      println(k3);
  //      println("k4");
  //      println(k4);
  //      println("k5");
  //      println(k5);
  //      println("k6");
  //      println(k6);   

        z4 = vectorAddition(
             vectorAddition(
             vectorAddition( 
             vectorAddition( z,
                             vectorScaling(k1, b2[0]) ), 
                             vectorScaling(k3, b2[2]) ),
                             vectorScaling(k4, b2[3]) ),
                             vectorScaling(k5, b2[4]) );

        z5 = vectorAddition(
             vectorAddition(
             vectorAddition(
             vectorAddition( 
             vectorAddition( z,
                             vectorScaling(k1, b1[0]) ), 
                             vectorScaling(k3, b1[2]) ),
                             vectorScaling(k4, b1[3]) ),
                             vectorScaling(k5, b1[4]) ),
                             vectorScaling(k6, b1[5]) );
        
  //      println("z4");
  //      println(z4);
  //      println("z5");
  //      println(z5);      
        
        eps = vectorAddition( z5, vectorScaling( z4, -1) ); 
        eps = vectorAbs(eps);
        maxEps = max(eps);
        
  //      println(eps);
  //      println(maxEps);
  //      println(h);
  
        s = 0.84 * pow( tolerance * h / (vectorNorm(eps)) , 0.25 );
        
  //      println(s);
  
        if( maxEps > tolerance ) {        
          h *= s;
        } 
        
      } // while( (iterationCount < maxIeration) && (maxEps > tolerance) )
      
      z = z5;
    }
        
    else {
      
      z[2] = constrain(z[2], -thetaLimit, thetaLimit);
      z[3] = 0;
     
      while( (iterationCount < maxIteration) && (maxEps > tolerance) ) {
        k1 = vectorScaling( equationConstrain(z), h );
        
        zTemp = vectorAddition( z,
                                vectorScaling(k1, 2) );  
        k2 = vectorScaling( equationConstrain(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition( z,
                                vectorScaling(k1, a3[0]) ),
                                vectorScaling(k2, a3[1]) );
        k3 = vectorScaling( equationConstrain(zTemp), h );
         
        zTemp = vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a4[0]) ), 
                                vectorScaling(k2, a4[1]) ),
                                vectorScaling(k3, a4[2]) );
        k4 = vectorScaling( equationConstrain(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a5[0]) ), 
                                vectorScaling(k2, a5[1]) ),
                                vectorScaling(k3, a5[2]) ),
                                vectorScaling(k4, a5[3]) );
        k5 = vectorScaling( equationConstrain(zTemp), h );
        
        zTemp = vectorAddition(
                vectorAddition(
                vectorAddition(
                vectorAddition( 
                vectorAddition( z,
                                vectorScaling(k1, a6[0]) ), 
                                vectorScaling(k2, a6[1]) ),
                                vectorScaling(k3, a6[2]) ),
                                vectorScaling(k4, a6[3]) ),
                                vectorScaling(k5, a6[4]) );
        k6 = vectorScaling( equationConstrain(zTemp), h );
        
  //      println("k1");
  //      println(k1);
  //      println("k2");
  //      println(k2);
  //      println("k3");
  //      println(k3);
  //      println("k4");
  //      println(k4);
  //      println("k5");
  //      println(k5);
  //      println("k6");
  //      println(k6); 
  
        z4 = vectorAddition(
             vectorAddition(
             vectorAddition( 
             vectorAddition( z,
                             vectorScaling(k1, b2[0]) ), 
                             vectorScaling(k3, b2[2]) ),
                             vectorScaling(k4, b2[3]) ),
                             vectorScaling(k5, b2[4]) );
        
        z5 = vectorAddition(
             vectorAddition(
             vectorAddition(
             vectorAddition( 
             vectorAddition( z,
                             vectorScaling(k1, b1[0]) ), 
                             vectorScaling(k3, b1[2]) ),
                             vectorScaling(k4, b1[3]) ),
                             vectorScaling(k5, b1[4]) ),
                             vectorScaling(k6, b1[5]) );
        
  //      println("z4");
  //      println(z4);
  //      println("z5");
  //      println(z5);      
        
        eps = vectorAddition( z5, vectorScaling( z4, -1) ); 
        eps = vectorAbs(eps);
        maxEps = max(eps);
        
  //      println(eps);
  //      println(maxEps);
  //      println(h);
  
        s = 0.84 * pow( tolerance * h / (vectorNorm(eps)) , 0.25 );
        
  //      println(s);
  
        if( maxEps > tolerance ) {        
          h *= s;
        } 
      } // while( (iterationCount < maxIeration) && (maxEps > tolerance) )
      
      z = z5;
      
    }     
    
    if( abs(z[2]) >= thetaLimit ) {
      z[2] = constrain(z[2], -thetaLimit, thetaLimit);
      z[3] = 0;
    }
    
    displacement = z[0];
    velocity = z[1];
    theta = z[2];
    omega = z[3];
    
    if( millis() > 2000  & millis() < 2009  & u == 0 ) {
      u = 350;
      println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    } else {
      u = 0;
    }
    
    println(omega);

  } // void RK45()
  void updateModel() {
    
    redPin.rotate(theta);
    
    pushStyle();
    fill(200, 250, 150, 180);
    smooth();
    ellipse(iniDisplacement + 100 * (displacement - iniDisplacement), groundHeight, 20, 20);
    stroke(200, 100);
    line(0, groundHeight, width, groundHeight);
    popStyle();
    
    shape(redPin, 
          iniDisplacement + 100 * (displacement - iniDisplacement) - refPoint.x*cos(theta) + refPoint.y*sin(theta),  // Display with 'cm' in horizontal direction
          groundHeight - refPoint.x*sin(theta) - refPoint.y*cos(theta));
          
    redPin.resetMatrix();
    redPin.scale(scaleFactor);
  }
  
}  // class invPendulum


一些向量計算的函數:
static float[] vectorScaling(float x[], float a) {
    int s = x.length;
    float[] _x = new float[s];
    for(int j = 0; j < s; j++) {
      _x[j] = a * x[j];
    }
    return _x;
  } // float[] vectorScaling(float x[], float a)
  
static float[] vectorAddition(float x[], float y[]) {
    int sx = x.length;
    int sy = y.length;
    float[] _x = new float[sx];
    if(sx == sy) {
      for(int j = 0; j < sx; j++) {
        _x[j] = x[j] + y[j];
      }
      return _x;
    } else {
      println("Vector Domension Error! Can't Apply Addition Calculation!");
      float[] error = {-1};
      return error;
    }
  } // float[] vectorAddition(float x[], float y[])
  
static float[] vectorAbs(float x[]) {
    int s = x.length;
    float[] _x = new float[s];
    
    for(int j = 0; j < s; j++) {
      _x[j] = abs(x[j]);
    }
    return _x;
  } // float[] vectorAbs(float x[])
  
static float vectorNorm(float x[]) {
    int s = x.length;
    float y = 0;
    for(int j = 0; j < s; j++) {
      y += sq(x[j]);
    }
    return sqrt(y);
  } // float[] vectorNorm(float x[])  

整體程式碼並不長,要特別注意的地方可能是在運動方程式與求解的部分需對邊界條件(Boundary)另外作處理,例如傾斜角度有最大值,倒單擺在邊界上的運動方程式是不同的,必須分開計算。寫好class之後,只要在主程式中呼叫就可以看到自訂的倒單擺模型並即時顯示其模擬的動態。
利用Processing 跑倒單擺模擬

另外網路上可以找到非常多關於RK45 mothed 的理論講解,基本上套入公式即可,一些參考資料如下:


有模擬器的協助,便比較能夠掌握控制器的設計及預期的結果。

2014年8月19日 星期二

Arduino〈倒單擺 Inverted Pendulum〉(4) 狀態回授控制

下午6:21 Posted by Unknown , , , No comments
狀態回授(State Feedback)是典型負回授系統中會使用的控制策略,方法非常直觀,就是將量測到的狀態(x)乘上增益(K)後回授,以此為新的控制命令(u),再輸入到控制對象(plant)中,如此反覆迭代,若增益選取得當,系統為穩定,狀態回授值會趨向參考值,此時控制命令趨近於零,而系統輸出(y)進入穩態(Steady State)。

來複習一下課本的小知識...

下面是一個典型的二階線性系統,彈簧(k) ─ 阻尼(c) ─ 質量塊(m)系統及其對應的運動方程式(equation of motion):



u為系統的施力,在控制方塊與硬體中,也可對應為控制命令或是馬達轉動所需的電壓。
由運動方程式可推導系統的特徵方程式,其方程式的根就是特徵值(eigenvalues),也是系統的極點(poles),這些抽象的值實際上決定了控制系統表現的行為,極點的實部表示衰減行為,虛部顯示振盪行為,這些皆可由系統參數(m、c、k)推導而得其量值。
考慮施加一控制力u,質量塊會由a點移動到b點,(b - a) 為位移 ( x ), a 到 b 之間的運動過程被稱為暫態響應(transient response),其運動的方式被極點決定,例如多快到達b點(settling time),運動是否會過頭(overshoot),振動的量值等等。若對這些行為不滿意,便可以透過控制的手段來改善,這時候狀態回授的方法便派上用場。

將感測器量到的狀態(位移量、速度等)乘以一個係數回授加到新的控制命令中,使得:



代入原來方程式變成

整裡可得


可以發現透過狀態回授的方法,可以改變原方程式的係數,也就像是改變系統的參數,比如說原機構中的彈簧,其彈性係數由 k 變成 k + K1,而阻尼也有類似的效果。當參數不同,便會產生新的特徵方程式與特徵值,那麼極點的位置也就隨之變化,藉此可以改善系統的行為,像是反應速度變快或是變穩定等等。

了解這個原理之後,再回到倒單擺。倒單擺的數學模型當然沒這麼簡單,基本上是非線性的,然而利用泰勒展開的概念可以推導其 Jacobian 矩陣,那麼就可以將原運動方程在平衡點進行線性化,得到類似上面的結果。

不過在程式撰寫方面,模型有沒有線性化並不是那麼重要,實際上程式有很大一部分都在處理和數學與控制無關的事......
下面是一個運用狀態回授進行倒單擺控制的Arduino程式碼範例,由於內容包含了自訂的資源庫(libary),相關數值的設定也必須搭配實際的硬體,所以要套用到其他的模組,程式必須得在作調整。

#include <Wire.h>
#include <SPI.h>
#include "InvPenSensor.h"
#include "InvPenActuator.h"

#define MIN_COMMAND 80
#define GAIN_ANGLE 2150
#define GAIN_OMEGA 20
#define REF 0.175

InvPenSensor sensor;
InvPenActuator actuator;

int command, forwardCount = 0, backwardCount = 0;
float omega, lastOmega = 0, pseudoOmega,
      angle, lastAngle = 0, pseudoAngle, 
      dt, reference = 0.175;
unsigned long time = 0, steady = 0;

void setup() {
  Serial.begin(9600);
  sensor.init();
  actuator.init();
  while(lastAngle == 0) {
    time = micros();
    lastAngle = sensor.getAngle();
  }
  steady = millis();
  steadyProcess();
}

void steadyProcess() {
  while( (millis() - steady) < 2000) {
    pseudoAngle = sensor.getAngle();
    dt = 0.000001 * (micros() - time);
    time = micros();
    sensor.getGY(&pseudoOmega);
    omega = pseudoOmega * PI / 2587.5;
    if( pseudoAngle != 0) {
      angle = 0.5 * (lastAngle + omega * dt) + 0.5 * pseudoAngle;
    } else {
      angle = lastAngle + omega * dt;
    }  
    lastAngle = angle;
  }
  sensor.getGY(&pseudoOmega);
  lastOmega = pseudoOmega * PI / 2587.5;
}

int counter = 0;

void loop() {
  while(1) {
    command = MIN_COMMAND;
    pseudoAngle = sensor.getAngle();
    dt = 0.000001 * (micros() - time);
    time = micros();
    sensor.getGY(&pseudoOmega);
    omega = 2 * pseudoOmega * PI / 2587.5 - lastOmega;
    if( pseudoAngle != 0) {
      angle =  0.98 * (lastAngle + omega * dt) + 0.02 * pseudoAngle;
    } else {
      angle = lastAngle + 1.5 * omega * dt;
    }  
    lastAngle = angle;
    lastOmega = omega;
    
/////////////////////////////////////////////////////////////////////////
  
      if( (angle - reference) > 0.0) {      
          command += GAIN_ANGLE * (angle - reference);
          if(omega > 0.00) {
            command += GAIN_OMEGA * omega;
          }
          command = constrain(command, MIN_COMMAND, 255);
          actuator.driveBackward(command);
          forwardCount = 0;
          backwardCount ++;
//          Serial.println("BACK"); 
  
      } else if( (angle - reference) < -0.0) {        
          command -= GAIN_ANGLE * (angle - reference);
          if(omega < -0.00) {
            command -= GAIN_OMEGA * omega;
          }
          command = constrain(command, MIN_COMMAND, 255);
          actuator.driveForward(command);
          backwardCount = 0;
          forwardCount ++;
//          Serial.println("FRONT");
      }
      
      reference = REF + 0.00002 * (forwardCount - backwardCount);

  } // while(1)
    
}

這裡是一個實際的demo影片




在理論分析的階段,我們依著數學推導的結果來
設計控制器,但是撰寫程式碼,完全是不同的思維,例如在規畫控制方塊的時候,並不會有判斷邏輯敘述( if(...){...} )的概念,但是這在程式碼中卻是稀鬆平常,又例如在計算控制命令時,很少關注 u 的正負符號與上下限問題,因為方程式總是能自動滿足,然而實際上,驅動電壓最高可能就5v,也不會有負電壓,因此這些數值上的對應細節都只能靠程式來調整,只要有一個環節不正確,那麼連帶控制理論就不成立,哪怕那是多高等的控制技巧都將無力施展。也因此,程式中常需要利用許多"非線性"的寫法來達成原本可能只是"線性"的控制策略。例如:

1. 由於馬達驅動有最低與最高電壓限制,對應的控制命令就必須要有最小與最大值。
2. 每次計算控制命令時,皆由最小值向上累加,以確保命令在可驅動範圍內。
3.在感測的角度變號時,控制命令的計算也要變號,以確保其值始終為正(才有辦法做analogWrite),但仍要可以區分其方向,所以會利用 if 判斷式,將不同計算式限制在不同的角度區間內。
4. 與角度同方向的角速度變化才做回授。
5. 須對參考值(reference)做回授校正。

最後一點其實很重要也是設計控制器時經常忽略的地方,在理論分析回授增益時,都會將參考值設為零,表示要使倒單擺控制到平衡點。當此值不為零,則表示要使倒單擺向某一側維持一個傾斜角,這時倒單擺需要不斷加速才能維持平衡。因此,如果要讓倒單擺乖乖留在原地,便要確保參考值維持在零點,也就是要設定reference在平衡點的角度上,然而受限於硬體安裝偏差或是配重的影響,這個角度很難事先量測,因此在程式中會先給一個近似的估測值(REF值),再透過回授的方式來持續校正到平衡點,校正的方式則類似積分控制器。
至於那些增益到底要設多少,理論分析當然可以計算出個值,但不如動手多做嘗試來得有感覺。

2014年8月12日 星期二

Arduino〈倒單擺 Inverted Pendulum〉
(3) 互補濾波器


陀螺儀能夠輕易的量測到角速度,但是要量到角度卻要陀螺儀與加速規的配合,而能不能準確的量到角度將是控制倒單擺維持平衡的關鍵。




理論上角度可由兩種方式求得:

1. 利用三軸加速規量測重力加速的大小與方向作為參考值,當加速規隨車體擺動時,重力加速度在加速規三個方向的分量會隨擺動的角度變化,計算其中兩個方向的分量比值再以反正切值(arctangent)推算目前倒單擺與鉛直線的夾角。

2. 利用陀螺儀量測車體擺動的角速度,並對其進行積分得到角度的訊號。


而實際上,感測器各有其限制,當角度快速變化(高頻)時,陀螺儀取其角速度,這時候積分得到的角度比較準確,但這種快速變化會對加速規造成顯著的外力干擾,反而無法透過計算重力加速度的分量來計算角度;另一方面,在角度幾乎靜止(低頻)時,加速規能夠很好的計算角度,但陀螺儀的offset與漂移(drift)的問題,會隨著積分不斷的累積誤差,反而失去量測角度的功能。

互補濾波器(Complementary Filter)便是利用這樣的特性,取高頻的陀螺儀訊號積分值,同時也感測低頻的加速規訊號,將兩者分別計算的角度以固定比例相加,如下列公式:


Angle_comp = a(Angle_gyro) +(1 - a)(Angle_acc)

 0 < a < 1   (通常a值接近1)

如此可達到感測互補的作用,既抑制加速規的干擾,又可消除陀螺儀的積分誤差,更重要的是這個方法在程式碼中容易實現,並具有調整的彈性,算是很好的方法了,下面是一段互補濾波器的虛擬程式碼(Pseudocode)範例,實際寫法得依個人做調整。

loop :
    accAngle <-- Acc.getAngle();
      
    omega <-- Gyro.getOmega();

    gyroAngle <-- lastAngle + omega * dt;
    
    compAngle <--  0.98 * gyroAngle + 0.02 * accAngle ;

    lastAngle <-- compAngle;
loop <-- go to


利用Processing做示波器記錄量測結果中便可以清楚看到差異,橫軸為時間,縱軸則是計算的角度,紅線是只用加速規計算角度的結果,在角度變化或是振動時都會受到嚴重干擾導致量測誤差,綠線則僅用陀螺儀計算,角度變化時反應很快,但在角度靜止時卻有明顯的偏移誤差。在使用互補濾波器後,藍線幾乎可以貼合紅線,同時又抑制了干擾,使得感測效果明顯提升。


Processing示波器記錄量測結果(1)


Processing示波器記錄量測結果(2)


除了互補濾波,還有其他的方法可以應用,其中最被廣泛討論的大概就是移動平均濾波(Moving Average Filter)與卡爾曼濾波器(Kalman Filter)了吧。 移動平均很好理解,把數個感測值加總後取其平均,以此平均值作為每次計算的依據,用這個方法能夠很好的去除訊號突波與白雜訊,但也會帶來感測訊號延遲的影響,等同降低了感測頻寬。若用於平均的訊號取樣數量越多,延遲便會更加嚴重,使得控制反應不及,因為濾波器在量到角度的正確變化之前,單擺就已經倒了。所以在倒單擺系統中,僅使用移動平均濾波恐怕是不夠的。相較之下,卡爾曼濾波就顯得高深且強大了,然而其背後的數學架構也是挺繁雜,過度簡化的設計恐怕無法帶來什麼優勢,效果甚至不如一個低通濾波器。我相信卡爾曼濾波器肯定能夠駕馭如此簡單的倒單擺系統,但是有沒有必要為此殺雞用牛刀,最好還是依據控制的目的作取捨。

2014年8月3日 星期日

Arduino〈倒單擺 Inverted Pendulum〉(2) 陀螺儀與加速規模組設定

凌晨2:46 Posted by Unknown , , , No comments

為了感測倒單擺傾倒的狀態,基本上陀螺儀與加速規缺一不可。其他像是姿態或是方向感測器幾乎也都是以這兩種感測器為基礎搭配磁力計整合成的n-DOF模組。 本篇要先來認識一下使用的感測器模組,以及如何對其進行設定。




1. 陀螺儀模組


使用的是 Grove-3-Axis Digital Gyro Module, 這在光華商場找得到,其內部感測核心為ITG-3200MEMS陀螺儀晶片,可量測物體三個方向的旋轉角速度與環境溫度,不過在倒單擺中只需要擷取一個方向即可,至於是哪個方向則要看感測器與倒單擺的組裝方式才好了解,而溫度感測器通常是用來校正感測器隨溫度變化所產生的漂移誤差(drift)。除了晶片的物理量測規格,datasheet也會載明感測器支援的通訊格式與晶片上暫存器(Register)的使用功能,從暫存器表格中可以知道感測器將三軸陀螺儀的量測值儲存在0x1D~0x22的位址上,每一軸資料分高低位元組共佔兩個位址,Arduino要讀取的值便是從這裡。
ITG-3200 datasheet中的暫存器功能表


要讓陀螺儀順利運作還需要設定幾組暫存器,簡單整裡如下:


(1). Power Manager(0x3E)


a. H_RESET 與 SLEEP 設為0。
b. 啟動特定軸向的陀螺儀,將其對應的bit設為0(normal)。
    例如假設僅使用Y軸,則STBY_YG = 0, STBY_XG = STBY_ZG = 1。
c. 時脈建議配合使用的陀螺儀為振盪參考來源,
    如使用Y軸,就選PLL with Y Gyro reference,
    因此Bit[2-0]設為010。


(2). DLPF, Full Scale(0x16)

a. 選擇量測範圍+/- 2000 degree/sec,Bit[4-3]設為11。
b. 設定晶片取樣頻率 8kHz, Bit[2-0] 為 000。


(3) Sample Rate Divider(0x15)

a.透過除法器進一步的設定(降低)晶片的取樣率,保留defult即可。

其他還有中斷功能的設定,不過這裡並不會用到,一樣保留defult。


ITG-3200支援I2C(400kHz)通訊協定的資料傳輸,其Slave Adress為0x68或0x69,Slave Adress可視為在I2C bus上用來呼叫特定裝置的ID,不同的感測器模組,只要支援I2C通訊,便會有一組固定的裝置ID,在bus上被呼叫ID的感測器才會有回應,如此便可以並聯多組不同的I2C感測模組仍不致衝突,Arduino可以利用Wire.h資源庫來跟感測器作I2C通訊,以下是一段簡單寫入與讀出I2C裝置的程式碼範例。


void writeGyro(uint8_t _register, uint8_t _data) {

  Wire.beginTransmission(ITG3200_DEVICE_ID);   // ITG3200_DEVICE_ID = 0x68
  Wire.write(_register);   
  Wire.write(_data);
  Wire.endTransmission();
}

writeGyro(0x3E, 0x2A);  //ex:  Configuring Power Manager of Gyro 

//****************************************************************************

int16_t readGyro(uint8_t addressh) {
  int data, t_data;

  Wire.beginTransmission(ITG3200_DEVICE_ID);  // ITG3200_DEVICE_ID = 0x68
  Wire.write(addressh);
  Wire.endTransmission();
  Wire.requestFrom(ITG3200_DEVICE, 2);
  
  if(Wire.available() > 0) {
    t_data = Wire.read();
    data = t_data << 8;
  }

  if(Wire.available() > 0) {

    data |= Wire.read();
  }
  
  Wire.endTransmission();

  return data;

}

int16_t gx = readGyro(0x1D);  //ex: Get Data From X-Axis Gyroscope



每個陀螺儀由於內部機構設計或是製造的因素,使得量測值都會有offset的存在,offset是指在沒有旋轉時,陀螺儀仍然有輸出值,雖可透過校正的方式降低offset的影響,但是單純的校正無法完全消除offset,原因是offset可能是個無理數且會隨著時間變化,而晶片上的暫存器有解析度的限制,即使大量取樣offset並取其平均,得到的仍是近似值,在white noise的作用下,感測器的輸出仍會稍微的偏向一側,所以陀螺儀除了校正,還需要其他的手段來抑制量測上的offset與drift。以下是以取樣取平均校正陀螺儀的程式碼範例。


int16_t   offset_GX = 0;

void calibrateGX(int samples, unsigned int sampleDelayMS) {
  int16_t gx_offset_temp = 0;
  int16_t gx = 0;
  offset_GX = 0;

  for (int i = 0; i < samples; i++) {
    delay(sampleDelayMS);
    gx =  readGyro(0x1D);  // Get Data From X-Axis Gyroscope
    gx_offset_temp += gx;
  }
  offset_GX = - gx_offset_temp / samples;  // Calculate offset

}

calibrateGX(100, 10);
int16_t gx_calibrated =  readGyro(0x1D) + offset_GX ;  // Calibrated Output

以上便完成陀螺儀的基本設定。


2. 加速規模組



使用的是Digital 3-Axis Acceleration of Gravity Tilt Module,一樣是常見的感測器,其內部的核心晶片為ADXL-345,可量測物體三個方向的加速度,其值暫存於0x32~0x37的位址中。通訊協定支援SPI與I2C兩種格式,在這裡使用SPI作資料傳輸,根據datasheet中的傳輸要求,在Arduino中作SPI初始化設定時,需將SPI的傳輸模式設為MODE3,並需要一個CS pin腳位作為傳輸的開關。

 // SPI initialization
  SPI.begin();   
  SPI.setDataMode(SPI_MODE3);
  pinMode(PIN_CS, OUTPUT);  // ex: PIN_CS can be pin 10 on Arduino 

  digitalWrite(PIN_CS, HIGH);


ADXL345 datasheet中的暫存器功能表


暫存器設定簡單整裡如下:
(1). Power Control(0x2D)

a. 由於不會用到感測器的中斷與休眠功能,
這裡只需要啟動Measure Mode 即可。 D3 設為1,其他為0。


(2). Data Format Control(0x31)

a. 量測範圍+/- 2g,D[1-0]設為00。
b. 使用4-wire SPI mode,D6設為0。
c. 其他保持defalt,皆設為0。


(3). Data Rate and Power Mode Control(0x2C)

a. 不會用到省電模式,D4設為0。
b. 資料輸出頻率設為最高值,D[3-0]為1111。


(4). XYZ-Axis Offset(0x1E-0x20)

加速規也有offset問題,也就是沒有加速度時,輸出仍然有值,將此offset值寫入這些暫存器,感測器在之後的量測便可以自動扣除,得到較準確的輸出值,這部分可以透過手動校正,依序將各軸加速規擺到水平位置,將不為零的值紀錄並寫入暫存器中。不過offset暫存器的解析度(15.6mg/LSB)與量測值的解析度不同(4mg/LSB),意思是兩個暫存器裡一個位元所代表的加速度值不相同,校正時建議多試幾次,觀察輸出的變化應該會比較清楚。

ADXL-345尚還有許多其他的功能,像是拍擊(Tap)偵測與掉落(Free Fall)偵測,不過這些在倒單擺的應用中都非必要,其暫存器的設定接保持defalt不使用。



以下是一段簡單寫入與讀出SPI裝置的程式碼範例。

void writeAcc(uint8_t _register, uint8_t _data) {
digitalWrite(PIN_CS, LOW);
SPI.transfer(_register);
SPI.transfer(_data);
digitalWrite(PIN_CS, HIGH);

}

writeAcc(0x1E, 0x01); // ex: set x-axis offset value(0x01) into OFSX register(0x1E)
//****************************************************************************

int16_t readAcc(uint8_t addressl) {
int data, t_data;

char address = 0x80 | addressl;
address = address | 0x40;

digitalWrite(PIN_CS, LOW);
SPI.transfer(address);

data = SPI.transfer(0x00);
  t_data = SPI.transfer(0x00);
  data |= (t_data << 8);

  digitalWrite(PIN_CS, HIGH);

  return data;

}

int16_t ax = readAcc(0x32);  //ex: Get Data From X-Axis accelerometer

以上便完成加速規的基本設置。


了解這些模組內部的暫存器設置,是為了方便我們撰寫感測器的資源庫(library),當然這些常見的感測器在網路上應該都找得到別人已經寫好並且功能完善的資源庫,但在使用時還是建議要去了解內部的設定,才不容易在一知半解的狀況下遇到無法預期的問題。

完成感測器的設定表示可以從這些模組讀取量測值了,但是這些讀值常會伴隨很嚴重的干擾,無法作為控制的依據,尤其是角度的量測。因此下一篇會介紹所使用的濾波方法來抑制干擾以得到較準確的量測輸出。