Mifare RFID-RC522模組實驗(三):讀取與寫入資料

伴隨Mifare讀寫器模組附贈的RFID卡(或感應扣),都是Mifare Classic 1KB類型,台灣的停車場使用的感應幣,也是Mifare Classic。這種RFID卡內部有1KB的EEPROM記憶體,為了妥善管理並達到一卡多用的功能,這個記憶體空間被劃分成16個區段(sector),每個區段有4個區塊(block)區段0的區塊0包含卡片的唯一識別碼(UID,也稱為「製造商識別碼」,Manufacturer Code)。

Mifare Classic卡片的記憶體劃分方式

程式設計師可根據不同的應用場合,自行規劃儲存內容,例如:區段1可儲存員工編號、區段2儲存部門編號(可限制員工能夠進出的區域)、區段3存放停車時間…等等,一張卡片兼具多重識別用途。

認識Mifare的控制區塊(Trailer Block)

每個區段的區塊3也叫做控制區塊(Sector Trailer, Trailer Block或Security Block),如果把上圖的資料結構想像成16層大樓,控制區塊相當於每一層樓的密碼鎖;進、出該層樓必須先輸入正確的密碼,而且每層樓都有兩組密碼。

控制區塊包含金鑰A和金鑰B兩組密碼(各6位元組),以及存取控制位元(4位元組,但僅使用前3位元組)。

Mifare的控制區塊(Trailer Block)

金鑰B預設是可見的,金鑰A則因為安全考量,在掃描時,全部顯示成00。存取控制位元用於控制區段裡的每個區塊(區塊0到區塊3)是否能被存取、寫入或其他操作,並且決定要透過金鑰A或金鑰B來驗證。0xFF0780是控制區塊的「出廠預設值」,代表:

  • 金鑰A不可見;
  • 若通過金鑰A或金鑰B驗證,即可讀取或寫入該區段的區塊0~2。
  • 若通過金鑰A驗證,可讀取或改寫存取該區段的存取控制位元和金鑰B,也能改寫金鑰A。

傾印Mifare卡片資料,觀察EEPROM記憶體資料結構

練習讀取和寫入區塊之前,先用一張空白卡測試,把它的資料全部傾印出來(dump,亦即,把全部資料顯示在序列監控視窗),藉以觀察EEPROM記憶體的資料結構。

執行Arduino主功能表的「檔案→範例→MFRC522→DumpInfo」,開啟DumpInfo(傾印卡片資料)範例程式,將它上傳到Arduino板之後,請打開序列埠監控視窗。底下是感應一張卡片的結果,在序列埠監控視窗顯示卡片的所有區段數值之前,請勿移開或移動卡片。

傾印Mifare卡片資料

若在讀取資料的過程,把卡片移開感應區,將會出現“Timeout in communication”(通訊超時)錯誤訊息,請重新感應。

從卡片資料輸出結果可知:

  • 空白卡片的資料區,除了紀錄識別碼的區塊0之外,預設值都是00。
  • 每個區段最後一個區塊,都是控制區塊(Trailer Block,如:屬於區段15的區塊63)。
  • 控制區塊的前6位元組是金鑰A,預設全是0xFF,但顯示成00。
  • 最右邊的AccessBits(直譯為「存取位元」)代表各個區塊的讀寫權限設定,這些參數經過計算之後,存放在控制區塊的第6, 7, 8位元組。如欲進一步了解「存取位元」設置及其計算方式,可參閱NXP半導體的MIFARE Classic技術文件的Access conditions單元(PDF格式),或者MIFARE Classic 1K Access Bits Calculator(存取位元計算器)

讀取與寫入資料到Mifare卡片

底下列舉本單元使用到的MFRC522程式物件的方法和屬性:

  • MFRC522物件.PCD_Authenticate():驗證金鑰,相當於比對輸入密碼和卡片裡的密碼,唯通過驗證才能存取區段資料。
  • MFRC522物件.GetStatusCodeName():取得狀態碼的名稱
  • MFRC522物件.MIFARE_Read():讀取指定區塊的內容
  • MFRC522物件.MIFARE_Write():在指定區塊寫入資料
  • MFRC522物件.PICC_DumpMifareClassicSectorToSerial():在序列埠監控視窗顯示指定的區段內容

設定金鑰

讀取卡片內部資料的流程,請參閱「Mifare RFID-RC522模組實驗(一):讀取Mifare RFID卡的UID識別碼」貼文,讀、寫資料之前,都需要通過金鑰驗證。設定儲存金鑰的變數的語法如下:

設定儲存金鑰的變數

金鑰值儲存在key物件裡的keyByte陣列屬性,空白卡的金鑰A和B的出廠預設值都是6組 0xFF

金鑰值儲存在key物件裡的keyByte陣列屬性

驗證金鑰

採用「金鑰A」驗證金鑰的PCD_Authenticate()方法的語法如下,其中的&key參數將指向上一節設定的金鑰值;金鑰值和卡片的唯一識別碼,都必須透過位址引用(亦即,在參數前面加上&符號),而非直接傳入數值。trailerBlock參數存放控制區塊的編號;這個方法將傳回一個包含驗證結果(通過與否)的狀態碼(status code)

驗證金鑰的PCD_Authenticate()方法

控制區塊以及資料區塊的編號值介於0~63。以讀、寫區段1的區塊1為例,此區段的控制區塊編號為7;區塊1的編號為5。因此,驗證區段1時,trailerBlock參數要傳入7

控制區塊以及資料區塊的編號值

程式可透過上圖底下的兩則運算式求得資料區塊和控制區塊的編號值。

儲存狀態碼的status變數,請透過底下的語法宣告。

儲存狀態碼的status變數

底下的條件判斷式將在驗證未通過時,於序列埠監控視窗輸出代表「驗證失敗」的訊息:

序列埠監控視窗輸出代表「驗證失敗」的訊息

讀取區塊的MIFARE_Read()方法

讀取區塊資料的核心敘述如下,請先設定一個儲存讀取值的陣列變數,雖然區塊資料的長度是16位元組,但是MFRC522程式庫規定至少需要準備18位元組大小的陣列來存放讀取值;讀取和寫入區塊資料的方法也會傳回狀態碼。

讀取區塊的MIFARE_read()方法

底下是負責讀取區塊資料的自訂函式readBlock(),它接受三個參數:區段編號區塊編號資料陣列

上面的程式可顯示單一區塊的資料,若要傾印Mifare Classic卡片的特定區段(亦即,4個區塊),可透過MFRC522程式庫內建的PICC_DumpMifareClassicSectorToSerial()。執行此方法時,需要傳入卡片識別碼、驗證碼和區段編號,例如,底下的敘述將在序列埠監控視窗輸出整個區段15的資料:

寫入資料的MIFARE_Write()方法

寫入區塊資料的核心敘述如下,請先宣告一個16位元組的陣列變數(此處命名為blockData),在其中存入即將寫入卡片的資料:

寫入資料的MIFARE_Write()方法

假設要把資料存入區段1的區塊1,區塊編號值請設定成5。

寫入與讀取Mifare卡片資料的程式碼

本實驗的電路和零件,與「Mifare RFID-RC522模組實驗(一):讀取Mifare RFID卡的UID識別碼」貼文相同。底下的程式碼將在掃描到Mifare Classic卡片時,在區段15的區塊1,寫入“Keep Hacking!”字串:

第128行程式使用Serial.write()而非Serial.print()方法,是因為print()會把字元的ASCII數字值轉換成文字顯示,而write()則直接把字元顯示出來(請參閱這個回應)。

上傳程式之後,請開啟序列埠監控視窗,並掃描Mifare卡片,將能見到下圖的結果:

顯示寫入與讀取卡片資料

延伸閱讀

46 thoughts on “Mifare RFID-RC522模組實驗(三):讀取與寫入資料

  1. 您好,可以請問一下,如果Key A被我改成不是預設值的0xFF,要怎麼變回預設值。或是在哪裡可以輸入驗證的密碼。

    謝謝

  2. 我把
    for (byte i = 0; i < 6; i++) {
    key.keyByte[i] = 0xFF;
    }
    換成(aaaaa)
    key.keyByte[0] = 0x61;
    key.keyByte[1] = 0x61;
    key.keyByte[2] = 0x61;
    key.keyByte[3] = 0x61;
    key.keyByte[4] = 0x61;
    key.keyByte[5] = 0xFF;
    仍然會出現PCD_Authenticate() failed: Timeout in communication.

  3. 你好,我想要請教一下
    我現在程式打完有一塊是
    把第5腳設置為輸出,第六腳為輸入
    請問為何我第六腳要直接讀取第五腳的準位(1or0)
    不管怎樣設定(強制為0),他只會讀到1
    是硬體本身不能嗎?

  4. 您好,我想請問 我照用了一次code 發現可以正常work,第一次有正常讀寫
    但卻無法再一次寫入或讀取,請問我是哪裡錯了嗎??

    1. 您好
      已自己找到解答
      只要修正最後的
      // Halt PICC
      mfrc522.PICC_HaltA();
      // Stop encryption on PCD
      mfrc522.PCD_StopCrypto1();
      即可~ 感謝

    2. 您好:
      我也遇到此問題,然後依照您說的修改後面程式
      // Halt PICC
      mfrc522.PICC_HaltA();
      // Stop encryption on PCD
      mfrc522.PCD_StopCrypto1();
      還是沒有辦法再次寫入或讀取
      請問我是哪裡錯錯嗎??
      (程式碼是依照上面)

    3. 好的,我明天測試看看。

      [4/9更新] 剛剛再度測試上文的程式,第一次寫入”Happy Hacking!”,第二次在同一個區塊寫入”Cool Hacking!”,全都讀寫正常。

      thanks,
      jeffrey

    4. 您好:
      如果我想要一次寫入兩筆資料可以怎麼做呢?
      我找了很多資料都只有一次寫入一筆。
      謝謝您
      祝您有個愉快的一天

    5. 「很多資料都只有一次寫入一筆」 –> 因為寫入多筆,就是重複寫入一筆的步驟。

      假設你要寫入的區段的金鑰A都是預設的6組 0xFF,那麼,你只需要執行兩次writeBlock()自訂函式:

      writeBlock(sector, block, blockData); // 第一組資料
      writeBlock(sector2, block2, blockData2); // 第二組資料

      如果金鑰不同,程式就要個別設定金鑰,例如,在writeBlock()自訂函式的定義當中,加入key參數:

      void writeBlock(byte _sector, byte _block, byte _blockData[], MFRC522::MIFARE_Key key) {
      :
      }

      並且分別設置金鑰:

      MFRC522::MIFARE_Key k1; // 儲存金鑰
      MFRC522::MIFARE_Key k2; // 儲存金鑰

      寫入兩個不同的區段:

      writeBlock(sector, block, blockData, k1);
      writeBlock(sector1, block1, blockData1, k2);

      以此類推。

      thanks,
      jeffrey

  5. 你好,
    我有重新下載過軟體、板子也都有設定好
    上傳後出現 開發板 Arduino/Genuino Uno 編譯錯誤 的問題
    請問該怎麼解決?

    1. 編譯錯誤通常是缺少安裝引用的程式庫,或者程式語法錯誤,實際要看錯誤訊息。

      thanks,
      jeffrey

  6. 老師您好:
    我已執行兩次
    writeBlock(sector, block, blockData); // 第一組資料
    writeBlock(sector2, block2, blockData2); // 第二組資料
    也在前面指定讀寫的區塊和區段還有要存入的字元
    但出現 sketch_apr11a:127: error: ‘blockData2’ was not declared in this scope
    這樣的錯誤
    我應該如何解決?

    想問老師
    關於 Mifare RFID 卡 的教學,是只有出現在”超圖解3″中嗎?
    還是您有其他書籍有更詳細的解說呢?
    謝謝老師

    1. ‘blockData2’ was not declared in this scope
      代表程式沒有定義變數,抱歉,我目前沒有其他RFID的文章,而且你的問題主因也不是RFID。

      thanks,
      jeffrey

  7. 老師你好:
    我想要連續讀取不同張卡片,讀取第一次時正常,但讀取第二次時監控視窗出現空白,請問是什麼原因?
    thanks

  8. 老師你好:
    第一次感測完卡片,我必須要把板子從電腦移除再重新插入才可以做第二次的讀取跟寫入的動作。
    這是正常的嗎?
    Thanks

    1. 嗯,不正常,執行上文的程式碼也不行嗎?讀取卡片之後有沒有執行 mfrc522.PICC_HaltA(); 這個敘述?

      thanks,
      jeffrey

  9. 老師你好:
    用上面的程式碼做執行,依然遇到相同問題。
    在執行第一次時,監控視窗可以正常顯示,在沒有移除板子和重新上傳程式下,直接進行第二次讀寫,監控視窗沒有任何反應!
    重新上傳程式但沒有移除板子執行後,監控視窗只會出現Please scan MIFARE Classic card…。
    只有在移除板子後才又可以正常執行!
    Thanks

    1. 剛剛再次接線、驗證程式碼,在我的板子上測試無誤,可以多次連續感應卡片。

      thanks,
      jeffrey

  10. 老師
    我想問一下,我想寫一套關於RFID密碼鎖的設計
    而流程是如果錯誤前兩次會顯示錯誤第三次會發出警報並且只能透過一張卡片讀寫這個密碼鎖其餘的卡片都會顯示錯誤
    請問該如何寫,謝謝老師

  11. 老師我想問一下,如果卡片讀寫成功不會復歸是正常的嗎?
    如果要讓它復歸原始狀態要怎麼做呢?謝謝老師

  12. 老師我想問你一下
    可以把這組程式改寫成只有讀取和驗證就好而不要寫入卡片請問該如何去修改程式碼?

  13. 老師對不起,我想特地再問一下
    如果RFID跟4X4第三篇文章結合可以防衝突Void loop()的那一個部分嗎?
    比如說卡片感應過驗證後,跳到密碼部分再次感應卡片就不會再跑回感應卡片的程式碼部分。
    因為是專題需要接觸到所以想特地問一下老師如何防衝突這一個區塊
    祝老師有個美好的一天!

    1. 妳的意思是卡片感應過後,讓用戶輸入密碼嗎?
      這兩個流程並不衝突,可以個別處理,
      妳可以用一個變數紀錄卡片是否通過感應,
      如果通過,就顯示輸入密碼的畫面,
      否則顯示WRONG…之類的。

      thanks,
      jeffrey

    2. 老師我還發現到因為RFID也有KEY這個字元存在導致系統會有所衝突請問有什麼可以解決這個問題?
      謝謝老師

    3. 我不懂妳的問題,妳是說,程式已經定義了名叫KEY的變數,另一個變數不能叫KEY,該怎麼辦……嗎?

  14. 老師我今天有在試過兩個放在一起
    現在我想問說,假如我想把void loop()的這段放進去副程式裡面是否有辦法
    if ( ! mfrc522.PICC_IsNewCardPresent()) {
    return; // 退回loop迴圈的開頭
    }
    // 選取一張卡片
    if ( ! mfrc522.PICC_ReadCardSerial()) { // 若傳回1,代表已讀取到卡片的ID
    return;
    }
    readBlock(sector, block, buffer);

    1. 就比如我這段自行宣告的副程式是否可以,還是說感應卡片的一定要放在VOID loop迴圈裏頭
      void readcard(){
      if ( ! mfrc522.PICC_IsNewCardPresent()) {
      return; // 退回loop迴圈的開頭
      }
      // 選取一張卡片
      if ( ! mfrc522.PICC_ReadCardSerial()) { // 若傳回1,代表已讀取到卡片的ID
      return;
      }
      readBlock(sector, block, buffer); // 區段編號、區塊編號、存放讀取資料的陣列
      Serial.println();
      mfrc522.PICC_HaltA();
      }

  15. 老師目前上面問題排除了,可是我想請教老師一件事
    假如我密碼鎖顯示解鎖之後要怎麼重新跑回先掃描卡片的地方
    老師我可以給你看我的程式嗎?
    謝謝老師
    have a wonderful day

  16. 老師上述的問題我都解決了,可是我想再請教老師一個問題
    就是我密碼鎖的程式跑過一次之後第二次會自動跳過掃描卡片的那段文字
    可是我有指令最後一行如果輸入正確會跑回void setup();
    我試過很多辦法去排除跳過該段程式碼的這個問題可是最後都還是失敗
    我想請教老師請問有甚麼程式可以阻擋跳過程式碼的辦法
    最後謝謝老師!!

  17. MFRC522::STATUS_OK老師我被專題老師考倒這一行了
    我想請問雙冒號代表什麼
    而STATUS_OK是甚麼意思
    謝謝老師

  18. 老師我還想問一個問題
    今天老師也跟我說動作要掃描卡片後,如果10秒內沒輸入密碼要跳回初始介面
    那我想請問老師要如何達成10秒內跳出該段副程式?用中斷的方式嗎

  19. 老師我最後想再問最後一個問題了。就是如果想要讓薄膜鍵盤按下去時可以發出聲音請問可以怎麼做?

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *