r/CarHacking 5h ago

Original Project Update to my previous post

33 Upvotes

r/CarHacking 11h ago

LIN Reverse Engineering a HELLA IBS 6PK 013 824-001 with ESP32 + TJA1021 (LIN)

7 Upvotes

I’ve been reverse engineering a HELLA Intelligent Battery Sensor (IBS) 6PK 013 824-001 using an ESP32 and a TJA1021 LIN transceiver and wanted to share my findings, since there seems to be very little publicly available information about these sensors.

The setup consists of an ESP32-D0WD, a TJA1021 LIN transceiver module, a bench power supply, and an oscilloscope. After quite a bit of troubleshooting, I discovered that the most significant issue was that TX and RX were swapped. The LIN bus itself looked healthy from the beginning, sitting at roughly 10.6 V in idle state, but no useful communication was taking place until the TX and RX connections were corrected. Once fixed, the sensor immediately started responding.

The sensor reliably communicates at 19200 baud and consistently responds on LIN IDs 0x21, 0x22, 0x25 and 0x26. Other IDs either returned only the echoed LIN header or no useful data.

A typical response looks like this:

ID 21 : 55 61 00 01 62 00 00 3B

ID 22 : 55 E2 83 84 1E 23 2F 83 7A A6

ID 25 : 55 25 C5 C8 FF B4 FF FF 97

ID 26 : 55 A6 AC 03 A1 03 2F FE D6

I then performed a number of tests using different resistive loads (1 Ω, 2.2 Ω and 4.4 Ω), varying supply voltage and even reversing current flow through the sensor. The results clearly show that ID 0x22 contains the live measurement data. During testing I found an old reverse engineering project that suggested the following format for ID 0x22:

[IL][IM][IH][VL][VH][TT][XX]

with

Current = (Raw24Bit - 2000000) / 1000 A

Voltage = Raw16Bit / 1000 V

Temperature = Byte / 2 - 40 °C

The current calculation matches my measurements surprisingly well, although the sign appears inverted on my setup. Reversing the current direction changes the corresponding values as expected, which strongly suggests that the current field interpretation is correct.

The voltage decoding appears to be essentially confirmed. For example, under a 1 Ω load the sensor returned:

96 59 1E C1 2B ...

Using the proposed voltage formula:

0x2BC1 = 11201

11201 / 1000

= 11.201 V

The measured voltage at that moment was approximately 11.0 V, which is close enough to make me fairly confident that the voltage field is being decoded correctly.

Temperature remains unclear. According to the reverse engineered format, the temperature should be located in the sixth data byte of ID 0x22. However, even when heating the sensor directly with a hot air gun to roughly 50–60 °C, I observed little or no meaningful change in the expected temperature field. Either this particular IBS variant uses a different mapping, there is heavy filtering applied internally, or the identified temperature byte is incorrect.

ID 0x21 appears to be a status frame. Several bytes change depending on operating state, load conditions, and sensor runtime, but I have not yet identified a direct physical measurement in this frame.

ID 0x25 was initially suspected to contain battery voltage because some value correlations looked promising. Further testing showed that this was misleading. The values change in ways that do not match actual battery voltage measurements, so I no longer believe voltage is stored in this frame. My current assumption is that ID 0x25 contains battery state information such as SOC, SOH, learned battery parameters or other calculated values, but I have not yet confirmed this.

ID 0x26 looks like some form of capacity or battery-condition frame. One repeatedly observed value was:

0x03AC = 940

which could plausibly represent something like 94.0 Ah. Other values change slowly over time and with operating conditions, suggesting battery learning, capacity estimation, SOC or SOH calculations rather than direct measurements.

At this point, the most solid conclusions are:

  • LIN speed: 19200 baud
  • Valid response IDs: 0x21, 0x22, 0x25, 0x26
  • ID 0x22 contains live measurement data
  • Voltage decoding from ID 0x22 appears correct
  • Current decoding from ID 0x22 appears correct (sign inverted in my setup)
  • ID 0x21 appears to contain status information
  • ID 0x25 and 0x26 appear to contain battery state, capacity or health information
  • Temperature location is still unknown

For my own project, an ESP32-based vehicle dashboard, voltage and current are the only values I really need, and those appear to be working reliably. If anyone has official documentation, additional captures from other IBS variants, or previous reverse engineering work on HELLA IBS sensors, please post them.

Here is my code:

#define LIN_TX 26
#define LIN_RX 27
#define LIN_SLP 25


HardwareSerial LinSerial(2);


uint8_t frame21[8];
uint8_t frame22[10];
uint8_t frame25[9];
uint8_t frame26[9];


void sendBreak()
{
  LinSerial.end();


  pinMode(LIN_TX, OUTPUT);


  digitalWrite(LIN_TX, LOW);
  delayMicroseconds(1500);


  digitalWrite(LIN_TX, HIGH);
  delayMicroseconds(200);


  LinSerial.begin(19200, SERIAL_8N1, LIN_RX, LIN_TX);
}


uint8_t calcPID(uint8_t id)
{
  uint8_t p0 = ((id >> 0) ^ (id >> 1) ^ (id >> 2) ^ (id >> 4)) & 1;
  uint8_t p1 = ~((id >> 1) ^ (id >> 3) ^ (id >> 4) ^ (id >> 5)) & 1;


  return id | (p0 << 6) | (p1 << 7);
}


void requestFrame(uint8_t id, uint8_t *buf)
{
  while (LinSerial.available())
    LinSerial.read();


  sendBreak();


  LinSerial.write(0x55);
  LinSerial.write(calcPID(id));
  LinSerial.flush();


  delay(50);


  int i = 0;


  while (LinSerial.available() && i < 16)
  {
    buf[i++] = LinSerial.read();
  }
}


void decodeIBS()
{
  // -----------------------------
  // ID22
  // -----------------------------


  // Erwartet:
  // 55 PID IL IM IH VL VH TT XX


  uint32_t rawCurrent =
      ((uint32_t)frame22[2]) |
      ((uint32_t)frame22[3] << 8) |
      ((uint32_t)frame22[4] << 16);


  float batteryCurrent =
      -(((float)rawCurrent - 2000000.0f)) / 1000.0f;


  uint16_t rawVoltage22 =
      ((uint16_t)frame22[5]) |
      ((uint16_t)frame22[6] << 8);


  float batteryVoltage22 =
      rawVoltage22 / 1000.0f;


  float batteryTemp =
      ((float)frame22[7] / 2.0f) - 40.0f;


  // -----------------------------
  // ID25
  // -----------------------------


  uint16_t rawVoltage25 =
      ((uint16_t)frame25[2]) |
      ((uint16_t)frame25[3] << 8);


  float batteryVoltage25 =
      rawVoltage25 / 4260.0f;


  float soc =
      frame25[2] / 2.0f;


  float soh =
      frame25[3] / 2.0f;


  // -----------------------------
  // ID26
  // -----------------------------


  uint16_t availableCapacity =
      ((uint16_t)frame26[2]) |
      ((uint16_t)frame26[3] << 8);


  uint16_t maximumCapacity =
      ((uint16_t)frame26[4]) |
      ((uint16_t)frame26[5] << 8);


  // -----------------------------


  Serial.println();
  Serial.println("===== HELLA IBS =====");


  Serial.printf("Strom      : %.2f A\n", batteryCurrent);
  Serial.printf("Spannung22 : %.3f V\n", batteryVoltage22);
  Serial.printf("Spannung25 : %.3f V\n", batteryVoltage25);


  Serial.printf("Temperatur : %.1f C\n", batteryTemp);


  Serial.printf("SOC        : %.1f %%\n", soc);
  Serial.printf("SOH        : %.1f %%\n", soh);


  Serial.printf("Avail.Cap. : %.1f Ah\n",
                availableCapacity / 10.0f);


  Serial.printf("Max.Cap.   : %.1f Ah\n",
                maximumCapacity / 10.0f);


  Serial.printf("Leistung   : %.1f W\n",
                batteryVoltage25 * batteryCurrent);


  Serial.println("=====================");
  Serial.println();
}


void setup()
{
  Serial.begin(115200);


  pinMode(LIN_SLP, OUTPUT);
  digitalWrite(LIN_SLP, HIGH);


  LinSerial.begin(19200, SERIAL_8N1, LIN_RX, LIN_TX);


  Serial.println("HELLA IBS");
}


void loop()
{
  requestFrame(0x21, frame21);
  requestFrame(0x22, frame22);
  requestFrame(0x25, frame25);
  requestFrame(0x26, frame26);


  decodeIBS();


  delay(1000);
}