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(0, int0, 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


吸盤回春術

之前收到朋友傳一個 Line 影片, 聽口音像是對岸強國人拍的, 介紹如何讓已經鬆脫不黏的吸盤起死回生, 其秘訣就是用蛋白塗在吸盤上而已, 這樣竟然就能重新展現強大吸附力, 甚至比剛買來的新貨吸力還強! 我之前買過好幾個吸盤式置物架, 像肥皂盒, 牙刷架等等, 大都用不到半年就紛紛墜地而亡. 如果是玻璃或鋁門窗表面較密緻的材質, 或許可以撐較久. 最後下定決心, 以後絕對不再買這種沒鳥用的貨色.

後來在特力屋看到一種強力黏膠式置物架, 它是用一大片黏膠加強支撐力, 所以我用到現在超過兩年了, 不管是洗髮精或沐浴乳之類的放在架上都能穩穩撐住, 唯一的缺點是 : 太貴了. 看到這個吸盤回春術後, 我就躍躍欲試, 還好之前那些沒鳥用的肥皂盒與置物盤都沒丟掉, 趁著有煮洋蔥煎蛋這道菜的時候, 把蛋殼內壁殘留的蛋白抹在吸盤上重新吸附在牆壁, 先不要放東西, 等過了一天蛋白完全乾掉後就能完全塞滿牆面縫隙, 牢牢地抓住牆面了. 目前我已讓三個吸盤肥皂盒成功地回到工作軌道, 而且非常牢靠, 真的.

網路上也可以找到其他撇步, 例如用肥皂水或防水式防曬乳, 特別是防曬乳效果似乎也不錯, 以後有機會也可以試試看 :


既然新買的吸盤用不久就會掉, 不如剛買來就用蛋白或防曬乳來使其密合, 不要等到黏不住時再處理.

參考 :

# 【生活智慧王】20020704吸盤力大復活
# 家裡的吸盤掛鉤老是黏不住?


2016年9月11日 星期日

2016 年第 35 周記事

本周上了六天班, 就為了補下周中秋節後的周五彈性放假. 而且連續寫了三份報告, 彙整兩份報告, 可說本周是報告周, 真累.

由於菁菁本周六要跟同學討論功課, 所以就只有我一個人回鄉下了, 好寂寞. 特別是今天在市場看到一個大約三四歲的女娃, 跟著她阿嬤來買菜, 我突然想到, 以前小狐狸們小時候在鄉下跟阿嬤住時也是這樣吧! 但我腦海裡沒有這樣的景象, 因為我沒跟母親上過市場. 算一算這不過是十年前的事而已. 時間真是令人既懷念又悔恨的東西.

今日完成了預定要做之事, 整理大通鋪; 趁著驕陽還在趕緊曬棉被; 種下兩棵同事給的絲瓜; 以及去巡家祠墓園, 砍除前陣子雨後孳生的雜草. 其實草不算多, 花十幾分鐘就解決了. 外公外婆的墓園就在咫尺之遙, 想說順便整理, 哪知上去一看發現都被含羞草覆蓋了, 我拿鐮刀要劈開入口, 竟然被刺了好幾次, 我看這沒有全副武裝是不行的, 只好下次約小舅有空時一起來砍除.

中秋節將至, 颱風莫蘭蒂卻要來攪局, 同事說如果下周五放颱風假, 那我們周六補班不就白補了? 是滴, 今天新聞說就是這樣沒錯. 但我覺得應該不會到周五才來, 或許週三就來了. 但這樣就會影響回鄉下的行程咧.


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

過去兩周把 Blynk 搞定之後, 突然覺得有點茫然, 因為一時不知道接下來要做甚麼. 其實 Arduino 還有很多課題還沒玩透, 但可能就是太多了才不知道該先玩哪一個. 今天隨便翻書看到按鈕開關測試, 嘿! 擇期不如撞期, 不如回來測試 Arduino 基本功能好了. 雖說這是 Arduino 比較基礎的實驗, 但實際測試後才發現學問還蠻多的, 特別是處理彈跳的部分, 不是我原先想的那麼簡單, 真的.

此實驗我主要是參考了下面四本書的範例進行修改 :
  1. Arduino 輕鬆入門, 葉難, 博碩出版 (第 3-3, 3-4 節)
  2. Arduino 完全實戰手冊, 王冠勛譯, 博碩出版 (第 2-2-4 節)
  3. Prototyping Lab, 小林茂, 馥林文化 (第 25 章)
  4. Arduino Cookbook 錦囊妙計, 歐萊禮 (第 5-2, 5-3 節)
按鈕開關 (Push button) 是一個常開按鈕, 平常為開路不連通, 按下去時才形成通路. 常見的按鈕開關是有四隻針腳的四方形元件, 中間有一圓鈕, 可以跨過麵包板溝槽插在上面, 如下圖所示 :


同一側兩隻腳在按鈕尚未按下時是開路的, 按下時為通路; 而對面的兩隻腳則永遠是連通的, 我們通常是使用同一側的兩個接腳, 電路結構如下圖 :


第一個實驗是要測試按鈕按下時, Arduino 內建的 D13 LED 會交替明滅, 即原來是暗的變亮, 原來是亮的變暗, 接線方式為將 Arduino D2 設定為使用內建上拉電阻之輸入腳, 並連線至按鈕開關一腳, 而按鈕開關另一腳則接地, 使按鈕按下時 Arduino 之 D2 接地, 放開時則被內建上拉電阻拉到 HIGH. 程式如下 :

測試 1 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
boolean ledState=LOW;  //initial state of LED

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
  digitalWrite(ledPin, ledState); //set LED OFF
  }

void loop() {
  boolean swState=digitalRead(swPin);
  if (swState==LOW) { //switch pressed
    if (ledState==HIGH) {ledState=LOW;} //ON:turn LED OFF
    else {ledState=HIGH;}  //OFF:turn LED ON
    digitalWrite(ledPin, ledState); //toggle LED
    }
  }

因為要記住 D13 LED 目前狀態, 因此必須用一個全域變數 ledState 來記錄, 預設狀態為 LOW, 所以程式開始執行後, 跑完 setup() 時 LED 是暗的. 這裡我們使用 INPUT_PULLUP 將 D2 腳設為內建上拉電阻的輸入腳, 這樣當 D2 浮接 (floating) 時, 其位準會被上拉至 HIGH.

注意, 開啟上拉電阻是必要的, 因為當輸入腳是浮接時, digitalRead() 讀到的是隨機的雜訊, 如果 D2 只設為 INPUT 而且浮接 (不接 GND 也不接 +5V), 則送電後 D13 LED 會不斷閃爍, 因為在 loop() 主迴圈中, 每一次迴圈讀到的 D2 值有時是 HIGH, 有時是 LOW, 於是看起來就是隨機地在閃爍, 只要將上面測試 1 中的 INPUT_PULLUP 改為 INPUT 即知, 如下面影片所示 :


若 D2 為 INPUT 模式, 沒有上拉電阻的話, 上電後 LED 就隨機閃爍, 直到按下按鈕被拉到 LOW 才停止, 但一放開變浮接又開始閃了. 關於上拉電阻更詳細說明參考 :

# Arduino Internal Pull-Up Resistor Tutorial (超棒)

上拉電阻有一個舊式用法, 先用 pinMode() 設定 DIO 為 INPUT 腳, 然後再用 digitalWrite() 輸出 HIGH 即可啟用 Arduino 數位接腳的內建上拉電阻 :

pinMode(swPin, INPUT);
digitalWrite(swPin, HIGH);

不過這是還未支援 INPUT_PULLUP 的舊版 (1.0.1 以前) Arduino IDE 之用法, 現在應該使用 INPUT_PULLUP 為宜. Arduino 的 DIO 若設為 INPUT, 其輸入阻抗約 100M 歐姆, 而內建的上拉電阻, 根據 "Arduino Cookbook 錦囊妙計" 5-2 節末尾描述, 其值約在 20K~50K 歐姆之間. 關於上拉電阻說明, 參考 :

# pinMode()
# Input Pullup Serial
# Arduino內部的上拉電阻
# 使用Arduino 的pull-up 電路
# 從按鈕開關看上拉pull-up電阻下拉電阻是蝦密碗糕
# https://www.arduino.cc/en/Tutorial/InputPullupSerial

上面測試 1 的程式上傳後, 測試發現實際上 LED 的明滅並非如我們所期望的那樣, 按一下亮, 再按一下滅, 而可能是本來亮著, 按一下還是亮, 再按一下滅, 再按一下閃一下又滅 ... 或者按著的時候亮, 放開即滅, 與程式的邏輯不同. 這種不正常現象來自按鈕開關的機械彈跳 (Bounce) 特性, 當按下按鈕時, D2 並不是一次從上拉的 HIGH 直接跳到 LOW, 而是在 HIGH 與 LOW 之間來回切換好幾次才成為穩定的 LOW 狀態, 這種彈跳的暫態現象通常可能持續 10 毫秒到 50 毫秒左右.

因為 loop() 執行速度很快, 所以在按鈕按下的瞬間, digitalRead() 已經讀取到數十次到數百次 HIGH 與 LOW 切換的值了, 因此如果在到達穩定狀態前讀取到偶數次的 HIGH-LOW 變換, 那麼 LED 就明滅偶數次, 看起來 LED 就好像沒變換狀態; 如果是奇數次, 就跟程式邏輯相符, LED 會變換狀態. 如果在上面測試 1 程式中加入計數器, 就可以知道在按下按鈕時迴圈已跑多少圈了, 參考 "Arduino 輕鬆入門" 第 3-3 節 :


測試 2 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
boolean ledState=LOW;  //initial state of LED
int count=0; //bounce counter

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
  digitalWrite(ledPin, ledState); //set LED OFF
  Serial.begin(9600);
  }

void loop() {
  boolean swState=digitalRead(swPin);
  if (swState==LOW) { //switch pressed
    if (ledState==HIGH) {ledState=LOW;} //ON:turn LED OFF
    else {ledState=HIGH;}  //OFF:turn LED ON
    digitalWrite(ledPin, ledState); //toggle LED
    Serial.print("Low count:");
    Serial.println(count); //output counter value
    count++; //increment bounce counter
    }
  }

此程式加入一個全域變數 count 來計算在 D2 為 LOW 期間 loop() 迴圈已跑幾次 (會累計), 通常按即放開至少都十幾次以上, 當然按著越久就越多次. 序列埠監控視窗擷取訊息如下 :

Low count:0
Low count:1
Low count:2
Low count:3
Low count:4
Low count:5
Low count:6
Low count:7
Low count:8
Low count:9
Low count:10
Low count:11
Low count:12
Low count:13
Low count:14

解決這個問題的辦法最直接的是時間延遲法, 意即當偵測到按鈕被按下, D2 位準變 LOW 後, 延遲一段時間若狀態還是 LOW, 表示彈跳情況已消失, 按鈕已穩定在被按下狀態, 這時再去反轉 LED 明滅狀態, 我參考 "Arduino 輕鬆入門" 第 3-4 節範例, 將其修改為下面測試 3 :

測試 3 :

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

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
  digitalWrite(ledPin, ledState); //set LED OFF initially
  }

void loop() {
  boolean swState=digitalRead(swPin);
  if (swState==LOW) { //switch pressed
    if (debounced()) { //debounced: reverse LED state
      ledState = !ledState; //reverse LED state
      digitalWrite(ledPin, ledState); //toggle LED    
      }
    }
  }

boolean debounced() { //check if debounced
  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() 函數來檢查現在的毫秒數 (程式開始執行以來) 跟上一次紀錄之毫秒數 (即上一次 D2 為 LOW 之時間) 是否已超過預期的彈跳時間 bounceDelay, 如果是, 表示按鈕已穩定地被按下, 就可以將 LED 狀態反轉了; 否則表示彈跳還沒結束, 不進行反轉.

在上面程式中我定義了一個全域變數 lastMillis 來記錄上一次 D2 為 LOW的時間, 其實也可以將此變數設為 static 變數放在 debounced() 內, 效果是一樣的, 因為跳出函數後, 宣告為 static 的區域變數不會消失, 會被保留下來, 下次呼叫時可再次存取. 參考 "Arduino 完全實戰手冊" 第 2-2-4 節 :

測試 3-1 :

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

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
  digitalWrite(ledPin, ledState); //set LED OFF
  }

void loop() {
  boolean swState=digitalRead(swPin);
  if (swState==LOW) { //switch pressed
    if (debounced()) { //debounced: reverse LED state
      ledState = !ledState; //reverse LED state
      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
  }

同樣是使用時間延遲, 我在 "Arduino Cookbook 錦囊妙計" 第 5-2 節找到不一樣的做法 (主要是 debounced 函數), 程式改編如下 :

測試 4 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
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
  digitalWrite(ledPin, ledState); //set LED OFF
  }

void loop() {
  if (debounced(swPin)) { //switch pressed and statble for 50 ms
    ledState =! ledState; //reverse LED state
    digitalWrite(ledPin, ledState); //toggle LED
    }
  }

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
  }

這裡 debounced() 要傳入欲偵測的腳位編號, 函數內宣告了兩個布林變數來儲存目前與上一次的開關狀態, 然後以 debounceDelay 當迴圈終止值每 1 毫秒去偵測現在開關狀態是否與上一次變化時所儲存的狀態相同, 如果不同, 表示還在彈跳, 就會重新計數, 並持續記錄狀態. 如果迴圈結束時, 開關狀態是 LOW, 就回傳 true 表示按鈕已穩定被按下, 否則傳回 false.

也可以使用 Bounce2 函式庫來處理彈跳問題, 在 "Arduino 輕鬆入門" 3-4 有介紹, 但那是比較舊的 Bounce 函式庫, 現在已更新為 Bounce2 了. 此函式庫可從下列網址下載 :

# http://playground.arduino.cc/Code/Bounce

將下載之 zip 檔解壓縮到 Arduino IDE 安裝目錄的 libraries 下, 重開 IDE 即可看到 "檔案/範例" 下有 Bounce2 的範例. 根據此範例改寫為如下測試 5 :

測試 5 : 

#include <Bounce2.h>

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

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
  digitalWrite(ledPin, ledState); //set LED OFF
  debouncer.attach(swPin); //monitor button switch
  debouncer.interval(debounceDelay); //set debounce interval
  }

void loop() {
  debouncer.update();
  int swState=debouncer.read();
  if (swState==LOW) {
    ledState = !ledState; //reverse LED state
    digitalWrite(ledPin, ledState); //toggle LED
    }
  }

與舊版 Bounce 不同的地方是, 在產生全域的 Bounce 物件時不要再傳入參數, 設定要偵測的腳位與彈跳時間要在 setup() 中分別用 Bounce 物件的 attach() 與 interval() 函數來設定. 此函式庫其實就像是一個濾波器, 會把彈跳期間的雜訊濾掉, 輸出穩定的 HIGH-LOW 或 LOW-HIGH 狀態變化. 使用此程式必須在 loop() 中不斷呼叫 Bounce 物件的 update() 來更新濾波結果, 然後再呼叫 read() 將偵測到的最新狀態讀取進來. 我照 "Arduino 輕鬆入門" 3-4 節範例去試結果不會動作, 那是舊版寫法, 新版的 Bounce2 要用新的寫法才行.

其實我有找到另外一個 Bounce 函式庫如下, 不過這更舊, 因為它的 Bounce.cpp 裡還在用 WProgram.h, 須改為 Arduino.h, 這麼麻煩我就不用了 :

# https://www.pjrc.com/teensy/td_libs_Bounce.html
# WProgram.h 这个文件是做什么用的?哪能下载到?

最後, 我發現 Arduino IDE 的 "檔案/範例" 裡就有 Debounce 的程式, 它的作法稍有不同, 我把它稍作整理便於閱讀如下 :

測試 6 : 

const int buttonPin=2; //the number of the pushbutton pin
const int ledPin=13; //the number of the LED pin
int ledState=HIGH; //the current state of the output pin

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
long debounceDelay=50; //the debounce time; increase if the output flickers

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP); //enable pullup resistor
  digitalWrite(ledPin, ledState);
  }

void loop() {
  int reading=digitalRead(buttonPin);
  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
        ledState = !ledState; //reverse LED state
        }
      }
    }
  digitalWrite(ledPin, ledState);
  lastButtonState=reading; //update last button state
  }

原理大致與上面類似, 不同之處是這裡它使用了兩個全域變數來記錄按鈕狀態 : buttonState 紀錄前一次的穩定狀態位準, 而 lastButtonState 則記錄動態的最近狀態. 如果目前狀態與最近狀態不同, 表示還在彈跳當中, 就更新最近去彈跳時間 lastDebounceTime 為目前已執行時間 millis(), 如果狀態相同, 表示可能已趨於穩定, 最近去彈跳時間 lastDebounceTime 就不會被更新, 以便迴圈持續偵測這個穩定是否能撐過 debounceDelay, 若撐過了還須比較現在狀態與上次穩定狀態是否不同, 是的話才進一步去看目前狀態是否為 LOW (按下了). 此程式乍看之下覺得其演算法有點難懂, 但實際測試發現, 在設定 50 毫秒延遲時, 其執行結果完全正確, 且一直壓住按鈕, LED 也不會像上面測試 3, 4 那樣閃爍.

我把上面測試 6 改成用 debounced() 函數來判斷是否已去除彈跳 :

測試 6-1 :

const int buttonPin=2; //the number of the pushbutton pin
const int ledPin=13; //the number of the LED pin
int ledState=HIGH; //the current state of the output pin

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
long debounceDelay=50; //the debounce time; increase if the output flickers

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP); //enable pullup resistor
  digitalWrite(ledPin, ledState);
  }

void loop() {
  if (debounced(buttonPin)) { //debounced
    ledState = !ledState; //reverse LED state
    }
  digitalWrite(ledPin, ledState);
  }

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;
  }

功能與測試 6 相同, 但這樣感覺比較有結構.

總之, 彈跳現象經過上面如此處理後, 執行起來大都正常了, 但也不是百分之百符合程式所期望的邏輯, 主要的關鍵在於 debouceDelay 要設幾毫秒為宜, 設得太短或太長都沒效果. 經我多次測試結果, 測試 3 與 3-1 要設 200 ms 延遲; 測試 4 要設 100 ms 延遲時, 而測試 6 與 6-1 則要設 50 ms 延遲, 這樣正確率最高, 幾乎無失誤; 而使用 Bounce2 函式庫最糟糕, 延遲從 20, 40, 60, ... 到 200 都試過, 沒有一個設定值是滿意的, 好像跟測試 1 差不多.

參考 :

# Arduino練習:以開關切換LED明滅狀態


2016年9月8日 星期四

大同複合無水料理鍋

昨晚去家樂福幫二哥買備用球鞋 (預防天雨弄濕), 看到這款大同複合無水料理鍋, 感覺是專門設計給不擅料理的人用的, 只要準備好食材放入鍋內, 單鍵設定就會自動烹煮了. 它的燒烤鍋與湯鍋都是不沾鍋設計, 而且鍋蓋與鍋身密合度高, 水蒸氣幾乎不會跑出來, 可以鎖住食材本身的鮮味, 獲得 2016 年德國 iF 設計獎 :

# https://member.tatung.com/Content/event/easycooker/recognize.asp

下面是我找到一個主婦的實際使用紀錄, 裡面的料理作法值得參考 :

# 主婦救星.多功能料理神器 ☆ 大同複合料理無水鍋 ☆ 輕鬆簡單吃出健康

此鍋在家樂福賣 4880, 我在露天找到最便宜是這兩家 :

# 露天 : 大同 複合料理無水鍋 TSB-3016EA $4199
# 露天 : TATUNG 大同 複合料理無水鍋TSB-3016EA全新未拆$4200含運

看來操作挺簡單, 如果買給爸的話應該是蠻適合的, 考慮中.

2017-04-10 補充 :

今天看到公司福利社有在賣此鍋, 價格 4850, 又心動惹!

# 大同複合料理無水鍋 體驗美味料理

找到這個賣 3800, 但無超商取貨付款, 又無聯絡資料, 不太敢下標 :

#【TATUNG大同】複合料理無水鍋 (TSB-3016EA) (宅配免運) $3800

2017-04-14 補充 :

# 消費情報讚-大同電鍋+無水鍋【超簡單料理 外食族的健康好幫手】


無水鍋料理食譜參考 :

# 美食部落客玩家常料理 (有多篇食譜介紹)
# QQ廚房-Q媽的創意料理日記

2016年9月5日 星期一

Verilog 與 VHDL

最近菁菁週日不想跟我上市集, 所以買完菜如果時間還夠, 我就會順路去市圖逛逛, 看看有甚麼好書沒有. 本周找到這本 "Verilog硬體描述語言實務 (全華 2011, 鄭光欽)" : 


Source : eslite


此為第一版, 這本書已經出第二版 (2013)了.

隨意翻一翻, 感覺我的硬體熱又被激發了. 我以前就是做 ASIC 設計的啊! 但那時代的技術跟現在比起來可差多了, 而且模擬軟體與工作站都很貴, 離開了公司就如同失去了翅膀的飛鷹, 你只能帶走經驗與知識而已, 沒有了平台這些都沒有用了, 個人是沒有財力購買這些昂貴開發設備的, 只能投效 Design House.

但 1984 年 Xilinx 公司發明的 FPGA (現場可程式邏輯門陣列) 與 1983 年出現的 Verilog 與 VHDL 改變了這種態勢, 不僅將硬體設計軟體化, 也使得個人 IC 設計工作室變成可能, 大型客製化 IC 設計變成人人可在書房現學現做的玩意, 再也不是高不可攀的高科技. NI 公司的 LabView 甚至將 VHDL 整合到其視覺化 HLS 開發工具, 使得開發 FPGA 更便利. 關於 FPGA 參考 :

# 現場可程式邏輯門陣列 (FPGA)
# FPGA 技術介紹:5 大優點
# FPGA 基本概念
# 轉玩 FPGA
# CPLD/FPGA介紹與VHDL之使用

FPGA 簡言之, 就像可反覆寫入的 DVD RW 一樣, 裡面的電路圖與佈線是可以隨時規劃, 立即生效的. 而 FPGA 所依賴的規劃工具就是 Verilog 與 VHDL 硬體描述語言, 參考 :

# https://zh.wikipedia.org/wiki/Verilog
# https://zh.wikipedia.org/wiki/VHDL

Verilog 與 VHDL 的比較摘要如下 :

 項目 Verilog VHDL
 誕生 1983 1983
 研發者 Gateway 公司 美國國防部
 來源語言 C Ada
 最近版本 IEEE 1364-2005 IEEE 1076-2000
 低階描述能力 有 無
 使用率 80% 20% (主要是美國政府)

所以有 C 或 Java 背景的人學 Verilog 會比較容易上手, 教學文件參考 :

# Verilog HDL 教學講義
# 由C語言學習Verilog的思維轉換 (C/C++) (C) (IC Design) (Verilog)

其他兩本 Verilog 的中文書參考 :

# 數位邏輯設計與晶片實務(Verilog) (全華 2014, 劉紹漢)
# Verilog 硬體描述語言數位電路:設計實務(五版) (儒林 2015, 鄭信源)

目前 FPGA 有兩大供應商 Xilinx (賽靈思, 台積電的大客戶) 與 Altera (阿爾泰拉), 還有一家規模較小的 Lattice, 我之前有想要購買它生產的 SPLD (簡單可程式邏輯裝置) GAL 系列, 參考 :

# 可程式化陣列邏輯 GAL16V8

Xilinx 是 FPGA 的始祖, 但感覺怎麼好像很多書都介紹 Altera 的產品呢? 據下列這篇文章說法, 可能是 Altera 的開發工具 Quartus 介面比較友善易學之故 :

# 該如何選擇FPGA

Altera 的開發工具 Quartus 有免費版本可自由下載 :

# Quartus Prime software Lite edition

在露天找到一些開發板如下 :

# Altera MAX II EPM240 CPLD 開發板 學習板 實驗板 ★ 259295 -035 ★ $390
# Altera EPM240開發板送EPM240擴展板FPGA/CPLD送雙DVD 海量資料送(8G) $500
# FPGA開發板,核心板,主晶片ALTERA公司Cyclone I 的EP1C3T100C8N $580
# FPGA開發板/學習板/最小系統核心板 Altera Cyclone EP1C3T144 $720
# Xilinx FPGA開發板Spartan3 XC3S50AN開發板 核心板 最小系統板 $780

裡面有些是 CPLD (複雜可程式邏輯裝置), 它與 FPGA 都是可程式的 IC, 主要差別是 :
  1. CPLD 邏輯閘數目在數千至數萬規模, 而 FPGA 則在數萬至數百萬.
  2. CPLD 的系統架構較簡單, 在可規劃程度上比 FPGA 欠缺靈活性. 而 FPGA 則大多含有高階的內建模組如加法器或乘法器, 且支援系統組態部分或全部重新配置.  
其實近期我也沒有打算重操舊業玩 IC 設計, 因為現在還用不到, 光是 Arduino Nano 這塊板子就可以做很多應用了, 用韌體去控制 ATmega328P, 其實就是把它變成一顆特製 IC, 根本還不需要自己設計一顆. 只是偶然看到 Verilog 書裡面的邏輯設計, 又讓我一時手癢想要一探究竟而已, 以上只是一些調查摘要罷了.

2016-10-07 補充 :

我在總館借到下面這本旗標的 "正確學會 Verilog 的 16 堂課" :




此書是以 Verilog 來重新描述邏輯設計, 使用 Altera 公司的 Quartus 2 作為開發工具.


2019-03-19 補充 :

最近為了準備正修 Logic Design 授課, 看到圖書館架上好多 VHDL 與 Verilog 的書, 雖然這次課程不包含這部分, 但對該選哪一種來學有點困惑, 看了下面這篇後覺得還是選 Verilog 好了 :

# Verilog HDL和VHDL的比較
# Verilog,VHDL,system C 三者的差異

2019-06-04 補充 :

最近看下面這本書發覺裡面有隨著各元件介紹其 VHDL 寫法, 例如講到正反器時後面就會說明如何用 VHDL 表示正反器, 此書值得一讀 :

# Digital Fundamentals, Global Edition (11 edition)


Source : Amazon

2016年9月4日 星期日

2016 年第 34 周記事

週六中午要參加堂兄嫁女兒喜宴, 因此早上就得回鄉下了, 但是拖到快 11 點才出發, 所幸走捷徑較快, 回到家剛好 11:30, 卸完行李立刻前往喜宴場. 入席時與堂姑的兒子們同桌 (我應該叫他們表哥吧), 其中老三很風趣, 跟我說他離婚很久了, 兒子都已大學畢業. 他自己現在有兩個女朋友, 一個在高雄, 一個在台北, 每個禮拜都會來鄉下找他, 我說那不會打架嗎? 原來她們都認識, 台北的還會交代高雄的就近多照顧阿娜答. 哇咧, 真是現代版齊人之福.

一周來的雨水不斷, 晒穀場前的蕪菁長得都比人高, 再不犁掉就會遮住視線了. 本周小狐狸們開學第一周, 我每天都要八點進公司, 因為五點得趕去鳳中載二哥放學, 所以我不能加班. 本來想來回都搭校車, 但二哥說怕有時社團或班上有事趕不上校車, 只搭上學的校車, 放學時我下班順路去載即可. 我看他不論等校車或等我下班, 都拿李麥克的小說在看, 好像對英文比較有在用心了.

週日早上去市場買菜時, 看到魚攤的小老闆娘跑到前面的炸菜圓鋪幫客人結帳 (她每次都叫我歐吉, 我有這麼老嗎? 叫我歐巴還差不多), 我問她怎麼換妳咧? 她說這是她的副業, 咦, 怎麼最近沒看到菜圓姐姐呢? 原來菜圓姐姐最近在城裡找到工作, 上班去了. 以前每次買菜都看到她在那邊顧攤, 沒事都在滑手機, 年紀輕輕這樣度過也實在可惜. 我剛上市場時常跟她買給菁菁吃, 雖說這菜圓是炸物太熱, 但偶而吃一下還真好吃. 後來菁菁自己怕胖就不買了.

下午大雷雨後, 近傍晚時突然停電, 打 1911 去問, 說正在搶修. 沒辦法只好趁還有一點亮光趕快去洗澡, 但煮菜時就必須點蠟燭了. 六點半吃飯時電仍未來, 只有繼續摸黑吃飯洗碗. 七點過了還沒來電, 邀菁菁騎車去夜遊, 巡一下這附近到底那些人家沒電. 發現我們家以上很多戶都暗暗的, 在半公里遠的十字路口看到台電工程車, 修護人員拿著長竹竿在打電桿上的線, 可能是雷擊造成跳電. 看他們在忙就沒詢問情況了, 折返還沒到家就看到路燈亮了, 有電真好! 這個夏天兩次停電讓我下定決心, 務必要裝個小型太陽能板來蓄電, 小容量也沒關係, 至少要能夠照明啊! 不然兩三個小時黑漆漆真痛苦.


2016年9月3日 星期六

年金是保險不是福利

最近小英政府積極處理年金改革問題, 引起軍公教反彈, 今天要上凱道抗議遊行. 我認為 :
  1. 年金不應該看每個月領多少, 關鍵在所得替代率合不合理. 你覺得接近 100% 合理嗎?
  2. 年金是保險不是福利, 不要把退休月領好幾萬所得替代率 98% 當成是考上公職的福利.
  3. 年金目的是照顧退休生活不虞匱乏, 不是遊山玩水周遊列國的經費. 
愚以為軍人與老師性質特殊, 政府應予以保障與照顧, 特別是老師, 現在教師員額不補實, 流浪趕場代課教師充斥校園, 鐘點費僅能糊口, 你覺得孩子們的未來有希望嗎? 但公務員跟我們一樣都是領薪水的雇員, 差別只是, 他們是政府雇員而已. 但現在不管軍保, 教保, 公保, 勞保, 每一個財務都瀕臨破產, 顯示這些年金制度絕對有問題, 繳得少領得多, 合理嗎? 不改行嗎?

年金改革很早以前學者專家就提出警告要早一點做了. 但過去 16 年扁馬做了甚麼?

2005 年陳水扁宣布軍公教 18% 優退利率改革. 2006 年銓敘部提出的 18% 改革方案在立法院被國親兩黨聯手阻擋廢止, 除提案譴責考試院長姚嘉文與銓敘部長朱武獻, 並移送監察院糾彈. 扁政府只不過才動一下 18% 而已就死翹翹了, 還談甚麼改革其他年金哩!

2010 年馬政府將原本是行政命令的 18% 優存辦法變成了法律, 還擴大發放, 大舉加重納稅人負擔, 引起全國各界反彈, 為了平息眾怒, 馬政府於 2011 年廢止才實施一個月的方案, 並調降所得替代率, 但還是高於扁政府方案的負擔.

2012 年馬英九說 "年金改革今天不做, 明天就會後悔!", 結果 2013 年公務人員退撫法案送進立法院, 就被軍公教揚言用選票教訓國民黨, 在國親立委聯手阻擋下胎死腹中. 馬英九退縮讓步的結果有幫國民黨贏得 2014 年縣市長選舉嗎? 沒有, 國民黨輸到只剩六席縣市長.

我覺得要是台灣近海發現大量石油的話, 不要說 18%, 就是 188% 都沒問題. 但問題就是沒有. 這些快要破產的公職年金, 包括 18% 優存實質上都是國庫用廣大中產階級納稅人的錢去填補的 (中央政府總預算歲入所得稅佔 43.8%), 而這些人很多都是工作沒保障的私人企業勞工, 他們自己的勞保也是快要完蛋了, 竟然還要去顧有工作保障的軍保, 公保, 教保哩! 到底是誰該上凱道啊!

年金改革要是失敗, 小英 2020 保證沒希望; 要是順利成功, 小英也不見得能連任, 用賽局理論簡單算一算, 小英妳就犧牲一下勇敢撩下去吧! 成功的話就算不能連任也能在歷史上留下治績, 重要的是也救了下一代年輕人.

至於上凱道反對年金改革的, 我認為既然這麼早就退休了, 身體又還硬朗的話, 還不如參加中油探勘隊, 想想看如果能早一點挖到石油的話, 不要說 188% 了, 我看 188888% 都有可能哩!

至於我自己的勞保, 嗚 ... 雖然已經少得可憐了, 但是為了後代子孫, 少一點也是沒關係啦! 我當然也是想越多越好啊! 但是都已經快坐吃山空了還能怎樣. 再不解決很快就會變成下一個希臘. 人生啊, 走遍千山萬水最後也是六尺見方, 不要吃乾抹淨債留子孫.

參考 :

# 退休軍公教人員優惠存款
# 別忘了馬英九說過:今天不做明天就會後悔
# 教師盟:月繳四千領七萬 真的很丟臉
# 大破產世代/軍公教福利太好「政府毀約應該的」
# 公務員「地板」是勞工「天花板」

PS:

我看了今天遊行人士批評小英的年金改革是躁進, 我明白了, 這夥人就是要慢慢改, 等他們領完了吃光了再來改 (這樣現在年輕的老師跟公務員怎麼辦? 管他的? 先領再說?). 其實, 16 年前扁政府時代如果開始改都已經太慢了! 太多不合理之處透過這次遊行一一被挖出來, 原來胡志強留學英國十年也算公職年資啊! 黨職也可以併公職合理嗎? 難怪國家都快要被吃垮了!

我查詢 "勞工 子女教育補助", 結果只找到勞動部 "失業勞工子女就學補助" 規定, 勞工子女教育補助? 別傻了, 乖乖去辦學貸吧, 晚一步可搶不到喔. 看看公職人員的子女教育補助, 補助最高的是私立大學達 35800 元, 連退休後都還可以領, 請看這篇 :

# 退休軍公教享子女教育補助 年耗國庫10億

福利這麼好了, 加上 18% 以及超高所得替代率, 羨慕吧! 來考啊! 就怕你考不上! 這是一位郵局老員工跟我談到月退俸時這麼說的. 是的, 我也很想享受這些民脂民膏. 總之一句話 : 這些人是得了便宜還賣乖! 改革不能溯及既往, 那請問誰要當第一屆倒楣鬼啊!

參考 :

# 軍公教遊行5點結束 綠委:笑了!真的是公務員
# 待遇天差地別 私校教師譴責軍公教遊行者 勿給年輕世代負擔
# 用改革話術包裝反改革?看懂93遊行的話術

還有更好笑的, 說年金改革就是要消滅中華民國, 這是該吃循立寧了嗎? 轉角的康是美有在賣.

# 蔡英文為何提年金改?退伍軍人協會副理事長吳其樑:就是要消滅中華民國

更可惡的是顛倒黑白的洪秀柱, 它說 :

"國民黨也有提出年金改革的訴求,但在立法院始終沒有通過,也缺少理性討論的空間,且民進黨也百般阻撓,未來還需要加強與各界的溝通。"

這個嚴重, 循立寧要整罐吃.

# 現身大遊行力挺 洪秀柱:這次改革 最生氣的是「不給大家尊嚴」
# 游盈隆專欄:歷史將如何記錄今天的九三軍公教大遊行?

2016-09-06 補充 :

我持續調查年金改革的歷史脈絡, 發現在 2013 年馬政府提出年金方案時, 社會上就有非常多的爭議與討論, 其中比較理性去探討的是苦勞網的一系列文章 :

# 超簡單理解「軍公教」退休改革

值得大家好好去研究該怎麼解決這個問題, 而不是在那邊用鬥爭式的語言互相謾罵.

2016-09-12 補充 :

今天看到這篇, 挺有意思的, 蔡政府用共識決看來不太可能凝聚共識沒錯, 因為大家只護衛自己的利益, 不可能自己割肉. 年金真的很專業, 我覺得還是要專家才行 :

# 年金改革─用你的愚笨證實我的無能是應該的

2016-09-19 補充 :

今天看到這篇 :

# 軍公教子女年補十億學費 退休照領?

人本來生來就是不公平, 但國家存在的目的是要透過政策盡量撫平不公平所帶來的不幸.


2016年9月2日 星期五

★ Blynk 的虛擬腳位用法整理

整理完 Blynk 的基本用法, 覺得虛擬腳位在整個架構中具有非常重要的地位, 因此我又重新咀嚼了官網教學文件關於 Virtual Pin 的敘述, 把散佈在之前測試文章中的用法做個摘要整理. 過去兩周對 Blynk 元件的使用作了研究, 心得記錄在下面六篇文章 :

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

Blynk 所設計的 Virtual Pin (vPin) 事實上是伺服器上的一個資料欄位, 乃設備 (微控器) 與手機 Blynk App 之間的一個資料傳遞通道或軟體 I/O 介面, 並非物理上實質的 I/O 腳位. 在 Blynk App 中我們可以為元件的資料輸出 (控制元件) 或輸入 (顯示元件) 綁定一個 Virtual Pin, 它會對應到伺服器中的特定資料欄位. 此虛擬腳位與設備端的繫結是透過 Blynk 函式庫中的兩個事件處理函數 : BLYNK.READ(vPin) 與 BLYNK.WRITE(vPin), 如下圖所示 :



上圖中 Blynk.virtualWrite(vPin, value) 函數是虛擬腳位的核心函數, 而 BLYNK_READ(vPin) 與 BLYNK_WRITE(vPin) 函數則是事件處理函數, 分別處理來自 Blynk 伺服器的讀取要求事件 (App 向伺服器發出要求, 伺服器又向設備發出要求), 以及 Blynk 伺服器的 vPin 寫入事件 (App 寫入伺服器, 伺服器又寫入設備).

Blynk.virtualWrite(vPin, value) 函數 : 

手機 App 可透過操作控制元件來更改虛擬腳位之值; 反方向來說, 設備端也可以呼叫 Blynk.virtualWrite(vPin, value) 函數將新值 value 寫入虛擬腳位, 此新值會立刻被傳送到 Blynk 伺服器, 再轉送至手機 App 中綁定此虛擬腳位之元件, 通常是用於在手機 Blynk App 的顯示元件上呈現設備端的狀態或數據.

虛擬腳位的值可以為數值 (整數, 浮點數), 也可以為字串, 例如 :

Blynk.virtualWrite(V0, "abc"); //傳遞字串
Blynk.virtualWrite(V0, 123); //傳遞整數
Blynk.virtualWrite(V0, 12.34); //傳遞浮點數

除了將數據傳送給顯示元件外, Blynk.virtualWrite() 函數還可以一次傳送最多四個參數 (除第一參數 vPin 之外) :

Blynk.virtualWrite(V0, "hello", 123, 12.34, 0.1234);  //一次最多紀錄 4 個參數

這種多參數的用法是用在需要將資料儲存於伺服器資料庫的場合, 如上所言, 虛擬腳位只是伺服器資料庫中的資料欄位, 設備端程式呼叫 Blynk.virtualWrite() 其實就是將這些參數寫入這個欄位中. 我們可以利用此功能來儲存系統狀態變數, 如同使用 ATmega328P 內部的 EEPROM 記憶體一樣, 當系統因為失去電力或網路斷線時, 最後狀態可以儲存在虛擬腳位中, 等設備重新與伺服器連線時再將這些狀態取出回到斷線前狀態繼續執行.

在官網教學中有一個利用 Blynk.virtualWrite() 將設備端變數儲存到 Blynk 伺服器的範例 :

# ServerAsDataStorage_MultiValue.ino

此範例程式不太實際, 因為其中 V0 儲存之 "SomeStaticData" 讓人摸不著頭緒. 我將此範例改編為如下程式. 在手機 Blynk App 中, 此專案包含了一個 Value Display 元件以及一個 Button 元件, 前者用來顯示程式已經執行的秒數; 後者用來產生一個虛擬腳位寫入事件, 我們要將此按鈕已被按下的次數記錄下來, 連同已執行秒數憶起儲存在伺服器的虛擬腳位中, 系統重開機時這兩個狀態會被重新載入, 程式就可以從系統關機時的狀態回復. App 的設定如下 :

按鈕綁定到虛擬腳位 V2 :


數值顯示器綁定 V1 :


而虛擬腳位不綁定任何 App 元件, 純粹是用來儲存資料用, 程式如下 :

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

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

char ssid[]="EDIMAX-tony";
char pass[]="1234567890";
char auth[]="3a5a3d8c4e0545669ffb847xxxxxx033";

int upTime; //seconds
int pressCount; //counter for pressing button

void setup() {
  Serial.begin(9600); //Set console baud rate
  esp8266.begin(9600); //Set ESP8266 baud rate
  Blynk.begin(auth, wifi, ssid, pass);
  timer.setInterval(1000L, pushUptime); //Don't send more that 10 values per second
  }

void loop() {
  Blynk.run();
  timer.run();
  }

BLYNK_CONNECTED() {  //called when connected to the server
  Blynk.syncVirtual(V0);  //get data stored in virtual pin V0 from server
  }

BLYNK_WRITE(V0) { //called when V0 updated (sync)
  upTime=param[0].asInt();  //restore from server
  pressCount=param[1].asInt();  //restore from server
  }

BLYNK_WRITE(V2) { //called when V2 updated
  ++pressCount; //increment counter
  }

void pushUptime() { //push Arduino uptime to virtual pin V0
  ++upTime; //seconds
  Blynk.virtualWrite(V0, upTime, pressCount); //store multi values to server
  Blynk.virtualWrite(V1, upTime); //update App display
  BLYNK_LOG("Uptime=%d",upTime);
  BLYNK_LOG("Press Count=%d",pressCount);
  }

此程式裡有兩個全域變數,  pressCount 用來記錄按鈕已經按下多少次, upTime 則記錄程式已經執行的秒數, 在 timer 的每秒中斷處理函數 pushUptime() 中我們利用 Blynk.virtualWrite() 將這兩個狀態變數儲存到伺服器裡此專案的虛擬腳位 V0. 而為了顯示程式已執行秒數, 則使用 V1 虛擬腳位來儲存, 並綁定到 App 裡的 Value 顯示器中.


序列埠監控視窗擷取訊息如下, 板子原先開機過一次, 按鈕 35 之後拔掉電源再重新插上去, 此為第二次開機時的 log, 可知當板子重新通電後, 在尚未與 Blynk 伺服器連線時, upTime 與 pressCount 為初始值, 直到連線成功後與伺服器同步, 取回斷電前狀態後就更新這兩個全域變數, 系統狀態就從斷電前開始執行了 :

[0] Blynk v0.3.8 on Arduino Nano
[499] Connecting to EDIMAX-tony
[1517] Failed to disable Echo
[2518] Uptime=1
[2518] Press Count=0
[3517] Uptime=2
[3517] Press Count=0
[4517] Uptime=3
[4517] Press Count=0
[15218] Uptime=4
[15218] Press Count=0
[16323] Uptime=5
[16323] Press Count=0
[16334] Uptime=6
[16334] Press Count=0
[16350] Uptime=7
[16360] Press Count=0
[16394] Uptime=8
[16403] Press Count=0
[16435] Uptime=9
[16446] Press Count=0
[16478] Uptime=10
[16489] Press Count=0
[16551] Ready (ping: 228ms).
[18176] Uptime=11
[18176] Press Count=0
[18589] Uptime=12
[18589] Press Count=0
[19098] Uptime=1485
[19098] Press Count=35
[19508] Uptime=1486
[19508] Press Count=35
[19917] Uptime=1487
[19918] Press Count=35
[20328] Uptime=1488
[20328] Press Count=35
[20738] Uptime=1489
[20738] Press Count=35

可見在 [19098] 時, 重新與伺服器連線後便從 V0 取回斷電時的數值繼續執行. 注意, 綁定顯示器元件的虛擬腳位只能儲存單一數值, 不可以存放多重數值, 例如上例中 App 的數值顯示器若綁定 V0 將不會顯示任何數值, 因為它不知道要顯示多重數值中的哪一個, 必須另外綁定只儲存單一數值的 V1.

此專案設計分享如下 :


要特別注意的是, 要避免在 loop() 函數中呼叫 Blynk.virtualWrite(vPin, value), 因為那樣設備很容易以過高的頻率更新 vPin 之值 (超過每秒 10 次), 使得伺服器發生 Flood Error (資料氾濫錯誤). 參考 :

# http://docs.blynk.cc/#troubleshooting-flood-error

在 loop() 中就算搭配 delay() 也無法解決 Flood Error 問題, 反而會產生其他問題, 例如因延遲太久導致連線中斷以及效能低落等等. 正確做法是使用 Blynk 函式庫的 SimpleTimer 計時器函數來控制數據傳輸頻率. Blynk 伺服器的上限是每秒 10 筆, 當資料傳送頻率超過每秒 10 筆時伺服器會自動切斷連線, 手機 Blynk App 會顯示 "Your hardware is offline". 如果更新 vPin 的頻率需要超過每秒 10 次, 則必須自建 Blynk 伺服器, 修改 server.properties 檔案中的 user.message.quota.limit 屬性, 同時也要修改 Blynk 函式庫 BlynkConfig.h 檔案中的 BLYNK_MSG_LIMIT 常數, 參考 :

# Troubleshooting


BLYNK.WRITE(vPin) 函數 :

BLYNK.WRITE(vPin) 是設備端虛擬腳位的 setter 函數 (寫入事件處理函數), 當寫入設備端 vPin 的事件發生時, 此函數就會被呼叫. 具體來說, 當操作手機 App 的控制元件時, 其新值將透過所綁定之虛擬腳位寫入伺服器之資料欄位, 伺服器同時將此新值放在全域變數 param 中傳遞給設備端, 這將觸發設備端寫入事件, 使事件處理函數 BLYNK.WRITE(vPin) 被呼叫. 設備端韌體可在此函數中利用 param 變數取得 App 端所傳遞的資料, 並呼叫 asInt(), asString() 等函數將 param 轉成適當資料型態 :

BLYNK_WRITE(V0) {  //虛擬腳位 V0 的寫入事件處理函數
  int v1=param.asInt(); //以整數型態取得參數
  Double v2=param.asDouble();  //以浮點數型態取得參數
  Float v3=param.asFloat();  //以浮點數型態取得參數
  String value=param.asStr();  //以字串型態取得參數
  //有些元件如搖桿會傳遞多的變數, 則 param 為一參數陣列
  int x=param[0].asInt();  //以整數型態取得參數
  int y=param[1].asInt();  //以整數型態取得參數
  //... 處理寫入值
  }

除了由 App 端的控制元件觸發 BLYNK.WRITE(vPin) 被執行外, 設備端也可以自己呼叫 Blynk.syncVirtual(vPin) 函數來觸發 BLYNK.WRITE(vPin) 被執行.

BLYNK.READ(vPin) 函數 :

BLYNK_READ(vPin) 函數是設備端虛擬腳位的 getter 函數 (讀取事件處理函數), 當伺服器向設備發出 vPin 讀取要求時, 此函數就會被呼叫, 此種要求通常來自 App 中設定為 Reading 模式的顯示元件, 在此模式中 App 的顯示元件會週期性透過伺服器向設備發出 vPin 資料讀取要求. 在此 BLYNK_READ(vPin) 函數中, 設備必須利用 Blynk.virtualWrite(vPin, value) 函數將 vPin 的新值 value 傳送到伺服器.  例如 :

BLYNK_READ(V0) {
  Serial.println(millis()/1000);
  Blynk.virtualWrite(V0, millis()/1000);
  }

當收到伺服器的 V0 讀取要求後, 此事件處理函數會被呼叫, 設備就將資料用 Blynk.virtualWrite() 函數傳送給伺服器. 當然 App 端的顯示元件必須設定在週期性的 Reading 模式, 不可以設在 Push 模式. 若設在 Push 模式就不需要 BLYNK_READ(vPin) 函數, 而是需要一個計時器來觸發一個自訂的資料推送函數, 參考 :

# Blynk 的顯示元件 (Displays)

其他參考 :

# How to use Virtual Pins in Blynk


杭州錢氏家族

今天在雅虎看到下面這則新聞, 報導 2008 年諾貝爾化學獎得主之一的錢永健猝逝於俄勒岡州 :

# 諾貝爾化學獎得主、中央研究院院士錢永健猝逝 享壽64歲

錢永健是華裔美國人, 生於紐約, 哈佛大學畢業, 英國劍橋大學生理學博士, 長期任教於加州柏克萊與聖地牙哥分校, 因為在水母中發現綠色螢光蛋白與其相關研究獲得 2008 年諾貝爾化學獎, 參考 :

# https://zh.wikipedia.org/wiki/钱永健

綠色螢光蛋白 (GFP) 應用在現今許多生技研究上, 其基因可被導入小至細菌大至人體細胞中而能保持其基因組. 但我比較好奇的是報導中提到錢院士是五代十國吳越國主錢鏐 (音同留) 的 34 世孫, 這就引起了我的歷史考古熱. 十國中我印象比較深刻的是李後主的南唐, 吳越我就生疏了. 所以我就做了一些調查, 摘要如下 :

錢鏐, 唐宣宗大中六年生於杭州臨安的一個農家, 因長相奇醜, 險被其父丟棄, 幸得祖母憐惜而保留性命, 故得小名婆留.  少年時不好詞章習武善射, 成年後從軍, 並將小名錢婆留改為錢鏐. 因武藝高強受到指揮使董昌的重用. 唐末叛亂四起, 董昌與錢鏐聯合浙江境內各地方民團武力組成 "八都軍", 逐漸削平各地大小軍閥, 建立了自己的地盤. 其後董昌因向朝廷討封不成反叛稱帝, 唐昭宗命錢鏐征討, 錢鏐雖感念董昌提攜之恩不願兵戎相見, 但董昌卻與楊行密合兵來攻. 董兵敗自殺後, 錢鏐因功封鎮海鎮東節度使兼中書令, 唐昭宗還特賜金書鐵券 (即免死金牌也) 免其本人九死.

唐亡後, 錢鏐仍奉中原王朝正朔, 未曾稱帝. 朱溫篡唐建立後梁, 封錢鏐為吳越王, 吳越國因此創立 (西元 907 年), 領有今浙江全境, 江蘇東南部與福建東北部地區, 都杭州. 在錢鏐戮力經營下, 吳越國算是五代十國中政治穩定, 經濟發展進步的國家. 吳越傳三代凡五位君主, 至其孫錢俶 (音同觸) 時, 迫於時勢於宋太宗太平興國三年 (西元 978) 獻土於宋, 享國 71 年, 是十國中歷時最長久的. 入宋後, 吳越國宗室多封節度使, 刺史等職, 臣下僚屬亦多在宋廷為官, 可見宋朝趙氏對投降者算是很厚道的, 未加殺戮, 這也是我蠻喜歡宋朝的原因.

錢氏家族後代在吳越國入宋後仍在江浙一帶繁衍, 歷代名人輩出, 例如清代史學家, 語言學家錢大昕 (江蘇嘉定); 國學大師錢穆 (江蘇無錫), 中研院院長錢思亮 (浙江餘杭), 乃至周恩來稱譽的中國科技界三錢 : 錢三強 (浙江湖州), 錢偉長 (江蘇無錫), 與錢學森 (浙江杭州), 他們都是吳越王錢鏐的後人. 其中有中國航天之父之稱的錢學森就是錢永健的堂叔.