Введение
Разработчики автомобильных систем диагностики уже давно присматриваются к стандартам с большей скоростью передачи данных, чем классический CAN. С каждым новым поколением растет количество электронных блоков в автомобилях, их сложность и размер памяти. Без использования высокоскоростных способов передачи данных, тщательная диагностика и обновление прошивок могут затянуться на несколько часов, снижая эффективность работ по обслуживанию.
Одна из возможных опций - применение Ethernet. В BMW подумали так же и разработали собственный протокол для решения этой задачи.
Чтобы заранее устранить возможную путаницу, обозначим два важных "не"
применяется не Automotive Ethernet, а самый обычный, "бытовой" Ethernet;
применяется не стандартный DoIP (Diagnostics over Internet Protocol), а проприетарный протокол BMW HSFZ (High Speed Fahrzeugzugang).
Чем может быть интересен HSFZ? Во-первых, своей "проприетарщиной" и отличиями от DoIP, а во-вторых, он появился раньше DoIP на несколько лет. Первая версия DoIP (ISO 13400-2) вышла в 2012 году, к этому времени на рынке уже вовсю продавались некоторые модели F-серии, с поддержкой диагностики по Ethernet.
Кратко про стек
Упрощенно стек протоколов используемых для диагностики представлен ниже:
Основным протоколом является UDS (Unified Diagnostic Services), который совершает всю прикладную работу - устанавливает диагностическую сессию, читает и записывает значения, запрашивает и стирает ошибки. UDS-сообщения могут быть очень длинными и превышать крошечный лимит в 8 байт, предлагаемый CAN. ISO-TP разбивает крупное сообщение на маленькие порции, годные для передачи по CAN.
Так устроены системы диагностики по CAN. При работе по Ethernet уже не нужно бороться с ограничениями по размеру, но добавляются задачи по обнаружению и идентификации автомобиля, так как Ethernet предполагает возможность подключения сразу нескольких автомобилей к одной сети. Транспортировкой UDS-сообщений занимаются протоколы HSFZ и DoIP, являясь альтернативами друг-другу.
Железо
Осмотр подопытного
Для экспериментов на разборке был приобретен битый BMW модуль ZGM (Central Gateway Module) от BMW 7-ой серии (F01):
ZGMы бывают нескольких видов, зависят от конкретных моделей и комплектаций, могут отличаться друг от друга материалом корпуса, наличием и количеством интерфейсов. Поддерживаемых интерфейсов действительно много: Ethernet, High-Speed CAN, Fault-Tolerant CAN, FlexRay, MOST25.
Скинем металлический панцирь и посмотрим на плату:
# |
Chip |
Manufacturer |
Description |
---|---|---|---|
1 |
LA4032V |
Lattice |
CPLD |
2 |
OS81050AQ |
Oasis |
MOST25 Network Interface Controller |
3 |
KSZ8893MQL |
Micrel |
10/100 Ethernet Switch |
4 |
MPC5567MVR132 |
NXP |
32-bit MCU |
5 |
CY7C1011DV33 |
Cypress |
CMOS Static RAM |
6 |
25LC256E |
Microchip |
Serial EEPROM |
7 |
MEGA48-15AT1 |
AVR |
8-Bit MCU |
8 |
91056B |
ELMOS |
Flexray Transceiver (up to 10 Mbit/s) |
Заглянем и на обратную сторону:
# |
Chip |
Manufacturer |
Description |
---|---|---|---|
1 |
CY7C1011DV33 |
Cypress |
CMOS Static RAM |
2 |
TJA1041AT |
NXP |
HS CAN transceiver (up to 1 Mbit/s) |
3 |
TJA1055 |
NXP |
FT CAN transceiver (up to 125 kbit/s) |
Проводка и подключение
Эта тема много где расписана подробно, но чтобы сделать статью удобнее для восприятия, этот материал тоже включен. Как было сказано в начале, для сборки переходника достаточно пяти проводов и одного резистора:
Выводы 3, 11, 12 и 13 используются для передачи данных. На выводе 16 постоянно присутствует напряжение 12В, вывод 8 - это активация работы Ethernet и пока на нем не будет напряжения подключиться не получится. Резистор 510 Ом соединяет выводы 8 и 16, номинал можно подобрать и другой, в пределах разумного.
Протокол и обмен данными
Протокол HSFZ занимает два порта: 6811/UDP и 6801/TCP. Соответственно, работа тоже разбивается на два этапа:
6811/UDP: обнаружение и идентификация автомобиля;
6801/TCP: обмен полезными данными (диагностика, кодирование, программирование).
Формат пакета
Порт 6811/UDP задействуется только на первом этапе работы. Формат пакета в этом случае следующий:
Все очень просто, есть всего три поля: длина полезной нагрузки, тип пакета и сама полезная нагрузка, например ASCII-строка с VIN-номером внутри.
На втором этапе используется уже порт 6801/TCP, но формат от этого меняется не сильно:
Поле полезной нагрузки теперь содержит адрес источника, адрес назначения и само UDS-сообщение.
Таблица с возможными типами пакетов:
Packet Type |
Port |
Description |
---|---|---|
|
6801/TCP |
message |
|
6801/TCP |
echo |
|
6811/UDP |
discovery |
Правила адресации
Анализ формата пакета показал, что для адресации источника и назначения достаточно по одному байту. Упрощенная схема автомобиля с подключенным диагностическим оборудованием и адресами:
Получается, если отправить сообщение от Ethernet Diag до блока FRM (Footwell Module), то поле src примет значение 0xf4
, а поле dst - 0x72
.
Диагностику блоков можно производить и по CAN и по Ethernet, в зависимости от этого будет меняться адрес источника. Нельзя одновременно применять оба способа, так как может возникнуть коллизия.
У блока ZGM неспроста указано два адреса. 0x10
используется для работы внешней диагностики с ZGM, например, при чтении ошибок блока. С 0xf0
ситуация чуть хитрее: ZGM ставит это значение в качестве src, когда сам ведет себя как диагностическое оборудование, без подключения внешней диагностики. Так, ZGM может представиться как 0xf0
и отправить диагностический запрос модулю CAS (Car Access System) 0x40
, принуждая его ответить VIN-номер:
| ID | DLC | Data |
| 6f0 | 5 | 40 03 22 f1 90 |
При передаче по CAN важно аккуратно использовать поле ID, сообщения с меньшим значением ID имеют больший приоритет. Поэтому исходное значение src0xf0
"сдвигается" на0x600
и превращается в 0x6f0
, чтобы не мешать общению важных блоков автомобиля. Адрес назначения dst 0x40
просто лежит первым байтом сообщения.
Адрес 0xdf
- широковещательный и применяется когда нужно работать сразу со всеми блоками. Например, рассылать UDS-сообщение "Tester Present".
Таблица с несколькими адресами (полный список гораздо больше) и расшифровкой аббревиатур:
Address |
Description |
---|---|
|
ZGM diag (Central Gateway Module) |
|
DME (Digital Motor Electronics) |
|
CAS (Car Access System) |
|
FRM (Footwell Module) |
|
broadcast |
|
ZGM internal (Central Gateway Module) |
|
CAN diag |
|
ETH diag |
Обнаружение автомобиля
В условиях автомастерской возможно обслуживание сразу нескольких автомобилей, что подразумевает их одновременное подключение к локальной сети. Чтобы избежать конфликта IP-адресов, ZGM умеет получать адрес динамически по DHCP, а не использует один статический.
Если автомобиль не обнаружил DHCP-сервер, то он присвоит себе IP-адрес из диапазона APIPA (169.254.xxx.xxx
).
Возникает вопрос: как однозначно сопоставить IP-адрес с VIN-номером автомобиля? На помощь приходит первая часть протокола HSFZ, работающая поверх UDP. При подключении к сети ZGM самостоятельно отправляет 3 одинаковых идентификационных сообщения с интервалом в 500 мс. Если диагностическое ПО проспало сообщение, его можно получить заново с помощью поискового запроса (тип пакета = 0x0011
):
0000 00 00 00 00 00 11 ......
На что в ответ придет идентификационное сообщение:
0000 00 00 00 32 00 11 44 49 41 47 41 44 52 31 30 42 ...2..DIAGADR10B
0010 4d 57 4d 41 43 30 30 30 30 30 30 30 30 30 30 30 MWMAC00000000000
0020 30 42 4d 57 56 49 4e 58 34 58 4b 43 38 31 31 38 0BMWVINX4XKC8118
0030 30 43 30 30 30 30 30 30 0C000000
Разберем полезную нагрузку на составляющие:
DIAG ADR 10
: диагностический адрес ZGM;BMW MAC 000000000000
: MAC-адрес ZGM;BMW VIN X4XKC81180C000000
: VIN-номер автомобиля.
Не совсем понятно для чего указывается марка автомобиля, возможно для отличия от других марок концерна, например Rolls-Royce или Mini.
Простой Python-скрипт для обнаружения автомобиля
import socket
bytesToSend = b"\x00\x00\x00\x00\x00\x11"
serverAddressPort = ("169.254.255.255", 6811)
bufferSize = 1024
UDPClientSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
UDPClientSocket.sendto(bytesToSend, serverAddressPort)
msg, addr = UDPClientSocket.recvfrom(bufferSize)
print(msg)
print(addr)
Обмен полезными данными
Автомобиль обнаружен и идентифицирован, можно приступить к обмену полезными данными (диагностика, кодирование, программирование).
Рассмотрим как это работает на примере одного и того же запроса и сравним, как происходит обмен по Ethernet и по CAN. Пусть необходимо обратиться к блоку FRM (0x72
) и прочитать DID 0xf150
. UDS-запрос такого вида будет выглядеть так: 0x22 0xf1 0x50
, где 0x22
- это операция чтения.
Начнем с Ethernet:
Сразу бросается в глаза наличие "эхо" пакета, который ZGM отправляет после получения запроса. Далее UDS-запрос оборачивается в ISO-TP и попадает в CAN-шину, где его принимает FRM, подготавливает ответ и отправляет обратно. Теперь FRM становится источником, а ETH DIAG - назначением, поэтому поля src и dst обмениваются значениями. Ответ доходит по Ethernet до ETH DIAG, где прикладное ПО приступает к его разборке и анализу.
Для CAN схема выглядит попроще:
CAN DIAG отправляет UDS-запрос, обернутый в ISO-TP. Поскольку запрос и адреса выглядят легитимно, ZGM ведет себя абсолютно прозрачно и передает его дальше. После получения запроса блок FRM подготавливает ответ и отправляет его обратно CAN DIAG, поменяв местами адреса.
Обработка ошибок
Любой минимально приемлемый протокол должен уметь сообщать о возможных ошибках. HSFZ в этом случае не исключение, так что давайте попробуем его поломать. У нас в распоряжении несколько болевых точек:
формат пакета;
тип пакета;
адреса источника (src) и назначения (dst);
размер полезной нагрузки.
В ответ на каждый некорректный запрос ZGM возвращает код ошибки в поле типа пакета; значения ошибок начинаются с 0x40
. Скриптами на Python и перебором было проверено большое количество возможных комбинаций, ниже будут показаны только самые наглядные.
Пойдем по-порядку. Начнем с формата пакета и а) отправим пакет вообще без адресов, б) отправим пакет без адреса назначения. Результатом будет ошибка 0x42
:
# no src & no dst -> error (0x42)
| payload len | type | src | dst | payload
TX: | 00 00 00 00 | 00 01 | | |
RX: | 00 00 00 00 | 00 42 | | |
# src = 0xf4 & no dst -> error (0x42)
| payload len | type | src | dst | payload
TX: | 00 00 00 01 | 00 01 | f4 | |
RX: | 00 00 00 00 | 00 42 | | |
Следующий на очереди - тип пакета. Возьмем абсолютно нормальный пакет и подсунем максимально возможное значение типа 0xffff
, что даст ошибку 0x41
:
# packet type = 0x01 -> echo (0x02)
| payload len | type | src | dst | payload
TX: | 00 00 00 05 | 00 01 | f4 | 10 | 22 f1 80
RX: | 00 00 00 05 | 00 02 | f4 | 10 | 22 f1 80
# packet type = 0xffff -> error (0x41)
| payload len | type | src | dst | payload
TX: | 00 00 00 05 | ff ff | f4 | 10 | 22 f1 80
RX: | 00 00 00 00 | 00 41 | | |
Перейдем к адресам и начнем с src. Если перебрать все возможные значения, то можно увидеть, что для всех src неравных 0xf4
и 0xf5
ZGM вернет ошибку 0x40
:
# src = 0x00 -> error (0x40)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | 00 | 00 |
RX: | 00 00 00 02 | 00 40 | ff | 00 |
...
# src = 0xf4 -> echo (0x02)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | f4 | 00 |
RX: | 00 00 00 02 | 00 02 | f4 | 00 |
# src = 0xf5 -> echo (0x02)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | f5 | 00 |
RX: | 00 00 00 02 | 00 02 | f5 | 00 |
...
# src = 0xff -> error (0x40)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | ff | 00 |
RX: | 00 00 00 02 | 00 40 | ff | ff |
Так обнаружился еще один валидный адрес источника 0xf5
, возможно он применяется в других сценариях диагностики.
Перебираем значения для dst и обнаруживаем, что не все адреса могут быть приняты, а также новый тип ошибки - 0x43
:
# dst = 0x03 -> echo (0x02)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | f4 | 03 |
RX: | 00 00 00 02 | 00 02 | f4 | 03 |
# dst = 0x04 -> error (0x43)
| payload len | type | src | dst | payload
TX: | 00 00 00 02 | 00 01 | f4 | 04 |
RX: | 00 00 00 02 | 00 43 | f4 | 04 |
Ради любопытства была составлена таблица валидных значений dst, при которых ZGM соглашается пропускать сообщения через себя:
Осталось доломать размер полезной нагрузки. Просто начинаем с нулевого значения и последовательно инкрементируем его до первого отказа и ошибки 0x44
:
# dst = 0x10 (ZGM), payload length = 0x1003 -> error (0x44)
| payload len | type | src | dst | payload
TX: | 00 00 10 03 | 00 01 | f4 | 10 |
RX: | 00 00 00 00 | 00 44 | | |
Внимание, минутка духоты. Максимальный размер равен 0x1002 = 4098 байт. Ну это понятно, 2 байта - это адреса src и dst, а на сами данные остается 4096 байт. Но ведь ZGM еще должен переварить все это и отправить по CAN, используя ISO-TP в качестве транспорта. Максимальный размер для классической версии ISO-TP равен 4095 байт.
2 + 4095 != 4098. Откуда еще один байт?
Поломав над этим голову и уже заподозрив BMW в допущении off-by-one ошибки, я решил повторить эксперимент, но с другим адресом dst 0x72
, то есть модулем FRM, который подключен к шине K2:
# DST = 0x72, payload length = 0x1002 -> error (0x44)
| payload len | type | src | dst | payload
TX: | 00 00 10 02 | 00 01 | f4 | 72 |
RX: | 00 00 00 00 | 00 44 | | |
Вот и отгадка - максимальный размер стал равен 0x1001 = 4097 байт и теперь все сходится. Получается размер зависит от адреса назначения, для ZGM он был на один байт больше поскольку он подсоединен к диагностике напрямую. В этом случае 4096 байт - это просто размер входного буфера для ZGM, никак не связанный с ограничениями ISO-TP.
Итоговая таблица с предполагаемыми типами ошибок (все они относятся к порту 6801/TCP):
Тип ошибки |
Описание |
---|---|
|
Неправильный адрес источника |
|
Неправильный тип пакета (пакет полный, но такой тип не поддерживается) |
|
Ошибка формата пакета (например, не хватает адресов) |
|
Неправильный адрес назначения |
|
Превышен максимальный размер полезной нагрузки |
Вдогонку отмечу, что во время пыток ZGM иногда начинал упорно отвечать ошибкой 0xff
на все пакеты, но полноценно воспроизвести условия при которых это происходит не удалось. Помогал сброс питания, после которого все возвращалось в норму.
Заключение
Автомобильная электроника и методы диагностики всегда тяжело поддавались стандартизации и унификации, тем более между разными производителями. Протокол HSFZ, появившийся раньше DoIP и применяемый до сих пор - очень любопытный этому пример, который было интересно расковырять.
atd
Очень круто!
Это вы всё сами накопали, или перевод?
Не пробовали подсмотреть, что там E-Sys передаёт?
pilot2k
WireShark в руки и все как на ладони
embeduin Автор
Спасибо, да сам расковыривал на досуге. Использовал WireShark + Python.
Логи E-Sys смотрел, но там совсем огрызки были, потому что блок только один.
Для полной машины логи должны выглядеть интереснее, там будет полный комплект блоков и несколько подсетей.