Arduino入門

2. デバイス&サンプル

5. 拡張モジュール&サンプル

アクセサリー

6. アプリケーション

PaperMono NFC 近距離無線通信

PaperMono の NFC(近距離無線通信)に関する API とサンプルプログラムです。

サンプルプログラム

ビルド要件

  • M5Stack ボードマネージャのバージョン >= 3.3.9
  • ボードオプション = M5PaperMono
  • M5Unified ライブラリのバージョン >= 0.2.20
  • M5GFX ライブラリのバージョン >= 0.2.27
  • M5UnitUnifiedNFC ライブラリのバージョン >= 0.1.0

PaperMono の NFC / RFID 有効化・リセット信号 PYB_NFC_EN は M5IOE1 GPIO4 (M5IOE1_PIN_4) に接続されています。このサンプルプログラムでは、まずこのピンを出力に設定して HIGH にし、リセットを解除して NFC モジュールを有効化します。この処理を行わない場合、NFC モジュールが正常に初期化できない可能性があります。

クイックスキャンと識別

このサンプルは、PaperMono の検出エリア内にある NFC-A タグを継続的に検出し、UID、タイプ、ATQA、SAK、ユーザー領域容量、総容量を出力します。

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <utility/M5IOE1_Class.hpp>
#include <vector>

using namespace m5::nfc::a;

namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;
m5::unit::UnitNFC unit{};
m5::nfc::NFCLayerA nfc_a{unit};

constexpr uint8_t NFC_IRQ_PIN = 6;
constexpr auto NFC_CONTROL_PIN = m5::M5IOE1_Class::gpio4;
constexpr int STATUS_HEIGHT = 92;
}  // namespace

void drawBottomStatus(const char* message)
{
    const int top = lcd.height() - STATUS_HEIGHT;
    lcd.fillRect(0, top, lcd.width(), STATUS_HEIGHT, TFT_WHITE);
    lcd.drawFastHLine(0, top, lcd.width(), TFT_BLACK);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold12pt7b);
    lcd.drawString(message, lcd.width() / 2, top + STATUS_HEIGHT / 2);
}

void stopOnError(const char* message)
{
    M5_LOGE("%s", message);
    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, lcd.height() / 2 - 48);
    lcd.drawString(message, lcd.width() / 2, lcd.height() / 2 + 40);
    lcd.display();
    while (true) {
        delay(1000);
    }
}

void drawReadyScreen()
{
    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, 90);
    lcd.drawString("Quick Scan", lcd.width() / 2, 170);
    lcd.drawString("Place NFC-A tag", lcd.width() / 2, 300);
    drawBottomStatus("Waiting for NFC-A tag");
    lcd.display();
}

void setup()
{
    Serial.begin(115200);

    auto cfg = M5.config();
    cfg.clear_display = false;
    M5.begin(cfg);
    lcd.setRotation(0);
    lcd.setEpdMode(epd_mode_t::epd_text);

    auto& ioe = M5.getIOExpander(0);
    ioe.setHighImpedance(NFC_CONTROL_PIN, false);
    ioe.setDirection(NFC_CONTROL_PIN, true);
    ioe.digitalWrite(NFC_CONTROL_PIN, true);
    delay(10);

    auto nfc_cfg = unit.config();
    nfc_cfg.using_irq = true;
    nfc_cfg.irq = NFC_IRQ_PIN;
    unit.config(nfc_cfg);

    if (!(Units.add(unit, M5.In_I2C) && Units.begin())) {
        stopOnError("NFC init failed");
    }

    M5_LOGI("M5Unit-NFC initialized");
    M5_LOGI("%s", Units.debugInfo().c_str());
    drawReadyScreen();
}

void loop()
{
    M5.update();
    Units.update();

    static bool tagWasShown = false;
    std::vector<PICC> piccs;
    if (!nfc_a.detect(piccs)) {
        tagWasShown = false;
        delay(20);
        return;
    }
    if (tagWasShown) {
        nfc_a.deactivate();
        delay(100);
        return;
    }
    tagWasShown = true;

    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(top_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, 24);
    lcd.drawString("Quick Scan Result", lcd.width() / 2, 76);
    lcd.setFont(&fonts::FreeSansBold12pt7b);

    uint16_t identified = 0;
    int y = 150;
    for (auto& picc : piccs) {
        if (!nfc_a.identify(picc)) {
            M5_LOGW("Failed to identify %s", picc.uidAsString().c_str());
            continue;
        }

        M5.Log.printf("PICC:%s %s %04X/%02X %u/%u\n", picc.uidAsString().c_str(),
                      picc.typeAsString().c_str(), picc.atqa, picc.sak, picc.userAreaSize(),
                      picc.totalSize());

        if (identified < 5) {
            char uidLine[64];
            char infoLine[96];
            snprintf(uidLine, sizeof(uidLine), "[%u] UID: %s", identified, picc.uidAsString().c_str());
            snprintf(infoLine, sizeof(infoLine), "%s  %04X/%02X  %u/%u", picc.typeAsString().c_str(),
                     picc.atqa, picc.sak, picc.userAreaSize(), picc.totalSize());
            lcd.drawString(uidLine, lcd.width() / 2, y);
            lcd.drawString(infoLine, lcd.width() / 2, y + 38);
            y += 92;
        }
        ++identified;
    }

    char countLine[32];
    snprintf(countLine, sizeof(countLine), "Found: %u tag(s)", identified);
    drawBottomStatus(countLine);
    nfc_a.deactivate();
    lcd.display();
    M5.Log.printf("==> %u PICC\n", identified);
    delay(500);
}

1 枚以上の NFC-A タグを PaperMono の NFC 検出エリアに近づけると、識別結果が画面に表示され、シリアルモニターにも出力されます。

全データの読み取り

このサンプルでは、BtnA で全データの読み取りを開始します。プログラムはタグを検出、識別、再アクティブ化した後、nfc_a.dump() を使用してタグの全データをシリアルモニターに出力します。画面にはタグの基本情報と読み取り状態が表示されます。

MIFARE Classic タグの場合、プログラムはデフォルトの KeyA FFFFFFFFFFFF で認証します。タグのキーが変更されている場合は、コード内の keyA を実際のキーに変更してください。

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <utility/M5IOE1_Class.hpp>

using namespace m5::nfc::a;
using namespace m5::nfc::a::mifare;
using namespace m5::nfc::a::mifare::classic;

namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;
m5::unit::UnitNFC unit{};
m5::nfc::NFCLayerA nfc_a{unit};

constexpr uint8_t NFC_IRQ_PIN = 6;
constexpr auto NFC_CONTROL_PIN = m5::M5IOE1_Class::gpio4;
constexpr Key keyA = DEFAULT_KEY;
constexpr int STATUS_HEIGHT = 92;
}  // namespace

void drawBottomStatus(const char* message)
{
    const int top = lcd.height() - STATUS_HEIGHT;
    lcd.fillRect(0, top, lcd.width(), STATUS_HEIGHT, TFT_WHITE);
    lcd.drawFastHLine(0, top, lcd.width(), TFT_BLACK);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold12pt7b);
    lcd.drawString(message, lcd.width() / 2, top + STATUS_HEIGHT / 2);
}

void stopOnError(const char* message)
{
    M5_LOGE("%s", message);
    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, lcd.height() / 2 - 48);
    lcd.drawString(message, lcd.width() / 2, lcd.height() / 2 + 40);
    lcd.display();
    while (true) {
        delay(1000);
    }
}

void drawReadyScreen()
{
    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, 110);
    lcd.drawString("Complete Reading", lcd.width() / 2, 190);
    lcd.drawString("Place tag and press BtnA", lcd.width() / 2, 340);
    drawBottomStatus("BtnA: scan and dump tag");
    lcd.display();
}

void setup()
{
    Serial.begin(115200);

    auto cfg = M5.config();
    cfg.clear_display = false;
    M5.begin(cfg);
    lcd.setRotation(0);
    lcd.setEpdMode(epd_mode_t::epd_text);

    auto& ioe = M5.getIOExpander(0);
    ioe.setHighImpedance(NFC_CONTROL_PIN, false);
    ioe.setDirection(NFC_CONTROL_PIN, true);
    ioe.digitalWrite(NFC_CONTROL_PIN, true);
    delay(10);

    auto nfc_cfg = unit.config();
    nfc_cfg.using_irq = true;
    nfc_cfg.irq = NFC_IRQ_PIN;
    unit.config(nfc_cfg);

    if (!(Units.add(unit, M5.In_I2C) && Units.begin())) {
        stopOnError("NFC init failed");
    }

    M5_LOGI("M5Unit-NFC initialized");
    M5_LOGI("%s", Units.debugInfo().c_str());
    drawReadyScreen();
}

void loop()
{
    M5.update();
    Units.update();

    if (!M5.BtnA.wasClicked()) {
        delay(20);
        return;
    }

    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, 80);
    lcd.drawString("Reading...", lcd.width() / 2, 180);
    drawBottomStatus("Waiting for tag...");
    lcd.display();

    PICC picc{};
    if (!nfc_a.detect(picc)) {
        drawBottomStatus("PICC not found");
        lcd.display();
        M5.Log.printf("PICC NOT exists\n");
        delay(500);
        drawReadyScreen();
        return;
    }

    if (!nfc_a.identify(picc) || !nfc_a.reactivate(picc)) {
        drawBottomStatus("Identify failed");
        lcd.display();
        M5_LOGE("Failed to identify/activate %s", picc.uidAsString().c_str());
        nfc_a.deactivate();
        delay(500);
        drawReadyScreen();
        return;
    }

    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(top_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, 30);
    lcd.drawString("Complete Reading", lcd.width() / 2, 86);
    lcd.setFont(&fonts::FreeSansBold12pt7b);
    lcd.drawString(picc.uidAsString().c_str(), lcd.width() / 2, 190);
    lcd.drawString(picc.typeAsString().c_str(), lcd.width() / 2, 240);
    drawBottomStatus("Dumping data to Serial...");
    lcd.display();

    M5.Log.printf("==== Dump %s %s %u/%u ====\n", picc.uidAsString().c_str(),
                  picc.typeAsString().c_str(), picc.userAreaSize(), picc.totalSize());
    nfc_a.dump(keyA);
    M5.Log.printf("==== Dump complete ====\n");

    drawBottomStatus("Dump complete");
    lcd.display();
    nfc_a.deactivate();
    delay(800);
    drawReadyScreen();
}

シリアルモニターの出力例:

[000/00]:04 49 37 F2
[001/01]:D2 A6 1C 90
[002/02]:F8 48 00 00
[003/03]:E1 10 12 00
[004/04]:49 41 4D 4C
[005/05]:49 55 42 4F
[006/06]:20 4D 35 53
[007/07]:54 41 43 4B
[008/08]:20 37 36 39
[009/09]:39 00 00 00
[010/0A]:00 00 00 00
[011/0B]:6C 6C 6F 20
[012/0C]:4D 35 53 74
[013/0D]:61 63 6B 11
[014/0E]:01 1A 54 02
[015/0F]:6A 61 E3 81
[016/10]:93 E3 82 93
[017/11]:E3 81 AB E3
[018/12]:81 A1 E3 81
[019/13]:AF 20 4D 35
[020/14]:53 74 61 63
[021/15]:6B 51 01 11
[022/16]:54 02 7A 68
[023/17]:E4 BD A0 E5
[024/18]:A5 BD 20 4D
[025/19]:35 53 74 61
[026/1A]:63 6B FE 78
[027/1B]:00 00 00 00
[028/1C]:00 00 00 00
[029/1D]:00 00 00 00
[030/1E]:00 00 00 00
[031/1F]:00 00 00 00
[032/20]:00 00 00 00
[033/21]:00 00 00 00
[034/22]:00 00 00 00
[035/23]:00 00 00 00
[036/24]:00 00 00 00
[037/25]:00 00 00 00
[038/26]:00 00 00 00
[039/27]:00 00 00 00
[040/28]:00 00 00 BD
[041/29]:04 00 00 FF
[042/2A]:00 05 00 00
[043/2B]:00 00 00 00
[044/2C]:00 00 00 00
==== Dump complete ====

NDEF タグの読み書き

このサンプルでは、BtnA をクリックして NDEF メッセージを読み取り、BtnB をクリックして NDEF メッセージを書き込みます。プログラムはタグが NDEF に対応しているかを確認します。読み取り時はレコード内容を画面とシリアルモニターに出力し、書き込み時は HTTPS URI レコードとテキストレコードを保存します。

注意
このサンプルは、MIFARE Ultralight、NTAG、およびその他の NDEF 対応タグに適用できます。未フォーマットの MIFARE Ultralight に初めて書き込む場合、mifareUltralightChangeFormatToNDEF() によってタグ形式が変更されます。この操作は元に戻せないため、タグ内に保持する必要のあるデータがないことを確認してください。
cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <utility/M5IOE1_Class.hpp>
#include <vector>

using namespace m5::nfc;
using namespace m5::nfc::a;
using namespace m5::nfc::a::mifare;
using namespace m5::nfc::ndef;

namespace {
auto& lcd = M5.Display;
m5::unit::UnitUnified Units;      // Unit manager
m5::unit::UnitNFC unit{};         // NFC unit driver
m5::nfc::NFCLayerA nfc_a{unit};   // NFC-A protocol layer

constexpr uint8_t NFC_IRQ_PIN = 6;                             // NFC interrupt pin
constexpr auto NFC_CONTROL_PIN = m5::M5IOE1_Class::gpio4;      // NFC power-enable pin (IO expander)

// Layout constants
constexpr int STATUS_HEIGHT = 92;   // Bottom hint bar height
constexpr int TITLE_Y       = 30;   // Title position
constexpr int STATUS_Y      = 100;  // Status line below the title
constexpr int CONTENT_TOP   = 160;  // First data line position
constexpr int LINE_STEP     = 44;   // Vertical spacing between data lines

constexpr const char* KEY_HINT = "BtnA: Read    BtnB: Write";  // Fixed key hint text
}  // namespace

// Draw the fixed key-hint bar at the bottom
void drawBottomStatus(const char* message)
{
    const int top = lcd.height() - STATUS_HEIGHT;
    lcd.fillRect(0, top, lcd.width(), STATUS_HEIGHT, TFT_WHITE);
    lcd.drawFastHLine(0, top, lcd.width(), TFT_BLACK);  // Separator line
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);
    lcd.setTextDatum(middle_center);
    lcd.setFont(&fonts::FreeSansBold12pt7b);
    lcd.drawString(message, lcd.width() / 2, top + STATUS_HEIGHT / 2);
}

// Render one page: title + status + data lines + bottom key hint
void renderPage(const char* status, const std::vector<String>& lines)
{
    lcd.fillScreen(TFT_WHITE);
    lcd.setTextColor(TFT_BLACK, TFT_WHITE);

    // Title
    lcd.setTextDatum(top_center);
    lcd.setFont(&fonts::FreeSansBold18pt7b);
    lcd.drawString("PaperMono NFC Test", lcd.width() / 2, TITLE_Y);

    // Status / prompt line
    lcd.setFont(&fonts::FreeSansBold12pt7b);
    lcd.drawString(status, lcd.width() / 2, STATUS_Y);

    // Data lines, stop before the bottom hint bar
    int y = CONTENT_TOP;
    for (auto&& line : lines) {
        if (y + LINE_STEP >= lcd.height() - STATUS_HEIGHT) break;
        lcd.drawString(line, lcd.width() / 2, y);
        y += LINE_STEP;
    }

    drawBottomStatus(KEY_HINT);
    lcd.display();
}

// Show a fatal error and halt
void stopOnError(const char* message)
{
    M5_LOGE("%s", message);
    renderPage(message, {});
    while (true) {
        delay(1000);
    }
}

// Initial screen
void drawReadyScreen()
{
    renderPage("Click BtnA to Read, BtnB to Write", {});
}

// Read NDEF and show the result under the title
void readNdef()
{
    std::vector<String> lines;

    // Verify the tag is NDEF formatted
    bool valid = false;
    if (!nfc_a.ndefIsValidFormat(valid)) {
        M5_LOGE("Failed to check NDEF format");
        renderPage("NDEF check failed", lines);
        return;
    }
    if (!valid) {
        M5.Log.printf("Data format is NOT NDEF\n");
        renderPage("Tag is not NDEF formatted", lines);
        return;
    }

    // Read raw NDEF data
    TLV message;
    if (!nfc_a.ndefRead(message)) {
        M5_LOGE("Failed to read NDEF");
        renderPage("NDEF read failed", lines);
        return;
    }

    // No message TLV present
    if (!message.isMessageTLV()) {
        M5.Log.printf("NDEF Message TLV is NOT exists\n");
        renderPage("No NDEF message", lines);
        return;
    }

    // Collect each record into display lines
    char buf[192];
    for (auto&& record : message.records()) {
        if (record.tnf() == TNF::Wellknown) {
            // Well-known type: show type and decoded payload
            const auto payload = record.payloadAsString();
            M5.Log.printf("SZ:%3u TNF:%u T:%s [%s]\n", record.payloadSize(), record.tnf(),
                          record.type(), payload.c_str());
            snprintf(buf, sizeof(buf), "Type: %s", record.type());
            lines.push_back(String(buf));
            snprintf(buf, sizeof(buf), "%s", payload.c_str());
            lines.push_back(String(buf));
        } else {
            // Other types: show type only
            M5.Log.printf("SZ:%3u TNF:%u T:%s\n", record.payloadSize(), record.tnf(),
                          record.type());
            snprintf(buf, sizeof(buf), "Type: %s", record.type());
            lines.push_back(String(buf));
        }
    }

    renderPage("NDEF Read Result", lines);
}

// Write NDEF, output written data into outLines for display
bool writeNdef(std::vector<String>& outLines)
{
    auto& picc = nfc_a.activatedPICC();

    // Format MIFARE Ultralight as NDEF if needed
    if (picc.isMifareUltralight() && !nfc_a.mifareUltralightChangeFormatToNDEF()) {
        M5_LOGE("Failed to format MIFARE Ultralight as NDEF");
        return false;
    }

    // Reserve one byte for the terminator TLV
    const uint32_t maxUserSize = picc.userAreaSize() > 1 ? picc.userAreaSize() - 1 : 0;
    if (maxUserSize == 0) {
        M5_LOGE("Invalid NDEF user area size");
        return false;
    }

    // Build the message with a URI and a text record
    TLV message{Tag::Message};
    Record uri;
    Record text;
    uri.setURIPayload("m5stack.com/", URIProtocol::HTTPS);
    text.setTextPayload("Hello PaperMono", "en");

    // Add URI record if it fits
    message.push_back(uri);
    if (message.required() > maxUserSize) {
        message.pop_back();
        return false;
    }
    outLines.push_back(String("URI: https://m5stack.com/"));

    // Add text record if it still fits
    message.push_back(text);
    if (message.required() > maxUserSize) {
        message.pop_back();
    } else {
        outLines.push_back(String("Text: Hello PaperMono"));
    }

    // Write to the tag
    if (message.records().empty() || !nfc_a.ndefWrite(message)) {
        M5_LOGE("Failed to write NDEF");
        outLines.clear();
        return false;
    }

    M5.Log.printf("Write NDEF OK!\n");
    return true;
}

void setup()
{
    Serial.begin(115200);

    // Keep current display content on boot
    auto cfg = M5.config();
    cfg.clear_display = false;
    M5.begin(cfg);
    lcd.setRotation(0);
    lcd.setEpdMode(epd_mode_t::epd_text);

    // Enable NFC unit power via IO expander
    auto& ioe = M5.getIOExpander(0);
    ioe.setHighImpedance(NFC_CONTROL_PIN, false);
    ioe.setDirection(NFC_CONTROL_PIN, true);
    ioe.digitalWrite(NFC_CONTROL_PIN, true);
    delay(10);

    // Configure NFC to use the IRQ pin
    auto nfc_cfg = unit.config();
    nfc_cfg.using_irq = true;
    nfc_cfg.irq = NFC_IRQ_PIN;
    unit.config(nfc_cfg);

    // Register and start the NFC unit
    if (!(Units.add(unit, M5.In_I2C) && Units.begin())) {
        stopOnError("NFC init failed");
    }

    M5_LOGI("M5Unit-NFC initialized");
    M5_LOGI("%s", Units.debugInfo().c_str());
    drawReadyScreen();
}

void loop()
{
    M5.update();
    Units.update();

    // Check button input
    const bool readRequested  = M5.BtnA.wasClicked();  // BtnA -> Read
    const bool writeRequested = M5.BtnB.wasClicked();  // BtnB -> Write
    if (!readRequested && !writeRequested) {
        delay(20);
        return;
    }

    // Prompt user to place the tag
    renderPage(readRequested ? "Place tag for READ..." : "Place tag for WRITE...", {});

    // Detect a tag in the field
    PICC picc{};
    if (!nfc_a.detect(picc)) {
        M5.Log.printf("PICC NOT exists\n");
        renderPage("PICC not found", {});
        return;
    }

    // Identify and (re)activate the tag
    if (!nfc_a.identify(picc) || !nfc_a.reactivate(picc)) {
        M5_LOGE("Failed to identify/activate %s", picc.uidAsString().c_str());
        renderPage("Identify failed", {});
        nfc_a.deactivate();
        return;
    }

    M5.Log.printf("PICC:%s %s %u/%u\n", picc.uidAsString().c_str(), picc.typeAsString().c_str(),
                  picc.userAreaSize(), picc.totalSize());

    // Dispatch based on tag capability and requested action
    if (!picc.supportsNDEF()) {
        M5.Log.printf("Not support the NDEF\n");
        renderPage("Tag does not support NDEF", {});
    } else if (readRequested) {
        readNdef();
    } else {
        std::vector<String> lines;
        const bool ok = writeNdef(lines);
        renderPage(ok ? "NDEF Write Result" : "NDEF write failed", lines);
    }

    M5.Log.printf("Please remove the PICC from the reader\n");
    nfc_a.deactivate();
}
  • Write

シリアルモニターの出力例:

PICC:047D9D82752291 MIFARE Ultralight EV1 11 48/80
Write NDEF OK!
Please remove the PICC from the reader
  • Read

シリアルモニターの出力例:

PICC:047D9D82752291 MIFARE Ultralight EV1 11 48/80
SZ: 13 TNF:1 T:U [https://m5stack.com/]
SZ: 18 TNF:1 T:T [Hello PaperMono]
Please remove the PICC from the reader

タグエミュレーション

このサンプルでは、PaperMono を固定 UID 04504150455201 の MIFARE Ultralight NFC-A タグとしてエミュレートし、テキスト PaperMONO を含む NDEF レコードを保存します。初期化に成功すると、画面にエミュレートしたタグの情報が表示されます。プログラムはリーダーからの要求を継続的に処理し、NFC-A の状態変化をシリアルモニターに出力します。

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
#include <Arduino.h>
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedNFC.h>
#include <wiring/m5_unit_unified_wiring.hpp>

namespace {

using m5::nfc::NFC;
using m5::nfc::EmulationLayerA;
using m5::nfc::a::PICC;
using m5::nfc::a::Type;

constexpr uint16_t kBlack = 0x0000;
constexpr uint16_t kDark = 0x52AA;
constexpr uint16_t kWhite = 0xFFFF;
constexpr uint8_t kFrontlightBrightness = 160;
constexpr int kNfcIrqPin = 6;

// Seven-byte NFC-A UID used by this demo.
constexpr uint8_t kUid[] = {0x04, 0x50, 0x41, 0x50, 0x45, 0x52, 0x01};

// MIFARE Ultralight memory. Page 4 contains an NDEF Text record whose
// language is "en" and whose text is exactly "PaperMONO".
uint8_t cardMemory[64] = {
    0x00, 0x00, 0x00, 0x00,  // UID0, UID1, UID2, BCC0
    0x00, 0x00, 0x00, 0x00,  // UID3, UID4, UID5, UID6
    0x00, 0xA3, 0x00, 0x00,  // BCC1, internal, lock bytes
    0xE1, 0x10, 0x06, 0x00,  // Capability Container
    0x03, 0x10, 0xD1, 0x01,  // NDEF TLV and record header
    0x0C, 0x54, 0x02, 0x65,  // payload length, Text type, language length, 'e'
    0x6E, 0x50, 0x61, 0x70,  // 'nPaper'
    0x65, 0x72, 0x4D, 0x4F,  // 'erMO'
    0x4E, 0x4F, 0xFE, 0x00,  // 'NO', terminator
};

m5::unit::UnitUnified units;
m5::unit::UnitNFC nfc;
EmulationLayerA emulation(nfc);
PICC card;
M5Canvas canvas(&M5.Display);

uint8_t calculateBcc(const uint8_t* data, size_t length,
                     uint8_t initial = 0)
{
    uint8_t value = initial;
    for (size_t i = 0; i < length; ++i) {
        value ^= data[i];
    }
    return value;
}

void embedUid()
{
    memcpy(cardMemory, kUid, 3);
    cardMemory[3] = calculateBcc(kUid, 3, 0x88);
    memcpy(cardMemory + 4, kUid + 3, 4);
    cardMemory[8] = calculateBcc(kUid + 3, 4);
}

void drawNfcMark(int cx, int cy)
{
    canvas.drawCircle(cx, cy, 78, kBlack);
    canvas.drawCircle(cx, cy, 69, kBlack);
    canvas.fillCircle(cx, cy, 8, kBlack);
    canvas.drawArc(cx, cy, 31, 25, 305, 55, kBlack);
    canvas.drawArc(cx, cy, 49, 43, 305, 55, kBlack);
    canvas.drawArc(cx, cy, 67, 61, 305, 55, kBlack);
}

void drawReadyScreen(bool ready)
{
    M5.Display.setEpdMode(epd_mode_t::epd_fast);
    canvas.fillSprite(kWhite);
    canvas.setTextDatum(middle_center);
    canvas.setTextColor(kBlack, kWhite);

    canvas.setFont(&fonts::FreeSansBold24pt7b);
    canvas.drawString("NFC CARD", 240, 100);
    canvas.setFont(&fonts::FreeSansBold18pt7b);
    canvas.setTextColor(ready ? kBlack : kDark, kWhite);
    canvas.drawString(ready ? "EMULATION READY" : "INITIALIZATION FAILED",
                      240, 170);

    drawNfcMark(240, 320);

    canvas.fillRoundRect(34, 450, 412, 176, 8, kBlack);
    canvas.setTextColor(kWhite, kBlack);
    canvas.setFont(&fonts::FreeSansBold12pt7b);
    canvas.drawString("MIFARE ULTRALIGHT", 240, 495);
    canvas.setFont(&fonts::FreeSans9pt7b);
    canvas.drawString("UID  04 50 41 50 45 52 01", 240, 548);
    canvas.drawString("NDEF TEXT  PaperMONO", 240, 590);

    canvas.setTextColor(kDark, kWhite);
    canvas.drawString(ready ? "Hold near an NFC reader"
                            : "Check serial output",
                      240, 700);
    canvas.pushSprite(0, 0);
}

bool beginCardEmulation()
{
    auto config = nfc.config();
    config.emulation = true;
    config.mode = NFC::A;
    config.using_irq = true;
    config.irq = kNfcIrqPin;
    nfc.config(config);

    if (!m5::unit::wiring::i2cClass(units, nfc, M5.In_I2C) ||
        !units.begin()) {
        return false;
    }

    embedUid();
    return card.emulate(Type::MIFARE_Ultralight, kUid, sizeof(kUid)) &&
           emulation.begin(card, cardMemory, sizeof(cardMemory));
}

const char* stateName(EmulationLayerA::State state)
{
    static constexpr const char* names[] = {
        "None", "Off", "Idle", "Ready", "Active", "Halt"
    };
    return names[static_cast<unsigned>(state)];
}

}  // namespace

void setup()
{
    Serial.begin(115200);
    delay(100);
    Serial.println("PaperMono NFC-A card emulation demo");

    auto config = M5.config();
    config.clear_display = false;
    config.internal_rtc = false;
    config.internal_imu = false;
    config.internal_mic = false;
    M5.begin(config);

    M5.Display.setRotation(0);
    M5.Display.setBrightness(kFrontlightBrightness);
    canvas.setColorDepth(16);
    if (!canvas.createSprite(M5.Display.width(), M5.Display.height())) {
        Serial.println("Display canvas allocation failed");
        return;
    }

    const bool ready = beginCardEmulation();
    drawReadyScreen(ready);
    if (!ready) {
        Serial.println("NFC-A emulation initialization failed");
        return;
    }

    const auto& picc = emulation.emulatePICC();
    Serial.printf("NFC-A emulation ready: %s\n",
                  picc.typeAsString().c_str());
    Serial.printf("UID: %s, ATQA: %04X, SAK: %u\n",
                  picc.uidAsString().c_str(), picc.atqa, picc.sak);
    Serial.println("NDEF text: PaperMONO");
}

void loop()
{
    // Keep the loop short because NFC-A response timing is strict.
    units.update();
    emulation.update();

    static EmulationLayerA::State previous = EmulationLayerA::State::None;
    const auto state = emulation.state();
    if (state != previous) {
        Serial.printf("NFC-A state: %s\n", stateName(state));
        previous = state;
    }
    yield();
}

スマートフォンで読み取ったタグ情報の例:

シリアルモニターの出力例:

PaperMono NFC-A card emulation demo
NFC-A emulation ready: MIFARE Ultralight
UID: 04504150455201, ATQA: 0044, SAK: 0
NDEF text: PaperMONO
NFC-A state: Off
NFC-A state: Idle
NFC-A state: Off
NFC-A state: Idle
NFC-A state: Ready
NFC-A state: Active
NFC-A state: Off

ライブラリ

Page Tools
PDF
On This Page