Open Source
    Microcontroller
    Automatic Control
    Coding Notes

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 個位元......所以還是得視情況作取捨才行。