Embedded

Расшифровка CAN-сообщений: DBC-файлы и сигналы

Введение

Сырые байты CAN-кадра из статьи 363 сами по себе не несут понятного смысла без знания, как конкретно закодированы значения внутри них — DBC-файл (формат от Vector Informatik, фактический отраслевой стандарт) описывает это сопоставление декларативно, аналогично тому, как .proto-файлы (gRPC-цикл, статья 323) описывают структуру Protobuf-сообщений. В статье разберём формат DBC и его применение для расшифровки реальных автомобильных сообщений.

Концепция

DBC-файл описывает каждое CAN-сообщение (по идентификатору) как набор «сигналов» — отдельных полей внутри 8 байт данных, каждый со своим битовым смещением, длиной в битах, множителем и смещением (для преобразования «сырого» целочисленного значения в реальную физическую величину — например, температуру в градусах), и единицей измерения. Без DBC-файла (или эквивалентного знания протокола, документированного производителем или восстановленного через reverse engineering) байты CAN-сообщения остаются непрозрачными — именно поэтому статья 361 явно предупреждала о невозможности интерпретации произвольных, неизвестных идентификаторов без дополнительной информации.

Пример кода: Расшифровка CAN-сообщений

; engine.dbc — фрагмент DBC-файла, описывающий сообщение оборотов двигателя
BO_ 192 EngineData: 8 ENGINE_ECU
 SG_ EngineRPM : 0|16@1+ (0.25,0) [0|16000] "rpm" Vector__XXX
 SG_ EngineTemp : 16|8@1+ (1,-40) [-40|215] "degC" Vector__XXX

; BO_ 192       — идентификатор сообщения (192 в десятичной системе = 0xC0)
; SG_ EngineRPM — имя сигнала
; 0|16          — битовое смещение 0, длина 16 бит
; @1+           — порядок байт (little-endian) и знак (unsigned)
; (0.25,0)      — множитель 0.25, смещение 0 — формула: raw_value * 0.25 + 0
; [0|16000]     — допустимый диапазон значения
; "rpm"         — единица измерения
// Упрощённая, ручная реализация расшифровки конкретного сигнала из DBC-описания выше
// (для production-кода стоит использовать готовую библиотеку парсинга DBC, см. подводные камни)
double extractEngineRpm(const QByteArray &payload)
{
    if (payload.size() < 2) return 0.0;

    // Битовое смещение 0, длина 16 бит, little-endian — первые два байта как uint16
    uint16_t rawValue = static_cast<uint8_t>(payload[0]) | (static_cast<uint8_t>(payload[1]) << 8);

    // Применение формулы из DBC: raw_value * множитель + смещение
    return rawValue * 0.25 + 0.0;
}

double extractEngineTemp(const QByteArray &payload)
{
    if (payload.size() < 3) return 0.0;

    // Битовое смещение 16 (третий байт), длина 8 бит
    uint8_t rawValue = static_cast<uint8_t>(payload[2]);

    return rawValue * 1.0 + (-40.0); // множитель 1, смещение -40 — типичная кодировка для температуры
}
// Использование в обработчике принятых кадров (продолжение CanReceiver из статьи 363)
void handleFrame(const QCanBusFrame &frame)
{
    if (frame.frameId() == 0x0C0) { // соответствует BO_ 192 = 0xC0 в DBC-файле
        double rpm = extractEngineRpm(frame.payload());
        double temp = extractEngineTemp(frame.payload());
        qDebug() << "Обороты двигателя:" << rpm << "об/мин, температура:" << temp << "°C";
    }
}

Пояснения к коду

DBC-фрагмент явно описывает математическую формулу преобразования «сырых» битов в физическую величину (raw_value * 0.25 + 0 для оборотов двигателя) — без этого описания значение rawValue из примера было бы просто числом без какого-либо понятного физического смысла; именно эта декларативная формула и есть основная ценность DBC-файла, аналогично тому, как номера полей .proto-файла придают смысл сырым байтам Protobuf-сообщения. extractEngineRpm()/extractEngineTemp() показывают ручную, ad-hoc реализацию расшифровки на основе конкретного, явно прочитанного DBC-описания — этот подход работоспособен для небольшого числа известных, стабильных сигналов, но плохо масштабируется на полный DBC-файл реального автомобиля, который может содержать сотни сообщений с тысячами отдельных сигналов.

Подводные камни

  • Ручная реализация расшифровки каждого сигнала по отдельности (как в упрощённом примере) для DBC-файла с большим количеством сообщений и сигналов — это быстро становится неподъёмным по объёму ручного, повторяющегося кода для реального автомобильного DBC-файла, типично содержащего сотни сообщений; для production-кода стоит использовать готовую библиотеку парсинга DBC (например, dbcppp, cantools на Python для офлайн-анализа), автоматически генерирующую или интерпретирующую формулы расшифровки непосредственно из DBC-файла, а не переписывающую их вручную для каждого отдельного сигнала.
  • Игнорирование порядка байт (endianness), явно указанного в DBC-файле через нотацию @1 (little-endian) или @0 (big-endian) — попытка применить неверный порядок байт при расшифровке многобайтовых сигналов даёт совершенно неверное числовое значение без какой-либо явной ошибки, что особенно опасно, поскольку неверное, но «похожее на правдоподобное» значение легко принять за корректное без дополнительной, независимой проверки.
  • Использование DBC-файла, не соответствующего точно той версии прошивки/модели автомобиля, для которого он был изначально документирован — производители нередко изменяют кодировку сигналов между разными моделями или даже между разными ревизиями прошивки одной и той же модели, и DBC-файл, найденный для похожей, но не идентичной системы, может дать систематически неверные значения, выглядящие правдоподобно, но содержащие постоянную ошибку.
  • Отсутствие проверки диапазона полученного значения относительно документированного допустимого диапазона ([0|16000] в примере DBC) — значения, выходящие далеко за документированный физически разумный диапазон, обычно сигнализируют о неверной интерпретации сообщения (неправильный идентификатор, ошибка в формуле расшифровки) или о реальной аномалии в самой системе, и явная проверка диапазона помогает отличить эти случаи от штатной работы, а не молча принимать любое полученное число как корректное.