2017年10月18日 星期三

Arduino 聲音感測模組測試

以前在露天買過一個麥克風模組, 也是放在零件箱中不見天日.

►267◄聲音感測器 聲音檢測模組 咪頭模組 聲控口哨開關 聲音模組 Arduino
向 allen_6833 採購電子零件模組一批

上週從母校高應大圖書館借到下面這本 Arduino 的書, 裡面12-1 節介紹麥克風模組, 可以用來偵測環境音量遂行控制, 例如聲控燈或聲控開關等.

跨入Maker物聯網時代 : 誰都可以用Arduino / 楊佩璐, 任昱衡編著

以下測試還參考了下面幾本書 :

# 超圖解 Arduino 互動設計入門 2, 趙英傑 (旗標)
# 輕鬆入門-Arduino 範例分析與實作設計, 葉難 (博碩)

麥克風模組 (或稱聲音感測模組, 麥克風放大器模組) 有兩種, 我買的這組是常見的三針腳式的模組, 只有 VCC, GND, 以及 DO (Digital Ooutput) 三隻腳, 運作電壓 3V~5V, 因此 Arduino 與 ESP8266 均可使用, 一般價位 17~40 元之間 :

T電子 現貨 Arduino模組 Arduino UNO R3 聲音模組 口哨模組 電子積木 聲音感測器 $30
Arduino 麥克風 放大器 聲音傳感器 模組 聲控 開關 數位輸出 01高低電平(附範例) $35

這種模組板上有一顆可變電阻來設定聲音感測的靈敏度, 當電容式麥克風感測到的聲音所產生的電壓高於靈敏度電阻所設之門檻電壓時, 板上的運算放大器 LM358 會驅動 OUT 針腳輸出 LOW 位準, 否則輸出 HIGH 位準. 缺點很明顯, 就是無法透過程式去控制門檻電壓, 所以不要買這種三腳貓的模組.

另外一種是四支接腳的, 它多了一個 AO 輸出 (Ananlog Output), 透過 Arduino 的類比輸入腳讀取經 A/D 轉換後, 呼叫 AnalogRead() 的會傳回值域 0~1023, 使用這樣的模組才能根據環境與應用來調整採取動作的門檻值 :

麥克風放大器模組 聲音模組MIC模組麥克風模組語音模組 $40
KY-037 高感度麥克風/聲音感測器模組 for Arduino / 附 範例 $40
廠家熱賣 麥克風放大器模組 聲音模組MIC模組麥克風模組語音模組 $40

也可以自己兜模組, 需要一個電容式麥克風, 一個 LM358 運算放大器, 電阻 1K (棕黑紅), 2.2K (紅紅紅), 68K (藍灰橙), 100K (棕黑黃) 各一個, 0.1uF (104) 電容一個, 參考趙英傑寫的 "超圖解 Arduino 互動設計入門 2" 這本書的 6-3 節, 電路圖如下 :




這裡主要的元件是 LM358 運算放大器與電容式麥克風, 可在露天購得 :

帶引腳 咪頭 6*5mm 電容式 駐極體話筒 拾音器 麥克風靈敏度52D $6
直插 LM358P 晶片 運算放大器 雙路 DIP-8 $3

LM358 內含兩組運算放大器, 這裡只用到其中一組而已. 上圖中麥克風的輸出會經過一個高通濾波器穿送至運算放大器的 + 腳, 然後與 1K+100K 分壓電阻進行差分放大, 其中 100K 電阻可改用 100K ~ 200K 可變電阻來調整信號放大倍率. 當然, 如果買有四隻腳的模組就比較方便, 不需要自己兜啦. 不過, 若要安裝在機箱中, 麥克風須拉到機殼外拾音, 或許自己兜會比較好安排元件配置.

下面測試 1 是從接到 Arduino A0 輸入的聲音感測模組的 AO 輸出讀取音量資料, 如果超過門檻值就點亮內建 LED, 否則就熄滅 :


測試 1 :  當環境聲音超過門檻值時點亮 LED

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

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

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal > 200) {  //若超過門檻值
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }


上面測試 1 只要拍手的音量超過門檻值, 板上 LED 就會切換狀態, 如果要連續拍兩次才動作的話該怎麼做呢? 下面測試 2 參考趙英傑寫的 "超圖解 Arduino 互動設計入門 2" 這本書的 6-4 節改寫 :


測試 2 : 連續拍手 2 次控制 LED 明滅

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

unsigned long current=0;  //紀錄目前過門檻時戳
unsigned long last=0;  //紀錄上次過門檻時戳
unsigned long diff=0;  //紀錄前後兩次時間差
unsigned int count=0;  //紀錄已偵測到的次數

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

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  if (micVal > 200) {  //若超過門檻值
    current=millis();  //紀錄目前時戳
    ++count;  //增量偵測次數
    Serial.print("count=");  //輸出偵測次數
    Serial.println(count);
    if (count >= 2) {  //若次數已達 2 次, 判斷間隔時間是否在 0.3~1.5 秒內
      diff=current-last;  //計算前後兩次時間差
      if (diff > 300 && diff < 1500) {  //判斷間隔時間是否在 0.3~1.5 秒內
        toggle = !toggle;  //反轉 LED 狀態
        count=0;  //計數器歸零
        }
      else {count=1;}  //間隔太短或太長則第二次不算, 計數器還原為 1
      }
    last=current;  //以目前時戳更新上次時戳, 以便下一次偵測時比較之用
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  }

當然也可以改為連續拍兩次手觸發, 那就需要多一個變數來儲存時戳了, 不過, 或許用有限狀態機 (FSM) 會比較容易. 如下列測試 3 所示 :


測試 3 : 連續拍手 3 次控制 LED 明滅 (使用有限狀態機)

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

unsigned long current=0;  //紀錄目前過門檻時戳
unsigned long last=0;  //紀錄上次過門檻時戳
unsigned long diff=0;  //紀錄前後兩次時間差

typedef enum {  //定義有限狀態機之狀態
  S_START,
  S_FIRST,
  S_SECOND,
  S_THIRD
  } State;

State state;  //建立 State 類型變數
boolean detect();  //函數 detect 原型宣告

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  state=S_START;  //起始狀態
  }

void loop() {
  switch(state) {  //判斷目前狀態
    case S_START:
      if (detect()) {  //偵測到第1個信號:進入狀態1
        state=S_FIRST;
        Serial.println("count=1");
        }
    break;
    case S_FIRST:
      if (detect()) {  //偵測到第2個信號:進入狀態2
        state=S_SECOND;
        Serial.println("count=2");
        }
    break;
    case S_SECOND:
      if (detect()) {  //偵測到第3個信號:進入狀態3
        state=S_THIRD;
        Serial.println("count=3");
        } 
    break;
    case S_THIRD:
      toggle = !toggle;  //反轉 LED 狀態
      state=S_START;  //回到起始狀態
    break;
    }
  if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
  else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
  }

boolean detect() {  //if volume reaches threshold return true
  micVal=analogRead(MIC);  //讀取感測器輸出
  boolean ret=false;  //預設傳回值 false
  if (micVal > 200) {  //若超過門檻值
    current=millis();  //紀錄目前時戳
    diff=current-last;  //計算前後兩次時間差 
    if (diff > 300 && diff < 1500) {  //間隔時間在 0.3~1.5 秒內
      ret=true;  //有效信號,傳回值改為 true
      last=current;  //更新上次時戳為目前時戳
      }
    }
  return ret;  //傳回偵測結果
  }

上面程式中使用 typedef 定義了一個 enum 的有限狀態機, 開機起始狀態是 S_START, 在 loop() 迴圈中使用 switch case 來判斷目前狀態, 然後依據全域變數 toggle 之值變更 LED 狀態. 偵測音量訊號的部分被寫成了 detect() 函數, 當音量超過門檻值, 且與上一次偵測到訊號之間隔在 0.3~1.5 秒內表示訊號有效, 就會更新上一次時戳 last 並傳回 true.

在每一個 case 中會先呼叫 detect() 函數, 若傳回 true 表示偵測到有效的信號, 就進入下一個狀態, 狀態機依序從 S_START 走到 S_FIRST, S_SECOND, S_THIRD, S_START, ... , 周而復始. 當進入第三狀態 S_THIRD 時, LED 狀態變數會被反轉, 使 LED 明滅, 同時狀態機也會回到初始的 S_START 狀態, 狀態遷移圖如下所示 :


使用有限狀態機技巧可以讓我們將一個複雜的邏輯運算分解為多個獨立的作業, 使運作邏輯的實作簡化, 有利於軟體後續的維護與擴充. 對於像 Arduino 這樣無作業系統的嵌入式設備, 有限狀態機似乎就是一個超迷你作業系統. 關於有限狀態機的用法, 可參考葉難寫的 "輕鬆入門-Arduino 範例分析與實作設計" 第 3-5 節與 6-5 節或下列文章 :

Arduino練習:Simon Says請你跟我這樣做
有限状态机在单片机编程中的应用
Arduino编程之----如何让你Arduino以状态机方式运行

在上面的範例中使用的音量偵測門檻值都是武斷的, 在實際應用中都需要根據環境噪音的強弱加以調整, 程式需重新編譯上傳非常麻煩, 實用性不高. 在楊佩璐寫的 "跨入Maker物聯網時代 : 誰都可以用Arduino" 第 12.1.3 節介紹了自動調整門檻值的方法, 我參考這個方法將上面測試 1 改寫為如下測試 4 :


測試 4 :  當環境聲音超過門檻值時點亮 LED (可偵測背景音量)

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量
int background=0;  //紀錄環境音量最大值

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  while (millis() < 3000) {  //以3秒時間偵測環境音量
    micVal=analogRead(MIC);  //讀取麥克風音量 
    if (micVal > background) {  //若大於前一次音量就更新為目前音量
      background=micVal;
      }
    }
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal-background > 10) {  //若超過背景音量 10
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }

此程式中添加了一個全域變數 background 來記錄背景音量的最大值, 然後在 setup() 中以一個 3 秒的迴圈來記錄背景雜音的最高值, 記錄在 background 中. 在 loop() 中的音量偵測就改為比 background 高出 10 視為有效之信號而使 LED 改變狀態.

不過上面測試 4 程式有個缺點, 它只在一開機或 reset 時會偵測一次背景音量, 之後若背景噪音變化就失靈了. 解決辦法就是週期性去偵測環境音量, 這就要用到計時器了, 參考之前做 NTP 測試時所用的 Time 與 TimeAlarm 這兩個函式庫 :

利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)

可從 GitHub 下載 TimeTimeAlarm 函式庫 :

https://github.com/PaulStoffregen/Time
https://github.com/PaulStoffregen/TimeAlarms

解壓縮 zip 檔後將 Time 與 TimeAlarm 兩個子目錄複製到 Arduino IDE 安裝目錄下的 libraries 下即可.


測試 5 :  當環境聲音超過門檻值時點亮 LED (可定時偵測背景音量)

#include <Time.h>
#include <TimeAlarms.h>

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量
int background=0;  //紀錄環境音量
void update_background();  //函數 update_background() 原型宣告

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  update_background();  //更新環境音量最高值
  Alarm.timerRepeat(60, update_background);  //設定計時器每 60 秒更新環境音量最高值
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal-background > 10) {  //若超過背景音量 10
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }

void update_background() {  //更新背景環境音量最大值
  while (millis() < 3000) {  //以3秒時間偵測環境音量
    micVal=analogRead(MIC);  //讀取麥克風音量 
    if (micVal > background) {  //若大於前一次音量就更新為目前音量
      background=micVal;
      }
    }
  }

上面程式在 setup() 中先呼叫 update_background() 做 background 的初始更新, 然後呼叫 Alarm.timerRepeat() 設定計時器, 每 60 秒去呼叫 update_background() 更新背景環境音量最大值.

參考 :

Arduino : 聲控開關
Arduino 聲控開關

2017年10月17日 星期二

幫姊姊訂台鐵與高鐵票

姊姊說這禮拜要回來, 因為之後要練啦啦隊比較忙. 上回她訂高鐵有 85 折學生優惠, 但時段有限, 數量也有限, 我上網一查發現不管幾折都是離峰時段, 根本不合用, 合用的也太晚訂被搶購一空, 參考 :

高鐵大學生優惠

除了時間問題外, 車錢才是真正有感的. 我查了一下北高票價, 高鐵全票 1490, 是自強號的 1.8 倍, 搭一趟高鐵幾乎是自強號兩趟了, 就算是早鳥 65 折也還是比自強號多出 141 元, 比較如下 :

 台鐵自強號 高鐵 
 高雄 - 台北 843 元
 新左營 - 台北 824 元 標準1490 元
 自由座 1445 元
 早鳥票 65 折 965 元
 大學生 85 折 1265 元
 大學生 7 折 1040 元
 大學生 5 折 745 元

當然, 自強號的時間是高鐵的兩倍, 所以不趕時間的話還是搭自強號好了. 上回聽阿英說家宏往返台中高雄都坐台鐵, 加入會員累積里程還可以換免費車票, 所以我就幫姐姐註冊了台鐵會員, 參考 :

台鐵會員系統

這個會員系統還有一個功能, 若買不到票, 又沒時間常上網去看看有無退票釋出的話, 可以透過會員系統自動媒合, 每天早上 06:00 後可以輸入所需班次與數量, 若有退票就會依序媒合, 媒合結果會以電子郵件通知. 不過媒合需求是以一天為單位, 若當日媒合失敗, 第二天還是要再次人工輸入需求, 並不會延續前一天的輸入. 媒合成功後, 買票還是要到台鐵訂票系統去買.

台鐵訂票系統
台鐵網路付款
台鐵列車時刻查詢
台鐵會員系統

預定台鐵車票是乘車日前 14 天開放, 參考 :

非乘車當日訂票

乘車前二週(十四天)開始預訂,即週一可預訂次次週一內之乘車票,但每逢週五可多預訂二天至次次週日之乘車票(即逢週五可預訂至次次週五、六、日之乘車票)。

經過這次幫姊姊訂自強號, 我整理了適合她往返北高的班次如下, 143 次與 136 次行車時間最少, 所以列入優先選項 :

回高雄 :

台北 - 新左營 139 次 : 17:00~21:39 (4:39)
台北 - 高雄 143 次  : 18:00~22:07 (4:07)  (逢週五開)

回台北 :

高雄 - 台北 普悠瑪 136 15:30~19:06  (3:36)
新左營 - 台北 138 次 : 15:49~20:31 (4:42)
新左營 - 台北 142 次 : 16:20~21:10 (4:50)

以信用卡網路付款後, 記下訂票電腦代碼於開車前 30 分鐘攜身分證至櫃台取票即可. 也可在超商用 ibon 取票, 但每張要付手續費 8 元, 參考 :

取票或網路付款截止時間:
您訂的車票已網路付款,請取票人持訂票人身份證明證件正本(僑胞及外籍人士持護照或居留証)及電腦代碼,於車站營業時間最遲於開車前至車站售票窗口或開車前30分鐘至全國各地電腦連線郵局、超商取票。(請斟酌取票時間,以免不及乘車)


  1. 會員服務系統委託訂票媒合提供會員於網路訂票系統開放訂票後,無法訂到所需班次,又無法經常上網查詢預訂時,可藉由會員制服務系統,於夜間將剩餘可售座位(即旅客逾期未取及退票後之剩餘座位),進行媒合,於媒合成功時,以電子郵件方式通知會員媒合成功之訊息。會員服務系統委託訂票媒合,僅開放乘車前3天到前12天,共計10天的媒合服務,主要係提供與車站窗口同步開放乘車日(含當日)12天前之預購服務。
  2. 會員制旅客服務系統帳號即是身分證字號,依據會員制消費金額累積及點數換算規定: 會員需實際搭乘本局列車抵達目的站始可累積消費金額,消費金額於實際乘車後翌日累積;每筆消費記錄可保留兩年,為確保消費金額累積正確,辦理購票/取票手續時, 於車站售票窗口提示身分證字號聲明具會員身份、網路付款購買本局車票,或於訂妥車票後利用本局對號列車自動售票機、郵局取票或超商取票等方式完成購/取票,並已完成搭車事實後,即可累計消費金額並享點數回饋酬賓兌換。
  3. 會員累積點數到達可兌換車票門檻後,可使用網路訂票,再去櫃檯進行兌換車票或直接至售票櫃檯進行兌換車票,只要您的會員點數達到兌換標準,可先於網路訂票(勿網路信用卡付款)後於取票期限內持會員身分證件正本至本局各車站售票窗口取票. 取票時,請向售票員說明為會員點數兌換車票即可.會員酬賓兌換之免費車票基本點數為500點,可兌換乘車區間100公里內乘車票一張。例如旅客已累積520點,則可用500 點兌換新竹=臺北區間(78.1公里)乘車票(不限車種)1張,其中剩餘之20點可繼續參加累積點數,原剩餘可兌里程 21.9公里(可兌換100公里-已兌換78.1公里)則視同放棄。


2017 年第 41 周記事

本周二哥與菁菁第一次段考, 考完讓他們輕鬆一下, 周末向補習班請假大家回鄉下. 已經好久沒有這樣大家一起回去了. 有時候也很後悔讓菁菁週日去補習. 談到我以前反對的補習, 唉, 這個教育, 算了, 投降.

姊姊連假返家帶回新身分證, 我週四中午午休時去新光銀行與元富證券幫她開戶, 打算開啟零股平台功能, 希望每月自動扣 3000 元長期買進台灣高股息存股, 只要每年過年時存入 40000 元,  就足夠扣一整年. 對於沒時間理財或不懂理財的人, 這種定期定額的存股術應該是最穩當了, 但是要越年輕開始越好, 正如巴菲特說的, 他們有足夠長的山坡可以滾下巨大的雪球.

由於卡努颱風以及東北季風共伴效應, 週五開始一連下了三天雨, 好像是梅雨季. 現在的天氣真是越來越奇怪了. 不過, 這周只上了三天班又放假了, 真好.

2017-10-29 補充 :

感謝網友留言提示, 長期投資台灣高股息在獲利與稅費方面似乎不如台灣 50, 因此可能要重新考慮了, 參考 :

股價才20出頭...「台灣高股息」放到第幾年才保證賺?這張圖告訴你
最適合上班族的ETF》為什麼0050比0056好?基金經理人沒說的3個真相

2017年10月16日 星期一

長距離低功耗無線通訊技術 LoRa

前陣子有網友詢問 LoRa 問題, 好像曾在哪裡看到此 RF 技術的介紹, 當時並沒有放在心上. 最近因為這個機緣, 連帶讓我想起之前買過的 nRF24L01 模組, 過去一周在採購的 LoRa 模組尚未收到之前, 我就先把玩了 nRF24L01 一番, 發現它雖然價格便宜 (一片約 30 元), 但是可能因為走較高的 2.4GHz 頻段以及使用 PCB 天線之關係, 傳輸距離太短, 僅 100 公尺不到, 而且無法穿越牆壁, 實用範圍屬於近距離通訊而已.

但 LoRa 技術就不同了, 由於先進的技術突破, 加上走 Sub-GHz 頻段, 因此傳輸距離與穿牆性能據說都令人刮目相看, 空曠地方可達 2km~5km, 在建築物內可穿透 7 層牆壁. 因此在測完 nRF24L01 後, 我想研究 LoRa, 實際測試看看是否真的那麼厲害.

目前市面上的 LoRa 模組主要是使用 SX1276 與 SX1278 這兩種晶片, 兩者性能與功能相同, 差別只是頻段不同而已. SX1276 頻段是 868MHz 與 915MHz, 主要用在歐洲與北美地區; 而 SX1278 頻段為 433MHz 與 470MHz, 主要用在中國, 東南亞, 南美洲, 以及東歐等地區, 參考 :

SX1276 与SX1278扩频芯片的区别

在露天拍賣上販售的大都是 SX1278 的模組. 我買的是這款 433MHz 的 (附彈簧天線) :

[史塔克實驗室][Arduino/RPi]每個180元2個一組販售SX1278 Lora module 模組433MHz $180

賣家有提供 Arduino 範例程式, 可在 dropbox 下載 :

https://www.dropbox.com/sh/beaswh5a3h69ho6/AABNL6J0KtAe9x_hfsz1P8SAa?dl=0

事實上這在 Aliexpress 上含運才賣 US$3.72 元而已, 折合台幣 111 元 :

433Mhz Lora SX1278 Long Range RF Wireless Module SPI Build-in Temperature Sensor For Arduino DRF1278F $3.72

注意, 這種裸片的模組大都沒有附底板或轉接板 (甚至沒有附彈簧天線), 而且其接腳間距為 2 mm, 因此必須找 2 mm 的排母來焊接, 例如 :

2.0MM 單排座1*40 單排母座 排針座(10只一拍) [1-89085] $70

我覺得最好是使用 2.0mm 的排母, 因為 2.0 mm 間距的排針較細, 一般杜邦線母接頭插上去根本就鬆垮垮的無法固定; 而 2.0 mm 排母的洞卻剛剛好可以讓一般杜邦線公插頭插進去沒問題.

如果要買有底板或轉接板, 可以焊接 2.54 mm 針腳的模組, 可以考慮下面這款 :

【傑森創工】SX1278 帶底板 LORA 模組 安信可 RA-02 Arduino $250

這塊使用的是安信可 (AI Thinker) 的 RA-02 SX1278 模組, 採用 IPEX 天線座而非短的彈簧天線, 而且有鐵殼包覆不會有電磁干擾問題 (FC/EC 相容認證). 其實這塊在 Aliexpress 一對含運才賣 US$13.5, 折合台幣 405 元, 平均一片才 203 元 :

Elecrow 2pcs/lot SX1278 LoRa Module 433M 10KM Ra-02 Ai-Thinker Wireless Spread Spectrum Transmission Socket for Smart Home DIY US$13.5

也有採用 nRF24L01 接腳配置 (8 Pins) 的模組, 一對含運 US$13.51, 折合台幣 405 元, 平均一片也是 203 元左右, 這款好處是免焊, 而且採用 IPEX 天線 :

2pcs/lot Newest SX1278 LoRa Module 433M 10KM Ra-02 Ai-Thinker Wireless Module Spread Spectrum Transmission Electronic Diy Kit  US$13.51

採用彈簧天線的是這款 :

Elecrow 2pcs/lot LoRa Module SX1278 Ai-Thinker 433M Wireless Spread Spectrum Transmission Ra-01 DIY Kit for Smart Meter Reading  US$13.59

如果買不含底板且搭配彈簧天線的 RA-01 模組的話更便宜, 一對 US$8.88, 折合台幣 266 元, 平均一片 133 元 :

2PCS Ra-01 SX1278 LoRa Spread Spectrum Wireless Module 433MHz Wireless Serial Port UART Interface Ra01 US$8.41+0.47=US$8.88

安信可的 Ra-02 模組通常都不附 IPEX 天線, 如果要買 IPEX 天線可參考 :

433m內置彈簧天線 433mhz模塊天線 433數傳天線 ipex介面 1.13線 $40
868MHZ/900MHz/915MHz/920MHz內置彈簧天線 無線數傳天線 高增益IPEX天線 $70
黑色868MHZ/900MHz/915MHz/920MHz/925MHz天線,SMA內針介面 $90

以上是關於模組採購的調查, 接下來整理一下 LoRa 技術的相關資訊.

LoRa 為 Low power long Range  (低功耗長距離) 的縮寫, 它是一種低功耗無線廣域網路 (LPWAN, Low Power Wide Area Network) 通訊技術, 最早源自法國長距離無線傳輸技術 IP 公司 Cycleo, 它在 2009 年提出了創新的低功耗長距離技術 LoRa, 於 2012 年被美國 Semtech 公司以 500 萬美元併購, 參考 :

Cycleo unveils its first innovative semiconductor IP bringing unprecedented range to wireless data transmission
Semtech Acquires Wireless Long Range IP Provider Cycleo

美商 Semtech (先科) 1960 年創立於美國加州, 是一家類比與混和信號 IC 供應商, 1967 年公開上市, 目前於 NASDAQ 掛牌 (SMTC), 市值約 25 億美元. 主力產品為電源管理晶片, 數位感測與高階通訊射頻 IC 等, 參見 :

https://en.wikipedia.org/wiki/Semtech
http://www.semtech.com/wireless-rf/internet-of-things/

Semtech 併購 Cycleo 後大力推廣 LoRa 技術, 結合全世界電信商, 設備商, 晶片商等組成非營利的 LoRa 聯盟, 目前會員數已超過 500 個, 包括 52 個電信運營商, 範圍橫跨全球 100 餘國, 超過 350 個城市正在測試與佈建 LoRaWan 網路, 近日 ( 2017-10-17 ~ 2017-10-25) 將於中國蘇州舉辦第九屆全球會員大會, 參考 :

https://www.lora-alliance.org
http://whatis.techtarget.com/definition/LoRa-Alliance

關於 LoRa 技術的特性摘要如下 :
  1. 採用線性 Chirp 展頻調變技術 (CSS, Chirp Spread Spectrum), 具低功耗, 長距離, 低成本, 可擴充, 與抗干擾等特性, 可使用電池長時間運作, 範圍可達數公里.  LoRa 省電的原因主要來自非同步通訊與自適應之傳輸速率功能. LoRa 節點的接收電流僅 10mA, 休眠電流 200nA, 因此 LoRa 技術的電池壽命高達 3~10 年.
  2. LoRaWAN 網路使用非同步方式通訊, 其媒介存取協定 (Media Access Protocol) 採用 ALOHA 法, 節點會依需要進入或長或短的休眠狀態, 從而降低了功率消耗; 而現行手機屬於同步通訊, 每 1.5 秒需與基地台同步一次, 功耗較大. 
  3. 在 LoRAWAN 中, 節點並不與特定閘道器 (Gateway) 相關聯, 而是與多個閘道器關聯, 所傳送之資料將被多個閘道器接收. 此外, LoRaWAN 閘道器具有容量高與可擴充特性, 可從大量節點接收數據, 這是其他 LPWAN 所欠缺的優點. 
  4. LoRa 具有網路與應用雙層安全防護, 網路節點不能檢視應用層數據, 並使用 AES 對傳輸之數據進行加密. 
LoRa 模組實測參考 :

Long Range Wireless Data Communicatoin using LoRa (Up to 10km Line of Sight)




參考 :

LoRa:長距離低功耗物聯網傳輸技術
LPWAN-大家天天熱聊的LORA技術到底是什麼?
智能家居無線技術解決方案:LoRa超遠距離無線通信
LPWAN:科技改變生活,淺談LoRa與物聯網技術
物聯網時代來臨,IBM 推 LoRa 技術讓機器也有自己的網路
新一代無線傳輸技術-LoRa
長距離無線通訊
5分鐘搞清楚LoRa技術是什麼
細說LoRa(一)——LoRa、LoRaWAN、LoRa聯盟的由來及簡介

2017年10月13日 星期五

Free Energy Light Bulbs 230v-using Magnet?

我在 youtube 看到下面這個多數人評為造假的影片, 看標題我也認為是假的, 小發電機不可能發出 230V, 而且永動機違反熱力學第二定律. 但怎麼看都看不出這影片的破綻, 難道視頻也有類似 P 圖的工具嗎?

https://www.youtube.com/watch?v=4rPWx8aaXjY




影片中使用壓電打火機 (piezo igniter) 產生的短暫電壓激勵馬達啟動, 讓內外兩個環形磁鐵因吸斥作用而持續轉動, 看起來似乎是非常完美的永動機, 但這個小小的電弧初始能量真的能產生比它還大得多的電能嗎? 能持續運轉下去嗎?

永動機與 free energy 儘管違反已知的物理定律, 但自古以來總是吸引許多人前仆後繼投入這種神奇機器的發明. 其實我的態度是很開放的, 從不以有限的所知知識侷限想像力, 因為我們對這宇宙的了解還是很有限, 這世界有無限種可能. 實驗是檢驗真理的唯一工具, 或許最後徒勞無功, 但有空的話倒是很想來複製影片中的實驗. 材料可在露天購得 :

壓電材料、壓電、點火器配件、打火機配件
釹鐵硼強力磁鐵 超強磁鐵 23*16*5 釤鈷磁鐵 超長磁鐵

另外還有一個類似的影片, 但它使用一顆電解電容來儲能, 參考 :

make free energy generator mini best of world 2017 new project




同樣底下留言也是一片 "fake!" "fakee!" "idiot!" 罵聲, 但就是沒看到有人捲起袖子複製實驗後再來罵. 人外有人, 天外有天, 最保險的態度就是保持 open-minded 的心胸, 避免成為見識淺薄的井底之蛙. 古典的熱力學在現代量子資訊理論衝擊下或許會有所變化也說不定. 科學就是永遠不排除有被推翻的可能.

關於永動機的探討, 參考 :

永動機有可能嗎?——《悖論:破解科學史上最複雜的9大謎團》
熱力學第二定律被打破:打造永動機或成為可能
愛因斯坦認為它永遠不會被推翻,如今被量子資訊理論逼到了死角

2017年10月11日 星期三

2017 年第 40 周記事

放了四天連假, 差點連周記都忘了寫了. 週三中秋只有一天就沒回鄉下了, 早上測試研究 nRF24L01 無線模組, 下午把後陽台壞掉的曬衣繩換新, 順便沖洗一下後陽台, 準備把洗衣機搬回來, 沖水後發現後陽台中間 3, 4 塊磁磚似乎低了約 1mm, 感覺有點積水. 聯絡師傅周四下午來看, 說要叫施工的人來修補. 這幾天想了一下, 反正只是一點點而已, 只要用拖把拖一下很快就乾了, 犯不著又再敲敲打打, 叫師傅不用再修補了.

姊姊週五晚上到高雄, 她說做統聯暈車不舒服, 我說以後都坐高鐵啦! 舒適又省時. 上回坐一次自強號覺得怎麼左右搖晃得厲害, 害我在火車上看書也不舒服, 以後我連台鐵都不想再坐哩. 週六與菁菁, 水某三人前往湖內明璜的魚塭參加同學會, 雖然天氣很熱, 但大家聊得好高興.

因為姐姐周一中午要參加同學會, 所以還是週日晚上就回高雄了. 其實本周在鄉下也沒時間做甚麼, 每周書都是帶去帶回, 帶安心地而已, 事實上只要帶一本書就夠了. 週日在鄉下圖書館找到下面這本好書 :

# 表裡日本, 蔡亦竹 (遠足文化出版)

作者為實踐大學應用日語系助理教授, 曾在日本留學生活多年, 對於日本歷史文化有深刻了解與研究. 本書以散文方式寫作, 雖然很有文化深度, 但讀來毫不費力氣, 我一天就看完了. 書中關於金閣寺的來龍去脈有精闢描寫, 原來足利義滿所建的金閣寺竟然與其架構一樣有三層特別的意義啊!

2017年10月8日 星期日

如何製做鉛酸電池

在 Youtube 看到下面這兩部尼泊爾人自製鉛酸電池的影片感到非常新奇, 原來鉛酸電池是這樣做出來的, 裡面所需要的部件大都是使用小坩鍋自行熔解廢鉛塊為液狀, 然後倒入模型中塑造再加工, 鉛是柔軟展性佳的有毒重金屬, 容易切割加工, 導電性低抗腐蝕性強, 熔點低約攝氏 328 度.

# Part 1 of 2 Local Battery Manufacturing in Nepalgunj




Part 2 of 2 Local Battery Manufacturing in Nepalgunj




現代電池工廠製造程序已高度自動化, 例如下面是美國 USA Batery 公司的深循環鉛酸電池的詳細製程 :

Deep Cycle Battery 101 manufacturing - OEM ending




鉛酸電池最早是 1859 年法國物理學家普蘭特 (Gaston Planté) 所發明, 由二氧化鉛當正極, 鉛板當負級, 防止正負極短路的中間隔板, 以及濃度 30~40% 的稀硫酸溶液組成. 普蘭特是巴黎工藝美術學院物理學教授, 早年曾在巴黎郊外發現了史前一種不飛鳥-冠恐鳥的化石, 這種已滅絕之大型不飛鳥便以其名字 Gaston 命名為加斯頓鳥. 另外, 月球上的普蘭特隕石坑也是因為他發明鉛酸電池的偉大貢獻而以其姓氏命名. 參考 :

https://baike.baidu.com/item/普兰特/15385934

鉛酸電池一格標稱電壓為 2V, 可充電到 2.4V, 放電至 1.5V. 通常由六格鉛酸電池串聯組成標稱電壓 12V 的模組. 鉛酸電池具有內阻低, 供電流大特性, 但過充時會產生易燃的氫氣, 須注意排氣避免爆炸之危險. 長期低電量會使電池壽命縮短, 使用久了之後電池極板上會有硫酸鉛結晶使蓄電量降低, 可用脈衝式充電加以消除.

鉛酸電池正負極的二氧化鉛與鉛會與稀硫酸電解液產生硫酸鉛溶液與水而放電; 當施予電壓時, 硫酸鉛溶液與水又會還原成二氧化鉛, 鉛與稀硫酸溶液, 這個充放電的電化學反應是可逆的. 參考 :

https://zh.wikipedia.org/wiki/铅酸蓄电池

其反應原理可參考下列影片 :

Working Principle of Lead Acid Battery




參考 :

DIY 一個成本 20 元的 360W 充電器 (1000W 亦同
Lead-acid storage battery
大學教授把垃圾場金屬變成自製的超級電池!

2017年10月7日 星期六

魚塭同學會

老同學兩周前在群組邀同學會, 這次是在湖內的魚塭舉辦烤肉, 雖然周末要回鄉下, 但因為從未到過湖內區, 而且大家要聚在一起也不容易, 所以我考慮了一周, 前天才回覆要參加. 原本想載爸一起去, 但他說對烤肉沒興趣, 所以就只有我, 水某, 菁菁三人. 姊姊昨晚雖回到高雄, 但今日要跟同學去高美館聽演講不能去; 而二哥則是要去學校上 APCS 課, 早上載他去學校後, 順路上國一到路竹交流道下去, 大約 40 分鐘.

同學家的魚塭在湖內湖中路尾, 那裏一眼望去都是魚塭實在很難找, 只有巷沒有門牌號碼, 我繞了一圈才找到. 我們是第二早到, 馬上開始準備烤肉, 等其他同學一來即可享用. 這次感覺比上回去大仁哥餐廳要多一些, 畢業後至今才見面的過敏也遠從桃園下來, 其他都是留在南部的老班底, 從早上邊吃邊喝邊聊天, 直到下午四點才快樂賦歸, 今天實在太高興了!

Arduino 無線傳輸模組 NRF24L01 測試

最近有網友詢問 LoRa 無線傳輸問題, 讓我想起以前也買過一款很便宜的無線模組 NRF24L01, 清查採購紀錄發現我買過兩組 (4 個模組), 但是一直都沒拿來測試過, 也不知是否能正常運作. 兩年前買大約 40 元一片, 只要 30 元就買得到. 參考 :

露天 XLAN 電子零件購買清單
[X-LAN] Arduino NRF24L01+ 功率加強版 2.4G 無線模組 $35
向露天賣家盼盼購買零件模組一批
【盼盼105】 NRF24L01+ 功率加強版 遠距離 2.4G 無線收發模組 Arduino 實驗用 $35
# 購買電子零件 (柏益 boyi101)
NRF24L01+ 功率加強版 24L01 2.4G 無線模組 台灣 IC 距離比原廠遠1倍 $30

注意, 現在有極低價才 17~20 元一片的貼片式模組 (一坨圓圓看不到 IC 的)  :

# (快速發貨已含稅)超薄款 類NRF24L01 2.4G無線模組 1.27MM間距 貼片 $17
類NRF24L01+ 2.4G無線模組 2.54MM間距 w2 $19
[37975] 2.4G 模組 貼片NRF24L01+模組 超小體積 w2 $20

這些便宜模組只是 "類" NRF24L01 而已, 不是 Nordic 的晶片, 採用的是中國製仿製品 BK2425, Arduino 程式無法完全通用, 須使用特別的函式庫 RMF7x, 被玩家評為 "Worst of the worst" 的產品, 參考 :

# library for ones of the worst of chinese nRF24l01+ "alternatives"

以下測試我參考了下面幾本書 :
  1. 用 Arduino 全面打造物聯網, 孫駿榮 (碁峰)
  2. Arduino 完全實戰手冊, 王冠勳譯, 博碩
NRF24L01 是 Nordic  開發的高度集成低功耗的 20 針腳 RF 無線傳輸收發晶片 (Transceiver), 運作在免執照的 2.4G ISM (Industrial, Scientific, Medical) 頻段, 屬於 VHF (S Band) 頻段 (1~2GHz 為 L Band HF, 2~4GHz 為 S Band VHF, 4~8GHz 為 C Band UHF), 可使用 2.4GHz ~ 2.525 GHz 的 126 個頻段 (即每頻段間隔 1MHz), 其分布如下 :

頻段 0 => 2400 MHz (RF24 頻段 1)
頻段 1 => 2401 MHz (RF24 頻段 2)
....
頻段 76 => 2476 MHz (RF24 頻段 77) 預設頻段
....
頻段 83 => 2483 MHz (RF24 頻段 84)
....
頻段 124 => 2524 MHz (RF24 頻段 125)
頻段 125 => 2525 MHz (RF24 頻段 126)

參考 RF24.cpp 第 680 行可知預設頻段是 76 :

  // Set up default configuration.  Callers can always change it later.
  // This channel should be universally safe and not bleed over into adjacent
  // spectrum.
  setChannel(76);    

nRF24L01 的每個頻段有 6 個通道 (Pipe), 亦即允許 6*126=756 個設備同時收發互不干擾, 最高傳輸速率 2Mbps. VCC 工作電壓 1.9~3.6V, 但其他接腳可與 5V 系統的微控器如 Arduino 等直接相連, 不需使用位準轉換器. 超低功耗設計在發射模式下發射功率 6dBm 時電流消耗為 9.0mA, 接收模式為 12.3mA, 比一顆 LED 耗電還低, 可使用電池供電.

NRF24L01 在空曠地區 (無遮蔽物), 以 250KPBS 速率傳輸可達 180~240 公尺, 但在室內有牆等遮蔽情況, 由於 2.4GHz 微波的繞射與穿透能力弱, 大概只能穿透一面牆, 最大發射功率下只能傳遞 5~10 公尺遠. 詳細規格參考官網 :

http://www.nordicsemi.com/eng/Products/2.4GHz-RF/nRF24L01

NRF24L01 模組採用 SPI 介面可與 Arduino 等微控器介接, 其接腳配置如下 :




接腳說明如下 :

 接腳 說明
 VCC 3.3V
 GND Ground
 CE Chip Enable Tx/Rx
 CSN Chip Select Node
 SCK SPI ClocK
 MISO  Master In Slave Out (Send)
 MOSI Master Out Slave In (Receive)
 IRQ Interrupt ReQuest

其中比較重要的接腳是 CE 與 CSN, CE 是用來控制 nRF24L01 是在 Standby/Active 模式; 而 CSN 則是用來告訴 nRF24L01 所傳送的是 SPI 指令還是要送出去的資料.

其實這種 2*4 的接腳與 ESP8266 ESP-01 模組是一樣的, 所以上回為 ESP-01 製作的轉接板也可以用在 NRF24L01, 參考 :

製作 ESP-01 模組轉接板




特別注意, NRF24L01 的 VCC 最高允許 3.6V, 不可施加 5V 電源, 否則有燒毀之虞, 通常運作於 3.3V. 不過除 VCC 外的接腳卻可接受 5V 位準, 故可與 Arduino 直接相連沒問題.

SPI (Serial Peripheral Interface) 是源自 Motolora 的全雙工同步資料傳輸協定, 可以讓微控器與多個周邊裝置進行短距離高速通訊, 常用於 SD 卡或 LCD 螢幕等周邊模組. SPI 是一種基本上為四線制的主從式架構, 其中微控器通常當主設備 (Master), 透過 /CSN (或 /SS, Slave Select) 接腳控制互連的周邊從設備 (Slave), 由於採用硬體連線方式選擇, 因此每多一個從設備時, 主設備就需要多一個輸出腳去控制, 若有 n 個從設備, 則主設備需要 n+3 支腳與所有從設備相連接, 但可用解碼器節省 GPIO 腳. 反觀 I2C 則是採用軟體方式選擇 (協定之第一個 byte), 不論多少從設備只需三條線.

Source :Wiki

當 /CSN 或 /SS 為低準位時, 該 Slave 設備即被主設備選定可與其通訊, 同一時間只有一個 /CSN 腳會被主設備拉到低準位. SPI 資料傳輸是透過 MISO (Master In Slave Out) 與 MOSI (Master Out Slave In) 這兩支腳, 兩端資料傳輸是利用 SCLK (或 SCK) 時脈來進行同步.  參考 :

序列周邊介面 (SPI)
認識UART、I2C、SPI三介面特性
SPI (Serial Peripheral Interface) 串列 (序列) 週邊介面
成大資工 Wiki : SPI

以下測試我使用 Arduino Nano 當微控器, Arduino Nano/UNO/Pro Mini 這四款板子的 NPU 為 ATMEGA328P, 內建 SPI 介面, 具有特定 SPI 硬體接腳如下 :

 Nano/UNO 功能 說明
 D10 /SS Slave Select
 D11 MOSI Master In Slave Out
 D12 MISO Master Out Slave In
 D13 SCK Sychronous Clock

參考 :

http://www.pighixxx.com/test/pinouts/boards/nano.pdf

根據下面這篇文章說明, nRF24L01 模組的 (CE, CSN) 可以接 Arduino 的任何 DIO 腳, 但 RF32.h 函式庫建議 Arduino 應使用 (D7, D8) 連接 (CE, CSN), 因此以下測試中不會使用 D10 當 CSN 使用. 另外 nRF24L01 的中斷 IRQ 可接可不接, Arduino 有兩個硬體中斷 : INT0 (D2) 與 INT1 (D3), 要接的話可使用 INT0. 總結硬體接線如下 :




 nRF24L01 接腳 Arduino 接腳
 VCC 3.3V
 GND GND
 CE D7
 CSN D8
 SCK D13
 MISO D12 (MISO)
 MOSI D11 (MOSI)
 IRQ D2 (INT0) (可不接)


註 : 樹莓派則是使用 GPIO(22, 8), GPIO 連接器編號 (15, 24)

在軟體方面, Arduino 的 SPI.h 函式庫提供 setDataMode(), begin(), end(), transfer() 等函式來進行 SPI 通訊, 參考 :

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

不過在操作 NRF24L01 時不必直接處理 SPI 協定, 因為已經有人將 NRF24L01 的 SPI 操作寫成函式庫 RF24, 可在 Github 按 "Clone or Download/download ZIP" 下載 (RF24-master.zip), 解壓縮後放在 Arduino IDE 安裝目錄的 libraries 子目錄下 :

https://github.com/nRF24/RF24

然後在程式中匯入 SPI, RF24, 與 nRF24L01 三個函式庫即可 :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

使用 RF24 函式庫首先要建立一個 RF24 物件, 傳入參數 (CE, CSN) 指定 nRF24L01 模組的 CE 腳與 CSN 腳與 Arduino 的哪一個腳位互連, 在 RF32.h 的第 1426 列有指定各種板子的適當接腳, 對於 UNO/Nano/Pro Mini 這三種板子應該指定 (7, 8) :

RF24 radio(7, 8); // (CE, CSN) 建立 RF24 物件 radio

其次是要宣告一個長度為 6 的字元陣列以儲存 nRF24L01 的節點位址, 並在傳送端用 radio.openWritingPipe() 函數指定 nRF24L01 節點位址以便寫入資料;

char node_address[6]='00001';
radio.openWritingPipe(node_address);

參數 node_address 可以是任意 5 個字元組成的字串, 例如 '00001', '1node' 等, 但在宣告字元陣列以儲存位址字串時必須多 1 個 byte 儲存結尾字元 \0, 故長度為 6. 注意, 使用 Multi-ceiver 星狀網路架構時每一個傳送板位址的第一個 byte 必須不同才能識別, 例如要用 "10000" 與 "20000", 不要用 "00001" 與 "00002", 因為第一個 byte 都是 '0' 無法區別, 詳見測試 4.

接著在接收板這一端則須用 radio.openReadingPipe() 函數指定 nRF24L01 節點位址並綁定通道編號以便讀取從傳送端收到的資料 :

radio.openReadingPipe(pipe_num, node_address);

第一參數 pipe_num 為 0~5, 最多只能 6 個通道 (位址), 一對一送收情況下綁定哪一個通道無礙接收, 但在 Multi-ceiver 星狀結構下, 通道就代表了所綁定之位址, 不能共用通道. 第二參數 node_address 為接收端 nRF24L02 的節點位址, 注意, 此位址必須與傳送端之位址相同才能進行通訊.

設定傳輸速率可使用 setDataRate(speed), 傳入參數有四種速率可選 :
  1. RF24_250KBPS (250kbs)
  2. RF24_1MBPS (1Mbps)
  3. RF24_2MBPS (2Mbps)
要取得目前的速率設定可呼叫 getDataRate() 函數.

傳送資料是呼叫 write(text, sizeof(text)) 函數將字串 text 傳送出去. 注意, nRF24L01 一次最多只能傳送 32 個 bytes, 超過的會被切斷丟棄. 如果要傳送多於 32 bytes 資料必須自己弄個協定讓接收板在收到每筆 32 bytes 之資料後重新組合還原為原來的資料, 參考 :

NRF24L01 XN297L 無線網路 區域遠距傳輸

更多函數用法 參考 :

# RF24 函式庫 API 

以下的測試主要是參考了下面這篇加以修改 :

Arduino Wireless Communication – NRF24L01 Tutorial

不過與原作有 2 個不同之處, 其一是此篇使用 Arduino Mega + nRF24L01 當 sender 每秒送出 "Hello World" 字串, 以及 Arduino Nano + nRF24L01 當 receiver 接收此字串. 我則是兩邊都使用 Arduino Nano. 其二是我在發送端使用了 sprintf() 來將計數器整數嵌入字元串列中, 這樣開啟序列埠監控視窗觀察接收端訊息時就可以看到不同的輸出字串, 關於 sprintf() 用法參考 :

How to convert integer to string in C?


測試 1 : 兩個 NRF24L01 一送一收成對傳送訊息 (程式分傳送與接收)

Sender (發送端程式) :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6] = "00001";  //節點位址為 5 bytes + \0=6 bytes

int counter=0;  //Hello 計數器
void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  radio.openWritingPipe(address);  //開啟寫入管線
  radio.setPALevel(RF24_PA_MIN);   //設為低功率, 預設為 RF24_PA_MAX
  radio.stopListening();  //傳送端不需接收, 停止傾聽
  }
void loop() {
  const char text[32];  //宣告用來儲存欲傳送之字串
  sprintf(text, "Hello World %d", counter);  //將整數嵌入字串中
  Serial.println(text);
  radio.write(&text, sizeof(text));   //將字串寫入傳送緩衝器
  ++counter;
  delay(1000);
  }

Receiver (接收端程式) :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6] = "00001";  //節點位址為 5 bytes + \0=6 bytes

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  printf_begin();  //初始化 RF24 的列印輸出功能
  radio.openReadingPipe(0, address);  //開啟 pipe 0 之讀取管線
  radio.setPALevel(RF24_PA_MIN);  //設為低功率, 預設為 RF24_PA_MAX
  radio.startListening();  //接收端開始接收
  radio.printDetails();  //印出 nRF24L01 詳細狀態
  Serial.println("NRF24L01 receiver");
  Serial.println("waiting...");
  }
void loop() {
  if (radio.available()) {  //偵測接收緩衝器是否有資料
    char text[32] = "";   //用來儲存接收字元之陣列
    radio.read(&text, sizeof(text));  //讀取接收字元
    Serial.println(text);
    }
  }

注意接收端程式多匯入了 RF24.h 的輸出函數 printf.h, 這是在呼叫 radio.printDetails() 必須用到的, 否則 printDetails() 將不會輸出訊息.




在空曠無阻礙物環境下測試, 最低功率時 (RF24_PA_MIN) 實測傳輸距離僅約 5~7 公尺 (與藍芽差不多), 而最大功率時 (RF24_PA_MAX) 則可達 70~90 公尺之遠, 且信號微弱處與天線指向性有關. 顯然號稱 100 公尺實際上大概要打八折. 功率放大器設定函數 setPALevel() 總共有四種功率放大器 PA (Power Amplifier) 可選 :
  1. RF24_PA_MIN  (最小功率 -12dB)
  2. RF24_PA_LOW (低功率 -12dB)
  3. RF24_PA_HIGH (高功率 -6dB)
  4. RF24_PA_MAX (最大功率 0dB)
參考 :

關於nrf2401的傳輸距離 #7
nRF24L01無線傳輸使用心得

接收端序列埠監控視窗輸出如下 :

NRF24L01 receiver
waiting...
STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3130303030 0xc2c2c2c2c2
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0xe7e7e7e7e7
RX_PW_P0-6 = 0x20 0x00 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x4c
RF_SETUP = 0x07
CONFIG = 0x0f
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
NRF24L01 receiver
waiting...
Hello World 0
Hello World 1
Hello World 2
Hello World 3
Hello World 4
Hello World 5
Hello World 6
Hello World 7
Hello World 8
Hello World 9
Hello World 10
Hello World 11
Hello World 12
Hello World 13
Hello World 14
Hello World 94
Hello World 99

注意, 雖然 Arduino 的 D9 被定義為 SPI 的 /SS 腳, 但是若在接收程式中指定 (9, 10) 作為 (CE, CSN) 將無法正常運作; 但在 sender 程式中卻沒問題, 不知原因為何?  比較安全的方法還是一律用 D7 與 D8 為宜.

如果要增加傳輸距離, 則要改用下面這種有外加天線設計的, 號稱可遠達 1.1 公里 :

T58 ~1100米遠距離 NRF24L01 PA LNA的無線模塊,送天線 $112

或者加焊延長天線, 參考 :

nRF24L01無線傳輸使用心得


上面測試 1 是傳送與接收程式不同, 必須在不同角色的模組上打上標記才知道這是傳送板還是接收板, 這樣很麻煩. 下面測試 2 改為使用 Arduino 的 D4 腳設定角色, 1 為接收板 (預設), 0 為傳送板, 這樣程式只要一套即可, 如下測試 2 所示 :


測試 2 : 兩個 NRF24L01 一送一收成對傳送訊息 (單一程式用 D4 決定送收)

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[][6] = {"00001","00002"}; //兩個節點位址,一個傳送,另一個接收
bool role=1; //1=sender (default), 0=receiver

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  printf_begin();  //初始化 RF24 的列印輸出功能

  radio.setPALevel(RF24_PA_MAX);  //設為高功率 (預設)
  pinMode(4, INPUT_PULLUP);  //D4=模式開關 (預設=1:傳送模式)
  role=digitalRead(4);  //讀取 D4 準位決定接收 or 傳送模式 (default)
  if (role==1) { //=1:傳送模式
    radio.stopListening();  //傳送模式:停止傾聽
    radio.openWritingPipe(address[1]); //使用位址 '00002'
    radio.openReadingPipe(0,address[0]); //pipe 0:使用位址 '00001'
    Serial.println("NRF24L01 sending...");
    }
  else { //=0:接收模式
    radio.startListening();  //接收端開始接收
    radio.openWritingPipe(address[0]);  //使用位址 '00001'
    radio.openReadingPipe(0,address[1]);  //pipe 0:使用位址 '00002'
    Serial.println("NRF24L01 receiving...");
    }
  radio.printDetails();
  }
void loop() {
  if (role == 1) { //傳送模式
    const char text[32];  //宣告用來儲存欲傳送之字串
    unsigned long us=micros();  //取得啟動後之微秒數
    sprintf(text, "Hello World %lu", us);  //將整數嵌入字串中
    Serial.println(text);
    if (!radio.write(&text, sizeof(text))) {  //將字串寫入傳送緩衝器
      Serial.println("Sending failed");
      }
    delay(1000); 
    }
  if (role == 0) { //=0:接收模式
    uint8_t pipe_num;   //通道號碼
    if (radio.available(&pipe_num)) {  //偵測接收緩衝器是否有資料
      char text[32] = "";   //用來儲存接收字元之陣列
      radio.read(&text, sizeof(text));  //讀取接收字元
      Serial.print("Pipe num=");  //顯示從哪一通道接收
      Serial.print(pipe_num);
      Serial.print(" ");
      Serial.println(text);  //顯示接收資訊
      }
    }
  }

此二合一程式中, 使用 role 變數來決定 nRF24L01 板子是做傳送板還是接收板, 預設是 1 為傳送板, 但在 setup() 中會去偵測 Arduino 的 D4 腳位準, 若為 0 (LOW) 為接收板; 否則為傳送板. 由於 D4 有啟動上拉電阻, 因此預設就是傳送板, 只有要當接收板時才需要將 D4 接地.

其次是讀寫位址部分, 此程式與測試 1 不同之處在於使用了 '00001' 與 '00002' 兩個位址, 當作為傳送板時使用 '00002' 位址寫入通道, 對方接收板也是用 '00002' 位址讀取通道; 而 '00001' 位址則是傳送板之讀取位址或接收板之寫入位址. 其實不管是傳送板或接收板, 在上面程式中都是使用 '00002' 位址, 使用兩個位址旨在說明不論是運作在哪一模式, 都可以同時開啟寫入與讀取通道, 因為 SPI 是全雙工的通訊協定.

此程式使用 micro() 函數傳回的開機後的微秒時戳來取代測試 1 中的 counter 功能, 由於是 unsigned long 型態, 所以在用 sprintf() 將時戳嵌入字串中時, 必須改用 'ul' 格式才行.  另外在接收模式中, 新增了 pipe_num 變數, 用來在呼叫 radio.available() 時取得讀取通道之編號.

傳送板序列埠監控視窗輸出訊息 :

NRF24L01 sending...
STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3130303030 0xc2c2c2c2c2
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0x3230303030
RX_PW_P0-6 = 0x20 0x00 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x4c
RF_SETUP = 0x07
CONFIG = 0x0e
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
Hello World 392940
Sending failed
Hello World 1445444
Sending failed
Hello World 2477536
Sending failed
Hello World 3509600
Sending failed
Hello World 4541664
Sending failed
Hello World 5573728
Sending failed
Hello World 6605792
Sending failed
Hello World 7637864
Sending failed

很奇怪的是, 雖然資料實際上有傳送成功, 但 radio.write() 的傳回值卻都是 0, 導致印出 "Sending failed". 參考函式庫原始碼 RF24.cpp 810~844 行的 write() 函數, 傳送成功應該傳回 1, 失敗傳回 0, 但不知為何傳送沒問題卻傳回 0

接收板序列埠監控視窗輸出訊息 :

NRF24L01 receiving...
STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3230303030 0xc2c2c2c2c2
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0x3130303030 
RX_PW_P0-6 = 0x20 0x00 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x4c
RF_SETUP = 0x07
CONFIG = 0x0f
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
Pipe num=0 Hello World 208899772
Pipe num=0 Hello World 209931904
Pipe num=0 Hello World 210964028
Pipe num=0 Hello World 211996152
Pipe num=0 Hello World 213028276
Pipe num=0 Hello World 214060400
Pipe num=0 Hello World 215092524
Pipe num=0 Hello World 216124648
Pipe num=0 Hello World 217156772
Pipe num=0 Hello World 218188896
Pipe num=0 Hello World 219221020
Pipe num=0 Hello World 220253144

上面兩個測試都是靠 Arduino Nano 上的 TX 燈閃爍與序列埠監控視窗觀察無線傳輸情況是否正常, 接下來要做個比較有感的遙控測試, 我在傳送板上加裝一個按鈕連接到 Arduino 的 D3 腳; 另外在接收板上加裝一個蜂鳴器, 同樣連接到 D3 腳, 當按下傳送板的按鈕時, 接收板的蜂鳴器會發出嗶聲. 我將測試 2 的二合一程式修改如下 :


測試 3 : 兩個 NRF24L01 一送一收成對傳送訊息 (按鈕遠端控制蜂鳴器)

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[][6] = {"00001","00002"}; //兩個節點位址,一個傳送,另一個接收
bool role=1; //1=sender (default), 0=receiver

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

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  printf_begin();  //初始化 RF24 的列印輸出功能

  radio.setPALevel(RF24_PA_MAX);  //設為低功率, 預設為 RF24_PA_MAX
  pinMode(4, INPUT_PULLUP);  //D4=模式開關 (預設=1:傳送模式)
  role=digitalRead(4);  //讀取 D4 準位決定接收 or 傳送模式 (default)
  if (role==1) { //=1:傳送模式
    pinMode(3, INPUT_PULLUP);  //D2=按鈕開關
    radio.stopListening();  //傳送模式:停止傾聽
    radio.openWritingPipe(address[1]); //使用位址 '00002'
    radio.openReadingPipe(0,address[0]); //pipe 0:使用位址 '00001'
    Serial.println("NRF24L01 sending...");
    }
  else { //=0:接收模式
    pinMode(3, OUTPUT);  //D3=接蜂鳴器
    radio.startListening();  //接收端開始接收
    radio.openWritingPipe(address[0]);  //使用位址 '00001'
    radio.openReadingPipe(0,address[1]);  //pipe 0:使用位址 '00002'
    Serial.println("NRF24L01 receiving...");
    }
  radio.printDetails();
  }
void loop() {
  if (role == 1) { //傳送模式
    const char text[10];  //宣告用來儲存欲傳送之字串   
    if (digitalRead(3)==LOW) {sprintf(text, "beep");}
    else {sprintf(text, "none");}
    Serial.println(text);
    if (!radio.write(&text, sizeof(text))) {  //將字串寫入傳送緩衝器
      Serial.println("Sending failed");
      }
    delay(1000); 
    }
  if (role == 0) { //=0:接收模式
    uint8_t pipe_num;
    if (radio.available(&pipe_num)) {  //偵測接收緩衝器是否有資料
      char text[10]="";   //用來儲存接收字元之陣列
      radio.read(&text, sizeof(text));  //讀取接收字元
      Serial.print("Pipe num=");
      Serial.print(pipe_num);
      Serial.print(" ");
      Serial.println(text);
      if (strcmp(text, "beep")==0) {alarmBeep(3);}
      }
    }
  }

此程式中 D3 腳在傳送板是接按鈕後接地並開啟上拉電阻, 因此沒有按下時狀態是 HIGH 送出 "none" 字串; 按鈕按下時為 LOW 傳送出 "beep" 字串. 在接收板 D3 被設為輸出腳外接一個無源蜂鳴器後接地, 當收到傳送板送來的字串是 "beep" 時便呼叫自訂的 alarmBeep() 函數, 利用 Arduino 的 tone() 函數發出 PWM 脈波產生 "嗶" 聲.

注意, 這裡使用了 strcmp() 函數來比較字串是否為不標字串, 當字串相等時傳回 0, 否則傳回 1 (大於) 或 -1 (小於), 參考 :

字串比較函數範例 strcmp
# 字串的比較、尋找、代換、分解與結合

當然也可以使用 ==  或 equals() 去比對, 但是這兩個運算對象都是 String 類型資料, 必須先將 char 陣列用 String() 轉型才能通過編譯 :

if (String(text.equals("beep")) {alarmBeep(3);}
if (String(text)=="beep") {alarmBeep(3);}

https://www.arduino.cc/en/Reference/StringEquals
https://www.arduino.cc/en/Reference/StringComparison




傳送板序列埠輸出訊息如下 :

NRF24L01 sending...
STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3130303030 0xc2c2c2c2c2
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0x3230303030
RX_PW_P0-6 = 0x20 0x00 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x4c
RF_SETUP = 0x07
CONFIG = 0x0e
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
none
Sending failed
none
Sending failed
none
Sending failed
none
Sending failed
none
Sending failed
none
Sending failed
none

接收板序列埠輸出訊息如下 :

NRF24L01 receiving...
STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3230303030 0xc2c2c2c2c2
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0x3130303030
RX_PW_P0-6 = 0x20 0x00 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x4c
RF_SETUP = 0x07
CONFIG = 0x0f
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
Pipe num=0 none
Pipe num=0 none
Pipe num=0 none
Pipe num=0 beep     (蜂鳴器發出嗶聲)
Pipe num=0 beep     (蜂鳴器發出嗶聲)
Pipe num=0 none
Pipe num=0 none
Pipe num=0 beep     (蜂鳴器發出嗶聲)
Pipe num=0 none
Pipe num=0 beep     (蜂鳴器發出嗶聲)
Pipe num=0 none


接下來要測試 nRF24L01 最強的星狀網路功能, 稱為 Multi-ceiver, 可支援 1 對 6 的星狀網路通訊, 亦即一個節點當 Hub receiver (又稱為 PRX : primary receiver), 其他子節點當傳送器 (又稱PTX : transmitter nodes), 可應用在如水位, 噪音, 溫度, 濕度, 含氧量等環境資訊的蒐集上. 注意, 雖然是星狀網路架構, 但 Hub 其實還是依序一次與一個子節點通訊, 而且每個子節點位址必須不同.

Source : Instructable


這張圖清楚地說明了 nRF24L01 的 Multi-ceiver  架構, 這是 125 個頻段中的一個頻段 (寬度 1MHz ), 每個頻段至多支援 6 個通道 (Pipe), 每個通道須綁定獨一無二的地址以便識別至多 6 個子節點. 注意, 在此架構下 PRX Hub 雖然是當作接收板用, 但它也是可以隨時切換至傳送模式傳送資料給子節點 (一次一個節點), 因為 SPI 協定是全雙工的.

在下面的測試 4 中我準備了三組 Arduino Nano+nRF24L01 板, 其中一組當接收 Hub, 其他兩組當傳送子節點, 兩個傳送板具有不同的位址, 在接收板上這兩個位址被綁定到不同通道 (pipe) 上, 不過這三塊 nRF24L01 都是在同一個頻段上通訊 (共有 126 個頻段). 選擇頻段要用 RF24 物件的 setChannel() 方法 :

radio.setChannel(channel_number);

我將上面測試 1 的程式擴充為如下面測試 4 的三個程式 :


測試 4 : 三個 NRF24L01 兩送一收 (Multi-ceiver)

傳送板 1 程式 : 

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6] = "1node";  //節點位址為 5 bytes + \0=6 bytes

int counter=0;  //Hello 計數器
void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  radio.setPALevel(RF24_PA_MAX);   //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108); //設定頻道=108 (0~125, 較高的頻道似乎比較 open)
  radio.openWritingPipe(address);  //開啟寫入管線
  radio.stopListening();  //傳送端不需接收, 停止傾聽
  }
void loop() {
  const char text[32];  //宣告用來儲存欲傳送之字串
  sprintf(text, "Board 1 sending : %d", counter);  //將整數嵌入字串中
  Serial.println(text);
  radio.write(&text, sizeof(text));   //將字串寫入傳送緩衝器
  ++counter;
  delay(1000);
  }


傳送板 2 程式 :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6] = "2node";  //節點位址為 5 bytes + \0=6 bytes

int counter=0;  //Hello 計數器
void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  radio.setPALevel(RF24_PA_MAX);   //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108);  //設定頻道=108 (0~125, 較高的頻道似乎比較 open)
  radio.openWritingPipe(address);  //開啟寫入管線
  radio.stopListening();  //傳送端不需接收, 停止傾聽
  }
void loop() {
  const char text[32];  //宣告用來儲存欲傳送之字串
  sprintf(text, "Board 2 sending : %d", counter);  //將整數嵌入字串中
  Serial.println(text);
  radio.write(&text, sizeof(text));   //將字串寫入傳送緩衝器
  ++counter;
  delay(1000);
  }

接收板程式 :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[][6]={"1node","2node"};  //節點位址為 5 bytes + \0=6 bytes

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  printf_begin();  //初始化 RF24 的列印輸出功能
  radio.setPALevel(RF24_PA_MAX);  //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108);  //設定頻道=108 (0~125, 較高的頻道似乎比較 open)
  radio.openReadingPipe(0, address[0]);  //開啟 pipe 0 之讀取管線
  radio.openReadingPipe(1, address[1]);  //開啟 pipe 1 之讀取管線
  radio.startListening();  //接收端開始接收
  radio.printDetails();  //印出 nRF24L01 詳細狀態
  Serial.println("NRF24L01 receiver");
  Serial.println("waiting...");
  }
void loop() {
  uint8_t pipe_num;   //通道號碼
  if (radio.available(&pipe_num)) {  //偵測接收緩衝器是否有資料
    char text[32] = "";   //用來儲存接收字元之陣列
    radio.read(&text, sizeof(text));  //讀取接收字元
    Serial.print("Pipe num=");  //顯示從哪一通道接收
    Serial.print(pipe_num);
    Serial.print(" ");
    Serial.println(text);  //顯示接收資訊
    }
  }


注意, 上面的程式中, 兩個子節點的位址取名為 "1node" 與 "2node" (也可以用 "10000" 與 "20000"), 如果使用 "00001" 與 "00002", 或者 "node1" 與 "node2" 的話, 則 Hub 接收板將讀取不到兩個子板傳送過來的訊息. 這是因為 nRF24L01 在一個頻段 (1MHz 寬度) 的 6 個通道中定址時, 事實上只有 Pipe 0 與 Pipe 1 的位址 (5 個 bytes) 才會被完整儲存起來, 而 Pipe 2~5 只儲存第一個 byte, 其餘 4 個 byte 是跟 Pipe 1 借來補足的. 由於區別 6 個通道是靠獨一無二的位址, 因此對於位址的設定只要 6 個位址的第一個 byte 都不同就可以了, 這就是為何使用 "00001" 與 "00002" 不行, 而用 "10000" 與 "20000" 卻可以的原因了, 上面測試 1~3 因為是一對一位址都一樣, 所以不受影響.

參考 RF24.h 的第 268~277 行 :


   * @note Pipes 0 and 1 will store a full 5-byte address. Pipes 2-5 will technically
   * only store a single byte, borrowing up to 4 additional bytes from pipe #1 per the
   * assigned address width.
   * @warning Pipes 1-5 should share the same address, except the first byte.
   * Only the first byte in the array should be unique, e.g.
   * @code
   *   uint8_t addresses[][6] = {"1Node","2Node"};
   *   openReadingPipe(1,addresses[0]);
   *   openReadingPipe(2,addresses[1]);
   * @endcode


呼叫 startListening() 會列印出接收板詳細資料, 其中前四行中的 RX_ADDR 便顯示了 6 個通道的位址 :

STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x3030303031 0x3030303032
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0xe7e7e7e7e7

可見只有 Pipe 0/1 具有完整位址, Pipe 2~5 都只有第一個 byte, 其餘 bytes 是從 Pipe 1 借用, 因此 Pipe 2 的真實位址是 0x30303030c3, 而 Pipe 5 則是 0x30303030c6. 

接收板 (Hub) 之序列埠監控視窗輸出訊息如下 : 

STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x65646f6e31 0x65646f6e32
RX_ADDR_P2-5 = 0xc3 0xc4 0xc5 0xc6
TX_ADDR = 0xe7e7e7e7e7
RX_PW_P0-6 = 0x20 0x20 0x00 0x00 0x00 0x00
EN_AA = 0x3f
EN_RXADDR = 0x03
RF_CH = 0x6c
RF_SETUP = 0x07
CONFIG = 0x0f
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
NRF24L01 receiver
waiting...
Pipe num=0 Board 1 sending : 0
Pipe num=1 Board 2 sending : 0
Pipe num=0 Board 1 sending : 1
Pipe num=1 Board 2 sending : 1
Pipe num=0 Board 1 sending : 2
Pipe num=1 Board 2 sending : 2
Pipe num=0 Board 1 sending : 3
Pipe num=1 Board 2 sending : 3
Pipe num=0 Board 1 sending : 4
Pipe num=1 Board 2 sending : 4
Pipe num=0 Board 1 sending : 5
Pipe num=1 Board 2 sending : 5
Pipe num=0 Board 1 sending : 6
Pipe num=1 Board 2 sending : 6
Pipe num=0 Board 1 sending : 7
Pipe num=0 Board 1 sending : 8
Pipe num=0 Board 1 sending : 9
Pipe num=1 Board 2 sending : 9
Pipe num=0 Board 1 sending : 10
Pipe num=0 Board 1 sending : 11
Pipe num=1 Board 2 sending : 11
Pipe num=0 Board 1 sending : 12
Pipe num=0 Board 1 sending : 13
Pipe num=1 Board 2 sending : 13
Pipe num=0 Board 1 sending : 14
Pipe num=1 Board 2 sending : 14
Pipe num=0 Board 1 sending : 15
Pipe num=1 Board 2 sending : 15

可見 Hub 接收板上會依序收到兩個子節點傳送的資訊. 透過 Multi-ceiver 功能可以在 Hub 上先將蒐集之子節點資訊整理後再透過 WiFi 或乙太網傳送給雲端伺服器, 這樣可以減少路由器的負荷. 

接下來要測試一個更有趣的 Multiceiver 應用, 這是我在 Instructable 找到的範例, PTR Hub 先產生一個 0~10 的隨機數來給 PTX node 猜, 每個 node 在送出猜測數字給 Hub 後馬上切換為接收模式等候 Hub 傳送結果; 如果猜對的話, Hub 會切換成傳送模式將正確數字傳給猜對的那個 node, 若 node 在 200 ms 內收到 Hub 回應的正確數字, 表示可能答對了 (因傳輸也許會錯誤), node 將 Hub 回應之正確數字與自己送出的猜測數字比對, 符合的話就確認真的猜對了, 這時就會停止再送出新的猜測數字. 如果沒有再 200 ms 時限內收到回應, 那表示可能猜錯了, 就產生新的隨機猜測數字送出去. 參考 :

NRF24L01+ Multiceiver Network

我將原始程式改編為如下測試 5 :

測試 5 : 猜數字 (Multi-ceiver)

傳送板 (PTX node) 1 程式 :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6]="1node";  //節點位址為 5 bytes + \0=6 bytes
bool done=false;  //用來判斷是否要停止傳送猜測數字

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  radio.setPALevel(RF24_PA_MAX);   //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108); //設定頻道=108 (0~125, 較高的頻道似乎比較 open)
  radio.openWritingPipe(address);  //開啟寫入通道
  radio.openReadingPipe(0,address);   //開啟讀取通道 pipe 0 (接收 PRX Hub 答案)
  radio.stopListening();  //PTX node 暫時不需要接收, 停止傾聽
  randomSeed(analogRead(0));  //利用 A0 腳的隨機狀態設定偽隨機序列種子
  }
void loop() {
  if (!done) {  //還沒猜對就繼續猜
    byte gnumber=(byte)random(11);  //產生 0~10 之隨機猜測數字 
    Serial.print("Guess number=");
    Serial.print(gnumber);
    Serial.print("...");
    //傳送猜測數字 (1 byte) 給 PTR Hub
    if (!radio.write(&gnumber,1)) {Serial.println("Sending failed");}  //傳送失敗
    else { //傳送成功
      Serial.println("Sending OK");
      radio.startListening();  //切換到接收模式等待 PTR Hub 回應
      unsigned long startTimer=millis(); //開啟計時器等候 200ms
      bool timeout=false;  //計時器逾時旗標,預設未逾時
      while (!radio.available() && !timeout) { //尚未收到回應且旗標為未逾時
        if (millis()-startTimer > 200 ) {timeout=true;} //等候回應直到逾時或收到回應
        }
      //未收到回應,可能猜錯了
      if (timeout) {Serial.println("Last guess may be wrong, try again.");}
      else {  //未逾時且收到回應,可能答對了
        byte rnumber;  //儲存 PTR Hub 傳來的回應數字
        radio.read(&rnumber,1);  //讀取 PTR Hub 傳來的回應數字 (1 byte)
        if (gnumber==rnumber) {  //由 PTR 回應確認猜對了
          Serial.println("You guess right!");
          done=true;  //猜對了就結束猜測
          }
        else {Serial.println("Something went wrong, keep guessing.");}
        }
      radio.stopListening();  //切回傳送模式
      }
    }
  delay(1000);
  }

此程式節點位址為 "1node", 在 setup() 中同時開啟讀取與寫入通道, 但因為主要是作為傳送用途, 因此先將接收模式關閉, 同時利用 A0 腳上隨機的漂移電壓呼叫 randomSeed() 設定偽隨機序列種子.

進入迴圈後先判斷是否已猜中 (done==TRUE), 若未猜中就呼叫 random() 產生一個 0~10 的隨機數 gnumber, 因為 random() 傳回值是 long, 故要強制轉型為 byte.  關於 randomSeed() 與 random() 參考 :

https://www.arduino.cc/en/Reference/RandomSeed
https://www.arduino.cc/en/Reference/Random

接著將此猜測的數字經由 pipe 0 傳送出去, 若傳送成功就切換到接收模式, 然後起始一個 200 ms 的計時器, 等候接收板傳送正確答案過來. 若在時限內收到接收板的回應, 就將正確答案與自己猜測的 gnumber 比對, 若相同就是確認猜中了, 就將 done 改為 true, 停止猜測.

第二個節點傳送板 2 程式與上面幾乎相同, 只是位址與綁定的通道不同, 分別是 "2node" 與 pipe 1, 除此之外其他都與傳送板 1 完全一樣, 如下所示 :


傳送板 (PTX node) 2 程式 :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[6]="2node";  //節點位址為 5 bytes + \0=6 bytes
bool done=false;  //用來判斷是否要停止傳送猜測數字

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  radio.setPALevel(RF24_PA_MAX);   //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108); //設定頻道=108 (0~125, 較高的頻道似乎比較 open)
  radio.openWritingPipe(address);  //開啟寫入通道
  radio.openReadingPipe(1,address);   //開啟讀取通道 pipe 1 (接收 PRX Hub 答案)
  radio.stopListening();  //PTX node 暫時不需要接收, 停止傾聽
  randomSeed(analogRead(0));  //利用 A0 腳的隨機狀態設定偽隨機序列種子
  }
void loop() {
  if (!done) {  //還沒猜對就繼續猜
    byte gnumber=(byte)random(11);  //產生 0~10 之隨機猜測數字 
    Serial.print("Guess number=");
    Serial.print(gnumber);
    Serial.print("...");
    //傳送猜測數字 (1 byte) 給 PTR Hub
    if (!radio.write(&gnumber,1)) {Serial.println("Sending failed");}  //傳送失敗
    else { //傳送成功
      Serial.println("Sending OK");
      radio.startListening();  //切換到接收模式等待 PTR Hub 回應
      unsigned long startTimer=millis(); //開啟計時器等候 200ms
      bool timeout=false;  //計時器逾時旗標,預設未逾時
      while (!radio.available() && !timeout) { //尚未收到回應且旗標為未逾時
        if (millis()-startTimer > 200 ) {timeout=true;} //等候回應直到逾時或收到回應
        }
      //未收到回應,可能猜錯了
      if (timeout) {Serial.println("Last guess may be wrong, try again.");}
      else {  //未逾時且收到回應,可能答對了
        byte rnumber;  //儲存 PTR Hub 傳來的回應數字
        radio.read(&rnumber,1);  //讀取 PTR Hub 傳來的回應數字 (1 byte)
        if (gnumber==rnumber) {  //由 PTR 回應確認猜對了
          Serial.println("You guess right!");
          done=true;  //猜對了就結束猜測
          }
        else {Serial.println("Something went wrong, keep guessing.");}
        }
      radio.stopListening();  //切回傳送模式
      }
    }
  delay(1000);
  }


接收板 (PTR Hub) 程式 :  

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(7, 8); //指定 Arduino Nano 腳位對應 nRF24L01 之 (CE, CSN)
const byte address[][6]={"1node","2node","3node","4node","5node","6node"};
byte rnumber;  //要讓 PTX node 猜的隨機數字

void setup() {
  Serial.begin(9600);
  radio.begin();  //初始化 nRF24L01 模組
  printf_begin();  //初始化 RF24 的列印輸出功能
  radio.setPALevel(RF24_PA_MAX);  //設為低功率, 預設為 RF24_PA_MAX
  radio.setChannel(108);  //設定頻道=108 (0~125, 較高的頻道似乎比較開放)
  radio.openReadingPipe(0, address[0]);  //開啟 pipe 0 之讀取通道
  radio.openReadingPipe(1, address[1]);  //開啟 pipe 1 之讀取通道
  radio.openReadingPipe(2, address[2]);  //開啟 pipe 2 之讀取通道
  radio.openReadingPipe(3, address[3]);  //開啟 pipe 3 之讀取通道
  radio.openReadingPipe(4, address[4]);  //開啟 pipe 4 之讀取通道
  radio.openReadingPipe(5, address[5]);  //開啟 pipe 5 之讀取通道
  radio.startListening();  //接收端開始接收
  radio.printDetails();  //印出 nRF24L01 詳細狀態
  Serial.println("NRF24L01 PRX Hub receiving ...");
  randomSeed(analogRead(0));  //利用 A0 腳的隨機狀態設定偽隨機序列
  rnumber=(byte)random(11);  //產生 0~10 的隨機數供 PTX node 猜測
  Serial.print("The number for PTX node to guess=");
  Serial.println(rnumber);  //輸出待猜測之數字
  Serial.println();
  }
void loop() {
  byte pipe_num;   //用來儲存 PTX node 傳送板之通道編號
  byte gnumber;  //用來儲存從 PTX node 傳來的猜測號碼
  if (radio.available(&pipe_num)) {  //偵測接收緩衝器是否有資料
    radio.read(&gnumber, 1);  //讀取接收的 1 個 byte 猜測數字
    Serial.print("Received from node=");  //顯示從哪一通道接收
    Serial.print(pipe_num);
    Serial.print(" guess number=");
    Serial.print(gnumber);  //顯示收到的猜測號碼
    Serial.print("...");  //顯示猜測結果
    if (gnumber==rnumber) {  //猜對了
      radio.stopListening();  //PRX Hub 暫時停止接收, 切換至傳送模式
      radio.openWritingPipe(address[pipe_num]);  //開啟 PTR Hub 之寫入通道
      //將待猜測之數字傳給答對者
      if (!radio.write(&rnumber, 1)) {Serial.println("Guess right!");}
      else {Serial.println("Sending failed!");}
      radio.startListening();  //重新開啟 PRX Hub 之接收功能
      }
    else {Serial.println("Guess wrong!");}  //猜錯了
    }
  }

此接收板程式中會開啟 108 頻段的 Pipe 0~5 (位址 "1node" ~ "6node") 全部 6 個通道, 然後利用 A0 腳上飄移的隨機位準呼叫 randomSeed() 設定隨機序列種子, 再呼叫 random() 得到一個 0~10 的隨機值做為被猜測的數字 rnumber, 接著進入迴圈等候傳送板傳遞猜測值過來進行比對. 一旦傳送板有傳資料過來, 就記下其通道號碼與所傳之猜測數字, 然後比對 rnumber 與 gnumber, 若相同表示猜對了, 這時就先停止接收, 切到傳送模式, 把正確答案 rnumber 傳送給猜對的節點. 注意, 這裡接收板切到傳送模式時傳入 openWritingPipe() 的是該通道所綁定之位址 address[pipe_num], 傳送成功後又再切回接收模式.

接收板 PTR Hub 之序列埠監控視窗輸出 :

STATUS = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1 = 0x65646f6e31 0x65646f6e32
RX_ADDR_P2-5 = 0x33 0x34 0x35 0x36
TX_ADDR = 0xe7e7e7e7e7
RX_PW_P0-6 = 0x20 0x20 0x20 0x20 0x20 0x20
EN_AA = 0x3f
EN_RXADDR = 0x3f
RF_CH = 0x6c
RF_SETUP = 0x07
CONFIG = 0x0f
DYNPD/FEATURE = 0x00 0x00
Data Rate = 1MBPS
Model = nRF24L01+
CRC Length = 16 bits
PA Power = PA_MAX
NRF24L01 PRX Hub receiving ...
The number for PTX node to guess=5

Received from node=0 guess number=8...Guess wrong!
Received from node=1 guess number=0...Guess wrong!
Received from node=0 guess number=5...Guess right!
Received from node=1 guess number=9...Guess wrong!
Received from node=0 guess number=7...Guess wrong!
Received from node=1 guess number=8...Guess wrong!
Received from node=0 guess number=4...Guess wrong!
Received from node=1 guess number=8...Guess wrong!
Received from node=0 guess number=7...Guess wrong!
Received from node=1 guess number=1...Guess wrong!
Received from node=0 guess number=2...Guess wrong!
Received from node=1 guess number=5...Guess right!
Received from node=0 guess number=6...Guess wrong!
Received from node=1 guess number=8...Guess wrong!
Received from node=0 guess number=0...Guess wrong!
Received from node=1 guess number=2...Guess wrong!
Received from node=0 guess number=5...Guess right! 
Received from node=1 guess number=0...Guess wrong!
Received from node=0 guess number=8...Guess wrong!
Received from node=1 guess number=2...Guess wrong!

傳送板監控視窗輸出訊息如下 : 

Guess number=7...Sending OK
Last guess may be wrong, try again.
Guess number=1...Sending OK
Last guess may be wrong, try again.
Guess number=5...Sending failed
Last guess may be wrong, try again.
Guess number=2...Sending OK
Last guess may be wrong, try again.
Guess number=4...Sending OK
Guess number=3...Sending OK
Last guess may be wrong, try again.
Guess number=5...Sending failed
Guess number=5...Sending failed
Guess number=5...Sending failed
Guess number=5...Sending OK
You guess right!  (終於停了)

但奇怪的是, 明明接收板顯示 Sending OK 表示它有成功將正確答案傳給猜對的傳送板, 但似乎猜對的傳送板沒收到, 以至於 done 沒有被更新為 true, 所以即使猜對了, 傳送板還是繼續送出猜測數字不會停止, 照理講猜對了就該停止才對.

2017-10-12 補充 :

經測試, 是因為 timeout 的緣故或傳送板 Sending failed 之故. 為何每次都在猜對時 Sending failed? 真是奇怪.

參考 :

http://playground.arduino.cc/InterfacingWithHardware/Nrf24L01#
Optimized High Speed Driver for nRF24L01(+) 2.4GHz Wireless Transceiver
Connecting the Radio
[Arduino] 以 nRF24L01+ 和 RF24 library 製作無線電端點
Arduino Wireless Communication – NRF24L01 Tutorial
# Arduino NRF24L01 文件
NRF24L01 功能說明
[Arduino]001 Arduino與NRF24L01 2.4G無線應用
邁入『物聯網』的第一步:如何使用無線傳輸:基本篇
[How To Arduino] Arduino 簡易測試 NRF24L01 無線傳輸
糊涂塔克学习笔记01 Arduino+nRF24L01
nRF24L01 Module Demo for Arduino
NRF24L01 XN297L 無線網路 區域遠距傳輸
nRF24L01 Module Demo for Arduino
4 Arduino 4 Nrf24L01 Wireless Communication
# nRF24L01 範例下載
# 跨入Maker物聯網時代 : 誰都可以用Arduino / 楊佩璐, 任昱衡編著