2017年10月7日 星期六

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

其教學影片如下 :

https://www.youtube.com/watch?v=7rcVeFFHcFM&t=275s




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

How to convert integer to string in C?
C 語言秘技 (2) – 使用 sprintf 將結構字串化 (作者:陳鍾誠)


測試 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 範例下載
BuyIC Arduino nRF24L01 資料下載

33 則留言 :

Blogger 提到...
網誌管理員已經移除這則留言。
Unknown 提到...

請問你使用那個軟體寫入程式?要如何寫入

Unknown 提到...

謝謝你的文章,從這些文章裏我方便的得到了一個入門,得已開始學習多點無線傳輸的知識。
複製另存文章內容中我把所有" PTR "字樣都改成" PRX ",以符合總體風格一貫。

針對
"測試 5 : 猜數字 (Multi-ceiver)"
"接收板 (PTR Hub) 程式 :" 的範例
我將其中
if (!radio.write(&rnumber, 1)) {Serial.println("Guess right!");}
else {Serial.println("Sending failed!");}
拿掉了'!'變更為
if (radio.write(&rnumber, 1)) {Serial.println("Guess right!");}
else {Serial.println("Sending failed!");}

另外在
if (gnumber==rnumber) { //猜對了
radio.stopListening(); //PRX Hub 暫時停止接收, 切換至傳送模式
兩行之間插入了延時,變這樣;
if (gnumber==rnumber) { //猜對了
delay(5);
radio.stopListening(); //PRX Hub 暫時停止接收, 切換至傳送模式

經過上面兩項草稿碼的更動,令人困擾"但不知為何傳送沒問題卻傳回 0"的問題就消失了,程式跑得很順很開心。加入延時能改善結果我猜是收發雙方還有些悄悄話沒說完(哈,我扯的)。

Unknown 提到...

請問[進行人機驗證]是什麼把戲?我被它玩了很久。簡體字啊,被植入了嗎?

小狐狸事務所 提到...

"進行人機驗證" 出現在哪裡?

Unknown 提到...

>>"進行人機驗證" 出現在哪裡?
按下張貼留言後進入[留下您的意見]網頁,在[發表您的意見]按鈕的上方空間。看來就是發表前要先驗證非機器人的機制。

小狐狸事務所 提到...

喔知道了, 那個不用管他啦, 驗證是否為機器人用的.

Lok Chon Mou 提到...

針對最後星形傳輸的例子, 我想問: nrf24是如何決定傳輸是用那一條pipe的???是否setup用了radio.openReadingPipe(1, "2node")來指定了pipe 1的位址是2node, 所以之後PRX用radio.openWritingPipe("2node")就會自動用了pipe1???否則只用了位址來開writing pipe, 怎決定是哪一條pipe???

提到...

謝謝您精心的講解與操作,讓我這個初學者能夠有機會學習。
但是我一直無法解決的問題是
發送端在序列埠視窗顯示皆為正常
但是接收端的部分本來應該要讀取到的字元,卻是空白和括號形同亂碼,如下
⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮)P

這實在令我費解,望老師您有空時能夠回覆我的疑問,謝謝。

小狐狸事務所 提到...

Hi! 出現亂碼可能是 bit rate 設定不一致, 您是第一個實驗就有問題嗎?

提到...

抱歉這麼久才看到版主您的回覆
後來第一個以及第二個實驗皆是如此,還是失敗@@
經觀察後目前的狀況為接收端和傳送端板子在序列埠視窗的位址不一樣(明明就同一個程式碼)
如:EN_EXADDR,RH_CH,RH_SETUP,DYNPD/FEATURE

是不是
Q1:需要購買天線?
Q2:使用穩壓器模組?

小狐狸事務所 提到...

1. 這麼近不是天線問題
2. 我使用 Arduino 3.3v 輸出

澄 提到...

你好,想請問一下
RF的TX是以廣播形式發送嗎??

假設一個TX發送了:HI
附近所有的RX全部都可以接收到HI嗎??

澄 提到...

補充一下
我是使用這款模組
https://wf8266.com/wf8266r/tutorials/3E_RC

https://www.taiwansensor.com.tw/product/315mhz-rf-%E7%84%A1%E7%B7%9A%E6%A8%A1%E7%B5%84-arduino-%E9%81%99%E6%8E%A7%E5%99%A8-315m-%E5%90%AB%E7%99%BC%E5%B0%84%E8%88%87%E6%8E%A5%E6%94%B6%E6%A8%A1%E7%B5%84/

因為一直接收失敗
所以想來請教一下ˊ口ˋ

PON 提到...

您好,感謝你的文章讓我得到許多知識,有個令我困惑的問題是...
在測試 4 : 三個 NRF24L01 兩送一收 (Multi-ceiver)
我測試成功了,並且我將2個傳送版改為固定的數值...如A為88,B為55(沒有用計數器),
於序列埠可以一次收到2個傳送版88與55的數據。
請教我該如何將這兩個數值相加,繼而顯示數值為143(88+55)。
謝謝您。

明月伴清風 提到...

很棒的文章

小狐狸事務所 提到...

感謝您!

明月伴清風 提到...

大大您好:
最近陸續玩到 測試 4 : 三個 NRF24L01 兩送一收,
程式都照您的打,但是接受板只收到Board2,一直收不到Board1的信息,請教一下會是哪裡出問題,
程式都照抄,只有改通道108成98而已,接線也檢查了好幾次,有點頭疼。

有空的話還請大大提點一下。
https://www.youtube.com/watch?v=MHCmozqDQwU&ab_channel=%E5%BC%B5%E7%BE%BF%E8%A8%A2

小狐狸事務所 提到...

我也忘記了, 等我複習一下

Unknown 提到...

你好 我想請問遺下 我按照你範例1的程式,但是我的接收端無法顯示字串,原本該顯示字串的地方會是空白,想請問該怎麼解決?

小狐狸事務所 提到...

Hi, 這些程式都是實作完直接貼上來的, 前面的網友做到測試 4 才出現問題, 可見測試 1 應該不會有問題. 實驗出問題的變數很多, 最常出現問題的是硬體接線不良, 零件或模組異常, 甚至 MCU 也可能出問題等等, 有時需要備品替換, 甚至電路拆掉重拉.

Joy 提到...

您好,我想請問一下,所以nrf24L01的Vcc是可以接到Uno板上的5v插腳,但不能直接外接5v電源是嗎?

小狐狸事務所 提到...

嗨, Joy, 不是這樣, VCC 要接 Arduino 的 3.3v 輸出腳, 不是接 5v, 也不是接外面的 5v, 它的 VCC 耐壓 3.6V 而已. 我一直都用 Nano, 它有一個 3.3v 電壓輸出, UNO 應該也有.

Gordon 提到...

您好,請問這個模組可以雙向傳輸嗎?

小狐狸事務所 提到...

嗨, Gordon, 沒錯是可以雙向傳輸的

Gordon 提到...

請問有範例程式嗎?

小狐狸事務所 提到...

嗨, Gordon, 我只做了文章中的基本測試而已, 並沒有實際應用的範例程式, 網路搜尋看看應該有人分享.

Gordon 提到...

您好版主,我是有上網搜尋過,但是都無法互相傳送字串

小狐狸事務所 提到...

不知道這是不是您要的 :
https://create.arduino.cc/projecthub/lightthedreams/nrf24l01-for-communication-1-way-and-2-way-80e65c

Gordon 提到...
作者已經移除這則留言。
Gordon 提到...

版主您好,我有用過這篇文章下去改程式碼,但是不知道是我裡解錯誤,還是程式的BUG,我無法讓它們互相傳送字串

小狐狸事務所 提到...

雖然我也很想繼續玩物聯網, 但現在重心不再這裡, 可能要明年才會有時間, 您先研究看看, 盼望多交流!

Gordon 提到...

好的謝謝版主