Авторизация
Зарегистрироваться

Ломаем квадрик: вандализация с элементами реверс-инжиниринга

В последнее время авторы взялись ранее купленные ненужные вещи раздербанивать и смотреть, что там внутрях. Подержу-ка и я эту инициативу — у меня много лет назад купленный баксов за 10 квадрик завалялся, и он мне, как есть, явно абсолютно не нужен. Но может для чего и сгодится — ниже вы найдете что-то наподобие реверс-инжиниринга с кучей библиотек.

Для чего я это все делал — в статью не вошло, и так слишком много получается, не у каждого хватит терпения прочитать.

Вот в таком виде квадрик сохранился — и коробка есть, и запасная батарейка, и запасные винты. Помнится, я его купил, когда еще здоров был, и даже несколько раз игрался. Буквально через месяц после покупки все началось — и первым поехало зрение. Картинка раздвоилась и что к чему — уже было непонятно. И каким из двух мультикоптеров управлять, и как далеко он находится — оставалось только гадать.





Берем картину мироздания и тупо смотрим, что к чему.

При раскурочивании игрушки целью было все-таки понять все протоколы, чтобы потом плату можно было использовать в мирных целях.
Раскручиваем пульт управления. В качестве приемо-передатчика используется микросхема XN297, регистры управления похожи на Nordic Semiconductor nRF24L01 с некоторыми дополнениями, но по формату посылки не совсем совместима. С микроконтроллера название сошлифовано, но мы же не лыком шиты, и чувствуем, что это TG54528.



К сожалению, от этого знания толку нуль. Что она из себя представляет и как ее программировать гугл не знает или не захотел мне рассказать. Придется использовать пульт как есть, протокол мы раскопаем позднее.



Дисплейчик используется некий BAOMEI BM-8025A. К сожалению, издевательств не выдержал и после сборки уже не работал.

Собираем пульт и пытаемся разобраться с управлением квадрика.



Обнаруживаем LDO XC6206P282MR на 2.8 Вольта.
Микроконтроллер STM32F031K4 (ARM®32-bit Cortex®-M0 up to 48 MHz, 4 Kbytes of SRAM, 16 Kbytes Flash), гироскоп с акселерометром MPU6881 (оказался вполне совместим с MPU6150, наверняка у DMP прошивка другая, но так как ее описания все равно нет, то и не заботит) и приемопередатчик — тот же XN297, что и в пульте управления.



Рисуем схему соединений — получается что-то типа такого, остальное нас мало волнует:



Подключаем контрольные точки — потенциальным критикам пайки могу только заметить — попробуйте сделать так с жуткой атаксией.



Точки подпайки — используем провод от дохлых наушников-вкладышей.



Вид снизу:



Для начала подключаемся к SPI и смотрим инициализацию XN297

Инициализация

160us
-----
0x200F/0xE00 *
-----
160us
-----
0x2009/0xE00 *
0x2A26/0xE00 0xA867/0x00 0x35CC/0x00 *
0xAFF/0xE26 *
0x3026/0xE00 0xA867/0x00 0x35CC/0x00 *
0x10FF/0xE26 *
0x390B/0xE00 0xDFC4/0x00 0xA703/0x00 *
0x3EC9/0xE00 0x9AB0/0x00 0x61BB/0x00 0xAB9C/0x00 *
0x1EFF/0xEC9 *
0x3F4C/0xE00 0x846F/0x00 0x9C20/0x00 *
0x1FFF/0xE4C *
0x2607/0xE00 *
0x310F/0xE00 *



А теперь, как пульт управления биндится с квадриком — для этого пульт нужно включить с ручкой trottle внизу, поднять ее до упора вверх и снова опустить. В это время нужно посмотреть, что происходит с SPI.

Bind

0x7FF/0x4040
0x2009/0x4000
0x61FF/0x40AA 0xFFFF/0xE62 0xFFFF/0x00 0xFFFF/0xBC 0xFFFF/0x7F7F 0xFFFF/0x201E 0xFFFF/0x4040 0xFFFF/0x00
0x2740/0x4E00
0xE200/0xE00
0x200F/0xE00
-------
0x2009/0xE00
0x2511/0xE00
0x200F/0xE00
0x7FF/0xE0E



После этого переключаемся на I2C и смотрим инициализацию MPU6881 — собственно, чтобы убедиться в совместимости с MPU6150.

Инициализация MPU6881

write to
0x68 ack data: 0x6B 0x02
write to
0x68 ack data: 0x19 0x01
write to
0x68 ack data: 0x1A 0x01
write to
0x68 ack data: 0x1B 0x18
write to
0x68 ack data: 0x1C 0x18
write to
0x68 ack data: 0x1D 0x00
write to
0x68 ack data: 0x1E 0x0A
write to
0x68 ack data: 0x3B
read to
0x68 ack data: 0xFF 0xD3 0xFF 0x8A 0x08 0xF9 0x09 0xF9 0xFF 0x7F 0x00 0x24 0xFF 0xFE
write to
0x68 ack data: 0x3B
read to
0x68 ack data: 0xFF 0xD3 0xFF 0x8A 0x08 0xF9 0x09 0xF9 0xFF 0x7F 0x00 0x24 0xFF 0xFE
write to
0x68 ack data: 0x3B
read to
0x68 ack data: 0xFF 0xD3 0xFF 0x8A 0x08 0xF9 0x09 0xF9 0xFF 0x7F 0x00 0x24 0xFF 0xFE



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



Теперь как все это будем программировать? Самое простое — использовать Microsoft Visual Studio Code — бесплатная среда, которая прекрасно работает с Linux, если вам это важно. Ставим плагин PlatformIO и создаем любой проект на базе любого контроллера STM32. Во время создания этого проекта PlatformIO сам натащит из интернета все нужные вам инструменты и библиотеки. К сожалению, STM32F031K4 там отсутствует. Но где наша не пропадала?
В каталоге .platformio/platforms/ststm32/boards создаем файл genericSTM32F031K4.json
со следующим содержимым:
genericSTM32F031K4.json

{
 
"build": {
   
"cpu": "cortex-m0",
   
"extra_flags": "-DSTM32F031x4",
   
"f_cpu": "48000000L",
   
"mcu": "stm32f031k4",
   
"product_line": "STM32F031x4",
   
"variant": "STM32F0xx/F031K4"
 
},
 
"debug": {
   
"jlink_device": "STM32F031K4",
   
"openocd_target": "stm32f0x",
   
"svd_path": "STM32F031.svd"
 
},
 
"frameworks": [
   
"arduino",
   
"cmsis",
   
"stm32cube",
   
"libopencm3"
 
],
 
"name": "generic STM32F031K4",
 
"upload": {
   
"maximum_ram_size": 4096,
   
"maximum_size": 16384,
   
"protocol": "stlink",
   
"protocols": [
     
"jlink",
     
"cmsis-dap",
     
"stlink",
     
"blackmagic",
     
"serial"
   
]
 
},
 
"url": "https://www.hotmcu.com/stm32f030f4p6-minimum-systerm-boardcortexm0-p-208.html",
 
"vendor": "Generic"
}



Теперь можно даже с помощью ардуино программировать этот контроллер, но не рекомендую. Сама по себе не особо эффективная среда ардуино будет работать поверх HAL. Не буду высказывать свое мнение, кубологи будут недовольны — но у такой связки простейшая моргалка светодиодом съест больше 50% доступной памяти. Поэтому я дальше буду пользоваться старой, как дерьмо мамонта, библиотекой libopencm3 или вообще напрямую в регистры писать.



Как всегда, начинаем с самого тупого — поморгаем светодиодами. Для этого нужно написать подпрограммы инициализации и выводы в порты ввода-вывода. Лиха беда начало, пишем:

system.cpp

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/cm3/nvic.h>
#include <libopencm3/cm3/systick.h>
#include <system.h>

volatile uint32_t TimeCounter;

void system_init(void)
{
  rcc_clock_setup_in_hsi_out_48mhz
();  // set STM32 to clock by 48MHz from HSI oscillator
 
TimeCounter=0;
  systick_set_clocksource
(STK_CSR_CLKSOURCE_AHB);
  STK_CVR
= 0;                                   // clear counter
  systick_set_reload
(rcc_ahb_frequency / 1000);  // Set up timer interrupt
  systick_counter_enable
();
  systick_interrupt_enable
();
}

void delay_us(uint16_t del_us)
{
  uint32_t cnt
= del_us << 2;
 
do
 
{
   
asm volatile("nop");
   
asm volatile("nop");    
   
asm volatile("nop");      
   
asm volatile("nop");    
 
} while (--cnt);
}

void delay(uint32_t time)
{
 
TimeCounter = time;
 
while(TimeCounter != 0);
}

void sys_tick_handler(void)
{
 
if (TimeCounter |= 0) TimeCounter--;
}

void init_LEDs(void)
{
 
// Enable clocks to the GPIO subsystems
  rcc_periph_clock_enable
(RCC_GPIOB);
  rcc_periph_clock_enable
(RCC_GPIOA);  
 
// LED OUTPUTS
  gpio_mode_setup
(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO2 | GPIO4 | GPIO12);
  gpio_mode_setup
(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO0 | GPIO2);
  gpio_clear
(GPIOA, GPIO2 | GPIO4 | GPIO12);
  gpio_clear
(GPIOB, GPIO0 | GPIO2);
}

void LedMask(uint8_t mask)
{
 
if (mask & 0x01) gpio_set(GPIOA, GPIO2); else gpio_clear(GPIOA, GPIO2);
 
if (mask & 0x02) gpio_set(GPIOA, GPIO4); else gpio_clear(GPIOA, GPIO4);  
 
if (mask & 0x04) gpio_set(GPIOB, GPIO0); else gpio_clear(GPIOB, GPIO0);  
 
if (mask & 0x08) gpio_set(GPIOA, GPIO12); else gpio_clear(GPIOA, GPIO12);  
 
if (mask & 0x10) gpio_set(GPIOB, GPIO2); else gpio_clear(GPIOB, GPIO2);
}

void LedSet(uint8_t mask)
{
 
if (mask & 0x01) gpio_set(GPIOA, GPIO2);
 
if (mask & 0x02) gpio_set(GPIOA, GPIO4);
 
if (mask & 0x04) gpio_set(GPIOB, GPIO0);  
 
if (mask & 0x08) gpio_set(GPIOA, GPIO12);
 
if (mask & 0x10) gpio_set(GPIOB, GPIO2);
}

void LedClear(uint8_t mask)
{
 
if (mask & 0x01) gpio_clear(GPIOA, GPIO2);
 
if (mask & 0x02) gpio_clear(GPIOA, GPIO4);  
 
if (mask & 0x04) gpio_clear(GPIOB, GPIO0);  
 
if (mask & 0x08) gpio_clear(GPIOA, GPIO12);  
 
if (mask & 0x10) gpio_clear(GPIOB, GPIO2);
}

void LedToggle(uint8_t mask)
{
 
if (mask & 0x01) gpio_toggle(GPIOA, GPIO2);
 
if (mask & 0x02) gpio_toggle(GPIOA, GPIO4);  
 
if (mask & 0x04) gpio_toggle(GPIOB, GPIO0);  
 
if (mask & 0x08) gpio_toggle(GPIOA, GPIO12);  
 
if (mask & 0x10) gpio_toggle(GPIOB, GPIO2);
}



Используем часть этих подпрограмм в основном цикле:
Blink

#include <system.h>

main
(void)
{
  system_init
();
  init_LEDs
();
 
while (1)
 
{
    uint8_t mask
= 1;
   
for(uint8_t i=0; i<5; i++)
   
{
     
LedMask(mask);
      mask
<<=1;
      delay
(500);
   
}
 
}
}



Моргает!



Для дальнейшей отладки нам не помешает последовательный вывод, одним светодиодиком придется пожертвовать, вместо него подключим вывод UART — это будет вывод PA2. Когда все будет готово, эту библиотеку нужно будет прибить насмерть, чтобы место не занимала и не мешала могралкам.

UART.cpp

#include <UART.h>
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/usart.h>
#include <math.h>

void init_USART(void)
{
 
// only tx at PA2
  rcc_periph_clock_enable
(RCC_GPIOA);
  rcc_periph_clock_enable
(RCC_USART1);
 
// Setup GPIO pins for USART transmit.  
  gpio_mode_setup
(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO2);
  gpio_set_af
(GPIOA, GPIO_AF1, GPIO2);
 
// Setup USART parameters.
  usart_set_baudrate
(USART1, 115200);
  usart_set_databits
(USART1, 8);
  usart_set_parity
(USART1, USART_PARITY_NONE);
  usart_set_stopbits
(USART1, USART_CR2_STOPBITS_1);
  usart_set_mode
(USART1, USART_MODE_TX_RX);
  usart_set_flow_control
(USART1, USART_FLOWCONTROL_NONE);
  usart_enable
(USART1);
}


uint8_t num2sym
(uint8_t symb)
{
  symb
&= 0x0F;
 
if (symb<10) symb+='0';
 
else         symb+='A'-10;
 
return symb;
}  

void tx_byte_UART(uint8_t c)
{
  tx_UART
('0');
  tx_UART
('x');
  tx_UART
(num2sym(c>>4));
  tx_UART
(num2sym©);  
}

void tx_word_UART(uint16_t c)
{
  tx_UART
('0');
  tx_UART
('x');
  tx_UART
(num2sym(c>>12));
  tx_UART
(num2sym(c>>8));
  tx_UART
(num2sym(c>>4));  
  tx_UART
(num2sym©);  
}

void tx_UART(uint8_t c)
{
 
while ((USART_ISR(USART1) & USART_ISR_TXE)==0); // TX empty
  usart_send
(USART1, c);  
}

void buff_UART(uint8_t *pointer, uint8_t length)
{
 
do
 
{
    uint8_t data
= *pointer;
    tx_UART
(num2sym(data>>4));
    tx_UART
(num2sym(data));      
    tx_UART
(' ');
    pointer
++;
 
} while(--length);
  tx_UART
(0xa);
  tx_UART
(0xd);
}



void int2s(uint8_t *String, int16_t Data)
{
  ldiv_t
DivNum;
  uint8_t
* Pointer;
 
bool sign = false;
 
if(Data<0)
 
{
    sign
= true;
   
Data = -Data;
 
}
 
 
Pointer = String;
 
for(uint8_t i=0; i<5; i++) *Pointer++ = ' ';
 
*Pointer = 0;
 
Pointer = String+4;
 
for(uint8_t i=0; i<4; i++)
 
{
   
if (Data<10)
   
{
     
*Pointer--= (char)Data+'0';
     
break;
   
}  
   
else
   
{  
     
DivNum = ldiv(Data, 10);    
     
*Pointer-- = (char)DivNum.rem+'0';
     
Data = DivNum.quot;
   
}      
 
}  
 
if (sign) *Pointer = '-';
}  

void str2UART(uint8_t *pointer)
{
  uint8_t maxl
=16;
 
do
 
{
    uint8_t data
= *pointer;
   
if (data==0) break;
    tx_UART
(data);
    pointer
++;
 
} while(--maxl);
}

void int2UART(uint16_t Number)
{
  uint8_t tx_buff
[16];
  int2s
(tx_buff, Number);
  str2UART
(tx_buff);
}



Больше большие куски кода в текст вставлять не буду, все можно загрузить отсюда. Так как неизвестно, что когда отключат и запретят, лежит на майлрушном облаке, его вряд ли тронут. Вроде как гитхаб уже грозился пакость с отключением устроить. Хотя если бы Муське была бы возможность сохранять небольшие файлы — это было бы намного удобно.

Теперь пробуем поиграться с пультом управления, заодно разберемся со структурой пакетов, которые пульт отправляет. Библиотеку, написанную для XN297 найдете в архиве, ссылка выше. Сам тест выглядит так:

Тест XN297

#include <XN297.h>
#include <SPI.h>
#include <UART.h>
#include <system.h>

void CheckVoltage(void)
{
  adc_read
();
  delay
(100);
  adc_read
();
 
if (adc_read()<CODE_MIN)
 
{  
   
for(uint8_t j=0; j<6; j++)
   
{
      uint8_t mask
= 1;
     
for(uint8_t i=0; i<5; i++)
     
{
       
LedMask(mask);
        mask
<<=1;
        delay
(100);
     
}
   
}
    standby
();
 
}
}

void TestRadio(void)
{
 
static bool binded = false;
  uint16_t out16
;
 
static uint8_t blink_cnt=0;
  out16
= transfer_word_SPI(0x07, 0xFF);
 
if (out16 & 0x40)
 
{
   
if (binded) rx_pack();
   
else
   
{
      bind
();
      binded
= true;
     
LedClear(0x10);
   
}
    buff_UART
(rx_buff,16);
 
}
 
else
 
{
   
if (!binded)
   
{
      blink_cnt
++;
     
if (blink_cnt==50)
     
{
        blink_cnt
=0;
       
LedToggle(0x10);
     
}
   
}
 
}
  delay
(2);
}
 

int main(void)
{
  system_init
();
  init_LEDs
();
  adc_init
();
  init_USART
();
  init_SPI
();
  init_XN297
();
 
CheckVoltage();
  uint8_t mask
= 1;
 
for(uint8_t i=0; i<5; i++)
 
{
   
LedMask(mask);
    mask
<<=1;
    delay
(500);
 
}
 
while (1)
 
{
   
TestRadio();
 
}
}



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





Управлять можно моторчиками с помощью PWM или сервами с PPM — в библиотеках все есть.

Крутим моторчики и включаем светодиодики с пульта


uint8_t u8map
(int16_t x, int16_t in_min, int16_t in_max, int16_t out_min, int16_t out_max)
{
 
return  (x - in_min) *(out_max - out_min) / (in_max - in_min) + out_min;
}


int main(void)
{
  system_init
();
  init_LEDs
();
  init_PWM
();
  init_SPI
();
  init_USART
();
  init_XN297
();

 
bool binded = false;
 
while (1)
 
{
    uint16_t out16
;
    out16
= transfer_word_SPI(0x07, 0xFF);
   
if (out16 & 0x40)
   
{
     
if (binded)
     
{
        rx_pack
();
        buff_UART
(rx_buff,16);    
        uint8_t status
;
        status
= rx_buff[14];
       
if(status &0x04) gpio_set(GPIOA, GPIO2);  else gpio_clear(GPIOA, GPIO2);
       
if(status &0x08) gpio_set(GPIOA, GPIO4);  else gpio_clear(GPIOA, GPIO4);
       
if(status &0x10) gpio_set(GPIOA, GPIO12); else gpio_clear(GPIOA, GPIO12);
       
if(status &0x20) gpio_set(GPIOB, GPIO0);  else gpio_clear(GPIOB, GPIO0);
       
if(status &0x40) gpio_set(GPIOB, GPIO2);  else gpio_clear(GPIOB, GPIO2);
        status
= rx_buff[6];
        status
= u8map(status, 0, 0xff, 0, 100);
        timer_set_oc_value
(TIM1, TIM_OC1, status);
        status
= rx_buff[7];
        status
= u8map(status, 0x43, 0xbb, 0, 100);
        timer_set_oc_value
(TIM1, TIM_OC2, status);
        status
= rx_buff[8];
        status
= u8map(status, 0x43, 0xbb, 0, 100);
        timer_set_oc_value
(TIM2, TIM_OC1, status);
        status
= rx_buff[9];
        status
= u8map(status, 0x43, 0xbb, 0, 100);
        timer_set_oc_value
(TIM2, TIM_OC2, status);        
     
}
     
else
     
{
        bind
();
        binded
= true;
     
}
   
}
    delay
(100);
 
}
}



Для полноты картины добавим в библиотеки функции для работы с MPU6881. Вообще-то он мне не нужен, но уж раз есть — надо иметь возможность пользоваться. Общеизвестно, что определить положение движущегося объекта при помощи только акселерометра достоверно нельзя, нужен еще гироскоп и, если совсем по-хорошему, и магнетометр. И арифметику вспомнить придется. Вы еще помните, что такое кватернионы? Я — нет.



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

Тест IMU

cImu IMU
;
uint8_t cout
;

void TestImu(void)
{
    IMU
.readAccelData();  // Read the x/y/z adc values
    IMU
.readGyroData();  // Read the x/y/z adc values
    IMU
.Mahony_no_mag_Update();
    delay
(20);
    cout
++;
   
if (cout==50)
   
{
      IMU
.quater2euler();
      cout
=0;
      int16_t
Angles[3];
     
Angles[0] = (int16_t)(IMU.roll);
       
Angles[1] = (int16_t)(IMU.pitch);
     
Angles[2] = (int16_t)(IMU.yaw);
     
     
for(uint i=0; i<3; i++)
     
{
        int2UART
(Angles[i]);
        tx_UART
(' ');  
     
}
      tx_UART
(0xa);
      tx_UART
(0xd);      
   
}  
}


int main(void)
{
  system_init
();
  init_LEDs
();
  init_USART
();
  init_i2c
();
 
 
        IMU
.resetMPU9250(); // Reset registers to default in preparation for device calibration
        IMU
.calibrateMPU9250(IMU.gyroBias, IMU.accelBias); // Calibrate gyro and accelerometers, load biases in bias registers
        IMU
.initMPU9250();
  IMU
.deltat = 0.02;
  cout
=0;
 
while(1) TestImu();
}



В этой библиотеке вы также найдете общеизвестный трюк по извлечению обратного квадратного корня из Quake III Arena.



Для чего я этот квадрик расковырял — может, в другой раз расскажу. А пока позвольте на этом откланяться — может, кто-то чего-нибудь полезного найдет в моем рассказике.
Добавить в избранное +21 +24
свернуть развернуть
Комментарии (6)
RSS
+
avatar
+1
Ну не знаю, какое-то противоречивое ощущение после обзора осталось.
С одной стороны конечно хороший реверсинг, а с другой — зачем вот это двукратное упоминание про «а зачем это всё было — я вам не скажу», и отмазка про «длинный обзор» тут не актуальна — видали раз в 10 больше, от того-же Кирича.
Так что осталось такое себе кликбейтовское послевкусие. Извини ДонБатон, ничего личного, просто моё мнение.
+
avatar
0
В другой раз — это потому, что процесс еще идет, и что получится или ничего — еще не знаю. Собственно, и то, что уже написано, мало кого заинтересовало.
avatar
  • greyyo
  • 02 мая 2022, 17:18
0
Комментарий ожидает проверки администрацией сайта. Подробнее...
+
avatar
0
немного оффтопа, есть нонейм контроллер mi-light 1809-28 (1at159) подозреваю, что общается по протоколу SPI, но как обнаружить среди ножек какая за что отвечает


какими инструментами пользоваться? (осциллографа конечно же нет)
avatar
+1
Логическим анализатором — дешево и сердито
avatar
  • ewavr
  • 02 мая 2022, 16:53
0
Логическим анализатором, стоит копейки.

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.