Автор: Nich1con
Технология RFID (Радиочастотная идентификация) позволяет при помощи радиосигнала быстро и безопасно передавать данные между специальными “считывателями” и “метками” – карточками, брелоками, браслетами и т.д. на небольшом расстоянии.
Одно из широко известных развитий технологии – NFC, при помощи которого можно оплачивать покупки или подключать устройства бесконтактно. Нам же доступны менее сложные, но не менее полезные и интересные применения, о которых будет сказано ниже.
Применение
Комплект RFID модуль + метки может быть использован:
- Как часть самодельных охранных систем
- При создании простых электронных замков (метка является ключом)
- В системах контроля доступа (однократный, многократный пропуск)
- В качестве электронного “кошелька” внутри собственного предприятия
- В роли интерактивного предмета в квестах и т.д.
Железо
RFID работает в нескольких частотных диапазонах, в свою очередь RFID модули и метки можно поделить на низкочастотные “LF” (125 кгц) и высокочастотные “HF” (13,56 MHz), существуют так же и ультравысокочастотные “UHF”, но они нас не интересуют.
Наиболее распространенные RFID Arduino-модули основаны на микросхеме MFRC522, работающей с HF метками 13,56 МГц. Поиск модулей и библиотек производится по этому же имени.
Существует два типа модулей MFRC522, с которыми вы скорее всего столкнетесь:
Отвечая на главный вопрос – не смотря на значительную разницу в размере, ощутимой разницы в работе модулей нет, можно брать любой. В комплекте к модулю как правило уже идет несколько меток.
Библиотеки
Для работы с модулем MFRC522 понадобится библиотека https://github.com/miguelbalboa/rfid. Библиотека тяжелая, индусская и имеет немало проблем, но достойных альтернатив просто нет – несколько других “облегченных” библиотек значительно уступают в функциональности и удобстве использования.
Подключение
Модуль MFRC522 подключается по аппаратному интерфейсу SPI, выбранная библиотека предоставляет следующую таблицу подключения к Arduino:
Сигнал | Модуль MFRC522 | UNO/NANO | Leonardo | Pro micro | Mega |
Reset | RST | D9 | RST/ISCP-5 | RST | D5 |
Chip select | SDA (SS) | D10 | D10 | D10 | D53 |
MOSI | MOSI | D11 | ISCP-4 | D16 | D51 |
MISO | MISO | D12 | ISCP-1 | D14 | D50 |
SCK | SCK | D13 | ISCP-3 | D15 | D52 |
- Контакты модуля RST и SDA (SS) указываются в скетче – можно использовать любые.
- У Leonardo подключение производится к 6-ти контактному ICSP разъему программатора.
Пример подключения модуля к Arduino Nano:
О RFID метках
Прежде всего самая сложная для понимания, но и самая важная часть – работа с RFID метками. В комплекте с модулем идут пара меток MIFARE Classic 1K, как понятно из названия – на 1 килобайт (на самом деле меньше, но об этом позже).
Чтобы изучить организацию памяти такой метки, можно воспользоваться примером из библиотеки, открыв Примеры > MFRC522 > DumpInfo. Однако для вашего удобства я подготовил вот такую карту:
Обратите внимание – память организована в виде 16-ти секторов, по 4 блока каждый. Итого – 64 блока по 16 Байт, как раз набегает 1 Килобайт. Деление по секторам носит скорее условный характер, так как адресация в памяти будет производиться по блокам.
Все сектора кроме нулевого имеют одинаковое строение – 3 блока данных + 1 блок безопасности, так называемый sector trailer. Каждый из этих блоков может быть прочитан и перезаписан (при соблюдении условий), исключение составляет нулевой блок (сектор 0).
Нулевой блок хранит в себе уникальный ID “UID”, тип метки и прочую информацию, записанную заводом-изготовителем. Нулевой сектор не может быть перезаписан, если речь идет о “классических” метках, к которым относятся комплектные с модулем. Таким образом UID позволяет отличить две с виду идентичные метки. UID как правило состоит из 4х байт, свободно считываемых из метки. Важно: китайский рынок может предложить вам “перезаписываемые” метки, UID в которых можно менять, путем перезаписи нулевого блока. Если в вашей системе используется только UID – учтите возможность очень простого копирования UID в метки-болванки (в том числе злоумышленниками).
Блоком безопасности является каждый 4й блок, каждый блок безопасности отвечает за свой сектор (предыдущие 3 блока данных) – он хранит 2 ключа доступа по 6 байт (ключи A и B), а также специальные “Access bits” (Биты доступа), грубо говоря настройки доступа. Ключи A и B могут быть использованы для аутентификации и последующего доступа к блокам данных в пределах сектора. То есть да, для того чтобы получить доступ к любому из блоков внутри сектора необходимо “разблокировать” этот сектор, при помощи одного из ключей.
Поэтому будьте уверены, если производитель позаботился о смене секретных ключей в своих RFID метках – скопировать или как-нибудь изменить содержимое штатными средствами вы уже не сможете, а ведь так хотелось? Идем дальше.
Биты доступа позволяют настроить условия доступа и возможности работы каждого блока в отдельности (каждого блока данных + блока безопасности). Наилучшим инструментом в работе с метками MIFARE Classic 1K является вот этот онлайн-калькулятор http://calc.gmss.ru/Mifare1k/. Если хотите разобраться чуть глубже – обязательно полистайте и опробуйте.
Я же хочу сэкономить ваше время, поэтому сразу уточню, что наиболее удачным решением в большинстве ситуаций будет оставить блоки данных в состоянии transport configuration, то есть по умолчанию.
Однако есть возможность настроить блоки на некоторые интересные сценарии, например защитить от записи (конфигурация 1-0-1 или 0-1-0). Или же сделать так, что прочитать блок можно при помощи как ключа A, так и ключа B, а вот для записи обязательно понадобится ключ B (конфигурация 1-0-0), в таком случае можно ограничить права некоторого оборудования и сделать систему безопаснее. И да, конфигурация 1-1-1 превращает блок в кирпич (обратимо).
В примерах ниже мы будем использовать конфигурацию блоков данных по умолчанию (0-0-0) и следующие принципы:
- Создаем ключ B, значение которого знаем только мы, длина ключа – 6 Байт.
- Ключ A будет полностью аналогичен ключу B, однако он не будет использоваться.
- Биты доступа для блоков безопасности будем использовать в конфигурации 0-1-1
Таким образом для всех операций с меткой применяется только ключ B, который невозможно считать из метки (впрочем, как и ключ A), даже если сектор предварительно разблокирован. Если хотите намертво зашить ключи A и B в блок безопасности – подойдет конфигурация 1-0-1, поменять будет уже невозможно. Ну а последняя 1-1-1 конфигурация блока безопасности заблокирует еще и настройки доступа к блокам данных!
В итоге 3 байта настроек доступа приняли следующие значения: 0x7F 0x07 0x88, байт USER может быть любой.
Начало работы
#include <SPI.h> #include <MFRC522.h> #define RST_PIN 9 // Пин rfid модуля RST #define SS_PIN 10 // Пин rfid модуля SS MFRC522 rfid(SS_PIN, RST_PIN); // Объект rfid модуля MFRC522::MIFARE_Key key; // Объект ключа MFRC522::StatusCode status; // Объект статуса void setup() { Serial.begin(9600); // Инициализация Serial SPI.begin(); // Инициализация SPI rfid.PCD_Init(); // Инициализация модуля for (byte i = 0; i < 6; i++) { // Наполняем ключ key.keyByte[i] = 0xFF; // Ключ по умолчанию 0xFFFFFFFFFFFF } } void loop() {}
Работу с RFID модулем удобнее всего производить в конце главного цикла программы, сейчас поймете почему. Для отслеживания поднесенной метки библиотека предлагает использовать пару методов с замысловатыми названиями:
if (!rfid.PICC_IsNewCardPresent()) return; // Если новая метка не поднесена - вернуться в начало loop if (!rfid.PICC_ReadCardSerial()) return; // Если метка не читается - вернуться в начало loop
Если поднесенная метка успешно считана – программа идет дальше, в противном случае происходит возврат в начало главного цикла, а весь блок кода связанный с RFID пропускается.
В итоге наш скетч имеет следующую структуру:
#include <SPI.h> #include <MFRC522.h> #define RST_PIN 9 // Пин rfid модуля RST #define SS_PIN 10 // Пин rfid модуля SS MFRC522 rfid(SS_PIN, RST_PIN); // Объект rfid модуля MFRC522::MIFARE_Key key; // Объект ключа MFRC522::StatusCode status; // Объект статуса void setup() { Serial.begin(9600); // Инициализация Serial SPI.begin(); // Инициализация SPI rfid.PCD_Init(); // Инициализация модуля for (byte i = 0; i < 6; i++) { // Наполняем ключ key.keyByte[i] = 0xFF; // Ключ по умолчанию 0xFFFFFFFFFFFF } } void loop() { // Занимаемся чем угодно if (!rfid.PICC_IsNewCardPresent()) return; // Если новая метка не поднесена - вернуться в начало loop if (!rfid.PICC_ReadCardSerial()) return; // Если метка не читается - вернуться в начало loop // Работаем с RFID }
Теперь давайте идти по порядку, осваивая основные методы для работы с RFID.
Чтение UID
Самое простое, что можно сделать с RFID меткой – прочитать UID, в некоторых системах уже этого функционала вполне достаточно, например – в простых электронных замках. Как уже сказано ранее, некоторые метки позволяют сменить UID, в свою очередь запретить считать UID из ваших меток не получится - учтите это при разработке.
Так или иначе пример чтения UID и вывод в порт приведен ниже:
rfid.uid.uidByte[]
который можно прочитать и использовать.
Чтение блока
Если чтением UID ваш интерес не ограничился – давайте узнаем, как читать блок данных из метки. Прежде чем что-либо читать или писать, необходимо аутентифицировать (разблокировать) сектор, в котором находится интересующий нас блок.
В примерах ниже будем работать с блоком под номером 6 (сектор 1), за первый сектор и соответственно блоки 4, 5 и 6 отвечает блок безопасности под номером 7. То есть запомнили – блок данных 6, блок безопасности 7.
Аутентификацию сектора производит метод PCD_Authenticate()
, по умолчанию все манипуляции с меткой возможны при помощи ключа A, а сам процесс аутентификации выглядит следующим образом:
status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 7, &key, &(rfid.uid)); if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Auth error"); // Выводим ошибку return; }
Обратите внимание на …KEY_A в первом аргументе и цифру 7 вторым аргументом – это и есть номер блока безопасности. Отслеживать статус не обязательно, но крайне желательно.
После успешной аутентификации можно свободно манипулировать разблокированным сектором, в нашем случае будем читать содержимое методом MIFARE_Read()
. Читать нужно в байтовый массив, размером 18 (!) байт, чтение происходит из блока 6 (первый аргумент) и выглядит так:
uint8_t dataBlock[18]; // Буфер для чтения uint8_t size = sizeof(dataBlock); // Размер буфера status = rfid.MIFARE_Read(6, dataBlock, &size); // Читаем 6 блок в буфер if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Read error"); // Выводим ошибку return; }
Важно: Не смотря на то, что блоки имеют размер 16 байт, буферный массив создается на 18 байт, а количество байт на чтение передается именно в виде указателя на переменную. В противном случае чтение закончится ошибкой, примите это как факт.
В случае успешного чтения, выведем содержимое блока 6 в монитор порта, все как обычно:
Serial.print("Data:"); // Выводим 16 байт в формате HEX for (uint8_t i = 0; i < 16; i++) { Serial.print("0x"); Serial.print(dataBlock[i], HEX); Serial.print(", "); } Serial.println("");
А завершать работу с меткой нужно при помощи еще двух замысловатых методов:
rfid.PICC_HaltA(); // Завершаем работу с меткой rfid.PCD_StopCrypto1();
В итоге полный скетч для чтения блока 6 из новой метки со стандартными ключами выглядит так:
После поднесения метки в мониторе порта должны отобразиться 16 байт - содержимое прочитанного блока.
Запись блока
При записи все 16 байт записываются в блок единовременно, для чего должен быть заранее подготовлен байтовый массив на 16 Байт, например такой:
uint8_t dataToWrite[16] = { 0x00, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x00, 0x00, 0x00 };
Как в случае и с чтением, сектор в котором находится нужный блок 6 нужно предварительно аутентифицировать, об этом уже сказано выше, так что переходим непосредственно к записи. Запись производится методом MIFARE_Write()
, в отличии от MIFARE_Read()
тут все несколько проще - указываем записываемый блок, массив и число 16 (количество байт на запись):
status = rfid.MIFARE_Write(6, dataToWrite, 16); // Пишем массив в блок 6 if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Write error"); // Выводим ошибку return; }
Итоговый скетч записи массива в блок 6 выглядит так:
После поднесения метки в мониторе порта должно отобразиться сообщение "Write OK".
К слову ничто не мешает нам объеденить запись и чтение в одном скетче:
Смена ключей безопасности и настройка блоков
Если вы хотите не только уметь читать и писать данные в метки, но и защить хранящуюся там информацию от стороннего вмешательства - необходимо сменить ключи доступа как минимум от тех секторов, в которых вы собираетесь хранить данные, однако в идеале установить новые ключи для всех секторов, чтобы усложнить задачу потенциальным взломащикам.
Для того, чтобы изменить ключи доступа к сектору, необходимо перезаписать блок безопасности, ответственный за данный сектор. В наших примерах фигурировал блок данных под номером 6 и соответственно блок безопасности под номером 7. Задача по смене ключей безопасности сводится к следующим шагам:
- Создать байтовый массив на 16 ячеек, включающий:
- Ключ A
- Байты настроек доступа
- Ключ B
- Провести аутентификацию выбранного сектора, пока что используя стандартный ключ 0xFFFFFFFFFFFF
- Произвести запись подготовленного массива в блок безопасности, в точности так же, как в случае с обычным блоком.
После этого ключи безопасности будут изменены, а мы сможем безопасно хранить данные в одном или нескольких блоках выбранного сектора! Сейчас покажу по шагам.
- Для создания образа блока безопасности (содержимого массива) придется вернуться вверх по тексту и вспомнить, что при выбранных нами настройках сектора, байты доступа получили значения 0x7F 0x07 0x88. Осталось придумать ключи доступа, имеющие длину 6 байт. Я буду использовать ключ 0xABABABABABAB, данный ключ не является безопасным и подходит исключительно для демонстрации! Так же напоминаю, что ключи A и B будут идентичны, однако использоваться будет только ключ B.
- 1 После того, как мы определились с настройками сектора ("Access Bits") и ключами безопасности - создаем нужный байтовый массив:
uint8_t secBlockDump[16] = { // Дамп блока безопасности 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, // < Ключ A 0x7F, 0x07, 0x88, // < Access Bits 0xFF, // < User байт (любой) 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB // < Ключ B };
Думаю по структуре массива все понятно и вполне наглядно.
- Производим аутентификацию сектора, как и раньше. Напомнинаю, что для этого используется блок безопасности 7:
status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 7, &key, &(rfid.uid)); if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Auth error"); // Выводим ошибку return; }
- После чего записываем дамп, в тот же блок безопасности - под номером 7:
status = rfid.MIFARE_Write(7, secBlockDump, 16); // Пишем массив в блок 6 if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Write error"); // Выводим ошибку return; }
Итоговый скетч записи новых ключей и настроек доступа:
Не забываем сменить "наполнение" ключа в void setup(){}
:
for (byte i = 0; i < 6; i++) { // Наполняем ключ key.keyByte[i] = 0xAB; // Пишем свой ключ }
Так же напоминаю, что отныне для сектора 1 (блоков 4...7) мы используем только ключ B, соответственно и команда аутентификации отныне выглядит чуть иначе:
status = rfid.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, 7, &key, &(rfid.uid)); if (status != MFRC522::STATUS_OK) { // Если не окэй Serial.println("Auth error"); // Выводим ошибку return; }
Работа с защищенным сектором
Теперь, после того как мы сменили ключи безопасности от сектора 1, в качестве примера будем выводить количество поднесений метки к считывателю в последовательный порт. Количество поднесений хранится в нулевом байте защищенного блока 6 и увеличивается при каждом поднесении:
Количество поднесений ограничено 255, потому что используется лишь 1 Байт, но для примера это и не важно.
Инкремент и декремент
Наболее продвинутые могут обратить внимание на то, что пример выше не имеет смысла, поскольку метки уже имеют функционал инкремента и декремента, позволяющего использовать защищенный блок в качестве электронного кошелька.
Для реализации данного функционала библиотека имеет методы MIFARE_Increment()
и MIFARE_Decrement()
, однако независимо от установленных байтов доступа данный функционал не показал работоспособность на десятке меток и нескольких модулях. Возможно всему виной поддельные чипы MFRC522, установленные в модули. В любом случае перечисленные методы возвращают ошибку при поднесении метки, а потому и пример для работы не привожу.
Особенности и костыли
- Периодическая перезагрузка и повторная инициализация. В главный цикл программы добавляется таймер на millis() с периодом 500…3000 мс, внутри которого производится принудительный сброс и инициализация модуля. Данный код располагается в начале главного цикла программы. Данный костыль является наиболее эффективным в борьбе с зависанием модуля.
static uint32_t rebootTimer = millis(); // Важный костыль против зависания модуля! if (millis() - rebootTimer >= 1000) { // Таймер с периодом 1000 мс rebootTimer = millis(); // Обновляем таймер digitalWrite(RST_PIN, HIGH); // Сбрасываем модуль delayMicroseconds(2); // Ждем 2 мкс digitalWrite(RST_PIN, LOW); // Отпускаем сброс rfid.PCD_Init(); // Инициализируем заного }
- Принудительная установка усиления и перезагрузка антенны. Полезность сомнительная, однако по некоторым заявлениям библиотека не всегда корректно настраивает антенну после инициализации модуля. Решается добавлением соответствующих строк помимо инициализации модуля в функции
void setup(){}
:rfid.PCD_SetAntennaGain(rfid.RxGain_max); // Установка усиления антенны rfid.PCD_AntennaOff(); // Перезагружаем антенну rfid.PCD_AntennaOn(); // Включаем антенну
- Повторное чтение или второй шанс. В примерах выше мы использовали следующую конструкцию для отслеживания поднесенной метки:
if (!rfid.PICC_IsNewCardPresent()) return; // Если новая метка не поднесена - вернуться в начало loop if (!rfid.PICC_ReadCardSerial()) return; // Если метка не читается - вернуться в начало loop
Фокус заключается в добавлении еще одной попытке чтения метки, в случае неудачи. Таким образом наша конструкция принимает следующий вид:
if (!rfid.PICC_IsNewCardPresent()) return; // Если новая метка не поднесена - вернуться в начало loop if (!rfid.PICC_ReadCardSerial()) { // Если метка не читается - пробуем еще раз if (!rfid.PICC_ReadCardSerial()) return; // Все равно не читается - вернуться в начало loop }
Наиболее эффективным и важным является первый костыль, располагайте его перед работой с модулем, желательно в начале цикла void loop(){}
, но ни в коем случае не в прерывании таймера. Однако несмотря на то, что приведенные выше хитрости действительно работают и многократно повышают стабильность работы RFID модуля – в случае с электронными замками и прочими системами контроля доступа рекомендуется иметь резервные способы входа, помимо RFID.