Хабрахабр

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

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


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

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

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

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

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

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

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


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


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

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

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


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

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

void UartISR (void) 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;

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

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

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

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

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

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

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

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

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

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

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

Кусок схемы передатчика, электрически эквивалентный оригиналу.

//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 наш сниффер

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

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

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

//ICC-AVR application builder : 11.02.2015 12:25:51
// Target : M328p
// Crystal: 16.000Mhz #include <macros.h>
#include <iccioavr.h>
#include <avrdef.h> #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 строк на языке Си.

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

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

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

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

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


Отсюда

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть