2015年9月29日 星期二

Upverter 匯出與匯入專案

最近使用 Upverter 線上繪製電子電路圖軟體越來越得心應手, 覺得其設計版面簡單容易操作, 最重要的是很多元件一搜尋就能找到, 不像其他軟體翻遍了都沒個影子, 一張電路圖就缺個 IC 根本畫不下去.

Upverter 有個方便的功能是把電路圖輸出成 JSON 檔案下載保存. 因為有時候需要在某個電路圖專案基礎上做些修改, 又不想改到原電路, 但是 Upverter 的專案不能複製舊的變成新的, 這可怎辦呢? 辦法是先在舊電路專案上, 按 "Project/Export Files" :


在底下 Export Design Data 選擇第一個下載按鈕, 儲存成 JSON 檔案 :


然後回到 Dashbord 中新增專案 :



然後在下方 Files to import 按 "Choose Files", 選擇剛剛下載的 JSON 檔, 按 "Create" 即可 :



這樣不僅可以離線儲存專案, 也能將專案 "Copy & Paste" 了.


第 39 周記事

中秋節連假, 已經兩個多月沒回鄉下的二哥終於可以回去了, 但是姊姊周六下午還要去畫室練素描, 所以也到周六傍晚才回去. 先到岳父的鄉下貨櫃屋把姐姐放下來, 因為小舅子他們要烤肉, 我則要回去煮菜, 二哥不想烤肉, 所以就先跟我回去. 我以為他一回到家就會開始玩電玩, 結果是幫阿公整理要煮的青菜. 其實二哥從小就蠻可愛的, 只是長大有點酷, 有點宅, 又愛玩電玩而已.

三天的連假, 很 "廉價" 地飛走了, 還帶來個杜鵑颱風. 下午已半年沒來找我的國中同學又出現了, 因為我昨天在 line 問他, 中秋沒回鄉下嗎? 下午我煮好晚餐的雞湯與燒酒雞, 間歇的陣風開始出現, 正準備要打開電腦完成我的實驗, 他就來了. 聊了一下午, 我才得知他母親的癡呆症已惡化到全部家人都不認識了. 大約五年前我幫伯母裝數位電視時, 她僅僅是常忘記如何按遙控器轉台而已.

老了真的啥都退化, 而且速度很快. 今天去養護中心看阿蘭, 順便看住在隔壁房的水某的大阿姨, 她最近大都躺在床上睡覺. 四, 五月的時候, 她還會自己推輪椅到洗手槽洗衣服, 還能跟我談上一, 二十分鐘, 精神好得很. 七月之後, 就常躺床上, 較少坐輪椅出來, 更別說洗衣服了. 她若是在睡覺, 我就不打擾她; 若醒著就陪她說話, 有一次要我跟護理長講, 叫她兒子來接她回去, 護理長說她每天都這麼說啦 ... 她腦筋其實還很清楚, 即使閉著眼睛休息, 我報上名字她都明白. 時代在進步, 社會在進步, 但是有些東西卻似乎是在退步.

回鄉下的兩天晚上, 我各做了一個奇怪的夢. 週六的晚上, 我夢見一位大學的老同學要搭我回鄉下的便車去某處, 然後我回高雄時再順路載其回去. 但是要回去時電話號碼卻一直找不到. 都畢業好久了, 除了偶遇也從不聯絡, 卻突然會在夢中出現, 真奇怪. 另外一個夢醒來沒特別思索, 就忘了. 我很少作夢, 可能放假睡到很晚才起床, 睡太飽了吧. 但是人睡著就失去意識, 或許作夢是平行人生的反射也說不定, 睡著之後就進入另一個人生 ...

晚上突然想到很久沒登入臉書 (短則數周, 長則半年), 就進去看看, 發現大家都過得很精采, 出國的, 登山的, 育兒的, ... 真是令人羨慕. 我的生活就平淡無奇多了, 上班, 回鄉下, 做料理, 整理菜園, 寫寫程式, 做些電子實驗, 如此而已, 其實沒啥好記的. 記下來只為了利用 Google 查考方便而已, 沒有要分享甚麼, 完全是~~自利行為.

這三天只完成了兩件事, 一是將回收紙箱扁平化, 待下周請人來收; 二是將鄉下三支 IP 監視器安裝妥當, 餐廳, 客廳, 廚房各一支. 爸是獨居老人, 我必須遠端查看生活作息較安心.

越晚風吹得更響, 這樣的雨夜睡起來更舒服, 睡覺去咧.


2015年9月24日 星期四

Kimberley 陳芳語的 "愛你"

菁菁很喜歡在 Youtube 上看音樂 MV, 一看再看, 一聽再聽. 但有些太 ROCK, 欠缺旋律感, 我實在不知道好聽在哪裡. 最近她給我洗腦的是這首陳芳語的 "愛你". 老實說, 還真的蠻好聽的, 而且這個 MV 很有意思, 到了三點整, 所有的人都會停止, 然後就換成這兩個模特兒動起來了 ... 十分鐘後一切又恢復了 ...

# https://www.youtube.com/watch?v=KGrulNiJ6Uc


以前沒聽過這個歌手, 查維基得知, 她是在澳洲出生的馬來西亞華僑, 曾參加星光大道挑戰四連勝, 唱功非常了得.

# Wiki : 陳芳語

還有一個版本是現場清唱的, 音質雖然不若 MV 錄音室版的好, 也別有不同風味, 特別是後半段接唱的男聲也不錯 :

# Kimberly陳芳語 眉毛有戲&素顏嗆公司




第 38 周記事

因姊姊週日 9/20 要跟同學去駁二參觀她們學姐的藝術展, 所以本周只有我跟菁菁回鄉下了. 週日去市場時, 魚攤前賣油炸揚物的菜圓姊姊問我說, 菁菁好像瘦很多 ... 沒錯, 她還蠻有毅力的, 我說她其實蠻想吃妳賣的菜圓, 但都忍住了.

週日下午午覺起來, 本來只是要將廚餘桶拿去菜園挖個洞埋起來, 看到野草蔓生, 實在火冒三丈, 擇日不如撞日, 拿出鋤頭便一勁地刮地皮, 把可惡的雜草全部滾成草球後推到芒果樹下, 下周變成乾草再拿來覆蓋地皮. 現在已九月了, 夏秋不穩的天氣已遠走, 種菜的好時節又來了, 但是去年構想的自動灌溉系統卻還沒個影子, 還真會拖. 我對自己浪費時間的惡習甚為痛恨.

週六晚上在鄉下吃完飯後, 正欲整理書房, 看到電腦旁邊的訊舟 Edimax IC-3115W 無線攝影機, 這款去年在露天跟一位賣家買的, 一次買了兩個, 但回去鄉下安裝時, 採用 WPS 方式一直無法順利連線, 因當時媽住院, 我也偶而回鄉下拿東西才有時間測試, 所以就擱一邊了. 買這個主要是我從遠端可以看到鄉下客廳或飯廳的情況 (監視器只能看到屋外情況), 因為有時媽出去電話會拿起來, 回來又忘記放回去, 我電話打不通就會擔心. 這次就順利安裝完成, 使用行動網路模擬從遠端連線也 OK. 參考 :

Edimax 訊舟 IC-3115W 雲端無線網路攝影機

不過這款產品雲端連線不是 App 一開就能順利連線, 通常都會顯示哭臉雲表示無影像, 要按了多次後才會出現影像. 而且用 WPS 鍵來與無線基地台連線的方式也很彆扭, 因為無線 AP 的 WPS 兼 RESET 功能, 必須按一下就放手, 否則 AP 會重開機. 應該學學人家小米的方式, 自動偵測附近的 AP, 選擇後自動連線.

雖然不是很滿意這產品, 但能用就好啦. 這是本周末唯一完成的事.



2015年9月23日 星期三

Arduino 數學函數測試

Arduino 的大部分應用都只用到簡單的四則運算, 其中比較值得注意的是做整數的除法時, 其結果若為浮點數, 必須將其宣告為 float, 而且運算元必須至少有一個是以浮點數表示, 例如下面的的指令將得到 0.00 而非 0.50 :

float a=1/2;    //a=0.00

欲得到正確的運算結果 0.50 必須這樣 :

float a=1.0/2;    //至少一個運算元是用浮點數表示
float a=1/2.0;    //至少一個運算元是用浮點數表示
float a=(float)1/2;    //被除數強制轉型為浮點數

複雜的數學運算例如三角函數, 指數或對數等超越函數就要用到 Arduino C 語言內建的數學函數, 參考官方文件中的 Math 與 Trigonometry 部分 :

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

Arduino 官方文件中的數學函數列表 (含三角函數) :

 數學函數 說明
 abs(x) 傳回 x 的絕對值
 min(x,y) 傳回 x 與 y 兩數中值較小者
 max(x,y) 傳回 x 與 y 兩數中值較大者
 constrain(x,min,max) 以 max, min 為上下限, 傳回 x 值
 pow(x,y) 傳回 x 的 y 次方
 sqrt(x)  傳回 x 的平方根
 floor(x) 傳回不大於 x 的最大整數
 ceil(x) 傳回不小於 x 的最小整數
 sin(x) 傳回 x 的正弦值
 cos(x) 傳回 x 的餘弦值
 tan(x) 傳回 x 的正切值
 random(max)  傳回 0~(max-1) 間的隨機數
 random(min,max) 傳回 min~(max-1) 間的隨機數
 randomSeed(x) 啟始一個假隨機數產生器, x 為整數

除了官網的所列的數學函數外, 其實 C 語言的其他數學函數也可以用, 例如自然對數 log, 常用對數 log10, 雙曲函數 sinh, cosh, tanh 等 (但 fmod 不行), 參考 :

C 教材:數學函式庫

 其他數學函數 說明
 fabs(x) 傳回浮點數 x 的絕對值
 asin(x) 傳回 x 的反正弦值 sin-1 
 acos(x) 傳回 x 的反餘弦值 cos-1
 atan(x)  傳回 x 的反正切值 tan-1
 sinh(x)  傳回 x 的雙曲正弦值 (ex-e-x)/2
 cosh(x) 傳回 x 的雙曲餘弦值 (ex+e-x)/2
 tanh(x)  傳回 x 的雙曲正切值 sinh(x)/cosh(x)
 exp(x) 傳回 x 的自然指數值 ex
 log(x)  傳回 x 的自然對數值 ln(x)
 log10(x)  傳回 x 的常用對數值 log10(x)
 ldexp(x,n) 傳回 x 與 2 的 n 次方乘積 x*2n

另外, AVR 晶片系列也定義了一些常用的數學常數 (Macro, 巨集), 都可以在 Arduino 程式中使用, 參考 :

# http://www.nongnu.org/avr-libc/user-manual/group__avr__math.html
http://wiki.secondlife.com/wiki/RAD_TO_DEG
http://wiki.secondlife.com/wiki/DEG_TO_RAD

數學常數 說明
 M_E  e=2.7182818284590452354 (自然指數)
 M_LOG2E log2e=1.4426950408889634074 (自然指數以 2 為底之對數)
 M_LOG10E log10e=0.43429448190325182765 (自然指數的常用對數)
 M_LN2 ln2=0.69314718055994530942 (2 的自然對數)
 M_LN10 ln10=2.30258509299404568402 (10 的自然對數)
 M_PI PI=3.14159265358979323846 (圓周率)
 M_PI_2 PI/2=1.57079632679489661923 (二分之一的圓周率)
 M_PI_4 PI/4=0.78539816339744830962 (四分之一的圓周率)
 M_1_PI 1/PI=0.31830988618379067154 (圓周率的倒數)
 M_2_PI 2/PI=0.63661977236758134308 (兩倍的圓周率倒數)
 M_2_SQRTPI 2/sqrt(PI)=1.12837916709551257390 (2 除以圓周率開平方)
 M_SQRT2 sqrt(2)=1.41421356237309504880 (2 開平方)
 M_SQRT1_2 1/sqrt(2)=0.70710678118654752440 (2 開平方的倒數)
 DEG_TO_RAD 角度*PI/180=0.0174532925199432958 (角度轉弧度常數)
 RAD_TO_DEG 弧度*180/PI=57.2957795130823208768 (弧度轉角度常數)
 NAN 非數字=nan
 INFINITY 無限大=inf

測試範例 (使用 Nano) :

void setup() {
  Serial.begin(9600);
  Serial.println(abs(-1));  //輸出 1
  Serial.println(abs(-1.235));  //輸出 1.23
  Serial.println(fabs(-1.235));  //輸出 1.23
  Serial.println(min(-1,-3));  //輸出 -3
  Serial.println(max(-1,1));  //輸出 1
  Serial.println(constrain(101,100,200));  //輸出 100
  Serial.println(constrain(150,100,200));  //輸出 150
  Serial.println(constrain(201,100,200));  //輸出 200
  Serial.println(constrain(201,100,200));  //輸出 200
  Serial.println(pow(2,3));  //輸出 8.00
  Serial.println(sqrt(2));  //輸出 1.41
  Serial.println(floor(1.9));  //輸出 1.00
  Serial.println(floor(-1.1));  //輸出 -2.00
  Serial.println(ceil(1.9));  //輸出 2.00
  Serial.println(ceil(-1.1));  //輸出 -1.00
  float deg=30;
  float rad=deg*PI/180;
  Serial.println(sin(rad));  //輸出 0.50
  Serial.println(sin(deg*DEG_TO_RAD));  //輸出 0.50
  Serial.println(cos(rad));  //輸出 0.87
  Serial.println(cos(deg*DEG_TO_RAD));  //輸出 0.87
  Serial.println(tan(deg*DEG_TO_RAD));  //輸出 0.58
  //轉回角度
  Serial.println(asin(0.5)*RAD_TO_DEG);  //輸出 30.00
  Serial.println(acos(0.87)*RAD_TO_DEG);  //輸出 29.54
  Serial.println(atan(0.58)*RAD_TO_DEG);  //輸出 30.11
  Serial.println(atan2(5.8,10)*RAD_TO_DEG);  //輸出 30.11
  Serial.println(sinh(1));  //輸出 1.18
  Serial.println(cosh(1));  //輸出 1.54
  Serial.println(tanh(1));  //輸出 0.76
  Serial.println(exp(1));  //輸出 2.72
  Serial.println(log(2));  //輸出 0.69
  Serial.println(log10(100));  //輸出 2.00
  Serial.println(ldexp(2,3));  //輸出 16.00
  Serial.println(M_E,19);  //輸出 2.7182817459106445312
  Serial.println(M_LOG2E,19);  //輸出 1.4426950454711914062
  Serial.println(M_LOG10E,19);  //輸出 0.4342945098876953125
  Serial.println(M_LN2,19);  //輸出 0.6931471824645996093
  Serial.println(M_LN10,19);  //輸出 2.3025851249694824218
  Serial.println(M_PI,19);  //輸出 3.1415927410125732421
  Serial.println(M_PI_2,19);  //輸出 1.5707963943481445312
  Serial.println(M_PI_4,19);  //輸出 0.7853981971740722656
  Serial.println(M_1_PI,19);  //輸出 0.3183098793029785156
  Serial.println(M_2_PI,19);  //輸出 0.6366197586059570312
  Serial.println(M_2_SQRTPI,19);  //輸出 1.1283792257308959960
  Serial.println(M_SQRT2,19);  //輸出 1.4142135620117187500
  Serial.println(M_SQRT1_2,19);  //輸出 0.7071067810058593750
  Serial.println(DEG_TO_RAD,19);  //輸出 0.0174532914161682128
  Serial.println(RAD_TO_DEG,19);  //輸出 57.2957801818847656250
  Serial.println(NAN);  //輸出 nan
  Serial.println(INFINITY);  //輸出 inf
  Serial.println(random(10));  //輸出 0~10 間之隨機數
  Serial.println(random(100,200));  //輸出 100~200 間之隨機數
  randomSeed(1023);
  }
void loop() {
  Serial.println(random(10));  //輸出 0~10 間之隨機數
  delay(1000);
  }

結果似乎在小數點後第五位就有誤差了 (雖然不需要這麼精確), 即使是常數也是如此, 不知是何原因?

關於超越函數詳參 :

維基 : 三角函數
維基 : 指數函數
維基 : 對數
維基 : 自然對數
維基 : 雙曲函數
維基 : 反雙曲函數


2015年9月22日 星期二

特殊服務

今天在 Youtube 無意中看到這部黃渤首度執導的微電影, 短短 30 分鐘左右, 演員兩個而已, 但說教不落俗套, 很有深度 :

# 《特殊服务》:7电影黄渤作品


黃渤就是在 "人再冏途之泰冏" 中, 飾演徐錚的同學高博的那個演員, 我印象比較深刻的是充滿草根喜感的王寶強, 以及演活 "老闆" 的徐錚. 我一直以為泰冏是 "人在冏途" 的續集, 原來是黃渤糾集原班人馬自己拍的, 引起了侵權糾紛.

人不滿足才會想不開, 其實是沒看過比自己境遇還爛的. 說到底, 沒病沒痛就是真幸福了. 走一趟醫院急診室看看就知道. 人得了寸就想進尺, 全然忘了啥是真快樂.

參考 :

# 黄渤处女导全能上阵《特殊服务》5分47秒有亮点



買尖嘴鉗與電阻

周一中午因為沒空出去, 所以周二下班後訂了小帆船便當, 順路從忠孝路穿過建國路到長明街的禾樺去買 10K+20K 電阻, 這周要把 ESP8266 + DHT11 溫溼度感測器的 IoT 實驗做完. 電壓轉換最後還是決定用最簡單的電阻分壓法來做, 有時候簡單的反而是最好的.

碳膜電阻很便宜, 5 個 1 元, 所以就 10K 與 20K 各買兩排 (一排 10 個), 小計 8 元. 看到有賣尖嘴鉗, 一把 80 元, 比水某在新光三越附近拼布店買的便宜多了, 所以就買一把, 合計 88 元, 會員打折後是 77 元, 大概是 88 折左右. 上回累積消費加入會員還是蠻划算的, 因為一些零組件只要不會比露天貴很多, 我寧可跑一趟長明街比較快.

 

向 nekocyh 採購零件一批

今天向 nekocyh 採購零件一批如下 :

# 圓孔排母 / 1x30pin / pitch=2.54mm $11*20=220
# UM66T-19L 音樂IC (3顆一拍=20元) $20*3=60
# BA033 穩壓IC $2*10=20

合計 80+45(郵資)=125.

我本來是只要買圓孔排母的, 但因賣家說一直找不到貨, 所以就下架了. 賣家說不好意思, 所以含郵算我 100 元, 還蠻有誠意, 列入最愛賣家. BA033 穩壓 IC 跟 AMS1117 一樣, 輸出 3.3V 電壓, 不過輸出較有力, 是 1A 的, 勝過 AMS1117 的 800mA. 而 UM66T 的音樂 IC 是 "給愛麗絲", 我覺得新奇就買下來玩.


母後 365 日

昨日 9/21 是母親逝世一周年, 雖然農曆 7/28 已做了對年, 但國曆的這天還是令我感傷. 那日輕度颱風鳳凰來襲, 但我還是堅持送母親回鄉下老家. 她不識字, 若在醫院往生, 我擔心她會找不到回家的路.

昨晚睡前誦阿彌陀經, 大悲咒, 往生咒, 與心經迴向. 


2015年9月21日 星期一

好書 : 13 堂課學會 Arduino 機器人製作

上週五 9/18 晚上去河堤還書時, 找到這本佳魁出版, 強國人毛勇寫的 "13 堂課學會 Arduino 機器人製作", 淺顯易懂, 我花不到一天 (事實上約三小時) 就看完了 :


此書從最簡單的固定走 8 字形, 到裝上微動開關避障, 地面灰階感測器循跡, 一直到走迷宮, 一步步介紹 Arduino 的常用函數, 非常適合初學者閱讀. 我已讀過好幾本 Arduino 書籍, 因此三個小時內就能看完.

硬體開發板雖然不斷推陳出新, 所謂長江後浪推前浪, 但如今 Arduino 依然傲笑江湖, 因為它已經成為創客世界的工業標準與典範. 加上強國人不斷壓低成本的 Arduino 相容板助陣, 是教育, 工業, 以及業餘玩家的首選. 這本書是我下一步製作自動車的入門參考.

書中範例下載 (到佳魁資訊搜尋 PB1508) :

# http://www.topteam.cc/06-download.php?action=download&did=192



2015年9月19日 星期六

高中班親會

姊姊上高一時我有跟菁菁一起去參加新生說明會, 開學後班親會因為有事就沒去了. 這學期她升高二了, 通知單上提到有繁星等升學說明, 而姊姊又要去補習, 所以我決定去參加. 她的恬恬導師我也只在上學期姊姊去上海六中交流時在機場見過一面.

昨天感冒上班精神不佳, 回家後竟頭痛, 連咳嗽都會牽動右下腹痛. 如果今早還是不舒服就不去了, 還好昨晚九點半就睡了, 早上五點半醒來發現好多啦, 至少不會頭痛了, 咳起來下腹部也只有微微做痛而已. 本來想騎 youbike 去捷運站比較快, 反正我的一卡通信用卡到現在都還沒去租過腳踏車. 但東摸摸西摸摸就太晚, 乾脆騎機車去. 一趟路約 9 公里騎了半小時才到, 中山路紅綠燈實在太多了.

聽完恬恬導師的說明, 大致了解現在升大學方式主要是三種, 一是繁星, 二是申請, 如果這兩個都沒上或不滿意, 最後就是指考, 即傳統的聯考分發. 姊姊當然希望上繁星,  這要校排前 20~30 名. 但是她們是美術班啊, 校排要怎麼算? 我太遲鈍沒有及時發問, 只好用 Line 請教恬恬老師囉.


2015年9月16日 星期三

專家只學會了武斷

睡前看到這篇 "吃冰就會經痛? 小心子宮病變" 的新聞, 讓我覺得, 專家如果學會了武斷, 那就表示已經故步自封, 不再前進了. 文中該醫生斥責婦女經期吃冰會導致肚子痛是因為冷飲會凍壞子宮這種想法, "全世界大概只有台灣人有這種怪想法".

其實只要中醫傳布的地方, 中日韓越等國民間都有這種想法, 哪裡只有台灣, 這是武斷之一. 而且那不只是想法而已, 據聞很多女性都有實際的體驗. 這位醫生說冰冷的食物進到胃裡時, 溫度早已被加溫到與體溫相當, 怎會凍壞子宮? 關鍵就在於, 身體要將冷飲加熱, 要耗費許多能量, 這能量是無中生有嗎? 當然要從脾腎子宮等器官調些過來支應啊! 久而久之子宮就會缺乏能量而太寒, 婦女病就來了. 中醫雖也非萬能, 但中醫是從整體平衡來看身體的疾患, 不像西醫分科很細, 細到見樹不見林. 胃跟子宮毫無關係, 這是武斷之二.

我一直勸戒姐姐與菁菁, 經期時萬萬不可吃冰冷食物, 平時也盡量少吃冰. 冰不是好東西.

至於這則新聞, 我的評論是 : "西醫跟核工博士一樣, 以為讀了一點書就是神了. 人體的奧妙還有很多我們不了解的地方. 讀書人最糟的就是學會了武斷".

這世界不是憨人想的哈尼甘丹.


2015年9月15日 星期二

購買車用 FM 發射器

去年在露天看到一款車用 FM 發射器, 可以將音源輸出轉成 FM 訊號發射出去, 只要將車內音響的收音機頻率調在相同頻率, 即可播放音樂. 記得當時似乎才 125 元左右, 加運費 200 元有找, 就買了一個, 但試用結果不滿意, 會有雜訊, 就擱一邊了.

後來菁菁喜歡在車上聽 Youtube 的 MV, 而 Serena QRV 的音源插座很爛, 換了兩次還是常接觸不良, 就找出這個 FM 發射器再試試看, 發現原來是我不會用, 不是產品不好, 試過好幾個頻道, 發現只要調到 94.6 MHz 這個無人使用的頻率, 幾乎不會有干擾, 效果比接觸不良的有線好太多了. 其實就算有干擾, 因為車內功率比較大都可以蓋台過去, 除非干擾源功率很大.

只是可能空置了一段時間沒用, 鋰電池劣化了, 前陣子竟然膨起來, 拆開來看是這種 3.7V 的小鋰電池, 一顆要 34 元, 加上運費快 100 元 :

# 3.7V聚合物鋰電池301055 100MAH 錄音筆 藍牙耳機 小音箱 小玩具 $34

還不如買個新的, 查了露天最便宜是這個 $165 元的 :

車用MP3播放器 插手機 直接聽 3.5mm音源轉換器 $165

下次去長明街禾樺問看看有無賣這種小鋰電池, 有的話再來換修 (還要看價錢划不划算).

2015-09-19 補充 :

今天與菁菁回鄉下時測試這個新買的小物, 發現 94.6MHz 在行經市區時仍偶有干擾, 改成 FM 最低的 87.5MHz 就沒有了.


2015年9月14日 星期一

Arduino 基本語法筆記

Arduino 的程式語法基於 C/C++, 其實就是客製化的 C/C++ 語言, 其程式架構仿自廣為藝術與設計界人士熟悉的 Processing 語言, 而其開發工具 Arduino IDE 則是衍生自以 Processing 為基礎的電子開發設計平台 Wiring. 由於 Processing IDE 使用 Java 撰寫, 因此 Arduino IDE 有自帶一個 JRE. Processing 語言撰寫的程式稱為 Sketch (草稿碼), 乃是簡化後之 Java 語法, 經 IDE 編譯變成可執行的 Java 類別. 而 Arduino IDE 則是以 Processing IDE 為架構, 但是採用了 C/C++ 語法.

Processing 也有支援網頁的 Processing.js, 參考 :

# https://processing.org/exhibition/
# https://en.wikipedia.org/wiki/Processing.js

Arduino 程式可由五個部分組成 :

//1. 匯入函式庫與定義 (可有可無)
#include <SoftEasyTranfer.h>
#define LEDPIN 13;

//2. 宣告常數與全域變數 (可有可無)
const float PI=3.14159;
int r;

//3. 設定函式 (必要)
void setup() {}

//4. 無限迴圈  (必要)
void loop() {}

//5. 自訂函式 (可有可無)
float area(float r) {
  float a=PI*r*r;
  return a;
  }

其中 setup() 與 loop() 是一定要有的函式 (均無參數無傳回值), 其他則視需要而定. Arduino 語言採用 C/C++ 語法, 加上以 Wiring 為基礎的電子設計核心函式庫組合而成, 包括 Digital I/O, Analog I/O 等函式庫. 內建的函式庫可直接調用, 但若有使用第三方函式庫 (例如驅動感測器模組所需的函式庫), 則必須使用 include 前置指令引入. 此外, 也可以用前置指令 define 定義一個常數或巨集 (運算式).

前置指令乃 C 編譯器指令, 不屬於 C 語言本身, 其用途有三 :
  1. 引入標頭檔 : 例如 #include <myLibrary.h> 或 "myLibrary.h"
  2. 定義常數 : 例如 #define PI 3.14159
  3. 定義巨集 : 例如 #define AREA(r) PI*r*r
所以前置指令的功能一言以蔽之就是替換, include 就是在標頭處以指定之檔案內容替換; 而 define 就是在程式中用到所定義之常數與巨集名稱時, 以其內容替換. 巨集的功能事實上與函數類似, 不同之處是函式呼叫使用堆疊, 而巨集則是直接放在原始碼中, 執行效率較快 (但若很多地方都要用到時, 編譯後就會比較大).

標頭檔可用角括號 < > 或雙引號 "", 差別是用雙引號時, 前置處理器會先從原始檔所在位置開始去搜尋標頭檔; 而用角括號則會先從 libraries 目錄開始找.

以下整理這些基本語法以利後續實驗時查考之用, 以 UNO/Nano/Pro Mini 這些主要板子為對象. Arduino 語法文件請參考 :

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

變數與函式命名 (識別字) :
  1. 只能使用英數字與底線組合, 且不能以數字開頭, 英文字母有分大小寫.
  2. 不可使用保留字.
宣告變數時不一定要同時初始化 (賦值), 但為了 debugging 方便, 最好宣告同時也初始化, 例如 int 變數就設為 0, boolean 變數設為 false 等 :

char c;
int i=0;
boolean  b=false;

因源自 C/C++ 語言, 因此宣告變數時必須指定其值之資料類型, 函式若有傳回值也要指定傳回值之資料類型, 否則須宣告為 void, 例如 setup() 與 loop() 兩個必要函式就不會有傳回值, 故必須宣告為 void.

宣告於 setup() 與 loop() 或自訂函數外的變數稱為全域變數 (global), 在程式的任何地方皆能存取; 宣告於函數內部的變數 (包含傳入之引數) 稱為區域變數 (local), 其值僅在函數內部有效. 如果全域變數與函數中的區域變數同名, 則在函數中存取到的是區域變數, 例如 :

int i=1;
void setup() {
  Serial.begin(9600);
  int i=0;
  Serial.println(i);    //輸出 0 (區域變數)
  }
void loop() {}


資料類型 :

Arduino 的資料型態與 C 語言一樣, 但資料長度可能因板子而異, 下表適用於 UNO, Nano, Pro Mini 等以 ATmega328 為處理器的板子, 只有 Due 板子有所不同 :

 資料型態 說明 記憶體長度 數值範圍
 boolean 布林 8 bits true (1, HIGH), false(0, LOW)
 byte 位元組 8 bits 0~255
 char 字元 8 bits -128~127
 short  短整數 16 bits -32768~32767
 int 整數 16 bits -32768~32767
 word 字 16 bits 0~65535
 long 長整數 32 bits -2147483648~2147483647
 float 浮點數 32 bits +-3.4028235e+38
 double 倍精度浮點數 32 bits +-3.4028235e+38

Arduino 還有兩個非數值的資料類型 :

資料型態 說明
 void 用來表示函數無傳回值時之資料類型
 String 用來表示字串 (Arduino 0019 Alpha 版以後)

要注意的是 : 一般 Arduino 板子的 double 跟 float 是完全一樣的, 都是 32 位元, 而 Due 板則跟一般 C 語言一樣是 64 位元. 其次, 整數的 short 與 int 也是一樣 16 位元的 (但在 Due 板子, int 是 32 位元). 文件中整數類的 char, int, 與 long 這三種型態有分 signed (有號) 與 unsigned (無號的), 沒有提到 unsigned short, 但我測試是有的.

資料型態 說明 記憶體長度 數值範圍
 unsigned char 字元 8 bits 0~255
 unsigned short 短整數 16 bits 0~65535
 unsigned int 整數 16 bits 0~65535
 unsigned long 長整數 32 bits 0~4294967295

有號與無號差別在於最高位元是否拿來當正負符號 (2 的補數), signed 使用最高位元來表示正負數, 1 為負數, 0 為正數; 而 unsigned 則全部都是正數, 因此其可表示的正數範圍是 signed 的兩倍, 例如儲存字元用的 char 也可以用來儲存較小的整數 (8 位元), 若宣告為 unsigned char, 其範圍便從 -128~127 變成 0~255.

Char, short, int, 與 long 預設是 signed, 亦即只有全部用做正數的變數才需要宣告 unsigned. 正負值都有的變數宣告為 signed 是多此一舉. 整數類中最常用的是 int, 因為其範圍可以滿足大部分應用所需.

Char 是其實是 8 位元整數, 用來表示 ASCII 字元, 例如 'A' 實際上是以其 ASCII 碼 65 儲存的, 好處是可以透過運算處理字元, 例如 'A' + 2 就得到 'C'. 所以字元可以用下面兩種方式表示 :

char c='A';   //字元必須用單引號括起來, 不能用雙引號
char c=65;

Byte 與 word 類型只有正數, byte 與 char 一樣是 8 位元, 等於 unsigned char; 而 word 與 int 一樣是 16 位元, 等於 unsigned int.

布林值可用 true/false (必須小寫), 0/1, 或者 HIGH/LOW 表示, 依據使用場合何者較有意義而定. 其中 HIGH/LOW 是 Arduino 定義的常數, 適合用在 digitalWrite() 函數中表示 LED 輸出位準, 而 true/false 適合用在程式邏輯的分岐判斷.

指定資料類型時須注意不能超過範圍, 超過時將歸零或變成負數等非預期結果, 成為 roll-over (反折), 例如 byte 最大 255, 若存入 256, 仍可通過編譯, 但真正存入的值會變成 0; 而整數最大為 32767, 若存入 32768, 結果變成 -32768, 例如 :

void setup() {
  Serial.begin(9600);
  byte b=256;
  Serial.println(b);    //輸出 0
  int i=32768;
  Serial.println(i);     //輸出 -32768
}
void loop() {}

另外, 初始化一個變數時, 所賦予之字面值 (Literal) 的表示方法也會影響運算結果之正確性, 例如 2048*16=32768, 用 int 來儲存是不夠的, 必須宣告為 long. 此外字面值 2048 或 16 也至少要有一個後面加一個 "L" 或者用 (long) 強制轉型才行, 表示要以資料類型 long 儲存, 否則它會以 int 為類型進行運算, 例如 :

void setup() {
  Serial.begin(9600);
  long a=2048*16;    
  Serial.println(a);      //輸出 -32768 (非預期之結果)
  long b=2048L*16;    
  Serial.println(b);      //輸出 32768 (預期之結果)
  long c=(long)2048*16;    
  Serial.println(c);      //輸出 32768 (預期之結果)
}
void loop() {}

整數字面值強制轉型的後綴有三個, 大小寫均可 :
  1. L 或 l : 強制轉成長整數
  2. U 或 u : 強制轉成無號之整數 (int)
  3. UL 或 ul : 強制轉成無號之長整數
同樣地, 浮點數運算也要注意字面值的問題, 運算元之中至少要有一個必須以浮點數表示, 否則會得到非預期之結果, 如下例所示 :

void setup() {
  Serial.begin(9600);
  float a=1/2;
  Serial.println(a);    //輸出 0.00 (被當成整數處理)
  float b=1.0/2;
  Serial.println(b);    //輸出 0.50 (至少一個是浮點數即可)
  float c=(float)1/2;
  Serial.println(c);     //輸出 0.50 (強制轉型)
}
void loop() {}

Arduino 內建六個函式來轉換不同資料型態如下表 :

 型態轉換函式 說明
 char(x) 將任何型態之 x 轉成 char 型態
 byte(x) 將任何型態之 x 轉成 byte 型態
 int(x) 將任何型態之 x 轉成 int 型態
 word(x), word(h,l) 將任何型態之 x 轉成 word 型態, x 可拆成高位元組 h 與低位元組 l
 long(x) 將任何型態之 x 轉成 long 型態
 float(x) 將任何型態之 x 轉成 float 型態

要注意, 記憶體長度大的轉成較小時會有 roll-over 問題, 例如 :

void setup() {
  Serial.begin(9600);
  int i=257;
  byte b=byte(i);   //輸出 1, byte 類型最大值 255, 256 時反折為 0
  Serial.println(b);
  }
void loop() {}

另外, 整數除了用十進位表示外, 也可以用二進位 (0b/0B 開頭), 八進位 (0 開頭), 與十六進位表示 (0x/0X 開頭), 例如 :

void setup() {
  Serial.begin(9600);
  byte dec=128;
  byte bin=0b10000000;
  byte oct=0200;
  byte hex=0x80;
  Serial.println(dec);  //輸出 128
  Serial.println(bin);  //輸出 128
  Serial.println(oct);  //輸出 128
  Serial.println(hex);  //輸出 128
  }
void loop() {}

內建常數 :

Arduino 定義了八個內建常數 (注意, true 與 false 須小寫) :

 常數 說明
 HIGH 輸出高電位
 LOW 輸出低電位
 INPUT 輸入腳
 OUTPUT 輸出腳
 INPUT_PULLUP 啟動上拉電阻之輸入腳
 LED_BUILTIN 內建 LED (Pin 13)
 true 真 (=1)
 false 假 (=0)
 PI 圓周率 3.14159
 DEG_TO_RAD 角度轉弧度常數=PI/180=0.0174533
 RAD_TO_DEG 弧度轉角度常數=180/PI=57.29578

例如 LED 閃爍程式 :

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  }

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
  }

也可以用 const 自行定義常數 (但不可重複定義內建常數), 例如可定義常數 LED 以取代上內建的 LED_BUILTIN, 效果一樣 :

const int LED=13;

常數賦值後無法更改, 否則編譯時即會失敗. 此外常數名稱通常使用大寫, 但這只是習慣, 並非語法規則. 另外, Arduino 的 I/O 針腳預設是 INPUT, 因此只需要宣告哪些腳要當 OUTPUT 即可, pinMode(2, INPUT) 其實是多此一舉的.

陣列與字串 :

陣列可以先宣告再賦值 (使用大括號), 例如 :

int a[4];                   //宣告含有四個整數元素之陣列
a={1,2,3,4};            //陣列賦值

或者元素逐一賦值 :

int a[4];
a[0]=1;
a[1]=2;
a[2]=3;
a[3]=4;

或者宣告同時賦值 :

int a[4]={1,2,3,4};   //宣告同時賦值

也可以不填元素數目, 由編譯器自動計算 :

int a[]={1,2,3,4};     //由編譯器自動計算

但多維陣列只能第一維不寫交給編譯器去算, 其他維必須填寫, 例如 :

int a[][2]={{0,0},{0,1},{1,0},{1,1}};   //這樣 OK
int a[][]={{0,0},{0,1},{1,0},{1,1}};     //這樣不 OK (編譯失敗)
int a[4][2]={{0,0},{0,1},{1,0},{1,1}}; //當然 OK

陣列之存取是透過以 0 起始的索引, 最後一個元素之索引為長度減 1, 必須注意不可超過範圍. 由於 Arduino IDE 的 C 編譯器不會檢查索引是否超出範圍 (但會檢查是否夠放), 因此良好的 coding 習慣是利用常數來表示陣列大小, 例如 :

void setup() {
  Serial.begin(9600);
  const int COUNT=4;        //使用常數設定陣列大小
  int a[COUNT]={1,2,3,4};  
  for (int i=0; i<COUNT; i++) {  //利用常數避免索引超限
    Serial.println(a[i]);
    }
  }
void loop() {}


如果把上述迴圈的 COUNT 改為 5, 程式仍編譯成功, 執行結果為 :

1
2
3
4
26624  (超限的索引 4 存取到陣列外之數值)

可知, 陣列宣告的元素個數可以比賦值的多, 但不能少 (亦即要夠放, 否則會編譯失敗). 沒有被賦值的元素會有預設值, 整數類是 0, char 是空字元, 浮點數則是 0.00, 例如 :

void setup() {
  Serial.begin(9600);
  int a[5]={1,2,3,4};   //5 個元素只賦值 4 個
  for (int i=0; i<5; i++) {
    Serial.println(a[i]);
    }
  }
void loop() {}

執行結果為 :
1
2
3
4
0  (未賦值=預設值)

Arduino 裡的字串可以用兩種方法來做, 一是用 C 語言本來就有的 char 類型的陣列; 二是用 Arduino IDE 0095 版後提供的 String 資料類型 (C 語言本身無此資料型別, 而是透過 string.h 函式庫提供). 使用 char[] 儲存字串時要注意, 必須在陣列結尾加一個 '\0' (NULL) 字元 (直接用整數 0 也是可以的), 這是字串的結束符號 :

char a[5]={'A','B','C','D','\0'};   //四個字元外加一個 NULL
char a[5]={'A','B','C','D', 0};

C 語言中字元陣列與字串的差別如下圖所示, C 是利用字元陣列後面另加一個 NULL 來表示字串的 :



字串變數也可以直接用字串常值 (Literal) 來賦值 (注意, Arduino 中, 字元須用單引號, 字串須用雙引號括起來, C 語言則沒有字串), 如果自行指定長度, 必須比資料字元數多 1, 否則編譯失敗, 要不然乾脆不要指定, 由編譯器處理就好 :

char a[5]="ABCD";    //自行指定陣列大小, 須比資料字元多 1
char a[]="ABCD";      //由編譯器自動計算陣列大小

例如 :

void setup() {
  Serial.begin(9600);
  char a[]="ABCD";       //由編譯器自動計算陣列大小
  Serial.println(sizeof(a));     //輸出 5, 多一個 byte 存放結尾的 NULL (0)
  for (int i=0; i<sizeof(a); i++) {
    Serial.print(a[i]);      //輸出 ABCD
    }
  }
void loop() {}

此處使用了 Arduino 內建函數 sizeof() 來計算陣列的長度. 注意, sizeof() 事實上是在計算資料所占的總 byte 數, 因為 char 一個字元就是 一個 byte, 因此剛好就是字元陣列的長度. 如果關掉 Arduino 序列埠監視視窗, 改用 AccessPort 來觀察串列埠, 可知編譯器確實自動在陣列尾加上 NULL :



如果是浮點數陣列, 一個浮點數占 4 個 byte, 則 sizeof() 就不能用來當作陣列長度了, 例如 :

void setup() {
  Serial.begin(9600);
  float a[]={1.0, 2.0, 3.0, 4.0};
  Serial.println(sizeof(a));    //輸出 16, 4*4=16
  for (int i=0; i<sizeof(a); i++) {
    Serial.print(a[i]);   //輸出 1.002.003.004.000.00-0.000.000.000.000.000.000.000.000.00ovfovf
    }
  }
void loop() {} 

若要正確取得陣列長度, sizeof() 須除以資料類型的長度, 例如浮點數 sizeof(float) 會傳回 4 :

void setup() {
  Serial.begin(9600);
  float a[]={1.0, 2.0, 3.0, 4.0};
  Serial.println(sizeof(a)/sizeof(float));   //輸出 4
  for (int i=0; i<sizeof(a)/sizeof(float); i++) {
    Serial.print(a[i]);    //輸出 1.002.003.004.00
    }
  }
void loop() {}

二維陣列範例如下 :

void setup() {
  Serial.begin(9600);
  int a[][2]={{0,0},{0,1},{1,0},{1,1}};
  Serial.println(sizeof(a));  //輸出 16
  Serial.println(sizeof(a)/sizeof(int));  //輸出 8
}
void loop() {}

總之, sizeof() 若要用來取得陣列長度, 只能用在 byte, char, boolean 這三個資料長度為 8 位元的陣列上

一般變數當作引數傳入函數時是傳值呼叫, 亦即函數中會複製該變數進行運算, 不影響原值. 陣列也可以當作引數傳入函數中, 但不是用傳值呼叫, 而是傳址呼叫, 是將陣列頭的位址傳給函數, 因此會影響原陣列之值. 除了傳陣列位址外, 還要傳入陣列大小, 參考碁峰楊明豐 "Arduino 最佳入門與應用" ˇ3-7-3 節範例 :

void setup() {
  Serial.begin(9600);
  int a[]={1,2,3,4};
  Serial.println(sum(a,4));  //輸出 10
  }
int sum(int a[], int size) {  //傳入陣列位址與大小
  int sum=0;
  for (int i=0; i<size; i++) {
    sum += a[i];
    }
  return sum;
  }
void loop() {}

OK, 回到字串主題, 字串也可以組成陣列, 當然可以用兩層有 NULL 的字元陣列來表示, 例如 :

char users[][5]={{'P','e','t','e','r','\0'},{'A','m','y','\0'},{'K','e','l','l','y','\0'}};

注意, 這裡第二維必須填上各字串中最長字元數 (不含 NULL), 第一維 (字串數) 則可不填, 編譯器會自動設定. 當然, 若第一維填入比最長字串還長的數也是可以的, 編譯器只讀到 NULL 便停止, 後面多出來的字元 (存的是無法預測的值) 其實是浪費的.

比較方便的賦值方式是直接用字串常數賦值 :

char users[][5]={"Peter","Amy","Kelly"};

Arduino 在 0019 版後加入了 String 型別 (其實是一種物件), 此函式庫提供許多字串函數使字串處理更方便, 參考 :

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

也可以參考 C 語言的 string.h :

# C 語言標準函數庫分類導覽 - 字串處理 string.h


 字串函數 說明
 length() 傳回字串長度
 indexOf(val), indexOf(val, from) 從左方搜尋子字串, 傳回首次出現位置索引
 lastIndexOf(val), lastIndexOf(val, from) 從右方搜尋子字串, 傳回首次出現位置索引
 substring(from), substring(from, to) 傳回子字串
 replace(substr1, substr2) 將子字串 substr1 以子字串 substr2 取代
 concat(str) 將 str 字串串接在後面
 remove(index), remove(index, count) 刪除索引 index 到結尾之字元, 或刪除指定字元數 count
 toLowerCase() 傳回轉成小寫後之字串
 toUpperCase() 傳回轉成大寫後之字串
 charAt(index) 傳回指定索引之字元
 setCharAt(index, c) 將字串索引 index 之字元以指定字元 c 取代
 equals(str) 是否與指定字串 str 雷同, 傳回 true/false
 equalsIgnoreCase(str) 是否與指定字串 str 相同 (不分大小寫), 傳回 true/false
 compareTo(str) 與 str 字串逐字比較 ASCII 字元, 雷同傳回 0, 在後傳回正值, 在前傳回負值 
 startsWith(str) 是否以指定字串 str 開頭, 傳回 true/false
 endsWith(str) 是否以指定字串 str 結尾, 傳回 true/false
 trim() 清除開頭與結尾的空白字元
 toInt() 將以數字開頭直到非數字字元之字串轉成長整數傳回
 toFloat() 將以數字開頭直到非浮點數字元之字串轉成浮點數傳回
 reserve(bytes) 要求保留指定 bytes 數之記

例如 :

void setup() {
  Serial.begin(9600);
  String str="Hello World!";
  Serial.println(str.length());    //輸出 12
  Serial.println(str.indexOf(" "));        //輸出 5 (有找到傳回索引)
  Serial.println(str.indexOf(" ", 6));    //輸出 -1 (沒找到傳回 -1)
  Serial.println(str.lastIndexOf("!"));  //輸出 11
  Serial.println(str.substring(6));         //輸出 World!
  Serial.println(str.substring(0,7));      //輸出 Hello W
  str.replace("World","Tony");          
  Serial.println(str);                              //輸出 Hello Tony!
  str.concat(" Good Day!");              
  Serial.println(str);                              //輸出 Hello Tony! Good Day!
  str.remove(16);                                  //從 Day 前面空格開始刪
  Serial.println(str);                              //輸出 Hello Tony! Good
  str.remove(11,5);                               //從 G 前面空格開始刪
  Serial.println(str);                              //輸出 Hello Tony!
  str.toLowerCase();                          
  Serial.println(str);                              //輸出 hello tony!
  str.toUpperCase();                  
  Serial.println(str);                              //輸出 HELLO TONY!
  Serial.println(str.charAt(1));             //輸出 E
  str.setCharAt(5,'+');                           //將空格改為 +
  Serial.println(str);                              //輸出 HELLO+TONY!
  String str2="Hello+Tony!";
  Serial.println(str.equals(str2));          //輸出 0
  Serial.println(str.equalsIgnoreCase(str2));    //輸出 1
  Serial.println(str.compareTo(str2));  //輸出 -32 (不同,  str 在前)
  Serial.println(str.compareTo("HELLO+TONY!"));   //輸出 0 (雷同)
  Serial.println(str.startsWith("HELLO"));   //輸出 1
  Serial.println(str.endsWith("TONY!"));     //輸出 1
  str=" HELLO ";
  str.trim();
  Serial.println(str.length());   //輸出 5 (已刪除前後空格)
  str="180 Days";
  Serial.println(str.toInt());     //輸出 180
  str="65.245KG";
  Serial.println(str.toFloat());  //輸出 65.25 (四捨五入到小數第二位)
  }
void loop() {}

注意, 以上字串處理函式是 Arduino C 專用, 一般 C 語言沒有這些函式. 另外, 字串串接在 Arduino C 可以像 Python/Javacript 那樣用 + 直接串接, 在傳統 C 語言要用 strcat() 函式或指標, 不能用 +, 參考 :

Arduino - 字串


流程控制與迴圈 :

這部分與 C 語言完全一樣, 值得注意的是 switch case 指令的 case 值只能用字元整數. 其他資料型態如字串, 浮點數, 布林值均不允許. 例如 :

switch (val) {
  case 1 :
     //do something (for 1)
     break;
  case 2 :
     //do something (for 2)
     break;
  case 3 :
  case 4 :
     //do something (for 3 or 4)
     break;
  default :
     //do something
     break;
  }

每一個 case 若為獨立處理方式, 必須用 break 斷開, 否則會連續執行下一個 case (稱為 fall-through), 例如上面 case 3 與 case 4 都會執行 case=4 的程式碼. Case 值為字元範例如下 :

switch (val) {
  case 'Y' :
     //do something
     break;
  case 'N' :
     //do something
     break;
  case '?' :
     //do something
     break;
  default :
     //do something
     break;
  }

在 "Beginning C for Arduino, 2nd edition" 這本書裡的第四章提到有範圍的 case, 使用刪節號 "..." 表示範圍, 例如 :

char grade;
int score;
switch (score) {
  case 0...59 :
     grade='F';
     break;
  case 60...69 :
     grade='D';
     break;
  case 70...79 :
     grade='C';
     break;
  case 80...89 :
     grade='B';
     break;
  case 90...99 :
     grade='A';
     break;
  default :
     break;
  }


指標 :

C 語言的指標在 Arduino C 也有提供, 在全華黃新華等著 "微電腦原理與應用" 的 3-5 節有稍微提到, 而 "Beginning C for Arduino, 2nd edition" 這本書的第八章則有詳細介紹. 指標專門用來儲存記憶體的位址以間接地存取變數的內容, 是四十多年來 C 語言能笑傲江湖的關鍵功能, 一般高階語言如 Java 並不提供, 少部分雖有指標功能, 但不像 C 的指標那麼自由, 可以對指標進行算術計算來操控記憶體, 參考 :

# Pointer (computer programming)

Arduino 所有的板子之 SRAM 都在 64KB 以下 (UNO/Nano/Pro mini 等板子所使用的 ATMega328 微控器只有 2KB), 因此 Arduino 的指標在 SRAM 記憶體中都是占 2 個 bytes 來儲存變數的位址,  16 bits 最高可定址 2^16=64K Bytes.

指標的宣告範例如下 :

int *ptrPrice;

指標名稱與一般變數命名規則相同, 但為了可讀性, 建議以 ptr 開頭. 其次, 指標宣告中要有一個星號, 通常放在指標名稱前面, 這是告訴編譯器, 這個名稱不是一般變數, 而是一個儲存位址用的指標. 星號不一定要緊貼指標名稱, 也可以這樣寫 :

int* ptrPrice;
int * ptrPrice;

而前面的資料類型 int 是指此指標所指位址之記憶體中儲存之資料型態. 取得一個變數的存放位址用 & 運算子, 而要存取指標的內容則用 * 運算子, 例如 :

void setup() {
  Serial.begin(9600);
  int price=100;
  int *ptrPrice;   //宣告一個指標變數 ptrPrice 儲存位址, 所指位址存放 int 數值
  *ptrPrice=99;
  Serial.print("The address of ptrPrice=");   //顯示指標變數本身位址
  Serial.println((long)&ptrPrice);  //輸出 2294 (因板子而異)
  Serial.print("The content of ptrPrice=");   //顯示指標變數內容 (是個位址)
  Serial.println((long)ptrPrice);    //輸出 25344 (因板子而異)
  Serial.print("The address of price=");       //顯示一般變數之位址
  Serial.println((long)&price);      //輸出 2996 (因板子而異)
  Serial.print("The content of price=");       //顯示一般變數內容
  Serial.println((int)price);            //輸出 100
  Serial.print("The content of *ptrPrice=");  //顯示指標變數所指位址之內容
  Serial.println((int)*ptrPrice);     //輸出 99
  }
void loop() {}

此範例中各變數之記憶體位址關係可用下列圖表示 :


可知指標變數 ptrPrice 存放在記憶體位址 2294, 其內容是一個位址 (整數值), 指向另一個記憶體位址 25344, 其內容為 99; 而一般變數 price 則存放在記憶體位址 2296, 其內容是個整數值 100.

參考 :

Arduino 程式設計
# Arduino : 關於記憶體二三事
# Arduino 筆記 – 認識 Arduino


2015年9月13日 星期日

第 37 周記事

本周時序來到白露 (9/8) 了, 再過兩周即是秋分 (9/23), 白天會越來越短, 炎熱的夏天即將過去, 迎來我喜愛的秋冬. 一般人忌諱的農曆七月也在昨天結束了.

這禮拜主要是週四 (9/10) 幫媽做對年, 週六 (9/12) 參加超渡法會, 都順利完成了. 鄉下農務方面, 本周修剪了果樹的枝葉, 以待來年豐收; 其次, 今日移植了雞冠花與圓仔花到前院右側花圃. 其實我每周能用的時間不多, 主要時間花在烹調三餐. 最好是每周訂個小進度去做, 不要野心太大, 這也想整理, 那也想收拾, 終究會一事無成.

二哥對英文似乎不甚用心, 明年會考在即, 我想是否暫停專心準備考試為宜? 我應該先去跟老師討論再做決定才對, 卻貿然詢問他意見, 那當然好啊. 老師說不要停, 我也改變想法, 但 ... 這兩天為此失策懊悔不已. 我個性優柔寡斷, 凡事都謹慎思考, 多方推求, 惟此事卻過於倉促鄉愿.


2015年9月12日 星期六

超渡法會

今天是農曆七月三十, 慈恩寶塔舉行超渡法會, 姊姊說她也想跟我去 (跟補習班請假), 所以早上就回鄉下. 整理好後出發, 到那裏時還沒 11 點, 但祭品已排到尾端了. 現場人卻不多, 我想是一大早就將祭品送來擺好就回去, 下午超渡時再來.

中午廟方有提供齋飯, 約開十桌左右. 我不知道中午有辦桌, 所以來的時候路過超商還買了麵包. 越近中午人越多, 一張供桌都擠兩家供品才放得下. 剛開始大家都還認真雙手合十, 不到一小時, 聊天的聊天, 滑手機的滑手機, 唉.

下午三點結束, 回到家還沒四點, 趁著天還亮著, 拿了鋸子與鐮刀修剪馬路邊的幾棵芒果樹與蓮霧樹, 以及院子的龍眼樹, 這樣明年果實才會結得好.


2015年9月10日 星期四

對參年

今天請假一天回鄉下給媽做對參年, 也就是媽過世一周年的祭祀禮. 媽逝於去年甲午年農曆八月二十八日, 因去年閏九月, 故對年為七月二十八日. 做完這個後就可以將骨灰罈移回祖墳合祀, 但因今年利南北, 不利東西, 故預計明年清明前擇日辦理, 與掃墓一併進行. 完成後母親之身後事即完善矣. 這些處理, 媽在病房中即有交代, 我依吩咐執行.

牲禮仍委請鎮上有能伯處理, 糕粄也是訂媽以前的習慣光顧的遠房親戚陳媽媽做的, 龜粄與發粄各兩包, 外加小阿姨前日交代的兩包芝麻龜粄. 水果昨晚已備妥, 另於全聯購糖果與豆腐乳. 因前天 9/8 日晨寤寐中, 第一次夢到母親, 她坐在椅子上問說是否還有豆腐乳, 我回說市場有在賣 ... 然後就被鬧鐘吵醒了. 我特地去全聯買一罐好吃的江記豆腐乳, 今日帶去祭拜. 不過再怎麼好吃, 也沒有媽自己做的好吃.

今日前來上香者有大阿姨, 左營阿姨, 小阿姨, 小舅, 小舅媽, 高樹阿姨, 阿泉伯, 阿泉伯母, 以及阿珍表妹, 萬分感謝他們抽空前來. 我一早去左營接左營阿姨與阿珍, 因對高鐵一帶路況不熟, 從大中路上高架橋左轉往翠華路方向, 下橋後右轉即翠華路矣. 但回頭要上國十時, 剛好上班時刻, 中華路蓮池潭一帶車子多, 遶到舊左營火車站迴轉上國十, 耽擱了些時間, 但還是在九點半後到達舉行.

中午在美綠訂一桌請小舅阿姨們, 傍晚載小阿姨去高鐵搭車. 這次我直接從國十尾端右轉開進高鐵四樓停車場, 因阿珍說可以往下走到二樓停車場, 可通後站. 結果下到三樓看到一邊是嘟嘟房停車場入口, 一邊是翠華路出口, 正猶豫該怎麼走時,  後面車在叭, 只好往翠華路出口開, 一出去才發現前面又有一個往高鐵站分叉, 這不是我剛剛從國十過來的路嗎? 結果又遶回到四樓停車場了, 好像在玩雲霄飛車, 真是好笑. 關鍵是在三樓要抽嘟嘟房停車卡進去, 但在下二樓前必須去繳費過卡 (0 元), 才會讓你出去下二樓, 否則會卡在出口, 會被後面的車叭. 到二樓後即可遶到後站去了.  終於搞懂前後站穿越方式了!




2015年9月9日 星期三

Arduino 的時間函式

做完 Arduino UART 串列埠測試後, 覺得對基本的函式了解不夠, 所以就從時間函式開始做個詳細的測試. 以下都使用小巧輕便的 Nano 實驗.

Arduino 的時間函式有如下三個 :

一. millis() :

此函式會傳回從開機到現在的毫秒數, 無參數, 傳回值為 unsigned long, 故最大值為  4294967295 毫秒, 即 4294967.295 秒, 換算成天數為 4294967.295/86400=49.71027 天, 即約 50 天就會重新歸零.

測試程式如下 :

unsigned long time;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  time=millis();
  Serial.println(time);
  }

執行後開啟串列埠監視視窗, 會看到數字飛快地往下跑, 觀察這些數字可發現差額約為 9~10 ms, 亦即 loop() 函式執行一圈大約為 0.01 秒 (100Hz), 一秒鐘可以跑 100 圈.

2054497
2054507
2054516
2054525
2054535
2054544
2054553

二. micros() :

此函式會傳回從開機到現在的微秒數, 也就是 millis() 的千分之一. 無參數, 傳回值為 unsigned long, 故最大值為  4294967295 微秒, 即 4294.967295 秒, 換算成分鐘數為 4294.967295/60=71.583 分, 即約 72 分鐘就會重新歸零.

測試程式如下 : 

unsigned long time;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  time=micros();
  Serial.println(time);
  }

擷取串列埠輸出資料, 差額為 9360, 即 9.36ms, 似乎較 millis() 精確些.

2429604
2438964
2448324
2457684
2467044
2476404
2485764

三. delay(ms)

暫停程式執行, 傳入參數是型態為 unsigned long 之毫秒數, 無傳回值.

測試程式 :

nsigned long time;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  time=millis();
  delay(1000);
  Serial.println(time);
  }

我們在呼叫 millis() 後加了 delay(1000) 來延遲 1 秒再往下執行, 結果序列埠輸出如下 :

0
999
1999
3000
4000
5000
6000
7001
8001
9000
10001
11001
12002
13002
14003
15002
16003

可見迴圈所耗時間幾乎完全被 delay() 主宰了. 


此函式是延遲指定之微秒數, 但是與 delay() 不同的是, 傳入參數類型為 unsigned int, 因此最大值為 16383, 即約 16 ms. 

測試程式 : 

unsigned long time;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  time=micros();
  delayMicroseconds(16383);
  Serial.println(time);
  }

擷取序列埠視窗輸出如下, 可見差額約為 16ms : 

0
16656
33424
50216
67008
83796
100592
117428
134276
151120
167972
184812

但若傳入超過 16383 之參數, 仍可編譯成功, 只是執行結果並非所預期者, 例如傳入 1000000 (延遲 1 秒) :

unsigned long time;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  time=micros();
  delayMicroseconds(1000000);
  Serial.println(time);
  }

序列埠輸出卻是差額 700 左右 :

0
148
752
1508
2320
3132
3936
4740
5552
6364
7176
7972

delayMicroseconds() 是給延遲數千微秒用的, 秒級的應該使用 delay().


2015年9月8日 星期二

第 36 周記事

這學期小狐狸們開學後, 菁菁變國一生了, 除周三外, 周一到周五放學後就直接到補習班報到, 姊姊則在學校圖書館讀書, 所以晚餐只有我跟二哥, 感覺很孤寂啊! 這就是將來離巢期的感受嗎?

自從去年媽住院後我就沒時間在家煮晚餐了. 姊姊也在去年上了高中, 晚餐都在補習班或校外吃, 可以說去年夏末開始, 我這個煮夫就徹底被解除武裝了. 以前星期天晚上回高雄時, 都滿載母親菜園種的青菜, 接下來的周一到周五要把它們煮完, 所以每天下班都是趕學校安親班接小狐狸們, 有時順路上市場, 再趕回家煮晚餐, 也順便帶明天的便當. 但現在是天天訂便當. 或許人生中的一個階段真的已經過去了, 也毋須喟嘆.

過去的十幾年裡, 我扮演了很多角色, 當了人夫, 父親, 姨丈, 姑丈 ... 我參與了小狐狸們的所有成長過程, 從洗澡換尿布, 陪睡陪玩, 每晚大家睡在和式地板上, 讓他們在床邊故事中慢慢睡著, 再爬起來寫程式 ... 從幼稚園到小學, 雖然不敢說無役不與, 但親職講座, 校外教學, 運動會, 班親會, 甚至晨光教學, 這些我都是幾乎都參與, 拍下非常多的照片影片.

二哥一歲半的時候, 那時我還要輪班, 沒班的時候我會用親子腳踏車載他沿著河堤社區的自行車道逛, 在炎炎的夏日中, 清風徐徐吹來, 停在河堤旁的樹蔭下聽蟬鳴, 二哥不多久就會睡著, 我還要用一隻手撐著他的小臉一邊慢慢騎回家 ... 這樣悠閒的生活畫面至今仍鮮活地在我腦海中.

過去這一周好像沒啥事情可記, 要嘛就是周三~周五二哥的國中畢旅, 還有週三晚上上司升職請本組同事去海港餐廳大快朵頤一番, 當日菁菁不用補習, 所以水某也帶她一起去現場訂位. 每次去吃到飽的餐廳都吃到過飽, 美食當前實在無法拒絕, 更何況是人家請客咧!

本週閒暇時專心學習 Arduinbo 的串列埠通訊, 到今天整整一週終於完成了. 光看書是沒有用的, 一定要親手作實驗才會真正了解其運作. Arduino 雖然標榜要給非電子背景人士發揮創意, 但是我實作下來發現, 一些簡單的小實驗當然沒啥問題, 但是牽涉較深背景知識的應用就要花很多苦心去研究了, 連我這個電機出身的人都要費點心了, 何況是業外人士. 不過, 天下無難事, 只怕有心人. 比起 8051, Arduino 算是相當簡化了.

週日到菜園繞一圈, 發現兩三顆冬瓜壞掉了, 沒有防曬與及時採摘真是浪費了.


2015年9月5日 星期六

ACER D260 小筆電系統回復

前幾天發現以前用的 AOD D260 小筆電開不了機, 用 GHOST 想要從 D 碟恢復系統, 卻發現 Primary partition 標示 "Diagnostic" 而非 NTFS 而無法選, GHOST 檔相當於是垃圾可刪除矣. 只好拿出還原光碟, 裝上外接光碟機, 重開機後就詢問是否要還原系統, 確定後就開始還原到出廠狀態, 只要兩片還原光碟即可, 第三片 Driver 用不到 (除非硬碟壞了換過硬碟).

還原後 C 碟是乾淨的 XP Home (13G 左右), 首先要安裝 Chrome, 然後重新安裝開發環境, 主要是 :
  1. Editplus 3.7 32 位元
  2. JDK7 32 位元
  3. Python 2.7.10 
  4. GAE SDK
  5. Appserv 2.5.10 (Appache+PHP+MySQL)
  6. Lua 5.3.1
  7. Arduino IDE 
  8. CH340G USB 驅動程式
  9. PL2303 USB 驅動程式 
  10. Fritzing
基本工具有 :
  1. WinCDEmu 4.0 (虛擬光碟)
  2. PotPlayer
  3. FreePrimo32
  4. BANDIZIP
  5. Adobe AIR
  6. Microsoft Silverlight
參考 :

新電腦的安裝設定
Carbonbook 系統更新與軟體安裝紀錄


2015年9月4日 星期五

串列埠測試軟體 AccessPort

串列埠軟體在公司最常用的是 PuTTY, 簡單小巧功能強, 而微軟以前 XP 時代的超級終端機我一直很排斥, 感覺是粗製濫造的產品. 前陣子測試 ESP8266 則使用 RealTerm, 感覺還不錯, 只是 UI 比較凌亂而已.

今天在葉難的 "Arduino 一試就上手" 這本書的 6-4 RS-232 通訊協定這節看到他推薦 AccessPort 這個對岸強國人開發的串列埠測試與監控軟體, 由於有繁體中文介面, 用起來比較順手, 所以就下載下來試試看, 可到 sudt.com 下載 zip 檔 (我掃過沒問題) 解壓縮即可使用, 不需安裝 :

Download AccessPort 1.37

此軟體有兩個頁籤 : terminal 與 monitor. 前者就是一般的終端機視窗, 顯示指定串列埠傳送與接收之資料, 而 monitor 視窗則可以監看指定串列埠所有資料之進出. 輸出視窗上面的按鈕可以選擇以字元還是 HEX 顯示傳送與接收之資料, 也可以將接收到的資料存成 TXT 檔 :


按左上角的齒輪即可設定串列埠, Arduino 測試通常使用 8,N,1, 9600 (8 位元資料, 無同位檢查, 停止位元 1, 9600 bps) :


# 通訊埠軟體介紹


2015年9月3日 星期四

使用 3.3V 稽納二極體與電阻做 ESP8266 位準轉換

之前為了 ESP8266 與 Arduino 介接問題, 探討過用 MOSFET 2N7000 製作位準轉換電路, 經我買零件實際測試可用, 而且是雙向有提升電阻, I2C 也可用的轉換方式, 參考 :

# 用 MOFFET 2N7000 做 5V 與 3.3V 位準轉換

不過對於 ESP8266 與 Arduino 介接還有其他方式, 雙向也不是必要, 因為 TX 與 RX 都是單向傳輸. 今天在下面文章中找到兩種新方法, 一個是用分壓電阻來將 Arduino 的 5V 降壓成 3.3V :

# A Simple IoT Project with the ESP8266 WiFi module

此文作者使用 3 個 1K 歐姆電阻串接, 將來自 Arduino 的 TX 接腳的 5V 電壓分壓, 得到 5*2/3=3.33 V 電壓給 ESP8266 的 RX 接腳. 由於 ESP8266 RX 內阻高, 所以電壓不會與理論差很多. 而 ESP8266 的 TX 接腳則直接與 Arduino 的 RX 連接, 不需要轉換為 5V, 因為在 TTL 邏輯而言, 大於 2.4V 即為 High 邏輯, 因此 ESP8266 的 RX 輸出的 3.3V 會被視為 High, 對於雛形板測試而言, 毋須提升到 5V 是沒問題的, 但對於產品而言, 基於穩定性, 不論 TX/RX 都做位準轉換為宜.

第二種方法是使用 3.3V 的稽納二極體, 如下列這篇 :

# ESP8266 wiring schemas

此文章列出六種介接方法, 其中第五種方法就是上面所提的電阻分壓法 (這裡用 220 歐姆), 而第三種方法就是稽那二極體法, 詳細說明參看 :

Level Shifting Between TTL and CMOS

使用 1N5226 3.3V 稽納二極體與 10K 電阻串接, 這樣當 Arduino 的 TX 腳送出高電壓時, 稽納就會將其轉成 3.3V 輸出. 注意稽納二極體有並接一個 100uF 電容以吸納電壓突波 (例如 ESP8266 開機時). 下面這篇也是用稽納二極體 :

# First Impression on the ESP8266 Serial-to-WiFi Module

3.3V 的稽納二極體很便宜, 一顆大約 2 元左右 :

# MOTOROLA 1N5226稽納二極體 SILICON 3.3V 20 mA ZENER DIODES 30顆一標 $20
# 《icshopping_com》1W 3.3V 稽納二極體(5pcs) $16
《icshopping_com》1/2W 3.3V 稽納二極體(5pcs) $16

我使用 upverter 繪製接線圖如下 :


20150918 補充 :

今早上班前把上回跟 ICSHOPPING 買的 3.3V 稽納找出來實際測試, 發現事實上不到 3.3V, 量起來只有 2.38V, 雖然 TTL 2V 以上就被視為 High, 但 margin 似乎有點小. 實驗後覺得好像用 10K + 20K 純電阻的比較簡單方便. (20151012 補充, 經測試, 10k+20k 會因為時間常數太大造成信號不穩, 應該用 1k+2k, 或 1k+1k+1k 組合). 


PS :  有黑色邊那側是陰極, 要接電阻, 參考 :

# 【婷婷的店】Arduino 1N4728A(3.3V/1W) 穩壓稽納基納二極體(Zener Diode)1顆


2015年9月2日 星期三

二哥國中畢旅

好像上國一才不過是昨天的事, 今天二哥卻要去國中畢旅了. 問他要不要帶相機, 他說用手機就好. 但到七點菁菁要上學時才發現, 手機放在椅子上忘了帶. 騎機車拿去給他, 順便載菁菁上學, 但到校門口時, 遊覽車剛好出發了.

二哥中年級之後就不喜歡照相, 所以我也沒有他小學畢旅的照片. 既然不愛照相那就算了, 我個人可能戀舊, 總想留住人生中的驚鴻一瞥, 看看舊照片會讓我跌回往日時光. 或許不愛照相的人不喜歡回顧過去, 總是向前看吧!

昨晚看了一下三天行程表, 跟姊姊以前畢旅差不多, 參觀科博館, 玻璃藝術館, 住新竹煙波飯店, 回程去麗寶樂園等等. 這學期被老師指定當班長, 他看起來老大不願意, 但卻推不掉 (他的理由是讓校內多元學習表現尚未達標的同學去做, 因他已達標). 我想讓他當當幹部學習如何管理班級也不錯.


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 真的讓我們省了非常多的工啊 !