Firmware hacking

A project log for Model S BMS hacking

Reverse engineer, reprogram or otherwise use the UV/OV/balancing circuits built into each of the 16 modules in a Tesla Model S battery pack.

JarrodJarrod 04/17/2016 at 20:222 Comments

Here is a rough schematic of the isolated bus circuitry.

It looks like there is some shared "open collector" style bus on CH4 of the isolator, with D8 and (I assume) a pullup on the BMS controller board. But so far I've only ever measured this line being low, thus disabling the entire bus. I haven't worked out what U5 is yet. probably a dual logic gate. Maybe it combines error signals out of the BMS IC..

Ch1 is used to bring the BMS out of low power mode. A low frequency oscillator powers up U4 periodically checking on the state of CH1. The whole thing then stays powered while CH1 is driven low (otherwise CH1 is high impedance if the bus side of the isolator is unpowered, the circuitry must detect this and shut down) I can't really be bothered tracing out much more of the circuit. the layer of varnish on everything and tracks disappearing into internal layers makes it a nightmare.

Ch2 and 3 look like the main comms lines, connected to the UART of the uC. J1 is set up such that Tx of one channel is connected to Rx of the next, so they daisy chain together. The first BMS in the chain must get a "go" command from a controller board.. so I just have to work out how to say "go" as discussed in my last post.

I thought I'd just check that Tesla had the security bit set to protect the firmware from reading.. already had a SiLabs JTAG/C2 programmer so I wired up the 2-wire C2 interface.

And turns out.. The firmware is totally readable! Tesla wanted to make my life a bit easier. Thanks Tesla! There is about 5KB of used codespace. I ran it through an 8051 disassembler, DASMx. The SiLabs C51 uC's are totally 8051 compatible, meaning binary operands and memory locations are the same, so DASMx can even decode the SFR addresses into memonics for me. Its supposed to be able to 'flatten' out the code, using all the calls and branches to make it more linear to read but it would only process 3% of the code when I tried that option.

Here is the hex:

And disassembled code:

And the chip datasheet: Documents/TechnicalDocs/C8051F52x-F53x.pdf

There are a few thousand lines of assembly. I started by looking at the interrupts, as some of the code is most likely interrupt driven, especially the UART RX.

Code at 0x12A9: mov iec,#0B0H, this writes to the interrupt enable Special Function Register, enabling interrupts on Timer2 and UART0. The UART interrupt vector is 0x0023, which contains ljmp L0C0F, which does another ljmp L0DB9 which contains the code below!

0DB9                        L0DB9:
0DB9 : C0 E0        "  "        push    acc
0DBB : C0 F0        "  "        push    b
0DBD : C0 83        "  "        push    dph
0DBF : C0 82        "  "        push    dpl
0DC1 : C0 D0        "  "        push    psw
0DC3 : 75 D0 00    "u  "        mov    psw,#000H
0DC6 : C0 00        "  "        push    X0000
0DC8 : C0 01        "  "        push    X0001
0DCA : C0 02        "  "        push    X0002
0DCC : C0 03        "  "        push    X0003
0DCE : C0 04        "  "        push    X0004
0DD0 : C0 05        "  "        push    X0005
0DD2 : C0 06        "  "        push    X0006
0DD4 : C0 07        "  "        push    X0007
0DD6 : 30 99 05    "0  "        jnb    ti,L0DDE
0DD9 : 75 2B 00    "u+ "        mov    X002B,#000H
0DDC : C2 99        "  "        clr    ti
0DDE                        L0DDE:
0DDE : 30 98 30    "0 0"        jnb    ri,L0E11
0DE1 : 85 99 2A    "  *"        mov    X002A,sbuf
0DE4 : E5 2C        " ,"        mov    a,X002C
0DE6 : 60 21        "`!"        jz    L0E09
0DE8 : E5 2B        " +"        mov    a,X002B
0DEA : 60 05        "` "        jz    L0DF1
0DEC                        L0DEC:
0DEC : 30 99 FD    "0  "        jnb    ti,L0DEC
0DEF : C2 99        "  "        clr    ti
0DF1                        L0DF1:
0DF1 : E5 2D        " -"        mov    a,X002D
0DF3 : 60 0D        "` "        jz    L0E02
0DF5 : E5 2A        " *"        mov    a,X002A
0DF7 : 54 FE        "T "        anl    a,#0FEH
0DF9 : 70 07        "p "        jnz    L0E02
0DFB : E5 2A        " *"        mov    a,X002A
0DFD : 44 80        "D "        orl    a,#080H
0DFF : FF        " "            mov    r7,a
0E00 : 80 02        "  "        sjmp    L0E04
0E02                        L0E02:
0E02 : AF 2A        " *"        mov    r7,X002A
0E04                        L0E04:
0E04 : 8F 99        "  "        mov    sbuf,r7
0E06 : 75 2B 01    "u+ "        mov    X002B,#001H
0E09                        L0E09:
0E09 : 75 29 01    "u) "        mov    X0029,#001H
0E0C : C2 98        "  "        clr    ri
0E0E : 12 12 95    "   "        lcall    L1295
0E11                        L0E11:
0E11 : D0 07        "  "        pop    X0007
0E13 : D0 06        "  "        pop    X0006
0E15 : D0 05        "  "        pop    X0005
0E17 : D0 04        "  "        pop    X0004
0E19 : D0 03        "  "        pop    X0003
0E1B : D0 02        "  "        pop    X0002
0E1D : D0 01        "  "        pop    X0001
0E1F : D0 00        "  "        pop    X0000
0E21 : D0 D0        "  "        pop    psw
0E23 : D0 82        "  "        pop    dpl
0E25 : D0 83        "  "        pop    dph
0E27 : D0 F0        "  "        pop    b
0E29 : D0 E0        "  "        pop    acc
0E2B : 32           "2"         reti
This is clearly an interrupt service routine, you can tell by the push operations at the start (to back up working registers) and the pulls at the end, finished by a "reti" - return from interrupt operand.

My analysis in comments:

	jnb	ti,L0DDE 		; branch if interrupt not caused by UART sending a byte.
	mov	X002B,#000H 	        ; clear 0x2B if byte had been sent already (0x2B is a 'byte sent flag' from later in this interrupt)
	clr	ti			; clear Tx interrupt flag
	jnb	ri,L0E11		; branch if interrupt not caused by UART recieving a byte. exits interrupt
	mov	X002A,sbuf		; copy serial buffer to 0x2A
	mov	a,X002C			; 0x2C set outside of interrupt.
	jz	L0E09			; jump if zero
	mov	a,X002B
	jz	L0DF1			;jump if Tx interrupt flag was set above, this means we were interrupted by both rx and tx.
	jnb	ti,L0DEC		; wait for Tx complete. as we sent a byte last time.
	clr	ti			; clear Tx interrupt flag
	mov	a,X002D			; set elsewhere
	jz	L0E02			
	mov	a,X002A			; move serial buffer to ACC
	anl	a,#0FEH			; bitwise AND
	jnz	L0E02			; branch if zero
	mov	a,X002A			; move serial buffer to ACC
	orl	a,#080H			; bitwise OR - set bit 7
	mov	r7,a			; move serial byte with bit7=1 to R7
	sjmp	L0E04
	mov	r7,X002A		; move serial byte to R7, either with bit0 cleared or not (if 0x2D == 0)
	mov	sbuf,r7			; write R7 back to serial buffer for transmission..
	mov	X002B,#001H		; this makes the interrupt wait for transmission next time it runs, the 'byte sent flag'
	mov	X0029,#001H		; set 0x29
	clr	ri			; clear Rx interrupt flag
	lcall	L1295		        ; this call clears ram 0x0023, 24, 25, 26
So basically it will receive a byte, save it to 0x2A then send it onwards IF 0x2C is nonzero. This supports the daisy chain hypothesis as it would appear (when enabled) the interrupt forwards packets. it also modifies bit7 of the packet under some circumstances, if 0x2D is set AND sbuf bit1-bit7 are zero (bit0 is ignored by the &0xFE) then it will set bit7. This would lead to a behaviour where the first device to get such a packet would modify it, the rest would simply forward it. Definately part of the daisy-chain control method. I'd imagine the packet would remain unmodified when a node is done transmitting. There is probably a 'Reset' packet to get the node transmitting again. bit0 is probably used for this as it remains unchecked and unmodified when forwarding packets.

There is definitely some logic elsewhere in the program, time to search for code that looks at RAM 0x2A and sets 0x2D. Given the nature of compiled C code this is a ridiculous task, there are branches and calls everywhere. I think an 8051 emulator might be the way to go here.


Another option is to try out sending packets like 0x00 and 0x01, see if anything happens. Need to know the baud rate for this. So, diving back into the datasheet, Timer1 is used to generate the UART clock:

UartBaudRate = 1/2 x T1_Overflow_Rate

T1_Overflow_Rate = T1CLK / ( 256 - TH1 )

Sysclk is configured with mov X00B2,#0C7H, internal oscillator enabled, SYSCLK derived from Internal Oscillator divided by 1, ie 24.5MHz

T1mode is set with mov tmod,#020H, Mode 2: 8-bit counter/timer with auto-reload as recommended for UART usage.

T1clk is configured with mov X008E,#018H to use sysclk with no divider

TH1 is set by mov th1,#0ECH.. Which is not in the table. WTF. Non standard baud rate? ok then.. ( 24500000 / ( 256 - 236 ) ) / 2 = 612,500 bps.

I guess the best way to test this out is another Silabs uC. I'll probably try that in the next log.


tomdebree wrote 02/08/2017 at 21:43 point

What kind of sampling frequency would be good to use to sample?

As this equates to something like 15us period 

  Are you sure? yes | no

Jarrod wrote 02/09/2017 at 01:10 point

Using a logic analyser, you would want to sample at least 10x the bitrate to make reconstruction easier. 10Mhz sample rate is a good place to start.

  Are you sure? yes | no