Open Source
    Microcontroller
    Automatic Control
    Coding Notes

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

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),當然這些常見的感測器在網路上應該都找得到別人已經寫好並且功能完善的資源庫,但在使用時還是建議要去了解內部的設定,才不容易在一知半解的狀況下遇到無法預期的問題。

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


2014年7月31日 星期四

Arduino〈倒單擺 Inverted Pendulum〉(1) 硬體架構

上午11:46 Posted by Unknown , , , , 1 comment
倒單擺,顧名思義就是倒立的單擺。要讓單擺維持穩定的倒立狀態,一直都是自動控制中經典的工程議題,其應用可說是相當廣泛且深遠。 現今可以找到各式各樣奧妙的控制手段,從數學的層面來對付這種系統,讓單擺幾乎沒有倒下的可能。然而一旦要透過硬體來實現整個控制方塊,情況似乎就會變得有些難以預料了,已經不會像在 MATLAB 與 SIMULINK 中來得那樣單純。

最簡單的倒單擺模型為一組台車,台車上有一支可自由擺動的單擺,在重力的作用下,單擺勢必下墜。實際的模型中,台車可對應到一對並列的車輪,而與車輪以軸承銜接的馬達和整組車體則可看作單擺的集合。無控制介入時,車體無法平衡,必會倒下。

簡單的倒單擺系統實作


馬達的作用是當單擺倒下的瞬間迅速的前/後轉動,使車體能夠向後/前擺動以維持自身的平衡。使用帶有減速齒輪箱的直流馬達應可滿足控制的需求,並搭配像是L298N這樣的H-bridge驅動模組來控制馬達的正反轉。稍微進階的馬達會帶有轉速表(tachometer)或是編碼器(encoder),這些可以用來感測馬達當前的轉速或是角度,作進一步倒單擺的位置控制或是改善控制表現,不過這些並非絕對必要。

L298N H-bridge 模組

馬達搭配車輪這樣的零件組合,在光華商場或是模型店很容易找到,以5v驅動居多,雖然可以透過改變電壓(PWM)來控制馬達的轉速,然而無論是馬達本身、齒輪組或是輪胎,都會有摩擦力的存在,加上車體的重量負載,這些都是馬達轉動的阻力,因此在一定的電壓之下,馬達是沒有辦法轉動的,換言之,驅動電壓有最小值,可能會在額定電壓的1/3到1/2。另一方面,不同的馬達多少有些差異,在同一驅動電壓下,兩組馬達轉速可能不一致,台車原來的直線運動可能會變成左右旋轉,亦會影響控制表現,這些也要在軟體中盡量修正與補償。


車體結構的製作也許是最不重要卻也是最麻煩的部分吧!結構上需要足夠的空間,能將所有的零件固定並配置妥當,包含馬達、驅動器電路、感測器模組、單晶片、電池還有一些必要的螺絲孔。可以利用一些免費的CAD軟體作2D平面結構的設計,像是LibreCAD或是DraftSight軟體都很方便,製圖完成接著輸出給提供雷射切割服務的廠商幫忙加工,或是尋求各地自造者空間的加工服務,像是Fablab Taipei便設置了各種加工機台,如3D列印、雷切或是木工機具等,可以加入會員或是在免費的開放時段使用。再不然就是到美術材料行與五金行找尋自己需要的材料,手動繪圖、手動加工也行。

利用CAD軟體規劃車體平面結構


馬達需要多少控制電壓是由感測器量測倒單擺當前的狀態進一步計算而決定,簡單來說,這裡的狀態指的是倒單擺與鉛直線的夾角與其倒下的速度,也就是角速度,可以使用陀螺儀與加速規來達成感測這些狀態的工作,也可以購買如MPU-6050這種內部整合加速規與陀螺儀的6-DOF模組,接線上會比較方便,不過本專題使用的是兩組獨立的感測器,型號分別是ADXL345(GY-291)與ITG3200,這些在光華商場也都很好找,其運作方式稍後再介紹。


三軸加速規(左)與三軸陀螺儀(右)

最後,硬體架構需要一組控制核心作為系統的大腦,接受感測器的訊號,執行邏輯運算並輸出命令以控制馬達的轉動,Arduino UNO與其搭載的AVR單晶片-ATmega328,方便、迅速,由於目前非常流行,就不多作介紹了。
Arduino UNO

下一篇會接著探討感測器的工作。





2014年6月3日 星期二

建立 8051 與 Arduino 間的串列埠通訊

上午9:03 Posted by Unknown , , , , No comments
串列埠通訊(Serial Communication)儘管速度慢,但仍是現在最常用也是最基礎的通訊協定之一,例如 RS-232 傳輸介面,幾乎每一種單晶片都會支援,Arduino 亦是透過串列埠來與 PC 傳遞資訊,通常用來抓感測器的讀值給電腦做處理或是由電腦下指令控制單晶片的工作,也可以用作程式除錯的監控。 Arduino的串列埠功能已經建立的很完整,甚至可以用軟體模擬 (通訊由程式定義的 I/O 與暫存器控制,而非透過晶片上串列埠專用的暫存器與 TX/RX 腳位傳輸) 的方式實現串列埠通訊 (SoftwareSerial),相較之下,8051便顯得原始許多,需要先設定幾組暫存器才能使用其硬體串列埠的功能。
  本篇建立 8051 串列埠通訊的目的,其實就是為了要 debug,透過 Arduino 的串接,省去額外的接線,讓 8051 能夠直接與 PC 通訊,可以由監控視窗驗證各階段的執行結果,不用再盲目亂猜,對於 8051 的開發頗有幫助。


8051、Arduino與PC間的串列埠通訊


 $ Arduino 端 $

程式碼非常單純,Arduino僅作為 8051 與 PC 間的通訊中繼站,利用 SoftwareSerial 函式庫實作可與 8051 傳輸資料的模擬串列埠物件 (mySerial),設定好鮑率後,在主程式中監控 PC 端與 8051 的串列埠,並將收到的資料互傳。 Arduino 的 RX ( pin 10 ) 接上 8051 的 TX ( P3.1 ), Arduino 的 TX (pin 11) 則接 8051 的 RX (P3.0),記得要共接地。



Arduino 與 8051 串接 



#include <SoftwareSerial.h>

SoftwareSerial mySerial(10, 11); // RX, TX

void setup()
{
  Serial.begin(9600);
  while (!Serial);

  Serial.println("Hello 8051!");
  // set the data rate for the SoftwareSerial port
  mySerial.begin(9600);

}
void loop()
{
  if (mySerial.available()) {
    char c = mySerial.read();
    if( c == 0x0A ) {
      Serial.println();
    } else {
      Serial.write(c);
    }
  }
  if (Serial.available())
    mySerial.write(Serial.read());
}


$ 8051 端 $

程式可由組合語言 (assembly) 或是 C 語言撰寫,若是組合語言,用最陽春的記事本就可以開始寫程式了,不過比較主流的方式還是透過 Keil C 的開發介面,可以用 C 也可以寫組語,並且免費、可直接編譯成燒錄檔 (*.HEX) 又支援各式各樣的 debug 功能,網路上已可找到非常豐富的資源,算是非常方便。

Keil C 的 IDE

要啟用 8051 的串列埠功能,至少要設定 TMOD (計時器模式)、TCON(計時器控制)、TH1、TL1 (Timer 1 的兩組計時器) 與 SCON (串列埠控制) 這五個暫存器。







串列埠通訊各暫存器設定參考表


簡單整理如下:

1. 確認 8051 使用的晶振頻率為 11.059 MHz,

欲傳輸之鮑率為 9600 Bd

2. Timer 1 須工作在 Mode 2 ( 自動載入模式 ) ,

所以設定 Timer 1 的 M1 = 1,M0 = 0。

3. TH1 與 TL1 設為 0xFD,以滿足所設定之鮑率。


4. 清除 Timer 1 溢位旗標 (TF1 = 0), 

並讓 Timer 1 開始計時 (TR1 = 1)。

5. SCON中,串列埠須設定在 Mode 1

 (SM0 = 0, SM1 = 1, SM2 = 0),以滿足 RS-232 協定,
並啟動串列埠接收資料的功能 (REN = 1),
清除發送/讀入的中斷旗標 (TI = 0, RI = 0)。

本篇還利用了串列埠的中斷功能來實作資料傳輸,當 8051 發送資料或讀入資料時,會令中斷旗標 TI = 1 或 RI = 1,這時會觸發中斷副程式,並在副程式中處理傳輸的資料。 要使用中斷服務,得再設定 IP (中斷優先權) 與 IE (中斷致能) 這兩個暫存器。







6. 為讓串列埠有較高的中斷優先權,設 PS = 1。
7. 啟動串列埠中斷功能,設 ES = 1。
8. 當一切準備就緒,就可以將中斷致能的總開關打開,設 EA = 1。

組合語言的程式範例如下 :


; Serial Communication Example Coding by Assembly

COUNT EQU 65535
BUFF EQU 20H
;
ORG 0000H
LJMP RESET
ORG 000BH ; TIMER0 INTERRUPT ENTRY
LJMP INT_TIMER0
ORG 0023H ; SERIAL INTERRUPT ENTRY
LJMP INT_SERIAL

RESET: MOV R0, #00H
DJNZ R0, $         ; SYSTEM SETTLING...
MOV SP, #60H
MOV R1, #(BUFF)
MOV BUFF, #00H
MOV P0, #0FEH          ; for MAIN TEST
MOV P2, #0FFH          ; for TIMER0 TEST
MOV A, #00100001B  ; TIMER1 in MODE 2 for SERIAL
MOV TMOD, A  ; TIMER0 in MODE 1 (16 bits Counter)
;
; TIMER0 SETTING
;
MOV TH0, #(65535 - COUNT) / 256
MOV TL0, #(65535 - COUNT) MOD 256
CLR TF0
SETB TR0 ; TIMER0 START
;
; TIMER1/SERIAL SETTING
;
MOV TH1, #0FDH    ; BAUD: 9600
MOV TL1, #0FDH
CLR TF1
SETB TR1            ; TIMER1 START
;
MOV SCON, #01000000B ; SERIAL in MODE 2
;
; INTERRUPT SETTING
;
SETB ET0 ; ENABLE TIMER0 INTERRUPT
;
CLR TI
CLR RI
SETB PS ; SERIAL GETS HIGH PRIORITY
SETB REN ; SERIAL RECEIVE ENABLE
SETB ES ; ENABLE SERIAL INTERRUPT
;
SETB EA ; ENABLE SYSTEM INTERRUPT
;
MAIN: MOV A, P0
RR A
MOV P0, A
ACALL DELAY
SJMP MAIN
;
INT_TIMER0:
CLR TF0
MOV TH0, #(65535 - COUNT) / 256
MOV TL0, #(65535 - COUNT) MOD 256
CPL P2.2
RETI
;
INT_SERIAL:
JNB RI, TX_SERIAL
CLR RI
MOV A, SBUF ; READ CHARACTER FROM BUFFER(SBUF)
MOV @R1, A
CJNE @R1, #21H, RX_POST
MOV R2, #H
DJNZ R2, $
MOV @R1, #80H
MOV R0, #(BUFF)
MOV B, R1
SJMP TX_SERIAL
RX_POST:
INC R1
SJMP S_RETI

TX_SERIAL:
CLR TI
MOV SBUF, @R0
MOV A, R0
CJNE A, B, TX_POST
JNB TI, $
MOV BUFF, #00H
MOV R1, #(BUFF)
CLR TI
SJMP S_RETI
TX_POST:
INC R0
SJMP S_RETI

S_RETI: RETI

DELAY: MOV R7, #0FFH
D1: MOV R6, #7FH
D2: DJNZ R6, D2
DJNZ R7, D1
RET
;
END


RESET、TIMER0 SETTING、IMER1/SERIAL SETTING 與 INTERRUPT SETTING 處為程式初始化時對各計時器、串列埠控制與中斷控制暫存器作設定,接著進入主程式MAIN,這是一個無窮迴圈,相當於 Arduino 的 void loop(){},當沒有任何中斷發生時,程式再這裡反覆執行。
串列埠中斷發生時,程式會先跳到中斷進入點 0023H 處,再跳到 INT_SERIAL 處作資料傳輸。 這裡的串列埠功能預設先做讀入,將 PC 傳來的字元依序存入程式記憶體中,而當 8051 接收到 ' ! ' 字元時,便將先前存入的字元全部發送回去。 程式設計了最多可存入 64 個字元的資料緩衝區,利用 R1 當作堆疊指標,每當存入 1 個字元,指標值便加 1,指向下一個記憶體,功能類似 SP ( 8051 的堆疊指標)。 那為啥不用 SP 作 PUSH 跟 POP 呢 ? 因為我的實習板在PUSH 跟 POP 時會影響到其他 IO Port 的狀態,目前原因不明......

發送或接收時,串列埠資料都是放在 SBUF 暫存器(並不是共用,只是名稱相同),接收中斷時,先清除 RI 旗標確保接收完成再到 SBUF 裡抓資料;發送時,也要先清除 TI 旗標確保發送動作正確。


程式另外加入了 Timer 0 與其中斷的功能,與串列埠無關,僅是為了實驗多組中斷時程式互相影響的情形。


功能相似的 C 語言範例如下:


#include <REGX51.h>
#include <intrins.h>       // 左右旋功能

#define COUNT 65535
#define RR(x) _cror_(x,1);
volatile unsigned char buffer_data[64];
volatile unsigned char sensor_data[] = {"Hello Arduino!"};
static int rx_index = 0;
static int tx_index = 0;
void delay(unsigned int x) {
while( x > 0) {
x--;
}
}

void loop(void) {
while(1) {
P0 = RR(P0);
delay(5000);
}
}

void main(void) {

  P0 = 0xFE;
  P2 = 0xFF;

TMOD = 0x21;

TH0 = (65535 - COUNT)/256;
TL0 = (65535 - COUNT)%256;

TH1 = 0xFD;
TL1 = 0xFD;

TCON = TCON | 0x50;
SCON = 0x50;

IP = 0x10;
IE = 0x92;

  loop();
}

void timer0(void) interrupt 1 using 3 {
TF0 = 0;
TH0 = (65535 - COUNT)/256;
TL0 = (65535 - COUNT)%256;
P2_2 = ~P2_2;

}

void serial(void) interrupt 4 using 2 {
if(RI) {
RI = 0;
buffer_data[rx_index] = SBUF;
if(buffer_data[rx_index] != 0x21) {
if(rx_index < 62) {
rx_index++;
} else {
rx_index = 0;
}
RI = 0;
return;
} else {
EA = 0;
delay(100);
buffer_data[rx_index] = 0x00;
tx_index = rx_index;
}
}

TI = 0;
SBUF = '\n';
while(!TI);

TI = 0;
for(rx_index = 0; rx_index <= tx_index; rx_index++) {
SBUF = buffer_data[rx_index];
while(!TI);
 TI = 0;
}

SBUF = '\n';
while(!TI);
TI = 0;

for(rx_index = 0; rx_index < 14; rx_index++) {
SBUF = sensor_data[rx_index];
while(!TI);
 TI = 0;
}

rx_index = 0;
RI = 0;
EA = 1;
}

由 Arduino 監控視窗鍵入字串,便可以存入 8051 的記憶體中。


鍵入 ' ! ',便由 8051 發送原字串與一組回應字串給 Arduino

C語言的可讀性較佳,不過程式效能降低,程式容量也差很多,以組合語言開發的串列埠功能,程式記憶體才佔不到 200 個位元, C 語言卻用掉超過 2,000 個位元......所以還是得視情況作取捨才行。