Введение

Разработчики автомобильных систем диагностики уже давно присматриваются к стандартам с большей скоростью передачи данных, чем классический 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. Соответственно, работа тоже разбивается на два этапа:

  1. 6811/UDP: обнаружение и идентификация автомобиля;

  2. 6801/TCP: обмен полезными данными (диагностика, кодирование, программирование).

Формат пакета

Порт 6811/UDP задействуется только на первом этапе работы. Формат пакета в этом случае следующий:

Все очень просто, есть всего три поля: длина полезной нагрузки, тип пакета и сама полезная нагрузка, например ASCII-строка с VIN-номером внутри.

На втором этапе используется уже порт 6801/TCP, но формат от этого меняется не сильно:

Поле полезной нагрузки теперь содержит адрес источника, адрес назначения и само UDS-сообщение.

Таблица с возможными типами пакетов:

Packet Type

Port

Description

0x0001

6801/TCP

message

0x0002

6801/TCP

echo

0x0011

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

0x10

ZGM diag (Central Gateway Module)

0x18

DME (Digital Motor Electronics)

0x40

CAS (Car Access System)

0x72

FRM (Footwell Module)

0xDF

broadcast

0xF0

ZGM internal (Central Gateway Module)

0xF1

CAN diag

0xF4

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):

Тип ошибки

Описание

0x40

Неправильный адрес источника

0x41

Неправильный тип пакета (пакет полный, но такой тип не поддерживается)

0x42

Ошибка формата пакета (например, не хватает адресов)

0x43

Неправильный адрес назначения

0x44

Превышен максимальный размер полезной нагрузки

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

Заключение

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

Комментарии (3)


  1. atd
    23.09.2022 14:53

    Очень круто!

    Это вы всё сами накопали, или перевод?

    Не пробовали подсмотреть, что там E-Sys передаёт?


    1. pilot2k
      23.09.2022 14:58

      WireShark в руки и все как на ладони


    1. embeduin Автор
      23.09.2022 15:57
      +2

      Спасибо, да сам расковыривал на досуге. Использовал WireShark + Python.

      Логи E-Sys смотрел, но там совсем огрызки были, потому что блок только один.

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