Проект «Мультиключ». Как мы побеждали контактные ключи Metacom и Cyfral

Как и обещал в предыдущей статье, пишу о нашем опыте работы с контактными ключами Metacom и Cyfral.

Про русские народные протоколы контактных ключей

Эти ключи построены на микросхемах 1233KT1 и 1233KT2, которые не сильно друг от друга отличаются и имеют очень схожий принцип работы.

При подаче питания ключ просто выдает свой id. При этом никакие команды ключ не принимает и не посылает, а проверка правильности считывания ключа производится путем повторного считывания. Первым, для определения начала передачи, всегда идет стартовое слово. В отличие от ключей Dallas, они работают не по напряжению, а по току. Это менее распространенные и более дорогие ключи. Таким образом, логические уровни определяются сопротивлением ключа (около 400 Ом и 800 Ом). А значение бита определяется длительностью удержания низкого и высокого значения потребления тока.

Разберем эти ключи по отдельности.

Про метаком

Этот ключ построен на микросхеме 1233KT2 (хотя у метаком есть и ключи dallas) .

c1e305b8f79f87e4746844b93496d8e8.png

В начале передается синхронизирующий сигнал. Передача синхронизирующего бита представляет собой удержание потребляемого тока на высоком уровне в течение целого периода передачи одного бита. При этом период передачи одного бита может быть от 50 до 230 мкс. За ним передаётся трёхразрядное стартовое слово, которое содержит порядковый номер разработки — 210=0102 без контроля на чётность.

Далее передаются сами биты ключа: передача каждого представляет собой последовательное удержание потребляемого тока сначала на низком, а затем на высоком уровне. При этом, при передаче логической »1» около 2/3 периода удерживается низкий уровень сигнала и остальные ⅓ высокий.  При передаче логического »0», соответственно, наоборот. Ключ Metakom посылает 4 байта, где каждый байт заканчивается битом четности.

0cafcc6486930bb0447cfa96ac0d136f.png

Кстати, все это есть в datasheet. Будем считать, что с теорией всё понятно. Осталось понять, как прочитать этот ключ микроконтроллером.  Напомню, на данный момент мы используем ESP8266 и программируем его в среде Arduino IDE.
Для начала надо понять как определять изменения потребления тока. Какого-либо датчика тока ни у Arduino ни у ESP8266 нет, но зато есть АЦП и закон ома! При увеличении тока в цепи, падает напряжение, а его мы как раз можем измерить.

Новая схема ключа

Новая схема ключа

Тут нас ждет сразу несколько проблем:
1. Точно неизвестно, относительно какого уровня напряжения определять низкий уровень потребления тока и высокий.

2. Период передачи одного бита может быть от 50 до 230 мкс, а время работы функции analogRead у ESP8266 около 90 мкс, у ардуино вообще 112 мкс (знал бы я это сразу, было бы попроще).

3. Точно не известен период передачи 1 бита (от 50 мкс до 230 мкс это не очень точно).

Ну, пойдем по очереди. На самом деле, очень помог проект от Alex Malov EasyKeyDublicator, правда пришлось почти все переписать.

Определить граничную величину напряжения довольно просто, это средне-медианное напряжение при взаимодействии с ключем. То есть, просто вызываем analogRead 256 раз, складываем результаты и делим на 256. В итоге напряжение при высоком уровне потребления тока будет немного ниже медианного, а при низком — немного выше. У меня это делает функция calcAverage ():

 int calcAverageU() {
    unsigned long sum = 512;
    for (byte i = 0; i < 255; i++) {
      delayMicroseconds(10);
      sum += analogRead(A0);
    }
    sum = sum >> 8;
    return sum;
  }

Ускоряем AnalogRead

Теперь надо разобраться с АЦП. На ардуино проблем нет, достаточно просто изменить делитель, при этом, правда, снизится точность, но с этим проблем не возникает. Или можно вообще использовать функцию analogReadFast от AlexGyver. Так же можно ещё читать напрямую из регистров, но это уже лишнее.

void setup()
{
  Serial.begin(9600);
  ADCSRA &= B11111000; // очистить три младших бита
  ADCSRA |= B00000100; // установить в них комбинацию 100 что дает делитель 16 и тактовую частоту АЦП 1МГц
}

// Функция analogReadFast от AlexGyver
// ВНИМАНИЕ! Нужное опорное установлено DEFAULT, можно изменить на своё
uint16_t analogReadFast(uint8_t pin) {
  pin = ((pin < 8) ? pin : pin - 14);    // analogRead(2) = analogRead(A2)
  ADMUX = (DEFAULT<< 6) | pin;    // Set analog MUX & reference
  bitSet(ADCSRA, ADSC);            // Start 
  while (ADCSRA & (1 << ADSC));        // Wait
  return ADC;                // Return result
}

void loop()
{
  Serial.println(analogRead(0));
  Serial.println(analogReadFast(0));
}

С ESP все немного сложнее, прямого доступа ни к регистрам, ни к функции analogRead нет. Казалось бы, ничего не поделаешь, но нашлась встроенная функция system_adc_read_fast, которая работает намного быстрее.

Правда, эта функция делает несколько измерений, для меня это оказалось неудобно, поэтому коды вышли костыльным (не ругайте).

int analogReadFast(byte pin) //pin просто для совместимости
{
  uint16_t adc_addr[1]; // значения которые считываются ацп, может быть [1, 65535]
  system_adc_read_fast(adc_addr,1,10); //костыльное решение проблемы со временем, но работает за 17мкс, а не 80 (знаяения, колво, adc_clk_div)
  return adc_addr[0];
}

Читаем ключ

Теперь скорости нам хватает, давайте попробуем что-то прочитать. А точнее написать функцию, которая будет измерять длительность низкого уровня потребления тока при передачи одного бита.

unsigned long  GetLowPulseDuration(int Average = 510, unsigned long timeOut = 1500)  
  {
    bool AcompState;
    unsigned long tEnd = micros() + timeOut;
    do {
      //Если напряжение выше среднего, значит это начало низкого уровня потребления тока
      if (analogReadFast(A0) < Average) { 
        tEnd = micros() + timeOut;
        do {
          //Если напряжение ниже среднего, значит это конец возвращаем время
          if (analogReadFast(A0); > Average) return (unsigned long)(micros() + timeOut - tEnd);  
        } while (micros() < tEnd);
        return 0;  //таймаут, импульс не вернуся обратно
      }            
    } while (micros() < tEnd);
    return 0;
  }

 Функция возвращает длительность низкого уровня потребления тока, а далее по этой длительности мы можем определить, один это или ноль. Проще всего оказалось высчитать половину периода передачи и сравниваться с ней. Если длительность больше полупериода, то это 1, если меньше — 0.

Осталось определить половину периода. Для этого просто считаем среднее для нескольких вызовов функции GetLowPulseDuration.

int calcAverageP(int aver = 510) { // aver – среднее значение из calcAverage
    unsigned long tSt = 0;
    for (int i = 0; i < 20; i++) {
      tSt += GetLowPulseDuration(aver);
    }
    return tSt / 20;
  }

Теперь у нас есть всё для чтения первого бита ключа.

byte readBit(int aver, int halfT) {
    int ti = GetLowPulseDuration(aver);
    if ((ti == 0) || (ti > 400)) return 2; // ошибка чтения
    if (ti >= halfT * 2) return 3;  // Сигнал синхронизации метаком
    if (ti < halfT) return 1;
    else return 0;
  }

Попробуем прочитать весь ключ:

bool read_metacom(byte* buf) {
    int aver = calcAverageU();
    int halfT = calcAverageP(aver);
    unsigned long tEnd = millis() + 30;

    byte b1 = 1; //4 бита стартового слова
    byte b2 = 1;
    byte b3 = 1;
    byte b4 = 1;

    do {

      b1 = b2;
      b2 = b3;
      b3 = b4;
      b4 = readBit(aver,halfT);
      if (b4 == 2) continue;
      
      if (b1 == 3 && b2 == 0 && b3 == 1 && b4 == 0) { //Ожидаем стартовое слово
        buf[0] = 0b00100000;  //добавляем кодовое слово
      } else  continue;

      for (int byteInd = 1; byteInd < 5; byteInd++) {  //читаем 4 байта по 8 бит
        int k = 0;                                     //для подсчета четности
        for (int bitInd = 0; bitInd < 8; bitInd++) {
          byte bit = readBit(aver,halfT);
          if (bit == 2) return false;
          if (bit == 1) {
            bitSet(buf[byteInd], 7 - bitInd);
            k++;
          }
        }
        if (k % 2 == 1)  //Проверка четность
        {
          //Serial.print("Fiasko");
          if (byteInd == 4) buf[byteInd]++;  //Последний бит хрен считаешь, поэтому костылим
          else return false;
        }
      }
      return true;
    } while ((millis() < tEnd));
    return false;
  }

Для универсальности, ключ мы, как и dallas, записываем в 8-байтный массив. Первый байт — это код семейства, у метакома мы запишем 0b00100000, т.е. стартовое слово, и потом сам код. Для надежности можно считать пару раз. Считать получилось, осталось попробовать эмулировать.

Эмуляция Metacom

На самом деле, это оказалось самым простым. Правда возникло пара нюансов)

Для начала функция эмуляции одного бита:

void SendBitMetacom(bool Bit) {
    //берем период за 100 мкс
    digitalWrite(EmulatePort, 1);  //низкий уровень тока
    if (Bit) delayMicroseconds(70);  //1 это удержание 2/3 периода
    else delayMicroseconds(30);
  
    digitalWrite(EmulatePort, 0);
    if (Bit) delayMicroseconds(30);
    else delayMicroseconds(70);
  }

То есть, мы просто подаем на порт эмуляции HIGH потом ждем 2/3 периода для передачи 1 и ⅓ для 0, потом подаем LOW и ждем оставшуюся часть периода.

Сам ключ отсылается тоже не сложно:

 void SendKey(byte Key[8]) {
    //Бит синхронизации и стартовое слово
    digitalWrite(EmulatePort, 0);
    delayMicroseconds(90); // с учетом выключения ждать надо меньше
    SendBitMetacom(0);
    SendBitMetacom(1);
    SendBitMetacom(0);

    byte bInd = 0;
    for (int i = 1; i < 5; i++) { //4 байта
      for (int bInd = 0; bInd < 8; bInd++) { // по 8 бит
        bool bit = (((Key[i]) & (0x1 << (7 - bInd))) != 0);  //получаем бит
        SendBitMetacom(bit); // отправляем бит
      }
    }
  }

Я долго возился с подбором резисторов, чтобы уровень сигнала у эмулятора совпадал с настоящим ключем. Но оказалось, что домофону глубоко фиолетово на уровень сигнала :), и достаточно подключить порт напрямую (как и в схеме с dallas). Но не фиолетово на период между передачами ключа. Нужно, чтобы ключ хотя бы несколько раз передавался непрерывно. Я просто решил вызывать функцию эмуляции в цикле.

Про Цифрал

Микросхема в этом ключе практически такая же, как и в метаком. Даже называются похоже — 1233KT2. Но есть несколько отличий.

Cyfral циклично отправляет 9 нибблов (1 ниббл = 4 бита): 1 стартовый и 8 ID. Ниббл может иметь всего четыре значения для ID и одно значение для стартового слова.

Остальные комбинации запрещены, что может быть использовано для контроля достоверности считывания кода. В отличие от метаком, сигнала синхронизации нет, передается сразу стартовое слово 00012, и далее 8 ниблов по 4 бита. То есть, всего в сумме 36 бит, как и у метакома.

9e0ed8d45e0cbaa37292915803b8d1de.png

Для удобства дальнейшей обработки, в нашем коде не используется разбиение на ниблы и таблица кодирования, а ключ пишется просто побайтно. Каждый байт четный, поэтому все функции чтения аналогичны функциям работы с Metacom. По сути, отличается только стартовое слово, поэтому не вижу смысла их тут приводить.
Эмуляция тоже полностью аналогична, только слегка подправлены тайминги, но метаком на таких таймингах тоже работает.

  void SendBitCyfral(bool Bit) {
    digitalWrite(EmulatePort, 1);  //0 на выходе транзисторного ключа

    if (Bit) delayMicroseconds(79);  //Иначе, если 1 - задержка 79,2мкс (>0,6Tп=113us)
    else delayMicroseconds(39);      //Если передаётся 0 задержка 39,6мкс (<0,4Tп=107us)

    digitalWrite(EmulatePort, 0);    //1 на выходе транзисторного ключа
    if (Bit) delayMicroseconds(33);  //Иначе, если 1 - задержка 33,2мкс (<0,4Tп=113us)
    else delayMicroseconds(67);      //Если передаётся 0 задержка 67,2мкс (>0,6Tп=107us)
  }

  void SendKey(byte Key[8]) {
    //стартовое слово
    SendBitCyfral(0);
    SendBitCyfral(0);
    SendBitCyfral(0);
    SendBitCyfral(1);

    byte bInd = 0;
    for (int i = 1; i < 5; i++) {
      for (int bInd = 0; bInd < 8; bInd++) {
        bool bit = (((Key[i]) & (0x1 << (7 - bInd))) != 0);  //получаем бит
        SendBitCyfral(bit); //отправляем бит
      }
    }
  }

  void Emulate(byte Key[8]) {
    pinMode(EmulatePort, OUTPUT);
    for (int i = 0; i < 10; i++) {
      SendKey(Key); //просто 10 раз посылаем ключ
    }
  }

Но доступный мне для проверки домофон Cyfral CCD 2094.1 на эту эмуляцию так и не реагирует :(

Еще, из интересного, нашлись способы перекодирования этих ключей в формат dallas. У метакома все просто (у меня вышло почти с первого раза): ключ переписывается в прямом или обратном порядке (зависит от домофона) и считывается контрольная сумма. У Cyfral несколько вариантов перекодировок (по сути, различная запись ниблов, иногда с ошибками): три я реализовал, но проверить ещё не удалось, поэтому про эту тему в следующий раз.

P.S. Тем временем в устройстве появилось многостраничное меню, вывод напряжения батарей (скоро будет просто индикатор заряда) и меню для ввода ключей вручную.

1967d936482333457afe8569b3b9a5da.jpgУже меньше брелка от машины, но будет ещё компактнее :)

Уже меньше брелка от машины, но будет ещё компактнее:)

© Habrahabr.ru