Open Source
    Microcontroller
    Automatic Control
    Coding Notes

顯示具有 Processing 標籤的文章。 顯示所有文章
顯示具有 Processing 標籤的文章。 顯示所有文章

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月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年5月29日 星期四

利用Processing 顯示 Arduino 加速規模組姿態

清晨7:56 Posted by Unknown , , 1 comment



加速規,用來量測加速度的裝置,但也因為重力恆向下,也可以拿來測量重力於其他方向的分量,進而推算得到物體目前的姿態。

本篇文章主要討論如何從三軸加速規擷取訊號到Arduino,並透過序列傳輸將三軸的分量傳至電腦由Processing接收,並將物體姿態繪出顯示於螢幕。




Arduino ADXL345 三軸重力加速模組 




本篇範例所使用的 Arduino ADXL345 三軸重力加速模組 (數位輸出) Datasheet 。


我們有兩種方法能讓Arduino與加速度模組溝通,分別是SPI 跟 I2C ,文章使用 I2C 通訊作為範例,主要因為Arduino 提供非常方便的 library 供我們使用。


我們可以在Arduino 的 Wire Library 說明頁中看到 Uno 的腳位是  A4 (SDA),  A5 (SCL),因此加速度模組與Arduino 的連接也非常的簡單。


ADXL345 的VCC 接到 Arduino 的 3.3V ,GND 接到 Arduino 的 GND,SDA 接到 Arduino 的 A4,SCL 接到 Arduino 的 A5,這樣接線就完成了。



Arduino 與加速度感測器的連接


硬體準備就緒後,就進入軟體部分。


首先下載 Arduino 的 Wire Library,解壓縮後放到 arduino 的Library資料夾中。


接著下載 ADXL345 的 Arduino Library 。若不想用別人的程式碼可以可以照著 ADXL345 的 Datasheet 自己試著寫,但在此我們先用別人寫好的來修改,畢竟我們的目的是在Processing 上顯示出姿態。


在 ADXL345 的 Datasheet 第10頁中解釋了如何透過I2C跟 ADXL345 溝通,也因此可以看到 Library 的前段是在配置記憶體位置,為了避免記憶體衝突。


為了將資料傳至Processing(注意:鮑率要對應),我們必須修改一小段程式碼,請將 readAccel() 的副程式改寫成如下:

void readAccel() {

  uint8_t howManyBytesToRead = 6;

  readFrom( DATAX0, howManyBytesToRead, _buff); //read the acceleration data from the ADXL345



  // each axis reading comes in 10 bit resolution, ie 2 bytes.  Least Significat Byte first!!

  // thus we are converting both bytes in to one int

  int x = (((int)_buff[1]) << 8) | _buff[0];  

  int y = (((int)_buff[3]) << 8) | _buff[2];

  int z = (((int)_buff[5]) << 8) | _buff[4];




    Serial.write('X');

    Serial.write(_buff[1]);

    Serial.write(_buff[0]);

    Serial.write(_buff[3]);

    Serial.write(_buff[2]);

    Serial.write(_buff[5]);

    Serial.write(_buff[4]);



}

因為 Serial 是一個 byte 一個 byte 傳送,因此我們在Arduino 讀取到的高位元與低位元的值就直接傳至 Processing 再處理,不用再多做一個步驟,並加上一個 'X' 當做起始字元。

接下來就是Processing 的程式碼(再次提醒:鮑率要對應!):

import processing.serial.*;



Serial serial;

int[] invalue=new int[6];

int[] Val=new int[3];



float G=250;

float xacc;

float yacc;

float zacc;



void setup(){

  size(600,300,P3D);

  fill(0, 102, 153);

  println(Serial.list());

  serial=new Serial(this,Serial.list()[3],9600);

  println("Start!");



}



void draw(){

  if(serial.available()>20){

      if(serial.read()=='X'){

        int sign = 1;

        invalue[1]=serial.read();

        invalue[0]=serial.read();

        invalue[3]=serial.read();

        invalue[2]=serial.read();

        invalue[5]=serial.read();

        invalue[4]=serial.read();

          if( invalue[1]>>7 == 1 ) {

             invalue[1]= 255 - invalue[1];

             invalue[0] = 256 - invalue[0];

             sign = -1;

          } else {

            sign = 1;

          }Val[0] = sign * ( invalue[1] << 8 | invalue[0] );

          if( invalue[3]>>7 == 1 ) {

             invalue[3]= 255 - invalue[3];

             invalue[2] = 256 - invalue[2];

             sign = -1;

          } else {

            sign = 1;

          }Val[1] = sign * ( invalue[3] << 8 | invalue[2] );

          if( invalue[5]>>7 == 1 ) {

             invalue[5]= 255 - invalue[5];

             invalue[4] = 256 - invalue[4];

             sign = -1;

          } else {

            sign = 1;

          }Val[2] = sign * ( invalue[5] << 8 | invalue[4] );

          println(Val);

          background(0);

          textSize(20);

          translate(width/2, height/2);

          text("z-acc:"+Val[2],-250,-100);

          text("x-acc:"+Val[0],-250,-80);

          text("y-acc="+Val[1],-250,-60);



          rotateX(asin(Val[1]/G));  //xacc

          rotateZ(asin(-Val[0]/G));   //Yacc

 

          box(100,20,100);

        }

     }

    }

注意到程式碼紅、藍、綠的地方,這裡我們要對接收到的高低位元資料做處理,ADXL345 傳過來的資料要合併這裡整理出一個規則:
如果值為正,則不改變,但如果值為負,則全部的0變為1;全部的1變為0。

也就是說若值為 256,則高低位元分別是 00000001 00000000;

而值為 -256,則高低位元是 11111110 11111111;
也因此有了上面的處理步驟。


得到資料後,便透過重力在個座標的分量來計算出現在加速規的姿態,這步驟需要一點空間概念,不過並不難,思考一下應該可以得到跟筆者一樣的答案。

以上就是如何利用Processing 顯示 Arduino 加速規模組姿態。




2014年5月13日 星期二

利用 Arduino + Processing 做簡易示波器(Oscilloscope)

中午12:28 Posted by Unknown , , 1 comment


Arduino 內建 10 位元的ADC,可以整合各式感測器,像是磁力感測器、紅外線感測器、旋轉編碼器或是加速規等等,利用 Arduino 量測各種類比訊號,通常只能透過串列埠(Serial port)的監控視窗來檢查讀值的狀況,串列埠畫面的快速刷新有時候不好判斷訊號的前後關係,這時候利用 Processing 豐富的視覺化輸出,便可以做一個簡易的示波器介面。

Processing 的 serial library 可以讓 Processing 使用串列埠的通訊功能,當 Arduino 將感測器的讀值丟到串列埠時,Processing 便可以從那將他們抓進來做繪圖處理。 串列阜通訊是由 UART 傳輸器控制,一般以 10 位元為一個傳輸組,包含 1 位元的起始位元, 1 位元的停止位元,以及夾在兩者之間的 8 位元資料,這 8 位元才是我們真正要傳輸的感測值。 8 位元亦即 0 ~ 255,因此當感測器的值大於 255 (或是資料宣告的型態大於 8 位元,如:float ),在串列埠傳輸時便需要拆成數個傳輸組,待接收之後才能將他們一一合併為原始讀值。

串列埠通訊以鮑率 (Baud rate) 為資料傳輸速度的單位,例如鮑率 9600 Bd,表示每秒可傳輸 9600 個位元,按前述可知,此時每秒傳輸 960 個傳輸組,也就是 960 的位元組,所以是 960 Bps,其實很慢,如果傳輸的值較大,或占用較多的位元組,速度將會更慢。因此,使用這種方法做的示波器,其頻寬最高也只在數百 ~ 數 kHz 之間,變化太快的感測值,示波器是量不到的,若進一步考量 Arduino 與 Processing 處理接收資料的速度,頻寬更是會低到不行喔

以下簡介簡易示波器的實現流程:



$ Arduino端 $


為求 demo 方便,利用 Arduino 來模擬感測器讀值的產生,並將其不斷的丟到串列埠上。



1. 在 setup() 的設定中, 以 Serial.begin(115200) 初始化串列通訊,115200為傳輸鮑率,依需求可變更,前提是接收端所設鮑率必須跟此值一樣。



2. 在 loop() 中,產生虛擬的時變感測值,例如低頻的弦波訊號,值域為 0 ~ 1023 的整數intValue,有接上感測器的話,這裡便是存入感測器的原始讀值。




3. 由於 Arduino 的 int 變數會佔用 2 個位元組 (16個位元),為確保傳輸資料的正確性,在串列傳輸前必須先拆解為2 個獨立的位元組,可分別設為高位元 ( byteHigh ) 與低位元組 (byteLow),以 uint8_t ( unsigned int 8 bit ) 宣告這兩個變數。

   a. intValue >> 8 : 將 intValue 的二進位碼全部向右移 8 個位元,用這個方法取高位元組。

    b. intValue  & 0xff : 由 intValue 二進位碼的邏輯運算取出低位元組。














4. 準備好傳輸的位元組,就可以依序丟到串列埠了,但由於資料是連續不間斷的傳輸,資料又被分成兩個傳輸組,為了讓接收端 ( Processing ) 知道哪裡是資料的開端,在每個讀值前應要加上一個可供判別的標頭字元,例如 'H' 字元,讓傳輸資料依序為 標頭字元、高位元組、低位元組,如此重複。

















以上完成 Arduino 端的設定,便可以直接燒入單晶片,Arduino便會不間斷的將虛擬感測值送到串列埠。





$ Processing 端 $


首先 Processing 64 bit 版本不支援 Serial 傳輸應用,所以若要使用 Serial  librery,必須下載 Processing 32 bit 的版本。



1. import Serial 的 library ,並宣告 Serial 物件 Ard。

















2. 在 setup() 中,建構 Serial 物件,Serial.list() 會將連上電腦 com port 的裝置都列出來,通常沒有連其他裝置的話, Arduino 會列在第一個,在此例中其名稱為 COM4,此名稱應該與 Arduino 燒綠時的串列埠名稱相同。後面指定傳輸鮑率。
















3. 建立一個陣列 Rec[],用來暫存串列埠抓來的值。並利用自訂函數 receive() 接收串列埠的值。由於感測值的大小 mapping 為視窗的高度,而陣列 Rec[] 的長度為視窗的寬度,因此陣列中的每一個元素便可以對應到視窗中的一個座標,以此達成讀值的視覺化輸出。
















4.  自訂的函數 receive() 中,預設接收值 Val 為 -1,直到確實收到值在回傳。 由於已經知道三個位元組為一個完整的感測資料,透過 p.available() 確認 Serial buffer 中有足夠的位元組再開始接收值。以 p.read() 抓取 buffer 中的資料,直到抓到標頭字元確認讀值的起始端。每從 buffer read 一次,buffer 後面的資料便會向前遞補,因此抓到標頭字元後,就可以知道下兩次 read 分別為資料高位元組與低位元組。將兩者合併為 16 位元資料完成感測值還原。
















5. 在迴圈 draw() 中,每次僅  receive() 一個感測值,並從視窗左方出現,剩餘舊資料全部向視窗右側平移,將所有資料點連接為曲線,視覺上便可以看到如弦波的波形並呈現資料持續刷新。





2014年5月8日 星期四

將 Processing 程式完整打包成 jar 檔案

上午10:28 Posted by Unknown , , No comments

Processing 這款開源軟體,對於從事科技藝術的創作工作者來說肯定不陌生,以其繪圖功能見長,即便不是專業的程式設計師,透過幾行簡單的程式碼,就可以產生有趣的視覺輸出,而越來越豐富的 library 資源與社群,使得以 processing 開發圖形介面、遊戲、網頁或是 android app 程式都日趨方便。


Processing 支援跨平台,其程式語言以 JAVA 為基礎,程式編輯腳本 (script) 被稱為 sketch,並儲存為 *.pde 檔,當程式撰寫完成,雖然可以直接按下 run 執行,但其實我們更應該將程式打包成一可獨立運作的應用程序,這時候第一個想到的就是 Processing 內建的 Export Application 功能。 較舊的 Processing 版本,支援跨平台的匯出,也就是在 Windows下編輯好的程式,可以匯出檔案給 Macintosh或是 Linux 系統執行,但自 Processing 2.1 起,匯出功能僅針對開發者的作業系統生成出相應的檔案,如在 Windows (x86) 下就只產生 *.exe 檔,附帶一包相連的 lib 資料夾,雖然多了一些匯出 JAVA 的功能,但我覺得還是不太方便。

Processing 2.1+ 匯出功能
Processing 舊版匯出功能















執行匯出的 *.exe 檔仍然會去抓 lib 資料夾裡的 java(*.jar) 檔案,表示這 Processing 小程式仍然不是獨立運作,這還蠻詭異的 ....... 既然是基於 JAVA 語言的開發,而 JAVA 的優勢之一便是其可跨平台執行的能力,怎麼搞了半天還是綁手綁腳呢? 我們需要更完整的打包方法。  

lib 資料夾裡的 *.jar 檔
不太方便的 *.exe 檔


為了打包 Processing 程式,網路上可找到非常實用的小工具:

1. SvgExe.jar
2. ProcessingLibrary.jar

主要的打包流程是利用 SvgExe.jar 這個程式將 Processing 匯出時產生的 *.jar 檔案與 Processing 的原生資料庫( ProcessingLibrary.jar) 整合一起匯出,便可產生一份跨平台執行的獨立 *.jar 檔案,只要是有安裝較新版 JRE 的系統,基本上以滑鼠雙擊就可以執行開啟,非常方便。

當你在 Processing 執行匯出時,在 lib 資料夾中會產生一份與原檔名相同的 *.jar 檔,將其與下載的 ProcessingLibrary.jar 一同匯入 (Add)  SvgExe.jar 中:

在 Step 1: Classes 中將 *.jar 檔案 Add 進來


對於較單純的應用, Step 1 完之後可直接跳到 Step 5: Output,在此輸入主要類別名與檔案輸出路徑,基本上這些也都跟原檔名相同,按下GO,便可產生打包好的 *.jar 檔案:

Step 5: Output 設定



雙擊 *.jar 檔彈出繪圖視窗,直接執行 Processing 的繪圖內容,大功告成。

產生的 *.jar 檔案與執行視窗



更詳細的說明,可以參考這裡: