この記事は レコチョク Advent Calendar 2024 の1日目の記事となります。
株式会社レコチョクでNX開発推進部に所属している木村です。
レコチョクでは昨年に引き続き、今年もMaker Faire Tokyo 2024に出展しました。
レコチョク「新しい音楽体験研究所」プロジェクトが日本最大のDIY展示発表会「Maker Faire Tokyo 2024」に出展
今回はMaker Faire Tokyo 2024の出展作品「カメレオンノーツ」で作曲情報の入力に用いる「エモブロック」の読み取りに利用したRFIDリーダーについて解説します。
「カメレオンノーツ」全体で扱っている技術について解説している記事も公開していますのでよかったらこちらもご一読ください。
要件
「カメレオンノーツ」では特定の場所に置いたブロックの情報を元にAI作曲を行う作品です。
そのため、以下の要件を満たす入力装置を開発する必要がありました。
- ブロックは1つ1つ異なる情報を持つ(識別が可能)
- 特定の場所に置いたブロックの情報を読み取ることができる
- ブロックを取り除いたことを検出できる
- 上記を4箇所同時に行うことができる
異なる情報を持たせるという要件にはRFIDタグを、置く・取り除くという要件にはRFIDリーダーを、それを制御するデバイスには昨年の出展作品でも実績のあるM5 Stackを利用することにしました。
利用する機器は以下です。
全て繋いでもデバイス構成は非常にシンプルです。
この記事ではこれらの機器を利用する前提での実装について説明します。
※ArduinoやM5Stackの基本的な使い方や実装についての説明は割愛します
RFIDタグについて
デバイスやセンサーを購入する際、RFIDタグも購入しました。
WS1850Sで利用できるRFIDタグの規格を調べたところ ISO14443A
というプロトコルに対応しているものは読み書きができるとのことだったので以下のようなスペックのタグを購入しました。
- NFCチップ : NTAG213
- プロトコル : ISO14443A
- 周波数:13.56MHZ
- メモリサイズ:144バイト
- 読み取り範囲:1~5cm
- 有効期間:100,000回
- サイズ:10x20mm
正しくタグが使えるかを確認するため、NFCの読み書きが行えるアプリを使ってデータの書き込みや読み取りが行えることを無事確認しました。
MIFARE Ultralight C メモリ構成
NFC読み取りアプリでタグを読み込んだ際、このタグが「MIFARE Ultralight C」という規格であることが分かりました。この規格では以下のようなメモリ構成となっているようです。
- ページ0~3
- UID (Unique Identifier)や固定データが格納されている領域
- 読み取り専用
- ページ4~39
- ユーザデータ領域
- 書き込みと読み取りが可能
- ページ40以降
- 認証データや鍵などのセキュリティ関連領域
- その他
- 1ページページあたり4バイトのサイズとなっていてその単位で読み書きを行う必要がある
RFIDタグの読み書き
RFIDリーダーを使った読み取り
では実際にRFIDリーダーからの読み込みを実装します。
やりたいことは複数のリーダーからの読み取りですが、最初は1つだけ読み込みます。
今回利用するWS1850SというリーダーはMFRC522_I2Cというライブラリを使って読み取ることができます。
#include <M5Unified.h>
#include <Wire.h>
#include <MFRC522_I2C.h>
#define WIRE Wire
#define RFID_ADDRESS 0x28
#define PIN_RESET 12
MFRC522_I2C mfrc522(RFID_ADDRESS, PIN_RESET, &Wire);
void begin() {
// M5Stack初期化
auto cfg = M5.config();
M5.begin(cfg);
mfrc522.PCD_Init(); // RFIDユニット初期化
}
void loop() {
if (mfrc522.PICC_IsNewCardPresent()) {
if (mfrc522.PICC_ReadCardSerial()) {
String data = readUserMemory();
Serial.print(data);
}
}
}
// MIFARE Ultralightのユーザメモリを読み取る
String readUserMemory() {
MFRC522_I2C::StatusCode status;
byte buffer[18]; // 読込みデータ16byte + エラーチェック用CRC用2byte分の配列を準備
byte bufferSize = sizeof(buffer);
String data = "";
for (byte page = 4; page <= 8; page += 4) {
status = (MFRC522_I2C::StatusCode)mfrc522.MIFARE_Read(page, buffer, &bufferSize);
if (status != MFRC522_I2C::STATUS_OK) {
Serial.printf("Reading failed on page : %d\n", page);
return "";
}
for (byte i = 0; i < 16; i++) {
data += char(buffer[i]);
}
}
return data;
}
このコードで実際に動かしてRFIDのタグを読み込んだところ、前述のNFC読み書きスマホアプリで設定したデータが読み取れる!と思ったのですが、何度やっても先頭数文字が文字化けしてしまいました。
ユーザデータ領域の先頭に意図しない情報が書き込まれているのですが、この内容が書き込むたびに変わるため何らかの可変長データが入っているようでした。前述の通り本来であればページ4~39にユーザーデータが格納されているのが期待値なのでこれは意図した結果ではありません。
RFIDリーダーを使った書き込み
実装した読みコードに誤りがあるのか、原因切り分けのために一度書き込み処理を実装してみることにしました。
void loop() {
if (M5.BtnA.wasClicked()) {
writeUserMemory("this is a pen.");
}
}
// MIFARE Ultralightのユーザメモリに書き込む
bool writeMifareUltralight(String dataToWrite) {
byte buffer[4];
byte dataLength = dataToWrite.length();
byte index = 0;
for (byte page = 4; page <= 39; page++) {
memset(buffer, 0x00, sizeof(buffer));
for (byte i = 0; i < 4; i++) {
if (index < dataLength) {
buffer[i] = dataToWrite[index++];
} else {
buffer[i] = 0x00;
}
}
if (!writePages(page, buffer)) {
return false;
}
}
return true;
}
bool writePages(byte page, byte *data) {
MFRC522_I2C::StatusCode status;
byte buffer[16]; // 16バイトのバッファを用意
memset(buffer, 0x00, sizeof(buffer)); // バッファをクリア
memcpy(buffer, data, 4); // 目的のページに4バイトのデータを書き込む
// 16バイト(4ページ)を一度に書き込む
status = (MFRC522_I2C::StatusCode)mfrc522.MIFARE_Write(page, buffer, 16);
if (status != MFRC522_I2C::STATUS_OK) {
Serial.print("MIFARE_Write() failed on page: ");
Serial.println(page);
Serial.print("Reason: ");
Serial.println(mfrc522.GetStatusCodeName(status));
return false;
}
return true;
}
書き込みする単位等に少し工夫が必要でしたが、この書き込み処理でRFIDタグに書き込みを行い、先ほど実装した処理での読み込みを行ったところ、無事に書き込んだ内容を読み取ることができていました!
原因の深掘りまではしていないですが、利用したスマホアプリ側が独自にユーザデータ領域の先頭に可変長のデータを入れていたようです。気軽にタグの読み書きができるスマホアプリをあまり疑っていなかったのでここに至るまでに結構試行錯誤がありました。。。
別な規格のRFIDタグも読み取り可能にする
開発をしている中で最初に買ったタグが足りなくなってきたので追加で買うことにしました。
前回同様 ISO14443A
に対応しているタグである前提、且つタグによる使い勝手を検証したかったので前回とは別のタグを選定したのですが、これがまた問題を生むこととなります。
早速購入したタグに対して書き込みを行おうと思ったのですが、書き込みエラーが起きました。
改めてこのタグの規格を見ると「MIFARE Classic 1K」となっており、前回の「MIFARE Ultralight C」とは異なる規格でした。これはもう捨てるしかないか・・・という考えも一瞬頭をよぎりましたが、めげずに原因を調査します。
まずは「MIFARE Classic 1K」のメモリ構造を調べてみます。
- 総容量: 1KB (1024バイト)
- セクター数: 16セクター
- セクター0 (ブロック0~3)
- ブロック0
- カードのUID(Unique Identifier)やメーカー情報が格納されている
- 読み取り専用
- ブロック1~2
- ユーザデータ領域
- 書き込みと読み取りが可能
- ブロック3
- セクタートレーラ
- ブロック0
- セクター1~15 (ブロック4~63)
- ブロック0~2
- ユーザデータ領域
- 書き込みと読み取りが可能
- ブロック3: セクタートレーラ
- ブロック0~2
- セクター0 (ブロック0~3)
- ブロック数: 各セクターは4つのブロックを持つ (16セクター × 4ブロック = 64ブロック)
- ブロックサイズ: 各ブロックは16バイト
- アクセス制御: 各セクターにある「セクタートレーラ」で管理
完全にこれが原因です。
なんかもう構造が全然別物なので「MIFARE Ultralight C」とは別の実装が必要になります。
やっぱりもう捨ててしまおうかと思いましたが、もう少し調べてみるとライブラリ側で規格の取得ができるため、処理を分岐することができそうです。
// ユーザーメモリの読み取り(規格の違いを吸収する)
String readUserMemory() {
// カードの種類を判定
MFRC522_I2C::PICC_Type cardType = (MFRC522_I2C::PICC_Type)mfrc522.PICC_GetType(mfrc522.uid.sak);
if (cardType == MFRC522_I2C::PICC_TYPE_MIFARE_1K) {
Serial.println("MIFARE Classic 1K card detected");
return readMifareClassic(1); // MIFARE Classicのブロック1を読み取り
} else if (cardType == MFRC522_I2C::PICC_TYPE_MIFARE_UL) {
Serial.println("MIFARE Ultralight card detected");
return readMifareUltralight(); // MIFARE Ultralightのデータを読み取り
} else {
Serial.println("Unsupported card type");
return "";
}
}
// MIFARE Classic 1Kのユーザメモリを読み取る
String readMifareClassic(int block) {
MFRC522_I2C::StatusCode status;
MFRC522_I2C::MIFARE_Key key;
// デフォルトのキー(全て0xFF)
for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
// 認証
status = (MFRC522_I2C::StatusCode)mfrc522.PCD_Authenticate(
MFRC522_I2C::PICC_CMD_MF_AUTH_KEY_A, block, &key, &(mfrc522.uid)
);
if (status != MFRC522_I2C::STATUS_OK) {
Serial.print("Authentication failed for block: ");
Serial.println(block);
Serial.println(mfrc522.GetStatusCodeName(status));
finalizeMifareClassic();
return "";
}
// データの読み取り
byte buffer[18];
byte bufferSize = sizeof(buffer);
status = (MFRC522_I2C::StatusCode)mfrc522.MIFARE_Read(block, buffer, &bufferSize);
if (status != MFRC522_I2C::STATUS_OK) {
Serial.print("Read failed on block: ");
Serial.println(block);
Serial.println(mfrc522.GetStatusCodeName(status));
finalizeMifareClassic();
return "";
}
// 読み取ったデータをStringに変換
String data = "";
for (byte i = 0; i < 16; i++) {
data += char(buffer[i]);
}
finalizeMifareClassic();
return data;
}
void finalizeMifareClassic() {
// Mifare Classic 1kは連続読み込みのために必ずこれらを実施する必要がある
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
mfrc522.PCD_Init();
}
String readMifareUltralight() {
// 省略
}
「MIFARE Classic 1K」は「MIFARE Ultralight」に比べてデータ容量が大きいだけではなく、セキュリティ面での考慮があるため、認証処理や認証に伴う追加の処理が必要でした。データを読み込む前に規格で分岐させることで読み込み処理の中で規格によるロジックの差異を吸収をすることができました。途中で諦めてタグを捨てなくて本当に良かったです。今回はこの二つの規格のみの対応ですが、同様に他の規格も必要に応じて実装を追加する事で対応できそうです。
複数のRFIDリーダーの制御
ようやくRFIDタグの読み書きができるようになったため、目的である4つのRFIDリーダーの制御を実装していきます。
TCA9548AというハブはClosedCube TCA9548AというライブラリとWireというI2C制御ライブラリを使って利用します。
#include <Wire.h>
#include "ClosedCube_TCA9548A.h"
#include "TCA9548A.h"
#define WIRE Wire
#define PaHub_I2C_ADDRESS 0x70
#define RFID_READER_COUNT 4 // 利用するRFIDリーダーの数
ClosedCube::Wired::TCA9548A tca;
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
setupRfId();
}
void loop() {
readAllRfid()
delay(100);
}
void setupRfId() {
Wire.begin();
Wire.setClock(100000);
tca.address(PaHub_I2C_ADDRESS);
for (uint8_t t = 0; t < RFID_READER_COUNT; t++) {
tcaselect(t);
Wire.beginTransmission(RFID_ADDRESS);
if (Wire.endTransmission() == 0) {
mfrc522.PCD_Init(); // RFID初期化
}
}
delay(500);
}
void tcaselect(uint8_t i) {
if (i >= RFID_READER_COUNT) return;
Wire.beginTransmission(PaHub_I2C_ADDRESS);
Wire.write(1 << i); // これを呼ぶことでmfrc522が参照するリーダーを切り替える
Wire.endTransmission();
}
void readAllRfid() {
for (int channel = 0; channel < RFID_READER_COUNT; channel++) {
tcaselect(channel);
String data = readUserMemory();
}
}
これで接続しているRFID4つのデータを読むことができました。
ポイントは Wire.write
の箇所で、これを呼び出すとmfrc522のインスタンスが参照するリーダーが切り替わるため、mfrc522のインスタンスの状態を意識することなく処理をすることができます。ループ処理と組み合わせることで どのリーダーがどんな値を読み込んだ を判別することができます。
読み取り精度の改善
ここまでの実装で、冒頭に挙げた要件を満たせました。
と、思っていたのですが、使っていく中でリーダーにタグを置いた時に反応しないこともあれば、リーダーからタグを離しても検出できないことがあるなど、タグの読み取り精度に課題があることがわかりました。
もう一度読み取り処理の呼び出しを見てみます。
void loop() {
if (mfrc522.PICC_IsNewCardPresent()) {
if (mfrc522.PICC_ReadCardSerial()) {
String data = readUserMemory();
Serial.print(data);
}
}
}
ここで使っている MFRC522_I2C.PICC_IsNewCardPresent()
と MFRC522_I2C.PICC_ReadCardSerial()
はRFIDリーダーを使う際のオマジナイのように使っていましたが、改めて役割を確認していきます。
MFRC522_I2C.PICC_IsNewCardPresent()
- 役割
- RFIDリーダーの電波圏内に、新しいRFIDカードが近づいたかをチェックする
- 戻り値
- true: 新しいカードがリーダーの範囲に入った
- false: カードがない、または新しいカードではない
MFRC522_I2C.PICC_ReadCardSerial()
- 役割
- 現在リーダーの範囲内にあるカードのUID(カード固有のID)を読み取る
- 動作条件
- このメソッドを呼び出す前に、PICC_IsNewCardPresent()でカードが検出されている必要がある
- 戻り値
- true: UIDの読み取りが成功
- false: UIDの読み取りに失敗
PICC_ReadCardSerial()は前提としてPICC_IsNewCardPresent()が呼ばれていないと実行できない仕様であることはわかったのですが、実行していくうちに不思議な挙動を見つけました。PICC_IsNewCardPresent()でタグを読み取った状態(trueを返す)のまま、もう一度PICC_IsNewCardPresent()を呼び出すとfalseが返るというものです。どうやらこの動作はRFIDリーダー側が返している状態によって起きているようで、ライブラリ側で改修できないいわばリーダーの仕様とも言えるようです。ただ、PICC_IsNewCardPresent()はtrue/falseを交互に返し、実際に読み込んでいるかはPICC_ReadCardSerial()にて確認すれば良いため、下記のように修正しました。
bool hasCard() {
// PICC_IsNewCardPresentを呼び出して検出を試みる
if (mfrc522.PICC_IsNewCardPresent()) {
if (mfrc522.PICC_ReadCardSerial()) {
return true;
}
} else {
// 2回目の呼び出しで再度検出を試みる
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
return true;
}
}
// 2回PICC_IsNewCardPresentを呼び出してもreturnしていない場合はカードがないと判断
return false;
}
String readUserMemory() {
if (!hasCard()) {
// カードが読み取れない場合は読み取り処理を行わない
return "";
}
// 以下読み取り処理
}
上記の修正によりRFIDタグを置いたり取り除いた状態を正確に検出することができるようになり、無事カメレオンノーツの入力装置が完成しました。
まとめ
カメレオンノーツ製作の裏でRFIDからの読み取りで思いのほか壁にあたり、試行錯誤したナレッジをまとめました。M5Stackを使うとセンサーも充実して知識が少なくても物を作るハードルが非常に下がっていると感じますが、今回のような解決すべき細かい問題は数多く潜んでいます。あまり一般的な使い方ではない部分もありますが、今後同じ問題にぶつかった方の一助になれば幸いです。ここまでお読みいただきありがとうございました。
明日の レコチョク Advent Calendar 2024 は2日目「君のSPAがもっさりなのは○○のせい」となります。お楽しみに!
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。