├── LICENSE ├── README.md ├── bq27541.csv ├── bq27545.csv └── hdq.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jens J. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hdq_python 2 | python example for reading Texas Instruments battery gas gauge ICs via HDQ protocol via UART 3 | 4 | useful for reading data, e.g. voltage, temperature, current, power, state-of-charge, et al., from things like iphone batteries 5 | 6 | ### hardware 7 | * some kind of cheap usb to uart adapter, e.g. CP2102, CH340G. [Banggood: CP2102 USB-UART adapter](http://www.banggood.com/CJMCU-CP2102-USB-To-TTLSerial-Module-UART-STC-Downloader-p-970993.html?p=WX0407753399201409DA) 8 | #### connection 9 | * RXD pin of uart is connected to HDQ pin 10 | * TXD pin of uart is tied to RXD/HDQ via small signal diode, e.g. 1N4148, with cathode pointing back to TXD, anode to RXD/HDQ 11 | * if that doesn't work, e.g. not using open-drain uart, refer to the TI app note (slua408a referenced below, fig. 5) 12 | 13 | ### software 14 | python (duh!) 15 | requires pyserial: 16 | ``` 17 | pip install -U pyserial 18 | ``` 19 | 20 | ### example usage 21 | from an old iphone 5S battery, with 1k ohm resistor across +/- terminals: 22 | ``` 23 | $ ./hdq.py /dev/ttyUSB0 -f bq27545.csv 24 | name short hex dec unit 25 | AtRate() AR 0x0000 0 mA 26 | UnfilteredSOC() UFSOC 0x7FFF 32767 % 27 | Temperature() TEMP 0x0BB8 3000 0.1K 28 | Voltage() VOLT 0x1048 4168 mV 29 | Flags() FLAGS 0x0180 384 N/A 30 | NomAvailableCapacity() NAC 0x04C7 1223 mAh 31 | FullAvailableCapacity() FAC 0x0548 1352 mAh 32 | RemainingCapacity() RM 0x045D 1117 mAh 33 | FullChargeCapacity() FCC 0x04DE 1246 mAh 34 | AverageCurrent() AI 0xFFFD -3 mA 35 | TimeToEmpty() TTE 0x4173 16755 Minutes 36 | FilteredFCC() FFCC 0x0000 0 mAh 37 | StandbyCurrent() SI 0xFFFC -4 mA 38 | UnfilteredFCC() UFFCC 0x0600 1536 mAh 39 | MaxLoadCurrent() MLI 0xFF38 -200 mA 40 | UnfilteredRM() UFRM 0x0B99 2969 mAh 41 | FilteredRM() FRM 0x0568 1384 mAh 42 | AveragePower() AP 0xFFF3 -13 mW/cW 43 | InternalTemperature() INTTEMP 0x1048 4168 0.1°K 44 | CycleCount() CC 0x01F4 500 Counts 45 | StateOfCharge() SOC 0x005A 90 % 46 | StateOfHealth() SOH 0xFF76 -138 %/num 47 | PassedCharge() PCHG 0x0000 0 mAh 48 | DOD0() DOD0 0x0600 1536 HEX# 49 | SelfDischargeCurrent() SDSG 0x0080 128 mA 50 | ``` 51 | note that in Apple implementation, not all of the outputs seem to correspond to datasheet values. 52 | 53 | A few useful datapoints that did seem like they are reporting correctly: 54 | * temperature 55 | * voltage 56 | * flags (for determining charged/discharging status) 57 | * remaining capacity 58 | * avg current/avg power 59 | * state of charge 60 | 61 | ### references 62 | inspired by some articles on ripitapart blog: 63 | https://ripitapart.com/2014/09/30/reading-out-hdq-equipped-battery-fuel-gauges-with-a-serial-port/ 64 | https://ripitapart.com/2014/09/30/looking-inside-a-fake-iphone-5s-battery-2/ 65 | 66 | TI's app note about interfacing HDQ protocol with simple UARTs: 67 | http://www.ti.com/lit/an/slua408a/slua408a.pdf 68 | 69 | TI's bq27545 battery gas gauge IC datasheet: 70 | http://www.ti.com/lit/ds/symlink/bq27545-g1.pdf 71 | 72 | -------------------------------------------------------------------------------- /bq27541.csv: -------------------------------------------------------------------------------- 1 | name, shortname, regaddrs, unit, access 2 | AtRate(),AR,0x02/0x03,mA,R/W 3 | AtRateTimeToEmpty(),ARTTE, 0x04/0x05,Minutes,R 4 | Temperature(),TEMP,0x06/0x07,0.1K,R 5 | Voltage(),VOLT,0x08/0x09,mV,R 6 | Flags(),FLAGS,0x0A/0x0B,N/A,R 7 | NomAvailableCapacity(),NAC,0x0C/0x0D,mAh,R 8 | FullAvailableCapacity(),FAC,0x0E/0x0F,mAh,R 9 | RemainingCapacity(),RM,0x10/0x11,mAh,R 10 | FullChargeCapacity(),FCC,0x12/0x13,mAh,R 11 | AverageCurrent(),AI,0x14/0x15,mA,R 12 | TimeToEmpty(),TTE,0x16/0x17,Minutes,R 13 | TimeToFull(),TTF,0x18/0x19,Minutes,R 14 | StandbyCurrent(),SI,0x1A/0x1B,mA,R 15 | StandbyTimeToEmpty(),STTE,0x1C/0x1D,Minutes,R 16 | MaxLoadCurrent(),MLI,0x1E/0x1F,mA,R 17 | MaxLoadTimeToEmpty(),MLTTE,0x20/0x21,Minutes,R 18 | AvailableEnergy(),AE,0x22/0x23,10mWhr,R 19 | AveragePower(),AP,0x24/0x25,mW/cW,R 20 | TTEatConstantPower(),TTECP,0x26/0x27,Minutes,R 21 | CycleCount(),CC,0x2A/0x2B,Counts,R 22 | StateOfCharge(),SOC,0x2C/0x2D,%,R 23 | -------------------------------------------------------------------------------- /bq27545.csv: -------------------------------------------------------------------------------- 1 | name, shortname, regaddrs, unit, access 2 | AtRate(),AR,0x02/0x03,mA,R/W 3 | UnfilteredSOC(),UFSOC,0x04/0x05,%,R 4 | Temperature(),TEMP,0x06/0x07,0.1K,R 5 | Voltage(),VOLT,0x08/0x09,mV,R 6 | Flags(),FLAGS,0x0A/0x0B,N/A,R 7 | NomAvailableCapacity(),NAC,0x0C/0x0D,mAh,R 8 | FullAvailableCapacity(),FAC,0x0E/0x0F,mAh,R 9 | RemainingCapacity(),RM,0x10/0x11,mAh,R 10 | FullChargeCapacity(),FCC,0x12/0x13,mAh,R 11 | AverageCurrent(),AI,0x14/0x15,mA,R 12 | TimeToEmpty(),TTE,0x16/0x17,Minutes,R 13 | FilteredFCC(),FFCC,0x18/0x19,mAh,R 14 | StandbyCurrent(),SI,0x1A/0x1B,mA,R 15 | UnfilteredFCC(),UFFCC,0x1C/0x1D,mAh,R 16 | MaxLoadCurrent(),MLI,0x1E/0x1F,mA,R 17 | UnfilteredRM(),UFRM,0x20/0x21,mAh,R 18 | FilteredRM(),FRM,0x22/0x23,mAh,R 19 | AveragePower(),AP,0x24/0x25,mW/cW,R 20 | InternalTemperature(),INTTEMP,0x28/0x29,0.1°K,R 21 | CycleCount(),CC,0x2A/0x2B,Counts,R 22 | StateOfCharge(),SOC,0x2C/0x2D,%,R 23 | StateOfHealth(),SOH,0x2E/0x2F,%/num,R 24 | PassedCharge(),PCHG,0x34/0x35,mAh,R 25 | DOD0(),DOD0,0x36/0x37,HEX#,R 26 | SelfDischargeCurrent(),SDSG,0x38/0x39,mA,R 27 | -------------------------------------------------------------------------------- /hdq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hdq over uart implementation 4 | # based on http://www.ti.com/lit/an/slua408a/slua408a.pdf 5 | # (c) 2017 jens jensen 6 | 7 | import serial 8 | import argparse 9 | import binascii 10 | import csv 11 | import ctypes 12 | 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("port") 15 | ap.add_argument("-d","--debug", action="store_true") 16 | ap.add_argument("-f","--file", help="csvfile containing reg info") 17 | args = ap.parse_args() 18 | 19 | ser = serial.Serial(args.port, 57600, stopbits=2, timeout=1) 20 | 21 | HDQ_BIT1 = 0xFE 22 | HDQ_BIT0 = 0xC0 23 | HDQ_BIT_THRESHOLD = 0xF8 24 | 25 | def reset(): 26 | #reset 27 | ser.send_break() 28 | ser.read() 29 | 30 | def write_byte(byte): 31 | #convert and write 8 data bits 32 | buf = bytearray() 33 | for i in range(8): 34 | if (byte & 1) == 1: 35 | buf.append(HDQ_BIT1) 36 | else: 37 | buf.append(HDQ_BIT0) 38 | byte = byte >> 1 39 | if args.debug: 40 | print("sending:", binascii.hexlify(buf)) 41 | ser.write(buf) 42 | # chew echoed bytes 43 | ser.read(8) 44 | 45 | def read_byte(): 46 | #read and convert 8 data bits 47 | buf = ser.read(8) 48 | buf = bytearray(buf) 49 | # lsb first, so reverse: 50 | buf.reverse() 51 | if args.debug: 52 | print("recv buf:", binascii.hexlify(buf)) 53 | byte = 0 54 | for i in range(8): 55 | byte = byte << 1 56 | if buf[i] > HDQ_BIT_THRESHOLD: 57 | byte = byte | 1 58 | return byte 59 | 60 | def uint16le(bl, bh): 61 | word = bh << 8 | bl 62 | return word 63 | 64 | def read_reg(reg): 65 | write_byte(reg) 66 | return read_byte() 67 | 68 | def write_reg(reg, byte): 69 | write_byte(0x80 | reg) 70 | write_byte(byte) 71 | 72 | #main 73 | reset() 74 | 75 | if args.file: 76 | with open(args.file) as csvfile: 77 | reader = csv.DictReader(csvfile, skipinitialspace=True) 78 | print("%24s %8s %6s %6s %s" % ("name","short", "hex", "dec", "unit")) 79 | for row in reader: 80 | regs = row["regaddrs"].split('/') 81 | byte_low = read_reg(int(regs[0],16)) 82 | byte_high = read_reg(int(regs[1],16)) 83 | value = uint16le(byte_low, byte_high) 84 | name = row["name"] 85 | shortname = row["shortname"] 86 | unit = row["unit"] 87 | value_int16 = ctypes.c_int16(value).value 88 | print("%24s %8s 0x%04X %6d %s" % (name, shortname, value, value_int16, unit)) 89 | 90 | else: 91 | #demo 92 | print("sendng Control() get DEVICE_TYPE (0x0001)...") 93 | write_reg(0x00, 0x01) 94 | write_reg(0x01, 0x00) 95 | b1 = read_reg(0x00) 96 | b2 = read_reg(0x01) 97 | value = uint16le(b1,b2) 98 | print("value: 0x%04X" % value) 99 | --------------------------------------------------------------------------------