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

其教學影片如下 :

# 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 資料下載

2017年10月6日 星期五

免電池的無線門鈴

今天在露天拍賣找數位電視機上盒時, 看到賣家也賣下面這款免電池的無線門鈴, 感到非常好奇 :

# 超遠150米!加購【防水免電池按鈕】學習型高穿透性門鈴【可1對多+多對1】 $350
# 電池防水【1對1】150米超遠高穿透性!環保學習型無線門鈴組【可1對多+多對1】

免電池? 那發射器的電能哪裡來? 原來是門鈴發射器裡有一個微能量發電機, 靠著按壓門鈴的力道讓小磁鐵切割線圈, 產生的電能儲存在鉭質電容上, 用來驅動低功號無線發射晶片. 有效距離長達 150 米, 可穿透 3 層樓牆壁, 還有中繼功能.

# 不用電池的呼叫器 — Linbell G2 自發電無線門鈴評測和拆解

2017年10月5日 星期四

購買數位電視機上盒

鄉下家的第四台 9/30 到期須繳下半年費用 3300 元, 因上個月申裝了 MOD, 爸說只有在看財經台, 其他都沒在看, 乾脆拆機算了, 每個月單一費率 550 元買一堆不會看的頻道, 不能像 MOD 那樣選購頻道, 實在是因為區域壟斷經營的關係.

前天南國來剪線並取回數位機上盒後, 爸在客廳就看 MOD, 在飯廳就看以前高雄拆回去的舊款數位電視 (DVB-T, 只能收到 6 台). 但 MOD 基本款並沒有數位電視的台視財經台, 所以只好單訂, 每個月 5 元, 加上寰宇財經每個月多付 35 元.

為了讓飯廳能收到全部 22 台數位電視, 今天上露天欲購買下面這款 DVB-T2 機上盒加天線 :

# 現貨送HDMI線!2017年DVB-T2雙頻數位機上盒【強化版】台灣區22台免費HD節目 $680
# 38DB小鋼炮!超強DVB-T2天線【5米線+5V增益器】室內車用兩用高強度柱型地上波天線.完勝大通菱形天線 $450

合購是 680+450+70 運費共 1200 元, 但我發現賣家有自己的網站賣場, 加入會員有優惠 70 元, 比露天還便宜 100 元, 所以就直接在賣場購買 :

# 送HDMI線 22數位HD台【DVB-T2+小鋼炮增益5米天線】 房東最愛 EPG選單 取代第四台1年保 $1100

要等收到貨回鄉下安裝才能確認是否可完整收到 22 台訊號.

2017-10-22 補充 :

上週帶回鄉下安裝, 小鋼炮可以完整收到 22 台頻道, 品質 ok, 只有台視信號較弱, 有時會馬賽克.

2017年10月4日 星期三

曬衣繩更換記 (new)

今天中秋節早上在家解 APCS 題目, 下午把買來很久的新曬衣繩安裝上去, 兩年前在小北買的那條 80 元的尼龍繩不耐用, 才兩年就風化了, 拉繩時會出現白色屑屑, 最後整個斷裂. 數月前又去小北買了 120 元表皮是橡膠, 內芯是鋼絲的曬衣繩, 但一直沒時間更換, 下午趁著整理後陽台, 驗收防漏工程之便, 把曬衣繩換新.

舊曬衣繩

新曬衣繩

另, 後陽台驗收有瑕疵, 中間較低會積水, 已通知老闆來檢視.

參考 :

# 曬衣繩更換記 (2015-03-30)

APCS (大學程式先修檢測) 105 年 10 月觀念題解析

解完 105 年 3 月份 APCS 觀念題後感覺越來越上手了, 繼續來解 10 月份的觀念題.

1. 右側 F() 函式 執行 後,輸出 為何?

void F( ) {
  char t, item[] = {'2', '8', '3', '1','9'};
  int a, b, c, count = 5;

  for (a=0; a<count-1; a=a+1) {
    c = a;
    t = item[a];
    for (b=a+1; b<count; b=b+1) {
       if (item[b] < t) {
         c = b;
         t = item[b];
         }
       if ((a==2) && (b==3)) {
         printf ("%c %d\n", t, c);
         }
       }
    }
  }

(A) 1 2     (B) 1 3     (C) 3 2       (D) 3 3

解析 : 

此題乍看之下很複雜, 是雙重迴圈, 但實際上不需要從頭追蹤程式執行流程, 只要看它的輸出條件即可, 其唯一的輸出指令 printf() 會被執行的條件是外迴圈索引 a=2 且內迴圈索引 b=3 時, 只要追蹤此條件即可. 

外迴圈 a=2 時, c=2, t=item[2]=3
然後進入內迴圈, 只須看 b=3 時, 因 item[b=3]=1 小於外迴圈得到的 t=3, 因此 if 為真, 執行 c=b=3, 以及 t=item[b=3]=1, 接著判斷 a==2 且 b==3 都符合, 因此輸出 t,c 為 1 3, 答案為 (B).


2. 下列 switch 敘述程式碼可以如何以if-else  改寫?   

switch (x) {
  case 10: y = 'a'; break; 
  case 20:
  case 30: y = 'b'; break; 
  default: y = 'c';
  }

   (A) if (x==10) y = 'a' ;
       if (x==20 || x==30) y = 'b';
       y = 'c';

   (B) if (x==10) y = 'a' ; 
       else if (x==20 || x==30) y = 
       else y = 'c';

   (C) if (x==10) y = 'a' ; 
       if (x>=20 && x<=30) y = 'b';
       y = 'c';

   (D) if (x==10) y = 'a' ; 
       else if (x>=20 && x<=30) y = 'b';
       else y = 'c';

解析 : 

switch case 相當於 if else if else 架構, 不是 if if if 架構, 故 (A) 與 (C) 均排除. (B) 與 (D) 基本上相同, 差別在 else if 裡面兩個條件是 AND 還是 OR. 由於 case 20 是空的, 無 break 跳出, 若 x=20 會直接往下執行 case=30 選項, 因此 x=20 或 x=30 都是執行 x=30, 是 OR 關係, 答案是 (B).


3. 給定下列 G() , K() 兩函式,執行 G(3) 後所回傳的值為何 ?

int K(int a[], int n) {
  if (n >= 0)
    return (K(a, n-1) + a[n]); 
  else return 0;
  }
int G(int n){
  int a[] = {5,4,3,2,1}; 
  return K(a, n);
  }

(A) 5    (B) 12   (C) 14     (D) 15

解析 : 

此題是遞迴函數題, 追蹤如後 :

呼叫 G(3) : 把陣列 a 與 n=3 傳入呼叫 K(a,3)
呼叫 K(a,3) : 因 n=3 大於 0, 故回傳 K(a, n-1) + a[n]=K(a, 2) + a[3]=K(a,2)+2
呼叫 K(a,2) : 因 n=2 大於 0, 故回傳 K(a, n-1) + a[n]=K(a, 1) + a[2]=K(a,1)+3
呼叫 K(a,1) : 因 n=1 大於 0, 故回傳 K(a, n-1) + a[n]=K(a, 0) + a[1]=K(a,0)+4
呼叫 K(a,0) : 因 n=0 等於 0, 故回傳 K(a, n-1) + a[n]=K(a, -1) + a[0]=K(a,-1)+5
呼叫 K(a,-1) : 因 n=-1 小於 0, 故遞迴結束回傳 0, 一一傳回原呼叫者, 最後回傳的結果是 2+3+4+5=14, 答案是 (C)


4. 下列程式碼執行後輸出結果為何 ?

int a=2, b=3;
int c=4, d=5;
int val;
val = b/a + c/b + d/b;
printf ("%d\n", val);

(A) 3      (B) 4      (C) 5     (D) 6

解析 : 

C 語言的整數除法是無條件捨去 (只取整數), 因此 b/a + c/b + d/b=3/2+4/3+5/3=1+1+1=3, 答案是 (A).


5. 下列程式碼執行後輸出結果為何 ?

int a[9] = {l, 3, 5, 7, 9, 8, 6, 4, 2}; 
int n=9, tmp;

for (int i=0; i<n; i=i+1) {
  tmp = a[i];
  a[i] = a[n-i-1];
  a[n-i-1] = tmp;
  }                                        
for (int i=0; i<=n/2; i=i+1)
  printf ("%d %d ", a[i], a[n-i-1]);

(A) 2 4 6 8 9 7 5 3 1 9
(B) 1 3 5 7 9 2 4 6 8 9
(C) 1 2 3 4 5 6 7 8 9 9
(D) 2 4 6 8 5 1 3 7 9 9

解析 : 

此程式有兩個 for 迴圈, 第一個迴圈是以陣列元素 a[4]=9 為中心左右交換兩次, i=0~3 左右交換一次變成 a[]={2,4,6,8,9,7,5,3,1}, i=4 時 9 與 9 自己交換, i=5~8 時左右又交換一次還原, 變回原來的 a[]={1,3,5,7,8,9,8,6,4,2}, 等於是虛晃一招.

第二個迴圈拜訪陣列 i=0~9/2=0~4, 會印出 a[i] 與其以 a[4]=9 為軸, 從尾端算過來的對應元素, 例如 a[0] 對應 a[8], 印出 12, a[1] 對應 a[7] 印出 34, a[2] 對應 a[6] 印出 56, a[3] 對應 a[5] 印出 78, a[4] 對應 a[4] 印出 99, 整個輸出 1234567899, 故答案是 (C).


6. 右側函式以 F(7) 呼叫後回傳值 為 12,則<condition> 應為何 ?

int F(int a) {
  if ( <condition> )
    return 1;
  else
    return F(a-2) + F(a-3);

(A) a < 3
(B) a < 2
(C) a < 1
(D) a < 0

解析 : 

此為遞迴函數題, 求其終止條件為何才能讓呼叫 F(7) 得到回傳值 12.

若條件為 a < 3 :
F(7)=F(5)+F(4)=3+2=5 (非也)
F(5)=F(3)+F(2)=2+1=3
F(4)=F(2)+F(1)=1+1=2
F(3)=F(1)+F(0)=1+1=2

若條件為 a < 2 :
F(7)=F(5)+F(4)=4+3=7 (非也)
F(5)=F(3)+F(2)=2+2=4
F(4)=F(2)+F(1)=2+1=3
F(3)=F(1)+F(0)=1+1=2
F(2)=F(0)+F(-1)=1+1=2

若條件為 a < 1 :
F(7)=F(5)+F(4)=5+4=9 (非也)
F(5)=F(3)+F(2)=3+2=5
F(4)=F(2)+F(1)=2+2=4
F(3)=F(1)+F(0)=2+1=3
F(2)=F(0)+F(-1)=1+1=2
F(1)=F(-1)+F(-2)=1+1=2

若條件為 a < 0 :
F(7)=F(5)+F(4)=7+5=12 (正確)
F(5)=F(3)+F(2)=4+3=7
F(4)=F(2)+F(1)=3+2=5
F(3)=F(1)+F(0)=2+2=4
F(2)=F(0)+F(-1)=2+1=3
F(1)=F(-1)+F(-2)=1+1=2
F(0)=F(-2)+F(-3)=1+1=2

故答案為 (D)


7. 若 n為正整數,下列程式三個迴圈執行完畢後 a 值將為何 ?

for (int i=1; i<=n; i=i+1)
  for (int j=i; j<=n; j=j+1)
    for (int k=1; k<=n; k=k+1)
      a = a + 1;

(A) n(n+1)/2
(B) n3/2
(C) n(n-1)/2
(D) n2(n+1)/2

解析 : 





9. 下列是依據分數 s 評定等第的程式碼片段,正確的等第公式應為:

90~100 判為 A 等
80~89 判為 B 等
70~79 判為 C 等
60~69 判為 D 等
0~59 判為 F 等



這段程式碼 在處理 0~100 的分數時,有幾個分數的等第是錯?

(A) 20    (B) 11     (C) 2   (D) 10



~未完待續~

2017 年第 39 周記事

後陽台防漏與主臥室地磚更新工程上周四完工後約需一周時間讓其乾燥凝固, 房間的床在施工時被立起來, 所以這一周來我都在客廳睡活動躺椅, 還好最近沒蚊子, 躺椅也蠻舒服的, 只是不能翻身而已.

自從上周在我家菜園種植小番茄後, 小舅開始常住鄉下了, 每天都會來我家, 除了灌溉之外, 還屋前屋後到處整理可種菜的區域, 圍牆外水溝邊, 門口大馬路邊通通難逃魔爪. 這樣也好, 反正我這麼忙, 菜園就交給他打理好了, 小舅種菜十餘年, 據說鄰居都誇他把菜園弄得像花園, 我且拭目以待.

這個週日 10/1 突然訪客甚多, 先是小舅媽與婷婷表妹一道回鄉下, 帶來中秋禮物吳記月餅 (超好吃) 與柚子. 接著信宏也來了, 想找我去爬山運動, 我說我還在滷冬瓜沒辦法, 結果他一出發就馬上下起大雨. 然後是高樹阿姨也是帶著月餅柚子來了, 不久國中同學阿錦也是帶著月餅柚子來找, 天啊! 這下月餅柚子吃不完了! 上週日傍晚跑步到他家去找他聊天, 結果他也是去運動沒遇到. 今天他提到他已徒步走遍我們鄉下各村里, 終於搞清楚那些甚麼寮的舊地名到底是在哪裡, 問我有無興趣過年時跟他一起去走, 我想冬天走路不會熱到爆, 當即欣然應允.

母親仙遊後家裡就沒這麼熱鬧過了, 這個周日真好.

2017年9月30日 星期六

APCS (大學程式先修檢測) 105 年 3 月觀念題解析

今年度大學程式先修檢測 (APCS) 第二次測試報名將在 10/5 截止, 考試日期 10/28 (六). 參見 :

# https://apcs.csie.ntnu.edu.tw

我把 105 年 3 月 5 日的觀念題做個不專業的解析如下 :


1. 下列程式在不修改第4 行及第7 行程式碼的前提下,最少需修改幾行程式碼以得到正確輸出?

(A) 1  (B) 2   (C) 3   (D) 4



1 int k = 4;
2 int m = 1;
3 for (int i=1; i<=5; i=i+1) {
4   for (int j=1; j<=k; j=j+1) {
5     printf (" ");
6     }
7   for (int j=1; j<=m; j=j+1) {
8     printf ("*");
9     }
10 printf ("\n");
11 k = k – 1;
12 m = m + 1;
13 }

解析 : 

此題會輸出五列文字 (因 printf 是在外迴圈每一圈結束時輸出 "\n" 跳行, 而外迴圈要跑 5 圈), 第一列先輸出 4 個空格再輸出 1 個星號; 第二列輸出 3 個空格再輸出 3 個星號; 第三列輸出 2 個空格再輸出 5 個星號 ... 亦即星號呈 1,3,5,7,9 遞增 2, 而前方空格呈 4,3,2,1,0 遞減 1.

程式第 3~13 列為外部迴圈控制要輸出幾列文字 (i=1~5 共 5 列), 第一個內迴圈 (4~6 列) 控制每列前面要輸出幾個空格 (1~k, k 隨外圈遞減 1, 即第 11 列 k=k-1), 因此 4~6 列沒問題. 第二個內迴圈 (7~9 列) 控制每列星號要輸出幾個 (1~m), 原程式第 12 列 m 隨外圈遞增 1, 這就有問題了, 如上述應遞增 2 才對, 故只要將第 12 列 m=m+1 改為 m=m+2 即可, 故答案為 (A) 1.


2. 給定一陣列 a[10]={ 1, 3, 9, 2, 5,8, 4, 9, 6, 7 },i.e., a[0]=1,a[1]=3, …, a[8]=6, a[9]=7,以f(a, 10)呼叫執行下列函式後,回傳值為何?

(A) 1   (B) 2   (C) 7   (D) 9




int f (int a[], int n) {
  int index = 0;
  for (int i=1; i<=n-1; i=i+1) {
    if (a[i] >= a[index]) {
      index = i;
      }
    }
  return index;
  }

解析 : 

函數 f 有兩個參數, 第一個參數是整數陣列, 第二個參數是整數, C 語言程式若要將陣列傳給函數處理的話, 同時必須將陣列長度 (即陣列元素個數) 也傳進去, 因為傳進來的陣列名稱只是陣列的開頭位址, 函數無法從位址得知陣列長度, 故呼叫 f(a,10) 就是將陣列開頭位址與其長度傳給函數 f.

函數 f 內首先宣告整數變數 index 並初始化為 0, 然後進入迴圈從索引 1 跑到 9 (因傳入 n=10, n-1=9). 接著用 if 判斷從 a[1]~a[9] 的元素是否大於等於 a[index] (因 index 初始化為 0, 故第一次是跟 a[0] 比), 是的話就用目前的迴圈索引更新 index, 亦即,

a[10]={ 1, 3, 9, 2, 5,8, 4, 9, 6, 7 }, a[0]=1, 第一圈時, a[1]=3>a[0], 故 index 被更新為 1; 第二圈時 a[2]=9>a[1], 故 index 被更新為 2; 但隨後之元素值都小於 9, 故 index 都不會被更新, 仍然是 2. 直到 i=7, 亦即比較 a[7]=9 時, 因為符合 >= a[index]=a[2]=9, 因此 index 才又被更新為 7. 最後的兩圈音元素值都小於 a[7]=9, 維持 index=7 不變, 故最後傳回之 index 為答案 (C) 7.


3. 給定一整數陣列a[0]、a[1]、…、a[99]且a[k]=3k+1,以value=100 呼叫以下兩函式,假設函式f1 及f2 之while 迴圈主體分別執行n1 與n2 次 (i.e, 計算if 敘述執行次數,不包含 else if 敘述),請問n1 與n2 之值為何? 註: (low + high)/2 只取整數部分。

(A) n1=33, n2=4   (B) n1=33, n2=5    (C) n1=34, n2=4   (D) n1=34, n2=5




int f1(int a[], int value) {
  int r_value = -1;
  int i = 0;
  while (i < 100) {
    if (a[i] == value) {
      r_value = i;
      break;
      }
    i = i + 1;
    }
  return r_value;
  }


int f2(int a[], int value) {
  int r_value = -1;
  int low = 0, high = 99;
  int mid;
  while (low <= high) {
    mid = (low + high)/2;
    if (a[mid] == value) {
      r_value = mid;
      break;
      }
    else if (a[mid] < value) {
      low = mid + 1;
      }
    else {
      high = mid - 1;
      }
    }
  return r_value;
  }

解析 : 

此具有 100 個元素的陣列 a 其值為 a[k]=3k+1, 亦即其值的分布情形是 :

k        0   1   2   3  .......  32   33    34 .....
a[k]   1   4   7   10 .......  96  100  103 ....

首先看 f1() 執行情形. 函數 f1 先宣告回傳值 r_value 並初始化為 -1, 然後用 while 迴圈從頭拜訪陣列 a, 在迴圈內判斷是否元素值為 100, 是的話就用當時的 i 值來更新回傳值 r_value, 並且用 break 跳出迴圈, 然後傳回此 r_value. 從上面的 a[k] 分布可知, 當 i=k=33 時, a[k]=a[33]=100, 因此傳回的 r_value=33. 但是要注意, 題目問的是 f1 函數中 while 迴圈執行了 n1 次, i=33 時迴圈停止, i=0~33, 從 0 起算故 n1=33+1=34 次. 

接著看 f2, 此函數先宣告兩個整數 low=0 (低標) 與 high=99 (高標), 還有一個 mid (中間值) 為 low 與 high 之平均取整數 (因 C 的整數除法運算子 / 只傳回商的整數部分, 即無條件捨去). 在 while 迴圈中首先會將目前的 low 與 high 取平均值存入 mid 內, 然後把 mid 當陣列索引, 判斷 a[mid] 是否等於 value=100, 是的話將目前的 mid 傳回並以 break 結束迴圈; 若 a[mid] 小於 100, 把 mid 增量 1 後更新 low; 若 a[mid] 大於 100 則把 mid 減量 1 後更新 high 之值.

以下是走訪迴圈的結果 :

迴圈        mid       a[mid]       low      high     a[mid] ? 100
    1           49           148          0          48             >
    2           24             73        25          48             <
    3           36           109        25          35             >
    4           30             91        31          35             <
    5           33           100                                       =

第一圈時, 因 mid=(low+high)/2=(0+99)/2=49, 而 a[49]=3k+1=3*49+1=148 > 100, 符合 else 條件, 因此 high=mid-1=49-1=48, low 仍然是 0.
第二圈時, 因 mid=(low+high)/2=(0+48)/2=24, 而 a[24]=3k+1=3*24+1=73 < 100, 符合 else if 條件, 因此 low=mid+1=24+1=25, high 仍然是 48.  
第三圈時, 因 mid=(low+high)/2=(25+48)/2=36, 而 a[36]=3k+1=3*36+1=109 > 100, 符合 else 條件, 因此 high=mid-1=36-1=35. low 仍然是 25.  
第四圈時, 因 mid=(low+high)/2=(25+35)/2=30, 而 a[30]=3k+1=3*30+1=91 < 100, 符合 else if 條件, 因此 low=mid+1=30+1=31, high 仍然是 35.
第五圈時, 因 mid=(low+high)/2=(31+35)/2=33, 而 a[33]=3k+1=3*33+1=100 , 符合 if 條件, 因此 把 mid=33 設給 r_value 傳回.

因此 while 迴圈在 f2 共跑了 5 次. 答案是 (D) n1=34, n2=5


4. 經過運算後,下列程式的輸出為何?

(A) 1275    (B) 20      (C) 1000     (D) 810




for (i=1; i>=100; i=i+1) {
  b[i] = i;
  }
a[0] = 0;
for (i=1; i>=100; i=i+1) {
  a[i] = b[i] + a[i-1];
  }
printf ("%d\n", a[50]-a[30]);

解析 :


此題需要用到等差級數觀念, 程式開頭應該是宣告兩個具有 101 個元素的陣列 a 與 b :

int a[101], b[101];

第一個迴圈用來給 b[1], b[2], ... b[100] 賦值, 元素的值就是索引本身, 即 :

b[1]=1, b[2]=2,..., b[99]=99, b[100]=100.

第二個迴圈用來給 a[0], a[1], a[2], .... a[100] 賦值, a[i]=b[i] + a[i-1], 即 :

a[0]=0
a[1]=b[1]+a[0]=1+0=1
a[2]=b[2]+a[1]=2+1=3
a[3]=b[3]+a[2]=3+3=6
a[4]=b[4]+a[3]=4+6=10
....
a[30]=b[30]+a[29]=30+a[29]
a[31]=b[31]+a[30]=31+a[30]
.....
a[50]=b[50]+a[49]=50+a[49]
......

題目要求 a[50]-a[30] 之值 :

a[50]=50+a[49]=50+49+a[48]=50+49+48+a[47]=50+49+48+47+46+45+....+32+31+a[30]

因此 a[50]-a[30]=50+49+48+47+....+32+31

利用等差級數和公式 :

50+49+48+....+2+1=50*(1+50)/2=1275
30+29+28+....+2+1=30*(31)/2=465

故a[50]-a[30]=1275-465=810, 答案為 (D)


5. 函數f 定義如下,如果呼叫f(1000),指令 sum=sum+i 被執行的次數最接近下列何者?



(A) 1000   (B) 3000  (C) 5000   (D) 10000

int f (int n) {
  int sum=0;
  if (n<2) {
    return 0;
    }
  for (int i=1; i<=n; i=i+1) {
    sum = sum + i;
    }
  sum = sum + f(2*n/3);
  return sum;
  }

解析 :

此程式用到了遞迴, 在函數 f 中前面的  if (n<2) 是遞迴的終止條件, 但 n=1000 判斷不會成立, 因此直接執行 for 迴圈進行 sum 的累加 1000 次. 接著在 sum = sum + f(2*n/3) 中呼叫自己形成遞迴, 這時作業系統會將 sum 放進記憶體的堆疊中, 然後執行 f(2*1000/3)=f(666), 注意 2*n/3 是無條件捨去. 同樣在呼叫 f(666) 中 n<2 也不符合, for 迴圈執行 666 次, 再次遞迴呼叫 f(666*2/3)=f(444), ..., 直到 n<2 成立為止傳回 0, 再從各層堆疊中取出 sum 計算總合. 每一層遞迴呼叫的 n 加起來就是 for 迴圈執行的總次數 :

1000+666+444+296+197+131+87+58+38+25+16+10+6+4+2=2980

故答案為 (B) 3000   (D) 10000

2023-02-26 補充 : 

一位網友指出此題解題錯誤, 答案應該是 900336, 所以最接近的應該是 (4) 10000 才對. 因為我把 sum=sum+i  看成 sum=sum+1, 所以才會誤將第一個迴圈結果認為是 1000, 每個遞迴的傳回值的累加結果應該是 :

sum=399836+177725+78935+34979+15476+6830+3002+1291+550+225+89+34+13+3=900336

但也有可能是 APCS 出題者把 sum=sum+1 誤打成 sum=sum+i (手算做大數加法很耗時間). 


6. List 是一個陣列,裡面的元素是element,它的定義如右。List 中的每一個element 利用next 這個整數變數來記錄下一個element在陣列中的位置,如果沒有下一個element,next 就會記錄-1。所有的element 串成了一個串列 (linked list)。例如在list 中有三筆資料 :


 1 2 3
 data='a'
 next=2
 data='b'
 next=-1
 data='c'
 next=1


它所代表的串列如下圖的上方之鏈結串列 :


RemoveNextElement 是一個程序,用來移除串列中current 所指向的下一個元素,但是必須保持原始串列的順序。例如,若current 為3 (對應到 list[3]),呼叫完RemoveNextElement 後,串列應為上圖中的下方鏈結串列, 請問在空格中應該填入的程式碼為何?

(A) list[current].next = current ;
(B) list[current].next = list[list[current].next].next ;
(C) current = list[list[current].next].next ;
(D) list[list[current].next].next = list[current].next ;



解析  : 

此題為鏈結串列觀念題, 題目雖長, 但卻不用紙筆推演. 欲刪除鏈結串列目前結點的下一個節點, 只要將下一個節點的鏈結拿來放到目前結點的鏈結中即可, 這樣下一個節點就從整個串列中脫鉤消失了.

若目前節點是 list[3], 亦即 data 為 'c' 者, 其下一個節點為 list[1] ('a') (因其 next=1), 而下下節點為 list[2] ('b'), 若要從串列中刪除 list[1] ('a'), 只要將 list[1] 的 next 設給 list[3] 的 next 即可. 目前串列索引若為 current, 則要被刪除的下一個節點之索引為 list[current].next, 它所指向之下下節點索引為 list[list[current].next].next, 目前節點的 next 只要設為此索引就可以直接指向下下節點了, 即 list[current].next=list[list[current].next].next, 答案為 (B).



7. 請問以 a(13,15)呼叫下列 a()函式,函式執 行完後其回傳值為何?

(A) 90  (B) 103  (C) 93  (D) 60




int a(int n, int m) {
  if (n < 10) {
    if (m < 10) {
      return n + m ;
      }
    else {
      return a(n, m-2) + m ;
    }
  }
else {
  return a(n-1, m) + n ;
  }
}

解析 : 

此題為遞迴函數問題, 呼叫 a(n,m) 時會先判斷 n 與 m 是否小於 10, 若 n>=10 就遞迴呼叫 a(n-1,m); 若 n<10 10="" m="">=10 遞迴呼叫 a(n,m-2); 遞迴終止條件是 n 與 m 都小於 10. 遞迴呼叫 a() 的過程表列如下 :  <10 :="" a="" m.="" m="9" n="" p="">

<10 10="" m="">

 呼叫 傳回值
 a(13,15) a(12,15)+13
 a(12,15) a(11,15)+12
 a(11,15) a(10+15)+11
 a(10,15) a(9,15)+10
 a(9,15) a(9,13)+15
 a(9,13) a(9,11)+13
 a(9,11) a(9,9)+11
 a(9,9) 18


最後呼叫 a(9,9) 時滿足 n 與 m 均小於 10 的遞迴終止條件傳回 18, 從層層堆疊中回來時把傳回值從底下依序代入上表中的 a(), 最後的傳回值就是所有後面數字總和, 即 :

18+11+13+15+10+11+12+13=103

答案是 (B)


8. 一個費式數列定義第一個數為0 第二個數為1 之後的每個數都等於前兩個數相加,如下所示:

0、1、1、2、3、5、8、13、21、34、55、89…。

下列的程式用以計算第N 個(N≥2)費式數列的數值,請問 (a) 與 (b) 兩個空格的敘述(statement)應該為何?


int a=0;
int b=1;
int i, temp, N;
…
for (i=2; i<=N; i=i+1) {
  temp = b;
  (a) ;
  a = temp;
  printf ("%d\n", (b) );
  }



(A) (a) f[i]=f[i-1]+f[i-2]        (b) f[N]

(B) (a) a = a + b                     (b) a

(C) (a) b = a + b                     (b) b

(D) (a) f[i]=f[i-1]+f[i-2]         (b) f[i]


解析 :

此費伯納西數列程式中, a 是前數, b 是後數, 演算法是在迴圈中進行, 先將原後數先放在 temp 中暫存 (因為演算後它要變成前數, 但演算時 b 會被改變), 然後計算新的後數 b=a+b 並列印出來, 最後將暫存於 temp 的元後數改存至 a 變成前數, 故答案是 (C)


9. 請問下列程式輸出為何?

(A) 1    (B) 4     (C) 3      (D) 33



int A[5], B[5], i, c;
…
for (i=1; i<=4; i=i+1) {
  A[i] = 2 + i*4;
  B[i] = i*5;
  }
c = 0;
for (i=1; i<=4; i=i+1) {
  if (B[i] > A[i]) {
    c = c + (B[i] % A[i]);
    }
  else {
    c = 1;
    }
  }
printf ("%d\n", c);


解析 :  

此程式第一個迴圈是為 A, B 陣列賦值, 迴圈執行完後兩陣列內容如下 :

A={6,10,14,18}
B={5,10,15,20}

第二個迴圈拜訪陣列計算 c 的值, 追蹤如下 :

i=1 : 因 B[1]=5 小於 A[1]=6, 故 c=1
i=2 : 因 B[2]=10 等於 A[2]=10, 故 c=1
i=3 : 因 B[3]=15 大於 A[3]=14, 故 c=1+(15%14)=1+1=2
i=4 : 因 B[4]=20 大於 A[4]=18, 故 c=2+(20%18)=2+2=4

故答案為 (B) 4


10. 呼叫下列 g() 函式,g(13) 回傳值為何?

int g(int a) {
  if (a > 1) {
    return g(a - 2) + 3;
    }
  return a;
  }

(A) 16      (B) 18       (C) 19        (D) 22

解析 : 

g() 為遞迴函數, 終止條件為 a 小於等於 1, 追蹤如下 :

呼叫 g(13) 回傳 g(11) + 3, 呼叫 g(11) 回傳 g(9) + 3, 呼叫 g(9) 回傳 g(7) + 3, 呼叫 g(7) 回傳 g(5) + 3, 呼叫 g(5) 回傳 g(3) + 3, 呼叫 g(3) 回傳 g(1) + 3, 呼叫 g(1) 回傳 1 結束遞迴, 回傳結果為 :

g(13)=1+3+3+3+3+3+3=1+3*6=19, 答案為 (C).


11. 定義 a[n] 為一陣列(array),陣列元素的指標為0 至n-1。若要將陣列中a[0]的元素移到a[n-1],下列程式片段空白處該填入何運算式?

int i, hold, n;
…
for (i=0; i<=      ; i=i+1) {
  hold = a[i];
  a[i] = a[i+1];
  a[i+1] = hold;
  }

(A) n+1   (B) n    (C) n-1    (D) n-2

解析 : 

此題意思是要將陣列頭元素 a[0] 一步一步往後移到陣列尾 a[n-1] 位置, 其他元素往前移一格. 程式使用迴圈來逐一搬移元素, 當 i=n-2 時, a[0] 已來到 a[n-2] 位置, 只要跟 a[n-2] 交換就來到陣列尾了, 故答案為 (D) n-2.

n 元素陣列只要 n-1 次就可以將陣列頭移到陣列尾, 因為從 0 起算, 故迴圈最後索引為 n-2.


12. 給定下列函式 f1() 及 f2()。f1(1)運算過程中,以下敘述何者為錯?

void f1 (int m) {
  if (m > 3) {
    printf ("%d\n", m);
    return;
    }
  else {
    printf ("%d\n", m);
    f2(m+2);
    printf ("%d\n", m);
    }
  }
void f2 (int n) {
  if (n > 3) {
    printf ("%d\n", n);
    return;
    }
  else {
    printf ("%d\n", n);
    f1(n-1);
    printf ("%d\n", n);
    }
  }

(A) 印出的數字最大的是4
(B) f1 一共被呼叫二次
(C) f2 一共被呼叫三次
(D) 數字2 被印出兩次

解析 : 

追蹤如下 :
呼叫 f1(1), 輸出 1, 呼叫 f2(3), 輸出 3, 呼叫 f1(2), 輸出 2, 呼叫 f2(4), 輸出 4, 返回 f1(), 輸出 2, 返回 f2() 輸出 3, 返回 f1() 輸出 1. 故 (A) 印出最大數字是 4 正確, (B) f1 被呼叫二次也正確, (C) f2 被呼叫 3 次錯誤, 應為 2 次, (D) 數字 2 被印出 2 次正確. 故答案是 (C).


13. 右側程式片段擬以輾轉除法求 i 與 j 的最大公因數。請問while 迴圈內容何者正確?

i = 76;
j = 48;
while ((i % j) != 0) {
  ________________
  ________________
  ________________
  }
printf ("%d\n", j);

(A) k = i % j;
       i = j;
       j = k;
(B) i = j;
      j = k;
      k = i % j;
(C) i = j;
      j = i % k;
     k = i;
(D) k = i;
      i = j;
      j = i % k;

解析 : 

輾轉相除法作法是以較大的數當被除數, 較小的數當除數, 相除後將原來的除數當被除數, 餘數當除數繼續相除, 直到餘數為 0 時, 最後的除數就是最大公因數 (GCD), 故答案為 (A).

其中 k 是 % 運算而得的餘數, i=j 就是把原除數當被除數, 而 j=k 就是把餘數當新的除數.


14. 下列程式輸出為何?

void foo (int i) {
  if (i <= 5) {
    printf ("foo: %d\n", i);
    }
  else {
    bar(i - 10);
    }
  }
void bar (int i) {
  if (i <= 10) {
    printf ("bar: %d\n", i);
    }
  else {
    foo(i - 5);
    }
  }
void main() {
  foo(15106);
  bar(3091);
  foo(6693);
  }

(A) bar: 6
       bar: 1
       bar: 8
(B) bar: 6
      foo: 1
      bar: 3
(C) bar: 1
      foo: 1
      bar: 8
(D) bar: 6
      foo: 1
     foo: 3

解析 : 

此題為兩個函數 foo(i) 與 bar(i) 互相呼叫, 在 foo() 中當 i 大於 5 時 呼叫 bar(i-10), 否則印出 "foo" 與 i 值; 在 bar() 中當 i 大於 10 時 呼叫 foo(i-5), 否則印出 "bar" 與 i 值, 因此 i 會在互相呼叫中遞減 5 或 10, 呼叫 foo(i) 時 i 每隔 15 又會呼叫 foo(i); 同理, 呼叫 bar(i) 時 i 每隔 15 又會呼叫 bar(i); 因此可用 i%15 來推測傳入大數值 i 的執行結果, 因為若 i 很大時要一步步追蹤執行結果是很花時間的. 不過由於判斷的門檻是 5 與 10, 因此求 i%15 時不要除盡, 要讓餘數大於 15 做最後判斷:

呼叫 foo(15106) :
15106/15=1006 餘 16, 故下一步是呼叫 foo(16), 因大於 5, 故呼叫 bar(16-10)=bar(6), 因 i 小於 10 故停止交互呼叫而輸出 bar:6.
呼叫 bar(3091) :
3091/15=205 餘 16, 故下一步是呼叫 bar(16), 因大於 10, 故呼叫 foo(16-5)=foo(11), 因 i 大於 5 故呼叫 bar(11-10)=bar(1), 因 i 小於 10 故停止交互呼叫而輸出 bar:1.
呼叫 foo(6693) :
6693/15=445 餘 18, 故下一步是呼叫 foo(18), 因大於 5, 故呼叫 bar(18-10)=bar(8), 因 i 小於 10 故停止交互呼叫而輸出 bar:8.

故答案是 (A). bar: 6     bar: 1    bar: 8


15. 若以f(22)呼叫右側f()函式,總共會印出多少數字? 

(A) 16     (B) 22      (C) 11     (D) 15


void f(int n) {
  printf ("%d\n", n);
  while (n != 1) {
    if ((n%2)==1) {
      n = 3*n + 1;
      }
    else {
    n = n / 2;
    }
  printf ("%d\n", n);
  }
}


解析 : 

此題之 f() 內有一個無窮迴圈, 終止條件為 n=1, 呼叫 f(22) 過程追蹤如下 :

呼叫 f(22)  ->    印出 22
進入迴圈
迴圈   n    n%2   印出        
  1    22    0    n=n/2=11  
  2    11    1    n=3*n+1=34
  3    34    0    n=n/2=17
  4    17    1    n=3*n+1=52
  5    52    0    n=n/2=26
  6    26    0    n=n/2=13
  7    13    1    n=3*n+1=40
  8    40    0    n=n/2=20
  9    20    0    n=n/2=10
 10    10    0    n=n/2=5
 11     5    1    n=3*n+1=16
 12    16    0    n=n/2=8
 13     8    0    n=n/2=4
 14     4    0    n=n/2=2
 15     2    0    n=n/2=1

迴圈跑了 15 次印出 15 個數字, 加上進函數時印出的 20, 加起來一共 16 個, 故答案是 (A).


16. 下列程式執行過後所輸出數值為何?

(A) 11      (B) 13       (C) 15        (D) 16


void main () {
  int count = 10;
  if (count > 0) {
    count = 11;
    }
  if (count > 10) {
    count = 12;
    if (count % 3 == 4) {
      count = 1;
      }
    else {
      count = 0;
      }
    }
  else if (count > 11) {
    count = 13;
    }
  else {
    count = 14;
    }
  if (count) {
    count = 15;
    }
  else {
    count = 16;
    }
  printf ("%d\n", count);
  }

解析 :  

此程式第一個 if 成立, count 被改為 11; 故第二個 if 也成立, count 被改為 12, 但 count%3 為 0, count 被改為 0, 最後一個 if 不成立, count 被改為 16, 故答案為 (D) 16.


17. 右側程式片段主要功能為:輸入六個整數,檢測並印出最後一個數字是否為六個數字中最小的值。然而,這個程式是錯誤的。請問以下哪一組測試資料可以測試出程式有誤?

(A) 11 12 13 14 15 3
(B) 11 12 13 14 25 20
(C) 23 15 18 20 11 12
(D) 18 17 19 24 15 16

#define TRUE 1
#define FALSE 0
int d[6], val, allBig;
…
for (int i=1; i<=5; i=i+1) {
  scanf ("%d", &d[i]);
  }
scanf ("%d", &val);
allBig = TRUE;
for (int i=1; i<=5; i=i+1) {
  if (d[i] > val) {
    allBig = TRUE;
    }
  else {
    allBig = FALSE;
    }
  }
if (allBig == TRUE) {
  printf ("%d is the smallest.\n", val);
  }
else {
  printf ("%d is not the smallest.\n", val);
  }
}

解析 : 

此題程式錯誤處在於比較結果旗標 allBig 的最後狀態取決於陣列的最後元素 d[6] 與 val 比較之結果, 與 d[1]~d[5] 無關, 這四組輸入之 allBig 結果如下 :

(A) 11 12 13 14 15 3    allBig=TRUE
(B) 11 12 13 14 25 20  allBig=TRUE
(C) 23 15 18 20 11 12  allBig=FALSE
(D) 18 17 19 24 15 16 allBig=FALSE

(A) 的最後 1 個數 3 確實是 6 個中最小的, 程式執行結果正確; (B) 的 20 並非最小, 但卻被 d[5]=25 大於 20 改成 TRUE, 執行結果錯誤, 故答案為 (B).  (C) 的 12 因為大於 d[5]=11 使得 allBig 被改為 FALSE, 執行結果正確, 但檢測不出程式是錯的, (D) 也是如此.

正確的寫法應該將下列錯誤程式碼 :

  if (d[i] > val) {
    allBig = TRUE;
    }
  else {
    allBig = FALSE;
    }

改為如下 :

  if (d[i] <= val) {
    allBig = FALSE;
    }

即預先假定 val 是最小的, 但拜訪陣列過程中, 只要 d[1]~d[5] 中有任何一個不大於 val, 就把旗標 allBig 改為 FLASE.


18. 程式編譯器可以發現下列哪種錯誤?

(A) 語法錯誤   (B) 語意錯誤    (C) 邏輯錯誤     (D) 以上皆是

解析 :

編譯器只能找出語法錯誤, 無法查知語意與邏輯錯誤, 答案為 (A).


19. 大部分程式語言都是以列為主的方式儲存陣列。在一個8x4 的陣列(array) A 裡,若每個元素需要兩單位的記憶體大小,且若A[0][0]的記憶體位址為 108 (十進制表示),則
A[1][2]的記憶體位址為何?


(A) 120    (B) 124     (C) 128      (D) 以上皆非

解析 :

陣列在記憶體中為連續排列, A[8][4] 的二維陣列

A[0][0]  + 0
A[0][1]  + 1
A[0][2]  + 2
A[0][3]  + 3
A[1][0]  + 4
A[1][1]  + 5
A[1][2]  + 6
A[1][3]  + 7
........

因此 A[1][2] 是從 A[0][0] 開始算 + 6 個元素, 若每個元素占 2 個記憶單位, 則差距是 2*6=12 個記憶單位, 故 A[1][2] 的位址是 108+12=120, 答案為 (A).


20. 下列為一個計算n 階層的函式,請問該如何修改才會得到正確的結果?

1. int fun (int n) {
2.   int fac = 1;
3.   if (n >= 0) {
4.     fac = n * fun(n - 1);
5.     }
6.   return fac;
7.   }

(A) 第2 行,改為 int fac = n;
(B) 第3 行,改為if (n > 0) {
(C) 第4 行,改為fac = n * fun(n+1);
(D) 第4 行,改為fac = fac * fun(n-1);

解析 :

此題程式不管第二列 fac 是 1 還是 n, 呼叫 fun() 結果都是 0, 原因是遞迴的最後呼叫 fun(1-1)=fun(0) 時, fac=n*fun(n-1)=0*fun(-1)=0, 層層回傳的結果, 最後 fac 必定為 0, 關鍵是第 3 列的判斷式 if (n >= 0) 中含有等於 0, 只要去掉 = 就可以了, 答案是 (B). (C) 不可能, 因為 n+1 會越來越大, 不可能收斂. (D) 也不行, 因為 if (n >= 0) 還是會讓它最後傳回 0.


21. 下列程式碼,執行時的輸出為何?

void main() {
  for (int i=0; i<=10; i=i+1) {
    printf ("%d ", i);
    i = i + 1;
    }
  printf ("\n");
  }

(A) 0 2 4 6 8 10
(B) 0 1 2 3 4 5 6 7 8 9 10
(C) 0 1 3 5 7 9
(D) 0 1 3 5 7 9 11

解析 :

由於迴圈內有兩個 i=i+1, 因此每次迴圈 i 會增量 2, 即從 0 開始, 2, 4, 6, ... 10, 故答案是 (A).

22. 下列 f() 函式執行後所回傳的值為何?

int f() {
  int p = 2;
  while (p < 2000) {
    p = 2 * p;
    }
  return p;
  }

(A) 1023
(B) 1024
(C) 2047
(D) 2048

解析 :

追蹤此函數執行結果 :

迴圈     p
   0        2
   1        2*2
   2        2*2*2
   3        2*2*2*2
  .....      .....
1000     2*2*2*2......*2  (共 1001 項)

這裡最後的 p 有 1001 個 2 相乘, 因為當 p=1000 時, 2*p=2000 剛好跳出無窮迴圈, 因此加上初始的 p=2 總共是 1001 個 2 相乘=2**1001=2048, 答案是 (D).


23. 下列 f() 函式 (a), (b), (c) 處需分別填入哪些數字,方能使得 f(4) 輸出 2468 的結果?

int f(int n) {
  int p = 0;
  int i = n;
  while (i >=  (a)   ) {
    p = 10 –  (b)  * i;
    printf ("%d", p);
    i = i -   (c)   ;
    }
  }

(A) 1, 2, 1     
(B) 0, 1, 2      
(C) 0, 2, 1
(D) 1, 1, 1

解析 :

解此種題目看起來似乎無捷徑, 就是將 A,B,C,D 四個選項一一代進去驗證結果, 不過若先觀察程式特徵, 可以快速剔除錯誤選項, 用排除法迅速找出正確答案. 此程式中 p=10-b*i 決定 p 值, 當呼叫 f(4), 在第一次迴圈裡 p=10-b*4, 若要輸出 p=2, 則 b 須為 2, 四個選項中僅 (A) 與 (C) 符合, (B) 與 (D) 就出局了.

(A) 與 (C) 僅 a 不同, 亦即迴圈的終止條件不同, (A) 是 i >=1 而 (C) 是 i>=0, 每次迴圈 i 會遞減 1, 因此 (A) 會輸出 4 個數字 (對應 i=4,3,2,1); 而 (C) 則會輸出 5 個數字 (對應 i=4,3,2,1,0), 最後輸出的數字是 10 (因最後一圈 i=0, 故 p=10-2*0=10), 題目要的是 2468 四個數字, 故答案是 (A).

追蹤 (A) 輸出結果為 2468, 而 (C) 則是 246810.


24. 右側g(4)函式呼叫執行後,回傳值為何?

(A) 6     (B) 11      (C) 13       (D) 14
int f (int n) {
  if (n > 3) {
    return 1;
    }
  else if (n == 2) {
    return (3 + f(n+1));
    }
  else {
    return (1 + f(n+1));
    }
  }

int g(int n) {
  int j = 0;
  for (int i=1; i<=n-1; i=i+1) {
    j = j + f(i);
    }
  return j;
  }

解析 :

此為遞迴函數題目, 追蹤如下 :

呼叫 g(4) :
進入 for 迴圈跑三圈, i=1~3, j=j+f(i)
i=1 時 : j=0+f(1)
呼叫 f(1) 回傳 1+f(2), 呼叫 f(2) 回傳 3+f(3), 呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=f(1)=1+3+1+1=6;
i=2 時 : j=6+f(2)
呼叫 f(2) 回傳 3+f(3), 呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=6+f(2)=6+3+1+1=11;
i=3 時 : j=11+f(3)
呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=11+f(3)=11+1+1=13;

答案是 (C) 13.


25. 下列Mystery()函式else 部分運算式應為何,才能使得 Mystery(9) 的回傳值為34。

int Mystery (int x) {
  if (x <= 1) {
  return x;
  }
else {
  return ____________ ;
  }
}

(A) x + Mystery(x-1)
(B) x * Mystery(x-1)
(C) Mystery(x-2) + Mystery(x+2)
(D) Mystery(x-2) + Mystery(x-1)

解析 :

這也是遞迴題目, 終止條件為 x <= 1, 四個選項中的 (C) 呼叫了 Mystery(x+2), x 會持續增加無法收斂, 故先排除. 另外 (B) 為乘法, 遞迴第一層為 9*Mystery(8), 最後一層為 2*Mystery(1)=2, 這兩項乘積為 18, 再乘以中間層 (都是整數) 最後等於 34 是不可能的, 因此也排除, 剩下 (A) 與 (D).

追蹤 (A) :
呼叫 Mystery(9), 回傳 9+Mystery(8), 呼叫 Mystery(8), 回傳 8+Mystery(7), 呼叫 Mystery(7), 回傳 7+Mystery(6), ... , 呼叫 Mystery(2), 回傳 2+Mystery(1), 呼叫 Mystery(1), 回傳 1, 故最後回傳結果是 9+8+7+6+.....+2+1=(9+1)*9/2=45, 不是 34, 故排除 (A), 答案應是 (D).

追蹤 (D) :
呼叫 Mystery(9), 回傳 Mystery(7)+Mystery(8), 呼叫 Mystery(8), 回傳 Mystery(6)+Mystery(7), 呼叫 Mystery(7), 回傳 Mystery(5)+Mystery(6), ... , 呼叫 Mystery(3), 回傳 Mystery(1)+Mystery(2), 呼叫 Mystery(2), 回傳 Mystery(0)+Mystery(1), 呼叫 Mystery(1) 回傳 1, 呼叫 Mystery(0) 回傳 0. 故呼叫 Mystery(2) 與 Mystery(1) 結果均為 1, 分兩段計算 :
Mystery(9)=Mystery(7)+Mystery(8)=Mystery(5)+Mystery(6)+Mystery(6)+Mystery(7)=Mystery(5)+Mystery(6)+Mystery(6)+Mystery(5)+Mystery(6)=2*Mystery(5)+3*Mystery(6)
Mystery(5)=Mystery(3)+Mystery(4)=Mystery(1)+Mystery(2)+Mystery(2)+Mystery(3)=Mystery(1)+Mystery(2)+Mystery(2)+Mystery(1)+Mystery(2)=5
Mystery(6)=Mystery(4)+Mystery(5)=Mystery(2)+Mystery(3)+Mystery(5)=Mystery(2)+Mystery(1)+Mystery(2)+Mystery(5)=3+Mystery(5)=8
故 Mystery(9)=2*Mystery(5)+3*Mystery(6)=2*5+3*8=34

呵呵, 花了四天時間斷斷續續終於解完 25 題了, 發現遞迴函數考蠻多的哩! 其次是迴圈運算, 而且有些題目需要用到基礎數學概念如級數和與最大公因數等等. 另外像第 14 題則需要一點小小的求餘數技巧, 總之, 不只是考程式而已, 也考一些數學觀念.