ZKETECH EBC-A20 Battery Tester
Table of Contents
1. Introduction
The ZKETECH EBC-A20 is a battery tester with integrated charging and discharging circuits. Its basic capabilities are:
- voltage range when discharging 0 - 30 V
- voltage range while charging 0 - 18 V
- charging current range 0.1 - 5 A
- discharge current range: 0.1 - 20 A
The device is pretty popular and there are multiple places where a manual can be downloaded with more detailed specifications. The focus of this article is the control and monitoring interface provided by the battery tester
tldr;
You can use the code on Github gists
2. Control interface
The device is equipped with a Mini USB socket on the back where a provided cable is connected. The cable contains a USB <-> serial converter as the Mini USB connector actually carries serial port signals. I have not checked what happens when an ordinary USB cable is plugged there and connected to a USB hub. I hope the designers took that possibility into account.
The control interface can be used by a vendor-provided piece of software - "EB Software(English Version)_v1.8.8.rar" (mirror). This piece of software can also be found relatively easily.
The physical layer of the control interface is a 9600 bps, 8 bit, odd parity.
2.1. Control protocol
2.1.1. Packet structure and field encoding
The control protocol exchanges packets (also called "frames" in this document) between the controller PC and the battery tester. The packets are binary encoded, a typical packet is shown below in hexadecimal:
fa 0c 00 00 02 1e 00 00 00 00 00 0a 01 3c 00 0a 09 24 f8
The basic structure of the encoded packet is shown below:
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Notes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Value | fa | 0c | 00 | 00 | 02 | 1e | 00 | 00 | 00 | 00 | 00 | 0a | 01 | 3c | 00 | 0a | 09 | 24 | f8 | ||
Description | SOF | CRC | EOF |
Each packet begins with a 0xfa byte and ends with 0xf8. These are labeled as SOF and EOF. The payload are the bytes between offset 1 and 16 (inclusively). A packet also contains a "CRC" which is a simple checksum not an actual Cyclic redundancy check. It can be calculated by performing a XOR operation on the payload bytes:
>>> hex(0x0c ^ 0x00 ^ 0x00 ^ 0x02 ^ 0x1e ^ 0x00 ^ 0x00 ^ 0x00 ^ 0x00 ^ 0x00 ^ 0x0a ^ 0x01 ^ 0x3c ^ 0x00 ^ 0x0a ^ 0x09) '0x24' >>>
A number of packet types sent by the battery tester to the PC or vice-versa have been identified. They are listed in a table below. In order to help in understanding the details about each packet type and example packet and its description has been shown below. This packet is sent by the PC to the charger when the controlling software requests cell charging:
Type | Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Notes |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Start charging | ||||||||||||
fa | 21 | 00 | 32 | 01 | 3c | 00 | 0a | 1c | f8 | |||
SOF | PT | I1 | I2 | V1 | V2 | CUT1 | CUT2 | CRC | EOF |
The first payload byte is the packet type (PT) identifier - 0x21 represents the Start charging packet. Next there are 3 16-bit fields encoding:
- the charging current (in units of 10 mA)
- the charging voltage (in units of 10 mV)
- the cutoff current (in units of 10 mA)
Each 16-bit field value is encoded with 2 bytes using the following scheme:
>>> I1 = 0x00 >>> I2 = 0x32 >>> 240 * I1 + I2 50
In effect this is a "base240" encoding and it's purpose is likely to prevent values larger than 240 (0xf0) to be present in the packet bytes. This makes sense when you consider the fact that bytes 0xfa and 0xf8 are used as "control codes" to mark start and end of packets. In summary the packet above encodes the following fields:
>>> I1 = 0x00 >>> I2 = 0x32 >>> 240 * I1 + I2 50 >>> V1 = 0x01 >>> V2 = 0x3c >>> 240 * V1 + V2 >>> CUT2 = 0x0a >>> 240 * CUT1 + CUT2 10 >>>
Fields which contain measurable physical quantities like voltage, current and charge are furthermore processed so that the precision and range information is encoded. Explaining this encoding in prose would be tedious therefore a Python implementation of the decoding algorithm is provided:
def decode_ranged(h: int, l: int) -> float: """ Decode a base240 value to a float taking into account different offsets and scaling factors for multiple ranges. """ if h & 0x80 == 0x80: if h & 0xe0 == 0xe0: # range is > 200.0 v = 240 * (h & 0x3f) + l m = 0.1 offset = 0x1c00 else: # 10.00 < range < 200.0 v = 240 * (h & 0x7f) + l m = 0.01 offset = 0x800 else: # range is < 10.00 v = 240 * h + l m = 0.001 offset = 0 return (v - offset) * m
The above code was created based off of the algorithm implemented in the esp-ebc-mqtt code.
What follows is a table with descriptions of all of the packets that have been observed so far together with their types, encoded fields and other extra information.
2.1.2. Field reference
Fields like current or voltage have common encoding for all packet types:
Field name | Description | Unit | Notes |
---|---|---|---|
PT | Packet type | ||
DT | Device type | Known types (reference): | |
0x05 is EBC-A05 | |||
0x06 is EBC-A10H | |||
0x09 is EBC-A20 | |||
(FW1, FW2) | Firmware version | Displayed in the ZKETECH aplication, for example 302 is displayed as 'V3.02', not confirmed 100% | |
(T1, T2) | Time | min | |
(I1, I2) | Current | auto | Units and ranges are encoded in raw 16-bit field value |
(V1, V2 | Voltage | auto | |
(C1, C2) | Charge count | auto | |
(E1, E2) | Unknown | Might be energy in Wh | |
(CC1, CC2) | Charging current | 10 mA | Charging |
(CV1, CV2) | Charging voltage | 10 mV | Suspected but not confirmed that ranges are encoded in raw 16-bit field values |
(CUT1, CUT2) | Cutoff current | 10 mA | |
(CC1, CC2) | Discharging current | 10 mA | Discharging |
(CUTV1, CUTV2) | Cutoff voltage | 10 mV | Suspected but not confirmed that ranges are encoded in raw 16-bit field values |
2.1.3. Packet reference
This table does not exhaust all of the packet that I have seen while sniffing the traffic between the original software and the battery tester. A bit more is documented in the Python code.
Kind | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Description | ||||||||||||||||||||
Status | fa | 02 | 00 | 00 | 0a | 13 | 00 | 14 | 00 | 00 | 00 | 32 | 01 | 0a | 00 | 0a | 09 | 35 | f8 | |
Tester idle, | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | X1 | X2 | Y1 | Y2 | Z1 | Z2 | DT | CRC | EOF | |
sent after connecting (X1, X2), (Y1, Y2), (Z1, Z2) | ||||||||||||||||||||
Are parameters from a previous charge/discharge | ||||||||||||||||||||
operation. | ||||||||||||||||||||
Idle FW report, sent after connecting | Status | fa | 66 | 00 | 00 | 08 | 88 | 00 | 14 | 00 | 00 | 01 | 3e | 0c | 8f | 09 | 05 | 09 | 4b | f8 |
Maybe HW version too? | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | FW1 | FW2 | unk | unk | unk | unk | DT | CRC | EOF | |
unk means unknown | ||||||||||||||||||||
Start CC-CV charging | Command | fa | 21 | 00 | 32 | 01 | 3c | 00 | 78 | 02 | f8 | |||||||||
SOF | PT | CC1 | CC2 | CV1 | CV2 | CUT1 | CUT2 | CRC | EOF | |||||||||||
CC-CV time? | Command | fa | 0a | 00 | 03 | 00 | 00 | 00 | 00 | 02 | f8 | |||||||||
sent by the original SW every minute while charging | SOF | PT | T1 | T2 | CRC | EOF | ||||||||||||||
CC-CV charging in progress | Status | fa | 0c | 00 | 32 | 07 | de | 00 | 00 | 00 | 00 | 00 | 32 | 01 | b4 | 00 | 0a | 09 | 63 | f8 |
SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | CC1 | CC2 | CV1 | CV2 | CUT1 | CUT2 | DT | CRC | EOF | ||
CC-CV charging FW report | Status | fa | 70 | 00 | 00 | 04 | 53 | 00 | 00 | 00 | 00 | 01 | 3e | 0c | 8f | 09 | 05 | 09 | 63 | f8 |
sent for a few seconds after charging begins | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | FW1 | FW2 | unk | unk | unk | unk | DT | CRC | EOF | |
CC-CV charging end | Status | fa | 16 | 00 | 0a | 0a | 64 | 00 | 14 | 00 | 00 | 00 | 32 | 01 | 0a | 00 | 0a | 09 | 5c | f8 |
sent when current cutoff threshold is reached | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | CC1 | CC2 | CV1 | CV2 | CUT1 | CUT2 | DT | CRC | EOF | |
CC discharge idle | Status | fa | 00 | 00 | 00 | 10 | 49 | 00 | 00 | 00 | 00 | 00 | 32 | 01 | 3c | 00 | 78 | 09 | 27 | f8 |
SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | CC1 | CC2 | CV1 | CV2 | CUT1 | CUT2 | DT | CRC | EOF | ||
Start CC discharge | Command | fa | 01 | 00 | 03 | 00 | 00 | 00 | 00 | 02 | f8 | |||||||||
(T1, T2) is the time limit, 0 means no time | SOF | PT | CC1 | CC2 | CUTV1 | CUTV2 | T1 | T2 | CRC | EOF | ||||||||||
limit | ||||||||||||||||||||
Adjust CC discharge | Command | fa | 07 | 00 | 03 | 00 | 00 | 00 | 00 | 02 | f8 | |||||||||
(unused in zketech_ebc_a20.py) | SOF | PT | CC1 | CC2 | CUTV1 | CUTV2 | T1 | T2 | CRC | EOF | ||||||||||
Stop CC discharge | Command | fa | 08 | 00 | 03 | 00 | 00 | 00 | 00 | 02 | f8 | |||||||||
(unused in zketech_ebc_a20.py) | SOF | PT | CC1 | CC2 | CUTV1 | CUTV2 | T1 | T2 | CRC | EOF | ||||||||||
CC discharge in progress | Status | fa | 0a | 00 | 32 | 0f | 41 | 00 | 02 | 00 | 00 | 00 | 32 | 01 | 3c | 00 | 3c | 09 | 4e | f8 |
SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | CC1 | CC2 | CUTV1 | CUTV2 | T1 | T2 | DT | CRC | EOF | ||
CC discharge FW report | Status | fa | 64 | 00 | 00 | 0f | 41 | 00 | 00 | 00 | 00 | 01 | 3e | 0c | 8f | 09 | 05 | 09 | 63 | f8 |
sent for a few seconds after charging begins | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | FW1 | FW2 | unk | unk | unk | unk | DT | CRC | EOF | |
CC discharge end | Status | fa | 14 | 00 | 32 | 0c | 77 | 01 | 59 | 00 | 00 | 00 | 32 | 01 | 3c | 00 | 78 | 09 | 7b | f8 |
voltage reaches reaches (CUTV1, CUTV2) | SOF | PT | I1 | I2 | V1 | V2 | C1 | C2 | E1 | E2 | CC1 | CC2 | CUTV1 | CUTV2 | T1 | T2 | DT | CRC | EOF | |
or (T1, T2) expires | ||||||||||||||||||||
Connect | Command | fa | 05 | 00 | 00 | 00 | 00 | 00 | 00 | 05 | f8 | |||||||||
After this '-PC-' appears on LCD | SOF | PT | CRC | EOF | ||||||||||||||||
Disconnect | Command | fa | 06 | 00 | 00 | 00 | 00 | 00 | 00 | 06 | f8 | |||||||||
After this '-PC-' disappears from LCD | SOF | PT | CRC | EOF | ||||||||||||||||
Stop | Command | fa | 02 | 00 | 00 | 00 | 00 | 00 | 00 | 02 | f8 | |||||||||
Used for both charging and discharging, sent | SOF | PT | CRC | EOF | ||||||||||||||||
before disconnecting |