2016年9月27日 星期二

關於運動手環

上週我那只戴了十幾年的手錶罷工了, 因為前陣子進了一些水氣我沒理它, 更不想再花錢去修, 抱著堪用就用的心態, 結果不到一個月就真的跟我 say goodbye 了. 去年 11 月拿去給寶島鐘錶修, 跟我報價 1000 元, 說要大整修加換防水封條, 結果咧, 不到半年又進水, 我只不過洗手沒拿下來而已, 從來不曾戴著洗澡. 因大整修有保固, 拿去再檢修回來用不到半年又進水了. 這回對寶島鐘錶沒信心了, 乾脆換運動手環好了. 關於手環產品演進可參考下列文章 :

618看我的-2016 什麼智能手環值得買

這篇描述得非常詳盡, 但我的基本要求就是不要太貴, 只要具有時鐘鬧鐘功能可取代手錶即可, 運動紀錄功能不是很重要, 有計步或甩手統計功能即可. 檢視目前市面上的產品, 大致考慮下列三個 :

一. 小米手環 2 :

定價 865 元, USB 充電, 具有 OLED 時鐘顯示功能, 可開啟抬手顯示 (時間與日期不同頁面). 具備十組鬧鐘設定, 時間到時以震動叫醒, 若貪睡十分鐘後會再叫醒. 可量心跳, 生活防水防塵, 充一次電約可使用 20 天, 續航力還不錯. 打開藍芽與手機連線的話這款以我的要求來講真的 CP 值超高, 可惜因為熱銷小米網站經常顯示暫時缺貨, 通常要在中午 12 點搶購才買得到. 參考 :

小米手環2忙供貨 在台熱銷4萬支
有螢幕了!還是不可思議便宜嗎?!小米手環2 開箱動手玩
不要買 小米手環 的理由
小米手環 - 智慧鬧鐘 Q&A(新手必看 小米手環團隊出品)
小米手環2初體驗,附21個常見問題解答 
# BLE 裝置研究 - 小米手環 (1) (小米手環與物聯網)
BLE 裝置研究 - 小米手環 (2) (小米手環與物聯網)
# E68精品館 炫彩 小米手環 2代 替換帶 二代 運動手環 腕帶 智能手環 彩色手環 錶帶 $59

小米運動 App 要求 iphone 4S 以上 / Android 作業系統 4.3 以上版本,且需支援藍牙 4.0 的裝置. 還好, 我的華為榮耀 3C 符合此要求.


二. GoLife Care One :

市價 1680 元, USB 充電, 具有 OLED 時鐘與抬手顯示以及鬧鐘震動提醒功能 (日期時間同一畫面), 可偵測睡眠品質. 此款為 Papago 產品, 功能與小米手環 2 相當, 但價格較高. 此款機身與錶帶結合在一起, 蠻有時尚感的, 但是沒辦法像 Care 那樣更換錶帶, 所以錶帶壞了整個也就 GG 了不能配戴了. 同時其充電方式也跟小米手環 2 不同, 採用一個 USB 充電夾夾住機身充電. 充飽電只能使用約 7 天, 這一點就跟小米手環沒得比了, 不過有一點比較特別的是它每周會寄送運動統計報表.

# PChome : GOLiFE Care One 智慧健康手環-黑色 $1680
羽毛般的輕柔~Golife care one智慧健康手環
GOLiFE Care 健康智慧手環,平價、實用、方便、健康管理
使用紀錄】GOLiFE Care 智慧手環_Part_3_我們個性不合
GOLiFE Care 智慧手環_Part_1_今天純開箱 (使用 USB 充電夾充電)
[心得]好看又實用的GOLIFE 運動手環
實測 平價智慧手環 PAPAGO GOLiFE Care 震動叫起床

Care 與 Care One 差別 :

"care錶面為不銹鋼材質有支援各種訊息通知(來電/簡訊/FB/LINE等等)care one錶面非不銹鋼材質沒有支援各種訊息通知這是最大的差異"


三. Garmin Vivofit 3 :

此為 GPS 大廠 Garmin 產品, 市價約 3500, 功能當然比上面兩款強, 支援 26 種語言, 而且時尚感十足, 最主要的差別是它不是內建鋰電池, 而是跟一般石英錶一樣使用一顆 CR1632 鈕扣電池驅動 (市價約 50 元), 續航力大約一年, 不需要三天兩頭充電, 有電力顯示器可知是否需要更換電池. 其次是具備 50 公尺防水功能, 可戴著游泳潛水, 是幾近全功能運動手環, 而且可以替換錶帶. 其 OLED 螢幕是一直開啟, 不需要抬手顯示功能. 美中不足的是沒有鬧鐘震動功能, 另外久坐提示也只有聲音沒有震動.

參考 :

Garmin Vivofit 3 vs Vivofit 2 – 5 Key Differences
GARMIN VIVOFIT 2 VS VIVOFIT 3
Garmin vivofit 3 健身手環 開箱試用分享
Garmin Vivofit 懶人必備的健康緊箍咒
買了 Garmin Vivofit
<敗家> Jawbone UP vs Garmin Vivofit 穿戴裝置大PK
# 10 理由 為何 Garmin Vivofit 3 更好

另外還有一款人因科技的 MWB181, 市價 990 元 :

# PCHome 人因科技 MWB181 ERGOLINK 生活智慧手錶 $999
# 順發 PCHome 人因科技 MWB181 ERGOLINK 生活智慧手錶 $990
# 人因 MWB181 ERGOLINK 生活智慧手錶 開箱

它比小米手環多了控制手機播放音樂功能, 但我覺得不是很必要, 而且查不到電池可用幾天資料, 所以還是優先考慮小米手環 2. 我同事他兒子最近買了一個小米手環 2 給他, 使用起來感覺還不錯, 他說可以叫他兒子幫我上網搶購再寄回來.

2016-10-04 (二) 補充 :

今天中午 12:00 上小米網站去搶購小米手環 2, 超扯的, 我 11:40 就登入系統等倒數結束, 結果時間一到按放入購物車, 馬上出現伺服器忙線中, 但已在排隊, 1 分鐘後排到了, 出現選擇購買件數畫面, 選 2 個, 竟然回應黑色已售罄! 真是非常瞎! 竟然一分鐘就賣完了! 有這麼多人搶購嗎? 小米這樣搞飢餓行銷恐怕會餓死自己喔!

2016-10-12 (四) 補充 :

今天同事把他兒子幫我搶購的小米手環帶來了, 馬上進行充電, 然後打開手機的藍芽與小米運動 App, 但是連結了好久都失敗, 搞了半天原來是之前另外一個同事有給我小米手環一代, 當時 App 有跟它綁定, 所以 App 就一直在連結那個一代手環, 難怪都連不上, 解除鎖定後馬上就連上了. 接著會先進行升級 :










颱風政治學

今天梅姬颱風來襲, 高雄只放下午半天颱風假, 據說陳菊臉書被灌爆, 民眾怨聲載道. 早上上班時確實只下小雨, 伴隨不強的陣風, 感覺陳菊是對的. 但到了十點左右風勢就轉強了, 我十點半休息時間下樓去領錢, 雨傘差點被反折, 颱風提前兩三個小時報到.

中午 12 點下班, 開車去鳳中接二哥的路上, 不時見到機車騎士不敵強風, 有的被吹倒, 有的在路邊暫停不敢前進, 真的是險象環生. 我開著被同事喻為戰車的 QRV 都感覺到車子不時晃動了, 難怪那些機車騎士哀哀叫. 想到廣大勞工朋友還要騎機車去接小孩放學, 我就想, 這些所謂的父母官有在苦民所苦嗎? 陳菊說她也想當聖誕老公公, 言下之意, 她是受限於法令不得不作此決定嗎? 難道她不知道這是中央授權地方首長做決策的嗎?

我個人認為柯 P 所謂放錯颱風假對不起國家民族的說法是自命清高, 沽名釣譽. 地方父母官對得起國家民族, 但冒著生命危險上下班接小孩的人民, 那就抱歉對不起囉. 地方首長挨罵了就把責任推給氣象局, 有一點科學常識的人都知道, 氣象這種東西千變萬化, 都已經用設計核武等級的超級電腦在計算預測了, 還是無法像模擬核彈試爆那樣準確, 饒了氣象局吧! 他們的預報比起以前已經很準確了, 但還只能是參考, 首長要體察民眾苦處多加裁量, 就算是到 11 點, 甚至第二天 6 點再決策也不遲. 像這樣放半天還不如不放, 大家待在教室與辦公室可能會比冒著強風趕時間衝去接小孩要安全.


光靠數據治國是不行的, 那跟用機器人下決策有何兩樣, 更何況現在機器人已經進步到人工智慧了哩! 雖說依據氣象局預報, 預估下午南部才會感受到強風來襲, 但有點統計學知識的人都了解, 預測都有誤差, 凡事都要預留餘裕 (Margin), 受薪階級都知道上班不遲到的方法只有一個, 那就是提早十分鐘出門. 但是現在縣市長颱風假的裁量都不知預留餘裕, 政治人物的腦筋不知道是越來越僵化, 還是態度越來越傲慢!


2016年9月26日 星期一

2016 年第 37 周記事

本週五去 NISSAN 做 QRV 75000 公里保養 (實際上已跑到 77000 公里), 花了 5000 元. 下次 80000 公里大保養要換四個輪胎, 費用上看 20000 元. 雖說原廠保養費用較貴, 但一來較安心, 二來在等候區有吃有喝有報紙看, 也是一種難得的悠閒哩.

週五中午跟露天買的莊頭北瓦斯爐到貨, 花了 3800. 週六晚飯後將新瓦斯爐換上去, 測試 OK, 老牌莊頭北工業的果然好用.

九月又快到尾聲了, 每天都覺得很忙, 但回想起來又不知道在忙甚麼. 生活就是在這樣 : 很忙 ... 不知在忙甚麼之中度過. 自問 2015 年在忙甚麼? 忘了! 真的, 沒有幾個人可以清楚記得過去十年, 五年, 甚至去年, 上個月在忙甚麼. 人生難道就像陀螺一樣, 日復一日旋轉, 都不知道自己在轉甚麼勁嗎?

又來了一個叫梅姬的颱風, 但明天高雄只放半天, 所以還是早點睡覺吧.


谷歌街景圖的歷史印象

今天在 Yahoo 看到這則新聞 :

# 谷歌老家 喜見亡故大伯母街影

宜蘭縣府員工聽說 Google 街景圖可能會錄下已故親人影像, 輸入住址竟真的發現已故親人曾被谷歌街景車拍攝入鏡. 我也輸入鄉下老家住址試試, 谷歌街景車還真的在 2012 年 3 月經過我家門口拍攝, 當時母親還在世, 可惜沒被拍到, 不過卻留下那時她整理的菜園欣欣向榮的景象 :


下面這張截圖是大門口影像, 那時高大的波蘿蜜樹因為太密都被母親鋸掉, 現在又都長回去了. 門口那一大欉月桃是母親每年端午節包鹼粽必用, 前年她往生後, 爸說沒人會包粽子了, 而且長得太茂盛會影響進出視線, 就全部砍除改種花.


2012 年 3 月啊, 我已忘記那時都在忙些甚麼了. 歲月悠悠, 往日時光總是讓我感到一絲淡淡的憂傷.

PS :

我擷取保存這些街景圖照片只是為了抓住那時的景象, 因為現在已經改變甚大.


Arduino 聲音測試 (二) : 模擬特雷門琴

兩周前去母校高應大圖館還書時, 無意間在書架上看到這本 "Make : 簡易的Arduino 專題製作 (莊啟晃等譯, 碁峰出版)" :

Source : 三民書局

翻閱之下發現雖然內容簡單, 沒甚麼較吸睛的實驗, 但其中有一個特雷門琴 (Theremin) 還蠻有趣的, 也是 tone() 函數的應用, 它只需要一個蜂鳴器, 一個光敏電阻, 以及一個 10K 電阻就可以進行測試了. 實際電路接線如下 :

光敏電阻與 10 K 電阻串聯, 光敏電阻一端接 5V, 與電阻串接處接 Arduino 的 A0 腳, 電阻另一端接地. 而無源蜂鳴器則 + 端接 Arduino 的 D9,  - 端接地.

我將其範例程式改編為下面的測試 1 :

測試 1 :

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

void loop() {
  int value=analogRead(A0);  //return 0~1023
  Serial.println(value);
  int f=map(value, 400, 1000, 120, 1500);  //mapping 400~1000 to 120~1500
  tone(9, f, 10);  //make sounds
  delay(1); //delay for stable response
  }

此程式從 A0 讀取光敏電阻與 10K 電阻的分壓電壓值 (0~5V), AnalogRead() 會經 A/D 轉換後傳回 0~1023 的數值, 然後呼叫 map() 函數將其對應至 120 Hz~1500 Hz 頻率丟給 tone() 函數發音. 用燈光照射光敏電阻會讓頻率變高亢, 用手遮住光線就變低沉, 實際測試感覺聲音好難聽.

在葉難寫的 "Arduino 輕鬆入門" 這本書的 6.3 節也介紹了這個特雷門琴, 此琴為俄國發明家 Leon Theremin 所發明的電子合成樂器, 具有一個狀似天線的金屬桿以及一個環狀金屬圈, 前者控制發聲頻率, 後者控制音量強度. 透過演奏者的手與金屬桿與金屬圈之距離變化, 改變電容量之大小, 使內部震盪電路之頻率改變而發出不同頻率之聲音. 參考 :

https://zh.wikipedia.org/wiki/特雷門

在此實驗中我們是利用光敏電阻的阻值變化來模擬真正特雷門琴的電容變化以改變發聲頻率, 但沒辦法模擬音量變化.

我將 "Arduino 輕鬆入門" 6.3 節範例改編如下 :

測試 2 :

#define C5  523

int buzzerPin=9;

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

void loop() {
  int value=analogRead(A0);  //return 0~1023
  Serial.print("A0=");
  Serial.print(value);
  Serial.print(" ");
  int f=C5 + value/4;
  Serial.print("f=");
  tone(buzzerPin, f);  //make sounds
  Serial.println(f);
  }

此程式計算發聲頻率的方式與上面測試 1 不同, 它以中央 C (低音 Do) 為基準頻率, 將自 A0 獨到的 0~1024 數值除以 4 後加上此基準頻率為發音頻率. 此程式發出之頻率範圍為 523 Hz ~ 779 Hz 之間變動. 因 tone() 沒有指定持續時間, 因此會持續發聲.

真正的特雷門琴據說不好演奏, 需要極高的技巧, 如下影片所示 :


演奏者看起來就像在摸著空氣演奏一樣, 它沒有小提琴那樣實質的弦, 所以抓音階位置全憑演奏者的演練經驗.


2016年9月21日 星期三

★ 用 Blynk 同時控制多個設備

昨天有網友問我是否可以用 App Inventor 2 設計一個 App 來同時控制多個設備, 以我的菜園灑水控制器來說, 若菜園有多處裝有 Arduino 灑水控制器, 希望在 App 上按一個按鈕就可以控制它們同時灑水或停止. 這讓我馬上想到 Blynk 應該可以輕易完成這個任務, 不需要花時間自行設計一個 App 才對.

以下使用我自行焊接的兩個 Arduino+ESP8266 物聯網模組來測試 Blynk 群控的功能, 利用 App 上的一個 Slider 滑桿元件同時控制 Arduino NANO 板上的 D13 LED 閃爍速度. 此 IOT 模組的製作方式參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (四) : 完結篇

也可以根據下列電路圖用洞洞板連線 (AMS1117 可用 3.3V 穩壓模組取代) :


關於 Blynk 的用法, 參考之前的一系列實驗記錄 :

Blynk 應用 (一) : 用手機控制 Arduino
Blynk 應用 (二) : 在手機顯示即時溫溼度與光度
Blynk 的控制元件 (Controllers)
Blynk 的顯示元件 (Displays)
Blynk 的通知元件 (Notifications)
Blynk 的其他元件 (Others)
# Blynk 的虛擬腳位用法整理

在 Blynk App 上則是先新增一個專案, 在此專案中新增一個 Slider M 元件, 然後綁定一個虛擬腳位 V0, 修改值域為 100 ~ 1000, 代表 LED 閃爍的頻率 (其實是延遲時間), 100 表示快閃, 1000 表示慢閃.



此專案分享如下 :


測試程式如下 :

測試 1 :

#define BLYNK_PRINT Serial  //Comment this out to disable prints and save space
#include <SoftwareSerial.h>
#include <ESP8266_Lib.h>
#include <BlynkSimpleShieldEsp8266.h>

SoftwareSerial esp8266(7, 8); //(RX, TX)
ESP8266 wifi(&esp8266); //create wifi object

char ssid[]="H30-L02-webbot";
char pass[]="1234567890";
char auth[]="f4d4aa2dc86849adb0971d0xxxxxx371";
volatile int blinkRate=100;  //default blink rate

void setup() {
  Serial.begin(9600); //Set console baud rate
  esp8266.begin(9600); //Set ESP8266 baud rate
  Blynk.begin(auth, wifi, ssid, pass);
  }

void loop() {
  Blynk.run();
  blinkD13Led(blinkRate);
  }

BLYNK_WRITE(V0) { //called when V0 updated by App slider
  blinkRate=param.asInt(); //store value from App slider
  Serial.println(blinkRate);  //show current blink rate
  }

void blinkD13Led(int t) {
  digitalWrite(13, HIGH);
  delay(t);          
  digitalWrite(13, LOW);
  delay(t);          
  }

此處我們須準備一個特定的寫入事件處理函數 BLYNK_WRITE(V0) 讓 V0 腳位值更新時被呼叫, 因為手機 App 的 Slider 是一個控制元件, 當滑動滑桿時會將滑桿的值寫入伺服器中此專案之虛擬腳位 V0, 產生一個寫入事件, 會觸發設備中的寫入事件處理函數被呼叫一次. 在此函數中, 我們用特定的變數 param 取得最新的 V0 腳位之值, 用來更新全域變數 blinkRate, 此變數會被 blinkD13LED() 用來更改 LED 的閃爍頻率. 跟中斷事件類似, 由於 blinkRate 隨時會被更新, 因此要宣告為 volatile 以避免被編譯器優化.

這兩塊 IOT 模組都上傳相同的程式, 上電後, 由於 blinkRate 初始值設為 100, 因此會先快閃, 等我們滑動滑桿之後就會改變閃爍頻率 :


從上面影片可知, 滑動滑桿時, 兩個 NANO 板上的 D13 LED 都同時改變其閃爍頻率, 可見用 Blynk 可輕易達成群控之目的.

但上面測試 1 有個問題, 即每次重開機預設都是快閃 (blinkRate 預設 100), 必須在 App 上操作滑桿才會變更頻率. 要如何讓設備一開機或重新連線時能以最後一次滑桿之值閃爍呢? 這可以用特定之 BLYNK_CONNECTED() 函數來實現, 此函數會在設備重新與 Blynk Cloud 伺服器連線成功時被呼叫, 我們只要在此事件處理程序中呼叫 Blynk.syncVirtual(vPin) 即可取得伺服器上該虛擬腳位之值, 從而觸發虛擬腳位之寫入動作, 使 BLYNK_WRITE() 函數被呼叫, 這樣就會以上次的設定值閃爍了.

測試 2 :

#define BLYNK_PRINT Serial  //Comment this out to disable prints and save space
#include <SoftwareSerial.h>
#include <ESP8266_Lib.h>
#include <BlynkSimpleShieldEsp8266.h>

SoftwareSerial esp8266(7, 8); //(RX, TX)
ESP8266 wifi(&esp8266); //create wifi object
int blinkRate;

char ssid[]="H30-L02-webbot";
char pass[]="1234567890";
char auth[]="f4d4aa2dc86849adb0971d0xxxxxx371";

void setup() {
  Serial.begin(9600); //Set console baud rate
  esp8266.begin(9600); //Set ESP8266 baud rate
  Blynk.begin(auth, wifi, ssid, pass);
  }

void loop() {
  Blynk.run();
  blinkD13Led(blinkRate);
  }

BLYNK_WRITE(V0) { //called when V0 updated by App slider
  blinkRate=param.asInt(); //store value from App slider
  Serial.println(blinkRate);
  }

BLYNK_CONNECTED() { //get data stored in virtual pin V0 from server
  Blynk.syncVirtual(V0);
  }

void blinkD13Led(int t) {
  digitalWrite(13, HIGH);
  delay(t);        
  digitalWrite(13, LOW);
  delay(t);        
  }

此程式中的 BLYNK_CONNECTED() 會在設備連上 Blynk 伺服器時被呼叫 (全部大寫的函數都是事件處理函數), 在裡面我們呼叫了指定虛擬腳位的同步函數 Blynk.syncVirtual(V0) 向伺服器取得 V0 的值, 這會導致一個 V0 刷新動作, 使得 BLYNK_WRITE(V0) 被呼叫, 從而使 LED 以上次的設定閃爍. 以上測試 2 也同時回應了網友關於程式重新起始時, 滑桿之值不是停留在上一次設定值的問題.


母親兩周年忌日

今天是母親兩周年忌日, 我已經當了兩年沒媽的孩子了.


2016年9月20日 星期二

玉山信用卡紅利點數兌換

今天在考慮買哪種瓦斯爐時, 發現購物網站可以用信用卡紅利點數折抵, 找出玉山信用卡帳單一瞧, 哇, 我的玉山信用卡點數已累積超過 3 萬, 但是划不划算呢? 依據雅虎商城紅利折抵方式說明, 試算如下 :

發卡銀行   最高折抵上限(註1)   消費限制(註2)   點數限制(註3)   折抵比例(註4)
玉山銀行   50%                            120元                  1000點                1000 點:60元

註 1 - 最高折抵上限:可折抵消費金額的最高比例,例銀行規定最高折抵上限為 50% ,則消費 800 元最高可用點數折抵 400 元。
註 2 - 消費限制:指的是購物車總結帳金額需達到銀行所規定的消費限制。
註 3 - 點數限制:持卡人最少需要的點數,超過此限制點數方可進行銀行紅利折抵。
註 4 - 銀行信用卡紅利點數折抵現金的比例及折抵單位,如1000點:70元,即每1000點紅利點數可折抵70元。

以玉山來說, 最高折抵上限為 50%, 所以買 4600 元的瓦斯爐只能折抵一半, 即 2300 元; 折抵比例 1000 點兌 60 元, 即 2300 元需用掉 (2300/60)*1000=38333 點, 卡好, 我現在才剛滿 30000 點, 等於全部用光還不夠哩! 只能折抵 30000/1000*60=1800 元而已!

看看銀行本身我想兌換的商品 :
  1. 松騰空氣清淨機 : 32000 點
    商品編號: 10950526
    自付額商品編號: 10950522
    (市價 1800~2000)
  2. 國際牌數位無線電話(公司貨): 31600 點
    商品編號: 88050226
    自付額商品編號: 88050222
    (市價約 2200~2300)
  3. 三洋來電顯示無線電話機 : 23300 點
    商品編號: 10990096
    自付額商品編號: 10990092
    (市價約 1600)
  4. 尚朋堂微電腦電磁爐 :29800 點
    商品編號: 10991156
    自付額商品編號: 10991152
    (市價約 1300)
  5. 歌林手持直立旋風吸塵器 : 28900 點
    商品編號: 88040566
    自付額商品編號: 88040562
    (市價約 1300~1400)
我想兌換國際牌數位無線電話, 但是點數還不到 31600, 其他的品項還比不上買瓦斯爐折抵 30000 點, 所以似乎用紅利折抵較划算.


買瓦斯爐

上週回鄉下煮菜時就發現瓦斯爐右邊那口出現黃色火焰 (這會造成鍋底變黑), 只剩下左邊那口可用, 這樣煮菜速度就減半了. 這台是媽跟市集街瓦斯爐具行買的, 歷史悠久, 我看起碼用了 20 年有了.

週日帶姐姐與菁菁去市集買完菜, 繞到上元問看看有沒有賣瓦斯爐, 結果最便宜的竟然要 4800, 而且還是沒聽過的牌子. 菁菁說又醜又貴不要買, 我想有道理, 反正貨比三家不吃虧, 不要急, 回高雄再去特力屋看看.

今天先上特力屋網站找到莊頭北這款 TG-6603 型的定價 5290 元, 滿 5000 折 450 為 5290-450=4840 元, 似乎尚可接受, 而且是有品牌的 :

# TG-6603(LPG) 莊頭北內焰安全瓦斯爐 液化 (能源效率二級) $5290

但後來在比價網發現 Yahoo 商城更便宜, 才賣 4600, 乾脆跟 Yahoo 買就好了, 省得還要跑一趟特力屋 :

# A1153【莊頭北】內焰安全瓦斯爐 TG-6603(不銹鋼色+桶裝瓦斯)。含運送安裝。 $4600
【原廠送貨及安裝.不怕受騙】TG-6603內焰不銹鋼面板台爐安全瓦斯爐免運費(全省服務) $4250

我在露天也有找到莊頭北其他較便宜的款式, 但不是內焰型的, 內焰的據說熱效率較好 :

# 【莊頭北】安全瓦斯爐(TPG-603)*.桶裝瓦斯 $3980 (不含安裝)
# 高雄莊頭北 TG-6001 T 安全瓦斯台爐 【廚房世界 實體店面】$3200

還有一種紅外線瓦斯爐, 價格差不多, 據說優點是較省瓦斯 (3 成), 但爐頭怕水, 但是開火無聲, 沒有一般瓦斯爐火燃燒時的嘶嘶聲, 就像無聲的電動車一樣, 用起來可能會怪怪的 :

# 一點都不想重來之廚房家電選購篇
# 紅外線瓦斯爐與一般瓦斯爐的優缺點
# *鎮隆*【和家牌】遠紅外線雙口陶瓷瓦斯爐KS-888(本機型分天然/液化機種) $4580

另外我也找到這篇關於內焰型的說明 :

# 內焰式瓦斯爐
# [真.開箱]林內瓦斯爐120 天之使用與品質追蹤報告

想來想去, 還是跟 Yahoo 買好了.

20160921 補充 :

今天打電話到莊頭北公司詢問, 客服說 TG-6603 是另一家公司櫻花的產品, 不是莊頭北, 驚! 產品上明明標誌莊頭北三字, 怎變成櫻花? 原來莊頭北因投資大陸虧損造成財務危機, 莊頭北三字的中文商標被競爭對手櫻花公司標走了, 參見 :

莊頭北商標拍賣,台灣櫻花公司得標

在莊頭北工業的網站上找到這款 AS-627 TSV, 似乎還不錯 :

# 莊頭北工業 AS-627TSV-A 檯爐 $4800
# 【 達人水電廣場】 莊頭北 AS-627 TSV 不銹鋼安全瓦斯爐 $3800
# 【阿貴不貴屋】 莊頭北 AS-627 TSV 不銹鋼安全瓦斯爐 $3800

20160923 補充 :

我最後決定跟高雄苓雅區這家水電鋪買莊頭北工業的 AS-627TSV, 我想還是買傳統雙環的瓦斯爐比較習慣, 不需要甚麼內焰式的啦 而且老牌莊頭北有口碑 :

【BONA居家水電舖】莊頭北工業老莊頭北瓦斯爐台爐 AS-627TSV銅合金二環爐頭不銹鋼瓦斯爐 $3889

早上老闆送貨來時還送一瓶洗碗精.


2016年9月19日 星期一

2016 年第 36 周記事

本周因為週三莫蘭蒂颱風緣故連休五天! 這跟春節連假差不多了, 上班族大多盼望颱風假, 但農人或當老闆的可就憂心匆匆. 據說颱風不比我們少的日本竟然沒有颱風假哩! 經驗中大部分颱風都在晚上或凌晨呼嘯而過, 上下班時間狂吹的似乎很少, 所以其實不影響上班, 但這次莫蘭蒂就是在白天下午肆虐高雄屏東, 我看上得了班卻下不了班, 不放假是不行的.

今年初買的六棵果樹種在菜園, 其中一棵紅心芭樂才一條枝枒長到腰部高而已, 竟然已經開花結果, 爸摘下給我看, 已經成熟散發出芭樂特有的果香了, 吃起來蠻香甜的. 週日下午岳父來時檢視了這六株果樹, 我才知道原來這些接枝的果樹繁茂的枝葉原來都是從底下母株抽出來的, 接枝的反而長不大, 棗子樹甚至接枝的已經枯萎, 都被岳父給剪除了, 希望這樣能讓接枝的部分獲得養分, 重新抽出新芽.


本周花了週五週六兩天空閒時間把 Arduino 的發音功能測試完畢, 我覺得挺好玩的. 其實我主要用途只是要讓 Arduino 在系統異常時能發出告警聲罷了, 但既然開始了那就做完整一點囉, 真是龜毛的完美主義者. 原本預計連假要對鄉下的兩間書房進行大整理, 但時間花在 Arduino 上就停不下來, 只在周日晚上煮菜間隔時間整理冰箱, 清出好幾瓶母親以前做的醬菜, 像黃豆醬鳳梨, 這是煮香菇雞湯的好材料, 實在捨不得丟.


2016年9月17日 星期六

Arduino 的聲音測試 (一)

很早就想做這個實驗, 因為只需要 Arduino NANO 加上一個蜂鳴器就可以進行了, 頂多加一顆按鈕開關, 這跟物聯網無關, 因此不需要用到 ESP8266 上網. 趁著中秋連假, 就把這個簡單的小實驗做完吧!

以下實驗所用的程式是參考下列幾本書裡面的範例加以修改來的 :
  1. Arduino 輕鬆入門, 葉難, 博碩出版 (第 6 章)
  2. Arduino 互動設計入門 2, 趙英傑, 旗標 (第 13-1, 13-2 節)
  3. Arduino 最佳入門與應用第二版, 楊明峰, 碁峰 (第 11 章)
  4. Arduino Cookbook 錦囊妙計, 歐萊禮 (第 9 節)
我使用之前製作的 Arduino NANO+ESP8266 IOT 模組來進行實驗, 在設計此模組時有考量到如果偵測到異常狀況可能需要發出警告音, 所以有加裝了一個無源蜂鳴器 (Passive piezo speaker), 透過一個跳線針腳連接到 Arduino 的 D9 腳, 另一端接地即可. 由於無源蜂鳴器為高阻抗, 因此不需要串聯限流電阻. 如果專案中不需要用到蜂鳴器, 且又必須用到 D9 腳時, 可將跳線帽拔掉, 讓蜂鳴器與 D9 脫鉤即可, 如下圖左半部所示 (左上是跳線帽, 左下圓形物為蜂鳴器, 此為第一版的 IOT 模組) :



關於此模組佈線圖與製作方式, 參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (四) : 完結篇

注意, 購買蜂鳴器要指名無源蜂鳴器, 這樣才能透過 PWM 方式控制其發聲頻率 (又稱為它激式), 不要買到有源蜂鳴器, 這種蜂鳴器內建震盪電路 (又稱為自激式), 只要一通電就會發出固定頻率的聲音, 無法利用 PWM 對其音頻進行控制. 另外還要注意其電壓, 要配合 Arduino 板是跑 3.3V 還是 5V 而定, 我都使用 5V 系統, 需要 3.3V 時再用 AMS1117 來降壓. 我買的無源蜂鳴器長相如下 :



剛買來上面有一張貼紙, 標示其接腳有分正負極, 長腳為正, 短腳為負.

首先說明一下蜂鳴器的發聲原理, 一般的無源蜂鳴器主要是由一片金屬片 (銅片) 與壓電感應材料構成, 通電後金屬片會移動 (稱為壓電效應), 無電時金屬片復位, 亦即壓電效應可將電能轉換成機械能, 跟一般喇叭的音盆震動發聲原理相似.

原本聲音是連續的類比信號, 數位系統無法輸出類比信號, 但是可以利用不同頻率的方波來發聲, 因為如果對蜂鳴器施予週期性方波, 金屬片就會來回震動, 只要方波週期在 20Hz 到20KHz 之間, 就會發出人耳聽得到的聲音.


利用數位方波來模擬類比效果的方法稱為脈寬調變 PWM (Pulse Width Modulation), 參考 :

[Arduino] 脈衝寬度調變 (PWM) 與 Arduino – Pulse Width Modulation
Make: Projects|技能培養系列:進階Arduino聲響合成技術
[Arduino] 脈衝寬度調變 (PWM) 與 Arduino – Pulse Width Modulation

PWM 有兩個參數, 一個是單位為 Hz 的頻率, 即週期的倒數, 它決定了聲音的音高 (Pitch); 另一個參數是 Duty Cycle (工作週期), 方波一個週期有 HIGH (峰值) 與 LOW (谷值) 兩個狀態, 峰值時間佔整個週期的百分比稱為 Duty Cycle, 它決定了聲音的音色.

Duty cycle=峰值時間/週期

Arduino 內建了 tone() 與 noTone() 兩個函式來對數位接腳輸出 Duty Cycle 為 50% 的方波 (即 HIGH, LOW 各佔週期之一半) :

 函式 說明
 tone(pin, frequency, duration) 對指定數位接腳 pin 發出 duration 毫秒, 頻率為 frequency 之方波
 tone(pin, frequency)  對指定數位接腳 pin 持續發出頻率為 frequency 之方波
 noTone(pin) 停止對指定數位接腳 pin 發出方波

其中 tone() 有三個參數 tone(pin, frequency, duration) 與兩個參數 tone(pin, frequency) 兩種, 前者有指定發音期間的時間到時就會停止; 而後者會一直發音直到呼叫 noTone(pin) 才會停止.

注意, Arduino 同時只能對一個數位接腳輸出一個音調, 它既不能同時讓兩個數位接腳發聲, 也不能對同一接腳發出兩種頻率的聲音. 這是因為 ATmega328 處理器只有三個硬體計時器, 其中 timer0 被 millis() 函式用掉了 (它也負責 D5 與 D6 的 PWM 輸出); 而 timer1 則被 Servo.h 函式庫用掉了 (它也負責 D9 與 D10 的 PWM 輸出); 所以 tone() 只能用剩下的 timer2 來產生音調. 注意, 因為 timer2 負責 D3 與 D11 的 PWM, 所以使用 tone() 函數時會干擾這兩個接腳的 PWM 輸出功能. ATmega328 硬體計時器使用情形摘要如下 :

 硬體計時器 控制 PWM 接腳 預設使用者
 timer0 D5, D6 millis() 
 timer1 D9, D10 Servo.h
 timer2 D3, D11 tone()

參考 "Arduino 輕鬆入門" 第 6-4 節說明以及下列文章 :

Questions about millis()
# http://playground.arduino.cc/ComponentLib/Servotimer1

下面我們就來測試一下 tone() 函數, 首先測試 1 是讓 Arduino 發出 1 KHz 的告警音, 這可以用在偵測到異常時發出訊號用 :

測試 1 :

int buzzerPin=9;  //D9 conectted to a buzzer

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

void loop() {
  alarmBeep(buzzerPin);
  }

void alarmBeep(int pin) {
  tone(pin, 1000, 1000);
  delay(2000);
  }

此程式會先發出 1 秒 Bee 後停兩秒, 如此周而復始.

下面測試 2 則是模擬鬧鐘的鬧鈴音 :

測試 2  :

int buzzerPin=9;

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

void loop() {
  alarmClockBeep(buzzerPin);  
  }

void alarmClockBeep(int pin) {
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(1000);
  }

此程式是發出 4 聲短促的 Bee 聲後暫停 1 秒, 周而復始.

另外我在 "Arduino 最佳入門與應用" 11-3 節看到一個模擬電話鈴聲的範例, 我將其改寫為如下測試 3 :

測試 3 :

int buzzerPin=9;

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

void loop() {
  ringTone(buzzerPin);  
  }

void ringTone(int pin) {
  for (int i=0; i<10; i++) { //repeat 10 times
    tone(pin, 1000);
    delay(50);
    tone(pin, 500);
    delay(50);
    }
  noTone(pin);
  delay(2000);
  }

電話振鈴音是以 1000 Hz 與 500 Hz 間隔 50 毫秒重複 10 次模擬出來的, 然後用 noTone() 來暫停 2 秒. 我覺得這電話鈴聲跟真的一樣耶! 還蠻有趣的.

還有一個常作為告警鈴聲的是警車的笛聲, 我參考了下面這篇文章加以改編成測試 4 :

# Arduino Police Light and Sound

測試 4 :

int buzzerPin=9;

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

void loop() {
  policeSiren(buzzerPin);  
  }

void policeSiren(int pin) {
  for (int i=150; i<1800; i++) { //upward tone
    tone(pin, i, 10);
    delay(1);
    }
  for (int i=1800; i>150; i--) { //downward tone
    tone(pin, i, 10);  
    delay(1);
    }
  }

還有一個可以表示系統有問題的音訊是救護車 "歐伊歐伊" 的笛聲, 我參考下面這篇文章裡的範例, 將其修改為測試 5  :

Arduino Fun – Door Entry Alarm

測試 5 :

int buzzerPin=9;

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

void loop() {
  ambulenceSiren(buzzerPin);  
  }

void ambulenceSiren(int pin) {
  tone(pin, 400);        
  delay(500);
  tone(pin, 800);        
  delay(500);
  noTone(pin);
  }

其他參考 :

# Arduino Siren Sound Generator Circuit - Customizable

除了產生機械音外, Arduino 也可以產生樂音, 即依據樂理發出特定頻率與節拍的聲音. 頻率部分, 聲音的頻率有時又被稱為音高 (Pitch), 但所謂的音高其實是指我們的聽覺對物理世界中的聲音頻率之主觀認知, 與機械性客觀的頻率意涵不同, 但在指涉頻率高低上兩者意思一樣, 所以兩個詞也就經常被混用了.

樂理上把一個音階分成了 8 個音度, 首尾兩個 Do 在頻率上剛好相差 2 倍. 而這 8 個音度加上半音又分成 12 個音符, 音符有唱名 (Do, Re, Mi, Fa, So, La, Si, Do) 與相對之音名 (A, B, C, D, E, F, G, A) 兩種標示. 其中的 C 大調低音 La 為 440 Hz, 被用來作為樂器調校的標準音. 一個音階的 12 個音符要差兩倍頻率的話, 每一個頻率之間的乘數就是 2^(1/12), 即 2 開 12 次方, 大約是 1.05946 倍. 根據此倍數可計算出各音符的近似頻率如下表 :

 1 2 3 4 5 6 7 8 9 10 11 12
音名 C C# D D# E F F# G G# A A# B
 0 16 17 18 19 21 22 23 25 26 28 29 31
 1 33 35 37 39 41 44 46 49 52 55 58 62
 2 65 69 73 78  82  87  93  98  104 110 117 123
 3 131 139 147 156 165 175 185 196 208 220 233 247
 4 262 277 294 311 330 349 370 392 415 440 466 493
 5 523 554 587 622 659 698 740 784 831 880 932 988
 6 1046 1109 1175 1245 1319 1397 1480 1568 1661 1760 1864 1976
 7 2093 2217 2349 2489 2637 2794 2960 3136 3322 3520 3729 3951
 8 4186 4435 4699 4978 5274 5588 5920 6272 6645 7040 7459 7902

其中藍色字體部分為標準 88 鍵樂器的音域範圍, 而 C4 (262 Hz) 即為鋼琴鍵盤的中央 C (C 大調低音 Do). 依據這張樂音頻率分配表就可以利用 tone() 函數讓蜂鳴器發出各種音符了.

除了頻率之外還需要節拍 (tempo) 才能構成音樂的旋律, 這就需要利用 tone() 函數的持續時間以及 delay() 函數來模擬停頓效果了, 關於節拍參考 :

# 音符時值

下列測試 6 是我參考 "Arduino 互動設計入門 2" 13-1 節瑪莉歐旋律的範例加以改編的 :

測試 6 :

int buzzerPin=9;

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

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, 523, 150);  //C5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, 784, 150);  //G5
  delay(3000);
  }

這裡每個音符之間要用 delay(150) 來模擬每個琴鍵轉換之空檔, 否則所有的音符就會黏在一起. 另外, 四分休止符的時間長度也剛好是停頓 150 毫秒. 如果要像上面的程式一樣, 每次都要查頻率表實在太麻煩了, 而且一堆數字看起來令人眼花撩亂, 其實每個音符可以用 define 或 const 將頻率以音名來代換, 這樣程式可讀性比較高, 如下測試 7 所示 :

測試 7 :

#define E5 659
#define C5 523
#define G5 784

int buzzerPin=9;

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

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, C5, 150);  //C5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, G5, 150);  //G5
  delay(3000);
  }

如果換另一首樂曲就要 define 所用到的音符的話很麻煩, 乾脆將上面的頻率分配表另外打成一個 notes.h 檔, 然後用 include 匯入即可. 此檔內容如下 :

#define C0  18
#define CS0 17
#define D0  18
#define DS0 19
#define E0  21
#define F0  22
#define FS0 23
#define G0  25
#define GS0 26
#define A0  28
#define AS0 29
#define B0  31
#define C1  33
#define CS1 35
#define D1  37
#define DS1 39
#define B0  31
#define C1  33
#define CS1 35
#define D1  37
#define DS1 39
#define E1  41
#define F1  44
#define FS1 46
#define G1  49
#define GS1 52
#define A1  55
#define AS1 58
#define B1  62
#define C2  65
#define CS2 69
#define D2  73
#define DS2 78
#define E2  82
#define F2  87
#define FS2 93
#define G2  98
#define GS2 104
#define A2  110
#define AS2 117
#define B2  123
#define C3  131
#define CS3 139
#define D3  147
#define DS3 156
#define E3  165
#define F3  175
#define FS3 185
#define G3  196
#define GS3 208
#define A3  220
#define AS3 233
#define B3  247
#define C4  262
#define CS4 277
#define D4  294
#define DS4 311
#define E4  330
#define F4  349
#define FS4 370
#define G4  392
#define GS4 415
#define A4  440
#define AS4 466
#define B4  494
#define C5  523
#define CS5 554
#define D5  587
#define DS5 622
#define E5  659
#define F5  698
#define FS5 740
#define G5  784
#define GS5 831
#define A5  880
#define AS5 932
#define B5  988
#define C6  1047
#define CS6 1109
#define D6  1175
#define DS6 1245
#define E6  1319
#define F6  1397
#define FS6 1480
#define G6  1568
#define GS6 1661
#define A6  1760
#define AS6 1865
#define B6  1976
#define C7  2093
#define CS7 2217
#define D7  2349
#define DS7 2489
#define E7  2637
#define F7  2794
#define FS7 2960
#define G7  3136
#define GS7 3322
#define A7  3520
#define AS7 3729
#define B7  3951
#define C8  4186
#define CS8 4435
#define D8  4699
#define DS8 4978
#define E8  5274
#define F8  5588
#define FS8 5920
#define G8  6272
#define GS8 6645
#define A8  7040
#define AS8 7459
#define B8  7902

由於係自訂的標頭檔, 因此要放在主檔案目錄內, 如下圖所示 :



而且要用雙引號匯入 (內建函式庫才是用角括號匯入) :

# include "notes.h"

使用 notes.h 將上面程式改為如下測試 8 :

測試 8 :

#include "notes.h"

int buzzerPin=9;

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

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, E5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(300);
  tone(pin, C5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(300);
  tone(pin, G5, 150);
  delay(3000);
  }

效果是一樣的. 在 Arduino IDE 中也有內建一個 toneMelody 範例, 可從 "檔案/範例/02.Digital/toneMelody" 開啟, 我將其改編為如下測試 9 :

測試 9 :

#include "notes.h"

int buzzerPin=9;
int note[]={C4, G3, G3, A3, G3, 0, B3, C4};  //note name
int duration[]={4, 8, 8, 4, 4, 4, 4, 4};  //note type (tempo)

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

void loop() {
  melody(buzzerPin);
  }

void melody(int pin) {
  for (int i=0; i<8; i++) { //visit 8 notes
    int d=1000/duration[i]; //transform tempo to ms
    tone(pin, note[i], d);
    int p=d * 1.3; //suitable pause=plus 30% duration
    delay(p); //pause between notes
    noTone(8);
    }
  delay(2000);
  }

在這個程式中, 我分別為音符與節拍定義了 note 與 duration 兩個陣列, 其中 note 第 6 個音符 0 表示頻率為 0, 也就是休止符的意思. 而 duration 陣列之值, 4 表示 四分音符; 8 表示八分音符, 這樣可讀性比較高, 但在 melody() 函數中, 我們必須將其轉換成毫秒數, 在此以全音符為 1 秒, 則四分音符為 1000/4=250 毫秒, 亦即用 1000 毫秒除以 duration 陣列的元素值即可. 另外每個音符之間必須模擬一個琴鍵轉換的停頓, 否則每一個音符會黏在一起, 這裡以節拍的 1.3 倍時間為最適宜.

在 "Arduino 最佳入門與應用" 的 11-3-4 節有一個演奏小蜜蜂的範例, 我把它改編成下列測試 10, 小蜜蜂的簡譜如下 :

|5 3 3 - |4 2 2 - |1 2 3 4 |5 5 5 - |

|5 3 3 - |4 2 2 - |1 3 5 5 |3 - - - |

|2 2 2 2 |2 3 4 - |3 3 3 3 |3 4 5 –|

|5 3 3 - |4 2 2 - |1 3 5 5 |1 - - - |

參考 :

# 小蜜蜂簡譜

其中 1, 2, 3, 4, 5 分別對應到 C5, D5, E5, F5, G5 五個音符, 將其轉換成 note[] 陣列, 每個音節有 4 拍, 以四分音符當作一拍.

測試 10 :

#include "notes.h"

int buzzerPin=9;

int note[]={G5, E5, E5, 0, F5, D5, D5, 0, C5, D5, E5, F5, G5, G5, G5, 0,
            G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, E5, 0, 0, 0,
            D5, D5, D5, D5, D5, E5, F5, 0, E5, E5, E5, E5, E5, F5, G5, 0,
            G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, C5, 0, 0, 0};
         
int duration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

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

void loop() {
  littleBee(buzzerPin, sizeof(note)/sizeof(int));
  }

void littleBee(int pin, int count) {
 for (int i=0; i<count; i++) {
    int d=1000/duration[i];
    tone(pin, note[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

這裡比較特別的地方是要把 note[] 或 duration[] 陣列的長度傳進函數中, 以便 for 迴圈擷取樂譜中的全部音符與音長. 由於 sizeof() 事實上是傳回這個陣列所占的 byte 數, 並非元素個數, 因此必須除以此陣列元素之資料型別長度, 才會得到元素個數, 即陣列長度.

所以只要有簡譜, 我們可以很快地將其轉換成音符與音長陣列, 套用上面的程式架構讓 Arduino 透過蜂鳴器演奏音樂.

下面是小星星的簡譜 :

|1 1 5 5|6 6 5 -|4 4 3 3|2 2 1 -|

|5 5 4 4|3 3 2 -|5 5 4 4|3 3 2 -|

|1 1 5 5|6 6 5 -|4 4 3 3|2 2 1 -|

這裡簡譜的 1, 2, 3, 4, 5, 6 分別對應到 
C5, D5, E5, F5, G5, A5 六個音符, 將其轉換成 note[] 陣列, 每個音節有 4 拍, 以四分音符當作一拍. 其演奏程式如下 :

測試 11 :

#include "notes.h"

int buzzerPin=9;

int note[]={C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0,
            G5, G5, F5, F5, E5, E5, D5, 0, G5, G5, F5, F5, E5, E5, D5, 0,
            C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0};
         
int duration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

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

void loop() {
  littleStar(buzzerPin, sizeof(note)/sizeof(int));
  }

void littleStar(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/duration[i];
    tone(pin, note[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

很簡單齁! 其實凡事只要抓住了 pattern 或架構, 應用就很簡單, 就是把內容餵進去而已, 玩久了就是所謂 "熟能生巧" 的老狗把戲罷了. 


最後我想把上面小蜜蜂與小星星兩首曲子做成音樂盒, 透過按鈕控制播放不同歌曲, 開機時預設靜音, 按一下播放小蜜蜂, 按一下播放小星星, 再按一下又變靜音, 如此周而復始. 此實驗參考 "Arduino 最佳入門與應用 (第二版)" 的 11-3-5 節範例修改.

測試 12 :

#include "notes.h"

int buzzerPin=9;
int littleStarNote[]={C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0,
                      G5, G5, F5, F5, E5, E5, D5, 0, G5, G5, F5, F5, E5, E5, D5, 0,
                      C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0};
int littleStarDuration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                          4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                          4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};
int littleBeeNote[]={G5, E5, E5, 0, F5, D5, D5, 0, C5, D5, E5, F5, G5, G5, G5, 0,
                     G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, E5, 0, 0, 0,
                     D5, D5, D5, D5, D5, E5, F5, 0, E5, E5, E5, E5, E5, F5, G5, 0,
                     G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, C5, 0, 0, 0};
int littleBeeDuration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

const byte swPin=2;  //switch pin for interrupt
volatile int swValue=0;  //initial swValue:0=silent
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(buzzerPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  switch (swValue) {
    case 1 :
      playLittleBee(buzzerPin, sizeof(littleBeeNote)/sizeof(int));
      break;
    case 2 :
      playLittleStar(buzzerPin, sizeof(littleStarNote)/sizeof(int));
      break;
    default :
      noTone(buzzerPin);  //silent buzzer
      break;  
    }
  }

void playLittleBee(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/littleBeeDuration[i];
    tone(pin, littleBeeNote[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

void playLittleStar(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/littleStarDuration[i];
    tone(pin, littleStarNote[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced:
    ++swValue; //increment swValue
    if (swValue > 2) {swValue=0;} //reset swValue
    }
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced

  }

這個程式我使用了 D2 (中斷 0) 的硬體中斷來更改一個紀錄按鍵狀態的全域變數 swValue 之值, 每按一次就會將原本上拉至 HIGH 的 D2 準位拉到 LOW 而觸發中斷, 在中斷處理函數 int0() 裡面會將 swValue 增量, 但若超過 2 又會重置為 0, 藉此來讓 loop() 裡的 switch-case 判斷要執行哪一個演奏函數, 若為 1 執行 playLittleBee(); 若為 2 執行 playLittleStar(), 否則就靜音. 此程式使用了之前按鈕測試中所使用的去彈跳函數, 參考 :

# Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

注意, 按鈕按下去只是改變 swValue 之值, 並非馬上切換所播放之歌曲, 要等到目前播放的曲子結束, 從函式回到 loop() 迴圈時才會判斷 swValue 之值決定接下來要播放哪一曲或靜音. 

參考 :

# Use tone() with Arduino for an Easy Way to Make Noise
# Arduino Based Digital Clock with Alarm
Ardunio+蜂鳴器(Buzzer)播音樂
# [Arduino] 會唱歌的蜂鳴器 – Controlling Piezo
# Arduino 筆記 – Lab6 控制蜂鳴器發聲
# Brett Hagman 的 Tone 函式庫 (播放多個頻率)


2016年9月15日 星期四

數位日記簿重現江湖

今天中午回到鄉下, 沿路看到好多被莫蘭蒂颱風吹折的樹枝橫在馬路上, 雖然看新聞知道屏東高雄慘兮兮, 但沒出來看真的無法體會原來災情這麼嚴重.

下午姐姐開啟書房那台老 XP 電腦, 竟然順利開機了, 我以為被二哥拆過後, 好像哪裡沒插回去開不了機了呢! 晚上我又開機查看硬碟檔案, 找到我 2004~2009 年之間用 photodiary 網頁軟體寫的電子日記.

記得這是從我買的第一本互動網頁設計書上看到的範例加以改寫而成的 (作者是織夢平台的茶米站長), 原作使用 VBScript 撰寫, 我則將其改為 Javascript 版本. 又因為當時不容易找到穩定的免費 ASP 主機, 所以又用 ActiveX 加寫了一個本機使用版, 只要用連覽器存取隨身碟上的 ACESS 資料庫即可.

我用這個 photodiary 記了整整五年的日記, 直到 2009 年改到無名網站去為止. 翻閱這些日記, 好像又重溫當時的歲月, 有些我也遺忘了, 若不是靠著這本電子日記, 彷彿這些事件從來也沒發生過一樣. 下面這篇是 2007 年 2 月 12 日紀錄菁菁鄉居生活鬧過的笑話, 這些都是我聽母親轉述之後記下來的 :


哇! 好懷念! 都是小狐狸們小時候的生活日記哩! 當時我正好修完我的碩士學位, 在計算語言學課程裡自己學會了 ASP 後端網頁技術, 於是為公司寫了一個工作日誌系統, 從此踏入軟體世界. 改造這個 photodiary 也是其中一段時期的小作業. 大約在 2004 年 1 月底時完工開始使用. 記得那時申請了 DomainDLX 的虛擬主機寄放, 但這些免費主機會莫名奇妙刪帳號或資料庫, 導致部分資料遺失, 所以才會改寫為可攜式的本機版. 下面是管理畫面截圖, 那時花了很多時間調校版面配置哩 :


這個 photodiary 因為使用 ActiveX, 所以必須在 IE 上執行, 而且在 IE6 跳到 IE7 時, 取得檔案位址
的方式有改變, 導致當時的程式在目前 IE 7 以上都無法正確執行, 必須做如下改變 :

var sys_path=unescape(window.location.pathname);
//var pos=sys_path.lastIndexOf("\\");  //for IE 6
var pos=sys_path.lastIndexOf("\/");  //for IE7 above

原來這跟我以前寫的大量語音資料處理程式遇到的問題是一樣的, 參考 :

# 搞定 IE6 程式不能在 IE7/8/9 執行問題

今晚花了點時間修改程式後, 就可以看到這些舊日時光的生活日記了, 超開心.


2016年9月14日 星期三

雙颱來襲

今天因莫蘭蒂颱風來襲, 高雄屏東都放一天颱風假. 但早上起床發現並沒有狂風大雨啊! 難道昨晚已出境? 看新聞才知道颱風還在屏東東南方哩! 據氣象預報, 莫蘭蒂可能是 21 年來最強陸地風速, 預估可達 17 級風! 可不能小看啊!

此外, 雲圖顯示後面還有個馬勒卡, 可能襲擊台灣北部, 哇咧, 現在是前有狼後有虎態勢啊! 看天吃飯的農民可是苦哈哈了.

PS:

11:30 我到全聯買蛋, 才 10 分鐘就出來, 發現已經開始變臉了, 到 12 點後轉為鬼哭神號的狂風暴雨, 原來早上是暴風雨前的寧靜啊!


2016年9月13日 星期二

梨汁蜂蜜治咳嗽

今天看到這篇梨汁治咳嗽的新聞, 這以前在醫書或食補的書上有看過, 不過作法不同, 我以前看到的作法是將梨削皮後放在碗中, 用電鍋蒸熟放溫後加些蜂蜜吃, 但是感覺效果普普. 而此篇的作法是將梨榨汁或打汁, 用小火加熱放溫後加蜂蜜調和後喝.

# 消水腫、防大腸癌!水梨加一味變身天然止咳糖漿

這幾天小狐狸們都被我傳染感冒咳嗽, 除了蒸洋蔥汁來治之外, 也可以來試試這個梨汁蜂蜜湯, 剛好冰箱還有兩顆水梨的說.


2016年9月12日 星期一

Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

做完 Arduino 按鈕開關實驗才發現, 一個這麼簡單的按鈕竟然這麼難搞, 印證了物理與數學之間確實有一道鴻溝. 數理邏輯嚴謹, 一就是一, 二就是二, 但在物理機械特性上就沒這麼黑白分明了. 物理世界有一個慣性, 所以一個作用不會一次到位, 按鈕的彈跳現象就是彈性與慣性作用下的結果, 微控器必須不斷追蹤 I/O 輸入的狀態變化, 將雜訊濾除掉, 取得穩態訊號後才能做出符合期望的控制動作, 這種方式叫做輪詢法 (Polling). 前次的按鈕開關測試全部都是這種方法, 參考 :

#  Arduino 按鈕開關測試 (一) : 輪詢法

輪詢法必須在 loop() 主迴圈中持續追蹤輸入狀態, 會一直占用主程序資源 (執行時間與記憶體空間). 同時, 由於處理器必須主動檢查周邊狀態, 使得 MPU 的負荷與時間延遲都增加, 如果後面還有其他對 timing 要求很高的程序的話就不太好了.

其實微控器要偵測周邊狀態的變化, 還有一個辦法, 就是使用硬體外部中斷, 透過一個自訂函數來處理中斷事件, 處理器是被動接受中斷要求才去服務周邊, 這樣可大幅提高處理器工作效率. 這兩種 I/O 處理方式比較如下 :
  1. Polling (輪詢法) :
    微控器主動檢查輸入腳的狀態變化, 亦即在 loop() 主迴圈中不斷地檢查輸入腳, 若發現有變化就處理, 耗費系統資源較多.
  2. Interrupt (中斷法) :
    微控器不須在主迴圈檢查周邊設備狀態, 而是被動因應周邊中斷的觸發, 將目前的狀態存入堆疊, 暫停現在執行中的程序去處理中斷事件, 控制權移轉到中斷處理函數, 處理完再從堆疊取回被中斷程序繼續執行原先的程序, 因此所耗費之系統資源少. 
我先將 Arduino 的中斷功能大致整理如下 :

一般微控器大都有特定之數位接腳可觸發外部硬體中斷, Arduino 各種板子的硬體中斷因其使用之微控器不同而有差異, 如下表所示 :

 板子 微控器 中斷 0 中斷 1 中斷 2 中斷 3 中斷 4
 UNO/NANO/Pro mini ATmega328P D2 D3
 Leonardo/Micro ATmega32u4 D3 D2 D0 D1 D7
 Mega2560 ATmega2560 D2  D3 D21 D20 D19

採用 ATmega328P 微控器的 UNO, NANO, 或 Pro mini 都只有兩個硬體中斷 D2 與 D3. 除此之外, Arduino 第一個基於 ARM 架構的 Due 採用 Atmel SAM3X8E ARM Cortex-M3 處理器, 其 54 個數位接腳每一個都具有外部硬體中斷功能, 參考 :

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

這些特定數位接腳的外部硬體中斷功能預設是關閉的, 必須使用 attachInterrupt() 函數予以設定並啟用. 其 API 如下, 具有三個參數 :

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);

其中第一個參數是呼叫 digitalPinToInterrupt() 函數並傳入數位腳編號, 也可以不呼叫此函數直接傳入腳位編號 (不建議); 第二個參數是自訂的中斷服務函數名稱 (Interrupt Service Routine); 第三個參數則是中斷觸發模式, 有 LOW, RISING, FALLING, CHANG, 以及 HIGH (只能用於 Arduino Due).

當外部硬體中斷發生後, ATmega328 處理器會將程式暫存器推入堆疊保存, 然後跳到 ISR 所在位址執行 ISR 的第一條指令, 這樣總共需要 82 個時脈週期. 參考 :

http://www.gammon.com.au/interrupts (超詳細)

Arduino 的中斷相關函數如下表 :

 函數 說明
 attachInterrupt(int, ISR, mode) 指派中斷服務函式
 int=中斷編號, 0 或 1
 ISR=中斷服務函式名稱
 mode=中斷模式 (LOW, CHANGE, RISING, FALLING)
 detachInterrupt(int) 移除指定腳位之中斷功能, int=中斷編號, 0 或 1
 noInterrupt() 停止全部中斷功能 (除 reset 外)
 interrupts() 重新啟用全部中斷功能

其中後面三個函數都是需要動態控制中斷功能時才會用到, 例如當要取得目前的 LED 狀態時, 我們不想此變數被中斷改變, 這時就可以先停止所有中斷 (Reset 除外), 取得變數值後再恢復 :

noInterrupts();  //disable all interrupts
boolean state=ledState;  //get volatile variable set by ISR
interrupts();  //enable interrupt

要注意的是暫停不要太久, 否則會影響計時器的運作 (計數器會溢位). 而 detachInterrupt(int) 則是移除指定腳位 (0/1) 的中斷功能.

中斷處理函數 ISR 係自訂, 但與一般的自訂函數不同, 必須符合下列限制 :
  1. ISR 不可以有參數亦無傳回值.
  2. ISR 要儘可能地短, 最好在五行指令以內, 以免中斷時間過長.  
  3. 不要在 ISR 內使用與時間有關的函式如 millis(), micro(), 與 delay(), 因為這些函數也是依賴中斷功能去計數或計時 (delay 是依賴 millis, millis 是依賴計時器), 在 ISR 內呼叫它們毫無作用 (micro 只是剛開始有效). 只有 delayMicroseconds() 因為不使用計數器可在 ISR 內正常運作. 如果在 ISR 內必須使用時間延遲功能, 必須自行撰寫時間延遲函數. 
  4. 在 ISR 執行期間, 序列埠輸出如 Serial.print() 可能會遺失一些資料, 因為新版 Arduino IDE 使用中斷來做序列埠輸出入, 故不要在 ISR 內使用序列埠指令. 
  5. 因 ISR 不能有參數, 故必須透過全域變數與主程式或其他函數分享資料. 這些可能在 ISR 內被改變其值之全域變數務必加上 volatile (會被改變的) 關鍵字
  6. 中斷 0 與中斷 1 具有相同優先權, 但當一個中斷發生時預設其他中斷會被忽略 (因為中斷功能會被禁能), 直到目前的中斷結束為止. 若要讓其他中斷恢復有效, 必須呼叫 interrupts() 函數來致能. 
參考 :

attachInterrupt()

關於第 6 項我持保留意見, 因為根據下面這篇文章裡的 ATmega328 中斷向量優先表, MPU 在執行每道指令後會檢查這張中斷向量表, 首先檢查是否有按下 Reset 鍵, 接著檢查中斷 0, 然後是中斷 1 ... 如果都沒發生中斷, 就執行下一道指令 (可見我們的外部硬體中斷只是 26 種中斷裡的兩個而已). 所以看起來中斷 0 是比中斷 1 優先的 :

http://www.gammon.com.au/interrupts (超棒的)

特別注意, ISR 內的全域變數必須宣告為 volatile 是因為編譯器的程式碼優化功能, 在 ISR 中可能會破壞程式碼原本規劃的邏輯, 使得執行功能異常. 例如在趙英傑的 "Arduino 互動設計入門 2" 附錄 D 就舉了一個範例 :

int a, b;
int c=a+b;
int d=a+b;

如果 C 編譯器的優化選項有勾選的話, 那麼上面的程式碼在編譯器執行優化後會變成 :

int a, b;
int c=a+b;
int d=c;

因為編譯器覺得既然 c 與 d 都是 a 與 b 之和, 就不需要浪費時間再做一次 a+b 運算. 這在一般程式不會有問題, 但若 a 或 b 的值會在中斷處理函數中被改變的話, 則 d 的值就會跟 c 不一樣了, 因為萬一執行完 c=a+b 之後發生中斷, 這時系統暫存器狀態會被推入堆疊中保存, 然後程式計數器會被導向中斷處理函數 ISR, 如果 a 或 b 在中斷處理函數內被改變其值, 則當中斷結束, 控制權交回主程式繼續執行 d=c 指令時, 這時 d 的值將無法反映 a 或 b 已經改變的現況, 仍與 c 一樣是舊的數據. 如果 a, b 加上 volatile 宣告的話, 就可以避免這個問題, 這個 volatile 就是通知編譯器, 這兩個全域變數不要進行優化 :

volatile int a, b;

此外, 被編譯器優化過的變數會放一個副本在暫存器中直接運算, 而非從 RAM 裡面重新載入, 但這樣可能被中斷處理函數覆蓋掉原來的值, 導致得到錯誤的運算結果. 而宣告為 volatile 是告訴編譯器, 此變數隨時會被中斷處理函數改變, 執行時必須重新從 RAM 中載入暫存器, 不可以只依賴存放在暫存器裡的副本 (被優化的變數就是直接取用暫存器裡的副本, 這樣就節省了重新自 RAM 載入的時間).

下面這篇文章對 C 語言的 volatile 有深入說明 :

# C語言: 認識關鍵字volatile

此文中有一段重點 : 一般書籍對 volatile 的用法解釋並不多, 所以造成我們對此修飾詞了解不夠或甚至是誤解. 例如volatile 用在指標時, 下面兩個用法意思不同 :
  1. int * volatile ptr :
    關鍵字 volatile 修飾 ptr, 意思是指標 ptr 本身是 volatile, 但指標所指之內容不是 volatile, 所以編譯器不會對指標本身進行優化, 但對指標內容 *ptr 之運算卻會進行優化.
  2. volatile int *ptr :
    此處 volatile 修飾的是指標內容 *ptr, 因此 volatile 的是指標的內容, 它不會被優化; 但指標本身卻不是 volatile 的, 指標本身的運算是會被優化的.
還有其他 volatile 使用上的陷阱必須注意, 詳參原文. 如果覺得實際執行結果與預期的不同, 有可能是我們對 volatile 的用法了解不夠所致.

OK, 對中斷有了基本了解之後, 就可以進行測試了. 以下實驗我主要是參考了下面四本書的範例進行修改 :
  1. Arduino 輕鬆入門, 葉難, 博碩出版 (第 3-4 節)
  2. Arduino 完全實戰手冊, 王冠勛譯, 博碩出版 (第 2-2-4 節)
  3. Prototyping Lab, 小林茂, 馥林文化 (第 25 章)
  4. Arduino Cookbook 錦囊妙計, 歐萊禮 (第 18-2 節)
  5. 微電腦原理與應用, 黃新賢等, 全華 (第 8 章)
延續上一篇的控制按鈕開關讓 LED 交替明滅的實驗, 將之前輪詢法的測試 1 改為採用中斷法的測試 7 :


測試 7 : 

const byte intPin=2; //interrupt pin
const byte ledPin=13; //built-in LED
volatile boolean state=LOW; //initial value of switch pin
void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(intPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0int0, LOW); //assign int0
  }

void loop() {
  if (state) {digitalWrite(ledPin, HIGH);} //turn LED on
  else {digitalWrite(ledPin, LOW);}  //turn LED on
  }

void int0() { //interrupt handler
  state=!state; //reverse state
  }

上面程式在 setup() 中用 attachInterrup() 啟動 D2 腳的 LOW 中斷功能, 即當 D2 位準變 LOW 時觸發中斷 0, 執行中斷處理函數 int0(), 將全域變數 state 狀態反轉. 中斷處理函數執行完畢後控制權跳回 loop() 主迴圈時, 就會根據 state 變數的目前狀態來使 LED 明滅. 由於中斷處理函數會改變全域變數 state 之值, 因此必須宣告為 volatile, 以免被編譯器優化而使動作不正常.

程式上傳後執行發現, 跟前一篇的測試 1 一樣,  理論上每按一下按鈕,  D13 LED 應該由亮變滅, 或由滅變亮才對, 但實際上並非一亮一滅規律切換, 而是有時候正常, 有時候又不正常, 是典型的彈跳現象, 可見外部硬體中斷不會幫我們解決彈跳問題. 我參考前一篇測試 3-1 的程式, 為上面的程式加上去彈跳功能, 如下面測試 8-1 :

測試 8-1 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced
  }

主要就是在中斷處理程式裡面先去呼叫 debounced() 函數, 看看是否已經撐過了預定的彈跳期間, 如果是的話就傳回 true, 中斷處理程式反轉 LED 狀態後, 主控權回到 loop() 主迴圈時, LED 就會變換狀態了. 使用中斷就不需要再去判斷 D2 是否為 LOW 了, 因為在 attachInterrupt() 時已指定 LOW 觸發中斷. 注意, 雖然 debounced() 函數中有用到 millis(), 但因為它並不是直接放在中斷處理函數 int0() 裡面, 所以它還是有作用的.

如果使用前一篇測試 4 的 debounced(pin) 函數大致可以, 只是偶而會沒反應 (why?), 如下面測試 8-2 所示 :

測試 8-2 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=100; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced(swPin)) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

boolean debounced(int pin) {
  boolean currentState; //current pin state
  boolean previousState; //previous pin state
  previousState=digitalRead(pin); //record current state as previous
  for (int i=0; i<debounceDelay; i++) { //detect if stable
    delay(1); //delay 1 ms
    currentState=digitalRead(pin); //get current state
    if (currentState != previousState) { //still unstable
      i=0; //reset counter
      previousState=currentState; //updtae previous state
      }
    }
  if (currentState==LOW) {return true;} //switch pressed (pull-up)
  else {return false;} //switch released
  }

這個函數與測試 8-1 所用的不同之處在於要傳入按鈕接腳編號, 注意其彈跳時間為 100 毫秒. 實際執行結果發現偶而會動作不正常.

但是如果使用前一篇測試 6-1 的 debounced(pin) 函數卻完全沒反應, 如下測試 8-2 所示 :

測試 8-2 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=50; //debounce delay (ms)

int buttonState; //previous stable state of the input pin
int lastButtonState=LOW; //the previous reading from the input pin
long lastDebounceTime=0; //the last time the output pin was toggled

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced(swPin)) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

boolean debounced(int pin) { //check if debounced
  boolean debounced=false;  //default
  int reading=digitalRead(pin);  //current button state
  if (reading != lastButtonState) { //button state changed
    lastDebounceTime=millis(); //update last debounce time
    }
  if ((millis() - lastDebounceTime) > debounceDelay) { //overtime
    if (reading != buttonState) { //button state has changed
      buttonState=reading; //update previous stable button state
      if (buttonState == LOW) { //button pressed
        debounced=true;
        }
      }
    }
  lastButtonState=reading; //update last button state
  return debounced;
  }

奇怪, 怎麼看都沒問題呀! Why? 

總之, 從上面的測試結果來看, 不論是前一篇的輪詢法還是本篇的中斷法, 都能夠的正確地執行出預期邏輯的 ifDebounced() 函數只有測試 3 或 3-1 以及測試 8-1 的這個 (改寫自 "Arduino 完全實戰手冊" 這本書). 似乎簡單才是硬道理啊!

接下來我想測試使用兩個中斷的情況, 我參考了黃新賢等著的 "微電腦原理與應用" 第 8 章的範例改寫, 讓中斷 0 (D2) 與中斷 1 (D3) 同時啟用, 一個控制 D13 LED 快閃 (D2); 另一個控制 D13 LED 慢閃 (D3), 所以我需要兩個按鈕開關分別連接到 D2 (INT 0) 與 D3 (INT 1), 另一端接地, 然後開啟 D2 與 D3 的內建上拉電阻, 以便按鈕未按下時這兩個中斷輸入腳會被上拉到 HIGH 以消除隨機雜訊之影響. 程式如下 :

測試 9 :

const byte swPin2=2;  //switch pin for int0 (D2)
const byte swPin3=3;  //switch pin for int1 (D3)
const byte ledPin=13;  //built-in LED
volatile int blinkRate;  //blink rate of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin2, INPUT_PULLUP); //enable pull-up resistor of input pin D2
  pinMode(swPin3, INPUT_PULLUP); //enable pull-up resistor of input pin D3
  digitalWrite(ledPin, LOW); //set LED OFF
  attachInterrupt(0, int0, LOW); //enable int0 (D2)
  attachInterrupt(1, int1, LOW); //enable int1 (D3)
  }

void loop() {
  blinkD13Led(blinkRate);
  }

void int0() { //interrupt handler
  if (debounced()) {blinkRate=200;} //fast flashing
  }

void int1() { //interrupt handler
  if (debounced()) {blinkRate=1000;} //slow flashing
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced
  }

void blinkD13Led(int t) {
  digitalWrite(13, HIGH);
  delay(t);            
  digitalWrite(13, LOW);  
  delay(t);            
  }

此程式中我自訂了一個 blinkD13Led() 函數來讓 LED 閃爍一下, 傳入參數 t 可以控制閃爍的頻率. INT 0 與 INT 1 分別設定 LOW 準位觸發中斷, 在各自的中斷處理函數中, 於消除彈跳現象後會更新全域變數 blinkRate 的值, 以便讓 loop() 主迴圈內的 digitWrite() 調整 LED 的閃爍頻率.


左邊按鈕是中斷 1, 接到 D3 (黃線); 右邊是中斷 0, 接至 D2 (白線), 所以按左鍵閃爍變慢, 按右鍵會變快. 兩個若同時按下的話, 右邊的中斷 0 會獲勝 (快閃), 因為照中斷向量順序表來說, 中斷 0 會先被檢出.

這個範例可能引發一個問題, 就是如果一個中斷發生時, 在其 ISR 執行期間會不會被另一個中斷給中斷 (巢狀中斷, nested interrupts)? 答案是不會, 因為當中斷發生時, ATmega328 處理器就會在硬體上將所有中斷禁止 (Reset 除外), 以避免形成無窮的遞迴中斷 (recursive interrupts), 當然實際上不可能無窮啦! 堆疊很快就會爆掉而當機了. 所以進入 ISR 後預設是不會被中斷, 直到 ISR 執行完畢, 處理器再將中斷致能. 當然有特殊原因的話, 也是可以在 ISR 內呼叫 interrupts() 自行將中斷致能, 但要妥善處理好堆疊以免導致非預期結果.

最後我在 Arduino 官網發現這個 PinChangeInt Library, 這是可將 Arduino UNO, NANO, Duemilanove 這三種板子的數位接腳變成具有外部硬體中斷功能的函式庫, 可在下列網址下載 (不要從說明文件底下所連的 Google Code Archive 下載, 那個少了 PinChangeIntConfig.h 這個檔) :

https://github.com/Ltalionis/PinChangeInt

解壓縮後將目錄 PinChangeInt-master 放到 Arduino IDE 安裝目錄的 libraries 下, 但因為此函式庫已經比較舊了, 它使用的 WProgram.h 在新版 IDE 已經改名為 Arduino.h, 所以須編輯 PinChangeInt.cpp 檔, 將其中的 include "WProgram.h" 改成 include "Arduino.h" :

#ifndef WProgram_h
#include "Arduino.h"
#endif

其使用方法參考, 必須匯入 PinChangeInt.h 與 PinChangeIntConfig.h 這兩個函式庫, 並使用 PCintPort::attachInterrupt() 來起始指定腳位的硬體中斷功能 :

PinChangeInt Example

我將上面測試 8-1 的程式改成下列測試 10 :

測試 10 :

#include <PinChangeInt.h>
#include <PinChangeIntConfig.h>

const byte swPin=5;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  PCintPort::attachInterrupt(swPin, int0, RISING); //Enable PCI on swPin
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced
  }

這裡用 D5 腳的上升緣 (RISING) 既然是 Change, 當然也可以用下降緣 (FALLING) 觸發, 但我實驗結果發現用 RISING 很穩, 用 FALLING 有時不靈光, 不知原因為何 (彈跳期間太短嗎?).

還有一個比較新的 PCI Manager 函式庫也可以將其他 D2/D3 以外的數位接腳變成具有外部硬體中斷功能, 下載網址與使用範例如下 :

PciManager

將解壓縮後的目錄 arduino-pcimanager-master 複製到 Arduino IDE 安裝目錄的 libraries 下, 參考範例將上面測試 10 改為測試 11 如下 :

測試 11 :

#include <PciManager.h>
#include <PciListenerImp.h>

const byte swPin=5;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void onPinChange(byte changeKind);  //declare self-defined function
PciListenerImp listener(swPin, onPinChange);  //create lister object

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  PciManager.registerListener(swPin, &listener);  //register change event on swPin
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced
  }

void onPinChange(byte changeKind) { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

這裡需匯入 PciManager.h 與 PciListenerImp.h 這兩個函數, 然後在建立 PciListenerImp 物件之前必須先宣告自訂的事件處理函數, 否則會出現如下錯誤 :

pcimanager:7: error: 'onPinChange' was not declared in this scope

 PciListenerImp listener(INPUT_PIN, onPinChange);

當然也可以把 onPinChange() 函數放到建立 listener 物件之前, 但這樣結構上有點怪. 測試結果也是 OK 的, 只是這個沒辦法選擇是要上升或下降時觸發中斷而已, 因為我們這裡使用上拉電阻, 所以是下降時觸發; 如果用外部下拉電阻的話, 就會變成上升觸發了.

好了, 關於 Arduino 外部硬體中斷功能的測試大概就是這樣了, 其他比較深奧的需要對 AVR 處理器內部深入了解後才看得懂.

參考 :

# Arduino – 中斷功能 (寫得好)
# 從 Arduino 到 AVR 晶片(2) -- Interrupts 中斷處理 (深入韌體)
關於中断(Interrupt)的一些五四三... 中斷 . . (精闢)
Do interrupts interrupt other interrupts on Arduino?
# Global manipulation of the interrupt flag
# EXTERNAL INTERRUPTS ON THE ATmega168/328
# Using millis() and micros() inside an interrupt routine


2016-10-27 補充 :

今天在 "Arduino 從零開始學" 這本書上的 4.2.7 硬體中斷這節看到定時中斷, 作者介紹了 FlexiTimer2.h 與 MsTimer2.h 這兩個定時函式庫, 可以很方便地使用定時中斷. 其範例如下 :

#include <MsTimer2.h>
int pin=13;
volatile int state = LOW;  
void setup() {
  pinMode(pin, OUTPUT);
  MsTimer2::set(500, blink);            
  MsTimer2::start();      
  }

void loop() {
  digitalWrite(pin, state);
  }

void blink() {
  state = !state;                            
  }

此程式利用定時中斷每 0.5 秒會改變 state 變數之值, 從而使 D13  LED 閃爍. MsTimer2.h 可在此書範例原始碼 zip 檔解開後的 Libraries 目錄中找到, 上面範例在 4-5 資料夾下, 請至碁峰網站下載 :

http://books.gotop.com.tw/download/ACH018200