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                                        

Links

Github  Sourcehut  Hackaday

  Fediring