Reverse engineering BMS Firmware / Reflashing BMS

My Nissan Leaf Forum

Help Support My Nissan Leaf Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.
Dumping back, the firmware did not change any eeprom data. I wonder if it is not exactly SOH but instead cell Ah capacity and SOH is derived from it. I'll fiddle with the values to see if I can get the Ah to read what my catl cells are supposed to be.
 
From the LS screen:

The 2.1 million Amps was promising :ROFLMAO:

Also the VIN was showing back as an ASCII string from the eeprom dump. Is that the part number printed on the circuit board?
 
The 3rd set of 4 bytes is also a percentage, math is uint32 / 1024 = Percent x 100. Possibly HX? It is set to 100.00% when the eeprom is reset but both my bin and the 30kwh bin shows a higher number than the initial value, and my 24kwh eeprom is significantly higher than the 32kwh eeprom which might make sense if its HX. Unconfirmed, just thinking out loud
Hx represents battery conductance (inverse of resistance) per LeafSpy, and as such is not a percentage.
It tracks SOH over a battery's life time, decreasing. My data (mohms - inverse of Hx) from my '13 Leaf indicate this:

11/20/14 -13,700 miles, 76 mohms per LeafDD, 20 Deg, 73% SOC, 90% SOH
11/27 -13,800 miles, 67 mohms per LeafDD, 25 deg, 63% SOC
11/30 - 13,900 miles, 56 mohms per LeafDD, 27 deg, 71% SOC
12/2 - 14.100 miles, 55 mohms per LeafDD, 28 deg, 67% SOC
12/16 - 14,500 miles, 89 mohms per LeafDD, 15 deg, 93% SOC
12/27/14 - 14,800 miles, 103 mohms per LeafDD, 11 deg, 24% SOC
3/10 - 17,400 miles, 60 mohms per LeafDD, 30 deg, 73% SOC
3/14 - 17, 550 miles, 56 mohms per LeafDD, 32 deg, 47% SOC, 85% SOH
4/14 - 19,100 miles, 59 mohms per LeafDD, 25 deg. 38% SOC
5/4 - 19,989 miles, 64 mohms per LeafDD, 24 deg. 48% SOC
5/15 - 20,400 miles, 73 mohms per LeafDD, 20 deg. 41% SOC
5/22 - 20,700 miles, 58 mohms per LeafDD, 28 deg. 50% SOC
12/10/15 - 28,000 miles, 90 mohms per LeafDD, 19 deg. 92% SOC
4/5 - 32,000 miles, 74 mohms per LeafDD, 24 deg, 55% SOC
5/16 - 33,700 miles,89 mohms per LeafDD, 22 deg, 47% SOC
5/16 - 33.700 miles, 58 mohms per LeafDD, 31 deg, 76% SOC, 80% SOH
10/5 - 39,300 miles, 100 mohms per LeafDD, 22 deg, 50% SOC
10/6 - 39,400 miles, 61 mohms per LeafDD, 30 deg, 51% SOC
10/7 - 39,500 miles, 80 mohms per LeafDD, 25 deg, 56% SOC
10/15 - 40,000 miles, 71 mohms per LeafDD, 27 deg, 45% SOC
10/30 - 41,000 miles, 74 mohms per LeafDD, 23 deg, 66% SOC
12/26/16 - 43,000 miles, 110 mohms per LeafDD, 13 deg, 77% SOC
6/10/17 - 49,600 miles, 89 mohms per LeafDD, 19 deg, 70% SOC
7/1/17 - 51,000 miles, 62 mohms per LeafDD, 33 deg, 44% SOC
8/15/17 - 53,400 miles, 61 mohms per LeafDD, 35 deg, 57% SOC, 75% SOH
 
Last edited:
Thx @lorenfb, I'll put a new value there and see what changes on Leafspy.

As for the challenge/response, here is the function that generates the response to the challenge:
void possiblyCrypto1?(void)

{
bool bVar1;
int iVar2;
ushort uVar3;
undefined1 *puVar4;
undefined1 *puVar5;
undefined1 *puVar6;

DAT_03ffc470 = 0x61;
DAT_03ffc471 = 0x65;
puVar5 = &DAT_03ffc472;
uVar3 = 0;
puVar4 = &DAT_03ffb9fc;
do {
*puVar5 = puVar4[3];
puVar5[1] = puVar4[2];
puVar5[2] = puVar4[1];
puVar5[3] = *puVar4;
puVar5[4] = puVar4[7];
puVar5[5] = puVar4[6];
puVar5[6] = puVar4[5];
puVar5[7] = puVar4[4];
puVar5[8] = puVar4[0xb];
puVar5[9] = puVar4[10];
uVar3 = uVar3 + 1;
puVar5[10] = puVar4[9];
puVar6 = puVar4 + 8;
puVar4 = puVar4 + 0xc;
puVar5[0xb] = *puVar6;
puVar5 = puVar5 + 0xc;
} while (uVar3 < 10);
iVar2 = 2;
puVar4 = &DAT_03ffba74;
do {
puVar6 = puVar5;
*puVar6 = puVar4[3];
puVar6[1] = puVar4[2];
puVar6[2] = puVar4[1];
puVar6[3] = *puVar4;
puVar6[4] = puVar4[7];
puVar6[5] = puVar4[6];
puVar6[6] = puVar4[5];
puVar6[7] = puVar4[4];
puVar6[8] = puVar4[0xb];
puVar6[9] = puVar4[10];
puVar6[10] = puVar4[9];
puVar6[0xb] = puVar4[8];
puVar6[0xc] = puVar4[0xf];
puVar6[0xd] = puVar4[0xe];
puVar6[0xe] = puVar4[0xd];
puVar5 = puVar4 + 0xc;
puVar4 = puVar4 + 0x10;
puVar6[0xf] = *puVar5;
bVar1 = iVar2 != 1;
iVar2 = iVar2 + -1;
puVar5 = puVar6 + 0x10;
} while (bVar1);
puVar6[0x10] = DAT_03ffba97;
puVar6[0x11] = DAT_03ffba96;
puVar6[0x12] = DAT_03ffba95;
puVar6[0x13] = DAT_03ffba94;
puVar6[0x14] = DAT_03ffba98._3_1_;
puVar6[0x15] = DAT_03ffba98._2_1_;
puVar6[0x16] = DAT_03ffba98._1_1_;
puVar6[0x17] = (undefined)DAT_03ffba98;
puVar6[0x18] = DAT_03ffba9c._3_1_;
puVar6[0x19] = DAT_03ffba9c._2_1_;
puVar6[0x1a] = DAT_03ffba9c._1_1_;
puVar6[0x1b] = (undefined)DAT_03ffba9c;
return;
}

I'm certainly no crypto expert so I'll see what copilot and claude can come up with.
 
The 230JT1125... code
Maybe that is the battery serial number, along the lines of Group 84 found in Dala's guide:

Code:
Group 84
Name: Battery Serial
Type: String
Description: Battery serial number
Query:
0x79B 02 21 84 00 00 00 00 00;length,Pid,Group
0x79B 30 00 00 00 00 00 00 00; request more time
Answer:
0x7BB 10 16 61 84 32 33 30 55;reply,len,P,G,data
0x7BB 21 4B 31 31 39 32 45 30;2nd line reply, data
0x7BB 22 30 31 34 38 32 20 A0; 3rd  "
0x7BB 23 00 00 00 00 00 00 00; 4th  "
Formula: (323330554b313139324530303134383220A0).decode(’utf-8’)
 
Last edited:
I have a BMS sitting around that came with a junkyard battery I bought last spring. My leaf is a 2011 and the batter came out of a later model(I think 2013) so I can’t even use it in my car for anything.

I have it torn apart for curiosity sake and have been thinking about doing exactly what is going on in this thread.

PM me if I can be of help.
 
PID27_65 Code. When issuing PID27,command 0x65, the BMS will send you a 4 byte Challenge. You have 10seconds to reply with the correct (Edit: A wait time of 10sec is requried for a new Challenge to stop brute forcing) response.

Here is the function which sends and checks your reply:

undefined4 PID27_65(char param_1)

{
undefined4 uVar1;

if (DAT_03ffe371 == -0x40) {
if ((CAN_Process_State_Machine_Flag & 4) == 0) {
if ((Can_TX_Buffer_Byte_1 == 2) && (param_1 == 'e')) {
if (((CAN_Process_State_Machine_Flag & 0x20) == 0) && (Pid27_65_Timeout_Counter == 0)) {
uVar1 = GeneratePID27_65_Seed();
CAN_Process_State_Machine_Flag = CAN_Process_State_Machine_Flag | 0x20;
Can_TX_Buffer_Byte_5 = (undefined)((uint)uVar1 >> 0x10);
Can_TX_Buffer_Byte_2 = 0x67;
Can_TX_Buffer_Byte_4 = (undefined)((uint)uVar1 >> 0x18);
Can_TX_Buffer_Byte_3 = 0x65;
Can_TX_Buffer_Byte_6 = (undefined)((uint)uVar1 >> 8);
Can_TX_Buffer_Byte_7 = (undefined)uVar1;
Decypt_Seed(uVar1,&Decypted_Output_Array_Byte0);
return 6;
}
}
else {
if ((Can_TX_Buffer_Byte_1 != 10) || (param_1 != 0x66)) goto LAB_000157b4;
if ((CAN_Process_State_Machine_Flag & 0x20) != 0) {
Pid27_65_Timeout_Counter = 1000;
if (Decypted_Output_Array_Byte7 != CAN_RX_Buffer_Byte_10 ||
(Decypted_Output_Array_Byte6 != CAN_RX_Buffer_Byte_9 ||
(Decypted_Output_Array_Byte5 != CAN_RX_Buffer_Byte_8 ||
(Decypted_Output_Array_Byte4 != CAN_RX_Buffer_Byte_7 ||
(Decypted_Output_Array_Byte3 != CAN_RX_Buffer_Byte_6 ||
(Decypted_Output_Array_Byte2 != CAN_RX_Buffer_Byte_5 ||
(Decypted_Output_Array_Byte1 != CAN_RX_Buffer_Byte_4 ||
Decypted_Output_Array_Byte0 != CAN_RX_Buffer_Byte_3))))))) {
CAN_Process_State_Machine_Flag = CAN_Process_State_Machine_Flag & 0xdb;
Update_Can_TX_Buffer_2,3,4(0x27,0x35);
return 3;
}
Can_TX_Buffer_Byte_2 = 0x67;
Can_TX_Buffer_Byte_3 = 0x66;
CAN_Process_State_Machine_Flag = CAN_Process_State_Machine_Flag & 0xdf | 4;
Pid27_65_Timeout_Counter = 1000;
return 2;
}
}
}
Update_Can_TX_Buffer_2,3,4(0x27,0x22);
}
else {
LAB_000157b4:
Update_Can_TX_Buffer_2,3,4(0x27,0x12);
}
return 3;
}

The Seed is generated from a 32bit integer which increments every 10mS and is xor'd with memory data.

The decrypt_seed function is as follows:

#include <stdio.h>
#include <stdbool.h>


unsigned int FUN_0001539c(unsigned int param_1,unsigned int param_2)

{
bool bVar1;
unsigned int uVar2;
unsigned int uVar3;
unsigned int uVar4;
unsigned int uVar5;
unsigned int uVar6;
unsigned int uVar7;
unsigned int uVar8;
unsigned int uVar9;
unsigned int uVar10;
unsigned int uVar11;
int iVar12;

param_1 = param_1 & 0xffff;
param_2 = param_2 & 0xffff;
uVar10 = 0xffff;
iVar12 = 2;
do {
uVar2 = param_2;
if ((param_1 & 1) == 1) {
uVar2 = param_1 >> 1;
}
uVar3 = param_2;
if ((param_1 >> 1 & 1) == 1) {
uVar3 = param_1 >> 2;
}
uVar4 = param_2;
if ((param_1 >> 2 & 1) == 1) {
uVar4 = param_1 >> 3;
}
uVar5 = param_2;
if ((param_1 >> 3 & 1) == 1) {
uVar5 = param_1 >> 4;
}
uVar6 = param_2;
if ((param_1 >> 4 & 1) == 1) {
uVar6 = param_1 >> 5;
}
uVar7 = param_2;
if ((param_1 >> 5 & 1) == 1) {
uVar7 = param_1 >> 6;
}
uVar11 = param_1 >> 7;
uVar8 = param_2;
if ((param_1 >> 6 & 1) == 1) {
uVar8 = uVar11;
}
param_1 = param_1 >> 8;
uVar9 = param_2;
if ((uVar11 & 1) == 1) {
uVar9 = param_1;
}
uVar10 = (((((((((((((((uVar10 & 0x7fff) << 1 ^ uVar2) & 0x7fff) << 1 ^ uVar3) & 0x7fff) << 1 ^
uVar4) & 0x7fff) << 1 ^ uVar5) & 0x7fff) << 1 ^ uVar6) & 0x7fff) << 1 ^ uVar7)
& 0x7fff) << 1 ^ uVar8) & 0x7fff) << 1 ^ uVar9;
bVar1 = iVar12 != 1;
iVar12 = iVar12 + -1;
} while (bVar1);
return uVar10;
}

unsigned int FUN_000152ee(unsigned int param_1,unsigned int param_2,unsigned int param_3)

{
return (param_3 ^ 0x780 | param_2 ^ 0x116) * ((param_1 & 0xffff) >> 8 ^ param_1 & 0xff) & 0xffff;
}

short FUN_0001532a(short param_1,short param_2)

{
unsigned short uVar1;

uVar1 = param_2 + param_1 * 0x5ba & 0xff;
return (uVar1 + param_1) * (uVar1 + param_2);
}

int FUN_00015354(int param_1,int param_2)

{
unsigned int uVar1;

param_1 = param_1 & 0xffff;
param_2 = param_2 & 0xffff;
uVar1 = param_2 & (param_1 | 0x5ba) & 0xf;
return ((int)param_1 >> uVar1 | param_1 << (0x10 - uVar1 & 0x1f)) *
(param_2 << uVar1 | (int)param_2 >> (0x10 - uVar1 & 0x1f)) & 0xffff;
}

int CyptAlgo(unsigned int param_1,unsigned int param_2,unsigned int param_3)

{
int uVar1;
int uVar2;
int iVar3;
int iVar4;

uVar1 = FUN_00015354(param_2,param_3);
uVar2 = FUN_0001532a(param_2,param_3);
uVar1 = FUN_000152ee(param_1,uVar1,uVar2);
uVar2 = FUN_000152ee(param_1,uVar2,uVar1);
iVar3 = FUN_0001539c(uVar1,0xffc4);
iVar4 = FUN_0001539c(uVar2,0xffc4);
return iVar4 + iVar3 * 0x10000;
}



void Decypt_Seed(unsigned int SeedInput,char *Crypt_Output_Buffer)

{
int uVar1;
int uVar2;

uVar1 = CyptAlgo(0x609,0xdd2,SeedInput >> 0x10);
uVar2 = CyptAlgo(SeedInput & 0xffff,SeedInput >> 0x10,0x609);
*Crypt_Output_Buffer = (char)uVar1;
Crypt_Output_Buffer[7] = (char)((unsigned int)uVar1 >> 0x18);
Crypt_Output_Buffer[3] = (char)((unsigned int)uVar1 >> 8);
Crypt_Output_Buffer[2] = (char)((unsigned int)uVar2 >> 8);
Crypt_Output_Buffer[5] = (char)((unsigned int)uVar1 >> 0x10);
Crypt_Output_Buffer[4] = (char)((unsigned int)uVar2 >> 0x10);
Crypt_Output_Buffer[1] = (char)uVar2;
Crypt_Output_Buffer[6] = (char)((unsigned int)uVar2 >> 0x18);
return;
}
unsigned char OutputArray[8];

int main() {

Decypt_Seed(0xAC573D0B,OutputArray);
for (int i = 0; i < 8; i++) {
printf("0x%02X ", OutputArray);
}
printf("\n");




return 0;
}

Once returned true, Flags are set which allows other can commands which will otherwise return an error response. This includes EERPOM writing

I have not yet tested the above code, when the spare BMS's arrive I'll give it a go.
 
Last edited:
Pid31_03_01 will reset SOC/SOH, if CAN_Process_Flag == 0xC0.
void PID31_03(void)

{
if (CAN_Process_Flag == -0x40) {
if ((CAN_Process_State_Machine_Flag & 4) == 0) {
Update_Can_TX_Buffer_2,3,4(0x31,0x33);
Can_TX_len = 3;
return;
}
if (Can_TX_Buffer_Byte_1 == 3) {
if (CAN_RX_Buffer_Byte_3 == '\0') {
SOHx100 = 10000;
SOHx100_volatile = 10000;
SOCx100 = 10000;
SOCx100_volatile = 10000;
DAT_03ffb2ab = DAT_03ffb2ab & 0xf9;
Can_TX_Buffer_Byte_2 = 0x71;
Can_TX_Buffer_Byte_3 = 3;
Can_TX_Buffer_Byte_4 = 1;
Can_TX_len = 3;
return;
}
if (CAN_RX_Buffer_Byte_3 == '\x01') {
Can_TX_Buffer_Byte_2 = 0x71;
Can_TX_Buffer_Byte_3 = 3;
Can_TX_Buffer_Byte_4 = 2;
Can_TX_len = 3;
return;
}
}
}
Update_Can_TX_Buffer_2,3,4(0x31,0x12);
Can_TX_len = 3;
return;
}

To set CAN_Process_Flag to 0xC0: Send PID10_C0


void CAN_PID_0x10(void)

{
undefined4 uVar1;

if (Can_TX_Buffer_Byte_1 == 2) {
if (0xbf < Can_RX_Buffer_Byte_2) {
if (Can_RX_Buffer_Byte_2 == 0xc0) {
if (CAN_Process_Flag == -0x10) {
CAN_Process_State_Machine_Flag = CAN_Process_State_Machine_Flag & 0xf5;
}
else if (CAN_Process_Flag == -0xf) {
CAN_Process_State_Machine_Flag = CAN_Process_State_Machine_Flag & 0xee;
}
DAT_03ffe370 = CAN_Process_Flag;
CAN_Process_Flag = -0x40;
Can_TX_Buffer_Byte_2 = 0x50;
Can_TX_Buffer_Byte_3 = 0xc0;
uVar1 = 2;
FUN_000150f8();
goto LAB_00001030;
}

This doesn't appear to require a seed/key response though to preserve to EEPROM it does. I'll test this today
 
I have a BMS sitting around that came with a junkyard battery I bought last spring. My leaf is a 2011 and the batter came out of a later model(I think 2013) so I can’t even use it in my car for anything.

I have it torn apart for curiosity sake and have been thinking about doing exactly what is going on in this thread.

PM me if I can be of help.
for sure you can help dumping the memory content of the eeprom (probably the same as shown in previous pthotos in the thread).

waiting for you contribution :)

cheers
 
I have a BMS sitting around that came with a junkyard battery I bought last spring. My leaf is a 2011 and the batter came out of a later model(I think 2013) so I can’t even use it in my car for anything.

I have it torn apart for curiosity sake and have been thinking about doing exactly what is going on in this thread.

PM me if I can be of help.
If you have a USB-Can bus interface, and a 12v power supply there are a few tests you could run
 
So in my case, my SOH is 37.75%, which is stored as 3775 in SOHx100_volatile. It is then multiplied by 0xC20B2 (this number pops up a lot in the firmware, now we know why) = 0xB2D620CE and passed to Function 00023678.
Sorry, I'm again quite late to the party; many things going on at present.

Multiplying by a strange constant and doing a right shift by (word size in bits) is a common strength reduction optimisation used by compilers. In this case, they seem to be dividing by about 5403.82, as 0xC20B2 = 794,802 = 2³² / 5403.82. The problem is that this results in a number less than one, but I note that the decompilation suggests that both this value and the value before it was shifted right by 32 bits are passed to the function. So it's like a 64 bit fixed point number with the binary point in the middle, i.e. one half has the number before the binary point, and the other half has the value after the binary point. It seems a strange way to pass a non-integer number, but it's not totally insane. Some would call this a Q32 number (a fixed point number with 32 bits after the binary point).

[ Edit: Originally I forgot to divide by the nominal capacity. ]

SOH = capacity / nominal_capacity * 100. For a 30 kWh battery, SOH = cap_Wh / 30,000 * 100 = cap_Wh / 300.

So SOH / 5403.8 = cap_wh / 300 / 5403.8 = cap_Wh / 1,621,140. 1,621,140 / 65536 = 24.73, so these would be units of 24.7 kWh. In other words, this could be the capacity relative to a new 24 kWh battery (presuming 69.6 Ah per module when new, or 34.8 Ah per cell).

So this could be a Q16 number, of the capacity compared with a new gen 1 battery. It's a stretch, I'll agree.

And I suspect I'm still wrong; sigh.
 
Last edited:
And the matching leafspy screenshot
That ss from post #60 shows the VIN in the header, whereas when reading just the battery pack we saw a "?? serial number?" in the header that had digits stored on the eeprom. i found another battery pack read with what i think is the serial number in the header,
LS screamshot.png

This was found on the NZ Drive EV site which provides a battery pack reading service.
 
Hey all, not a software expert but have 24kwh 2013 car bms (black interior) that i can poke open and do pictures of, down to microscopic level if it helps. Probably take readings of some chips etc too. But if that is already covered then nevermind :)

PS. don't forget to wipe your phone lenses, helps a lot with picture clarity very often!
 

Attachments

  • IMG_8262.jpg
    IMG_8262.jpg
    1.4 MB
Back
Top