1. 環境構築: Arduino IDE入門ガイドを参考にして、IDE のインストールと対応するボードパッケージおよび必要なライブラリのインストールを完了してください。
2. 必須ライブラリ:
3. 必須ハードウェア:


最適な読み書き性能を得るため、テストの際は、下の画像に示すユニットの NFC 検出面に NFC タグカードを置いてください。
// NFCプロトコルレイヤーインスタンス
m5::nfc::NFCLayerA nfc_a{unit}; // NFC-Aプロトコルレイヤー(リーダーモード)
m5::nfc::EmulationLayerA emu_a{unit}; // NFC-Aエミュレーションレイヤー(タグエミュレーションモード)
// カードオブジェクト
PICC picc{}; // 検出されたカードconstexpr Key keyA = DEFAULT_KEY;
constexpr Key keyB = DEFAULT_KEY;
// DEFAULT_KEY is 0xFFFFFFFFFFFF (デフォルト値)典型的なNFCリーダー操作フローには、以下の手順が含まれます:
M5.begin() および Wire.begin()nfc_a.detect() または nfc_a.detect(piccs) を使用してカードを検索nfc_a.identify() を使用してカード類型とメモリレイアウトを決定nfc_a.reactivate() を使用して完全な通信パラメータを取得mifareClassicAuthenticateA/B() で認証nfc_a.deactivate() を使用してカードを解放| メソッド | 戻り値 | 説明 |
|---|---|---|
picc.isMifareClassic() | bool | Classic1K/4K か確認 |
picc.isMifareUltralight() | bool | Ultralight シリーズか確認 |
picc.isMifareDESFire() | bool | DESFire シリーズか確認 |
picc.isUserBlock(block) | bool | ブロックがユーザーアクセス可能か確認 |
picc.uidAsString() | string | UIDを16進数文字列として取得 |
picc.typeAsString() | string | カード型名を取得 |
picc.userAreaSize() | uint16_t | ユーザー領域サイズを取得 |
picc.totalSize() | uint16_t | カード容量を取得 |
タグエミュレーション(Tag Emulation)により、デバイスはNFCカード としに機能し、他のNFCリーダーにより検出・通信が可能になります。これはさまざまなNFCカード型(MIFARE Ultralight、NTAG など)をエミュレーションする必要があるアプリケーションで非常に一般的です。
タグエミュレーション の主要な手順:
emu_a.begin() を呼び出してエミュレーションを開始emu_a.update() を呼び出してリーダークエリを処理constexpr Type type{Type::MIFARE_Ultralight}; // エミュレート するタグ型を選択(例:MIFARE_Ultralight または NTAG_213)
constexpr uint8_t uid[] = {0x04, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE}; // 7バイトUID
uint8_t picc_memory[64]{}; // エミュレートタグメモリバッファ(サイズはカード型に依存)エミュレーション操作
| メソッド | 関数 |
|---|---|
picc.emulate(type, uid, uid_len) | エミュレートするカード型とUID を構成 |
emu_a.begin(picc, memory, mem_size) | カード情報とメモリでエミュレーション開始 |
emu_a.emulatePICC() | 現在エミュレートしている PICC オブジェクトを取得 |
emu_a.update() | エミュレーション状態を更新(メインループで呼び出し) |
emu_a.state() | 現在のエミュレーション状態を取得 |
状態値
エミュレータには以下の状態があります:
None(なし)、Off(オフ)、Idle(アイドル)、Ready(準備完了)、Active(アクティブ)、Halt(停止)ヘルパー関数
| 関数 | 関数 |
|---|---|
embed_uid(memory, uid) | 7バイトUID をUltralight/NTAG メモリレイアウトに埋め込む |
bcc8(data, len, init) | BCC(ブロックチェック文字)を計算してUID検証 |
このサンプルは、NFCカードを迅速にスキャン・認識する方法を示します。プログラムはリーダー範囲内のカードを継続的に検出し、検出されたカードごとに2段階の識別プロセスを実行します:まず detect() を使用して予備分類を行い、次に identify() を使用して正確な識別を行います。識別成功後、カードのUID、型、ATQA、SAK情報が出力されます。これはNFCアプリケーション実装の基本的なステップです。
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <Wire.h>
#include <vector>
using namespace m5::nfc::a; // NFCプロトコルレイヤーの使用(ISO 14443-3A)
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units; // ユニット統一マネージャーインスタンス
m5::unit::UnitNFC unit{}; // NFC ユニットインスタンス(I2C インターフェース)
m5::nfc::NFCLayerA nfc_a{unit}; // NFC-Aプロトコルレイヤーインスタンス (ISO 14443-3A カード用)
} // namespace
void setup()
{
M5.begin();
// スクリーンをランドスケープモードに設定
if (lcd.height() > lcd.width()) {
lcd.setRotation(1);
}
auto board = M5.getBoard();
bool unit_ready{};// ユニット初期化ステータスフラグ
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.end(); // 既存のI2C接続を閉じる
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
// NFCユニットをマネージャーに追加して初期化
unit_ready = Units.add(unit, Wire) && Units.begin();
if (!unit_ready) {
// 初期化失敗:画面を赤にして無限ループに入回
M5_LOGE("Failed to begin");
lcd.fillScreen(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
M5_LOGI("M5UnitUnified initialized");
M5_LOGI("%s", Units.debugInfo().c_str());
lcd.setFont(&fonts::FreeMono9pt7b);
lcd.fillScreen(0);
}
void loop()
{
M5.update();
Units.update();// すべての登録済みユニットを更新
// PICC(カード)リストを作成して、近くのカードを検出
std::vector<PICC> piccs;
if (nfc_a.detect(piccs)) { // カードが検出されました
lcd.fillScreen(0);
lcd.setCursor(0, 0);
uint16_t idx{}; // 正常に識別されたカード数
for (auto&& u : piccs) {
// detect は SAK に基づく予備分類のみを実行するため、さらに識別が必要です
if (nfc_a.identify(u)) {// カードの正確な識別を実行
// カード情報を出力:UID、型、ATQA、SAK、ユーザー領域サイズ、容量
M5.Log.printf("PICC:%s %s %04X/%02X %u/%u\n", u.uidAsString().c_str(), u.typeAsString().c_str(), u.atqa,
u.sak, u.userAreaSize(), u.totalSize());
lcd.printf("[%u]:PICC:\n<%s>\n%s\n", idx, u.uidAsString().c_str(), u.typeAsString().c_str());
++idx;
} else {
M5_LOGW("Failed to identify %s %s %04X/%02X %u/%u", u.uidAsString().c_str(), u.typeAsString().c_str(),
u.atqa, u.sak, u.userAreaSize(), u.totalSize());
}
}
if (idx) {
lcd.printf("==> %u PICC\n", idx);
M5.Log.printf("==> %u PICC\n", idx);
}
nfc_a.deactivate();// すべてのカードとの通信を非アクティブ化
}
}上記のコードをメインコントローラーに upload した後、シリアルモニターを開き、Unit NFCセンシングサーフェスの近くに1枚以上のタグカードを配置すると、認識結果が表示されます。
シリアルモニター 出力例:
PICC:3E86E2D5 MIFARE Classsic1K 0004/08 752/1024
==> 1 PICC
PICC:047D9D82752291 MIFARE Ultralight EV1 11 0044/00 48/80
PICC:04327CD2B97880 MIFARE Plus 2K X/EV SL0 0044/20 1520/2048
==> 2 PICC このプロセスでは BtnA をクリックしてカードをリーダーに近づけます。プログラムがカードを検出した後、自動的に読み取ってスクリーンとシリアルポートにデータを出力します。読み取りプロセス中に、プログラムはカードの完全な識別とアクティベーションを実行します。
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <Wire.h>
#include <vector>
using namespace m5::nfc::a; // NFC-Aプロトコルレイヤー
using namespace m5::nfc::a::mifare; // MIFARE カード共通操作
using namespace m5::nfc::a::mifare::classic; // MIFARE Classic カード固有操作
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units; // ユニット統一マネージャーインスタンス
m5::unit::UnitNFC unit{}; // NFC ユニットインスタンス(I2C インターフェース)
m5::nfc::NFCLayerA nfc_a{unit}; // NFC-Aプロトコルレイヤーインスタンス (ISO 14443-3A カード用)
// すべてのブロックを認証できるKeyA
// 異なるキー値に変更してください
constexpr Key keyA = DEFAULT_KEY; // デフォルト値:0xFFFFFFFFFFFF
} // namespace
void setup()
{
M5.begin();
// スクリーンをランドスケープモードに設定
if (lcd.height() > lcd.width()) {
lcd.setRotation(1);
}
auto board = M5.getBoard();
bool unit_ready{};// ユニット初期化ステータスフラグ
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.end();// 既存のI2C接続を閉じる
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
// NFCユニット をマネージャーに追加して初期化
unit_ready = Units.add(unit, Wire) && Units.begin();
if (!unit_ready) {
M5_LOGE("Failed to begin");
lcd.fillScreen(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
M5_LOGI("M5UnitUnified initialized");
M5_LOGI("%s", Units.debugInfo().c_str());
lcd.setFont(&fonts::FreeMono9pt7b);
lcd.fillScreen(0);
lcd.setCursor(0, 0);
lcd.printf("Please put the PICC\nand click\nBtnA");
M5.Log.printf("Please put the PICC and click BtnA\n");
}
void loop()
{
M5.update();
Units.update();// すべての登録済みユニットを更新
if (M5.BtnA.wasClicked()) {
lcd.fillScreen(0);
lcd.setCursor(0, 0);
PICC picc{}; // カードオブジェクトを作成
if (nfc_a.detect(picc)) { // 単一カードを検出
// カード型を識別してリアクティベーション(完全な通信パラメータを取得)
if (nfc_a.identify(picc) && nfc_a.reactivate(picc)) {
lcd.printf("%s\n%s", picc.uidAsString().c_str(), picc.typeAsString().c_str());
// 詳細情報を出力:UID、型、ユーザー領域サイズ、容量
M5.Log.printf("==== Dump %s %s %u/%u ====\n", picc.uidAsString().c_str(), picc.typeAsString().c_str(),
picc.userAreaSize(), picc.totalSize());
// すべてのカードデータをダンプ(MIFARE Classicに必要なキー、その他の型ではキーパラメータを無視)
nfc_a.dump(keyA); // MIFARE classicの場合はキーが必要、MIFARE classic でない場合はキーを無視
nfc_a.deactivate();
} else {
lcd.printf("Failed to identify");
M5_LOGE("Failed to identify/activate %s", picc.uidAsString().c_str());
}
} else {
lcd.printf("PICC NOT exists");
M5.Log.printf("PICC NOT exists\n");
}
}
}
シリアルモニター 出力例:
==== Dump 3E86E2D5 MIFARE Classsic1K 752/1024 ====
Sec[Blk]:00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F [Access]
-----------------------------------------------------------------
00)[000]:3E 86 E2 D5 8F 08 04 00 62 63 64 65 66 67 68 69 [0 0 0]
[001]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[002]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[003]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1]
01)[004]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[005]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[006]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[007]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1]
02)[008]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[009]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[010]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[011]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1] このサンプルはNFC タグエミュレーション機能を示します。他のNFCリーダー(スマートフォンなど)がタグに近づくと、タグを認識して読み取ることができます。プログラムは MIFARE Ultralight と NTAG 213の2つの一般的なタグカード型のエミュレーションをサポートしており、対応するUID とメモリデータ(NDEFメッセージを含む)を持つように各型が構成されています。
重要なポイント:
update() を継続的に呼び出して状態を更新#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <Wire.h>
#include <vector>
using namespace m5::nfc; // NFC共通名前空間
using namespace m5::nfc::a; // NFC-Aプロトコルレイヤー
using namespace m5::nfc::a::mifare; // MIFAREカード共通操作
using namespace m5::nfc::a::mifare::classic; // MIFARE Classic カード固有操作
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units; // ユニット統一マネージャーインスタンス
m5::unit::UnitNFC unit{}; // NFC ユニットインスタンス(I2C インターフェース)
m5::nfc::EmulationLayerA emu_a{unit}; // NFC-Aエミュレーションレイヤーインスタンスを作成してデバイスをNFCタグとしてエミュレート
PICC picc{}; // エミュレートするカードオブジェクト
// ===== エミュレートするタグ型を選択 =====
#define EMU_MIFARE_ULTRALIGHT // MIFARE Ultralight タグ
// #define EMU_NTAG213 // NTAG213 タグ
// ===== MIFARE Ultralight エミュレーションデータ =====
#if defined(EMU_MIFARE_ULTRALIGHT)
constexpr Type type{Type::MIFARE_Ultralight};
constexpr uint8_t uid[] = {0x04, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE};// 7バイトUID (Ultralight/NAG シリーズは7バイトUID使用)
// エミュレートされたタグメモリデータ(NDEFメッセージを含む:URL https://m5stack.com/ およびテキスト "Hello M5Stack")
uint8_t picc_memory[] = {
0x00, 0x00, 0x00, 0x00, // ページ 0: UIDバイト(embed_uid により埋められます)
0x00, 0x00, 0x00, 0x00, // ページ 1: UIDバイト(続き)
0x00, 0xA3, 0x00, 0x00, // ページ 2: 内部データ、ロックビット
0xE1, 0x10, 0x06, 0x00, // ページ 3: CC(能力容器)- NDEF表示形式識別子
0x03, 0x25, 0x91, 0x01, // ページ 4: NDEF TLV開始
0x0D, 0x55, 0x04, 0x6D, // ページ 5: URIレコード(https://)
0x35, 0x73, 0x74, 0x61, // ページ 6: "5sta"
0x63, 0x6B, 0x2E, 0x63, // ページ 7: "ck.c"
0x6F, 0x6D, 0x2F, 0x51, // ページ 8: "om/" + テキストレコード開始
0x01, 0x10, 0x54, 0x02, // ページ 9: テキストレコードヘッダー
0x65, 0x6E, 0x48, 0x65, // ページ 10: "enHe"(言語コード "en" + "He")
0x6C, 0x6C, 0x6F, 0x20, // ページ 11: "llo "
0x4D, 0x35, 0x53, 0x74, // ページ 12: "M5St"
0x61, 0x63, 0x6B, 0xFE, // ページ 13: "ack" + NDEF ターミネータ 0xFE
0x44, 0x45, 0x46, 0x00, // ページ 14: パディングデータ
0x44, 0x45, 0x46, 0x00, // ページ 15: パディングデータ
};
// ===== NTAG213 エミュレーションデータ =====
#elif defined(EMU_NTAG213)
constexpr Type type{Type::NTAG_213};
constexpr uint8_t uid[] = {0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33};// 7バイトUID
// エミュレートされたタグメモリデータ(多言語NDEFメッセージを含む:URL + 中文/英文/日文テキスト)
uint8_t picc_memory[] = {
0x00, 0x00, 0x00, 0x00, // ページ 0: UIDバイト
0x00, 0x00, 0x00, 0x00, // ページ 1: UIDバイト(続き)
0x00, 0x48, 0x00, 0x00, // ページ 2: 内部データ、ロックビット
0xE1, 0x10, 0x12, 0x00, // ページ 3: CC(能力容器)
0x01, 0x03, 0xA0, 0x0C, // ページ 4: NDEF能力データ
0x34, 0x03, 0x58, 0x91, // ページ 5: NDEF TLV + メッセージ開始
0x01, 0x0D, 0x55, 0x04, // ページ 6: URIレコードヘッダー(https://)
0x6D, 0x35, 0x73, 0x74, // ページ 7: "m5st"
0x61, 0x63, 0x6B, 0x2E, // ページ 8: "ack."
0x63, 0x6F, 0x6D, 0x2F, // ページ 9: "com/"
0x11, 0x01, 0x11, 0x54, // ページ 10: 中文テキストレコードヘッダー
0x02, 0x7A, 0x68, 0xE4, // ページ 11: 言語コード "zh" + UTF-8 中文開始
0xBD, 0xA0, 0xE5, 0xA5, // ページ 12: UTF-8 "你好" エンコーディング
0xBD, 0x20, 0x4D, 0x35, // ページ 13: " M5"
0x53, 0x74, 0x61, 0x63, // ページ 14: "Stac"
0x6B, 0x11, 0x01, 0x10, // ページ 15: "k" + 英文テキストレコードヘッダー
0x54, 0x02, 0x65, 0x6E, // ページ 16: 言語コード "en"
0x48, 0x65, 0x6C, 0x6C, // ページ 17: "Hell"
0x6F, 0x20, 0x4D, 0x35, // ページ 18: "o M5"
0x53, 0x74, 0x61, 0x63, // ページ 19: "Stac"
0x6B, 0x51, 0x01, 0x1A, // ページ 20: "k" + 日文テキストレコードヘッダー
0x54, 0x02, 0x6A, 0x61, // ページ 21: 言語コード "ja"
0xE3, 0x81, 0x93, 0xE3, // ページ 22: "こ" UTF-8
0x82, 0x93, 0xE3, 0x81, // ページ 23: "ん" + "に" 開始
0xAB, 0xE3, 0x81, 0xA1, // ページ 24: "に" + "ち"
0xE3, 0x81, 0xAF, 0x20, // ページ 25: "は "
0x4D, 0x35, 0x53, 0x74, // ページ 26: "M5St"
0x61, 0x63, 0x6B, 0xFE, // ページ 27: "ack" + NDEF ターミネータ
0x00, 0x00, 0x00, 0x00, // ページ 28-39: フリーユーザーデータ領域
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0xBD, // ページ 40: NTAG213 設定ページ
0x02, 0x00, 0x00, 0xFF, // ページ 41: 設定ページ(続き)
0x00, 0x00, 0x00, 0x00, // ページ 42: パスワード保護
0x00, 0x00, 0x00, 0x00, // ページ 43: パスワード応答
0x00, 0x00, 0x00, 0x00, // ページ 44: 予約領域
};
#else
#error "Choose the target to emulate"
#endif
/**
* @brief BCC(ブロックチェック文字)を計算する - バイト列のXOR操作
* @param p 入力データへのポインタ
* @param len データ長
* @param init 初期値(デフォルト:0)
* @return BCC チェック値
*/
uint8_t bcc8(const uint8_t* p, const uint8_t len, const uint8_t init = 0)
{
uint8_t v = init;
for (uint_fast8_t i = 0; i < len; ++i) {
v ^= p[i];
}
return v;
}
/**
* @brief 7バイトUIDをUltralight/NAGメモリレイアウトに正しく埋め込む
*
* Ultralight/NAGメモリ内のUID保存形式:
* ページ 0: [UID0, UID1, UID2, BCC0] BCC0 = CT ^ UID0 ^ UID1 ^ UID2
* ページ 1: [UID3, UID4, UID5, UID6]
* ページ 2 プレフィックス: [BCC1] BCC1 = UID3 ^ UID4 ^ UID5 ^ UID6
*
* @param mem ターゲットメモリ バッファ(最低 9 バイト)
* @param uid 7バイトUIDデータ
*/
void embed_uid(uint8_t mem[9], const uint8_t uid[7])
{
memcpy(mem, uid, 3);
mem[3] = bcc8(uid, 3, 0x88 /* CT */);
memcpy(mem + 4, uid + 3, 4);
mem[8] = bcc8(uid + 3, 4);
}
// エミュレーション状態に対応する色テーブル
constexpr uint16_t color_table[] = {
// なし, オフ, アイドル, 準備完了, アクティブ, 停止};
TFT_BLACK, TFT_RED, TFT_BLUE, TFT_YELLOW, TFT_GREEN, TFT_MAGENTA};
// エミュレーション状態の文字識別子
// なし, オフ, アイドル, 準備完了, アクティブ, 停止
constexpr const char* state_table[] = {"-", "O", "I", "R", "A", "H"};
} // namespace
void setup()
{
M5.begin();
Serial.begin(115200);
// スクリーンをランドスケープモードに設定
if (lcd.height() > lcd.width()) {
lcd.setRotation(1);
}
// エミュレーション モード設定
auto cfg = unit.config();
cfg.emulation = true;
cfg.mode = NFC::A;
unit.config(cfg);
auto board = M5.getBoard();
bool unit_ready{};
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.end();// 既存のI2C接続を閉じる
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
// NFCユニットをマネージャーに追加して初期化
unit_ready = Units.add(unit, Wire) && Units.begin();
if (!unit_ready) {
M5_LOGE("Failed to begin");
lcd.fillScreen(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
M5_LOGI("M5UnitUnified initialized");
M5_LOGI("%s", Units.debugInfo().c_str());
lcd.setFont(&fonts::FreeMono9pt7b);
lcd.startWrite();
lcd.fillScreen(TFT_RED);
// エミュレーション初期化
if (picc.emulate(type, uid, sizeof(uid))) {// エミュレートするカード型とUIDを設定
embed_uid(picc_memory, uid);// UIDをエミュレーションメモリに埋め込む
// カードオブジェクトとメモリデータでエミュレーションレイヤーを開始
if (emu_a.begin(picc, picc_memory, sizeof(picc_memory))) {
lcd.fillScreen(TFT_DARKGREEN);
lcd.setTextColor(TFT_WHITE, TFT_DARKGREEN);
lcd.setCursor(0, 16);
// エミュレート された PICC 情報を取得して表示
const auto& e_picc = emu_a.emulatePICC();
Serial.printf("Emulation:%s %s ATQA:%04X SAK:%u\n", e_picc.typeAsString().c_str(),
e_picc.uidAsString().c_str(), e_picc.atqa, e_picc.sak);
lcd.printf("%s\n%s\nATQA:%04X\nSAK:%u ", e_picc.typeAsString().c_str(), e_picc.uidAsString().c_str(),
e_picc.atqa, e_picc.sak);
}
}
lcd.fillRect(0, 0, 32, 16, color_table[0]);
lcd.drawString(state_table[0], 0, 0);
lcd.endWrite();
}
void loop()
{
M5.update();
Units.update(); // すべての登録済みユニットを更新
emu_a.update(); // エミュレーションレイヤー状態を更新(ループで呼び出す必要があります)
// エミュレーション状態変化を監視してスクリーン指標を更新
static EmulationLayerA::State latest{}; // 前の状態を記録
auto state = emu_a.state(); // 現在のエミュレーション状態を取得
if (latest != state) {
latest = state;
lcd.startWrite();
// 状態に基づいて左上のカラーブロックとテキストを更新
lcd.fillRect(0, 0, 32, 16, color_table[m5::stl::to_underlying(state)]);
lcd.drawString(state_table[m5::stl::to_underlying(state)], 0, 0);
Serial.println(state_table[m5::stl::to_underlying(state)]);
lcd.endWrite();
}
}上記のコードをメインコントローラーに upload した後、Unit NFCはNFCタグをエミュレートします。スマートフォンまたは他のNFCリーダーをUnit NFCに近づけると、NFC タグを認識してそれに保存されているNDEF メッセージコンテンツ(URL + テキスト)を読み取ることができます。シリアルモニターはエミュレートされたタグ型、UID、ATQA、SAK情報を出力します。メインコントローラー画面の左上隅は状態指標(アイドル/準備完了/アクティブなど)を表示します。
スマートフォンで読み取られたタグ情報の例:
シリアルモニター 出力例:
Emulation:MIFARE Ultralight 043456789ABCDE ATQA:0044 SAK:0
O
I
R
A
H
R
A
H
R
A
O Emulation:NTAG 213 99887766554433 ATQA:0044 SAK:0
O
I
R
A
H
R
A
H
R
A
O このサンプルはNFCタグの直接読み書き機能を示しており、2つの方法をサポート します:ブロック間連続読み書きと単一ブロック読み書きです。
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <Wire.h>
using namespace m5::nfc; // NFC共通名前空間
using namespace m5::nfc::a; // NFC-Aプロトコルレイヤー
using namespace m5::nfc::a::mifare; // MIFAREカード操作
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;// ユニット統一マネージャーインスタンス
m5::unit::UnitNFC unit{}; // NFC ユニットインスタンス(I2C インターフェース)
m5::nfc::NFCLayerA nfc_a{unit};// NFC-Aプロトコルレイヤーインスタンス
// Classic デフォルトKeyA (0xFFFFFFFFFFFF)
// カードが異なるキー値を使用する場合は、ここで変更してください
constexpr classic::Key keyA = classic::DEFAULT_KEY;
// テストメッセージ文字列(カード容量に基づいて選択)
constexpr char long_msg[] = "This is a sample message buffer used for testing NFC page writes and data integrity verification purposes.";// 大容量カード用(ユーザー領域 >= 120 バイト)
constexpr char short_msg[] = "0123456789ABCDEFGHIJ";// 小容量カード用(ユーザー領域 < 120 バイト)
/**
* @brief ブロック間連続読み書きテスト(クリックでトリガー)
*
* 指定されたブロックからカードへテストメッセージを書き込み、
* データを読み取って整合性を検証し、その後すべてのゼロを書き込んでクリア します。
* 高レベルread()/write() API を使用し、ブロック/セクター間操作を自動的に処理 します。
*
* フロー: 書き込み -> ダンプ -> 読み取ると検証 -> クリア -> ダンプ
*
* @param sblock 書き込みを開始するブロック番号
* @param msg 書き込むテストメッセージ文字列
* @return すべての操作(書き込み、検証、クリア)が成功した場合は true
*/
bool read_write(const uint8_t sblock, const char* msg)
{
auto len = strlen(msg);
uint8_t buf[(strlen(msg) + 15) / 16 * 16]{}; // 16バイト単位でまるめる(Classic ブロックサイズ)
uint16_t rx_len = sizeof(buf);
// カードへテストメッセージを書き込む
M5.Log.printf("================================ WRITE block:%u len:%zu\n", sblock, sizeof(buf));
if (!nfc_a.write(sblock, (const uint8_t*)msg, len, keyA)) {
M5_LOGE("Failed to write block %u", sblock);
return false;
}
lcd.fillScreen(TFT_ORANGE);
// 書き込み したデータをダンプして視覚的に確認
nfc_a.mifareClassicAuthenticateA(classic::get_sector_trailer_block(sblock), keyA);// ダンプ前にセクターを認証
nfc_a.dump(sblock);
// データを読み取ってデータ整合性を検証
if (!nfc_a.read(buf, rx_len, sblock, keyA)) {
M5_LOGE("Failed to read");
return false;
}
lcd.fillScreen(TFT_BLUE);
bool verify_ok = (memcmp(buf, msg, len) == 0);// 読み取ったデータを元のメッセージと比較
M5.Log.printf("================================ VERIFY:%s\n", verify_ok ? "OK" : "NG");
if (!verify_ok) {
M5_LOGE("VERIFY NG!!");
m5::utility::log::dump(buf, rx_len, false);// デバッグ用に読み取り データをダンプ
}
// すべてのゼロを書き込んでクリア
memset(buf, 0, sizeof(buf));
lcd.fillScreen(TFT_MAGENTA);
if (!nfc_a.write(sblock, buf, sizeof(buf), keyA)) {
M5_LOGE("Failed to clear");
return false;
}
M5.Log.printf("================================ CLEAR\n");
// クリア されたデータをダンプして視覚的に確認
nfc_a.mifareClassicAuthenticateA(classic::get_sector_trailer_block(sblock), keyA);
nfc_a.dump(sblock);
return true;
}
/**
* @brief 単一ブロック読み書きテスト
*
* 低レベルread16()/write16() API を使用して、16バイトの単一ブロックに
* 固定テスト文字列を書き込み、読み取って検証し、その後クリア します。
* read_write() とは異なり、セクター間処理なしで正確に1つのブロックで動作 します。
*
* フロー: 認証 -> 書き込み前ダンプ -> 書き込み -> 書き込み後ダンプ -> 読み取ると検証 -> クリア -> ダンプ
*
* @param block 読み書きするブロック番号(セクタートレーラブロックであってはいけません)
*/
void read_write_single_block(const uint8_t block)
{
constexpr char msg[] = "M5Unit-RFID";// 固定テストメッセージ(16バイト ブロック内に収まります)
// 読み書き操作前にKeyAで認証
if (!nfc_a.mifareClassicAuthenticateA(block, keyA)) {
M5_LOGE("Failed to AuthA");
return;
}
// 書き込み 前のブロック内容をダンプ
M5.Log.printf("Before[%u] ----\n", block);
nfc_a.dump(block);
// テストメッセージをブロックに書き込む
M5.Log.printf("Write\n");
if (!nfc_a.write16(block, (const uint8_t*)msg, sizeof(msg))) {
M5_LOGE("Failed to write");
return;
}
// 書き込み 後のブロック内容をダンプ
M5.Log.printf("After[%u] ----\n", block);
nfc_a.dump(block);
// データを読み取ってデータ整合性を検証
uint8_t rbuf[16]{};
if (!nfc_a.read16(rbuf, block)) {
M5_LOGE("Failed to read");
return;
}
bool verify = (std::memcmp(rbuf, (const uint8_t*)msg, sizeof(msg)) == 0);// 読み取ったデータを元のメッセージと比較
M5.Log.printf("Verify %s\n", verify ? "OK" : "NG");
// 最小限のゼロデータを書き込んでブロックをクリア(ライブラリは16バイトにパッド)
M5.Log.printf("Clear\n");
uint8_t c[1]{};
if (!nfc_a.write16(block, c, sizeof(c))) {
M5_LOGE("Failed to write");
return;
}
// クリア 後のブロック内容をダンプ
nfc_a.dump(block);
}
} // namespace
void setup()
{
M5.begin();
// スクリーンをランドスケープモードに設定
if (lcd.height() > lcd.width()) {
lcd.setRotation(1);
}
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.end();// 既存のI2C接続を閉じる
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
// NFCユニットをマネージャーに追加して初期化
bool unit_ready = Units.add(unit, Wire) && Units.begin();
M5_LOGI("NFC Unit initialized: %s", unit_ready ? "OK" : "NG");
lcd.setFont(&fonts::FreeMono9pt7b);
lcd.setCursor(0, 0);
lcd.printf("Put Classic card & click/hold BtnA");
M5.Log.printf("Put Classic card & click/hold BtnA\n");
}
void loop()
{
M5.update();
Units.update();
bool clicked = M5.BtnA.wasClicked(); // ブロック間読み書き テスト用
bool held = M5.BtnA.wasHold(); // 単一ブロック読み書き テスト用
if (clicked || held) {
PICC picc;
if (nfc_a.detect(picc)) {
lcd.fillScreen(TFT_DARKGREEN);
if (nfc_a.identify(picc) && nfc_a.reactivate(picc)) {
// カード情報を出力:UID、型、ユーザー領域サイズ、容量
M5.Log.printf("PICC:%s %s %u/%u\n",
picc.uidAsString().c_str(),
picc.typeAsString().c_str(),
picc.userAreaSize(),
picc.totalSize());
// MIFARE Classic カード のみを処理し、他の型はスキップ
if (!picc.isMifareClassic()) {
M5.Log.printf("Not a MIFARE Classic card, skipped\n");
} else if (clicked) {
// ブロック間連続読み書き テスト
M5.Speaker.tone(2000, 30);
// カード容量に基づいてメッセージを選択
const char* msg = (picc.userAreaSize() >= 120) ? long_msg : short_msg;
bool ret = read_write(picc.firstUserBlock(), msg);// 最初のユーザーブロックから開始
lcd.fillScreen(ret ? TFT_BLACK : TFT_RED);// 黒 = 成功、赤 = 失敗
} else if (held) {
// 単一ブロック読み書き テスト
M5.Speaker.tone(4000, 30);
// セクタートレーラ(キーとアクセスビットを含む)を避けて、最後から2番目のブロックを使用
read_write_single_block(picc.blocks - 2);
}
nfc_a.deactivate();// カード通信を解放
} else {
M5_LOGE("Failed to identify/activate");
}
} else {
M5.Log.printf("PICC NOT detected\n");
}
lcd.setCursor(0, 0);
lcd.printf("Put Classic card & click/hold BtnA");
M5.Log.printf("Put Classic card & click/hold BtnA\n");
}
}シリアルモニター 出力例:
PICC:3E86E2D5 MIFARE Classsic1K 752/1024
================================ WRITE block:1 len:112
00)[000]:3E 86 E2 D5 8F 08 04 00 62 63 64 65 66 67 68 69 [0 0 0]
[001]:54 68 69 73 20 69 73 20 61 20 73 61 6D 70 6C 65 [0 0 0]
[002]:20 6D 65 73 73 61 67 65 20 62 75 66 66 65 72 20 [0 0 0]
[003]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1]
================================ VERIFY:OK
================================ CLEAR
00)[000]:3E 86 E2 D5 8F 08 04 00 62 63 64 65 66 67 68 69 [0 0 0]
[001]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[002]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[003]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1] PICC:3E86E2D5 MIFARE Classsic1K 752/1024
Before[62] ----
15)[060]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[061]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[062]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[063]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1]
Write
After[62] ----
15)[060]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[061]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[062]:4D 35 55 6E 69 74 2D 52 46 49 44 00 00 00 00 00 [0 0 0]
[063]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1]
Verify OK
Clear
15)[060]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[061]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[062]:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [0 0 0]
[063]:00 00 00 00 00 00 FF 07 80 69 FF FF FF FF FF FF [0 0 1] このサンプルは、Unit NFCを使用してNDEF形式でNFCタグを読み書きする方法を示し、以下の機能をサポートしています:
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <M5Utility.h>
#include <Wire.h>
#include <algorithm>
#include <vector>
using namespace m5::nfc; // NFC共通名前空間
using namespace m5::nfc::a; // NFC-Aプロトコルレイヤー
using namespace m5::nfc::a::mifare; // MIFAREカード操作
using namespace m5::nfc::ndef; // NDEF (NFC データ交換形式)
namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;// ユニット統一マネージャーインスタンス
m5::unit::UnitNFC unit{}; // NFC ユニットインスタンス(I2C インターフェース)
m5::nfc::NFCLayerA nfc_a{unit};// NFC-Aプロトコルレイヤーインスタンス
// PNG画像バイナリーデータ(64x64ピクセル、NDEFレコードへの書き込みに使用)
constexpr uint8_t poji_64_png[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x82, 0x12, 0x4c, 0x73, 0x00, 0x00, 0x00, 0x02, 0x62,
0x4b, 0x47, 0x44, 0x00, 0x01, 0xdd, 0x8a, 0x13, 0xa4, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00,
0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x46, 0xc9, 0x6b, 0x3e, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45,
0x07, 0xe8, 0x0b, 0x16, 0x08, 0x12, 0x36, 0x8d, 0x3c, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x77, 0x74, 0x45, 0x58, 0x74,
0x52, 0x61, 0x77, 0x20, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x38, 0x62,
0x69, 0x6d, 0x00, 0x0a, 0x38, 0x62, 0x69, 0x6d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x34, 0x30, 0x0a, 0x33,
0x38, 0x34, 0x32, 0x34, 0x39, 0x34, 0x64, 0x30, 0x34, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x33, 0x38, 0x34, 0x32, 0x34, 0x39, 0x34, 0x64, 0x30, 0x34, 0x32, 0x35, 0x30, 0x30, 0x30,
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x64, 0x34, 0x31, 0x64, 0x38, 0x63, 0x64, 0x39, 0x38, 0x66,
0x30, 0x30, 0x62, 0x32, 0x30, 0x34, 0x65, 0x39, 0x38, 0x30, 0x30, 0x39, 0x39, 0x38, 0x0a, 0x65, 0x63, 0x66, 0x38,
0x34, 0x32, 0x37, 0x65, 0x0a, 0xa6, 0x53, 0xc3, 0x8e, 0x00, 0x00, 0x00, 0x01, 0x6f, 0x72, 0x4e, 0x54, 0x01, 0xcf,
0xa2, 0x77, 0x9a, 0x00, 0x00, 0x00, 0x6e, 0x49, 0x44, 0x41, 0x54, 0x28, 0xcf, 0x63, 0xf8, 0x0f, 0x05, 0x0c, 0xc3,
0x98, 0xf1, 0x43, 0x1e, 0xcc, 0xf8, 0xbc, 0xf7, 0xf3, 0xf9, 0xbd, 0xe7, 0x81, 0x8c, 0xef, 0x36, 0xef, 0x81, 0x08,
0xc8, 0x78, 0xc2, 0x71, 0xfe, 0xb3, 0x80, 0x3a, 0x90, 0xf1, 0x4e, 0x22, 0xfe, 0x97, 0x44, 0x39, 0x90, 0xf1, 0x5e,
0x28, 0xfe, 0x97, 0xc7, 0x67, 0x20, 0xe3, 0x5c, 0xfc, 0xfc, 0x9f, 0xbf, 0x8a, 0x81, 0x8c, 0xf3, 0xff, 0xef, 0xff,
0xfe, 0xff, 0x19, 0x99, 0xf1, 0xfe, 0xff, 0xfb, 0xef, 0xff, 0xbf, 0x03, 0x19, 0xcf, 0xff, 0x7f, 0x7f, 0x0f, 0x24,
0x40, 0x0c, 0xa0, 0x15, 0x20, 0xc6, 0x67, 0x90, 0x95, 0x20, 0x2b, 0x7e, 0x83, 0x18, 0xf7, 0x07, 0x81, 0xdf, 0x69,
0xcc, 0x00, 0x00, 0x17, 0xc5, 0xed, 0x7a, 0x25, 0x80, 0xdc, 0xb3, 0x00, 0x00, 0x00, 0x50, 0x65, 0x58, 0x49, 0x66,
0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00,
0x01, 0x00, 0x00, 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x04, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0xa0, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x19, 0x25, 0x9b, 0x9b, 0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74,
0x65, 0x3a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x00, 0x32, 0x30, 0x32, 0x34, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x32,
0x54, 0x30, 0x38, 0x3a, 0x31, 0x35, 0x3a, 0x32, 0x31, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x28, 0xd2, 0x30, 0x68,
0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x79,
0x00, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x30, 0x34, 0x2d, 0x32, 0x38, 0x54, 0x30, 0x36, 0x3a, 0x35, 0x32, 0x3a, 0x32,
0x35, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0xcf, 0xa4, 0xfa, 0x1c, 0x00, 0x00, 0x00, 0x28, 0x74, 0x45, 0x58, 0x74,
0x64, 0x61, 0x74, 0x65, 0x3a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x00, 0x32, 0x30, 0x32, 0x34,
0x2d, 0x31, 0x31, 0x2d, 0x32, 0x32, 0x54, 0x30, 0x38, 0x3a, 0x31, 0x38, 0x3a, 0x35, 0x34, 0x2b, 0x30, 0x30, 0x3a,
0x30, 0x30, 0xa3, 0x99, 0x04, 0x05, 0x00, 0x00, 0x00, 0x11, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a,
0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x00, 0x31, 0x0f, 0x9b, 0x02, 0x49, 0x00, 0x00, 0x00,
0x12, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a, 0x45, 0x78, 0x69, 0x66, 0x4f, 0x66, 0x66, 0x73, 0x65,
0x74, 0x00, 0x33, 0x38, 0xad, 0xb8, 0xbe, 0x23, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69,
0x66, 0x3a, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x58, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x35,
0x31, 0x32, 0xb6, 0x2e, 0xb8, 0xdc, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a,
0x50, 0x69, 0x78, 0x65, 0x6c, 0x59, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x35, 0x31, 0x32,
0x2b, 0x21, 0x59, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
constexpr uint32_t poji_64_png_len = 738;// PNG画像データ長(バイト)
/**
* @brief DESFire カードをフォーマット(すべてのアプリケーションとファイルを削除)
*
* 注意:DESFire Light はformat操作をサポートしません
*/
void format_desfire()
{
auto& picc = nfc_a.activatedPICC();
if (picc.isMifareDESFire()) {
desfire::DESFireFileSystem dfs(nfc_a);
if (picc.type == Type::MIFARE_DESFire_Light) {
M5_LOGW("DESFire light can NOT format");
return;
} else {
if (!dfs.formatPICC(desfire::DESFIRE_DEFAULT_KEY)) {
M5_LOGE("Failed to formatPICC");
return;
}
uint32_t free_size{};
if (dfs.selectApplication() && dfs.getFreeMemory(free_size)) {
M5_LOGI("free(picc):%u", free_size);
}
}
}
}
/**
* @brief NDEFデータを読み取って表示
*
* アクティブ化されたカードからNDEFメッセージを読み取り、
* 解析して各レコードをスクリーン/シリアルに表示 します。
* Well-known 型(URI、テキストなど)およびMIME型(PNG画像など)をサポート します。
*/
void read_ndef()
{
// 非テストNDEF読み取りパスを無効にし、現在のデバッグログ のみを保持 します。
bool valid{};
if (!nfc_a.ndefIsValidFormat(valid)) {// カード内のデータが有効なNDEF形式かチェック
M5_LOGE("Failed to ndefIsValidFormat");
lcd.fillScreen(TFT_RED);
return;
}
if (!valid) {
M5.Log.printf("Data format is NOT NDEF\n");
return;
}
TLV msg;
// NDEF メッセージTLVを読み取る
if (!nfc_a.ndefRead(msg)) {
M5_LOGE("Failed to read");
lcd.fillScreen(TFT_RED);
return;
}
// 存在しない場合はNull TLVが返されます
if (msg.isMessageTLV()) {
lcd.setCursor(0, lcd.fontHeight());
// NDEFメッセージ内のすべてのレコードを反復処理
for (auto&& r : msg.records()) {
switch (r.tnf()) {
case TNF::Wellknown: {// Well-known 型レコード(例:URI "U"、テキスト "T")
auto s = r.payloadAsString().c_str();
M5.Log.printf("SZ:%3u TNF:%u T:%s [%s]\n", r.payloadSize(), r.tnf(), r.type(), s);
lcd.printf("T:%s [%s]\n", r.type(), s);
} break;
default:
// 他の型レコード(例:MIMEメディア型)
M5.Log.printf("SZ:%3u TNF:%u T:%s\n", r.payloadSize(), r.tnf(), r.type());
lcd.printf("T:%s\n", r.type());
// PNG画像の場合は、スクリーンに直接描画
if (strcmp(r.type(), "image/png") == 0) {
lcd.drawPng(r.payload(), r.payloadSize(), lcd.width() >> 1, lcd.height() >> 1);
}
break;
}
}
} else {
M5.Log.printf("NDEF Message TLV is NOT exists\n");
}
}
/**
* @brief NDEFデータをタグに書き込む
*
* URI、多言語テキスト、PNG画像を含むNDEFメッセージを作成して、タグに書き込み ます。
* タグ容量に基づいてレコード数を自動調整 します。
*/
void write_ndef()
{
auto& picc = nfc_a.activatedPICC();// 現在アクティベートされたカードを取得
/*
**** MIFARE Ultralight 注意 ****
Ultralight シリーズをNDEF形式に変更
注意:この変更は取り消せません
*****************************
*/
if (picc.isMifareUltralight()) {
// Ultralight カードをNDEF形式に変換(取り消し不可能な操作)
if (!nfc_a.mifareUltralightChangeFormatToNDEF()) {
M5_LOGE("Failed to mifareUltralightChangeFormatToNDEF");
lcd.fillScreen(TFT_RED);
return;
}
M5_LOGI("Changed NDEF format");
}
/*
**** MIFARE DESFire 注意 ****
DESFire カードがNDEF形式でない場合、PICC はフォーマットされます
つまり、既存のすべてのファイルとアプリケーションが削除されます!
DESFire light の場合、ファイル構造はNDEF仕様に準拠するように変更され、
データは上書きされます。
*****************************
*/
if (picc.isMifareDESFire() && picc.type != Type::MIFARE_DESFire_Light) {
bool valid{};
if (!nfc_a.ndefIsValidFormat(valid)) {
lcd.fillScreen(TFT_RED);
return;
}
M5_LOGI("NDEF format valid?:%u", valid);
if (!valid) {
format_desfire();// NDEF形式が無効な場合、最初にDESFire カードをフォーマット
// NDEF ファイル構造を準備
if (!nfc_a.ndefPrepareDesfire(picc.userAreaSize())) {
M5_LOGE("Failed to prepare NDEF files");
lcd.fillScreen(TFT_RED);
return;
}
M5_LOGI("Prepare for NDEF OK");
}
}
// NDEF メッセージを作成して書き込む
TLV msg{Tag::Message};
Record r[5] = {}; // Wellknown をデフォルトとして
// URIレコード
r[0].setURIPayload("m5stack.com/", URIProtocol::HTTPS);
// 言語型を持つテキストレコード
const char* en_data = "Hello M5Stack";
r[1].setTextPayload(en_data, "en");
const char* zh_data = "你好 M5Stack";
r[2].setTextPayload(zh_data, "zh");
const char* ja_data = "こんにちは M5Stack";
r[3].setTextPayload(ja_data, "ja");
// MIMEレコード
Record png{TNF::MIMEMedia};// MIME型レコードを作成
png.setType("image/png");// MIME型をPNGに設定
png.setPayload(poji_64_png, poji_64_png_len);// PNG画像データをペイロード として設定
r[4] = png;
// 最大利用可能領域を計算(ユーザー領域サイズからターミネータTLVの1バイトを差し引き)
uint32_t max_user_size = nfc_a.activatedPICC().userAreaSize() - 1 /* terminator TLV */;
for (auto&& rr : r) {
msg.push_back(rr);
if (msg.required() > max_user_size) {
msg.pop_back(); // 容量超過、最後の追加レコードを削除
break;
}
}
// NDEFメッセージをタグに書き込む
if (!nfc_a.ndefWrite(msg)) {
M5_LOGE("Failed to write");
lcd.fillScreen(TFT_RED);
return;
}
M5.Log.printf("Write NDEF OK!\n");
}
} // namespace
void setup()
{
M5.begin();
// スクリーンをランドスケープモードに設定
if (lcd.height() > lcd.width()) {
lcd.setRotation(1);
}
auto board = M5.getBoard();
bool unit_ready{};
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.end();// 既存のI2C接続を閉じる
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
// NFCユニットをマネージャーに追加して初期化
unit_ready = Units.add(unit, Wire) && Units.begin();
M5_LOGI("M5UnitUnified initialized");
M5_LOGI("%s", Units.debugInfo().c_str());
lcd.setFont(&fonts::FreeMono9pt7b);
lcd.setCursor(0, 0);
lcd.printf("Please put the PICC and click/hold BtnA");
M5.Log.printf("Please put the PICC and click/hold BtnA\n");
}
void loop()
{
M5.update();
Units.update();
bool clicked = M5.BtnA.wasClicked(); // 読み取り用
bool held = M5.BtnA.wasHold(); // 書き込み用
if (clicked || held) {
PICC picc{};
if (nfc_a.detect(picc)) {
if (nfc_a.identify(picc) && nfc_a.reactivate(picc)) {
M5.Log.printf("PICC:%s %s %u/%u\n", picc.uidAsString().c_str(), picc.typeAsString().c_str(),
picc.userAreaSize(), picc.totalSize());
// カード がNDEFをサポートしているかチェック
if (picc.supportsNDEF()) {
if (clicked) {
lcd.fillScreen(TFT_BLUE);
// nfc_a.dump();
read_ndef();
} else if (held) {
lcd.fillScreen(TFT_YELLOW);
write_ndef();
lcd.fillScreen(0);
}
M5.Log.printf("Please remove the PICC from the reader\n");
} else {
M5.Log.printf("Not support the NDEF\n");
}
} else {
M5_LOGE("Failed to identify/activate %s", picc.uidAsString().c_str());
}
nfc_a.deactivate();
lcd.setCursor(0, lcd.height() / 2);
lcd.printf("Please put the PICC and click/hold BtnA");
M5.Log.printf("Please put the PICC and click/hold BtnA\n");
} else {
M5.Log.printf("PICC NOT exists\n");
}
}
}シリアルモニター 出力例:
PICC:047D9D82752291 MIFARE Ultralight EV1 11 48/80
SZ: 13 TNF:1 T:U [https://m5stack.com/]
SZ: 16 TNF:1 T:T [Hello M5Stack] PICC:047D9D82752291 MIFARE Ultralight EV1 11 48/80
Write NDEF OK!
Please remove the PICC from the reader このサンプルは、Unit NFCを使用して電子ウォレット機能を実装する方法を示し、2つのモード(非充電可能ウォレット)をサポートしています:
非充電可能ウォレット(ボタンクリック):控除操作のみをサポートし、不正な充電を防止し、1回限りの消費シナリオに適しています。このモードは特定のアクセス許可設定を通じて充電を防止し、消費額のみが減少し、増加しないようにします。
充電可能ウォレット(ボタンホールド):控除と充電の両方をサポートし、繰り返し充電が必要なシナリオに適しています。妥当なアクセス許可設定を通じて、両方の操作を許可し、より柔軟なアプリケーション体験を提供 します。
NFC電子ウォレット の中核原理は、MIFARE Classic カードValue Block(バリューブロック)を使用して金額情報を保存・管理することです。バリューブロックは特別な内部形式を使用し、データバックアップと改ざん防止メカニズムがあります。各バリューブロックはカード上の1つのブロック領域(16バイト)を占有し、次を含みます:金額値(4バイト)、金額逆バックアップ(4バイト)、金額バックアップ(4バイト)、逆バックアップ(4バイト)。この冗長設計により、データが悪意を持って改ざんされるのを防ぎます。
認証操作
| メソッド | 関数 |
|---|---|
mifareClassicAuthenticateA(block, key) | KeyAでセクターを認証 |
mifareClassicAuthenticateB(block, key) | KeyBでセクターを認証 |
mifareClassicWriteAccessCondition(block, mode, keyA, keyB) | ブロックアクセス許可を変更 |
バリューブロック操作
| メソッド | 関数 |
|---|---|
mifareClassicWriteValueBlock(block, value) | バリューブロック を初期化、金額を書き込む |
mifareClassicDecrementValueBlock(block, amount) | 控除操作 |
mifareClassicIncrementValueBlock(block, amount) | 充電操作 |
mifareClassicRestoreValueBlock(block) | バリューブロック をカードからバッファに復元 |
mifareClassicTransferValueBlock(block) | バッファデータをカードに転送 |
ステータスクエリ
| メソッド | 関数 |
|---|---|
activatedPICC() | 現在アクティベートされたカード オブジェクトを取得 |
picc.isUserBlock(block) | ブロックがユーザーアクセス可能かチェック |
dump(block) | デバッグ用16進ブロック内容を出力 |
| ワークフロー段階 | 非充電可能ウォレット | 充電可能ウォレット |
|---|---|---|
| 1. 認証 | KeyA 認証 | KeyA認証、その後KeyB |
| 2. 初期化 | READ_WRITE_BLOCKモードに設定 | READ_WRITE_BLOCKモードに設定 |
| 3. 金額設定 | 初期金額を書き込む | 初期金額を書き込む |
| 4. アクセス許可 | VALUE_BLOCK_NON_RECHARGEABLE(書き込み不可) | VALUE_BLOCK_RECHARGEABLE(読み書き許可) |
| 5. 控除 | サポート ✓ | サポート ✓ |
| 6. 充電 | サポートされていない ✗ (失敗) | サポート ✓ |
| 7. データコピー | バリューブロック を隣接ブロックにコピー | バリューブロック を隣接ブロックにコピー |
| 8. 復元 | 通常ブロックに復元 | アクセス許可を復元してクリア |
中核的な違い: 2つのモード間の主な違いは、アクセス許可ビットの設定です。非充電可能モードはアクセス許可ビット設定を通じてIncrement コマンドの実行を防ぎ、充電可能モードは両方の操作を許可 します。
(코드 및 예제는 中文版과 동일하게 유지합니다...)
プロセスに従ってコードを実行した後、setup() はデバイスを初期化してプロンプト情報を表示 します。loop() では:
各操作プロセス:
dump() を使用してブロック内容を出力して、データ変更を確認出力情報の説明:
PICC: の後は、カード UID、型、容量情報です[062]: 形式はセクタ15、ブロック62データを示V:1234567 はバリューブロック に保存されている金額を示[0 0 1] はアクセス許可ビット(C1 C2 C3)を示し、読み書きと増分アクセス許可を決定非充電可能ウォレットを実行:
充電可能ウォレットを実行:
[0 0 1] から [1 1 0] に変更され、両方の操作がサポートされていることを示シリアルモニター 出力(中文版と同じですが文字が置き換わっています)