Работа с Arduino и RFID MFRC522

Автор: 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 конфигурация блока безопасности заблокирует еще и настройки доступа к блокам данных!

Некоторые варианты необратимы, но не переживайте – у вас есть целых 16 попыток! По одной на каждый сектор.

В итоге 3 байта настроек доступа приняли следующие значения: 0x7F 0x07 0x88, байт USER может быть любой.

Важно: изначально ключи A и B от всех секторов метки содержат значение 0xFFFFFFFFFFFF, так что если хотите защитить данные в ваших метках, не забывайте сменить оба ключа! Кстати, нулевой блок и соответственно UID свободно читаются из метки, даже если нулевой сектор был заблокирован секретными ключами.
Ну и последнее, что касается меток – реальный объем пользовательской памяти. Если не создавать костыли, а использовать только блоки для хранения данных – (по 3 блока в 15-ти секторах, и 2 блока в нулевом секторе) получаем 47 доступных блоков по 16 байт или 752 байта, что тоже неплохо.

Начало работы


Подключили модуль, распаковали свежие метки - начинаем работать!
Прежде всего создаем необходимые объекты, инициализируем интерфейс SPI и MFRC522, не забываем про ключ доступа. Изначально все ключи состоят из FF-ок, так что “наполняем” ключ.
#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 и вывод в порт приведен ниже:

Чтение UID
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну

  for (byte i = 0; i < 6; i++) {   // Наполняем ключ
    key.keyByte[i] = 0xFF;         // Ключ по умолчанию 0xFFFFFFFFFFFF
  }
}

void loop() {
  // Занимаемся чем угодно
  
  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  Serial.print("UID: ");
  for (uint8_t i = 0; i < 4; i++) {           // Цикл на 4 итерации
    Serial.print("0x");                       // В формате HEX
    Serial.print(rfid.uid.uidByte[i], HEX);   // Выводим UID по байтам
    Serial.print(", ");
  }
  Serial.println("");
}
После успешного чтения метки, ее 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 из новой метки со стандартными ключами выглядит так:

Чтение блока
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну
  for (byte i = 0; i < 6; i++) {   // Наполняем ключ
    key.keyByte[i] = 0xFF;         // Ключ по умолчанию 0xFFFFFFFFFFFF
  }
}

void loop() {
  // Занимаемся чем угодно

  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  /* Аутентификация сектора, указываем блок безопасности #7 и ключ 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;
  }

  /* Чтение блока, указываем блок данных #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;
  }

  /* Выводим содержимое блока в Serial */
  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();
}

После поднесения метки в мониторе порта должны отобразиться 16 байт - содержимое прочитанного блока.

Запись блока


При записи все 16 байт записываются в блок единовременно, для чего должен быть заранее подготовлен байтовый массив на 16 Байт, например такой:

uint8_t dataToWrite[16] = {
  0x00, 0x00, 0x00, 0x00,
  0xAA, 0xBB, 0xCC, 0xDD,
  0xAA, 0xBB, 0xCC, 0xDD,
  0x00, 0x00, 0x00, 0x00
};

Как в случае и с чтением, сектор в котором находится нужный блок нужно предварительно аутентифицировать, об этом уже сказано выше, так что переходим непосредственно к записи. Запись производится методом MIFARE_Write(), в отличии от MIFARE_Read() тут все несколько проще - указываем записываемый блок, массив и число 16 (количество байт на запись):

status = rfid.MIFARE_Write(6, dataToWrite, 16); // Пишем массив в блок 6
if (status != MFRC522::STATUS_OK) {             // Если не окэй
  Serial.println("Write error");                // Выводим ошибку
  return;
}

Итоговый скетч записи массива в блок 6 выглядит так:

Запись блока
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну
  for (byte i = 0; i < 6; i++) {   // Наполняем ключ
    key.keyByte[i] = 0xFF;         // Ключ по умолчанию 0xFFFFFFFFFFFF
  }
}

void loop() {
  // Занимаемся чем угодно

  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  /* Аутентификация сектора, указываем блок безопасности #7 и ключ 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;
  }

  /* Запись блока, указываем блок данных #6 */
  uint8_t dataToWrite[16] = {                    // Массив на запись в блок
    0x00, 0x00, 0x00, 0x00,
    0xAA, 0xBB, 0xCC, 0xDD,
    0xAA, 0xBB, 0xCC, 0xDD,
    0x00, 0x00, 0x00, 0x00
  };

  status = rfid.MIFARE_Write(6, dataToWrite, 16); // Пишем массив в блок 6
  if (status != MFRC522::STATUS_OK) {             // Если не окэй
    Serial.println("Write error");                // Выводим ошибку
    return;
  }

  Serial.println("Write OK");                     // Завершаем работу с меткой
  rfid.PICC_HaltA();                              
  rfid.PCD_StopCrypto1();
}

После поднесения метки в мониторе порта должно отобразиться сообщение "Write OK".

К слову ничто не мешает нам объеденить запись и чтение в одном скетче:

Запись + чтение блока
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну
  for (byte i = 0; i < 6; i++) {   // Наполняем ключ
    key.keyByte[i] = 0xFF;         // Ключ по умолчанию 0xFFFFFFFFFFFF
  }
}

void loop() {
  // Занимаемся чем угодно

  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  /* Аутентификация сектора, указываем блок безопасности #7 и ключ 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;
  }

  /* Запись блока, указываем блок данных #6 */
  uint8_t dataToWrite[16] = {                    // Массив на запись в блок
    0x00, 0x00, 0x00, 0x00,
    0xAA, 0xBB, 0xCC, 0xDD,
    0xAA, 0xBB, 0xCC, 0xDD,
    0x00, 0x00, 0x00, 0x00
  };

  status = rfid.MIFARE_Write(6, dataToWrite, 16); // Пишем массив в блок 6
  if (status != MFRC522::STATUS_OK) {             // Если не окэй
    Serial.println("Write error");                // Выводим ошибку
    return;
  }

  /* Чтение блока, указываем блок данных #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;
  }

  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();
}

Смена ключей безопасности и настройка блоков


Если вы хотите не только уметь читать и писать данные в метки, но и защить хранящуюся там информацию от стороннего вмешательства - необходимо сменить ключи доступа как минимум от тех секторов, в которых вы собираетесь хранить данные, однако в идеале установить новые ключи для всех секторов, чтобы усложнить задачу потенциальным взломащикам.

Для того, чтобы изменить ключи доступа к сектору, необходимо перезаписать блок безопасности, ответственный за данный сектор. В наших примерах фигурировал блок данных под номером и соответственно блок безопасности под номером 7. Задача по смене ключей безопасности сводится к следующим шагам:

  1. Создать байтовый массив на 16 ячеек, включающий:
    • Ключ A
    • Байты настроек доступа
    • Ключ B
  2. Провести аутентификацию выбранного сектора, пока что используя стандартный ключ 0xFFFFFFFFFFFF
  3. Произвести запись подготовленного массива в блок безопасности, в точности так же, как в случае с обычным блоком.

После этого ключи безопасности будут изменены, а мы сможем безопасно хранить данные в одном или нескольких блоках выбранного сектора! Сейчас покажу по шагам.

  1.  Для создания образа блока безопасности (содержимого массива) придется вернуться вверх по тексту и вспомнить, что при выбранных нами настройках сектора, байты доступа получили значения 0x7F 0x07 0x88. Осталось придумать ключи доступа, имеющие длину 6 байт. Я буду использовать ключ 0xABABABABABAB, данный ключ не является безопасным и подходит исключительно для демонстрации! Так же напоминаю, что ключи A и B будут идентичны, однако использоваться будет только ключ B.

  1. 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
    };

    Думаю по структуре массива все понятно и вполне наглядно.

  2. Производим аутентификацию сектора, как и раньше. Напомнинаю, что для этого используется блок безопасности 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;
    }
  3.  После чего записываем дамп, в тот же блок безопасности - под номером 7:
    status = rfid.MIFARE_Write(7, secBlockDump, 16); // Пишем массив в блок 6
    if (status != MFRC522::STATUS_OK) {              // Если не окэй
      Serial.println("Write error");                 // Выводим ошибку
      return;
    }

Итоговый скетч записи новых ключей и настроек доступа:

Запись блока безопасности
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну
  for (byte i = 0; i < 6; i++) {   // Наполняем ключ
    key.keyByte[i] = 0xFF;         // Ключ по умолчанию 0xFFFFFFFFFFFF
  }
}

void loop() {
  // Занимаемся чем угодно

  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  /* Аутентификация сектора, указываем блок безопасности #7 и ключ 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;
  }

  /* Запись блока, указываем блок безопасности #7 */
  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
  };

  status = rfid.MIFARE_Write(7, secBlockDump, 16); // Пишем массив в блок 7
  if (status != MFRC522::STATUS_OK) {              // Если не окэй
    Serial.println("Write error");                 // Выводим ошибку
    return;
  }

  Serial.println("Write OK");                     // Завершаем работу с меткой
  rfid.PICC_HaltA();                              
  rfid.PCD_StopCrypto1();
}
Важно: при повторном поднесении метки мы неизбежно получим ошибку аутентификации, это связано с тем, что стандартный ключ больше не подходит к перенастроенному сектору!

Не забываем сменить "наполнение" ключа в 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 и увеличивается при каждом поднесении:

Инкремент при поднесении
#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();                 // Инициализация модуля
  rfid.PCD_SetAntennaGain(rfid.RxGain_max);  // Установка усиления антенны
  rfid.PCD_AntennaOff();           // Перезагружаем антенну
  rfid.PCD_AntennaOn();            // Включаем антенну

  for (byte i = 0; i < 6; i++) {   // Наполняем ключ B
    key.keyByte[i] = 0xAB;         // Пишем свой ключ
  }
}

void loop() {
  // Занимаемся чем угодно

  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();                      // Инициализируем заного
  }

  if (!rfid.PICC_IsNewCardPresent()) return;  // Если новая метка не поднесена - вернуться в начало loop
  if (!rfid.PICC_ReadCardSerial()) return;    // Если метка не читается - вернуться в начало loop

  /* Аутентификация сектора, указываем блок безопасности #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;
  }

  /* Чтение блока, указываем блок данных #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;
  }

  /* Выводим количество поднесений метки */
  Serial.print("Count: ");                         // Выводим количество
  Serial.println(dataBlock[0]);                    // Хранится в нулевом байте массива
  dataBlock[0]++;                                  // Инкремент

  /* Запись блока, указываем блок данных #6 */
  status = rfid.MIFARE_Write(6, dataBlock, 16);    // Пишем массив в блок 6
  if (status != MFRC522::STATUS_OK) {              // Если не окэй
    Serial.println("Write error");                 // Выводим ошибку
    return;
  }

  rfid.PICC_HaltA();                               // Завершаем работу с меткой
  rfid.PCD_StopCrypto1();
}

Количество поднесений ограничено 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.

5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
guest

50 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх