Эта первая (вводная) статья серии о том, как я собираюсь доработать медиасистему автомобиля. Сам проект в процессе, времени, как и у всех — нет, поэтому, дорогие читатели, запаситесь терпением, ибо часто клепать статьи не обещаю.
А началось все с того, что у меня появился Prius.
И первое, что бросилось в глаза — проблемы с обновлением навигации. Следующее — весьма скудные, но местами необходимые возможности устройства с названием «Многофункциональный дисплей» (в простонародье — голова). И это на фоне огромного количества китайских радио с Android на борту, и множеством приятностей. Но их установка на штатное место подразумевает лишение таких «плюшек», как диаграмма распределения энергии и управление климатом.
Родилась идея как-то соединить Android магнитолу с автомобилем более плотно, чем предлагают братья-китайцы. Об этом и статья.
Исходная ситуация
Итак. На борту имеется около 7-дюймовый дисплей с резистивным тач-скрином, соединенный с прочей электроникой линиями TX+ и TX-. И таких пар от головы идет аж 3. В схеме это чудо поименовано AVC-LAN, и выглядит следующим образом:
Часть 1: Осматриваемся внутри
Как видно, голова стоит в разрыве сети, между маршрутизатором и дальнейшей цепочкой из магнитолы, усилителя (он отдельный у меня), и по отдельному каналу следует связь с блоком навигации. Где-то еще болтается блок автопарковки, никак не упомянутый в имеющихся у меня схемах. Ну, что ж… я решил отложить близость с оным до лучших времен. Тем более, что автопарковка — скорее игровая ф-ция, нежели реально нужная.
Убрав все лишнее, получим примерно следующую блок-схему устройств:
Размышления
Была мысль просто заменить блок навигации на что-нибудь андроидное, однако она угасла, когда я глубже разобрался, как они общаются с головой. Помимо AVC-LAN эти модули соединены так же линией GVIF (Gigabit Video InterFace), причем этот самый фэйс у производителей конвертеров может случайно треснуть, если еще и я куплю преобразователь видеосигнала в GVIF за более, чем 100 долл. «Жить без лица — быть может трудно, но..» — прозвучало в голове на мотив известной песни, и решение мне не понравилось.
Встречались в сети решения с установкой китайской магнитолы вместо радиоресивера. Это меня не устроило тем, что два дисплея — необоснованная избыточность. Имхо.
Решение
Родилось следующее решение: заменить целиком голову, и доработать андроид-магнитолу, подружив ее с Prius-ом, для чего:
- Разработать аппаратный конвертер USB <-> AVC-LAN
- Разработать firmware к нему, чтобы он подключался, как USB-HID.
- Сделать его composite, чтобы одна из функций детектировалась, как обычная аппаратная клавиатура (с целью использовать в качестве нативного управления с кнопок на панели)
- Разработать Android-приложение с функционалом, аналогичным (или превосходящим) родной, приусовский
- Согласовать работу задней камеры
- Решить задачи по механической части (установка на штатное место)
В процессе предстит разработать еще одно приложение для андроид — обыкновенный снифер, чтобы удобнее было реверсить пакеты по AVC-LAN. Заодно и потренироваться.
Выглядеть это все должно следующим образом:
В качестве аппаратной основы было решено использовать обучающую плату на SM32F103:
заказанную с AliExpress за $2.05.
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, я обнаружил в сети одну неприятную вещь: практически нет или весьма скудно рассмотрен вопрос создания составных устройств.
В том виде, в котором они есть, выкладывать бессмысленно — бардака в интернете и без меня хватает.
USB, Composite, HID
Буквально несколько слов на эту тему. Предполагается, что Вы более или менее знакомы со стандартом USB. Если нет — лучше сначала ознакомится и поэкспериментировать с примерами из CubeFX.
Итак, имеем:
Стек USB от STM и пример реализации мыши. Там у нас настроены какие-то дескрипторы и функциональная конечная точка. Это помимо пары 0x00 и 0x80 для управления устройством целиком.
Для реализации моего проекта требуется, чтобы конечная точка клавиатуры была двунаправленной (не знаю, зачем — пригодится) и еще пара конечных точек, которые будут использованы для обмена данными со второй — 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,
И добавляем еще пару точек:
/* 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 самостоятельно занимается процессом приема/передачи пакета данных, нужно задать ему буферы в этом кусочке памяти для каждой отдельно взятой конечной точки:
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 с константой в виде конечной точки. Решение пока не самое эстетичное, но для эксперимента сойдет, и даже если и останется — конкретному проекту это жить мешать не будет.
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_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.:
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;
}
Далее в программе:
- Сборка преобразователя логических уровней AVC-LAN, и подключение к плате. Анализ физического уровня AVC-LAN, реальные осциллограммы.
- Обработка интерфейса на уровне контроллера и отправка пакетов репортами
- Сквозной интерфейс и реверс-инжиниринг Prius. Снифер пакетов (или мое первое Android-приложение)
P.S.
- Статью решил написать, поскольку меня заставили (почти), убедив, что этим нужно делиться. Даже если и не доведу проект до конца, некоторое количество свежей информации может кому-то помочь даже в «сыром» виде.
- Критика проекта приветствуется, т.к. сам пока не до конца представляю, что это получится.
- Критика статьи, оформления, изложения — особенно, т.к. это первая статья для ресурса. При продолжении работы хотелось бы излагать мысли в привычном и удобоваримом для читателей виде
Автор: remixoff