Battery Management System Testing
Battery Management System Testing
Introduction:
We had purchased a battery from D-espat in order to power the whole system. This battery
was also integrated with a BMS (Battery management system) inside it. Basically, a BMS is s
device used to control and monitor the battery parameters.
Initially when we received this battery there were 3 ports, one for power another for
charging and the last one for communication. We also got a charger and a connector to USM
wire to connect the BMS to the system (laptop/ raspberry pi) to monitor and control BMS
parameters.
There was no instruction manual received with this, initially when we asked for BMS
documentation they gave the technical specification document, this wasn’t useful as it had
no information on how to communicate with the system. Then a request for the
communication protocol document was requested and this was given. Also, with this their
proprietary software was also given (only works on Windows).
We first connected the BMS to the laptop to see if it was functioning properly and all the
data seemed to be displaying well. Then we referred the document to understand what
were the available registers and how to access them. After going through it, a test python
script was written and the data was read but the response was not as expected.
Debugging:
1) We thought that the way in which checksum was being calculated was the mistake
but no matter what the checksum was we got the same response.
Inference: Checksum was disabled/ ignored by the BMS. When this issue was raised
in the mail it was confirmed by the vendor.
2) The baud rate in the document was mentioned as 9600 and 15200 but after trying
both 115200 only gave response.
3) We sent test values to access the RTD bank, CTRL bank and CFG bank based on the
“Master command packet format” given in the document and tried to observe the
output. It was not as expected. Instead of sending values randomly one by one we
wrote a script to read all possible values from 0000 to FFFF (Hex) and see the
response as the command was of 4 byte length. We only got error free response
when the first byte was set and the next byte was 0. Later we got a conformation
that the document was wrong and we only had set the first byte to send the
command. Eg. If we had to access the first register we had to send 0x 0100 and not
0x 0001.
Inference: The command is only single byte and not 2 bytes.
4) We tried to access different registers and found that the CFG bank gave valid
response when we converted it into ASCII character but we didn’t write command to
access CFG bank we tried to access a different bank. Then later after multiple testing
we found that the binary address to access the bank was not given correctly and this
was confirmed by the company. We came to this inference based on the data length
given by the BMS.
Inference: The bank address for command byte is wrong in the document the correct
assignment are : 001-Config Bank, 010-Control Bank, 011-Real Time Data Bank.
5) The ASCII characters made sense, but when we tried to access other data it made no
sense and we wrote a mail to raise this issue and we got a response saying “When
sending an address or data in command, LSB is sent first. The BMS responds with LSB
first data in case of RTD bank and MSB first data in case of CONFIG bank.”
Inference: We should infer numerical data according to how the register works.
6) In order to read voltage from RTD Bank we sent the necessary command and got
[1,27] as response. This corresponded to 27 * 256 + 1 = 283mV. But after raising this
issue, we understood that the voltage is in 0.1V and not in mV. This it corresponds to
28.3V which made sense as this was the same as shown in proprietary software.
However, the data for cell voltage should be treated as mV only. For nominal voltage
from CFG bank we need to ignore the MSB and only process the LSB.
Inference: Unit for a battery related voltage is 0.1V and for cell related voltage is mV.
Registers with MSB and LSB, mostly likely you have to ignore MSB to make sense of
the data.
7) When we tried to access the state of charge (SOC), there was no response and when
this issue was raised the BMS vendor had told that this model doesn’t have the SOC
and the software that they gave calculates it based on the voltage level.
8) When we tried to access temperature data in the way they told to access the voltage
data it made no sense. Then we got feedback to just take the first byte and subtract it
with 128 in order to get the temperature in degree Celsius.
Inference: You have to subtract 128 from the BMS data to get temperature.
9) Currently after successfully creating a communication with BMS, even though the
task given was to only interface with UART I tried to enable the broadcast feature by
enabling the but in CTRL bank. I tried enabling one while disabling the other, kept
both enabled but was not able to read data. For reading the data through CAN I
wrote a new script but I was not able to read any data. For both CAN and RS485 I
tried all possible standard baud rates but I didn’t get any response. I sent this
complaint and got a response that the RS485 broadcast is disabled as it is very
inefficient because of the overall current consumption that it uses. But I got the same
response for CAN protocol and I didn’t proceed further to clarify this as everything
worked fine as it was.
Final Working:
Based on the register value conversion that has been obtained through testing it was found
that sending a command each second to the RTD bank and reading the response and
decoding it was the easiest solution to implement. Thus this has been done and a dashboard
has been made to control (on and off) and display the CFG bank values and the RTD values
each second and this is stored in a CSV file.
import dash
from dash import html, dcc, Input, Output
import dash_daq as daq
import csv
import time
import os
from datetime import datetime
import serial
if ser != None:
print("COnnection success")
def get_config_fixed():
d = {
"Manufacturer name": None,
"Model number": None,
"Hardware version": None,
"Firmware version": None,
"Cell chemistry": None,
"Date of manufacture": None,
"Client Name": None,
"BMS Serial Number": None,
"Nominal Voltage": None,
"Rated Current": None,
"Cell Series": None
}
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(1,
9)]
x = []
for i in hex_values:
COMMAND = 0x10
REGISTER_ADDRESS = int(i, 16)
DATA_VALUE = 0x0000
x.append(send_command_read_string(COMMAND, REGISTER_ADDRESS,
DATA_VALUE))
for i, key in enumerate(d.keys()):
if i < len(x):
d[key] = x[i]
d["Cell Series"] = send_command(0x10, 0x2100, 0x0000)[2]
temp = send_command(0x10, 0x2200, 0x0000)
d["Nominal Voltage"] = str(round((temp[1] * 256 + temp[2]) * 0.1, 1)) +
"V"
d["Rated Current"] = d["Model number"][-4:]
return d
def get_config_var():
d = {
"bp": {
"Charge Cutoff (V)": None,
"Discharge Cutoff (V)": None,
"Sleep Cutoff (V)": None,
"Max Charging Current (A)": None,
"Max Discharging Current (A)": None,
"High Temperature Cutoff (°C)": None,
"Low Temperature Cutoff (°C)": None
},
"cp": {
"Charge Cutoff (mV)": None,
"Discharge Cutoff (mV)": None,
"Sleep Cutoff (mV)": None,
},
"op": {
"Balance Type": None
}
}
return d
def get_active_state_description(active_state_value):
state_descriptions = {
100: "System fault",
101: "Temperature trip",
102: "Short circuit trip",
103: "Overload current trip",
104: "Cell voltage fault",
105: "Over-charge trip",
106: "Over-discharge trip",
107: "Pre-charge state",
108: "Normal operation",
109: "Critical over-charge trip",
110: "Critical over-discharge trip",
90: "User disabled state",
91: "Sleep state",
}
return state_descriptions.get(active_state_value, "Unknown state")
def get_rtd():
temp = send_command(0x30, 0x0F00, 0x0000)
v = round((temp[1] + temp[2] * 256) * 0.1, 1)
temp = send_command(0x30, 0x1300, 0x0000)
i = round((temp[1] + temp[2] * 256) * 0.1, 1)
temp = send_command(0x30, 0x0100, 0x0000)
status = get_active_state_description(temp[1])
temp = send_command(0x30, 0x4600, 0x0000)
bms_temp = temp[1] - 128
bat_temp = []
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(72,
76)]
for j in hex_values:
COMMAND = 0x30
REGISTER_ADDRESS = int(j, 16)
DATA_VALUE = 0x0000
bat_temp.append((send_command(COMMAND, REGISTER_ADDRESS,
DATA_VALUE)[1]) - 128)
bat_v = []
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(101,
108)]
for j in hex_values:
COMMAND = 0x30
REGISTER_ADDRESS = int(j, 16)
DATA_VALUE = 0x0000
temp = send_command(COMMAND, REGISTER_ADDRESS, DATA_VALUE)
temp = temp[2] * 256 + temp[1]
bat_v.append(temp)
return [datetime.now().strftime("%H:%M:%S"), v, i, status, bms_temp,
bat_temp, bat_v]
app.layout = html.Div([
html.Div([
html.H1("Battery Management System Dashboard", style={
"textAlign": "center",
"color": "white",
"fontWeight": "bold",
"margin": "0",
"padding": "20px" # Padding for spacing inside the background
})
], style={
"backgroundColor": "#003366", # Background color
"borderRadius": "10px", # Rounded corners
"boxShadow": "0px 4px 10px rgba(0, 0, 0, 0.2)", # Subtle shadow
"marginBottom": "20px" # Spacing below the title
}),
html.Div([
html.H2("Variable Configuration", style={"color": "#003366",
"marginBottom": "10px"}),
html.Table([
html.Thead(html.Tr([html.Th(col, style={
"border": "1px solid black",
"padding": "5px",
"textAlign": "left",
"backgroundColor": "#e6f2ff"
}) for col in ["Category", "Parameter", "Value"]])),
html.Tbody(id="variable-config", style={"border": "1px
solid black"})
], style={"width": "100%", "borderCollapse": "collapse"}),
], style={"width": "48%"}),
], style={"display": "flex", "justifyContent": "space-between",
"marginBottom": "20px"}),
@app.callback(
[Output("fixed-config", "children"),
Output("variable-config", "children")],
Input("fetch-config-btn", "n_clicks"),
prevent_initial_call=True
)
def fetch_config(n_clicks):
fixed_config = get_config_fixed()
time.sleep(1)
fixed_config = get_config_fixed()
variable_config = get_config_var()
@app.callback(
Output("battery-status", "children"),
Input("battery-toggle", "value")
)
def toggle_battery(state):
COMMAND = 0xA0
REGISTER_ADDRESS = 0x0100
DATA_VALUE = 0x0100 if state else 0x0000
send_command(COMMAND, REGISTER_ADDRESS, DATA_VALUE)
return f"Battery is now {'On' if state else 'Off'}"
@app.callback(
Output("real-time-data", "value"),
[Input("log-data-btn", "n_clicks"), Input("stop-logging-btn",
"n_clicks")],
prevent_initial_call=True
)
def control_data_logging(log_clicks, stop_clicks):
global is_logging
ctx = dash.callback_context
if not ctx.triggered:
return dash.no_update
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
if triggered_id == "log-data-btn":
is_logging = True
return "Logging started..."
elif triggered_id == "stop-logging-btn":
is_logging = False
return "Logging stopped."
@app.callback(
Output("real-time-data", "value", allow_duplicate=True),
Input("interval-component", "n_intervals"),
prevent_initial_call=True
)
def update_real_time_data(_):
global is_logging
if not is_logging:
return dash.no_update
file_exists = os.path.isfile(output_file)
with open(output_file, mode="a", newline="") as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(["Time", "Voltage", "Current", "State", "BMS
Temp", "Battery Temps", "Battery Voltages"])
rtd_data = get_rtd()
writer.writerow(rtd_data)
display_data = (
f"Time: {rtd_data[0]}, Voltage: {rtd_data[1]}V, Current:
{rtd_data[2]}A, "
f"State: {rtd_data[3]}, BMS Temp: {rtd_data[4]}°C\n"
f"Battery Temps: {rtd_data[5]}\nBattery Voltages: {rtd_data[6]}"
)
return display_data
if __name__ == "__main__":
app.run_server(debug=True, use_reloader=False)