伍、軟體程式設計與分析


本次製作筆者所使用的工具是Arduino官方所提供的開發整合環境(Integraed Development Environment,又簡稱為IDE),而所使用的語言則是由他們所開發類似C但又帶點JAVA風格的語言,只是語法較簡單且格式較自由。由於可能有些讀者還不曾接觸過Arduino這個產品,所以筆者打算從頭帶領各位從開發軟體的下載與安裝開始,一路到最後程式的設計與說明為止,如果已經有過Arduino程式設計經驗的讀者,請自行選擇跳過這章節前面基礎的部份。


五.1 Arduino整合開發軟體下載與安裝

俗語說”工欲善其事,必先利其器”,如果徒有性能強大的硬體裝置,卻缺少良好的軟體配合也是惘然,而Arduino官方開發團隊當然不會辜負這麼好的硬體模組板,因此也同時提供了一個功能強大的整合開發環境軟體,一般我們就稱為Arduino IDE。以目前筆者撰寫此文時Arduino IDE的版本已到1.6.3,如果還沒安裝的讀者請先連線到Arduino官方網站(http://www.arduino.cc),應該可以看到圖50的”Arduino官方網站首頁”畫面,接著點選左上方”Download”這個選項以進入下載開發軟體的頁面(如圖51所示)由於筆者是使用Win7的個人電腦系統所以就以此為範例說明,讀者可看到圖中編號為1的圓圈便是目前最新的Arduino IDE版本(版本號碼為1.6.3),如果讀者看到本文時去下載的話,由於時間的落差,可能會看到更新的版本也不一定,不過整個下載與安裝的流程應該是大同小異!而網頁右上方用紅線框起來的是Windows版的開發程式,有兩種選項可以下載,其中編號2的是一般標準的Windows安裝版(Windows Installer),下載完成之後依照一般在Windows底下軟體安裝(Install)的步驟進行就可以。而編號3的則是單機獨立版(Windows ZIP file),也就是說下載後,解壓縮之後便可以直接執行,不需要再經過安裝的過程;很多綠色自由軟體似乎都是這樣,兩者在使用上並沒有什麼差異,只是後者對於整個開發環境軟體程式的管理(例如刪除或反安裝等)都必須自己處理就是了。

圖50.最新Arduino官方網站首頁

圖51.Arduino IDE下載選擇頁面


在圖51中不管點選了那一中Windows版的IDE開發程式,接下來都會進入圖52的下載頁面,在網頁的右下方有兩個下載的選項,第1項是直接下載,而第2個則是贊助他們後再下載;說實在Arduino社群的這些朋友們不計酬勞讓大家享用他們的辛苦成果,的確是讓人很欽佩!如果可以的話贊助他們一些也不為過,不過由於是跨國界的付款,假如沒有適當的信用卡好像還無法共襄盛舉,所以大家如果無法贊助的話就記得要感念在心頭就是了。


圖52.Arduino IDE下載頁面


當Arduino IDE下載及安裝(或解壓縮)完成後,我們如果打開Arduino IDE所在的資料夾來看的話,其內容如圖53所示,其中編號1的便是Arduino IDE的主程式,只要點擊它便可以起動程式。而編號2的libraries」資料夾是Arduino IDE的函式庫資料夾所在之處,也是這個整合開發工具的精華之所在,因為其中包括了眾多由Arduino開發團隊所撰寫設計的一些內建函式,其中大部份都和擴充應用模組(shield)、週邊裝置及感測器有關,例如液晶顯示器(LCD)、非同步串列傳輸(Serial/UART)、伺服馬達(Servo Motor)步進馬達(Stepper Motor)、網際網路(Ethernet)、無線網路(WIFI)及各式各樣的感測器(Sensor)如溫度、濕度、壓力、紅外線‧‧‧等等!可說是琳琅滿目種類繁多,這讓許多的使用者能夠很容易的去完成他們的程式設計,難怪那麼多人會喜歡使用它!

圖53.Arduino IDE資料夾內容


至於編號3的「drivers」資料夾是存放所有Arduino家族模組板的USB轉UART/RS232介面晶片的驅動程式,當第一次把Arduino模組板插上你的電腦時就和所有使用USB介面的裝置一樣,電腦都會偵測到並自動去安裝驅動程式,以Win7以上的系統來說,幾乎不用耽心會有找不到驅動程式的問題,可是如果你的電腦還是Windows XP系統的話,由於Arduino家族不同模組板使用了不同的USB轉UART/RS232介面晶片,Windows XP系統常會無法自動偵測到及安裝驅動程式,這時就必須用手動的方式將驅動程式的來源指向這個“drivers”資料夾的路徑與位置。

編號4的「examples」資料夾是存放所有Arduino的應用範例程式,更是Arduino IDE軟體的精華所在,前面提到的程式庫中所有的種類都有相關的應用範例可供參考,而且大部份程式庫還不只一個應用範例,可說是內容豐富;只要能把這些範例程式弄清楚,大概就可以稱為Arduino的高手了,建議有興趣而且想學好Arduino的讀者不妨把它們叫出來仔細研究一下!

當程式正常起動時會先看到圖54-1的起動畫面,完成後接著跳到圖55的新建程式畫面,在圖54-1畫面中顯示的是最新版IDE(1.6.3)起動畫面,和之前的版本(圖54-2)有很明顯的不同之處,我們除了可以看到Arduino的官方Logo(編號為1的紅色框框處)之外,眾開發團隊人員的姓名在此已隱而不見,取而代之的是編號2紅色框框的介紹連結網址(arduino.cc/credits);且右邊的圖案也改成類似前面提到過的Fritzing軟體的零件標記,不管如何我們還是要記得向這些熱心的英雄們致敬哦!有拜有庇佑,這樣也許在寫程式時比較不容易出錯^_^! 

圖54-1‧新版Arduino 1.6.3IDE起動畫面


圖54-2‧舊版Arduino IDE起動畫面

圖55‧Arduino IDE新建程式畫面

  

當進入圖55的新建程式畫面時,左上方標記1的抬頭文字”sketch_apr20a| Arduino 1.6.3” 左邊是這個新建立程式的名稱,右邊的文字代表的是這個IDE整合開發環境的版本(1.6.3版);而如果各位讀者去翻英文字典查一下”sketch”這個單字的意義可以得知代表『素描』或『草稿』的意思;在前面筆者曾經說過Massimo Banzi先生等Arduino開發團隊當初在設計這個系統時本來就是針對一些非電子專業人士,而且是一些藝術或是創意工作者,所以他們對這些程式檔案也視為藝術工作的內容,因此才會取”sketch”這個名字作為檔案的預設名稱。而後面部份則是這個檔案的新建日期,其中” apr20”代表四月(April)20日,然後依照檔案建立的順序由a、b、c、…這樣一路延伸下去,因此為了讓檔案能更具有意義及可讀性,使用者最好把每一個新的檔案另存(即”Save As”)成不同的名稱。

而在圖55新建程式畫面右下方標記為2之處的文字內容則分別代表所使用的Arduino模組板型號(在此為Duemilanove板),及在個人電腦上所佔用的串列通訊埠(即COM10),如果這組訊息與使用者目前實際的情形不合時,我們就必須先動手去修改它才行。修改的方式分成兩個部份,當要修改Arduino模組板型號時,如圖56所示,首先我們用滑鼠點選標記1的”Tools”下拉功能選項,然後點選標記2”Board”這個選項,這時Arduino IDE便會把目前所有可用的模組板型號都列在右方的標記3之處,此時使用者便可依自己的需要點選正確的模組板型號。

圖56‧Arduino IDE選取模組板型號畫面

圖57‧Arduino IDE使用電腦通訊埠選擇畫面


如果要修改Arduino模組板所使用的電腦通訊埠時則可依圖57中所示的步驟,首先和前面一樣先我們用滑鼠點選標記1的”Tools”下拉功能選項,然後點選標記2”Port”這個選項,同樣的Arduino IDE會把目前我們電腦中所有可用的通訊埠都列在右方的標記3之處,此新版的Arduino IDE 1.6.3還會把接在這個埠的模組型號(在此為Leonardo模組板)也一並顯示出來,接著使用者便可依自己的需要點選正確的通訊埠號了。當上述Arduino模組板與所使用的電腦串列通訊埠都設定好之後,此IDE開發工具視窗的右下方便會呈現出使用者最後的設定內如容(即標記4之處),。

有時在我們的電腦中會存在著一個以上的串列通訊埠,如果使用者無法確認自己所使用的Arduino模組板是被分配到那一個串列通訊埠,最好先進到系統中檢查一下,筆者就分別以Windows XP和Win7兩種操作系統來為各為說明如。果你還在使用Windows XP系統的電腦,就請先用滑鼠點選螢幕左下方的『開始』按紐,等選擇表單彈出後找到『控制台』這個選項並按下滑鼠左鍵,此時可看到圖58的控制台內容畫面,接著從一堆的圖示選擇名為『系統』的這個元件,並點選它便可進入到圖59的系統選項內容畫面不過要先將頁面切到『硬體』這個頁面,接著按下『裝置管理員』這個按鈕,便可看到圖60的系統串列通訊埠檢視畫面,然後按下圖中紅色框框(名稱為『連接埠(COM和LPT)』)左方的『+』符號,將它的內容展開後,便可看到目前電腦中所有通訊埠的名稱與編號,這樣使用者便可以得知自己的Arduino模組板是使用了那一個連接埠了。

圖58‧Windows XP系統控制台內容畫面

圖59‧系統選項內容畫面

圖60‧Windows XP系統串列通訊埠檢視畫面


假如你的電腦OS是Win7的話,其實檢視的過程也差不多,同樣先用滑鼠點選螢幕左下方的『開始』圖示,等開始表單彈出後找到『控制台』這個選項然後按下滑鼠左鍵選它,如此便可看到圖61的控制台內容畫面,這時只要直接選取『硬體和音效』選項中用紅色框框起來的『檢視裝置和印表機』項,就會看到圖62的”Win7系統串列通訊埠檢視”畫面,而在『未指定』這一欄中便可以找到我們的Arduino模組板是佔用了那一個連接埠了。以圖63中即筆者的電腦來說,就是紅色框框起來的『Arduino UNO(COM12)』項目。

圖61‧Win7控制台內容畫面

圖62‧Win7系統串列通訊埠檢視畫面


理論上來說不管我們撰寫那一種語言的程式基本都是使用英文,以Arduino IDE這個開發工具所使用的語言來說是一種類似C又採用了一點JAVA的語法,只是又比這兩種語言簡單自由些;不管怎麼樣都是英文的環境,照理來說使用者對英文應該很熟,不過還是有許多朋友只對程式的指令與語法是OK,可是對工具中其他環境與操作部份的英文字彙還是不太能接受;而Arduino IDE中也針對不同的語系提供了支援,如果使用者想工作在中文的環境底下也是有路可行。首先如圖63所示,先點選Arduino IDE畫面左上方標記1的『File』,然後選擇標記2的『Preferences』功能,便可看見圖64的”環境與偏好設定”畫面;其中標記1之處是我們所寫程式的預設存放位置,如果要改變的話可以透過標記2的「Browse」按鈕去調整;其中標記3的『Editor Language』下拉清單即為Arduino IDE開發工具所使用的語言選擇項,如果要選用我們習用的繁體中文(標記4)語系,就必須往下移動滑桿到中間的位置才看得到。當這些設定都選取好之後,最後按下右下方的『OK』按鈕便大功告成。

不過要注意的是Arduino IDE開發工具並不會馬上改轉換成中文環境,必須先結束程式重新再啟動才行!圖65是圖63的中文化之後的結果,讀者們可以比較一下兩者的異同之處。

圖63‧Arduino IDE中文環境設定畫面一


圖64‧Arduino IDE中文環境設定畫面二


圖65‧Arduino IDE 1.6.3中文環境畫面


依筆者實際測試的結果,Arduino IDE 1.6.3版之前版本的中文環境除了1.5.4版之外,似乎都有些問題,並不是所有的文字都會轉換成中文,只會在有些下拉選單中會間歇的參雜一些中文而已,這樣一來看起來就怪怪的;不過如果是之前的Arduino IDE 1.5.4版,當設定程中文環境時不管在Windows XP和Win7兩種電腦系統都可以正常轉換;除此之外,在起動Arduino IDE時,除了1.6.3版,其它的版本不管在Windows XP或Win7兩種電腦系統上,常常都會出現一些啟動異常的訊息,雖然最後都還是可以使用,不過感覺上就是怪怪的!所以有興趣使用Arduino IDE的讀者,強烈建議就採用目前最新的1.6.3版。


五.2 ArduinoIDE與模組板連接測試

  經過前面的一番選擇與設定後,感覺上Arduino IDE和我們所選用的模組板似乎可以連接使用了,不過即使電腦已經正確偵測及安裝好模組板的驅動程式,也不保證就可以正常下載燒錄寫好的程式;如果是第一次使用Arduino IDE這個開發系統的使用者,或者說要使用新的模組板,一般來說最好先試燒錄一些程式看看。在太極拳中第一招叫『起手式』,而在寫C語言或者JAVA語言的程式時都會先寫一個起手程式,就是在螢幕上顯示”Hello World!”這個字串;一樣的Arduino也有一公認的起手程式,就是【Blink】這個內建的範例程式,由於它已經內建在Arduino IDE裏面,所以即使還沒學過Arduino語言程式的新手,也可以藉由開啟這個範例的方式來測試Arduino IDE與模組板的狀態。接下就讓我們看看如何進行這個測試的過程。

圖66‧開啟Arduino IDE內建範例Blink程式畫面


  請參考圖66的Arduino IDE畫面,首先我們用滑鼠點選螢幕左上方標記1的『檔案』,等下拉選擇頁面展開之後,將滑鼠移到標記2的『範例』,這時應該可以看到一卡車的範例程式列表彈出來供使用者選擇,接著請展開標記3名稱為『01.Basic』的選項,此時可在其中看到『Blink』(標記4)這個範例程式,最後點選它,便可以看見圖67這個範例程式Blink的內容畫面了。

圖67‧範例程式Blink內容畫面


  前面電路設計與分析的章節中,在介紹圖44的”Arduino Uno模組板外觀圖”時筆者曾經說過,幾乎每一片Arduino模組板上都有一顆LED燈並接在編號為13的數位接腳上,當13腳輸出High時便會點亮這顆LED燈,如果輸出Low則會熄滅;而『Blink』這個範例程式會讓這顆LED燈點亮一秒後熄滅一秒,並且不斷的重複下去。在Arduino IDE中寫好的程式一樣要先經過編譯才能載入/燒錄到模組板的單晶片內,圖67左上方標記1的【】按鈕符號稱為「Verify」,直接翻譯為”驗證”,其實也就是一般程式中所謂”編譯”(Compiling)的意思;而旁邊另一個標記為2的【】按鈕符號稱為「Upload」,直譯為”上傳”,按下它便則會連續執行”編譯”與”載入”(即燒錄)兩個動作。如果上傳燒錄程式成功,則會如圖68所示,在Arduino IDE下方的狀態視窗中出現標記1【上傳完畢】與標記2的「avrdude done. Thank you.」訊息;假如編譯發生錯誤或是燒錄失敗,下方的狀態視窗都會出現相關的訊息提醒使用者,使用者便可依訊息的內容找出問題再去修正,直到一切無誤且燒錄成功為止。由於Arduino IDE1.5.7版之後的環境都會自動在視窗的左方替程式加上行號,因此當有錯誤發生時,只要依下方的狀態視窗中標示的錯誤行列處尋找,便可以很容易的找到bug並予以修正。

如果一切都正確無誤的話,使用者便可看到Arduino Uno模組板上編號為13的數位接腳LED燈以亮一秒滅一的節奏閃爍著;如果在開發測試的過程中出了一些狀況無法確定是什麼問題時,筆者都會以燒錄『Blink』這個範例程式來測試系統的狀態,建議讀者們也可以比照辦理。

圖68‧範例程式Blink編譯完成/燒錄成功畫面


五.3 ArduinoIDE程式語言架構說明

  在Arduino IDE中所使用的程式語言基本上和一般習用的C語言幾乎是一模一樣,只是在結構和語法上比較簡單易學,由於本論壇製作的內容是假設讀者們已經具有基本的C語言設計觀念,因此除非有特殊之處,否則筆者不會去詳細說明程式設計的基本概念、指令或是函式、語法等等;如果沒學過C語言讀者就請先去找一些入門的書看一下,或者直接找一些Arduino相關的書也可以,否則當你閱讀後面的程式時可能會有些吃力!

  當我們啟動Arduino IDE時會看到圖55的”Arduino IDE新建程式”畫面,在其中Arduino IDE已經幫我們設定好兩個基本的架構了,分別是:

void setup(){

}

void loop() {


這兩個基本結構是一定不能缺少,前者稱為”設定” (setup),主要是用來初始化一些變數、I/O腳位的輸出/入模式或是執行一些只會作一次的動作;這些起始動作執行完之後程式便進入後面的無窮迴圈(loop)部份一直重複執行,直到關閉電源為止。前面說過Arduino IDE程式語言基本上就是C語言的一種,而學過C語言讀者應該知道,C語言的基本結構就是一個main()函式,可是在這個Arduino IDE新建程式中卻看不到main()函式的蹤影!其實在Arduino IDE編譯程式時會產生一個中介檔案,而這個中介檔案會先產生一個main()函式的外框,然後再把上述兩個基本結構也就是我們所撰寫的程式內容包含進去,成為一個新的檔案後再重新編譯,以產生最終的目的檔。如果我們打開這個中介檔案可看到下列的內容:

int main(void)

{

init();

setup();

for(;;)

loop();

return 0;


也就是說Arduino IDE會先執行內建的init()函式來初始化Arduino系統的硬體裝置,然後呼叫setup()函式,最後由於loop()函式是在for(;;)這個無窮迴圈的控制之下,所以永遠不會停止,當然也就不會往下執行到”return 0”這段敘述了,而這才是Arduino程式的真正面目。


五.4 ArduinoIDE內建函式說明

  Arduino這個系統最為人稱道的就是種類繁多內容豐富的內建程式庫(libraries),或稱為內建函式(function)庫,而這些函式可分成兩大類,一是不用經過任何宣告或是引入(include)程序就可直接在程式中呼叫使用的,在此筆者稱為「基本型」內建函式;另外一種則是必須在程式的最前面使用引入(#include)這個前置敘述指令才能呼叫到的,在此筆者稱為「引入型」內建函式,這種內建函式全部都存放在前面圖53的”Arduino IDE資料夾內容”中名稱為”libraries”的資料夾裏面,由於它們的種類與數目繁多,所以筆者在此僅針對本次製作中有使用到的部份來為各位說明。

五.4.1 基本型內建函式

  不用經過任何宣告或是引入(include)程序就可直接在程式中呼叫使用的函式,又稱為基本型內建函式,本次製作使用到的有下列幾個:

1.delay(unsigned long ms)

幾乎每一種單晶片微電腦的程式都會用到這一類最基本的時間延遲函式,在Arduino IDE中有幾個和時間有關的函式,這個是專用來延遲以毫秒也就是ms(10-3秒)為單位的時間之用,其中”ms”這個參數的資料型態為” unsigned long”,共佔用4個bytes,也就是說等於232,換算結果最長約可以延遲到50天左右,所以應該沒有時間不夠長的問題了。


2.millis()

當Arduino 模組板接上電源起動時如果板子上的晶片已經有燒入程式,則系統內部就會啟動一個計時器開始計時,而其計時的長度為4個位元組(byte),時間的單位為毫秒(mS,10-3秒),也就是說等於23210-3秒,約50天左右會重複,此函式便是在取得呼叫時當下的計時值;這個函式在使用時不需要參數,但會回傳一資料型態為unsigned long的值。此函式和上一個delay()主要的差異是在執行delay()函式時系統只能等待不能再去處理其他的事情,而millis()則只是取得當時的計時值,如果我們想要一方面執行其它的指令,但又希望知道經過多少時間的話就可以使用這個函式。

使用範例:

unsigned long ms

ms=millis();


.pinMode(pin,mode)

  此函式是用來設定數位接腳的輸入/輸出模式,在函式中有兩個參數,第一個參數”pin”是用來定義接腳的號碼,以Uno板來說共有20根數位腳,所以的”pin”值可從0~19;不過最後面6根在Uno板中又定義為類比輸入腳,所以一般都只用到0~13,使用者可依實際使用的狀況去決定。Arduino IDE的程式語言雖然說是和C語言一樣,不過它的要求卻很鬆散,以”pin”這個參數來說,並沒有硬性規定它的資料型態,以筆者實際使用的結果來看,可以是直接數字的常數值,可以是位元組(byte)或整數(int,unsigned int)型的變數;由於Arduino系統畢竟只是資源與功能有限的單晶片微電腦,因此除非有必要,否則所使用的資料變數型態越小越好,這樣才不會佔用太多的內部記憶體,所以如果要用變數去定義這個”pin”時,由於最大值不過是兩位數,所以最好使用位元組(byte)這個型態以節省記憶空間。

第二個參數”mode”是用來決定該接腳的輸入/輸出模式,一般來說有3種模式可以使用,即”INPUT”、”INPUT_PULL”和”OUTPUT”,其中”INPUT”是定義該接腳為高阻抗的輸入腳,而”INPUT_PULL”則是有內部提升電阻的輸入腳,至於”OUTPUT”則是定義為輸出腳了,要注意的是這些模式名稱都必須是大寫才行;此外也可以用1和0來分別代表輸入/輸出模式。

舉例來說,在圖67這個”Blink”範例程式中,行號20的程式:

pinMode(13,OUTPUT)

代表了設定數位接腳第13號為輸出腳的意思。


4.digitalRead(pin)

這個函式是用來讀取數位接腳的輸入狀態,在函式中僅有一個參數”pin”,是用來定義接腳的號碼,其範圍和上面的pinMode函式是一樣;當Arduino模組板接上電源時,所有的接腳都是處於輸入狀態,如果曾經將某一接腳設定為輸出,之後使用pinMode(pin,INPUT)這樣的函式指令便可以強制”pin”這根接腳回到輸入狀態。

在Arduino IDE中讀回的值有兩種,分別是代表低態的【LOW】或是高態的【HIGH】,此外這些值的內容都必須是大寫才行;不過很有意思的是當我們用下面的程式去讀取”pin”這根接腳的狀態時:

val=digitalRead(pin);

“val”這個變數應該設定為那一種資料型態呢?依筆者實際測試的結果,在Arduino IDE中“val”這個變數可設定為所有的變數資料型態,即使浮點數(float)都可以!這麼鬆散與自由的資料結構要求不知道該說是Arduino IDE優點還是缺點?當然這對使用者來說是很方便,尤其是對C語言不太熟的人,隨便怎麼設定都可以過關!可是如果不注意這些變數資料型態的差異的話,常常會在程式中遇到不知道該如何處理的bug,也就是說自由是要付出代價的!不過為了節省系統的資源,所以沒有必要盡量把這些變數的資料型態設定成佔用最少記憶體的型態,以上面的程式範例也就是【byte】囉!


5.digitalWrite(pin,value)

這個函式是將一個邏輯值(value)輸出至指定的數位接腳(pin)上,和上面一樣共有兩種邏輯值,分別是代表低態的【LOW】或是高態的【HIGH】,不過也可以用0或1來代表這兩種狀態。例如要點亮UNO模組板上內建於第13腳的LED燈時,下列兩種寫法都可以達成:

範例 : byte  pin=13;

digitalWrite(pin,HIGH);

         digitalWrite(13,1);

6.analogRead(pin)

前面說過Uno板最後面6根(14~19)接腳是定義為類比輸入腳,在Arduino IDE中不必去定義這些接腳的內容,就可以直接呼叫這個函式去讀取其上的輸入電壓值;由於Arduino這些模組板所使用的單晶片元件內含的類比至數位轉換器(ADC)其轉換的資料長度為10位元(bits),也就是說它的轉換結果為0~1023;此外如果沒有特別去設定,最大輸入電壓也就是滿刻度電壓預設為晶片所使用工作電壓(在UNO板中為5V)。

此外這6根(14~19)接腳類比輸入腳在Arduino IDE中,實際使用的編號是A0~A5,而非前面的D14~D19,這點還請注意!由於最大的轉換值為1023,所以當我們使用下列的程式去讀取”pin”這根接腳的輸入電壓值時,其中讀回的“val”這個變數至少應該設定為整數(int)以上的資料型態以符合它的範圍;當然就像前面提到的,Arduino IDE對於變數資料型態的要求很寬鬆,即使定時為無號整數(unsigned  int,short),或是長整數(long, unsigned  long)都可以,只是比較浪費內部記憶體而已。

範例 : int val;

val=analogRead(0);

7.Serial.begin(speed)

在Arduino IDE中並沒有像筆者之前所使用的8051系統單晶片那樣有硬體模擬器(ICE)可以使用,為了彌補這個缺點,它們在電腦端提供了一個監控視窗,而這個監控視窗可透過原來用以下載/燒錄程式之用的串列通訊埠接收各式Arduino模組板所傳回來的資料並顯示出來。其實在本次的製作中和這個串列通訊埠有關的程式或函式並未真正的用在系統的動作上,主要是用來在開發設計階段的偵錯之用而已;但是由於這個串列通訊埠是個很重要的工具,而且後面會介紹到的軟體模擬串列通訊埠函式時,功能與內容和這個部份差不多,所以筆者還是決定拿來詳細說明一下。

前面圖67的”範例程式Blink內容”畫面右上方編號3的按鈕【】,便是用來開啟此監控視窗,除此之外也可以像圖57的”Arduino IDE使用電腦通訊埠選擇”畫面那樣,點選標記1的”Tools”下拉功能選項,然後再點選中間的”Serial Monitor”這個選項去起動監控視窗,如果系統一切正常我們便可看到圖69的監控視窗畫面,不過要注意的是由於幾乎所有的Arduino模組板接收到電腦端的IDE起動串列監控視窗時,會送出一重置(Reset)信號到單晶片微電腦的重置腳上,也就是說會讓Arduino模組板產生重置的動作。

圖69‧Arduino IDE串列監控視窗畫面


在圖69的Arduino IDE串列監控視窗中左上方標記1的地方所顯示的是正在使用的Arduino模組板在電腦中所佔用的通訊埠號碼,右下方標記3的下拉清單則是用來選擇此串列通訊埠的資料傳輸速率,如果點選展開它的話可以看到,可選擇的速率從300、600、….一路加倍到115200baud,而此串列監控視窗系統的預設值為9600baud。除此之外關於串列通訊埠的協定部份系統已經設死,無法改變,就是使用8個資料位元、1個停止位元且不做同位檢查(No Parity Check)。至於標記2的下拉清單則是用來選擇當串列監控視窗發送資料時在資料的最後面要不要由系統自動加上結尾字元?在此有4種選擇:

1.No line ending:不加任何字元在發送資料串後面

2.Newline:加上一個換行用的ASCII碼,即NL(0ah)

3.Carriage return:加上一個游標回至最前面用的ASCII碼,即CR(0dh)

4.Both NL & CR:同時加上換行(NL)與回頭(CR)用的ASCII碼


至於標記4的空白行是使用者在串列監控視窗中輸入發送資料的地方,一般來說不希望使用者一次發送太多的資料,所以只有一行的空間;當使用者輸入完後可按下電腦的【Enter】鍵,或是標記5的【Send】按鈕將輸入的資料發送出去。最後在串列監控視窗中央標記6的大片空白區便是接收資料顯示的地方,在這個區域的資料可選擇是否要自動上捲換行(即點選左下角的「AutoScroll」查核方塊),而且只要有接收到任何新的資料都會自動的顯示出來。筆者覺得這個串列監控視窗中最大的缺點,就是此接收資料顯示區少了一個清除的功能鍵,當接收到的資料很多時,這個視窗就會顯得很擁擠,而且顯示資料的上下移動變得較難控制,如果要讓資料看起來清爽些,唯一的方法就是先關閉這個串列監控視窗後,重新再起動一次,不過這樣一來Arduino又會被重置。

在我們的電腦中如果有接上任何的Arduino模組板,可是並沒有去起動Arduino IDE中的串列監控視窗,那麼便可以在程式中使用到這些串列傳輸函式,以作為Arduino模組板與電腦的資料傳輸或通訊之用;可是一但開啟了這個在IDE中的串列監控視窗,那麼所有傳送至電腦的資料都會被它攔截,這點在使用上還請讀者們注意!

介紹完Arduino IDE的串列監控視窗後,接下來我們看看在Arduino IDE中有那些和串列監控視窗相關的串列傳輸函式,首先介紹最基本的開啟通訊埠函式:

Serial.begin(speed);

在使用任何一組硬體的串列通訊埠之前都要使用這個函式來宣告,這樣原來的數位I/O接腳D0與D1便分別被設定為【RX0】與【TX0】的功能,這個函式只有一個參數(speed),代表通訊埠所欲使用的資料傳輸速率(即Baud Rate);不過要注意的是像UNO這類的小尺寸模組板只有一個硬體的串列通訊埠,而像Mega或DUE那樣的大板子則有4組硬體的串列通訊埠,如果要使用另外3個硬體的串列通訊埠,則函式指令分別為:Serial1.begin(speed)、Serial2.begin(speed)、Serial3.begin(speed),此外後面相關的串列傳輸函式也都要加上相對應的1~3的數字在「Serial」這個指令保留字之後。


8.Serial.print() ,Serial.println()

上述兩個函式的功能幾乎一模一樣,都是會將文字或數字以ASCII碼的格式從【TX0】這隻接腳發送出去,只是後者會加上一個回頭(CR)及換行(NL)的動作(也就是加上鍵盤的【Enter】鍵的功能),所以在此只介紹前者。一般來說這個函式有兩種格式:

Serial.print(val)

Serial.print(val,format);

  第一種格式的寫法中的第一個參數(val)可以是數字可以是文字字串,不過不管是那一種,最後Arduino IDE都會將它們轉換成ASCII碼的格式後再發送出去。

至於第二種格式寫法中的第一個參數(val)主要是針對數字,而第二個參數(format)則是設定該數字的輸出資料格式,假如該數字是整數型 (例如char、byte、int、long等)的數字,則可設定為2進制(BIN)、8進制(OCT)、10進制(DEC)、16進制(HEX)的型態來列印顯示;如果該數字是實數型(例如float、double等)的數字,且沒有使用到第二個參數(format)時,那麼系統預設是顯示兩位小數,若想顯示不同長度的小數點,可使用到第二個參數(format)來設定輸出小數點的位數,而且當原來的(val)值小數點過長時系統會先作四捨五入後再輸出。


9.Serial.read()

這個函式可讀取從【RX0】這根接腳所接收到的資料,函式本身並不需要任何參數且讀取到的資料為8位元的字元(char)或者是位元組(byte)型態

使用範例 : char ch;

ch=Serial.read();

五.4.2 引入型函式庫

在本次的製作中共使用了3個內建的函式庫分,別為<LiquidCrystal.h>、<EEPROM.h><SPI.h>,和一個外掛函式庫即本次所使用的RFID讀寫器模組專屬的<MFRC522.h>;而<SPI.h>這個函式庫是因為我們所使用的RC522讀寫器模組所使用的介面是SPI介面所以也必須宣告跟著這些內建的函式庫中各具有不同數目的內建函式,而且有些在使用前需要先宣告相關的變數,或是作初始化動作,為了節省篇幅,在此筆者僅就真正有在程式中使用到的函式來為各為說明。引入型的內建函式和前面基本型在使用上最大的不同點是必須在程式最前面宣告該函式庫的物件名稱後,再以延伸的方式(即在該變數名稱後加上“.”)將相關的方法實作出來


五.4.2-1 LiquidCrystal.h:LCD液晶顯示器函式庫

在使用<LiquidCrystal.h>這個函式庫時除了要先引入(include)之外,還必須宣告一個屬於LiquidCrystal資料型態類別的物件變數;在前面介紹硬體電路時曾說過,我們所使用的LCD顯示器電路共使用了6根I/O接腳,而一般標準的文字型LCD顯示器對外有EN(致能)、R/W(讀/寫)及RS(暫存器選擇)等3根控制腳,和D7~D0等8根資料匯流排接腳,可是在<LiquidCrystal.h>這個函式庫中R/W腳可以忽略不用,且它們也提供了只使用4位元資料匯流排(使用D7~D4)的函式,因此這個LiquidCrystal資料型態類別的物件宣告格式共有4種,分別是:

【1】LiquidCrystal(rs,r/w,enable,d4,d4,d6,d7)  //使用4位元資料匯流排+R/W

【2】LiquidCrystal(rs,enable,d4,d4,d6,d7) //使用4位元資料匯流排

【3】LiquidCrystal(rs,r/w,enable,d0,d1,d2,d3,d4,d4,d6,d7) //使用8位元資

料匯流排+R/W

【4】LiquidCrystal(rs,enable,d0,d1,d2,d3,d4,d4,d6,d7)//使用8位元資料匯流排


  範例:

LiquidCrystal  lcd(8,9,4,5,6,7);


便是使用了上述【2】的宣告格式,其中的”lcd”便是此物件(或是變數)的名稱,其中RS(暫存器選擇)腳使用了接腳8,而EN(致能)腳則是接到了第9腳,至於d4~d7是分別接在數位的4~7腳;以後所有的動作或是函式必須以它為主去實作出來。接下來就讓我們看一下由”lcd”這個物件所衍生出來的內建函式。

lcd.begin(cols,rows)


這個函式是用來建立一個新的LCD顯示器物件,其中” cols”這個參數是指定這個LCD顯示器所擁有的列數(X軸),而” rows”這個參數則是指定這個LCD顯示器所擁有的行數(Y軸),在實際使用上要注意這兩個參數的值要和LCD顯示器硬體實際的數目相同,以免出現不可預期的錯誤。

例如最後面【陸、全體程式列表】章節中的:

42. const  int  numRows=2;

43. const  int  numCols=20;


54. lcd.begin(numCols,numRows);


便是宣告建立了一個螢幕可顯示兩行,每行可有20個字的LCD顯示器變數以做為開始使用LCD顯示器的初始化動作。


lcd.clear()

此函式會將LCD顯示器上所有顯示的文字清除,並將游標移回到螢幕的左上角第一個位置,使用本函式時不必使用到任何的參數。


lcd.home()

這個函式會將LCD顯示器的游標移回到螢幕左上角第一個位置,同樣的使用時不必使用到任何的參數。


lcd.blink()/lcd.noBlink()

這個函式用來設定LCD顯示器的游標在螢幕上出現時是否要閃爍,同樣的使用時不必使用到任何的參數。


lcd.setCursor(col,row)

此函式用來設定游標在螢幕上的顯示位置,使用時必須先設定兩個參數,其中” col”參數是設定游標在LCD顯示器列的位置(或者說X軸的值),而” row”這個參數則是指定這個LCD顯示器行的位置(或者說Y軸的值);在實際使用時這兩個參數的值都是由0開始計算,而且不能超過前面的『lcd.begin(cols,rows)』函式所訂定的範圍,否則會出現不可預期的錯誤,這點還請讀者們注意。


lcd.print(data)

此函式用途是將(data)這個參數以文字的方式列印或者說顯示在LCD顯示器上,而這個參數(data)的資料型態可以是char、byte、int、long或者是string ;不過在使用此函式前記得要先用前面的『lcd.setCursor(col,row)』函式將游標設定在欲顯示的位置上,免得到時看不到要顯示的文字內容!


五.4.2-2 EEPROM.h:非揮發性記憶體內建函式

這個函式庫是Arduino IDE中少數在使用內建函式前不需要先宣告類別物件的,只要先引入(#include)函式庫就可以開始使用;不過依筆者的使用與比對後發現在1.6.X以後的IDE中其函式庫也就是圖53中的Arduino IDE資料夾(即libraries)中,已經看不到<EEPROM.h>這個函式庫了應該是已經改為內建才是可是如果不在程式前面加上”#include <EEPROM.h>”這個前置指令的話在編譯時還是會發出錯誤訊息!所以不管它是改為內建或是改成其它的名稱,我們還是乖乖的照以前的方式使用就對了。在本次的製作中我們只用到兩個內建函式,接著就讓我們看看它們的功能與內容:

1.EEPROM.read(addr)

這個函式可以從Arduino家族模組板上的單晶片微電腦中讀取存放在EEPROM記憶體內的資料,在使用時必須先設定所欲讀取的位址(即”addr”這個參數),而且一次只能讀取一個byte的資料;此外要注意的是不同型號的Arduino模組板上所使用的單晶片微電腦型號也不盡相同,因此所擁有的EEPROM記憶體容量就不見得一樣,所以使用者在使用這類的內建函式時要先確認EEPROM記憶體容量的大小,以免出現無法偵測到的錯誤(例如”addr”值大於實際的EEPROM記憶體容量)。


2.EEPROM.read(addr,data)

此函式和上一個正好相反,會將一個byte的資料寫入Arduino家族模組板上的單晶片微電腦中的EEPROM記憶體內,同樣的在使用時必須先設定所欲讀取的位址也就是”addr”這個參數,以及要寫入的資料值(即”data”這個參數);當然實際使用時也要注意不同型號的Arduino模組板所擁有的EEPROM記憶體容量大小不同的問題。


五.4.2-3 SPI.hSerial –Peripheral Interface bus

所謂的SPI(Serial –Peripheral Interface bus)介面又稱為四線同步序列資料協定(Four - Wire bus protocol),是由SS(Slave Select)SCK(Serial Clock)、MOSI(Master Out Slave IN)MISO(Master In Slave Out)這四種功能的信號線所構成。而在Arduino族系的各種模組板子上,後面三隻腳分別接到了數位的D11腳(MOSI)、D12(MISO)與D13(SCK),如果使用了SPI介面的功能,這三隻腳就不能再作其他用途,除非透過指令把它們釋放出來。至於SS(Slave Select)這根致能選擇腳一般都預設使用D10接腳。此外由於在一個系統中可能同時存在好幾個都使用SPI介面的擴充模組板(例如arduino 官方的Ethernet Shield板就同時包括了SD記憶卡而兩者都使用SPI介面),這時我們就必須針對不同的裝置使用不同的SS(Slave Select)致能控制腳以避免不同裝置之間的資料傳輸發生衝突當然這也就意味著這些裝置或模組板不能同時動作。

由於在本次的製作中並沒有直接使用到這個介面,而是交由RC522這個RFID讀寫器使用,所以程式中也只有作一些簡單的定義與宣告而已,因此在僅介紹兩個最基本的函式。

1.SPI.begin()

這個函式是用來初始化SPI這個介面也就是說宣告數位的D11腳(MOSI)、D12(MISO)與D13(SCK)已經被使用不能再另作它用;在最後面【陸、全體程式列表】章節中的第86行(位在setup()單元中)正是此SPI介面初始化的指令所在:


86. SPI.begin();                // Init SPI bus


2.SPI.end()

這個函式是用來結束SPI介面的使用,以便釋放出數位的D11腳(MOSI)、D12(MISO)與D13(SCK)這三隻接腳,好另作它用;不過在本次的製作中由於我們的RFID讀寫器RC522一直會使用到SPI介面所以主程式中並沒有使用到這個函式



五.4.2-4 MFRC522.h:RFID讀寫器函式

“MFRC522.h”是我們這次製作所使用的RFID讀寫器的外掛函式庫,讀者們可至” https://github.com/miguelbalboa/rfid”這個網頁下載,在此網頁頁面(圖70)的右下腳角標記為1處便是下載的點選按紐,下載後的完整檔名為”rfid-master.zip”;而左半部標記2之處的檔案便是此壓縮檔解開後的內容,不過我們還得經過一番的整理後才能使用這個函式庫。如果我們開啟此函式庫所提供的一些範例程式來看,會發現它實際所引用的函式庫名稱為“MFRC522.h”也就是說我們不能直接把”rfid-master.zip”這個壓縮檔解開後就放到Arduino IDE的函式庫資料夾(即『libraries』資料夾)中,還必須把它的名字改成『MFRC522』才行,圖71便是加入“MFRC522”這個函式庫後『libraries』資料夾的內容,其中標記為1之處便是新加入的”MFRC522”函式庫。

由於Arduino IDE在程式開始執行時就已經把『libraries』函式庫的資料都載入了,所以在程式開發設計階段所加入的函式庫都不會立即呈現在Arduino IDE中,因此如果使用者是在中途加入函式庫,就必須把Arduino IDE結束後重新再啟動,才會看到新加入的函式庫

圖70‧MFRC522.h下載網頁畫面

圖71‧Arduino IDE『libraries』資料夾的內容


其實”rfid-master.zip”這個壓縮檔中我們真正必須使用到只有兩個檔案,即“MFRC522.h”和“MFRC522.cpp”此外一般我們還會把相關的範例也放在『MFRC522』這個函式庫資料夾中,圖72便是筆者Arduino IDE內『MFRC522』資料夾的內容,也就是說只放三個檔案/資料夾就夠用了。

圖7‧『libraries』資料夾中『MFRC522』資料夾之內容


在使用“MFRC522.h”這個函式庫的內建函式之前,當然要先在程式的最前面使用〈#include〉這個前置指令來引入,此外我們一樣必須先定義好我們這個RC522裝置所使用的致能控制腳(SS:Slave Select)及重置腳(RST:Reset),接著才宣告一個「MFRC522」類別的變數(即mfrc522),這些動作是在最後面【陸、全體程式列表】章節中的23~25行中實現,作完這些前置動作後我們便可以開始呼叫相關的函式來使用。由於RC522讀寫器模組對外所使用的硬體通信介面是SPI協定,這個介面的其它接腳(即MOSI、MISO與SCK)在Arduino族系的模組板中都是使用固定的腳位,所以在此就不用另外宣告。


4. #include <MFRC522.h>

…………..

23. #define SS_PIN 10    //Arduino Uno

24. #define RST_PIN 9

25. MFRC522 mfrc522(SS_PIN, RST_PIN);  // Create MFRC522 instance.


在“MFRC522.h”這個函式庫中有許多有用的函式,不過由於本次製作我們只需要讀出Mifare卡中的序號而已,所以只會使用到其中的5個函式表三的內容是這5個函式的種類與功能,其中的「PCD」是“Proximity Coupling Device”即『近接藕合裝置』的縮寫,也就是RC522這顆RFID讀寫器晶片的意思;而「PICC」則是” Proximity Integrated Circuit Card”的縮寫,亦即MIFARE卡或電子標籤之意。接下來就讓我們詳細看一下它們的內容:


編號

函式名稱

功能

void mfrc522.PCD_Init()

初始化RC522讀寫器模組

boolean mfrc522.PICC_IsNewCardPresent()

測試是否有新卡出現在RC522上

byte mfrc522.PICC_HaltA()

令MIFARE卡從動作狀態進入暫停狀態

void mfrc522.PCD_StopCrypto1()

令RC522離開授權狀態

boolean mfrc522.PICC_ReadCardSerial()

讀取MIFARE卡片的序號

表三、MFRC522外部引入函式庫使用函式列表


1.mfrc522.PCD_Init():MFRC522卡初始化函式

就跟許多其它模組或元件的函式一樣,一般都會先作一初始化的動作,在此這個初始化的動作主要是先設定好Arduino的SPI介面接腳功能,然後再將RC522這顆讀寫器晶片的內部暫存器規劃好,這個函式的型態為void所以沒有傳回值,而且使用時也不必任何參數。有興趣深入了解這個函式庫功能的讀者,可以將“MFRC522.h”和“MFRC522.cpp”這兩個檔案的內容叫出來看一看,就可以發現還真是不簡單的工作,以筆者列印出來的結果前者內容為10頁(以A4大小的紙張來計)而後者則到達36頁其內容之複雜可想而知所以在使用這些函式庫時要記得感謝那些作者的辛勞才是!

下面的及接著的程式都是最後面【陸、全體程式列表】章節中相關函式出現的位置行號,它本身是一個不傳回值的函式,呼叫時也不需要任何的參數,所以直接呼叫即可。


86. mfrc522.PCD_Init();        // Init MFRC522 card


2.mfrc522.PICC_IsNewCardPresent():測試是否有新卡出現函式

此函式是用來偵測RC522讀寫器模組板的天線感應區上是否有RFID的電子標簽(即卡片)出現?若有還會判斷是新卡?還是原本就已經放在模組板上的?這個函式的資料型態是boolean,若有新卡片出現會傳回true的結,反之如果是原本就已經擺放在上面的卡片或是根本沒有任何卡片則會傳false;下面是【陸、全體程式列表】章節中的部份程式,左方的數字為行號,在此系統會一直等到新卡片的出現後才結束此無窮等待的迴圈。


115. while( ! mfrc522.PICC_IsNewCardPresent())

116. {};


3.mfrc522.PICC_HaltA():

當我們要讀取卡片時首先要讓卡片啟動(Active),接著進行一連串的通訊及授權查核無誤之後,卡片才會進入可讀取或是回傳資料;而當我們要停止讀取卡片上的資料或是說停止通訊時,首先必須令卡片回到停止(Halt)的狀態,這就是這個函式的功用;雖然說它是一個「byte」型態的函式,由於它的傳回值並沒有什麼用,所以在使用時可以不管回傳的值為何,此即下列使用的範例中沒有設定變數接收回傳值的原因!


305. mfrc522.PICC_HaltA(); // Halt PICC


4.mfrc522.PCD_StopCrypto1():

本函式上一個函式一般都會連在一起使用,也就是說當RC522晶片要停止和卡片通信時,除了要先告訴MIFARE卡端(即PICC)進入暫停(Halt)狀態之外,讀寫器裝置端(即PCD)也必須離開所謂的授權(encryption)狀態,這樣才是一個完整的通信協定終止狀態。本函式的型態為void因此沒有傳回值,使用時也不必任何參數。下列範例是【陸、全體程式列表】章節中的程式列表內容,看起來應該很單純才是。


306.        mfrc522.PCD_StopCrypto1();  // Stop encryption on PCD


5.mfrc522.PICC_ReadCardSerial():

這個函式是用來讀取MIFARE卡的序號值,它的資料型態為”boolean”如果讀取成功會傳回「true」,反之則傳回「false」;此函式乍看之下在使用時好像沒有用到任何參數而它本身又是一個只會傳回”boolean”的函式那MIFARE卡的4byte的序號值是怎麼傳回來的呢如果我們打開函式庫中的“MFRC522.h”來看的話其中有一段的內容如下

        // A struct used for passing the UID of a PICC.

        typedef struct {

                byte    size;       // Number of bytes in the UID. 4, 7 or 10.

                byte    uidByte[10];

                byte    sak;       // The SAK (Select acknowledge) byte returned from the PICC after successful selection.

        } Uid;


在這段程式中定義了一個名稱為”Uid”的結構體(struct)其中的第一個成員「size」代表MIFARE卡的序號值的長度由於MIFARE卡的種類很多其序號值的長度就有410(byte)三種,而本次製作所針對的悠遊卡這款的MIFARE卡其序號值長度為4位元組;接著的成員「uidByte[10]」則是用來存放讀取的序號值亦即當我們呼叫過”mfrc522.PICC_ReadCardSerial()”之後如果傳回值為「true」那麼我們便可以從「uidByte[]」這個陣列中取出讀到的序號值,而實際的序號長度會由第一個成員「size」決定

下列範例程式是【陸、全體程式列表】章節中的程式列表內容其中841行呼叫”mfrc522.PICC_ReadCardSerial()”去讀取MIFARE卡的序號值如果成功則850~855行程式會將4位元組卡片序號從「mfrc522.uid.uidByte[i]」陣列中轉存到RFID_SN[i]這個我們自訂的卡片序號專用陣列內,最後在交由其他部份的程式來處理此卡片的序號值


837.    flag=false;

838.    for(byte j=0;j<5;j++)

839.    {

840.        delay(20);

841. if (  mfrc522.PICC_ReadCardSerial())

842. {

843.            flag=true;

844.            break;

845.     }

846.   }

847.    if(flag==false) return(false);

848.        // Select one of the cards

849.        Serial.print("Card UID:");    //Dump UID

850.        for (byte i = 0; i < mfrc522.uid.size; i++) {

851.            Receive_Data[i]=mfrc522.uid.uidByte[i];

852.            RFID_SN[i]=Receive_Data[i];

853.          Serial.print(Receive_Data[i] < 0x10 ? " 0" : " ");

854.          Serial.print(Receive_Data[i], HEX);

855.  


五.5 自建函式(副程式)說明

雖然說Arduino IDE中已經內建種類繁多且內容豐富的內建函式庫,可是在本次的製作中為了簡化程式的撰寫,筆者還是自己設計了一些專用的函式,下面就是針對一些特定用途以及本次製作系統上所使用的硬體去設計的一些函式。


五.5.1 LCD顯示器相關函式

下面的表四所列舉的就是筆者在這次製作中為了LCD顯示器所撰寫的副程式,由於Arduino IDE中已經建構了許多和的LCD顯示器內建函式,所以不必像之前使用8051組合語言那樣,必須自己設計一堆的副程式,算算真正自行建構的只有兩個函式而已,接著就讓我們看看這兩個函式的功能、用途與動作原理:

編號

副程式名稱

功能

程式列表行號

void LCD_Clear_Line

清除 LCD 顯示器單 1行螢幕

863~867行

void Show_RFID_SN

MIFARE卡片序號轉ASCII碼顯示副程式

823~832行

表四。LCD顯示器相關自建函式列表


【1】void LCD_Clear_Line(int LineNum):清除 LCD 顯示器單 1行螢幕函式(程式列表: 863~867行)

在Arduino IDE中當引入<LiquidCrystal.h>這個函式庫時,有一個名稱為”.clear()”的實作函式,可以用來將LCD顯示器上所有的內容都清除掉;可是在許多場合中我們可能只是要清除其中單一的一行(比較精確的說法應該是”列”,不過筆者習慣用”行”來代表LCD顯示器水平方向)而已,所以筆者就仿照之前的慣例設計一個可以清除單一行的函式;在呼叫此函式時需先給定一個參數(int Line_Num),用來指定要清除的行號,在此只能使用1或2兩個值。

整個函式的主體很簡單只有2行而已,程式首先呼叫”lcd.setCursor(x,y)”這個由<LiquidCrystal.h>函式庫所提供的函式,游標設定在目標行(Line_Num)的第一個位置,然後再顯示長度為20的空白字元(即20h),這樣就等於是把該行的內容清除掉了!如果讀者使用的是其他規格的LCD顯示器,例如16X2也就是水平每行可以顯示16個字元的,則只要將程式第3行中的空白字元改成16個就可以了。


LiquidCrystal  lcd(8,9,4,5,6,7); // 先宣告一個名稱為”lcd”的LCD顯示器變數


  1. void  LCD_Clear_Line(int Line_Num){

  2.     lcd.setCursor(0,Line_Num); // 設定游標在目標行的第一個位置

  3.     lcd.print("                    "); // 顯示長度為20的空白字元

  4. }  



【2】void Show_RFID_SN:MIFARE卡片序號資料轉ASCII碼顯示函式(程式列表: 823~832行)

在程式列表的第30行我們定義了一資料型態為位元組(byte)的陣列變數” Receive_Data[4]”,用來存放RFID讀寫器模組讀取到的MIFARE卡片序號資料;整個程式的主體(程式3~9行)是一個迴圈次數為4的”for”迴圈,在迴圈中依序從陣列變數” Receive_Data[]”中取出4個位元組卡片序號資料,然後顯示在LCD顯示器指定的位置上。

在上篇中我們介紹過lcd.print(data,BASE)這個函式,其中的”BASE”參數是用來決定前一個參數”data”的顯示格式,它總共有二進制(BIN)、八進制(OCT)、十進制(DEC)及十六進制(HEX)等四種基底可以選擇,用起來非常方便;可是當我們要顯示像十進制(DEC)或十六進制(HEX)的資料時,由於一個位元組(byte)的值其範圍以十六進制(HEX)來說是從”00”到”FF”,不過當這個值小於16時則其範圍會限制在”00”~”0F”,也就是說最高位數的值是’0’,筆者之所以不厭其煩的拿出來說明,是因為lcd.print(data,BASE)這個函式在遇到高位數的值是’0’時會自動的把它去掉,也就是說”00”~”0F”的數字內容在使用lcd.print(data,HEX)這個函式之後,在LCD顯示器原上只會看到”0”~”F”的數字而已;為了保留十六進制(HEX)資料原汁原味的兩位數字,所以我們的程式會先判斷要顯示的數字是否小於16(10h)(程式第6行),如果是,則先在前面補顯示一個字元”0”,這樣一來每一筆資料不管最高位數的值是否為’0’,在LCD顯示器上都會看兩位的數字。


byte  Receive_Data[4]; //先宣告一個名稱為” Receive_Data”的位元組

(byte)陣列變數


  1. void  Show_RFID_SN()

  2. {

  3.     for(byte i=0;i<4;i++)

  4.     {

  5.         EEPROM_data[i]=Receive_Data[i];

  6.         if(Receive_Data[i]<0x10) // 測試該位元組資料是否小於16(10h)

  7.         lcd.print("0"); // 若是則先在前面補顯示一個字元”0”

  8.        lcd.print(Receive_Data[i],HEX); / 以16進制的方式顯示該位元組資料

  9.     }

  10. }          



五.5.2 4X4鍵盤相關函式

  本次製作中和4X4鍵盤輸入有關的副程式共有6個,分別是:

編號

副程式名稱

功能

程式列表行號

boolean  keyPress

測試是否有按鍵被按下

900~906

byte findKeyNumber

找出對應按鍵值副程式

888~898行

void WaitKeyRelease

等待所有鍵都放開

771~776

boolean WaitKeyIn

按鍵輸入副程式

752~768

boolean Password_In

密碼輸入副程式

711~731

boolean Accept_Cancel

測試使用者按下選取A(Accept)或取消C(Cancel)鍵 

734~749行

表五。4X4鍵盤相關自建函式列表


這部份的程式可說是本次製作最大的特點,由於整個4X4鍵盤輸入電路所使用的Arduino模組板接腳由上次的5根縮減為1根而所對應的副程式也跟著精簡許多接著就讓筆者一一為各位介紹它們的內容:

【1】boolean  keyPress:測試是否有按鍵被按下函式(程式列表: 900~906行)

這個函式是用來測試使用者是否按下任何一個按鍵?呼叫時不必使用任何參數,並且會依結果傳回一個布林(boolean )型態的值;在標準的C語言或是其他的高階程式語言中,只要是副程式或函式就一定要指定是否要傳回值,如果要還必須指定傳回值的型態,就像變數一樣,而這個”keyPress”函式如果測到有按鍵被按下時會回傳”true”布林型態值的結果給呼叫者,反之則為”false”。

要了解這部份的程式內容請參考前面的圖46”4X4鍵盤輸入電路圖”,本次製作最大的賣點就這個電路,我們從之前使用5根接腳簡化到只用1根接腳而已,當然程式也跟著簡單許多;在圖46”4X4鍵盤輸入電路圖”中,這些按鍵的輸入輸出是透過7個分壓電阻來完成,並沒有用到任何的掃瞄輸出腳,而分壓後的信號是接到Arduino Uno模組板的A0類比輸入腳,因此不管那一個按鍵被按下,類比輸入腳A0上都會有電壓出現,雖然輸入電壓值不能確定是多少,但一定不是0電壓!

程式一開始(第2行)是在讀取並測試類比輸入腳A0上的電壓轉換值,接著測試轉換的結果是否大於40(程式第3行),若是,代表有按鍵被按下,則結束程式並傳回”true”的結果;反之代表沒有任何鍵被按下,改為傳回false”值。當沒有任何鍵被按下時,照理輸入腳A0上的電壓應該是0伏特,可是系統上包括電源部份一定會有雜訊,而且類比轉換時也會有誤差,依我們電路的結構可得到表二的”4X4鍵盤按鍵值與分壓電壓之對照值,也就是說類比輸入腳A0上最小的輸入電壓為Vh/8,轉換後的結果約為127左右(滿刻度為1023),因此可以加上適當的寬容度,以免得到錯誤的結果。


1. boolean keyPress(){

2.    KeyIn=analogRead(A0); // 偵測類比輸入腳A0上的電壓

3.  if(KeyIn>40) // 若轉換值>40代表有鍵被按下

4.    return(true); // 傳回”true”的結果

5.  else

6.    return(false); // 反之傳回”false”

7. }    


為了讓讀者們可以比對本次與上次所設計函式差異,筆者特地將上次函式的內容附錄於下,從兩個函式的長度來看即使不計入掃瞄用的陣列變數新的函式幾乎是原來的一半而已各位應該可以看出程式精簡的程度了吧!


const  byte  keyScan[]={18,17,16,15};  //先宣告一個名稱為” keyScan”的位元組

//陣列,其內容即為鍵盤掃瞄輸出腳的號碼


  1. boolean keyPress(){

  2.   for(byte i=0;i<4;i++) // 次數為4的for迴圈

  3.   {

  4.     pinMode(keyScan[i],OUTPUT);  // 將對應I/O接腳設定成輸出腳

  5.     digitalWrite(keyScan[i],1); // 令該接腳輸出高態電壓

  6.   }

  7.   KeyIn=analogRead(A0); // 偵測類比輸入腳A0上的電壓

  8.   if(KeyIn>50) // 若轉換值>50代表有鍵被按下

  9.   {

  10.     return(true);} // 傳回”true”的結果

  11.   else

  12.  return(false); // 反之傳回”false”


【2】byte findKeyNumber:找出對應按鍵值函式(程式列表: 888~898行)

當使用上一個”keyPress”函式測試到有按鍵被按下後,便可以呼叫這個函式將按下的按健對應值找出來,這個函式傳回值的資料型態是位元組(Byte),且使用時不需要任何參數。雖然前一個『keyPress()』已經有執行過轉換A0輸入腳上電壓的動作,但為了去除所謂按鍵彈跳現象,所以函式一開始會先呼叫『delay(10)』函式,以等待彈跳狀態消失,然後再次的將A0腳的輸入電壓轉換值成對應值(程式2~4行);接著6~10行程式是一個迴圈次數為16的”for”迴圈,在此迴圈中會依序將A0腳輸入電壓的轉換值和”keyValue[]”這個預先定義好的轉換值陣列內容作比較,如果比對符合(也就是輸入轉換值大於預先定義陣列的內容)再以該陣列的順序值為參數從另一個按鍵值對應陣列”keyNumber[]”找出真正的對應按鍵數值回傳給呼叫者。

在表二的”4X4鍵盤按鍵值與分壓電壓”之對照值是以圖46”4X4鍵盤輸入電路圖”中16個按鍵的分壓值去計算而得出來的不過在Arduino Uno中我們還是必須以電壓的轉換值來處理所以筆者將表二中的電壓值轉換成ADC的轉換結果並加上誤差容忍值後定出”keyValue[]”這個轉換值陣列,基本上應該是一個最佳化的值。至於”keyNumber[]” 這個陣列的內容,則是筆者所使用的4X4鍵盤按鍵(請參考上篇的圖1)上的標示值,其中數字10~15分別代表英文的A~F,如果讀者們使用不同排列的鍵盤,請自行依實際排列方式更改”keyNumber[]”這個陣列的內容值。


36. keyValue[]={1000,800,670,500,440,390,325,305,280,245,230,215,190,160,135,

110}; // 4X4 鍵盤16個按鍵分壓後的預期轉換值

37. const  byte  keyNumbers[]={1,14,7,2,0,8,3,15,9,10,13,12,4,5,6,11};

// 變數內容代表的4X4 鍵盤的對應值

//上面變數的行號為【陸、全體程式列表】章節中的程式列表中變數定義的行號1. byte findKeyNumber(){

2.     delay(10);

3.       KeyIn=analogRead(A0);

4.       KeyIn=analogRead(A0); // 讀取類比輸入腳A0的轉換值

5. //    Serial.println(KeyIn);

6.     for(byte i=0;i<16;i++) // 依序比對量測輸入及預期轉換值

7.     {

8.       if(KeyIn>keyValue[i]) // 轉換結果與陣列”keyValue[]”比對

9.         return(keyNumbers[i]) ; //若成立找出對應的按鍵數值

10.     }

11. }



【3】void WaitKeyRelease:等待所有鍵都放開函式(程式列表: 771~776行)

本函式是用來監控使用者是否放開按鍵,所以不需要使用任何參數,也不會傳回任何值;整個函式很簡單,只是利用前面介紹過的『keyPress()』函式而已,當呼叫前面的『keyPress()』函式時,如果有按鍵按下時會傳回【true】,所以我們使用”while”這個內建指令去測試按鍵的狀態(程式第3行),如果傳回值為真,也就是代表按鍵尚未放開,程式便會執行一個空指令(程式第4行),一直到按鍵放開為止;最後再呼叫一個延時函式(程式第5行),以便消除鍵盤的彈跳現象。


  1. void WaitKeyRelease()

  2. {

3.   while(keyPress()) // 測試按鍵的狀態

4.    {}

5.   delay(30); // 延遲一段時間以消除按鍵彈跳現象

6.  }



【4】boolean WaitKeyIn(byte wait):按鍵輸入函式(程式列表: 752~768行)

本次製作中幾乎所有和按鍵輸入有關的部份都會用到這個函式,而本函式的型態是” boolean”,而非一搬的按鍵數值;筆者之所以會這樣設計,主要是考慮到在實際的按鍵輸入時,由於使用者可能會過久甚至忘記再按下按鍵,所以本函式並不會直接傳回按鍵輸入值,而是透過一個全域變數”KeyNum”來傳遞按鍵的對應值。在呼叫此函式時需先給定一個參數(byte wait),用來指定按鍵的等待時間(單位為”秒”),如果使用者超過這個設定的時間沒有再按下任何一個鍵,則函式會傳回”false”的返回值,假如是正常時間按下任意鍵,則傳回的結果為”true”。 

在我們的系統中作按鍵輸入時會頻繁的使用到這個函式,由於Arduino模組板的硬體速度相較於人來說是非常的快,因此當連續使用這個函式時,如果不考慮人類操作的反應速度,那麼當我們從按下按鍵到放開的那一段時間,Arduino模組板已經不知道會偵測到幾次按鍵被按下了,為了解決這個問題,所以我們的程式在一開始時(第3行) 先呼叫“WaitKeyRelease”這個函式,等待使用者放開按鍵後才執行新的按鍵輸入動作;接著用” Wait_Time”來取得目前系統內建的計時時間值”millis()”,而5~12行程式則是用來等待及測試使用者是否按下按鍵,其核心的部份(第9行)會不斷的比較目前系統內建的計時時間值是否已經超過呼叫者所給定的時間範圍(即參數wait),如果超過了,函式就會立即結束並傳回”false”的結果(第10行)

不過有時候在等待按鍵輸入時我們並不需要測試是否逾時,這時只要讓這個參數wait值為’0’,這個函式便會忽略逾時的問題,以便提供更彈性的應用,此即程式第7行「if(wait!=0)」的用意

如果使用者在限定時間內按下了任意鍵,程式會先呼叫延時函式『delay(20)』(第13行)以消除按鍵的彈跳現象,然後呼叫前面介紹過的『findKeyNumber』函式找出對應的按鍵值,並交由”KeyNum”這個全域變數傳回給呼叫者,最後在發出一提示音之後傳回”true”的結果代表本函式按鍵讀取成功。


33. byte  KeyNum,PW_Error;

34. unsigned long Wait_Time;

//上面變數的行號為【陸、全體程式列表】章節中的程式列表中變數定義的行號

  1. boolean WaitKeyIn(byte wait)

  2. {

  3.    WaitKeyRelease(); // 先等之前的按鍵狀態結束

  4.    Wait_Time=millis(); // 取得目前系統的計時值

5.   while(!keyPress()) // 開始等待使用者按下按鍵

6.    {

7.     if(wait!=0) // 若計時參數“wait”=0代表不測試逾時狀態

8.     {

9.         if((millis()-Wait_Time)>=(wait*1000))

10.           return(false); //按鍵讀取逾時,傳回”false”的結果

11.     }  

12.     }

13.   delay(20);

14.    KeyNum=findKeyNumber(); // 取得按鍵轉換值至變數”KeyNum”

15.   BuzzerSound(100); // 發出一短暫(0.1秒)提示音

16.   return(true); // 讀取按鍵成功函式傳回”true”的結果

17.  }



【5】boolean Password_In:密碼輸入函式(程式列表: 711~731行)

本函式是專門用來輸入密碼,在使用時不必提供任何參數,而且會依輸入成功與否傳回一boolean值給呼叫者,以供後續的應用參考。程式首先在LCD顯示器第一行顯示提示訊息” Password:”,並發出一短暫(0.3秒)提示音後,開始等待使用者輸入4位數的密碼(程式3~6行);在此輸入密碼是以for迴圈的方式來實現(程式7~21行),而真正的按鍵輸入部份則是程式9~12行的do迴圈,主要是呼叫上一個按鍵輸入函式”WaitKeyIn(10)”等待使用者的按鍵輸入,且時間上限為10秒,如果有按鍵被按下的話,”WaitKeyIn(10)”函式會返回”true”的結果到「flag」這個變數,且按鍵對應值由”KeyNum”傳遞回來;反之當發生逾時的情形時「flag」這個變數的內容會是”false”,如此一來本函式會立即結束離開,而且會回傳”false”的結果給呼叫者(第15行)

在上述的do迴圈中由於程式第12行已經設立了限制條件,所有按鍵輸入值必須為”0~9”或”C”鍵(即KeyNum==12)才有效,而”C”鍵在此代表取消(Cancel)的意思,所以接下的程式會先測試使用者是否按下了”C”鍵(第17行)?如果是本函式也會馬上結束,只是回傳給呼叫者的結果是”true”(第18行),而同時”KeyNum”這個變數也會將按鍵”C”的值傳遞回去假如使用者按下的是”0~9”鍵,則其按鍵值會依序儲存在”RFID_PW[]”這個全域位元組陣列變數中,並顯示在LCD顯示器上(程式19、20行),這個按鍵輸入過程會連續執行4次才算密碼輸入成功;若密碼輸入正常完成,函式最後會傳回”true”的結果給呼叫者(第23行)。


  1. byte  In_Data,Receive_Data[20],EEPROM_data[8],RFID_SN[4],RFID_PW[4];

//上面變數的行號為【陸、全體程式列表】章節中的程式列表中變數定義的行號


boolean Password_In()

  1. {

  2.     LCD_Clear_Line(1); // 清除LCD顯示器第一行

  3.     lcd.setCursor(3,1); // 設定游標在LCD顯示器第一行第四格

  4.     lcd.print("Password:"); // 顯示提示字串"Password:"

  5.     BuzzerSound(300); //發出一短暫(0.3秒)提示音

  6.     for(byte i=0;i<4;i++) // 四為數密碼輸入迴圈

  7.     {

9.     do // 按鍵輸入等待迴圈

10.      {

11.         flag=WaitKeyIn(10); // 等待按鍵輸入,時間上限為10秒

12.      } while(!((KeyNum==12)||((KeyNum<10)))); // 按鍵輸入值必須為0~9或”C”鍵

13.     if(flag==false) // 測試按鍵輸入是否有逾時

14.      {

15.          return(false); // 若是則令函式傳胡回” false”

16.      }

17.      if(KeyNum==12) // 如果按鍵為”C”鍵同樣結束函式

18.         return(true); // 但傳回”true”

19.      RFID_PW[i]=KeyNum; // 先將按鍵輸入值儲存在密碼陣列上

20.      lcd.print(KeyNum,HEX); // 再將按鍵輸入顯示在LCD顯示器上

21.  }

23.    return(true); // 若密碼輸入正常完成函式傳回”true”

24. }              



【6】boolean Accept_Cancel:測試使用者按下選取A(Accept)或取消C(Cancel)鍵函式(程式列表: 734~749行)

每次使用者操作完一個功能或動作之後,例如上述的密碼輸入動作,我們的系統都會再詢問一次是否接受操作者所輸入的結果,這個功能便是由此函式來處理;在使用本函式時不必提供任何參數,同樣的本函式並不會直接將按鍵輸入值傳回來,而是透過”KeyNum”這個變數傳遞回來,函式本身會依輸入成功與否傳回一boolean值給呼叫者。

程式一開始先清除LCD顯示器第一行,並顯示"A-Accept C-Cancel:"提示訊息(程式3~5行),然後執行6~14行do{…}while(1)無窮迴圈,以等待使用者的按鍵輸入。如果要想離開這個無窮迴圈只有三種情況,一是按鍵輸入逾時,在此設定值為10秒,如果是這種情形,則函式會馬上結束並傳回”false”的結果給呼叫者(程式第9行);另外兩個條件則是使用者按下了’A’或’C’鍵之一,此時同樣會結束函式,只是函式傳回的結果是”true” (程式第15行), 而按鍵值則是交由”KeyNum”這個變數儲存起來,所以函式結束後呼叫者便可由它得知按鍵內容。如果使用者按下的不是’A’或’C’鍵之一,則程式會發出警告聲,並繼續等待按鍵輸入的動作,直到前述的三種情況出現為止。


  1. boolean Accept_Cancel()

  2. {

  3.     LCD_Clear_Line(0); // 清除LCD顯示器第一行

  4.     lcd.setCursor(0,0); //設定游標在LCD顯示器第一行第四格

  5.     lcd.print("A-Accept C-Cancel:"); // 顯示"A-Accept C-Cancel:"提示訊息

  6.     do // 按鍵輸入等待迴圈

  7.     {

  8.       if(WaitKeyIn(10)==false) // 等待按鍵輸入,時間上限為10秒

  9.         return(false); // 若逾時則函式傳回”false”值

  10.       else if((KeyNum==10)||(KeyNum==12)) // 若使用者按下的是

  11.         break; // ’A’或’C’鍵之一則跳出迴圈

  12.       else

  13.         BeeBee(); // 如果是其他鍵則發出警告聲

  14.     } while(1); // 

  15.     return(true);        // 使用者已按下’A’或’C’鍵之一

  16.  } // 因此函式傳回”true”值


五.5.3 聲音產生相關函式

  雖然說Arduino IDE中已經內建了幾個讓蜂鳴器發音的函式,可是本次製作所使用的聲音電路仍和以前採用8051單晶片微電腦時是一樣,為了配合相關的電路,所以筆者還是自行撰寫了所需的函式,其實數目也不多,本次製作和聲音有關的副程式共有2個而已,分別是:

編號

發聲副程式名稱

功能

程式列表行號

void BuzzerSound()

讓蜂鳴器發出長1KHz的聲音

877~885

void BeeBee()

讓蜂鳴器發出一短(0.1秒)一長音(0.5秒) 1KHz的聲音

870~874行

表六。蜂鳴器發聲副程式列表


【1】void BuzzerSound:讓蜂鳴器發出長1KHz的聲音函式(程式列表: 967~975行)

本函式是讓蜂鳴器或者是喇叭發出一1KHz的聲音信號,並不會傳回任何值,由於Arduino Uno板所使用的晶片並沒有內鍵數位至類比轉換電路(DAC),所以在此我們只是單純產生一方波信號而已;而1KHz的方波信號基本上是由一個寬度為0.5mS(或500US)高態脈波和一相同寬度的低態脈波所組成,由於筆者使用了一個PNP電晶體作為Arduino Uno板與蜂鳴器之間的電流緩衝放大電路,因此先送出一低態電壓至蜂鳴器輸出控制腳讓此PNP電晶體導通,然後延遲500US,接著再送出一高態電壓至蜂鳴器輸出控制腳將PNP電晶體關閉,再延遲500US,如此一來便可以產生一個單一週期的1KHz的方波信號(程式4~7行)

在呼叫這個函式時需提供一個參數(int  BeeTime),這個參數會作為程式2~8行這個for迴圈的計數值,用來決定要產生多少個1KHz的方波信號,其實也就等於要產生多長的聲音;例如我們要發出0.2秒的聲音則:


1KHz的方波週期時間為 🡺1mS

∴ 0.2S / 1mS = 200


也就是說參數(int  BeeTime)的輸入為200,只要給不同的參數值,便可以讓蜂鳴器產生不同時間的聲音出來。


  1. void  BuzzerSound(int BeeTime){

  2.   for(int i=0;i<BeeTime;i++) // 蜂鳴器發聲的時間由外部參數決定

  3.   { // 時間以mS為單位

  4.     digitalWrite(Buzzer,0); // 先令蜂鳴器輸出腳狀態為Low

  5.     delayMicroseconds(500); // 產生一個500US的低態間隔

  6.     digitalWrite(Buzzer,1); // 接著再令蜂鳴器輸出腳狀態為High

  7.     delayMicroseconds(500); // 產生另一個500US的高態間隔

  8.   }

  9. }  



【2】void BeeBee():讓蜂鳴器發出一短(0.1秒)一長音(0.5秒) 1KHz的聲音函式(程式列表: 960~964行)

在本次的製作中有許多時候筆者會讓蜂鳴器發出一短一長的提示音,為了節省篇幅,所以把這個功能設計成函式來使用;其中短音時間為0.1秒,而長音為0.5秒,中間隔了0.1秒的間隔,因為功能簡單所以呼叫時不需給參數,當然也沒有傳回值;整個函式內容只有3行,都是呼叫前面介紹過的『BuzzerSound』函式,讀者們應該不會看不懂才是,所以就不再多作說明了。


  1. void  BeeBee(){

  2.   BuzzerSound(100); // 發出一01秒的短音

  3.   delay(100); // 等待01秒的靜音間隔

  4.   BuzzerSound(500); // 再發出一0.5秒的較長音

  5. }  



五.5.4 EEPROM相關函式

  本次製作中和EEPROM使用有關的副程式共有2個,主要是擴充型的應用,它們分別是:

編號

副程式名稱

功能

程式列表行號

boolean Check_Card_Exist

檢查卡片序號是否已儲存在EEPROM記憶體中

779~803

boolean Save_Check_EEPROM

RFID卡片序號或密碼比對副程式

806~820

表七。Arduino Uno內建EEPROM相關副程式列表


【1】boolean Check_Card_Exist:檢查卡片序號是否已儲存在EEPROM記憶體中函式(程式列表: 779~803行)

本次製作所使用的Arduino Uno模組板上的晶片已經內建了1Kbytes的EEPROM記憶體,而且Arduino IDE中也有對應的函式可使用,不過這些內建的函式都是以單一位元組為單位來存取資料,沒有辦法一次讀寫多位元組;而我們的系統中像密碼或者是RFID卡片的序號都是以4個位元組為單位,所以筆者便把和RFID卡片序號及密碼讀寫有關的動作整合成兩個函式。而本函式是用來比對RFID讀寫器模組所讀取到的卡片序號(儲存在陣列變數“RFID_SN[4]”中)是否已經學習/儲存在EEPROM記憶體中(存放在陣列變數“EEPROM_data[8]”前四位元組上),如果是,函式會傳回”true”的結果,反之則為”false”,在呼叫時不用給任何的參數。

程式一開始(第3行)先將已儲存在EEPROM記憶體中的卡片數目從位址0中讀出,並存放在變數”CardCount”中,以作為後面for迴圈(程式7~23行)的計數值;而迴圈中的第7、8行也是一個for迴圈,其計數值為8,用來讀取單一使用者的RFID卡片序號及密碼,在此所以為把儲存的密碼值也一併讀取出來,主要是後面的密碼比對函式會用到,為了簡化程式,所以就一次讀完,而讀出來的8個bytes資料是存放在EEPROM_data[]這個位元組陣列中。接著的10~17行程式又是一個for迴圈,其計數值為4,是用來比對EEPROM_data[]組陣列中前4個bytes的RFID卡片序號資料,和RFID讀寫器模組所感應及讀取到的是否相同?如果相同,則本函式會將該卡片的儲存號碼交由”CardNumber”這個全域變數傳回給呼叫者,且立即結束函式,並傳回”true”的結果(程式18~22行);假如所有的卡片儲存資料都比對過且沒有任何一個相同的話,函式就會結束,且傳回”false”的結果給呼叫者參考(程式第24行)。


  1. byte  In_Data,Receive_Data[20],EEPROM_data[8],RFID_SN[4],RFID_PW[4];

// 上面變數的行號為後面程式列表中變數定義部份行號

  1. boolean Check_Card_Exist()

  2. {

  3.    CardCount=EEPROM.read(0); // 從EEPROM中讀取已儲存卡片的數目

  4.     Serial.println(CardCount); // 傳回卡片數目至電腦監控視窗

  5.     for(int i=0;i<CardCount;i++) // 開始已儲存卡片序號測試迴圈

  6.     {

  7.     for(byte j=0;j<8;j++) // 將卡片序號與對應密碼

  8.       EEPROM_data[j]=EEPROM.read(i*8+8+j); //共8 byte一次讀出

9.         equal=true; // 先設定旗標【equal】為”true”

10.        for(byte j=0;j<4;j++) // 測試前4byte部份的序號資料

11.        {

12.     Serial.print(EEPROM_data[j],HEX); // 將訊息傳回電腦串列通訊

13.     Serial.print(","); // 埠的監控視窗

14.     Serial.println(RFID_SN[j],HEX); // 

15.         if(EEPROM_data[j]!=RFID_SN[j]) // 若量測到的與儲存之序號

16.           equal=false; // 不相等則設定旗標【equal】為”false”

17.       }

18.       if(equal) // 如果找到相同的序號

19.       {

20.         CardNumber=i+1; // 儲存該筆資料的號碼

21.         return(true); // 結束函式並傳回”true”的結果

22.       }

23.     } // 已儲存卡片序號測試迴圈外環

24.    return(false); // 如果找不到相同的序號則函式傳回”false”值

25. }



【2】boolean Save_Check_EEPROM(int address,byte data[]):RFID卡片序號與密碼儲存函式(程式列表: 806~820行)

這個函式會把使用者的資料也就是卡片序號與密碼共8個位元組的資料儲存在EEPROM記憶體中指定的位置上,所以在呼叫前要先給定欲儲存位址(address)與儲存的資料來源(byte data[]即8個位元組的儲存位置)兩個參數,在寫入之後程式還會將儲存值再次讀出與原來資料再比對一次,如果寫入成功函式會回傳”true”的結果,反之則為”false”。

程式的3、4行是一個for迴圈,其計數值為8,在執行使用者的RFID卡片序號及密碼資料(共8個位元組)寫入EEPROM記憶體動作,接著的第5~13行也是一個for迴圈,其計數值同樣為8,是將已寫入資料再讀回作比對;如果有任一個位元組的資料不相等,便立即結束函式並傳回”false”的結果給呼叫者參考(程式第11行);假使8個位元組的資料都正確無誤,則函式會自然結束且回傳”true”的結果(程式第14行)。


  1. boolean Save_Check_EEPROM(int address,byte data[])

  2. {

  3.     for(byte i=0;i<8;i++) // 將新的卡片序號與對應密碼

  4.       EEPROM.write(address+i,data[i]); // 共8 byte一次寫入EEPROM內

  5.     for(byte i=0;i<8;i++) // 再將寫入資料讀出後與原來

  6.     { // 資料比對是否相同

  7.       byte Echo_Data=EEPROM.read(address+i);

  8.       if(Echo_Data!=data[i])

9.       { // 如果比對不符代表寫入失敗

10     Serial.println("Data Error!"); // 傳回警告訊息至電腦監控視窗

11.         return(false); // 結束函式並傳回” false”的結果

12.       }

13.     }

14.    return(true); // 寫入成功函式傳回”true”的結果

15. }  


五.5.5 RFID讀寫器模組相關函式

  在上次的製作中筆者共設計了6個和RFID讀寫器模組相關的函式而本次製作由於已經使用了和RC522有關的函式庫“MFRC522.h”所以使得自行設計的函式就簡化到只剩下1個了,即:

編號

副程式名稱

功能

程式列表行號

boolean Read_RFID_SN

尋卡及讀取卡片序號副程式

835~861行

表八。RFID讀寫器相關副程式列表


在使用這個函式之前,我們必須先呼叫前面介紹過的「mfrc522.PICC_IsNewCardPresent()」函式,去偵測是否有卡片出現在RC522模組的有效感應範圍之內?如果有的話,接下來才呼叫本函式去讀取該卡片的序號。程式開始先讓讀取旗標(flag)值為false(第3行),接著的4~12行是一個for的讀取迴圈,其次數為5,筆者之所以會這樣設計,是因為在實務上測試時發現,由於使用者所持的卡片因使用者手移動的問題,如果只讀取一次的話常會失敗,因此才會採用連續讀取的方式,以解決人為操作的小錯誤。

在此for的讀取迴圈中會呼叫mfrc522.PICC_ReadCardSerial()這個函式去讀取卡片序號如果連續5次都沒有成功讀取到,代表真的有問題而不是人為操作的錯誤那麼便會結束函式並傳回”false”的結果(第13行)但只要有一次讀取成功則此for讀取迴圈便會立刻結束,這時讀取到的4位元組的序號值會儲存在mfrc522.uid.uidByte[]」這個由函式庫“MFRC522.h”所定義的陣列中而其餘的程式會先將讀取到的卡片序號由「mfrc522.uid.uidByte[]」轉存至RFID_SN[]陣列以供使用者後續使用同時也把它們傳送到電腦端Arduino IDE上的串列監控視窗以供開發時偵錯之用;而筆者之所以會這樣作除了是為了開發階段測試之用外其實還預留可以在電腦端用其他程式語言去設計一個功能更強大的門禁管制或者差勤管理系統,等筆者發想完成後,也許可以再位讀者們提供另外一個新的製作範例,希望各為讀者能不吝給予意見和指教。

至於最後的23~26行程式還會將該張讀取到卡號的卡片型號也送到電腦的串列監控視窗,並以傳”true”的結果作為本函式的結束;由於卡片型號在本次的製作中並沒有什麼用途,所以只單純傳回電腦就不另外顯示在LCD顯示器上了。


  1. boolean Read_RFID_SN()

  2. {

  3.     flag=false; // 先設定讀取旗標為false

  4.     for(byte j=0;j<5;j++) // 設定讀取迴圈次數為5

  5.     {

  6.         delay(20); // 讀取間隔為20millis

  7.         if (  mfrc522.PICC_ReadCardSerial()) // 開始讀取Mifare卡序號

  8.         {

  9.             flag=true; // 若讀取成功則設定旗標為true

  10.             break; // 離開讀取迴圈

  11.         }

  12.     }

  13.       if(flag==false) return(false); // 若讀取旗標為false則結束函式

  14.         // Select one of the cards

  15.         Serial.print("Card UID:");    // Dump UID至串列監控視窗

  16.         for (byte i = 0; i < mfrc522.uid.size; i++) 

  17.        {  將讀取到的卡片序號轉存至RFID_SN[]陣列以供使用者後續使用

  18.             Receive_Data[i]=mfrc522.uid.uidByte[i];

  19.             RFID_SN[i]=Receive_Data[i];

  20.           Serial.print(Receive_Data[i] < 0x10 ? " 0" : " ");

  21.           Serial.print(Receive_Data[i], HEX);

  22.         }  // 下列程式會將卡片型號傳至串列監控視窗

  23.         Serial.print(" PICC type: ");   // Dump PICC type

  24.         byte piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);

  25.         Serial.println(mfrc522.PICC_GetTypeName(piccType));

25.         Serial.println(" ");

26.   return(true); // 讀取成功函式傳回true的結果

27. }  


五.5.6 系統子功能相關函式

  當使用者進入管理者功能選擇項,也就是圖18【主控制卡卡片管理功能選擇】畫面之後,可以看見編號1~4的選項這4個選項筆者把它們設計成函式的方式,因為這樣會比較好管理,也比較容易看得懂;此外在出廠時我們還需要一個初始化的動作所以這部份的相關的函式共有5個,分別是:

編號

副程式名稱

功能

程式列表行號

boolean AddCard()

新增卡片功能副程式

312~418行

boolean DelCard()

刪除卡片功能副程式

422~517行

boolean ChangePW()

更改密碼副程式

520~603行

boolean Initial_All()

全體初始化功能副程式

606~634行

boolean Initial_System()

系統初始化功能副程式

636~708行

表九。主控制卡卡片管理功能副程式列表


【1】boolean AddCard:新增卡片功能函式(程式列表: 312~418行)

在圖18的【主控制卡卡片管理功能選擇】畫面中,當使用者按下按鍵1(1.Add)時,代表選擇了新增使用者功能,也就是說要增加新的使用者卡片,本函式便是負責所有的操作動作;在使用這個函式時不用給任何的參數,而回傳值為一布林(boolean)型態的值,用來指示函式執行的狀態,如果是正常操作結束,函式會傳回”true”的結果給呼叫者,反之則傳回”false”。

程式一開始先建立一個標記點「Wait_RFID_Card」,以作為後面其他程式的返回跳躍進入點(程式第3行),接著清除LCD顯示器的畫面,然後從Arduino Uno模組板內部的EEPROM記憶體第一個位置讀出目前系統已經學習與儲存的卡片數目至變數[CardCount](程式第6行),再下來的程式是一if測試與判斷指令,由於我們的系統最多提供120個使用者,所以程式會比較已經學習與儲存的卡片數目是否已到達120?如果是,便會執行8~15行區段的程式,這段程式會分別在LCD顯示器的第一行顯示提示訊息"Card Number=120",及在第二行顯示提示訊息"Sorry Cards Full!",即上篇中的圖19-2【儲存卡片數目已滿畫面】,用以通知使用者目前系統已經滿載了!然後等待使用者按下任何鍵離開,或是超過10秒不按下任何鍵,不管是那一種狀況,系統都會馬上結束此函式,並傳回” false”的結果給呼叫者參考。

假如已經學習與儲存的卡片數目尚未到達120,則程式接著會分別在LCD顯示器的第一行顯示提示訊息" Place New Card on ",及在第二行顯示提示訊息" RFID Reader...."(程式16~19行),也就是圖19-1的【選擇”1.Add Card”新增卡片功能等待新卡到來】畫面,要求使用者將卡片放置在RFID讀寫器模組的上方,以便系統開始讀取卡片序號。接著的21~31行區段程式主要由一個while的測試迴圈所構成,在此程式會先取得系統目前的時間(即呼叫” Wait_Time=millis”),並以此為基準等待卡片於10秒內靠近RFID讀寫器模組,如果超過這個時間沒有偵測到卡片的出現,或是使用者按下了任意鍵,則程式會發出提示聲,並立即結束這個函式及傳回” false”的結果。如果在限定時間內偵測到有效的卡片,則旗標變數[flag]的值會保持在”true”的狀態。

首先我們呼叫『Read_RFID_SN()』這個RFID卡片序號函式去讀取卡片的序號資料(程式第34行),如果此函式的傳回值為”false”代表讀取錯誤,則36~40行的程式會在LCD顯示器第二行顯示提示"Setting Error!"訊息,然後發出警告聲並離開本函式。當讀取卡片序號成功時(即flag值為”true”)程式會先在LCD顯示器第一行顯示提示訊息" Card SN:",然後呼叫『Show_RFID_SN()』這個卡片序號顯示函式,將讀到回的卡片序號資料顯示在後,也就是圖20【新增卡片已經學習儲存過】畫面的上半部份;接著呼叫『Check_Card_Exist()』函式檢查該筆卡片序號資料是否已經學習及儲存在系統中(第49行程式) ,如果函式傳回值為”true”,代表該張卡片的確存在,則系統會執行51~56行的區塊程式;在此程式會先於LCD顯示器第二行顯示"Card Already Exist!"也就是卡片已經存在訊息(即圖20【新增卡片已經學習儲存過】完整畫面),然後發出一警告聲,接著等待使用者按下任何鍵,或是超過10秒不作任何動作,不管時是那一種情形先發生,程式最後都會跳回到前面第3行的「Wait_RFID_Card」標記處重新等待使用者學習新的卡片(程式第55行)。

接下來從第57行程式開始到函式結束為止都是密碼輸入的部份,在此我們呼叫前面介紹過的密碼輸入函式『Password_In()』等使用者輸入新的密碼,此時LCD顯示器會呈現圖21的【新增卡片輸入密碼】畫面,若返回值為”true”代表密碼輸入動作完成,不過這並不代表使用者輸入的就是4位數的密碼,因此必須先測試使用者是否按下了’C’鍵?如果是代表使用者取消本次的密碼輸入,一般來說也就是重新來過的意思,因此程式會跳回到前面第3行的「Wait_RFID_Card」標記處重新開始學習新的卡片(程式59、60行)。假如是正常的密碼輸入動作,便會呼叫『Accept_Cancel()』函式測試使用者是否接受新密碼(程式第61行)?這時LCD顯示器則是顯示圖22的【新增卡片密碼輸入完成等待使用者確認】畫面,若『Accept_Cancel()』函式返回值為” false”,代表按鍵輸入逾時,那麼就立刻結束函式,並傳回” false”的結果給呼叫者辨認。這個函式中真正的按鍵值是存放在全域變數[KeyNum]上,因此我們先測試是否按下’C’鍵(即KeyNum的值=12),如果是,代表使用者不接受該組新的密碼,則同樣跳回到前面第3行的「Wait_RFID_Card」標記處,重新開始學習新卡片的過程(程式63、64行)。

如果使用者認可所輸入的密碼值,代表接受了新的卡片資料,則程式會從Arduino Uno板內部的EEPROM第一個位址讀出目前已學習及儲存卡片的數目至變數[CardCount],並將它加一(程式65、66行);接下來的67、77行程式分別將4 byte的卡片序號與4 byte的密碼移到陣列變數[EEPROM_data]內,然後呼叫『Save_Check_EEPROM』將新的卡片資料儲存起來,並檢查是否儲存成功?如果傳回值為”true”表示儲存成功,則會執行79~99 行的程式區塊。在此主要是實現圖23的【新增卡片儲存成功】畫面,也就是在LCD顯示器第一行顯示”Card Number=??”的提示訊息,而其中的問號為目前已儲存卡片的數目值,而LCD顯示器第二行則顯示提示訊息"New Card Saved!",告知使用者新卡片資料學習完成,同時也會將新的使用者數目值回存至Arduino Uno板內部的EEPROM(程式第89行)。接著在發出提示音後會等待一秒鐘,然後在LCD顯示器第一行顯示” Press any key ..”的提示訊息,提醒使用者按下任意鍵或是不作任何動作10秒鐘(程式90~95行);不管是那一種狀況,程式會測試使用者是否按下’C’鍵(即KeyNum的值=12),若是則結束此函式並傳回”true”值給呼叫者,否則會跳回到前面第3行的「Wait_RFID_Card」標記處,重新開始學習新卡片的動作(程式96~98行)。


9. const  byte  RFID_SIG=12; 

// 上面變數的行號為程式列表中變數定義部份行號

  1. boolean AddCard()

  2. {

  3.   Wait_RFID_Card: // 此處是一個標記點

4.     LCD_Clear_Line(0); // 清除LCD顯示器

5. LCD_Clear_Line(1); // 

6.    CardCount=EEPROM.read(0); // 從EEPROM中讀取已儲存卡片數

7.    if(CardCount==120) // 測試儲存卡片數是否已達上限(120)

8.     {

9. lcd.setCursor(2,0);   // 將游標移到LCD顯示器第一行第三格

10.       lcd.print("Card Number=120");  //顯示提示訊息"Card Number=120"

11.       lcd.setCursor(1,1); // 再將游標移到LCD顯示器第二行第二格 

12.      lcd.print("Sorry Cards Full!"); //顯示提示訊息"Sorry Cards Full!"

13.      WaitKeyIn(10); // 等待使用者按下任何鍵或是超過10秒

14.     return(false); // 結束函式並傳回” false”的結果

15.     }    

16.     lcd.setCursor(1,0); //將游標移到LCD顯示器第一行第一格

17.     lcd.print("Place New Card on"); //顯示提示訊息" Place New Card on "

18.     lcd.setCursor(2,1); //將游標移到LCD顯示器第二行第二格

19.    lcd.print("RFID Reader...."); //顯示提示訊息" RFID Reader...."

20.     WaitKeyRelease(); // 等待使用者放開按鍵

21.    flag=true; // 先令旗標暫存器flag值為”true”

22.    Wait_Time=millis(); // 取得目前系統的時間

23.    while(digitalRead(RFID_SIG)==1) // 等待卡片靠近RFID讀寫器模組

24.     { // 如果有卡片出現則RFID_SIG腳=0

25.        if(((millis()-Wait_Time)>=10000)||(keyPress()==true))

26.         { // 若時間到達10秒或是使用者按下任意鍵

27.           flag=false; // 令旗標暫存器flag值為” false”

28.          BeeBee(); // 發出警告聲

29.          return(false); // 結束函式並傳回” false”的結果

30.         }

31.     }

32.     if(flag==true) // 測試flag值是否為”true”

33.     { // 若是代表RFID讀寫器模組已經感應到有卡片靠近

34.       if(Read_RFID_SN()==false) // 測試讀取卡片序號是否成功

35.       { // 若傳回值為”false”代表讀取失敗

36.          LCD_Clear_Line(1); //清除LCD顯示器第二行

37.          lcd.setCursor(3,1);   // 將游標移到LCD顯示器第二行第四格

38.           lcd.print("Setting Error!"); // 顯示提示訊息"Setting Error!"

39.           BeeBee(); // 發出警告聲

40.          exit; // 離開本函式

41.        }

42.        else // 

43.         { // 讀取卡片序號成功則執行下列程式

44.          LCD_Clear_Line(0); // 清除LCD顯示器畫面

45.          LCD_Clear_Line(1); // 

46.          lcd.setCursor(2,0);   //將游標移到LCD顯示器第一行第三格

47.          lcd.print("Card SN:"); // 顯示提示訊息" Card SN:"

48.           Show_RFID_SN(); // 接著顯示讀取到的卡片序號

49.          if(Check_Card_Exist()) // 呼叫” Check_Card_Exist()”函式

50.          { // 如果卡片已經學習並儲存在系統中則執行下列程式

51.            lcd.setCursor(1,1);//將游標移到LCD顯示器第二行第二格

52.            lcd.print("Card Already Exist!"); //顯示卡片已經存在訊息

53.            BeeBee(); // 發出警告聲

54.             WaitKeyIn(10);  //等待使用者按下任何鍵或是超過10秒

55.             goto Wait_RFID_Card; // 跳回到前面第3行的標記處

56.         }

57.         if(Password_In()==true)   //呼叫密碼輸入函式” Password_In”

58.         { // 若返回值為”true”代表密碼輸入動完成

59.            if(KeyNum==12) // 測試是否按下’C’鍵如果是

60.               goto Wait_RFID_Card; //跳回到前面第3行的標記處

61.           if(Accept_Cancel()==false) //測試使用者是否接受新密碼

62.             return(false); //若返回值為” false”代表按鍵輸入逾時

63.             if(KeyNum==12) //測試是否按下’C’鍵如果是

64.               goto Wait_RFID_Card; //跳回到前面第3行的標記處

65.             CardCount=EEPROM.read(0); //讀取已學習卡片數目值

66.             CardCount++; // 將卡片數目值加一

67.             for(byte j=0;j<4;j++) // 

68.             {  //先將4 byte的卡片序號移到陣列EEPROM_data前面

69.               EEPROM_data[j]=RFID_SN[j]; // 

70.             Serial.print(EEPROM_data[j],HEX);

71.             }

72.             Serial.println();

73.             for(byte j=4;j<8;j++) // 

74.             { // 再把4 byte的密碼移到陣列EEPROM_data後面

75.                 EEPROM_data[j]=RFID_PW[j-4]; // 

76.                 Serial.print(EEPROM_data[j],HEX);

77.             }

78.           if(Save_Check_EEPROM(CardCount*8,EEPROM_data)==true)

79.             { // 如果新的使用者卡片序號與密碼資料儲存成功

80.                 LCD_Clear_Line(0); // 清除LCD顯示器第一行

81.                 lcd.setCursor(2,0); // 設定游標位置為第一行第三格

82.                 lcd.print("Card Number="); // 顯示提示訊息

83.                 if(CardCount<10) //測試卡片數目是否小於10

84.                   lcd.print("0"); // 若是則先補一個‘0’

85.                 lcd.print(CardCount); // 顯示已學習卡片數目

86.                 LCD_Clear_Line(1); // 清除LCD顯示器第二行

87.                 lcd.setCursor(2,1); // 設定游標位置為第二行第三格

88.                 lcd.print("New Card Saved!"); // 顯示提示訊息

89.                 EEPROM.write(0,CardCount); //儲存已學習卡片數目

90.                 BeeBee(); //發出提示聲

91.                 delay(1000); // 等待一秒鐘

92.                 LCD_Clear_Line(0); // 清除LCD顯示器第一行

93.                 lcd.setCursor(2,0); // 設定游標位置為第一行第三格

94.                 lcd.print("Press any key .."); //提醒使用者按下任意鍵

95.                 WaitKeyIn(10); //等待使用者按下任何鍵

96.                 if(KeyNum==12) // 測試是否按下’C’鍵

97.                     return(true); // 若是則結束程式並傳回”true”值

98.                 goto Wait_RFID_Card; // 否則跳回到前面第3行的

// 標記處再次執行新增卡片功能

99.             }

100.           }

101.       }

102.     }

103 }


【2】boolean DelCard:刪除卡片功能函式(程式列表: 422~517行)

在圖18的【主控制卡卡片管理功能選擇】畫面中,如果使用者按下按鍵2,表示選擇了刪除使用者功能(2.Del. Card),所有的動作都是由本函式負責;同樣的在使用這個函式時也不用給任何的參數,而回傳值為一布林(boolean)型態的值,用來指示函式執行的狀態,如果是正常操作結束,則函式會傳回”true”的結果給呼叫者,反之則傳回” false”以資辨識。

程式一開始先建立一個標記點「Delete_Card」以作為後面其他程式的返回跳躍進入點(程式第5行),接著6~14的程式會清除LCD顯示器的畫面,然後在LCD顯示器第二行顯示提示訊息"2.Delete Card ",表示已經進入了刪除使用者卡片的功能,再回頭在LCD顯示器第一行顯示"Card Number:"的提示訊息,並在發出提示聲後,將函式中會使用到的兩個全域變數[CardNum](卡片數目變數)與[NumDigital](輸入位數長度變數)初值都設定為0,開始等待使用者輸入欲刪除的卡片號碼(即圖26的【欲刪除卡片號碼輸入】畫面)。

整個欲刪除的卡片號碼的輸入動作是由15~42行的do..while()迴圈所構成,由於我們系統預設的使用者最多可到120位,也就是說卡片的號碼最多到3位數,而使用者輸入的位數值會存放在[NumDigital]這個變數內,這也就是此do..while()迴圈的結束測試條件(程式第42行) ,即[NumDigital]這個變數如果到達3便表示已經輸入完成。這個迴圈一開始先建立一個標記點「Input_Card_Num」以作為卡片號碼輸入迴圈起點,然後開始測試及等待使用者按下按鍵,如果按鍵輸入逾時超過10秒或是按下了’C’鍵,那麼便馬上結束這個函式,並且傳回” false”的結果給呼叫者(程式17~19行)。由於我們的系統可以容許使用者輸入1~3位數的卡片號碼,假如使用者輸入的數值低於3位數,則必須按下’F’鍵代表完成輸入動作;所以如果是正常的按鍵輸入,程式會先檢查按下的是否為按鍵’F’,如果是還要再檢查是否在一開始就按下了’F’鍵,因為這樣就代表使用者要立即結本函式,因此輸入位數變數值如果為零(即NumDigital=0),也就是說使用者沒有輸入任何卡片號碼,那麼同樣必須結束函式並傳回” false”的結果;若非則跳到43行的「Card_Num_End」標記處,離開這個卡片號碼輸入的do..while()迴圈(程式20~26行)。

由於我們的卡片號碼是使用十進制,所以接著的27~37行程式便會檢查使用者的按鍵輸入值是否介於0~9之間?如果不是代表使用者按到了’A’~’E’的鍵,此為不合法輸入,則程式會在LCD顯示器的第二行顯示警告訊息"Illegal Input!!"( 即圖27的【欲更改密碼卡片號碼鍵盤輸入錯誤】畫面),並發出警告聲,然後等待1.5秒鐘之後回到17行「Input_Card_Num」標記處重新執行卡號輸入動作。如果輸入的是正常卡片號碼,則迴圈中其餘的程式會將按鍵值顯示在LCD顯示器上,並將輸入的結果轉換成卡片號碼存放在全域變數[CardNum](即卡片數目變數)內,直到整個卡號輸入動作完成為止。

輸入完欲刪除卡片號碼後,接下來就要開始真正執行刪除卡片的動作了,不過前面使用者輸入卡片號碼的過程雖然正常,但不代表輸入的數字就合理,因為使用者可能只輸入了’0’這個不存在的號碼,或者輸入了管理者專用的’1’號,而這個號碼當然是不能刪除的;此外還有可能輸入的號碼大於目前系統所儲存的卡片數目,因此接著的程式會先從Arduino Uno板內部的EEPROM內讀出目前已儲存的卡片數目至「CardCount」這個變數(程式第46行),然後測試使用者輸入的卡片號碼是否為0?如果是代表輸入錯誤,那麼程式會在LCD顯示器第二行顯示警告訊息" Card Number Error!",然後發出警告聲,並在等待2秒鐘後回到第5行” Delete_Card”標記點重新來過(程式48~56行)。接著57~65行程式則是測試使用者輸入的卡片號碼是否為1?如果是,同樣代表輸入錯誤,接下來的動作與上面一樣,只是顯示警告訊息改為" Is Master Card!"而已(即圖29的【欲刪除卡片號碼為主控制卡錯誤訊息】畫面),最後同樣回到第5行” Delete_Card”標記點重新執行刪除卡片的動作。而下面66~74程式會測試使用者輸入的卡片號碼是否大於已儲存卡片數目?若是同樣是錯誤輸入,接著的動作也和上面一樣,會先發出警告聲與顯示訊息提醒使用者,而此時顯示的內容改為"Card number too big!" (即圖30的【欲刪除卡片號碼大於已儲存卡片數目錯誤訊息】畫面)。

由於刪除完卡片之後會在Arduino Uno板內的EEPROM記憶體產生無用的空格,為了有效使用記憶體的空間,所以我們的系統必須自動把這個空格補起來;筆者的作法是將最後一個號碼的卡片資料補到被刪除號碼的空間,這樣一來只要移動一筆資料就可以了。不過假如者輸入的卡片號碼正好是最後一個用戶,那麼就只要把儲存的卡片數目減一再回存至EEPROM記憶體內就可以;因此75、76行程式先測試輸入的卡片號碼是否為最後的用戶號碼?若是則跳到82行的「Dec_Card_Count」標記處,也就是說跳過移動一筆資料的部份。如果是其他的卡號,那麼77~81行的for迴圈會把將最後一筆儲存卡片資料移動到欲刪除卡片位置以覆蓋它,這樣就等同於將該筆資料刪除。剩下部份的程式會先將已儲存卡片數目值即「CardCount」這個變數減一後回存至EEPROM內的第一個位址(程式83~85行),接著在LCD顯示器第二行顯示卡片刪除成功提示訊息(即圖31的【欲刪除卡片功能成功】畫面),在發出提示聲及等待2秒鐘後,再次回到第5行「Delete_Card」標記點,重新開始執行刪除卡片的功能。


  1. boolean DelCard()

  2. {

  3.    int NumDigital,CardNum;

4.  

5.   Delete_Card: // 此處是一個標記點

6.    LCD_Clear_Line(1); // 清除LCD顯示器第二行

7.     lcd.setCursor(2,1); // 設定游標位置為第二行第三格

8.     lcd.print("2.Delete Card "); // 顯示提示訊息"2.Delete Card "

9.     LCD_Clear_Line(0); // 清除LCD顯示器第一行

10.     lcd.setCursor(1,0); // 設定游標位置為第一行第二格

11.     lcd.print("Card Number:"); // 顯示提示訊息" Card Number:"

12.      BeeBee(); //發出提示聲

13.     CardNum=0; // 設定卡片數目變數初值為0

14.     NumDigital=0; //設定輸入位數長度變數初值為0

15.     do // 與42行構成一do ….while迴圈,執行卡號輸入動作

16.     {   //只要輸入位數長度變數NumDigital<3便會繼續執行

17. Input_Card_Num: // 此處是一個標記點,為卡片號碼輸入迴圈起點

18.       if((WaitKeyIn(10)==false)||(KeyNum==12)) // 測試及等待使用

者按下按鍵,如果逾時超過10秒或是按下’C’鍵

19.         return(false); // 結束函式並傳回” false”的結果

20.       if(KeyNum==15) // 測試使用者是否按下按鍵’F’

21.       {

22.        if(NumDigital==0) // 測試使用者是否無任何輸入

23.         return(false); //如果是同樣結束函式並傳回” false”的結果

24.         else

25.           goto Card_Num_End; //若非則跳到43行標記處

26.     }

27.     if((KeyNum>9)) // 測試使用者按鍵輸入值是否大於9

28.     { // 若是代表按到’A’~’F’的鍵,此為不合法輸入

29.       LCD_Clear_Line(1); // 清除LCD顯示器第二行

30.   lcd.setCursor(2,1); // 設定游標位置為第二行第三格

31.         lcd.print("Illegal Input!!"); // 顯示警告訊息"Illegal Input!!"

32.         BeeBee(); // 發出警告聲

33.         delay(1500); // 等待1.5秒鐘

34.         LCD_Clear_Line(1); // 清除LCD顯示器第二行

35.         lcd.setCursor(13+NumDigital,0);

36.         goto Input_Card_Num;   //回到17行重新執行卡號輸入動作

37.       }

38      lcd.print(KeyNum); // 顯示出按鍵的數值

39.      CardNum=CardNum*10+KeyNum; // 將按鍵數值轉換成數字

40.     NumDigital++; // 已按鍵數目加一

41.          Serial.println(NumDigital);

42. } while(NumDigital<3); // 第15行do迴圈的結束點

43. Card_Num_End: // 此處為卡片號碼輸入結束標記點

44.     Serial.println(CardNum);

45.     Serial.println("CardNum Input end!");

46.     CardCount=EEPROM.read(0); // 先從EEPROM讀出已儲存卡片數目

47.            Serial.println(CardCount);

48.     if(CardNum==0) //測試使用者輸入的卡片號碼是否為0

49.     { 如果使用者輸入的卡片號碼為0代表輸入錯誤

50.         LCD_Clear_Line(1); // 清除LCD顯示器第二行

51.         lcd.setCursor(2,1); // 設定游標位置為第二行第三格

52.        lcd.print("Card Number Error!"); //顯示警告訊息" Card Number Error!"

53.         BeeBee(); // 發出警告聲

54.         delay(2000); // 等待2秒鐘

55.         goto Delete_Card; //回到第5行” Delete_Card”標記點重新來過

56.      }  

57.     if(CardNum==1) //測試使用者輸入的卡片號碼是否為1

58 { 如果使用者輸入的卡片號碼為1是管理員卡片,同樣代表輸入錯誤

59          LCD_Clear_Line(1); // 接下來的動作與上面一樣只是

60.         lcd.setCursor(2,1);

61.         lcd.print("Is Master Card!"); // 顯示警告訊息改為" Is Master Card!"

62.         BeeBee();

63.         delay(2000);

64.         goto Delete_Card;

65.     }  

66.     if(CardNum>CardCount) // 測試使用者輸入的卡片號碼是否

67.      { // 大於已儲存卡片數目,若是同樣為錯誤輸入

68.         LCD_Clear_Line(1); // 接下來的動作與上面一樣

69.         lcd.setCursor(0,1); // 發出警告訊息提醒使用者

70.         lcd.print("Card number too big!");

71.         BeeBee();

72.         delay(2000);

73.         goto Delete_Card;

74.      }  

75.     if(CardNum==CardCount) // 測試輸入的卡片號碼是否為最後的號碼

76.       goto Dec_Card_Count;  // 若是則跳到82行的標記處

77.     for(byte j=0;j<8;j++) // 欲刪除卡片號碼正常開始刪除目標卡片

78.     { // 將最後一筆儲存卡片資料移動到欲刪除卡片位置以取代它

79.     EEPROM_data[j]=EEPROM.read(CardCount*8+j);

80.         EEPROM.write(CardNum*8+j,EEPROM_data[j]);

81.     }

82   Dec_Card_Count:

83.     CardCount--; // 將已儲存卡片數目值減一

84.            Serial.println(CardCount);

85.     EEPROM.write(0,CardCount); // 然後回存至EEPROM內

86.       LCD_Clear_Line(1); // 清除LCD顯示器第二行

87.       lcd.setCursor(2,1); // 設定游標位置為第二行第三格

88.       lcd.print("Card "); // 顯示卡片刪除成功提示訊息

89.       lcd.print(CardNum); // 包括刪除卡片的號碼數目資料

90.     lcd.print(" been Del.!");

91.     BeeBee(); //發出提示聲

92.     delay(2000); // 等待2秒鐘

93.      goto Delete_Card; //回到第5行” Delete_Card”標記點重新來過

94. WaitKeyIn(0); // 等待使用者按下任意鍵

95. return(false); // 結束函式並傳回” false”的結果

96. }

            

【3】boolean ChangePW:更改密碼功能函式(程式列表:520~603行)

在圖18的【主控制卡卡片管理功能選擇】畫面中,按下按鍵3時(3.Chg. PW)便可以選擇更改使用者密碼的功能;在使用這個函式時同樣的不用給任何的參數,且回傳一個布林(boolean)型態的值,用來指示函式執行的狀態,如果是正常操作結束,則函式會傳回”true”的結果給呼叫者,反之則傳回” false”以資辨識。

程式一開始同樣的先建立一個標記點「Change_Password」以作為後面程式的返回跳躍進入點(程式第5行)。接著6~14行的程式會清除LCD顯示器的畫面,然後先在LCD顯示器第二行顯示提示訊息"3.Change PW. ",表示已經進入了更改使用者密碼的功能,再回頭在LCD顯示器第一行顯示" Card Number:"的提示訊息(圖32的【欲更改密碼卡片號碼輸入】畫面),並在發出提示聲後,將函式中使用到的兩個全域變數[CardNum](卡片數目變數)與[NumDigital](輸入位數長度變數)初值都設定為0,然後開始等待使用者輸入欲更改使用者密碼的卡片號碼。

整個欲更改密碼卡片號碼的輸入動作是由15~42行的do..while()迴圈所構成,和前一個函式一樣,因為我們系統預設的使用者最多到120位,所以卡片的號碼最多到3位數,而使用者輸入的位數值會存放在[NumDigital]這個變數內,這也就是此do..while()迴圈的結束測試條件(程式第42行) ,即[NumDigital]這個變數如果到達3便表示已經輸入完成。然後等待使用者按下任何鍵,或是超過10秒不按下任何鍵,不管是那一種狀況,系統都會馬上結束函式,並傳回” false”的結果給呼叫者參考。由於整個do..while()迴圈的動作的內容和上一個函式幾乎一模一樣,所以就請讀者們參考前面的說明,在此就不再贅言。

欲更改密碼卡片號碼輸入完後,同樣的要先檢查使用者輸入的卡片號碼是否正確,因為使用者輸入的號碼可能大於目前系統所儲存的卡片數目,因此程式接著會先從Arduino Uno板內部的EEPROM內讀出目前已儲存的卡片數目至「CardCount」這個變數(程式第44行),接著後面46~54程式會測試使用者輸入的卡片號碼是否大於已儲存卡片數目?若是會先在LCD顯示器的第二行顯示警告訊息" Card number too big!" (即圖33的【欲更改密碼卡片號碼鍵盤輸入錯誤】畫面),然後發出警告聲及等待2秒鐘,最後跳回到第5行的「Change_Password」標記點重新執行更改密碼卡片號碼輸入的動作。

假如欲更改密碼的卡片號碼輸入成功,則程式會跳到55行的「New_Password_In」標記處,開始輸入新的密碼值;在此我們呼叫前面介紹過的密碼輸入函式『Password_In()』等使用者輸入新的密碼,並測試輸入過程是否成功?若『Password_In』傳回值為“false”代表失敗,那麼必須結束本函式並傳回” false”的結果給呼叫者辨識(程式第57行)。

接下來從59行開始到函式結束,都是屬於新密碼正常輸入完成的處理程式,首先程式會測試輸入的按鍵值是否為取消鍵’C’(即變數”KeyNum”=12)?如果是則回到第5行的「Change_Password」標記點重新來過。假如不是,代表使用者輸入了4位數的新密碼,此時我們會呼叫『Accept_Cancel』這個函式測試使用者是否接受剛才輸入的密碼(即圖37的【欲更改卡片密碼新密碼輸入完成等待確認】畫面)?若『Accept_Cancel』函式傳回”false”的結果代表有問題,則立即結束函式並傳回”false”值給呼叫者。如果選擇操作正常我們必須先測試輸入的按鍵值是否為’取消鍵’C’若是那麼就會回到第5行的「Change_Password」標記點重新執行卡號輸入動作。假如使用者按下的是按鍵’A’,代表接受該組新的密碼,便執行68~72行的for迴圈,將4 byte的新密碼值寫入使用者輸入的卡片號碼儲存位址內。最後程式先在LCD顯示器第一行顯示提示訊息"New Password Saved!" (圖38的【更改卡片密碼完成】畫面),並發出提示聲和等待1.5秒鐘,然後同樣回到第5行的「Change_Password」標記點繼續執行更改密碼的動作直到更新密碼的操作結束為止。


  1. boolean ChangePW()

  2. {

3. int NumDigital,CardNum;

4.  

5. Change_Password: // 此處為一標記點

6.     LCD_Clear_Line(1); // 清除LCD顯示器第二行

7.     lcd.setCursor(2,1); // 設定游標位置為第二行第三格

8.     lcd.print("3.Change PW."); // 顯示提示訊息"3.Change PW."

9.     LCD_Clear_Line(0); // 再清除LCD顯示器第一行

10.     lcd.setCursor(1,0); // 設定游標位置為第一行第二格

11.     lcd.print("Card Number:"); // 顯示提示訊息" Card Number:"

12.     BeeBee(); //發出提示聲

13.     CardNum=0; // 設定卡片數目變數初值為0

14.     NumDigital=0; //設定輸入位數長度變數初值為0

15.     do// 與42行構成一do ….while迴圈,執行卡號輸入動作

16.     {   //只要輸入位數長度變數NumDigital<3便會繼續執行

17. Input_Card_Num: // 此處是一個標記點,為卡片號碼輸入迴圈起點

16. 

18.      if((WaitKeyIn(10)==false)||(KeyNum==12)) // 測試及等待使用

者按下按鍵,如果逾時超過10秒或是按下’C’鍵

19.         return(false); // 結束函式並傳回” false”的結果

20.       if(KeyNum==15) // 測試使用者是否按下按鍵’F’

21.       {

22.        if(NumDigital==0) // 測試使用者是否無任何輸入

23.         return(false); //如果是同樣結束函式並傳回” false”的結果

24.         else

25.           goto Card_Num_End; //若非則跳到43行標記處

26. }

27.       if((KeyNum>9)) // 測試使用者按鍵輸入值是否大於9

28.     { // 若是代表按到’A’~’F’的鍵,此為不合法輸入

29.       LCD_Clear_Line(1); // 清除LCD顯示器第二行

30.   lcd.setCursor(2,1); // 設定游標位置為第二行第三格

31.         lcd.print("Illegal Input!!"); // 顯示警告訊息"Illegal Input!!"

32.         BeeBee(); // 發出警告聲

33.         delay(1500); // 等待1.5秒鐘

34.         LCD_Clear_Line(1); // 清除LCD顯示器第二行

35.         lcd.setCursor(13+NumDigital,0);

36.         goto Input_Card_Num;   //回到17行重新執行卡號輸入動作

37.       }

38. lcd.print(KeyNum); // 顯示出按鍵的數值

39.      CardNum=CardNum*10+KeyNum; // 將按鍵數值轉換成數字

40.     NumDigital++; // 已按鍵數目加一

41.          Serial.println(NumDigital);

42. } while(NumDigital<3); // 第15行do迴圈的結束點

43.  Card_Num_End: // 此處為卡片號碼輸入結束標記點

44.      CardCount=EEPROM.read(0); // 先從EEPROM讀出已儲存卡片數目

45.            Serial.println(CardCount);

46.     if(CardNum>CardCount) // 測試使用者輸入的卡片號碼是否

47.     { // 大於已儲存卡片數目,若是同樣為錯誤輸入

48.         LCD_Clear_Line(1); // 清除LCD顯示器第二行

49.         lcd.setCursor(0,1); // 設定游標位置為第二行第一格

50.     lcd.print("Card number too big!");// 顯示警告訊息" Card number too big!"

51.         BeeBee(); // 發出警告聲

52.         delay(2000); // 等待2秒鐘

53.         goto Change_Password;   // 回到5行重新執行卡號輸入動作

54.     }  

55.  New_Password_In: // 此處標記代表欲更改密碼的卡片號碼輸入成功

56     if(Password_In()==false) // 呼叫密碼輸入副程式並測試是否成功

57.       return(false); // 若”Password_In”傳回值為“false”代表失敗

58.     else

59.     { 此處代”Password_In”傳回值為“true”,即密碼輸入成功

60.     if(KeyNum==12)   // 測試使用者輸入的按鍵值是否為’C’取消鍵

61.             goto New_Password_In; //若是回到5行重新執行卡號輸入動作

62.         if(Accept_Cancel()==false) //呼叫“Accept_Cancel”函式測試使用者

63.       return(false); // 對輸入密碼的選擇,若傳回”false”代

// 表有問題,則結束此函式並傳回”false”值

64.     else

65.       { // 密碼輸入選擇成立

66.         if(KeyNum==12)   // 測試輸入的按鍵值是否為’C’取消鍵

67.         goto Change_Password; //若是回到5行重新執行卡號輸入動作

68.       for(byte i=0;i<4;i++) // 寫入4 byte的for 迴圈

69. {   // 將4 byte的新密碼值寫入使用者輸入的卡片號碼位址

70.        Serial.print(RFID_PW[i]);

71.           EEPROM.write(CardNum*8+4+i,RFID_PW[i]);

72.         }

73.       Serial.println();

74.         Serial.println("New Password Saved!");

75.         LCD_Clear_Line(0); // 再清除LCD顯示器第一行

76.         lcd.setCursor(1,0); // 設定游標位置為第一行第二格

77.           lcd.print("New Password Saved!");//顯示提示訊息

78.         BeeBee(); //發出提示聲

79.       delay(1500); // 等待1.5秒鐘

80.       goto Change_Password; //重新回到5行執行更改密碼的動作

81.      }

82.     }

83. }

            

【4】boolean Initial_All():全體初始化功能函式(程式列表:606~634行)

在圖18【主控制卡卡片管理功能選擇】畫面中,如果使用者選擇的是第四個選項” 4.Init Sys.,代表使用者要執行系統重置或是初始化的動作,也就是說把所有的使用者(除了管理者本身)都清除掉,這算是很嚴重的事,因此系統會先發出提示音後,才在LCD顯示器第一行顯示提示訊息” 4.Initial All”及在第二行顯示提示訊息” Are you sure?!”,然後等待1.5秒鐘,再次先發出提示音提醒使者,也就是顯示圖40【選擇系統初始化】畫面,接著呼叫“Accept_Cancel”函式,請使用者再次確認是否要進行初始化的動作(圖41的【系統初始化選擇再次確認】畫面)?如果使用者按下了按鍵則函式會繼續執行剩下的程式(15~26行),也就是清除所有使用者(管理者除外)的動作,不過在此我們只是單純的把’1’這個數目字寫入EEPROM內第一個位址,也就是儲存已學習卡片數目的地方(程式第16行),因為這樣系統會認為只有一個使用者,即主控制卡,這樣也就等於回到出廠時的狀態。然後程式會在LCD顯示器第一行、第二行分別顯示” 4.Initial All”與” function end...”的提示訊息(圖42的【系統初始化完成】畫面),接著發出提示音並等待1.5秒鐘,最後結束此函式並傳回”true”值。若使用者按下其他鍵,則會立即結束此函式並傳回”false”值(程式第28行)。


  1. boolean Initial_All()      

2. {

3.      BeeBee(); // 先發出提示聲

4.      LCD_Clear_Line(0); // 再清除LCD顯示器第一行

5.      lcd.setCursor(0,0); // 設定游標位置為第一行第一格

6.      lcd.print("4.Initial All"); // 顯示提示訊息” 4.Initial All”

7.      LCD_Clear_Line(1); // 接著清除LCD顯示器第二行

8.      lcd.setCursor(2,1); // 將游標移到為第二行第三格

9.      lcd.print("Are you sure?!"); //顯示提示訊息” Are you sure?!”

10.     delay(1500); // 等待1.5秒鐘

11.     BeeBee(); // 再次先發出提示聲

12.     if(Accept_Cancel()==true) //呼叫“Accept_Cancel”函式測試使用者輸入

13.     {

14.        if(KeyNum==10) // 測試輸入的按鍵值是否為’A’確認鍵

15.        { // 若是則執行初始化也就是清除所有使用者(管理者除外)的動作

16.           EEPROM.write(0,1); // 將數字1寫入使用者數目儲存位址

17.           LCD_Clear_Line(0); // 清除LCD顯示器第一行

18.         lcd.setCursor(0,0); // 設定游標位置為第一行第一格

19.           lcd.print("4.Initial All"); // 顯示提示訊息” 4.Initial All”

20.           LCD_Clear_Line(1); // 接著清除LCD顯示器第二行

21.           lcd.setCursor(2,1); // 將游標移到為第二行第三格

22.           lcd.print("function end..."); // 顯示提示訊息” function end...

23.           BeeBee(); // 發出提示聲

24.           delay(1500); // 等待1.5秒鐘

25.           return(true); // 結束此函式並傳回”true”值

26.        }  

27. }

28.     return(false); // 若使用者按下其他鍵則結束此函式並傳回”false”值

29. }            


【5】boolean Initial_System():系統初始化功能函式(程式列表: 636~708行)

這個函式主要是用來設定新的管理者卡片與讓系統還原到出廠時的狀態,當我們的系統在接上電源時如果偵測到主控制卡設定接腳(即Arduino Uno模組板的D12腳)有被短接,也就是說要進入主控制卡出廠設定功能,此時便會啟動這個函式。函式一開始會先發出一聲提示音,並令LCD顯示器第一行、第二行分別顯示” Master Card Setting.”與” Please Process...”兩行提示訊息(即圖4的【主控制卡設定等待卡片到來】畫面),然後等待使用者將要作為主控制卡的卡片放到RFID讀寫器模組上可感測到的距離內(程式4~11行);當RFID讀寫器模組偵測到有卡片出現時,即『mfrc522.PICC_IsNewCardPresent()』這個函式的傳回值為”true”時便會結束第10行的while迴圈,然後呼叫『Read_RFID_SN()』這個函式測試讀取卡片序號是否成功?如果傳回值為”false”表示失敗,則程式13~18行會令LCD顯示器第二行顯示警告訊息” Setting Error!”,同時發出警告聲(即圖5的【主控制卡設定讀取失敗】畫面)。

如果讀取成功,則程式會執行20~37行區段程式作為處理卡片序號讀取成功之用;首先LCD顯示器第一行會顯示提示訊息” Card SN:”,然後呼叫函式『Show_RFID_SN()』將卡片序號顯示在後,並在LCD顯示器第二行顯示設定成功訊息”Setting OK!”(即圖6的【主控制卡設定成功】畫面);接著將預設的出廠密碼”1234”寫入Arduino Uno模組板內的EEPROM記憶體,為了確保密碼正確寫入,還會再呼叫序號寫入與驗證函式『Save_Check_EEPROM』檢查,如果此函式傳回”false”的結果表示寫入有錯,則在LCD顯示器第二行顯示警告訊息"Master SN Write Err!",並發出警告聲提醒使用者(即圖7【主控制卡儲存至Arduino Uno模組板失敗】畫面)。不管是RFID讀寫器模組讀取卡片序號錯誤或是成功,最後我們都必須等待卡片離開RFID讀寫器模組才會進行後續的動作,第37、38行的while迴圈便是在等待卡片離開。

接著的40~45行程式會在LCD顯示器上分別顯示設定成功訊息” 1.Keep Card Setting.”與 2.Initial All”,亦即圖8的【系統初始化選擇】畫面,然後執行47~60行的do….while迴圈程式,等待使用者的選擇輸入;在此迴圈中先呼叫『WaitKeyIn(10) 』函式等待使用者按下選擇鍵或測試按鍵逾時,如果函式傳回值為”true”代表有按鍵被按下,則測試使用者按下的鍵為何?而結果可由變數”KeyNum”的內容得知,如果KeyNum=1代表只更換新的主控制卡,而其他的使用者資料都要保留,便會結束do….while迴圈的動作 (程式52行)。假如按下的鍵是’2’表示要還原到出廠狀態,也就是說系統中只有一個使用者(主控制卡),那麼便呼叫內建函式 『EEPROM.write(0,1) 』將數目’1’寫入EEPROM內存放已學習使用者的位址上,才離開do….while迴圈。如果使用者並未按下任何鍵,也就是說會發生按鍵輸入逾時的狀況,在此同樣會被視為只更換新的主控制卡。

在離開do….while迴圈後,函式最後的61~64行程式會先發出一提示聲,然後在LCD顯示器第一行顯示設定成功訊息” System Initial OK!”(即圖9【系統初始化完成】畫面),並等待2秒鐘再結束函式,在此函式並沒有傳回特定的值,因為主程式並不會去偵測這個結果。


  1. boolean Initial_System()      

2. {

3.      BeeBee(); // 先發出提示聲

4.      LCD_Clear_Line(0); // 再清除LCD顯示器第一行

5.      lcd.setCursor(0,0); // 設定游標位置為第一行第一格

6.      lcd.print("Master Card Setting."); // 顯示提示訊息” Master Card Setting.”

7.      LCD_Clear_Line(1); // 接著清除LCD顯示器第二行

8.      lcd.setCursor(2,1); // 將游標移到為第二行第三格

9.      lcd.print("Please Process..."); //顯示提示訊息” Please Process...”

10.     while(! mfrc522.PICC_IsNewCardPresent()) // 等待卡片靠近RC522

11.       {};

12.     if(Read_RFID_SN()==false) // 測試讀取卡片序號是否成功

13.     { // 如果傳回值為”false”表示失敗,發出提示訊息與聲音

14.        LCD_Clear_Line(1); // 清除LCD顯示器第二行

15.        lcd.setCursor(3,1); // 將游標移到為第二行第四格

16.        lcd.print("Setting Error!"); // 顯示警告訊息” Setting Error!”

17.        BeeBee(); // 同時發出警告聲

18.      }

19.      else

20.      { // 本區段程式(20~37行)為處理卡片序號讀取成功之用

21.          LCD_Clear_Line(0); // 再清除LCD顯示器第一行

22.          lcd.setCursor(2,0); // 設定游標位置為第一行第三格

23.          lcd.print("Card SN:"); // 顯示提示訊息” Card SN:”

24.          Show_RFID_SN(); // 接著顯示讀取到的卡片序號

25.          LCD_Clear_Line(1); // 再來清除LCD顯示器第二行

26.          lcd.setCursor(4,1); // 游標移至第二行第五格

27.          lcd.print("Setting OK!");     //顯示設定成功訊息”Setting OK!”

28.          for(byte i=4;i<8;i++) // 出廠密碼”1234”寫入

29.            EEPROM_data[i]=i-3; // EEPROM迴圈

30.          if(Save_Check_EEPROM(8,EEPROM_data)==false)

31.          { // 呼叫序號寫入與驗證函式若傳回”false”表示有錯,發出警告

32.            LCD_Clear_Line(1); // 清除LCD顯示器第二行

33.             lcd.setCursor(0,1); // 游標移至第二行第一格

34.             lcd.print("Master SN Write Err!"); // 顯示警告訊息

35.          }

36.         BeeBee(); // 發出提示聲

37. }

38.      while(digitalRead(RFID_SIG)==0) // 等待卡片離開RFID讀寫器模組

39.        {};

40.      LCD_Clear_Line(0); // 清除LCD顯示器第一行

41.      lcd.setCursor(0,0); // 設定游標位置為第一行第一格

42.      lcd.print("1.Keep Card Setting."); //顯示設定成功訊息” 1.Keep Card Setting.”

43.      LCD_Clear_Line(1); // 再來清除LCD顯示器第二行

44. lcd.setCursor(0,1); // 游標移至第二行第一格

45. lcd.print("2.Initial All"); //顯示設定成功訊息” 2.Initial All”

46.   do // 本段程式(47~60行)為do….while迴圈

47.     { // 等待使用者按下選擇鍵或按鍵逾時

48.          if(WaitKeyIn(10)==true) // 測試使用者是否按下任何鍵

49.          {

50.    Serial.println(KeyNum);

51.            if(KeyNum==1) // 測試按下的鍵是否為’1’

52.               break; // 若是代表結束初始化主卡片動作

53.            if(KeyNum==2) // 測試按下的鍵是否為’2’

54.            { 如果是表示還原到出廠狀態,也就是說只有一個使用者

55.               EEPROM.write(0,1); // 將數目’1’寫入EEPROM內

56.                 break; // 代表只有一個使用者並結束

57.             }

58.             BeeBee(); // 發出提示聲

59.         }

60.      } while(1); // do….while迴圈後半段

61.      LCD_Clear_Line(0); // 清除LCD顯示器第一行

62.      lcd.setCursor(1,0); // 設定游標位置為第一行第二格

63.      lcd.print("System Initial OK!"); //顯示設定成功訊息” System Initial OK!”

64.      delay(2000); // 等待2秒鐘再結束函式

65. }


五.6 ArduinoIDE主體程式說明

介紹完所有的副程式之後,接著就是主體程式部份了,這次的製作整個程式如果包括註解行在內共有906行,比起上次的1008行好像又少了一百多行,其實這是個錯覺,因為有許多的程式都被隱藏在【MFRC522.h】這個函式庫裏面,不過以設計者的立場來說當然就是省了不少時間。說來也很巧,扣掉了前面介紹過的筆者自建函式之後也差不多是309行,也就是說主程式(包括變數定義部份)只佔了1/3左右,如果扣掉變數定義部份,初始設定(setup)部份加上主迴圈(loop),大概就只有250行左右了!而主程式所以會這麼短,主要就是筆者盡量把它們寫成函式之故,所以建議讀者們在設計程式時只要有需要及可能,就應該盡量以函式的方式來完成。

以前這個最後的單元筆者都會用”主程式”來稱呼,可是在Arduino IDE中這樣的稱呼似乎有點尷尬,所以筆者就改用了”主體程式”來代表它,希望讀者們能了解!由於副程式函式部份前面已經說明過了接,著按照慣例筆者還是把”主體程式”分成三個部份來說明,即:

  1. 引入(include)與變數(variable)定義部份

  2. 初始化設定(setup)部份

  3. 主體迴圈(loop)部份 

以下是這三個部份的內容:


五.6.1  引入函式庫與變數定義部份

後面【陸、全體程式列表】章節中1~45行是函式庫引入與變數定義的部份,在本次的製作中我們使用到了四種Arduino IDE引入型的內建函式,這些函式都是存放在『libraries』這個資料夾中,在使用前必須用前置指令「#include」把它們引入,一般都是放在程式最前面的地方;

1、1~4行:前四行便是4個函式庫宣告之處,這4個函式庫分別是與LCD顯示器有關的<LiquidCrystal.h>、與Arduini Uno模組板內的EEPROM記憶體讀寫有關的<EEPROM.h>、串並列轉換介面的<SPI.h>和RFID讀寫器模組RC522有關的 <MFRC522.h>等。

2、5~20行:在<MFRC522.h>這個函式庫的本文中有將Arduino家族中不同型號的模組板(主要是針對Uno和Mega2560)其SPI介面所使用的硬體接腳的位置、號碼及RC522上對應的Pin腳名稱詳細列出對照表此外也對PCD (Proximity Coupling Device)與 PICC (Proximity Integrated Circuit Card)這兩個名詞所代表的意義作了說明為了方面讀者們參考筆者特地把它們附列在這裏。

3、23~25行:這裏是指定RC522所使用的SPI介面的SS與RESET兩根接腳的號碼然後再依此宣告”mfrc522(SS_PIN, RST_PIN)”這個物件變數

4、26~29行:定義本次製作系統中一些硬體接腳的名稱,與在Arduino UNO板上使用到的接腳號碼,分別是蜂鳴器輸出腳(Buzzer=15)、系統出廠初始化輸入腳(InitialPin=17)、電鎖輸出控制腳(LockOut=16)及LCD背光電源控制腳(LCD_LT=8)。

5、30行:在此是定義一些陣列變數,這些大部份是在我們自建函式中會使用到,其中”Receive_Data[4] “是專用來接收RFID讀寫器模組回傳的資料,而”EEPROM_data[8]”則是存放與Arduino Uno模組板內的EEPROM記憶讀寫有關的資料之用,至於”RFID_SN[4]”是存放由RFID讀寫器模組讀回的卡片序號,最後”RFID_PW[4]”則是儲存使用者輸入的密碼值。

6、32~34行:這裏定義的是一些全域的變數,其中”KeyIn”是做為鍵盤類比分壓輸入的轉換值,”CardCount”是目前已學習儲存卡片的數目,而” CardNumber”則是代表使用者輸入的卡片號碼,”KeyNum”是4X4鍵盤中被按下的按鍵標示值,”PW_Error”為使用者密碼輸入錯誤次數的計數器,最後的”Wait_Time”則是用來設定系統在等待使用者操作時的最長等待時間值。

7、35~39行:本段的變數都與4X4鍵盤有關,主要的是一些陣列變數,例如“keyValue[]={1000,800,670,500,440,390,325,305,280,245,230,215,190,160,135,110}”是4X4鍵盤16個按鍵的電阻分壓的比對下限值,至於”keyNumber[4][4]={{1,2,3,10},{4,5,6,11},{7,8,9,12},{14,0,15,13}}則是如圖一所示的4X4鍵盤16個按鍵實際對應的數字,而最後的”keyChar[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}是前項按鍵所對應數字的ASCII碼。

6、41~45行:此區主要是定義和LCD顯示器有關的硬體接腳與相關規格,首先是宣告一個”LiquidCrystal”的物件變數,其名稱為”lcd(8,9,4,5,6,7)”,而括弧中的數字分別是對應到Arduino Uno模組板的輸數位接腳號碼,對 LCD顯示器來說依序對應到RS,、EN、 與D4 D5 D6 D7功能腳至於後兩項變數則是決定LCD顯示器列(numRows=2,即Y的值)與行(numCols=20,即X的數值)的數目,在此表示我們使用的LCD顯示器的規格是20*2,如果使用其他款類型的LCD顯示器,例如每行可顯示的字數為16,則X參數必須改成numCols=16。


五.6.2  初始化設定(setup)部份

程式列表中47~99行是系統初始設定的部份,在此區段中包含了一些程式偵錯用的電腦串列埠相關程式(即由Serial.開頭的指令),在實際應用時並不會對我們的系統有任何影響,讀者們可忽略不計。接下來就讓我們看看這段程式的動作內容

1、49~53行:這段程式主要是設定前面提過的一些硬體接腳工作在輸出的模式,並預先給定它們的起始輸出值,這些接腳分別控制蜂鳴器、電鎖與LCD顯示器的背光電源等,最後並初始化(即.begin)lcd這個LCD顯示器物件裝置。

2、54~85行:這段程式其實就是在實作圖2的【開機系統自我測試】畫面,首先LCD顯示器的第一行會顯示"Power Test.."的提示訊息,接著依序將LCD顯示器上下兩行填上兩次的0~9的ASCII字碼,以確認每一個位置都能正常顯示,且每一行的字數為20個。

3、86~92行:在此程式會先初始化串列監控視窗,並設定其傳輸速率為9600bps,接著向電腦端的Arduino IDE串列埠監控視窗發送"Start..."的提示訊息,在暫停一秒後再次發出一提示音,告訴使用者即將進行下一個步驟。

4、93~96行:這一整段的程式只是在在顯示圖3【開機測試完成顯示RFID模組型號與序號】畫面的上半部訊息而已。

5、97~98行:程式至此幾乎已經完成了整個開機初始化設定的動作,最後再測試使用者是否起動出廠初始化?也就是測試出廠初始化選擇腳InitialPin(即Arduino UNO板的數位D11腳)是否短接到地?如果是則呼叫『Initial_System()』這個函式執行系統出廠初始化的功能,將系統設定成只有一個使用者,也就是系統管理者的狀態。


五.6.3  主體迴圈(loop)部份

101行開始便是本製作的主程式迴圈(即無窮loop)部份,主程式的動作內容與功能如下:

1、103~116行:主程式一開始就先設定一標記點「Start:」以作為後面程式返回之用,在發出一提示音後,分別在LCD顯示器上顯示"RFID Security Sys."(第二行)與"MUST-2014A"(第二行)的提示訊息,並關閉顯示器的背光電源(即輸出高態至LCD_LT這隻接接腳),上述LCD顯示器的內容即為圖10的【系統開機完成後正常門禁管制功能待機】畫面。接著就無限期的等待RFID卡片出現,在此程式是以無窮的while迴圈方式為之,而此while結束的條件是當測試到『mfrc522.PICC_IsNewCardPresent()』這個卡片偵測函式的傳回為”true”為止,也就是說有新的卡片出口在RC522上,如果沒有任何合法的RFID卡片靠近的話,卡片偵測函式的傳回值將一直都為”false”。

2、117~129行:當RFID卡片出現時程式先點亮LCD的背光電源,好讓使用者易於觀察操作,接著呼叫『Read_RFID_SN()』這個卡片序號讀取函式,並測試讀取動作是否成功?若傳回值為”true”代表成功,則程式繼續往下執行,若失敗則跳到294行執行卡片序號讀取失敗處理動作。當讀取RFID卡片序號成功時系統會先發出一提示音,並呼叫『Check_Card_Exist()』函式檢查卡片是否已經學習儲存在系統中?假如卡片不在系統已儲存的名單之中,則程式只會在LCD顯示器第二行最後面顯示”..”的提示訊息而已,並不作任何的反應。

3、131~155行:如果讀取到的RFID卡片序號已經是儲存在系統中,那麼便先在LCD顯示器第一行顯示提示訊息"Card Number:",接著將該卡片的號碼顯示在後,然後設定密碼可輸入次數為3(即139行”PW_Error=3”),並以do{… }while(140~283行)測試迴圈開始密碼的輸入,而迴圈的結束條件為密碼輸入次數計數用變數”PW_Error=0”;在迴圈中我們先呼叫密碼輸入函式『Password_In()』讓使用者輸入卡片的密碼,在此共有3次密碼輸入機會,每錯誤一次計數變數”PW_Error”便會減一,如果到達3次,則會跳到後面284行的密碼輸入錯誤處理程式區。當使用者輸入完密碼時,LCD顯示器上便可看見圖12的【RFID卡片密碼輸入】畫面。當『Password_In()』函式傳回”true”代表密碼輸入完成,這時程式會先測試使用者是否按下了代表取消的按鍵’C’?如果是,則跳回到103行的「Start」起點標記處重新開始。本段其餘的程式在比對使用者輸入的密碼(存放在RFID_PW[]陣列變數內)和系統之前儲存(存放在EEPROM_data[]內)的四位數密碼是否相同?若完全一樣,會令”equal”這個旗標變數結果為”true”,反之若有任一位數不等則令旗標值為”false”;至於EEPROM_data[]的內容則是在前面呼叫『Check_Card_Exist()』函式時便已經從Arduino UNO板上的EEPROM記憶體中讀出。 

4、156~168行:156行程式會依”equal”這個旗標變數的結果測試密碼比對是否成功?如果密碼比對成功,還會再測試目前的RFID卡片是否為一般的使用者?只要該張卡片的用戶編號不是1就代表是一般的使用者,那麼程式便會顯示圖15的【密碼輸入成功之閘門開啟】畫面,接著起動電鎖輸出,然後發出一提示音,並以”break”指令結束140行開始的do {} while迴圈,也就是結束了本次讀卡動作,接著程式便再度回到103行的「Start」起點標記處重新開始。 

5、169~192行:如果該張卡片的用戶編號是1代表是系統的管理者,在此我們提供了兩種選擇,一是一般的開啟門禁功能,二是進行系統使用者的管理;因此174~179行的程式會先實作出圖16的【密碼輸入成功且為主控制卡功能】畫面,也就是在LCD顯示器第一行顯示"1.Open Gate"的訊息,及第二行"2.Card Management"的提示訊息讓使用者選擇。接著呼叫按鍵輸入函式『WaitKeyIn(10)』等待使用者輸入(180行),若按鍵輸入逾時(即函式傳回值為”false”)或按下’C’鍵(變數KeyNum==12)則跳回到103行的「Start」起點標記處重新開始,如果是正常的按鍵輸入則變數”KeyNum”會儲存按鍵轉換值的。接下來182行至192行程式在測試使用者是否按下了按鍵【1】?如果是代表選擇項目"1.Open Gate",也就是開啟電鎖,這時LCD顯示器會顯示圖17【主控制卡執行閘門開啟】畫面,並在啟動電鎖之後,回到103行的起點標記「Start」處重新開始。

6、193~205行:198行程式在測試是否按下按鍵【2】?如果是則執行選項"2.Card Management"卡片管理功能,這部份的程式區由此至267行,由於內容較長,為了便於解說在此分成幾個部份來說明;程式195行先設定卡片管理功能的起始標記點「Card_Management: 」以供後面程式返回之用,接著實作出圖18的【主控制卡卡片管理功能選擇】畫面,也就是在LCD顯示器的一、二行分別顯示"1.Add  2.Del. Card"及"3.Chg. PW 4.Init All"共四個項目供使用者選擇;然後一樣呼叫按鍵輸入函式『WaitKeyIn(10)』等待使用者輸入(203行),若按鍵輸入逾時(即時間超過10秒)或按下’C’鍵(變數KeyNum==12)則跳回到173行的「Master_Card」主控制卡功能程式起點標記處重新開始。

7、206~220行:本段程式會先測試是否按下按鍵【1】(即變數KeyNum=1)?如果是則執行選項”1.Add”即新增卡片功能,執行的方法是呼叫『AddCard()』這個新增卡片函式去處理整個新增卡片的動作,當函式結束後,如果傳回值為”false”則系統會顯示圖25的【新增卡片動作結束】畫面,也就是在LCD顯示器的一、二行分別顯示"1.Add Card "和"function end..."的提示訊息,並等待兩秒或按下任意鍵,再跳回到173行的「Master_Card」主控制卡功能程式起點標記處重新來過。

8、221~235行:這段程式和上一段很類似只是改成測試是否按下按鍵【2】(變數KeyNum=2)?如果是則執行選項” 2.Del. Card”也就是說刪除卡片功能,實際的方法是呼叫『DelCard()』這個刪除卡片函式去處理;當函式結束後,如果傳回值為”false”則系統會顯示圖28的【刪除卡片動作結束】畫面,即在LCD顯示器的一、二行分別顯示"2.Delete Card "和"function end..."的提示訊息,等待兩秒或按鍵按下任意鍵後,再跳回到195行的「Card_Management」卡片管理功能程式起點標記處重新來過。

9、236~250行:本段程式會測試是否按下按鍵【3】(即變數KeyNum=3)?如果是則執行選項” 3.Chg. PW”即更改卡片密碼功能,在此是以呼叫『ChangePW()』這個更改密碼函式去處理整個卡片密碼的更改動作,當函式結束後,如果傳回值為”false”則系統會顯示圖34之【更改卡片密碼功能結束】畫面,也就是在LCD顯示器的一、二行分別顯示"3.Change PW. "與"function end..."的提示訊息,並等待兩秒或按鍵按下任意鍵之後,跳回到195行的「Card_Management」卡片管理功能程式起點標記處重新執行。

10、251~265行:這段程式和前三段很類似,只是改成測試是否按下按鍵【4】(變數KeyNum=4)?如果是則執行選項” 4.Init All”亦即刪除卡片功能,一樣的是透過呼叫『Initial_All()』這個卡片初始化函式來完成,當函式結束後,如果傳回值為”false”則系統會顯示圖42的【系統初始化選擇再次確認】畫面,即在LCD顯示器的一、二行分別顯示"4.Initial All "和"function end..."的提示訊息,等待兩秒或使用者按鍵按下任意鍵,最後跳回到195行的「Card_Management」卡片管理功能程式起點標記處重新開始卡片的管理。如果使用者按下的按鍵值不是1~4,則程式會發出一警告聲後繼續回到171行的do..while(1)無窮迴圈去執行,直到使用者按鍵輸入逾時或按下代表取消動作的’C’鍵為止。

11、274~282行:在前面4、156行測試密碼比對如果失敗,便會跳到此段程式來處理密碼輸入錯誤的後續動作,其內容主要是顯示圖13的【RFID卡片密碼輸入錯誤】畫面,即在LCD顯示器的第二行顯示"Password Error!"的提示訊息,而且將密碼錯誤計數器變數”PW_Error”值減一,假如密碼輸入錯誤到達三次,便會結束由140行開始到283行的do…while(PW_Error==0)迴圈。

12、284~291行:284行程式會測試密碼輸入錯誤是否已到達三次(即PW_Error=0)?若是則顯示圖14的【密碼輸入錯誤達三次停機10分鐘】畫面,也就是在LCD顯示器的第一行顯示" System Hold 10 Min!"的提示訊息,並發出一警告聲後呼叫內鍵延遲函式『delay(600000) 』讓系統停止運作10分鐘。在等待時間到達之後會再度回到103行的起點標記「Start」處重新開始。

13、294~309行:在前面122行卡片序號讀取過程如果失敗的話,便會跳到本區程式來處理,在此LCD顯示器第二行會顯示提示訊息" RFID Read Error!",並發出一警告聲,然後等待卡片離開RFID讀寫器模組感應範圍,最後結束所有的動作重新回到loop主程式的執行迴圈起點標記「Start」處再次重新開始。


以上便是本次製作中loop主程式的動作內容,整體來說約此loop主體程式約有200行左右了,由於筆者已經將許多動作寫成副程式,所以才能把程式縮到這樣短;我們的整個程式如果包括註解在內共有906行,和上次的1008行感覺上好像精簡很多,不過這主要還是歸功於引用了”MFRC522.h”這個函式庫,其實真正的程式可是要長很多!由於Arduino IDE所使用的是類似C的高階語言,而且已經有不少內建的函式指令可用,相形之下程式的效率就不是那麼高了!還好Arduino家族所使用的單晶片程式記憶體的容量都還滿大的,一般的程式碼都還放得下,而且提供了豐富的函式庫可供使用者應用,這對許多有興趣自己動手做但對組合語言又望之怯步的朋友來說實在是一大福音。本次的製作便走筆至此,以後筆者會再用Arduino板設計一些有趣的專題製作來和各位讀者分享,還請大家不吝指教與支持。


沒有留言:

張貼留言

陸、全體程式列表

#include <LiquidCrystal.h> #include <EEPROM.h> #include <SPI.h> #include <MFRC522.h> /* * Signal     Pin            ...