[Из песочницы] Медиасистема для Toyota Prius (рестайл)

pwa0k_aaxeguedncth7a5fdkm70.jpeg

Эта первая (вводная) статья серии о том, как я собираюсь доработать медиасистему автомобиля. Сам проект в процессе, времени, как и у всех — нет, поэтому, дорогие читатели, запаситесь терпением, ибо часто клепать статьи не обещаю.

А началось все с того, что у меня появился Prius.

И первое, что бросилось в глаза — проблемы с обновлением навигации. Следующее — весьма скудные, но местами необходимые возможности устройства с названием «Многофункциональный дисплей» (в простонародье — голова). И это на фоне огромного количества китайских радио с Android на борту, и множеством приятностей. Но их установка на штатное место подразумевает лишение таких «плюшек», как диаграмма распределения энергии и управление климатом.

Родилась идея как-то соединить Android магнитолу с автомобилем более плотно, чем предлагают братья-китайцы. Об этом и статья.

Исходная ситуация


Итак. На борту имеется около 7-дюймовый дисплей с резистивным тач-скрином, соединенный с прочей электроникой линиями TX+ и TX-. И таких пар от головы идет аж 3. В схеме это чудо поименовано AVC-LAN, и выглядит следующим образом:

mluolei1uqdz85xpjdh43hxjyvk.png

Часть 1: Осматриваемся внутри


Как видно, голова стоит в разрыве сети, между маршрутизатором и дальнейшей цепочкой из магнитолы, усилителя (он отдельный у меня), и по отдельному каналу следует связь с блоком навигации. Где-то еще болтается блок автопарковки, никак не упомянутый в имеющихся у меня схемах. Ну, что ж… я решил отложить близость с оным до лучших времен. Тем более, что автопарковка — скорее игровая ф-ция, нежели реально нужная.

Убрав все лишнее, получим примерно следующую блок-схему устройств:

qbazglszcggkjhudk80w91l9tdg.png

Размышления


Была мысль просто заменить блок навигации на что-нибудь андроидное, однако она угасла, когда я глубже разобрался, как они общаются с головой. Помимо AVC-LAN эти модули соединены так же линией GVIF (Gigabit Video InterFace), причем этот самый фэйс у производителей конвертеров может случайно треснуть, если еще и я куплю преобразователь видеосигнала в GVIF за более, чем 100 долл. «Жить без лица — быть может трудно, но…» — прозвучало в голове на мотив известной песни, и решение мне не понравилось.

Встречались в сети решения с установкой китайской магнитолы вместо радиоресивера. Это меня не устроило тем, что два дисплея — необоснованная избыточность. Имхо.

Решение


Родилось следующее решение: заменить целиком голову, и доработать андроид-магнитолу, подружив ее с Prius-ом, для чего:

  1. Разработать аппаратный конвертер USB <-> AVC-LAN
  2. Разработать firmware к нему, чтобы он подключался, как USB-HID.
  3. Сделать его composite, чтобы одна из функций детектировалась, как обычная аппаратная клавиатура (с целью использовать в качестве нативного управления с кнопок на панели)
  4. Разработать Android-приложение с функционалом, аналогичным (или превосходящим) родной, приусовский
  5. Согласовать работу задней камеры
  6. Решить задачи по механической части (установка на штатное место)


В процессе предстит разработать еще одно приложение для андроид — обыкновенный снифер, чтобы удобнее было реверсить пакеты по AVC-LAN. Заодно и потренироваться.

Выглядеть это все должно следующим образом:

_x7jf2mr_9mp0ow3yxwds8yuhuc.png

В качестве аппаратной основы было решено использовать обучающую плату на SM32F103:

oou81yrznrr2hcnayb2emjfb_lw.jpeg

заказанную с AliExpress за $2.05.

Или поискать — спойлер

Возможно, лот уже удален продавцом, поэтому даю магическую строку для поиска по Ali:
STM32F103C8T6 ARM STM32 Minimum System Development Board Module


Чем она мне нравится:

  • Аппаратный модуль USB (Device) на борту у процессора
  • Адекватный USB-стек от производителя (в отличие от Freescale-овского, не к ночи будь помянут).
  • Свободные порты GPIO, которые можно использовать для подключения штатных кнопок по бокам монитора. Возможно, это позволит скрыть под панелью аппаратные кнопки магнитолы. Я пока не знаю, какой она будет
  • И на нее можно навесить конвертер AVC-LAN в логические уровни


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

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

Часть 2: USB, HID, дескрипторы, и все, чтобы получить пилотный прототип


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

USB HID Composite device на STM32


За что я решил взяться — это адаптировать пример от ST моим задачам, и получить USB устройство, которое опознается хостом, как составное из клавиатуры и «чего-то еще» — RAW HID Device. Первое, как я уже говорил, предназначено для нативного управления андроидом, второе — для прямого обмена AVC-LAN пакетами с программой на устройстве.

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

Исходные коды будут потом

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


USB, Composite, HID


Буквально несколько слов на эту тему. Предполагается, что Вы более или менее знакомы со стандартом USB. Если нет — лучше сначала ознакомится и поэкспериментировать с примерами из CubeFX.

Итак, имеем:

Стек USB от STM и пример реализации мыши. Там у нас настроены какие-то дескрипторы и функциональная конечная точка. Это помимо пары 0×00 и 0×80 для управления устройством целиком.

Для реализации моего проекта требуется, чтобы конечная точка клавиатуры была двунаправленной (не знаю, зачем — пригодится) и еще пара конечных точек, которые будут использованы для обмена данными со второй — RAW — функцией. Добавляем их.

Делаем точку двунаправленной, добавляя в дескриптор точку OUT:

Дескриптор конфигурации.
При правке дескриптора следите за индексами и размерами.
(2c5cf968121f0d8fa43a6755c09e15ef3a317791):
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/
  
  HID_EPOUT_ADDR,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  HID_EPOUT_SIZE, /*wMaxPacketSize: 4 Byte max */
  0x00,
  HID_FS_BINTERVAL,  



И добавляем еще пару точек:

Дескриптор конфигурации
(bc2bd583c98715e106fcb3ab07b266bc9221be36):
  /* 59 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/
  
  HID_EPIN_ADDR2,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  HID_EPIN_SIZE, /*wMaxPacketSize: 4 Byte max */
  0x00,
  HID_FS_BINTERVAL,          /*bInterval: Polling Interval (10 ms)*/
  /* 66 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/
  
  HID_EPOUT_ADDR2,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  HID_EPOUT_SIZE, /*wMaxPacketSize: 4 Byte max */
  0x00,
  HID_FS_BINTERVAL,          /*bInterval: Polling Interval (10 ms)*/



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

1. В нашем контроллере есть специально выделенный кусочек памяти, который тактируется вместе с модулями CAN и USB. Учитывая, что модуль USB самостоятельно занимается процессом приема/передачи пакета данных, нужно задать ему буферы в этом кусочке памяти для каждой отдельно взятой конечной точки:

USBD_LL_Init в файле usbd_conf.c
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , HID_EPOUT_ADDR , PCD_SNG_BUF, 0x100);
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , HID_EPIN_ADDR , PCD_SNG_BUF, 0x140);
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , HID_EPOUT_ADDR2 , PCD_SNG_BUF, 0x180);
  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , HID_EPIN_ADDR2 , PCD_SNG_BUF, 0x1B0);



Адреса буферов произвольные, лишь бы не пересекались.

Почему-то стек от ST написан из расчета, что в устройстве будет не более одной двунаправленной конечной точки, потому чуть дорабатываем стек:

Передача


Процедуру USBD_HID_SendReport переименовываем в USBD_HID_SendReportEP, добавляя еще один параметр — номер конечной точки. Процедуру со старым именем оставляем для обратной совместимости, но в теле вызываем USBD_HID_SendReportEP с константой в виде конечной точки. Решение пока не самое эстетичное, но для эксперимента сойдет, и даже если и останется — конкретному проекту это жить мешать не будет.

usbd_hid.c
uint8_t USBD_HID_SendReportEP (USBD_HandleTypeDef  *pdev, 
                                 uint8_t ep,
                                  uint8_t *report,
                                  uint16_t len)
{
 ... тело, бывшее раньше USBD_HID_SendReport
}

uint8_t USBD_HID_SendReport     (USBD_HandleTypeDef  *pdev, 
                                 uint8_t *report,
                                 uint16_t len)
{
  return USBD_HID_SendReportEP(pdev,HID_EPIN_ADDR,report,len);
  
}



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

Финализация


Порядка ради ищем по проекту и вызываем USBD_LL_CloseEP еще раз, но для вновь созданных конечных точек.

Прием


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

У меня в коде получилась вот такая вот некрасивая каракатица:

usbd_core.c
USBD_LL_PrepareReceive(pdev, HID_EPOUT_ADDR+(epnum&0x7F)-1 , hhid->Report_buf,
                          USBD_HID_OUTREPORT_BUF_SIZE);



Т.е. я исходил из того, что номера конечных точек идут подряд. Это плохо, имхо. Не делайте так. Впрочем, и как ST тоже не делайте.

Дальше остается только сходить в файл usbd_hid.c, а конкретно в функцию USBD_HID_DataOut, и позаботится о том, чтобы вызов обработчика принятых данных соответствовал вашим личным представлениям о прекрасном. У меня получилось тоже не очень, поэтому код и описание получатся длинными и непонятными. Проще сделать самому.

Репорт


Все, в этом месте мы получили композитное устройство, которое способно обмениваться данными через две двунаправленные точки. Последним штрихом «затыкаем» любопытство драйверу HID, описывая такой вот дескриптор репорта:

__ALIGN_BEGIN static uint8_t HID_ReportDesc2[33]  __ALIGN_END =
{
    0x06, 0x00, 0xff,              // USAGE_PAGE (Vendor Defined Page 1)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x1f,                    //   REPORT_COUNT (31)
    0x09, 0x00,                    //   USAGE (Undefined)
    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
    0x85, 0x02,                    //   REPORT_ID (2)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x1f,                    //   REPORT_COUNT (31)
    0x91, 0x00,                    //   OUTPUT (Data,Ary,Abs)
    0xc0                           // END_COLLECTION

}; 


Этот репорт говорит HID-драйверу: тут будут какие-то 31 байт данных. Не нужно разбираться, что за они — просто отдай их открывшей это устройство программе. В физическом репорте нулевой байт будет равен индексу репорта (REPORT_ID (2)). Соответственно, всего придет 32 байта.

И вписываем данные о нем в usbd-hid.c, функция USBD_HID_Setup.:

usbd-hid.c
    switch (req->bRequest)
    {
    case USB_REQ_GET_DESCRIPTOR: 
      if( req->wValue >> 8 == HID_REPORT_DESC)
      {
// TODO: !!! Отдать нужный дескриптор, в зависимости от значения req->wIndex !!!
        THIDDescPtrLen * rep = (req->wIndex==1)?&HID_ReportDesc:&HID_ReportDesc2;
        len = MIN(rep->len , req->wLength);
        pbuf = rep->ptr;
      }


Далее в программе:


  1. Сборка преобразователя логических уровней AVC-LAN, и подключение к плате. Анализ физического уровня AVC-LAN, реальные осциллограммы.
  2. Обработка интерфейса на уровне контроллера и отправка пакетов репортами
  3. Сквозной интерфейс и реверс-инжиниринг Prius. Снифер пакетов (или мое первое Android-приложение)


P.S.


  • Статью решил написать, поскольку меня заставили (почти), убедив, что этим нужно делиться. Даже если и не доведу проект до конца, некоторое количество свежей информации может кому-то помочь даже в «сыром» виде.
  • Критика проекта приветствуется, т.к. сам пока не до конца представляю, что это получится.
  • Критика статьи, оформления, изложения — особенно, т.к. это первая статья для ресурса. При продолжении работы хотелось бы излагать мысли в привычном и удобоваримом для читателей виде

© Geektimes