Это уже третья статья из этой серии, предыдущие можно найти по ссылкам.
Flipper на минималках. Как мы делаем устройство для чтения и эмуляции ключей от домофонов…
Проект «Мультиключ». Как мы побеждали контактные ключи Metacom и Cyfral
Никак не доходили руки до написания этой статьи, точнее я её планировал написать после полноценного перевода устройства на esp32 c3, который никак не состоится.
Вкратце напомню, о чем этот проект и чем он закончился в прошлой статье. Мы разрабатываем компактное устройство для чтения, хранения, записи и эмуляции электронных ключей (которые чаще всего встречаются у нас в подъездах и на проходных). Изначально это был проект одного из моих учеников. Но в этом году, для участия во ВсОШ по робототехнике ему пришлось поменять тему работы, которая тоже довольно интересная, как-нибудь про неё тоже напишу). А я по наличию времени и энтузиазма продолжил добивать программную часть.
В прошлой статье мы перевели устройство на esp8266, что сделало его более производительным и решило проблему с памятью. У нас получилось прочитать и эмулировать контактные ключи dallas и русские Сyfral и Metacom. После этого мы решили перейти к бесконтактным ключам стандарта EmMarine.
❯ Считыватель
Бесконтактные ключи уже так просто, при помощи одного резистора, не прочитаешь, нужен детектор-генератор на 125 кГц. На этом этапе опять очень помог проект от Alex Malov EasyKeyDublicator. У него я взял схему детектора без изменений. И первые тесты производил на Arduino Nano.

У этой схемы выходит всего 3 вывода.
GND — это, естественно, земля.
DAT, это вывод детектора, его нам надо подцепить к компаратору или на аналоговый порт.
CL — это вывод электромагнитного генератора 125кГц.
Причем C1 и L1 — это колебательный контур, рассчитанный на частоту 125кГц. Можно использовать и другие компоненты, резонансная частота считается по формуле:

Катушку можно намотать руками, первую я так и изготовил.

Но у катушек, собранных подобным образом, точность работы оставляет желать лучшего. Поэтому я попробовал катушки, добытые из ключей и карт. К сожалению, этот вариант тоже не сработал. На ключах используются катушки на ХХ мкГн, для них нужен конденсатор на 560пФ. Собранная схема заработала, но качество считывания оказалось ещё хуже, чем на самодельных катушках. Думаю, это из-за того, что у ключа и у считывателя получились одинаковые катушки.
В итоге я пришел к китайским катушкам на 345 мкГн, которые оказались и компактными и наиболее точными.

К этим катушкам я собрал компактную схему считывателя, а ученики мне её размножили :)

❯ Разгоняем ШИМ
Когда считыватель был завершен, пришла пора заводить его на esp8266. Как я уже писал выше, для работы считывателя нужна несущая частота 125 мГц. У ардуины с этим практически нет проблем, а вот на esp8266 нет аппаратного ШИМ, только программный. Смотрим на ограничения, а там от 100 Гц до 1 кГц, и то с ограничениями при использовании wi-fi. Обидно. Сначала думал прикрутить отдельную шимку, но это не наш метод :)
Я попробовал занизить разрядность, и в тупую задать частоту 125000, а оно взяло и заработало. Важный момент, частота контроллера при этом выставлена на 160MHz.
void rfidACsetOn() {
//включаем генератор 125кГц
pinMode(FreqGen, OUTPUT); //Инициализируйте pin как выходной
analogWriteResolution(4);//разрядность 4 бита
analogWriteFreq(125000); //частота 125 кГц
analogWrite(FreqGen, 10); //скважность чуть больше 50%
}

❯ Чтение EmMarine
Дальше начался процесс переписывания кода чтения, для работы его на esp8266. Проблему долгой работы AnalogRead мы победили в прошлой статье, поэтому функцию analogReadFast()
я использую почти без изменений. Опять же, с достаточной скоростью она работает только при 160MHz.
uint16_t analogReadFast() //pin просто для совместимости
{
uint16_t res; // значения, которые считываются ацп, может быть [1, 65535]
system_adc_read_fast(&res, 1, 10); //решение проблемы со временем, работает за 17мкс, а не 80
return res;
}
В стандарте EM4102 используется схема Манчестерского кодирования. При таком кодировании ключ изменяет логический уровень строго в середине периода передачи одного бита. Переход от низкого к высокому уровню означает логическую «1», а переход от высокого уровня к низкому логический «0».

Чтение одного бита я тоже подсмотрел у AlexMalow, но переписал под использование AЦП, вместо встроенного в ATmega328P компаратора.
//aver - среднее значение на порту A0
byte BitRead(int aver = 500, unsigned long timeOut = 7000) { // pulse 0 or 1 or -1 if timeout
byte AcompState, AcompInitState;
unsigned long tEnd = micros() + timeOut;
AcompInitState = (analogReadFast() > aver); // читаем флаг компаратора
do {
AcompState = (analogReadFast() > aver); // читаем флаг компаратора
if (AcompState != AcompInitState) {
//delayMicroseconds(1000 / (rfidBitRate * 4)); // 1/4 Period on 2 kBps = 125 mks
delayMicroseconds(10); // 1/4 Period on 2 kBps = 125 mks
AcompState = (analogReadFast() > aver); // читаем флаг компаратора
//delayMicroseconds(1000 / (rfidBitRate * 2)); // 1/2 Period on 2 kBps = 250 mks
delayMicroseconds(200); // 1/2 Period on 2 kBps = 250 mks
return AcompState;
}
} while (micros() < tEnd);
return 2; //таймаут, компаратор не сменил состояние
}
По идее, функция должна искать смену сигнала, после ждать четверть периода для подтверждения, и в конце ждать ещё половину периода, чтобы попасть на следующий бит. На практике оказалось, что лучше работает ожидание 10 мкс для подтверждения, и 200 мкс для попадания в следующий период. Впоследствии я вообще перешел от константных пауз к таймеру.
На самом деле, мне не очень нравится этот алгоритм чтения, нет четкого понимания начинаешь чтение ты с начала байта, или с середины, из-за чего надо очень точно соблюдать тайминги.
На esp32 у меня с этим и возникло большинство проблем. Просто в процессе чтения алгоритм попадает в следующий байт, и вот «0» и «1» у нас меняются местами, а все чтение ключа идет крахом :(. Эту проблему пока не получилось решить, но на esp8266 все работает неплохо.
Чтение одного бита готово, осталось прочитать весь ключ в правильном порядке.
❯ Описание протокола
Если вкратце, EM4102 совместимая RFID-метка содержит 64 бита памяти только для чтения. Обычно структуру памяти представляют в виде таблицы.

В начале передается 9 единиц, это стартовое слово, обозначающее начало передачи. Далее идут 10 групп по 4 бита данных и 1 бит чётности на каждую группу. Из них первые 2 группы — номер версии, остальное данные. В конце передается ещё 4 бита для контроля четности столбцов и стоп-бит, который всегда равен нулю.
Как понятно из таблицы, контроль четности осуществляется по строкам (группам) в процессе передачи, и по столбцам в конце.
Данные передаются последовательно, вот пример передачи метки с номером 06x001259E3 (06 — номер версии, остальное данные)

bool readEM_Marie(byte* buf) {
int aver = calcAverageU();
unsigned long tEnd = millis() + 50;
byte ti;
//ждем 9 адяниц
byte validate;
byte ones = 0;
while(millis() < tEnd)
{
ti = BitRead(aver) ;
if (ti == 1) ones++;
else ones = 0;
if(ones == 9) break;
}
if(ones != 9) return false; //не нашли
// Serial.println("Nashel");
//читаем 10 групп по 4 бита данных и 1 бит чётности на каждую группу
for(int i=0;i<10;i++)
{
validate = 0;
for(int j=0;j<5;j++)
{
if (BitRead(aver))
{
bitSet(buf[ones>>3], 7-(ones%8));
validate+=1; //Считаем кол-во едениц для контроля четности
}
else bitClear(buf[ones>>3], 7-(ones%8));
ones++;
}
if(validate&1) return false; //не четно
}
//Наконец, есть 4 бита контрольной суммы и последний стоповый бит, который всегда равен нулю.
for(int j=0;j<5;j++)
{
if (BitRead(aver))bitSet(buf[ones>>3], 7-ones%8);
else bitClear(buf[ones>>3], 7-ones%8);
ones++;
}
//добавляем 9 едениц в начало
buf[0] = 255;
bitSet(buf[1], 7);
return vertEvenCheck(buf); //контроль четности столбцов
}
Проверку четности столбцов я взял напрямую у AlexMalow, выглядит он страшно, но переписать руки ещё не дошли. Вообще надо это сделать в самой функции чтения.
byte rfidData[5];
bool vertEvenCheck(byte* buf) { // проверка четности столбцов с данными
byte k;
k = 1 & buf[1] >> 6 + 1 & buf[1] >> 1 + 1 & buf[2] >> 4 + 1 & buf[3] >> 7 + 1 & buf[3] >> 2 + 1 & buf[4] >> 5 + 1 & buf[4] + 1 & buf[5] >> 3 + 1 & buf[6] >> 6 + 1 & buf[6] >> 1 + 1 & buf[7] >> 4;
if (k & 1) return false;
k = 1 & buf[1] >> 5 + 1 & buf[1] + 1 & buf[2] >> 3 + 1 & buf[3] >> 6 + 1 & buf[3] >> 1 + 1 & buf[4] >> 4 + 1 & buf[5] >> 7 + 1 & buf[5] >> 2 + 1 & buf[6] >> 5 + 1 & buf[6] + 1 & buf[7] >> 3;
if (k & 1) return false;
k = 1 & buf[1] >> 4 + 1 & buf[2] >> 7 + 1 & buf[2] >> 2 + 1 & buf[3] >> 5 + 1 & buf[3] + 1 & buf[4] >> 3 + 1 & buf[5] >> 6 + 1 & buf[5] >> 1 + 1 & buf[6] >> 4 + 1 & buf[7] >> 7 + 1 & buf[7] >> 2;
if (k & 1) return false;
k = 1 & buf[1] >> 3 + 1 & buf[2] >> 6 + 1 & buf[2] >> 1 + 1 & buf[3] >> 4 + 1 & buf[4] >> 7 + 1 & buf[4] >> 2 + 1 & buf[5] >> 5 + 1 & buf[5] + 1 & buf[6] >> 3 + 1 & buf[7] >> 6 + 1 & buf[7] >> 1;
if (k & 1) return false;
if (1 & buf[7]) return false;
//номер ключа, который написан на корпусе
rfidData[0] = (0b01111000 & buf[1]) << 1 | (0b11 & buf[1]) << 2 | buf[2] >> 6;
rfidData[1] = (0b00011110 & buf[2]) << 3 | buf[3] >> 4;
rfidData[2] = buf[3] << 5 | (0b10000000 & buf[4]) >> 3 | (0b00111100 & buf[4]) >> 2;
rfidData[3] = buf[4] << 7 | (0b11100000 & buf[5]) >> 1 | 0b1111 & buf[5];
rfidData[4] = (0b01111000 & buf[6]) << 1 | (0b11 & buf[6]) << 2 | buf[7] >> 6;
return true;
}
И, собственно, сам вызов функций:
byte addr[8];
byte keyID[8];
bool searchEM_Marine() {
bool rez = false;
rfidACsetOn(FreqGen); // включаем генератор 125кГц и компаратор
delay(6); //6 мс длятся переходные прцессы детектора
if (!readEM_Marie(addr)) { //чтение
return rez;
}
rez = true;
keyType = keyEM_Marine;
//Вывод ключа в монитор порта
for (byte i = 0; i < 8; i++) {
Serial.print(addr[i], HEX);
Serial.print(":");
}
Serial.print(F(" ( id "));
Serial.print(rfidData[0]);
Serial.print(" key ");
unsigned long keyNum = (unsigned long)rfidData[1] << 24 | (unsigned long)rfidData[2] << 16 | (unsigned long)rfidData[3] << 8 | (unsigned long)rfidData[4];
Serial.print(keyNum);
Serial.println(F(") Type: EM-Marie "));
return rez;
}
❯ Эмуляция
С эмуляцией совсем проблем нет, просто дергаем ногой в нужную сторону с периодом 250мкс
void Emulate(byte buf[8]) {
digitalWrite(FreqGen, LOW); // отключаем шим
delay(20);
for (byte k = 0; k < 10; k++) {
for (byte i = 0; i < 8; i++) {
for (byte j = 0; j < 8; j++) {
if (1 & (buf[i] >> (7 - j))) {
pinMode(FreqGen, INPUT);
delayMicroseconds(250);
pinMode(FreqGen, OUTPUT);
delayMicroseconds(250);
} else {
pinMode(FreqGen, OUTPUT);
delayMicroseconds(250);
pinMode(FreqGen, INPUT);
delayMicroseconds(250);
}
}
}
// delay(1);
}
}
❯ Проблемы
И вот после этого я столкнулся с проблемой нехватки портов, точнее порта. На ESP8266 всего один аналоговый порт, который занят считывателем для ключей Cyfral и Metacom, соответственно, с чтением EmMarine возникают проблемы. Можно было немного поработать со схемой и добиться правильной работы, но я решил переехать на более компактный и продвинутый контроллер — esp32 c3.
У esp32 c3 пять аналоговых портов, которых мне с головой, но другая архитектура. Соответственно, старый код так просто не запустить.
Из плюсов у esp32 c3 есть цап, благодаря чему она может отлично генерировать 125 кГц.
void rfidACsetOn(int pwmpin){
//включаем генератор 125кГц
pinMode(pwmpin, OUTPUT);
ledcAttach(pwmpin, 125000, 4);
ledcWrite(pwmpin, 8); // 50% скважность (128 из 255)
}
Из минусов, ацп работает медленнее чем у esp8266. Пришлось повозиться и покопаться в документации (и исходниках), чтобы написать достаточно быструю функцию чтения. Но она все равно почти в 2 раза медленнее аналогичной на esp8266.
void analogSetup()
{
adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_DB_11);
adc1_config_channel_atten(ADC1_CHANNEL_4,ADC_ATTEN_DB_11);
}
uint16_t analogReadFast0() //чтение с A0
{
return adc1_get_raw(ADC1_CHANNEL_0);
}
uint16_t analogReadFast4() //чтение с A4
{
return adc1_get_raw(ADC1_CHANNEL_4); //4 пин
}
Для чтения Cyfral И Metacom этого хватило, а с чтением EmMarine возникли проблемы.
В остальном коде потребовалась куча мелких правок, в основном правка изменения и правка отличающихся встроенных функций.

Примерно на этой стадии подзавис проект. В итоге у меня получилось читать ключи EM4102 на esp32 c3 (код, использованный в статье, позволяет это делать), но далеко не все, и не очень надежно. Функция BitRead не всегда попадает в тайминги, из-за чего периодически путает «0» и «1». Причем, похоже, тайминги пляшут от ключа к ключу. Наверное, можно обойти эту проблему, увеличив скорость чтения, но это можно сделать либо внешним АЦП (компаратором), либо я пока не знаю как :) Программно пока не получилось.

Полный код из статьи можно найти у меня на GitHub.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

Комментарии (7)
sami777
15.05.2025 10:24Позволю себе придраться к словам автора! А именно вот к этому предложению - "Как я уже писал выше, для работы считывателя нужна несущая частота 125 мГц". Что за частота 125 мГц? Если "м" - это Мега? Ну тогда и пишите через большую "М"! Но постойте, какие МГц?! Размеры катушки с ее киллометрами проволоки явно не для Мгц! Может миллигерцы? Инфразвук? Тогда меди надо домотать! Выходит, как не читай, - все неправильно! Я, когда был помоложе, любил Ом писать с маленькой буквы. Ну проще же читается, например, 120оМ. Но что то с годами стал появляться какой то стыд перед тем, чью фамилию каждый раз используешь.
dlinyj
15.05.2025 10:24Новое - это хорошо забытое старое. Писал хороший код с гайдом, добавь аккумулятор и на ардуине с минимум пайки всё работает. Эмулятор RFID на Arduino
Самое ценное - это уметь считать катушки. В этой статье Эмулятор RFID достаточно подробно тоже всё разбирал.
VT100
15.05.2025 10:24Нагружать выход микроконтроллера на последовательный колебательный контур - так себе идея.
kenomimi
Мммм, техно-порно...
А разве не стоит такие вещи делать на FPGA, раз уж интересно работать на самом низком уровне? Контроллер, тем более двухядерный с RTOS, не лучший выбор в данном случае. Да и сейчас вроде бы китайцы успешно делают дешевые маленькие FPGA в паябельных корпусах (то есть не хBGA).
tequier0
Какие плис дешевле есп8266?
Из дешевых знаю gowin gw1nr (tang nano devboard), но и там цена дороже 1000.
А вообще, конечно, такие вещи лучше на плисе делать, осоьенно, если скорость важна, и уж точно не стоит строго привязываться к таймеру при чтении сигнала. На плисе эдж детектор на n разрядов (n зависит от частоты плис и требуемой точности) существенно облегчил бы проблемы с чтением.