Reads a JBD (Jiabaida) BMS over UART. Pack voltage, current, state of charge, cell voltages, temperatures, and energy used.
Tested with SP17S005P17S40A which differs from the bms-tools' JBD_REGISTER_MAP.md
9600 8N1 UART
Request/response with the host polling.
to BMS: 0xDD cmd reg len data[len] crc_hi crc_lo 0x77
from BMS: 0xDD reg status len data[len] crc_hi crc_lo 0x77
cmd 0xA5 read, 0x5A write
status 0x00 ok, anything else is a refusal
crc 0x10000 - sum(bytes covered), big-endian
The checksum covers different bytes in each direction. Requests cover
reg + len, excluding the command byte. Responses cover status + len + data,
excluding the register byte.
| offset | field | type | unit | map said |
|---|---|---|---|---|
0x00 |
pack voltage | U16 | 10 mV | same |
0x02 |
pack current | S16 | 10 mA, negative = discharge | same |
0x04 |
remaining capacity | U16 | 10 mAh | same |
0x06 |
nominal capacity | U16 | 10 mAh | same |
0x08 |
cycles | U16 | same | |
0x0A |
manufacture date | U16 | packed y/m/d | same |
0x10 |
protection flags | U16 | bitfield | same |
0x12 |
software version | U8 | state of charge | |
0x13 |
state of charge | U8 | percent | FET status |
0x14 |
FET status | U8 | bit0 charge, bit1 discharge | cell count |
0x15 |
cell count | U8 | NTC count | |
0x16 |
NTC count | U8 | (temperatures) | |
0x17… |
NTC values | U16 each | 0.1 K |
Also note there are 9 trailing bytes past the temperatures that are undocumented. Make sure to use the length value when decoding.
#include <jbd/jbd.h>
#include <jbd/serial.h>
int fd = jbd_serial_open("/dev/ttyUSB1"); /* NULL for the default */
jbd_frame_t frame;
jbd_basic_t basic;
if (jbd_serial_read_register(fd, JBD_REG_BASIC, 1000, &frame, NULL) == JBD_OK
&& jbd_decode_basic(&frame, &basic) == JBD_OK) {
printf("%.2f V %.2f A %u%%\n",
(double) basic.volts, (double) basic.amps, basic.soc_percent);
}
jbd_serial_close(fd);jbd_serial_read_register blocks up to its timeout.
jbd_feed() can parse one byte at a time.
jbd_status_t jbd_feed(jbd_parser_t *parser, uint8_t byte, jbd_frame_t *out);| returns | meaning |
|---|---|
JBD_OK |
*out holds a frame, checksum and status good |
JBD_INCOMPLETE |
the usual answer |
JBD_E_CRC |
checksum rejected it |
JBD_E_STATUS |
the BMS refused; out->status says why |
JBD_E_TRAILER |
end byte was not 0x77, so framing was lost |
jbd_energy_update(&energy, &basic, dt_ms);
energy.joules /* trapezoidal integral of volts x amps */
energy.coulomb_joules /* from the capacity the BMS reports */
jbd_energy_disagreement(&energy)Integrating is responsive and finely grained but it tends to drift: it depends on polling often enough to catch current swings. The BMS also coulomb counts, which will not drift but is coarser — on a 45 V pack one 10 mAh step is about 1.6 kJ.
cmake -S . -B build && cmake --build build -j
ctest --test-dir build
./build/bms-monitor /dev/ttyUSB1 200As a subproject:
add_subdirectory(BmsHub)
target_link_libraries(your_app PRIVATE jbd)