2015年9月1日 星期二

Arduino 串列埠測試 (UART)

今天要下午才進辦公室, 早上都在家, 所以研究測試了一下 Arduino 的串列埠, 紀錄整理如下. 所謂串列埠是源自 IBM PC 的 RS-232 通訊協定, 也就是個人電腦後面的 COM 埠 (9 針公座 DB-9), 現在新的桌上型電腦與筆電大都沒有接出 COM 埠了, 已經被 USB 取代, 因為透過轉換晶片, USB 串列通訊協定也可以轉成傳統的 RS-232 協定.

RS-232 是全雙工非同步串列通訊, 乃通用非同步收發器 UART (Universal Asynchronous Receiver/Transmitter) 技術的一種, 用來在兩個裝置之間互相進行資料傳遞, 其他工業界常用之非同步串列通訊協定有 RS-422 與 RS-485 等等. RS-485 是 RS-232 的改良版, 採用差動式電壓, 抗雜訊能力較強, 參考 : 

# WiKi : RS-232
# WiKi : UART
# Serial and UART Tutorial  

RS-232 串列埠通訊只用 TX (Out) 與 RX (In) 兩條線同時雙向互傳資料 (當然兩邊 GND 要共接才行, 共三條線), 在介面上比並列通訊要單純. 所謂全雙工是指傳送與接收線分開, 所以兩個方向的通訊可以同時進行. 而非同步是指需要傳送時才起始通訊程序, 它不需要時脈同步線, 而是在協定上設有通訊開始與結束訊息, 在兩邊 Baud rate 相同情況下就可以正確傳送與接收資料. Arduino 所用的 ATmega 微控器除了支援 UART 非同步串列通訊外, 也支援 I2C 與 SPI 這兩種同步串列傳輸 (需同步時脈). 

串列埠的實體位置在 UNO 板子上是 TX (Pin 0) 與 RX (Pin 1) 腳, 如下圖黃色框所示 :


而 Pro mini 的 TX/RX 腳如下圖所示 (上面是插入麵包板的針腳, 右方是連結上傳線的針腳), 注意, 板子上標得是 TXO (TX Out) 與 RXI (RX In), 是 I/O 不是針腳 1/0, 在針腳定義上仍然是 TX1 與 RX0 :


Nano 的接腳則僅有插入麵包板的針腳而已, 因為它本身就有 USB 接頭, 所以 TX/RX 也同時接到 USB 去. 注意, Nano 標的是 TX1 與 RX0 :


串列埠是 Arduino 用來與其他的控制器通訊的窗口, 最早的 Arduino 是採用 RS-232 介面與電腦相連通訊, 用來上傳程式或顯示狀態. 例如下面這張取自 Arduino 官網的 Arduino v3 (Severino) 與 Arduino Serial 就是採用 RS-232 介面 :

Arduino v3 Severino

Arduino Serial

上圖中左上角就是 DB-9 (D 型 9 針) 的 RS-232 的接口. RS-232 實體層採用 -15V 與 +15V 代表正負邏輯 (注意, 負電壓是正邏輯), 所以進 Arduino 後需要做位準轉換, 轉成 0 與 3.3V/5V 的所謂 TTL 邏輯 (Transistor-Transistor Logic), 同時也需要一顆如 MAX232 那樣的晶片來處理訊號, 參考 :

# RS232 → TTL 轉換介面

後來 USB 成為個人電腦串列通訊主流, 就改成了 USB 介面, 雖然實體層是 TTL 位準的 5V, 但 Arduino 仍需要一顆像 FTDI 或 CH340G 這樣的晶片將其訊框轉換成 RS-232 軟體層的 TX/RX 訊號, 參考 :

# 【整理】TTL和RS232之间的详细对比
# TTL介面、I2C 介面、RS232介面、UART 差別?
# 串列傳輸設計(UART Design by Verilog language)

RS232 與 TTL 只是在實體層之電壓位準不同, 在軟體協議層是完全一樣的, 其通訊協定如下圖所示 :


當無資料傳送時, TX 是在 High 準位, 此為 idle 狀態; 開始傳送時 TX 會變成 Low, 此週期為開始位元, 接下來就會連續傳送資料位元, 由 LSB (最低位元) 開始傳送直到 MSB (最高位元), 然後是同位檢查位元, 最後回到 High 之 idle 狀態. 以上是完整之協定, 但實際上要看串列埠設定, 例如 Arduino 一般是用 8, N, 1, 9600, 即資料有 8 bits, 無同位元檢查, 1 個停止位元, 速率 9600 bps. 收送端的設定必須一樣才能成功地通訊, 否則就會出現亂碼或無反應. 所謂非同步是指有資料要傳送時才將 TX 拉到低電位, 通知對方接收; 沒有時就停在高電位, 並非隨時都在傳送資料之意.

Arduino 內建的 Serial 函式庫 (物件) 提供了串列埠連線, 資料傳送與接收等等函式, 使得串列埠通訊程式設計得以大大地簡化. Serial 函式庫的說明文件詳見官網  :

https://www.arduino.cc/en/Reference/Serial

以下為常用的幾個函式整理 :

1. 串列埠連線 : begin

Serial.begin(speed)
Serial.begin(speed [, config])

此函式有一或兩個參數, 必要的第一參數 speed 為連線速率 (baud rate, 每秒傳送幾位元), 可傳入值為 300~115200 bps, 通常設為 9600, 此乃 Arduino IDE 的序列埠監控視窗預設為 9600, 為了避免每次開啟監控視窗還要去改視窗右下角的連線速率, 建議用 9600 即可.

開啟序列埠監控視窗

預設速率 9600 bps

第二參數 config 是可有可無的常數, 用來設定串列信號格式, 預設值為 SERIAL_8N1, 即傳送 8 個資料位元, 沒有同位檢查, 以及 1 個停止位元.

此函式無傳回值, 要放在 setup() 函式中, 因為它只需執行一次.

2. 傳送資料 (輸出) : print, println, write

long Serial.print(val [, format])
long Serial.println([val, format])

這三個函式都是用來在 TX 腳傳送資料, 傳回值為所傳送之 byte 數 (長整數). print 與 println 是將 val 值的每個字元都轉成可讀的 ASCII 字元後才輸出, 而 write 則是直接以二進位碼 (即 byte) 輸出. 亦即, print/println 處理的是字串, 而 write 處理的是字元. print 與 println 差別是 println 會在輸出資料後自動加上跳行 (\r\n), 而且 println 可以無參數, 這相當於是傳送跳行字元, 而 print 至少要有一個參數, 否則會編譯失敗.

參數 val 可以是任何型態資料 (字串, 數值, 布林), 參數 format 只有在 val 是數值 (整數, 浮點數) 時才能用, 字串或布林不可用. 當 val 是字串時, print/println 直接輸出字串, 例如 :

Serial.print("Hello World");     //無跳行, 輸出 Hello World
Serial.println("Hello World");  //有跳行, 輸出 Hello World
Serial.println();                         //純跳行=print("\r\n")

當 val 是整數時, format 可以用 DEC (十進位), BIN (二進位), OCT (八進位), HEX (十六進位) 四個值來設定以何種格式輸出此整數, 預設是 DEC, 例如 :

void setup() {
  Serial.begin(9600);
}

void loop() {
  int a=65;
  boolean b=true;
  Serial.println(a);             //輸出 65 (預設 DEC)
  Serial.println(a,DEC);    //輸出 65
  Serial.println(a,BIN);     //輸出 1000001
  Serial.println(a,HEX);    //輸出 41
  Serial.println(a,OCT);    //輸出 101
  Serial.println(b);             //輸出 1
  Serial.println(false);        //輸出 0
  delay(2000);
}

注意, 舊版 Arduino 的 format 可以用 BYTE 這個參數, 功能與 write 一樣是以 byte 為單位輸出. 但現在新版已經不再支援 BYTE 了. 參考 :

Arduino 筆記 – Serial Library 介紹

若輸出浮點數, 參數 format 須為正整數, 表示小數點後幾位 (四捨五入), 預設是兩位, 例如 :

Serial.print(3.14159);       //輸出 3.14
Serial.print(3.14159, 4);   //輸出 3.1416

至於輸出二進位資料 (byte) 的 write 函式, 其格式如下 :

Serial.write(val)
Serial.write(str)
Serial.write(buf, len)

單一參數時, 其值只能為字元, 字串, 整數, 或布林值, 不能輸出浮點數 (編譯失敗). 輸出整數時, 會輸出此整數所代表之 ASCII 字元, 因 ASCII 的可見字元編碼範圍為 32~126, 其他任何整數都不可見或顯示 "口". 布林值不論是 true 或 false 都只輸出空字元, 因為 0 與 1 在 ASCII 碼是空字元 NUL 與標題開始 SOH (控制字元).

例如 :

Serial.write(65);             //輸出 A (印出此整數所代表之 ASCII 字元)
Serial.write("A");           //輸出 A
Serial.write("Hello!");    //輸出 Hello!
Serial.write(999);            //輸出 "口"

第三個格式第一參數 buf 是一個 char 或 byte 陣列 (int 不行, 會編譯失敗), 參數 len 是要輸出的元素長度, 例如 :

 byte a[]={65,66,67};
 Serial.write(a,sizeof(a));  //輸出 ABC

另外值得一提的是, 當我們呼叫 Serial.print() 與 Serial.println() 時, 這兩個函式會立刻返回, 繼續執行下一個指令, 不會停在那裏等字串傳送完畢. 它們會建立一個緩衝區來存放要傳送的字串, 然後透過中斷來一個一個傳送字元. 所以如果需要讀取序列埠對傳送此字串後的回應 (RX 端) 來判斷下一步要執行的邏輯的話, 必須用 delay() 來暫時停住執行程序等待資料傳送完畢以及對方回應, 不過因為有兩項等待因素 (傳送+回應), 這樣有時較難評估該延遲多久. 這時可用 Serial.flush() 函式來停住程序, 直到資料傳送完畢才會往下執行, 這樣 delay() 只要針對可能的回應時間去估計即可. 注意, Serial.flush() 在 1.0 版以前的功能是用來清空序列埠的接收緩衝器, 但 1.0 版之後改為停住程序直到傳送緩衝區資料傳送完畢. 參考 :

# When do you use the Arduino’s Serial.flush()?

3. 接收資料 (輸入) : available, read

Serial.available()
Serial.read()

當 Arduino 從 RX 接腳接收對方傳來的資料時, 會儲存在緩衝記憶區 (Buffer), 可以用 available() 函式檢查緩衝區是否已經有資料, 其傳回值是 8 位元的 byte 數值 (即字元數, 型態為 byte 或 char). 如果傳回值大於 0 表示已收到對方傳來放在緩衝區之資料.

緩衝區大小是可以設定的, 在 Arduino 安裝目錄下的 HardwareSerial.h 檔案裡 :

D:\arduino-1.6.1\hardware\arduino\avr\cores\arduino\HardwareSerial.h

#if (RAMEND < 1000)
#define SERIAL_TX_BUFFER_SIZE 16
#define SERIAL_RX_BUFFER_SIZE 16
#else
#define SERIAL_TX_BUFFER_SIZE 64
#define SERIAL_RX_BUFFER_SIZE 64
#endif
#endif

可見若可用之記憶體 RAMEND 小於 1KB 時, 緩衝區只有 16 Bytes; 若大於 1KB, 緩衝區則有 64 Bytes, 一般而言緩衝區應該是 64 bytes.

而 read 函式就是用來從緩衝區將資料讀出一個 byte, 若緩衝區無資料就傳回 -1. 讀取後該 byte 資料就被緩衝區刪除了 (FIFO 先進先出). 還有一個 peek 函式, 其功能與 read 一樣都是從緩衝區中讀出一個 byte, 但不同的是, peek 不會在讀出後將該 byte 資料自緩衝區刪除, 故若連續呼叫 peek 函式, 將讀取到相同的 byte 資料 (我現在還想不出 peek 有啥用? 讀取後不刪除, 緩衝區不會爆掉嗎?).

下面範例是 Arduino 傳送資料到 PC 的實驗, 程式取自碁峰楊明豐 "Arduino 最佳入門與應用" 6-3 節稍作修改, 傳送 95 個可見的 ASCII 碼 (32~126) 給 PC :

測試 1 :  Arduino 傳送資料到 PC : serial_transmit.ino

void setup() {
  Serial.begin(9600);
}

void loop() {
  for (byte i=32;i<=126; i++) { 
    Serial.write(i);
    Serial.print("=");
    Serial.println(i);
    delay(1000);
  }


結果如下 :

 =32
!=33
"=34
#=35
$=36
...
...
4=52
5=53
6=54
7=55
8=56
9=57
=125
~=126

接著來測試 Arduino 從 PC 接收資料 :

測試 2 : Arduino 從 PC 接收資料 : serial_receive.ino

int i=0;
void setup() {
  Serial.begin(9600);
}
void loop() {
  if (Serial.available() > 0) {
    i=Serial.read();
    Serial.write(i);
  }
}

上傳 Arduino 後, 打開序列埠監控視窗, 在輸入框中敲一些字元, 按傳送 PC 就對 Arduino 送出此字串, 底下就會顯示 Arduino 從 RX 腳收到的字元 :

PC 傳送資料給 Arduino 

Arduino 接收到 PC 傳來的資料


接下來的測試是要讓兩個 Arduino 透過串列埠互傳資料, 範例參考碁峰柯博文 "Arduino 互動設計專題與實戰" 第七章 7.1.2 節 "兩個 Arduino 透過 UART 相互傳遞資料", 但我使用兩個 Arduino Nano 來實驗, 而不是書中範例用的 UNO.

測試 3 : Arduino 傳送資料給另一塊 Arduino : serial_transmit_receive.ino

第一塊 Nano 板子上傳範例 1 的 serial_transmit.ino 傳送程式, 第二塊 Nano 則上傳範例 2 的serial_receive.ino 接收程式, 然後將傳送板的 TX 腳連到接收板的 RX 腳, 然後打開接收板的序列埠監控視窗, 就可以看到從第一塊板子傳來的資料了 :


上面測試 3 只是單向傳送資料, 接下來要來測試兩個 Arduino 互傳資料 :

測試 4 : 兩塊 Arduino 互傳資料 : nano_left.ino, nano_right.ino

 此例取自全華黃新賢等著 "微電腦原理與應用 Arduino" 7-2 節兩個 Arduino 互傳資料的範例, 書中使用兩塊 UNO, 我則是使用 Nano 板, 其接線如下 :


上圖中兩塊 Nano 之 TX 與 RX 交叉互接 (紅黃線), 即左邊的 TX 接右邊的 RX, 左邊的 RX 接右邊的 TX, 最重要的是, 兩塊的 GND 要相接 (黑線), 因為我是用兩個獨立的行動電源供電, 必須 GND 共接才會形成迴路. 我在初次測試時忘記這個 GND 共接, 結果沒反應.

然後替左邊這塊撰寫程式如下 nano_left.ino :

int LED=13;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  Serial.write('Y');   //先對 TX 送出字元 Y
  while (!Serial.available()) {}  //檢查 RX 緩衝器, 直到有資料進來
  if (Serial.read()=='Y') {   //若收到 Y, LED 閃兩下
    led_blink();
    led_blink();
    }
  }

void led_blink() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(500);
  }

寫完後上傳到左邊這塊, 然後拔掉 USB, 換接右邊那塊 Nano, 為其撰寫程式如下  nano_right.ino :

int LED=13;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  while (!Serial.available()) {}   //檢查 RX 緩衝區, 直到有資料進來
  if (Serial.read()=='Y') {  //若收到字元 Y, LED 閃兩下
    led_blink();
    led_blink();
    }
  Serial.write('Y');   //在 TX 送出字元 Y 給對方
  }

void led_blink() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(500);
  }

接下來要測試 SoftwareSerial 函式, Arduino 板子一般常用的 UNO, Nano, Pro mini 都只有一組硬體 UART 串列埠, 即 DIO Pin0 (RX) 與 Pin1 (TX), 而 Mega 則有四組, 因為 UNO 與 Nano 的串列埠與 USB 並接, 因此如果 USB 接 PC 時就只能與 PC 通訊, 無法同時接其他設備, 例如 ESP8266 WiFi 模組. 於是 Mikal Hart 開發了SoftwareSerial 函式庫, 可以用軟體模擬方式定義多組 DIO 腳當作 UART 埠, 速率可達 11500 bps, Arduino IDE 1.0 版之後已經納入此函式庫, 參考 :

# SoftwareSerial Library

注意, 如果定義了多組軟體串列埠, 同時只能接收一組串列埠資料, 如果想同時收送多組串列埠, 須使用 AltSoftwareSerial 函式庫, 參考 :

# AltSoftSerial Library

測試 5 : 兩塊 Arduino 互傳資料 (使用軟體串列埠) 

以下參考全華黃新賢等著 "微電腦原理與應用 Arduino" 7-3 節範例稍做修改進行 SoftwareSerial 函式庫測試, 仍然使用兩塊 Nano 板子, nano_left 左方板子定義 10 (RX), 11 (TX) 腳做為軟體串列埠, 而硬體串列埠 (0, 1) 則保留給 USB 連接 PC, 以便從 PC 傳送資料給左方板. 右方 Nano 板的硬體串列埠與左方板的軟體串列埠對接, 即 nano_left 的 10 (RX) 接 nano_right 的 1 (TX); 而 nano_left 的 11 (TX) 接 nano_right 的 0 (RX), 接線圖如下 :


在上面測試 4 中, 兩個板子一送電, 左方板即率先送出 'Y' 字元並監測是否收到右方板回送之 'Y' 字元, 右方板收到後閃燈兩下回送 'Y' 字元, 如此周而復始. 此處們不要自動收送, 而是等待我們從 PC 向左方板送出 'Y' 字元才起始週閃燈程序, 而且不是周而復始, 而是下一次 'Y', 右方板先閃兩次, 換左方板閃兩次就停了. 程式部分只要將上面測試 4 稍改 nano_left.ino 即可, 右方板程式 nano_right.ino 不用改.

下面是左方板程式 nano_left.ino (接 PC) :

#include <SoftwareSerial.h>
SoftwareSerial mySerial(10,11);  //建立軟體串列埠腳位 (RX, TX)
int LED=13;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);        //設定硬體串列埠速率
  mySerial.begin(9600);   //設定軟體串列埠速率
  }

void loop() {
  while (!Serial.available()) {}  //等到PC傳送字元才到下一步
  mySerial.write(Serial.read());  //讀取PC傳送之字元,從軟體串列埠TX送給右方板
  while (!mySerial.available()) {}  //等到右方板傳送字元才到下一步
  if (mySerial.read()=='Y') {    //等到軟體串列埠RX收到右方板傳來'Y'字元
    led_blink();
    led_blink();
    Serial.println("Hello!");  //左方板向PC傳送字串
    }
  }

void led_blink() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(500);
  }

下面是右方板 nano_right.ino 程式 (與測試 4 之 nano_postsend.ino 相同) :

int LED=13;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  while (!Serial.available()) {}
  if (Serial.read()=='Y') {
    led_blink();
    led_blink();
    }
  Serial.write('Y');
  }

void led_blink() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(500);
  }

下面為實驗影片, 因為導線太短, 所以右方板我移到上面 (由行動電源供電), 左方板在下面 (由 PC 之 USB 供電), 先打開左方板之串列埠監視視窗, 輸入 Y, 按傳送, 右方板 (上方) 會閃兩次, 然後傳送 Y 給左方板 (下方) 的軟體串列埠, 使左方板也閃兩下 :


可見軟體串列埠有正常運作, 這樣就可以一邊用 PC 監看硬體串列埠, 一邊用軟體串列埠傳送資料給對方, 就不會互相干擾了.

上面的串列埠測試一次只能傳送一個欄位資料給對方, 如果要傳兩筆以上, 必須自行定義傳送協定. 下面測試 6 是在上面測試 5 的基礎上稍作改變, 一次傳送兩筆資料給對方, 例如讓對方的 LED 閃幾次, 以及一次亮滅持續的時間, 此處我們定義傳送協定為兩個欄位之間以逗號隔開. 主控端還是接 PC USB 的左方板, 我們在其串列埠監視視窗輸入 3,500 再按傳送, 左方板就透過軟體串列埠傳給右方板, 右方板收到後要自行解析接收的資料, 以逗號拆開資料, 前者為閃爍次數, 後者為持續時間.

測試 6 : 兩塊 Arduino 傳送多筆資料 (使用軟體串列埠) 

此測試接線圖與上面測試 5 一樣, 只是左右兩塊板子程式要改, 我是參考 O'REILLY "Arduino Cookbook 錦囊妙計" 4-4 節與 4-5 節範例修改的. 首先是左方板程式 nano_left.ino 改成如下 :

#include <SoftwareSerial.h>
SoftwareSerial mySerial(10,11);  //建立軟體串列埠腳位 (RX, TX)
int LED=13;
const int FIELDS=2;  //定義有2個資料欄位
int field_idx=0;  //目前接收之欄位索引
int data[FIELDS];  //定義儲存全部欄位資料之陣列

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);        //設定硬體串列埠速率
  mySerial.begin(9600);   //設定軟體串列埠速率
  }

void loop() {
  while (!Serial.available()) {}  //等到PC傳送字串到硬體串列埠RX才到下一步
  char ch=Serial.read();             //讀取PC傳來之字元
  if (ch >= '0' && ch <= '9') {   //收到之字元為0~9數字字元
    if (field_idx < FIELDS) {     //資料還沒收完, 索引尚未碰頂
      data[field_idx]=data[field_idx] * 10 + (ch - '0');        //轉成整數, 位數累進
      }
    }
  else if (ch == ',') {field_idx++;}      //遇到欄位分隔字元逗號, 欄位索引增量
  else {                              //除了0~9與逗號以外字元均結束接收工作
    if (field_idx != 0) {     //收足兩個欄位資料,經軟體串列埠TX向右方板送出閃燈指令
      mySerial.print(data[0]);      //送出閃燈次數
      mySerial.print(",");             //送出欄位分隔字元
      mySerial.print(data[1]);      //送出持續時間(毫秒)
      mySerial.println();              //送出跳行字元
      Serial.print("Transmit:");    //在監視視窗顯示傳送至軟體串列埠之資料
      Serial.print(data[0]);            //送出顯示閃燈次數
      Serial.print(",");                   //送出欄位分隔字元
      Serial.print(data[1]);            //送出持續時間(毫秒)
      Serial.println();                    //送出跳行字元
      //等待右方板做完閃燈工作
      while (!mySerial.available()) {}  //等到右方板傳送字元才到下一步
      if (mySerial.read()=='Y') {          //等到軟體串列埠RX收到右方板傳來'Y'字元
        Serial.println("Done!");             //左方板向PC傳送字串
        }
      //清空資料, 重新開始
      data[0]=0;
      data[1]=0;
      field_idx=0;
      }
    }
  }

送電後左方板會持續偵測硬體串列埠是否有資料傳送進來, 有則讀進 data 陣列, 其中用逗號當欄位分隔字元, 每次遇到逗號就將索引增量, 以儲存下一個欄位. 我們需要辨別的僅有 0~9 與逗號字元, 其餘一律當作是傳送結束. 當收到這兩種以外字元時, 就檢查資料陣列的索引, 如果有值, 表示有收到資料, 就取出來透過軟體串列埠傳送給右方板, 然後清除索引與陣列, 準備下一次接收指令. 此處接收字元轉成整數的方法利用 ASCII 字元的編碼 :

data[field_idx]=data[field_idx] * 10 + (ch - '0');

其中 '0' 之 ASCII 編碼為 48, 若收到 '1' 字元, 則 ch 值為 '1' 之 ASCII 編碼 49, 所以相減就得到 '1' 的整數值. 若接收到 123, 則每收一位數, 前面一位就變 10 倍, 故要乘以 10.

而右方板的程式 nano_right.ino 如下 :

int LED=13;
const int FIELDS=2;  //定義有2個資料欄位
int field_idx=0;  //目前接收之欄位索引
int data[FIELDS];  //定義儲存全部欄位資料之陣列
int count; //閃燈次數
int ms;  //亮滅持續時間(毫秒)

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  while (!Serial.available()) {}  //等到PC傳送字串到硬體串列埠RX才到下一步
  char ch=Serial.read(); //讀取左方板傳來之字元
  if (ch >= '0' && ch <= '9') { //收到之字元為0~9數字字元
    if (field_idx < FIELDS) { //資料還沒收完, 索引尚未碰頂
      data[field_idx]=data[field_idx] * 10 + (ch - '0'); //轉成整數, 位數累進
      }
    }
  else if (ch == ',') {field_idx++;} //遇到欄位分隔字元逗號, 欄位索引增量
  else { //除了0~9與逗號以外字元均結束接收工作
    count=data[0];  //更新閃燈次數
    ms=data[1];   //更新持續時間
    led_blink();  //閃燈
    //清除接收資料, 重新開始
    data[0]=0;
    data[1]=0;
    field_idx=0;
    Serial.write('Y');  //向左方板回報閃燈完成
    }
  }

void led_blink() {
  for (int i=0; i<count; i++) {
    digitalWrite(LED, HIGH);
    delay(ms);
    digitalWrite(LED, LOW);
    delay(ms);
    }
  }

右方板程式定義了 count 與 ms 分別儲存接收到的閃燈次數與持續時間, 其處理接收之欄位資料方法與左方板是一樣的 (協定要相同), 接收到資料後呼叫 led_blink() 函式去控制 LED 顯示, 完成後向左方板傳送 'Y' 字元, 左方板收到後輸出 "Done!" 於監視視窗.

要注意, 在左方板的監視視窗傳送 LED 控制指令時, 要把下方的結尾方式改為 NL(New Line), 這樣按下傳送時才會在最後面加上 Line Feed 字元 (ASCII 編碼 10), 我們的接收處理程式才會知道接收結束了 :


從測試 6 可知, 要傳送兩筆以上的資料須自行處理資料結構的協定有點繁雜, 所以在全華黃新賢等著 "微電腦原理與應用 Arduino" 7-4 節有提到 Bill Porter 設計了一個 EasyTransfer 的函式庫來簡化硬體串列埠傳送多個變數的程序, 只要將要傳遞的變數用 struct 定義在資料結構中, 再呼叫 sendData 函式即可, 不需要自行處理資料結構之收送, 要增加變數也很方便, 參考 :

EasyTransfer Library for Arduino
# Bill Porter's "EasyTransfer Arduino Library"

不過 EasyTransfer 函式庫有如下限制 :
  1. 只支援硬體串列埠, 不支援軟體串列埠
  2. 資料結構不可超過 255 Bytes
為了能夠在軟體串列埠中也能使用 EasyTransfer 的功能, Bill Porter 另外寫了 SoftEasyTransfer 函式庫, 因為 Arduino IDE 目前尚未納入此兩個函式庫, 請連線到 Bill Porter 的 GitHub, 點選右下角之 "Download ZIP" 下載 :

https://github.com/madsci1016/Arduino-EasyTransfer/archive/master.zip

解壓縮後可看到一共有四個目錄, 可見除了 EasyTransfer 與 SoftEasyTransfer 外, Bill Porter 也寫了 Wire 與 I2C 函式庫. 可將此四個資料夾複製貼上到 Arduino IDE 安裝目錄或 "我的文件\Arduino" 下的 Libraries 資料夾下面 :


然後 IDE 必須全部關掉重開才會抓到函式庫 :


底下測試 7 使用 EasyTransfer 與 SoftEasyTransfer 函式庫來改寫測試 6 :

測試 7 : 兩塊 Arduino 傳送多筆資料 (使用 EasyTransfer 與 SoftEasyTransfer) 

但此處我們不再由 PC 傳送指令給左方板 (因為還要自行處理指令協定), 而是經軟體串列埠固定傳送 3,500 給右方板. 左方板使用 SoftEasyTransfer 函式庫與右方板溝通 (傳送閃燈指令與接收回應), 因為硬體串列埠要用來接 PC, 以便顯示執行狀態, 右方板程式 nano_left.ino 修改為 :

#include <SoftwareSerial.h>
#include <SoftEasyTransfer.h>

SoftwareSerial mySerial(10,11);    //定義軟體串列埠 (RX,TX)
SoftEasyTransfer SET;                   //建立SoftEasyTransfer物件
struct DS {     //定義資料結構
  int count;
  int ms;
  };
int n;               //迴圈計數器
DS data;          //宣告資料結構實體

void setup() {
  Serial.begin(9600);      //設定硬體串列埠速率
  mySerial.begin(9600);   //設定軟體串列埠速率
  SET.begin(details(data), &mySerial);  //初始化軟體串列埠ET物件
  }

void loop() {
  data.count=3;                        //設定軟體串列埠欄位值
  data.ms=500;                        //設定軟體串列埠欄位值
  SET.sendData();                   //經軟體串列埠對右方板傳送資料
  Serial.print("Count=");         //在監視視窗顯示傳送至軟體串列埠之資料
  Serial.println(data.count);    //顯示閃燈次數
  Serial.print("ms=");              //持續毫秒數
  Serial.println(data.ms);        //持續時間(毫秒)
  while (!mySerial.available()) {}  //等右方板回傳'Y'到軟體串列埠RX才到下一步
  if (mySerial.read()=='Y') {     //收到右方板傳來 'Y' 字元, 對串列埠輸出 Done!
    Serial.print(n);
    Serial.println(" Done!");
    ++n;
    }
  }

右方板則使用 EasyTransfer 函式庫與左方板的 SoftEasyTransfer 函式庫溝通以接收其傳來之指令, 其程式 nano_right.ino 如下 (特別注意 struct 結尾大括號後面必須有分號, 否則無法通過編譯) :

#include <EasyTransfer.h>
int LED=13;
EasyTransfer ET;       //建立EasyTransfer物件
struct DS {  //定義資料結構
  int count;
  int ms;
  };
DS data;   //宣告資料結構實體

void setup() {
  Serial.begin(9600);                   //設定硬體串列埠速率
  ET.begin(details(data), &Serial);   //初始化硬體串列埠ET物件
  }

void loop() {
  if (ET.receiveData()) {  //硬體串列埠有收到資料
    led_blink();              //閃燈
    delay(5000);            //休息 5 秒再向左方板回應 'Y'
    Serial.write('Y');      //向左方板回傳'Y'字元表示完成閃燈
    data.count=0;           //重設接收資料
    data.ms=0;               //重設接收資料    
    }
  }

 void led_blink() {
  for (int i=0; i<data.count; i++) {
    digitalWrite(LED, HIGH);
    delay(data.ms);
    digitalWrite(LED, LOW);
    delay(data.ms);
    }
  }


送電後左方板即透過軟體串列埠向右方板傳送 3,500 指令, 右方板收到後依據收到之資料閃燈三下, 然後休息 5 秒再回傳 'Y' 字元給左方板, 左方板收到後即向串列埠輸出 "Done!" 顯示於監視視窗, 如此周而復始. 傳送資料只要呼叫 sendData() 函式就會將所定義之資料結構傳送出去, 接收資料則是呼叫 receiveData(), 當有收到資料時, 此函式會傳回非 0 值, 接收的資料會放在所定義之欄位變數中. 可見 EasyTransfer/SoftEasyTransfer 真的讓我們省了非常多的工啊 !


10 則留言 :

林琮閔 提到...

不好意思高手, 請教一下:
1. 測試三: 兩塊arduino傳遞資料測試, 為何接收端要使用Serial.write()輸出? 若使用Serial.print()可以嗎?
2. 若是要接收傳送端傳來的類比數值, 接收端要使用Serial.write()輸出, 還是Serial.print()輸出?
3. 我最近在設計公司的一個case, 就是要兩塊arduino採rs232連接序列單向傳輸類比值給接收端的arduino再輸出給lcd (Arduino1 -->rs232 -->rs232--->Arduino2--->lcd), 我利用10kohm電阻做實驗, 很奇怪, 接收端輸出採Serial.print給Serial monitor,數值show出來是錯的, 程式下一行我寫lcd.print()給lcd monitor, 數值show出來也是錯的, 後來改成Serial.write 和 lcd.write, Serial monitor數值會正常, 但lcd monitor數值會一次一個數值覆蓋更新, 例如970, lcd會出現9, 然後再出現7覆蓋9, 再0覆蓋7, 沒辦法一次輸出970. 但我測試單一片arduino接lcd(I2C連接), 是正常的..可以幫我看看是哪裡出問題嗎? 附上原碼.
傳送端:
const int portpin = A0;
int val;
void setup() {
Serial.begin(9600); // put your setup code here, to run once:

}

void loop() {
val=analogRead(portpin); // put your main code here, to run repeatedly:
Serial.println(val'\n');
delay(2500);
}
接收端:
#include // Arduino IDE 內建
#include
// addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
int i=0;
void setup() {
Serial.begin(9600);
lcd.begin(16, 2);

}

void loop() {
if (Serial.available() > 0){
i=Serial.read();
Serial.write(i);
lcd.setCursor(1, 1);
lcd.write(i);
delay(400);
}
}

ST Tang 提到...

您好:
請問我在Mega2560上接了RS232連接板,
測試PC(用AccessPort 1.37)與arduino的RS232傳輸,可以互傳字串,
但是想用Mega2560接收另一個(以RS232通訊的)訊號處理模組卻一直收不到訊號,
有可能是什麼原因?

我已經試過在PC上AccessPort 1.37只要訊號處理模組的線接上,
終端機就可以收到有效的資料串,
而且通訊協定都是38400.8N1

Tony Huang 提到...

Sorry! 我沒有用過 Mega, 但 Mega 具有四組硬體串列埠, 每個用法都一樣, 沒道理其中一組收不到. 會不會是 :
1. 接線問題 (tx/rx 接錯, 插槽或線接觸不良等)
2. 傳送的模組有問題, 根本沒送過來
試試看, 硬體問題不好抓喔, 要耐心一步步將問題隔離開來.

網誌管理員 提到...

我用UNO以TX輸出,以PC接收,接收到的資料不對,是不是準位的問題
因為UNO輸出是5V,但PC的RS232是+/-12V?因為我用示波器看,感覺有反相的現象

Tony Huang 提到...

一般 Arduino 的 tx/rx 經過 PL2303 之類的轉換後直接插在 USB 接口即可.

C SAM 提到...

請問一下, ESP8266模組的RX/TX要接到 ARDUINO 上的pin 只能接到default的 D0跟D1嗎? 還是可自定義其他pin角當作 TX/RX來用呢? 因為想用一塊ARDUINO 接WIFI /BT & RFID reader, 怕ARDUINO上的RX/TX不夠用!

匿名 提到...

您好 感謝分享這麼棒的教學
有些問題想請教
我想實作出 從Arduino 跟ESP01用UART通訊,並用ESP01以及ESP8266WebServer.h函式庫 來顯示資料 例如溫濕度
首先想用電腦傳字串給ESP01 但是ESP好像不能用Serial.write()
於是我用了Serial.println(i); 並且電腦傳"1"給ESP01
if (Serial.available() > 0) {
i=Serial.read();
Serial.println(i);
}
結果回傳了
49
13
10
就是1\r\n的ASCII
請問我該如何ESP01輸出字串呢?

Tony Huang 提到...

您好, 我是使用 software serial 來連接 Arduino 與 ESP8266 的, 參考 :

http://yhhuang1966.blogspot.tw/2015/10/esp8266-wifi-arduino.html

Oscar Yu 提到...

Tony大大!請教ㄧ下!我正在做用人機RS232控制arduino mega的serial0(RX,TX)再由同一台mega的serial2(TX2,RX2)控制變頻器轉速,mega得由232的從變成485的主、不知道這樣是否可行?目前分開控制都可以(人機對Mega,mega對變頻器)串起來時、從人機輸入數值就出現通訊錯誤、謝謝您!

Tony Huang 提到...

Dear Oscar,

Sorry, 我沒玩過 Mega, 您的意思是從 PC 透過 SERIAL0 對 MEGA 下指令, 然後利用 SERIAL2 去控制變頻器嗎? 這樣應該可以啊.