Оживляем дисплей от приборной панели автобуса

Насколько сложно запустить приборную панель от автобуса, грузовика или ещё какой-то спецтехники и вывести на неё какие-нибудь данные? Возможно ли вообще это сделать? Самое время выяснить это, попробовав провернуть всё самому.
Итак, сегодня мы попробуем запустить дисплейный блок от приборной панели старого автобуса. Разберёмся, по какому протоколу он работает и что с ним ещё можно сделать. Как водится, будет много интересного.
❯ Суть такова
Началось всё это довольно просто: ко мне в руки попал дисплейный модуль Kongsberg Automotive KAntrak 2710, снятый с какого‑то теперь уже неведомого автобуса на авторазборке.
Я давно интересовался, как работает электроника на подобном транспорте и какие у неё отличия в протоколе обмена или чём‑то ещё от того железа, что можно встретить на легковых автомобилях. Поэтому, заполучив девайс, было решено его запустить и попытаться что‑то отобразить на нём. Пригодиться это может много где, например, можно будет попробовать подключить железку к какому‑нибудь автосимулятору или использовать как блок индикации в каком‑нибудь следующем проекте.
❯ Что это вообще за KAntrak?
Для начала разберёмся с тем, что это вообще за штука.

Как легко понятно из документации, это специальный прибор для отображения параметров и диагностики на коммерческом транспорте.
У нас такие штуки ставилась на некоторые автобусы ЛиАЗ с двигателем MAN. Живьём мне довелось его увидеть на инструкторском пульте учебного автобуса ЛиАЗ 5292.22.

А вот этот же аппарат на приборной панели ЛиАЗ 5292.21 (справа от спидометра).
❯ Протоколы
Теперь разберёмся с тем, что вообще хочет на вход эта железка.
На легковом транспорте какого‑то стандарта коммуникации нет. Разные автопроизводители используют свои проприетарные протоколы. Существует, конечно, стандарты диагностики (например, знакомый многим OBD‑II), но к данным, которые отправляются на приборную панель, они не имеют отношения. Таким образом, чтобы заставить работать цифровой щиток приборов от какой‑то машины, надо либо подобрать адреса CAN, либо найти информацию о них (на тематических сайтах или путём записи обмена на шине).
Иначе обстоят дела на коммерческом транспорте — грузовиках, автобусах и спецтехнике. Там существует единый отраслевой стандарт — J1939. Для некоторого транспорта существуют другие протоколы, базирующиеся на нём же, но это редкая экзотика, в данный момент нас не интересующая. Более подробно о структуре данных рассказано тут.
Как следует из документации, KAntrak как раз поддерживает этот протокол. Помимо него упоминается ещё J1708/J1587 — ещё один стандартизированный протокол, использующий в качестве линии связи RS-485.
В J1939 сообщения передаются одним или несколькими пакетами CAN. Все пакеты имеют расширенный (29-битный) ID. Он представляет собой группу из трёх параметров: приоритет, PGN (Parameter Group Number) - код типа пакета или группы параметров (к примеру, PGN 65262 - температурные параметры), SA (Souce Address) — адрес блока, который послал это сообщение (к примеру, 0 — ЭБУ двигателя, 3 — блок управления КПП, 11 — ABS, 255 — зарезервированный код для широковещательной отправки).
Конкретный параметр определяет SPN (Suspect Parameter Number). Он не передаётся явно, спецификация J1939 (Digital Annex) для каждого PGN определяет в каких битах какого байта блока данных лежит этот параметр, в каком диапазоне он может лежать и какое разрешение имеет. Также имеется возможность передачи значений длиннее восьми байт путём использования специального транспортного протокола, но, скорее всего, нам всё это сейчас не понадобится.
❯ Обзор оборудования
Рассмотрим наш девайс поподробнее.

А вот и он.
Сходу его вообще можно принять за часы или метеостанцию, но масса, качество пластика и разъём сзади вызывают интерес.

Обратная сторона.
По центру разъём подключения (явно какой-то специфический, предназначенный специально для автомобильной промышленности). Крышка держится на четырёх саморезах. Один из них заклеен пломбой, которую мы, конечно же, отковыряем.

Ещё одна наклейка.

Ответная часть разъёма.

Это DEUTSCH DT06-12S от TE Connectivity, применяемый на коммерческом автотранспорте.

Конструкция довольно интересная, напоминает соединители СНЦ или Amphenol, где тоже в резиновый уплотнитель вставляются обжимные контакты.

Провода промаркированы печатью на них.
❯ Внутренности
Что мы делаем, когда к нам в руки попадает какая-то интересная железка? Правильно: мы её разбираем.

Открутив четыре самореза, снимаем переднюю часть.

К рамке дисплея приклеен термистор.

Всё самое интересное расположилось на обратной стороне платы.

Девайс построен на чипе Infineon SAK-C164CI-LM.
Это специализированный микроконтроллер на архитектуре C166, предназначенный для использования в промышленности и на автотранспорте.

EEPROM 93LC86.

Чипы памяти.

Импульсный преобразователь питания на чипе LM2574HVM-ADJ.

CAN-трансивер PCA82C251T/YM.
❯ Первый запуск
Зачищаем провода и подаём питание.

Девайс крякнул и показал заставку.

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

Нажатием кнопок можно переключать «раскладку» приборов. При нажатии кратковременно включается тусклая зелёная подсветка.

Можно отображать разные показания в виде графиков, чтобы смотреть на них в динамике.

Список ошибок, явно подгружаемый откуда-то извне.

При отсутствии данных на экране отображается вот такой значок.
❯ Подключаем CAN
Теперь попробуем подать какие-то данные.

Если у вас нет ответной части от разъёма, нумерация контактов здесь такая.
Подключать будем по CAN, так как именно эти провода имелись у меня. Конечно, можно разобрать разъём и переставить пины, но делать этого я пока не буду.

Конвертер USB-CAN, куда лучше подходящий для подобных опытов, я пока не раздобыл, поэтому экспериментировать будем с платой CAN на чипе MCP2515 и платой Arduino Nano. Связка из двух этих модулей у меня уже давно используется в качестве CAN-адаптера.

Нашёл первый попавшийся скетч для имитации данных J1939.
Девайс явно увидел обмен, ранее упомянутый транспарант пропал. Правда, стрелки всё так же безмолвствовали. Странно.
Скорость CAN оказалась угадана верно: 250 кбит/с. С другой поведение блока ничем не отличалось от такового с отключенной шиной.

Немного подкрутил тайминги, и вот первый результат.
Конечно, что-то тут явно не так и 119 передачи точно не бывает, но начало очень хорошее: девайс действительно хочет на вход J1939 со скоростью 250 кбит/с. С форматом данных разберёмся чуть позже.
Но почему же другие индикаторы не оживают? Дело в том, что KAntrak 2710 — не просто цифровой показометр, а по сути целый микрокомпьютер. Для него существуют какие-то утилиты конфигурации и даже SDK, позволяющий писать для этого дисплейного модуля приложения на Си. Всё это звучит достаточно грустно, так как никакого софта ожидаемо не ищется, а сама возможность что-то где-то настраивать намекает на то, что адреса, возможно, были изменены на какие-то свои.
Поскольку возможности записать обмен на рабочей машине у меня нет, выходов тут несколько: подобрать адреса вручную, отреверсить прошивку девайса или формат конфигурации в его EEPROM, либо попытаться найти ещё какую-то информацию, дающую понять, что ему вообще надо.
❯ Системные меню
Оказалось, что у девайса есть несколько меню с настройками.

Нажатие крайней правой кнопки позволяет изменить яркость и контрастность.

Удержание же отображает кое-что более интересное.

В настройках ничего интересного: пределы, язык, единицы измерения.
Сразу переключил подсветку в постоянный режим. Пункт «КОР. СИГН.» выключает писк кнопок. TIER4 POPOUT — какая‑то штука для AdBlue.

Также тут можно настроить уведомление о техобслуживании.

Следующий кадр интереснее.
Что за «Показ» со значениями 0 и 1 — понятия не имею. А вот далее идут уже напрямую относящиеся к нашей цели пункты.

Во-первых, это просмотр того, что происходит на шине.

Можно в реальном времени посмотреть, что приходит на вход прибора, и убедиться, что данные верные.

А вот этот пункт меню — самый важный. Здесь нас интересуют некие «АДР. ИС. ДАНН.».
Что это такое? Это адреса источников данных. Эти два параметра задают то, данные от каких устройств (проще говоря, пакеты с какими SA) вообще следует мониторить. Как мы помним, значение 3 в стандартном варианте J1939 соответствует адресу трансмиссии, именно поэтому номер передачи отображается верно. А вот в качестве первого источника указан какой-то адрес 17, тогда как двигатель обычно располагается на нулевом. Похоже, в этом и кроется причина того, почему другие данные не отображаются.
❯ Запускаем
С причиной глюков разобрались. Поэтому меняем в скетче SA двигателя с 0 на 17, загружаем в плату...

...и оно работает!

Можно зайти в третий пункт меню — Database Viewer. Здесь в реальном времени отображаются вообще все поддерживаемые значения.

Ещё один монитор CAN, судя по всему, какой-то низкоуровневый. Выйти из него можно только перезагрузкой блока.

Из интересного — если отправить ему напряжение по CAN, он будет показывать его вместо того, что измерил сам.
А вот тот самый текстовый скетч
J1939
#include <SPI.h>
#include <mcp2515.h>
#define CAN_CS_PIN 3
#define CAN_INT_PIN 2
// Адреса источников (SA)
#define SA_ENGINE 17 // 0x11
#define SA_TRANSMISSION 3 // 0x03
MCP2515 mcp2515(CAN_CS_PIN);
unsigned long lastSend = 0;
const unsigned long sendInterval = 100;
// --- Параметры двигателя ---
uint16_t engineRpm = 800;
uint8_t coolantTemp = 80;
int8_t gear = 125; //125 = N; < 125 = R; > 125 = F
uint16_t vehicleSpeed = 0;
uint8_t fuelLevel = 75;
uint16_t oilPressure = 400;
// --- Электрика ---
uint16_t batteryVoltage = 480; // 24.00 V (SPN 168)
int16_t netBatteryCurrent = 0; // А (SPN 114, 1 A/bit, знаковый)
uint16_t alternatorCurrent = 50; // А (SPN 115, 1 A/bit)
uint16_t chargingVoltage = 480; // 24.00 V (SPN 167)
// --- Температуры ---
uint8_t oilTemp = 90; // SPN 175, °C (+40 offset)
uint8_t turboOilTemp = 120; // SPN 176, °C (+40 offset)
uint8_t transmissionTemp = 70; // SPN 177, °C (+40 offset)
uint8_t fuelTemp = 25; // SPN 174, °C (+40 offset)
int16_t ambientTempRaw = 2931; // SPN 171, 0.03125 °C/bit, -273 offset
bool addressClaimedSent = false;
void setup() {
mcp2515.reset();
mcp2515.setBitrate(CAN_250KBPS, MCP_8MHZ);
mcp2515.setNormalMode();
}
void loop() {
if (!addressClaimedSent) {
sendAddressClaimed(SA_ENGINE);
sendAddressClaimed(SA_TRANSMISSION);
addressClaimedSent = true;
delay(50);
}
if (millis() - lastSend >= sendInterval) {
lastSend = millis();
// Имитация изменений
engineRpm = constrain(engineRpm + random(-20, 20), 600, 2500);
coolantTemp = constrain(coolantTemp + random(-1, 1), 70, 95);
gear = random(125, 134);
vehicleSpeed = map(engineRpm, 600, 2500, 0, 80);
fuelLevel = constrain(fuelLevel + random(-1, 1), 0, 100);
oilPressure = constrain(oilPressure + random(-10, 10), 200, 600);
batteryVoltage = constrain(batteryVoltage + random(-5, 5), 420, 480);
netBatteryCurrent = constrain(netBatteryCurrent + random(-2, 2), -30, 30);
alternatorCurrent = constrain(alternatorCurrent + random(-2, 2), 30, 80);
chargingVoltage = constrain(chargingVoltage + random(-5, 5), 420, 480);
oilTemp = constrain(oilTemp + random(-1, 1), 80, 110);
turboOilTemp = constrain(turboOilTemp + random(-2, 2), 100, 150);
transmissionTemp = constrain(transmissionTemp + random(-1, 1), 60, 95);
fuelTemp = constrain(fuelTemp + random(-1, 1), 15, 45);
ambientTempRaw = constrain(ambientTempRaw + random(-10, 10), 2800, 3100);
// Отправка сообщений
sendEEC1(engineRpm);
sendET1(coolantTemp, oilTemp, turboOilTemp, fuelTemp);
sendTC1(gear);
sendCCVS(vehicleSpeed);
sendFLI(fuelLevel);
sendEOP(oilPressure);
sendVEP1(batteryVoltage, netBatteryCurrent, alternatorCurrent, chargingVoltage);
sendTRF1(transmissionTemp);
sendAMB(ambientTempRaw);
}
}
// ------------------------------------------------------------------
// Address Claimed (PGN 60928)
// ------------------------------------------------------------------
void sendAddressClaimed(uint8_t sourceAddress) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (60928UL << 8) | sourceAddress;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[0] = 0x01;
frame.data[1] = 0x00;
frame.data[2] = 0xAB;
frame.data[3] = 0x00;
frame.data[4] = 0x00;
frame.data[5] = 0x00;
frame.data[6] = 0x00;
frame.data[7] = 0x00;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// EEC1 (PGN 61444) — обороты двигателя (SPN 190), SA = 17
// ------------------------------------------------------------------
void sendEEC1(uint16_t rpm) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (61444UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
uint16_t rpmRaw = rpm / 0.125;
frame.data[3] = rpmRaw & 0xFF;
frame.data[4] = (rpmRaw >> 8) & 0xFF;
frame.data[0] = 0xFF;
frame.data[1] = 0xFF;
frame.data[2] = 0xFF;
frame.data[5] = 0xFF;
frame.data[6] = 0xFF;
frame.data[7] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// ET1 (PGN 65262) — температуры двигателя, SA = 17
// SPN 110: байт 1, SPN 174: байт 2, SPN 175: байты 3-4, SPN 176: байты 5-6
// ------------------------------------------------------------------
void sendET1(uint8_t coolant, uint8_t oil, uint8_t turboOil, uint8_t fuel) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65262UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[0] = coolant + 40; // SPN 110, байт 1 (ОЖ)
frame.data[1] = fuel + 40; // SPN 174, байт 2 (топливо)
frame.data[2] = (oil + 40) & 0xFF; // SPN 175, байт 3 (масло ДВС, младший)
frame.data[3] = 0xFF; // SPN 175, байт 4 (старший, 0xFF = не используется)
frame.data[4] = (turboOil + 40) & 0xFF; // SPN 176, байт 5 (масло турбины, младший)
frame.data[5] = 0xFF; // SPN 176, байт 6 (старший)
frame.data[6] = 0xFF;
frame.data[7] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// TC1 (PGN 61445) — выбранная передача (SPN 524), SA = 3
// ------------------------------------------------------------------
void sendTC1(int8_t gear) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (61445UL << 8) | SA_TRANSMISSION;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
if (gear == 0) {
frame.data[0] = 0;
} else if (gear > 0) {
frame.data[0] = gear;
} else {
frame.data[0] = 256 + gear;
}
for (int i = 1; i < 8; i++) frame.data[i] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// CCVS (PGN 65265) — скорость (SPN 84), SA = 17
// ------------------------------------------------------------------
void sendCCVS(uint16_t speed) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65265UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
uint16_t speedRaw = speed * 256;
frame.data[1] = speedRaw & 0xFF;
frame.data[2] = (speedRaw >> 8) & 0xFF;
frame.data[0] = 0xFF;
for (int i = 3; i < 8; i++) frame.data[i] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// FLI (PGN 65276) — уровень топлива (SPN 96), SA = 17
// ------------------------------------------------------------------
void sendFLI(uint8_t fuelLevel) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65276UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[1] = fuelLevel / 0.4;
frame.data[0] = 0xFF;
for (int i = 2; i < 8; i++) frame.data[i] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// EOP (PGN 65263) — давление масла (SPN 100), SA = 17
// ------------------------------------------------------------------
void sendEOP(uint16_t pressure) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65263UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[3] = pressure / 4; // разрешение 4 кПа/бит
frame.data[0] = 0xFF;
frame.data[1] = 0xFF;
frame.data[2] = 0xFF;
frame.data[4] = 0xFF;
frame.data[5] = 0xFF;
frame.data[6] = 0xFF;
frame.data[7] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// VEP1 (PGN 65271) — электрическая система, SA = 17
// SPN 114: байты 1-2, SPN 115: байты 3-4, SPN 167: байты 5-6, SPN 168: байты 7-8
// ------------------------------------------------------------------
void sendVEP1(uint16_t battVolt, int16_t netCurr, uint16_t altCurr, uint16_t chgVolt) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65271UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
// SPN 114: Net Battery Current, байты 1-2, 1 A/bit, знаковый
frame.data[0] = netCurr & 0xFF;
frame.data[1] = (netCurr >> 8) & 0xFF;
// SPN 115: Alternator Current, байты 3-4, 1 A/bit
frame.data[2] = altCurr & 0xFF;
frame.data[3] = (altCurr >> 8) & 0xFF;
// SPN 167: Charging System Potential, байты 5-6
frame.data[4] = chgVolt & 0xFF;
frame.data[5] = (chgVolt >> 8) & 0xFF;
// SPN 168: Battery Potential / Power Input 1, байты 7-8
frame.data[6] = battVolt & 0xFF;
frame.data[7] = (battVolt >> 8) & 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// TRF1 (PGN 65272) — температура масла в КПП (SPN 177), SA = 3
// SPN 177 находится в байте 2 (data[1]), 1 °C/bit, -40 смещение
// ------------------------------------------------------------------
void sendTRF1(uint8_t temp) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65272UL << 8) | SA_TRANSMISSION;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[0] = 0xFF; // SPN 126 (oil pressure) — не используется
frame.data[1] = temp + 40; // SPN 177, байт 2
frame.data[2] = 0xFF; // SPN 161 (oil level) — не используется
frame.data[3] = 0xFF;
frame.data[4] = 0xFF;
frame.data[5] = 0xFF;
frame.data[6] = 0xFF;
frame.data[7] = 0xFF;
mcp2515.sendMessage(&frame);
}
// ------------------------------------------------------------------
// AMB (PGN 65269) — температура окружающего воздуха (SPN 171), SA = 17
// SPN 171: байты 4-5, 0.03125 °C/bit, смещение -273 °C
// ------------------------------------------------------------------
void sendAMB(int16_t tempRaw) {
struct can_frame frame;
frame.can_id = (6UL << 26) | (65269UL << 8) | SA_ENGINE;
frame.can_id |= CAN_EFF_FLAG;
frame.can_dlc = 8;
frame.data[0] = 0xFF; // SPN 108 — байт 1
frame.data[1] = 0xFF; // SPN 108 — байт 2
frame.data[2] = 0xFF; // SPN 170 — байт 3
frame.data[3] = tempRaw & 0xFF; // SPN 171, байт 4 (младший)
frame.data[4] = (tempRaw >> 8) & 0xFF; // SPN 171, байт 5 (старший)
frame.data[5] = 0xFF; // SPN 172 — байт 6
frame.data[6] = 0xFF; // SPN 173 — байт 7
frame.data[7] = 0xFF; // SPN 79 — байт 8
mcp2515.sendMessage(&frame);
}Для взаимодействия использовал эту библиотеку.
❯ Что же в итоге?
Итак, с помощью довольно простых манипуляций этот прибор удалось оживить.

За исключением нестандартного адреса особых проблем не возникло. Теперь девайс можно использовать в каком-то из своих проектов.
Один из виртуальных приборов я так и не смог запустить. Судя по иконке, это какая-то температура, но пакет, который её передаёт, я не нашёл. Также интересно, что разрешение некоторых величин несколько отличается от принятого в J1939, поэтому некоторые приборы изначально показывали чрезмерные или околонулевые значения. Но, само собой, исправить всё это не составляет труда.
Такие дела.
Может быть интересно:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.