[Из песочницы] Промышленный реверс-инжиниринг

Рассказ о процессе заимствования при разработке электроники на наглядном примере.

r44rgn9ckwjsxn3aqhznjl6yopg.png
Запись лога работы лифта самодельным сниффером

Однажды мне понадобилось скопировать довольно простое устройство. Компания-производитель перестала существовать, но по всей стране ещё был спрос на замену сломанных или отработавших свой ресурс девайсов.

Само устройство — кнопка вызова лифта на фото слева. Для опытов мне дали два экземпляра, один из которых можно было полностью разобрать.

Общий план работы выглядел примерно так:


  1. Изучение схемы подключения платы;
  2. Изучение элементной базы самой платы;
  3. Срисовывание её электрической схемы;
  4. Попытка считывания файла прошивки из микроконтроллера;
  5. Дизассемблирование прошивки;
  6. Извлечение алгоритма работы;
  7. Разработка новой платы;
  8. Написание новой прошивки.

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


Изучаем подопытного

_iysgkygkocn9mdncbzolz0u9q4.jpeg
Основной микроконтроллер

cnehfjp5bvviohtimlndliklm9i.png
Кусок электросхемы лифта, на которой наши платы обведены красным

Плата собрана на микроконтроллере 1997 года выпуска AT89C2051, в основе которого лежит архитектура Intel MCS-51. В 2020 году она празднует свой 40-летний юбилей на рынке встраиваемых систем.


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

rko2ufxrcsmtjghptrilwcpx3fq.jpeg
Разбираем плату для срисовывания электросхемы

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

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

Знакомый, работающий с китайскими производствами, рассказал мне, что китайцы занимаются подобным усложнением схем для увеличения стоимости разработки и производства, если то и другое делают одни люди. После такого я склонен ему верить:

fskulhqa3cjeb4v9ilr7bncg9y8.png
Одно и то же место на китайской двухслойной плате с обеих сторон. Три огромных резистора ни к чему не подключены. Я даже просвечивал плату мощным фонариком, чтобы убедиться.

Схема срисована, загадочные места смоделированы в мультисиме, берёмся за прошивку.


Пытаемся считать прошивку

Мне очень повезло, что на обоих платах в контроллерах не включена защита от чтения, поэтому я успешно слил два варианта прошивки подобной порнографией:

re9iepb510h1ylxdfsc29sfre80.jpeg
Фото из личного блога американского энтузиаста


Дизассемблирование прошивки

Следующим этапом нам нужно преобразовать этот машинный код во что-то более читаемое:

kptegnvznij68y49jd7y15vpwsi.png

Берём известный инструмент IDA Pro, в котором уже есть наш контроллер со всеми регистрами периферии, и открываем HEX файл прошивки:

yxg_zjqxt1nqg3f_jkvuwhhknqy.png
Обработка принимаемых платой данных на языке ассемблера

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

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


Извлечение алгоритма работы

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

void UartISR (void) {
    counter500ms = 0;

    //ClearFlag(isrFlags, ISR_FLAG_3);

    ProcessUart(recievedByte);  
}

void ProcessUart(uint8_t recievedData) {
    static uint8_t uartPacketsToRxLeft, uartRecievedCmd, uartCurrPacketCRC;

    static uint8_t i, carryFlag;

    static uint16_t uartIsrPointer;

    static uint8_t uartBuffer1[8], uartBuffer2[6];
    static uint8_t uartBuffer1Pos, uartBuffer2Pos;

    // 0 - 
    // 1 - 
    // 2 - 
    // 3 - led state, 0x0F
    // 4 - 
    // 5 - 
    // 6 - 
    // 7 - 
    // 8 - buttons time
    static uint8_t dataRegisters[9]; // RAM:0050

    uint8_t tmpVal, i;

    uint8_t dataToSend;

    if (GetFlag(UartISRFlags, UART_RECIEVED_FLAG)) {
        ClearFlag(UartISRFlags, UART_RECIEVED_FLAG);

        if (recieved9thBit) {
            switch (recievedData) {
                case 0xC1: 
                    uartPacketsToRxLeft = 8;
                    uartRecievedCmd = 1;

                    uartBuffer1Pos = 0;
                    uartBuffer1[uartBuffer1Pos] = recievedData;

                    //uartIsrPointer = 0x0037;
                    //tmpVal_0037 = recievedData;
                    uartCurrPacketCRC = recievedData;

                    UartRxOn();

                    return;

                    break;
                case 0xC2:
                    uartPacketsToRxLeft = 3;
                    uartRecievedCmd = 2;

Та же самая обработка принимаемых данных на языке Си

Кому интересен протокол передачи:


Станция управления лифтом общалась с платами кнопок вызовов по полнодуплексному 24-вольтовому интерфейсу. В обычном режиме платы кнопок слушали линию, ожидая 9-битный пакет данных. Если в этом пакете приходил адрес нашей платы (задавался DIP-переключателем на плате), то плата переключалась на 8-битный режим приёма, и все последующие пакеты аппаратно игнорировались остальными платами.

Первым после адреса шёл пакет с кодом команды управления. Конкретно эта плата принимала всего 3 команды:

  1. Запись в регистры данных. Например частота и длительность мигания кнопки при вызове;
  2. Включение подсветки кнопок;
  3. Запрос состояния кнопок (нажаты или нет).

Последним байтом шла контрольная сумма, которая представляет собой простой XOR всех байт после адреса.
После контрольной суммы плата опять переходила в режим ожидания своего адреса.


Разработка новой платы

Для этапа разработки новой электросхемы и печатной платы у меня нет картинок, но всё было примерно так:
qoo1ib5i-46roxxqoea1tw9sshq.jpeg

Составление электросхемы и разводка платы делались в Altium Designer. Изготовление печатной платы заказывалось в зеленоградском «Резоните».


Написание новой прошивки

Пока наша новая плата на изготовлении, едем на объект, где установлены такие кнопки вызова, и проверяем правильность разобранного протокола передачи с помощью собранного на ардуине сниффера:
aawl0ep77fnv2gz_txeorxjzkls.png
Кусок схемы передатчика, электрически эквивалентный оригиналу. На приёмнике просто оптопара.

//UART1 initialize
// desired baud rate:19200
// actual baud rate:19231 (0,2%)
// char size: 9 bit
// parity: Disabled
void uart1_init(void)
{
 UCSR1B = 0x00; //disable while setting baud rate
 UCSR1A = 0x00;
 UCSR1C = 0x06;
 UBRR1L = 0x33; //set baud rate lo
 UBRR1H = 0x00; //set baud rate hi
 UCSR1B = 0x94;
}

#pragma interrupt_handler uart1_rx_isr:iv_USART1_RXC
void uart1_rx_isr(void) {
  unsigned char tmp;
  unsigned int rcv = 0;

  if (UCSR1B & 0x02) {
    rcv = 0x100;
  }

  rcv |= UDR1;

  tmp = (rcv >> 4) & 0x0F;

  if (rcv & 0x100) {
    tmp |= 0xC0;
  }
  else {
    tmp |= 0x80;
  }

  txBuf12 = (rcv & 0x0F);
  txBuf11 = tmp;
  txState1 = 0;

  TX_ON();

  msCounter0 = 5000;
}

Говнокодим в ICC AVR наш сниффер

Дальше нужно было действовать крайне аккуратно, чтобы не спалить ничего в лифте и не допустить его остановки.

jifmg3osv9g5dxwr_4sq90ela6g.jpeg
Лезем в кнопку вызова. Толстые жёлтые провода — питание платы и интерфейс передачи. Белые на 4-пиновом разъёме — подключение кнопки и её подсветки.

Проверяем, что всё работает как надо, исправляем косяки и пишем новую прошивку под наше устройство:

//ICC-AVR application builder : 11.02.2015 12:25:51
// Target : M328p
// Crystal: 16.000Mhz

#include 
#include 
#include 

#include "types.h"
#include "gpio.h"

#define TX_OFF()    UCSR0B &= 0b11011111;
#define TX_ON()     UCSR0B |= 0b00100000;
#define TX_STATE() (UCSR0B &  0b00100000)

#define MAX_TIMEOUT 3000

//#define SNIFFER_MODE 1
//#define MASTER_MODE 1

// #pragma avr_fuse (fuses0, fuses1, fuses2, fuses3, fuses4, fuses5)
#pragma avr_fuse     (0xFF, 0xD1, 0xFC)
#pragma avr_lockbits (0xFC)

// AVR signature is always three bytes. Signature0 is always the Atmel
// manufacturer code of 0x1E. The other two bytes are device dependent.
#pragma avr_signature (0x1E, 0x95, 0x0F) // atmega32

static GPIOx errorLed, rcvLed, butUp, butDn, ledUp, ledDn, butLedUp, butLedDn, ledButUp, ledButDn;

static uint8_t msFlag = 0;
static uint8_t ledState = 0, buttonsState = 0;
static uint16_t rcvLedLitTime = 0, butMaskCalcTime = 0, timeoutTimer = 0;

typedef struct {
  uint16_t buffer[10];
  uint8_t dataLength;
} UartPacket;

static UartPacket txPacket, rxPacket;

#ifdef SNIFFER_MODE
  static uint8_t txBuffer[64], txBufferLength = 0, bufferMutex = 0;
#endif

static uint8_t GetPacketCRC(UartPacket* packet);
static void SendLedState(void);

uint8_t GetAddress(void) {
  return (PINC & 0x3F);
}

Код на Си для новой платы на основе микроконтроллера AVR ATmega328P

Простоту устройства и прошивки можно оценить по объёму кода, он содержит всего лишь около 600 строк на языке Си.

Процесс сборки выглядел так:


Плата другая, но принцип тот же

Фотографию готового устройства приложить не могу, просто поверьте, что оно до сих пор производится и продаётся.


Лирическое заключение

По поводу кнопок лифта «вверх» и «вниз» на этаже. Я заметил, что многие люди совершенно не понимают их назначение и жмут обе сразу.

r26re7kjkj8dhppaxmbkdcedvt8.png
Отсюда

У лифта есть два набора кнопок: в кабине — панель приказов, и на этаже — панель вызова. Уже по названию можно догадаться, что панель приказов имеет более высокий приоритет управления.

Все лифты, имеющие панели вызовов с кнопками «вверх» и «вниз», работают с каким-то из вариантов алгоритма оптимизации поездок, целью которых является перевозка максимального числа пассажиров за минимальное время и отдельное условие максимального времени ожидания на этаже (регулируется госстандартом).

Такой алгоритм обычно предполагает подбор пассажиров на этажах, если они едут в том же направлении, которое указывают нажатием кнопки вызова «вверх» или «вниз».

Представим ситуацию, что лифт с пассажирами едет вниз и по пути получает с этажа ниже вызов «вниз». Лифт остановится для подбора пассажира (да, есть ещё учёт загрузки кабины по весовому датчику, но его мы опустим).

Лифт едет дальше и получает с этажа ниже вызов «вверх». Логично, что лифт не остановится для подбора пассажира, так как не будет менять направление движения (это тоже регулируется стандартом), а подбирать пассажира, чтобы поехать вниз, а затем вверх — бесполезный расход энергии и места в лифте.

Лифт едет дальше и получает с этажа ниже сразу два вызова «вверх и «вниз», которые нажал какой-то нетерпеливый пассажир, которому нужно ехать вверх. Логично, что лифт остановится на этом этаже, но пассажир в него не войдёт, зато потратит время людей в кабине на замедление и остановку лифта, открытие дверей, ожидание, закрытие дверей и разгон до номинальной скорости.

Если у лифта только одна кнопка на этаже, то в 99% случаев он работает по алгоритму «собирательный вниз», и при наличии приказов в кабине останавливается только при движении вниз.

Если у вас есть навыки программирования на JS, то можете попробовать реализовать подобный алгоритм управления в онлайновой игре Elevator Saga. В ней есть все аспекты оптимизации поездок без углубления в хардкор вроде работы цепей безопасности лифта.

pikjigdf2mqsd2ptmjbtot-yd34.png

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

© Habrahabr.ru