Driver for Texas Instruments' battery state-of-charge estimator.

Dependents:   BQ34Z100G1-Utils BQ34Z100G1-ChemIDMeasurer

BQ34Z100G1 Driver

Newer releases of this driver have moved to GitHub: https://github.com/USCRPL/BQ34Z100G1-Driver

By USC Rocket Propulsion Lab / Arpad Kovesdy, Kyle Marino, Jamie Smith

After lots of development, testing, debugging, dead ends, backtracking, and retracing our steps, we at RPL are proud to present a complete driver for Texas Instruments' BQ34Z100G1 state-of-charge estimator. This handy chip handles all the complexities of monitoring a battery's charging and discharging cycle, and is one of the only real ways to have a real estimate of how much usable energy is left in a standard lithium-ion battery.

However, in order to perform this function, the IC needs a lot of information, including both the on-paper specifications of your battery system and the results of several different calibration runs of your real battery system. With one exception*, this driver can handle all the needed configuration, taking you from unconfigured to calibrated in as little time as possible.

Note: To initially program the Chem ID information into each chip to configure it for your battery, you will need an EV2300 or EV2400 programmer box from TI, as well as a header to plug it in on your board.

Calibrating the Chip

See here.

Credits

The initial version of this driver was taken from Ralim on GitHub here.

However, we have made a huge number of changes and additions since then, including porting the Arduino library to Mbed, cleaning up the code to use enums, breaking out the configuration to external constants, and fixing looots of bugs.

BQ34Z100.cpp

Committer:
Jamie Smith
Date:
2021-02-07
Revision:
1:6483d36150c3
Parent:
0:6d09d3ad58aa

File content as of revision 1:6483d36150c3:

#include "BQ34Z100.h"

#include <cinttypes>

BQ34Z100::BQ34Z100(PinName sda, PinName scl, int hz) : _i2c(sda,scl)
{
    //Set the I2C bus frequency
    _i2c.frequency(hz);
}

//For example, sending Control(0x0414)
//Translates into WRITE 0x00 0x14 0x04
//Command1 = 0x04 and Command2 = 0x14 in this case
void BQ34Z100::sendControlCommand(Control control)
{
	//First write two bytes, LSB first
	uint16_t controlBytes = static_cast<uint16_t>(control);
	write(Command::Control, controlBytes & 0xFF, (controlBytes >> 8) & 0xFF);
}

//For example, sending Control(0x0414)
//Translates into WRITE 0x00 0x14 0x04
//Command1 = 0x04 and Command2 = 0x14 in this case
uint16_t BQ34Z100::readControlCommand(Control control)
{
    //First write two bytes, LSB first
    uint16_t controlBytes = static_cast<uint16_t>(control);
    write(Command::Control, controlBytes & 0xFF, (controlBytes >> 8) & 0xFF);
    //Read two bytes from the register 0x00
    return read(Command::Control, 2);
}

//Writes over I2c to the device
//For example the start subcommand WRITE 01 00 to Register 00 requires
//write(0x00, 0x01, 0x00)
void BQ34Z100::write(Command command, const uint8_t cmd1, const uint8_t cmd2)
{
	//TODO, add i2c error checking like in read()
	_i2c.start();
	_i2c.write(GAUGE_ADDRESS | 0x0);
	_i2c.write(static_cast<uint8_t>(command));
	_i2c.write(cmd1);
	_i2c.write(cmd2);
	_i2c.stop();
}

//Similar to the previous function but only writes 1 byte
void BQ34Z100::write(Command command, const uint8_t cmd)
{
	_i2c.start();
	_i2c.write(GAUGE_ADDRESS | 0x0);
	_i2c.write(static_cast<uint8_t>(command));
	_i2c.write(cmd);
	_i2c.stop();
}

//Reads over i2c to the device
//For example, to finish the subcommand sequence (step 2), READ 2 bytes from register 00
//read(0x00, 2)
uint32_t BQ34Z100::read(Command command, const uint8_t length)
{
	uint32_t val = 0; //Contains next byte of data that will be read

    for (int i = 0; i < length; i++)
    {
    	_i2c.start();

    	int writeResult = _i2c.write(GAUGE_ADDRESS | 0x0);

    	if(writeResult != 1)
    	{
    		printf("A write error has occurred when transmitting address.\r\n");
    		return 0;
    	}

    	_i2c.write(static_cast<uint8_t>(command) + i);
    	_i2c.stop();

    	_i2c.start();
    	_i2c.write(GAUGE_ADDRESS | 0x1);

    	//Swap bytes around (convert to Little Endian)
    	val |= (_i2c.read(false) << (8 * i));

    	_i2c.stop();

	}

	return val;
}

void BQ34Z100::enableCal()
{
    sendControlCommand(Control::CAL_ENABLE);
}

void BQ34Z100::enterCal()
{
    sendControlCommand(Control::ENTER_CAL);
}

void BQ34Z100::exitCal()
{
    sendControlCommand(Control::EXIT_CAL);
}

//Warning: Enabling IT should only be done when configuration is complete
void BQ34Z100::ITEnable()
{
    sendControlCommand(Control::IT_ENABLE);
}

uint16_t BQ34Z100::getStatus()
{
	return readControlCommand(Control::CONTROL_STATUS);
}

uint16_t BQ34Z100::getChemID()
{
	return readControlCommand(Control::CHEM_ID);
}

uint16_t BQ34Z100::getStateOfHealth()
{
	return read(Command::StateOfHealth, 2);
}

uint8_t BQ34Z100::getSOC()
{
	return read(Command::StateOfCharge, 1);
}

uint16_t BQ34Z100::getError()
{
	return read(Command::MaxError, 1);
}

uint16_t BQ34Z100::getRemaining()
{
	return read(Command::RemainingCapacity, 2);
}

uint16_t BQ34Z100::getVoltage()
{
	return read(Command::Voltage, 2);
}

int16_t BQ34Z100::getCurrent()
{
	int16_t result = read(Command::Current, 2);
	if (result < 0) result = -result;
	return result;
}

double BQ34Z100::getTemperature()
{
	//The device returns an internal temperature in units of 0.1K
	//Convert to celsius by subtracting dividing by 10 then subtracting 273.15K
	return (read(Command::Temperature, 2)/10.0) - 273.15;
}

int BQ34Z100::getSerial()
{
	return read(Command::SerialNumber, 2);
}

void BQ34Z100::reset()
{
    sendControlCommand(Control::RESET);
	ThisThread::sleep_for(175ms); // experimentally determined boot time
}

void BQ34Z100::unseal()
{
	//Follows the UNSEAL commands listed on page 21 of the datasheet
	//First sequence required to read DATA FLASH
    sendControlCommand(Control::UNSEAL_KEY1);
    sendControlCommand(Control::UNSEAL_KEY2);
}

void BQ34Z100::seal()
{
	//Reseals the sensor, blocking access to certain flash registers
	//Would not recommend using this
	//See page 22 of the datasheet
    sendControlCommand(Control::SEALED);
}

//TODO: see if we can get away with taking the ThisThread::sleep_for away
//as they will slow down the main Hamster loop
void BQ34Z100::changePage(char subclass, uint16_t offset)
{
	ThisThread::sleep_for(10ms);
	//Enable block data flash control (single byte write)
	write(Command::BlockDataControl, 0x00);
	ThisThread::sleep_for(10ms);

	//Use DataFlashClass() command to access the subclass
	write(Command::DataFlashClass, subclass);
    currFlashPage = subclass;
	ThisThread::sleep_for(10ms);

	//Select the block offset location
	//Blocks are 32 in size, so the offset is which block the data sits in
	//Ex: 16 is block 0x00, 52 is block 0x01 (called "index" variable)
	currFlashBlockIndex =(uint8_t)(offset/32);
	write(Command::DataFlashBlock, currFlashBlockIndex);
	ThisThread::sleep_for(20ms);
}

void BQ34Z100::updateChecksum()
{

    uint8_t newChecksum = calcChecksum();

	//Send new checksum thru I2C
	write(Command::BlockDataCheckSum, newChecksum);
    printf("Writing new checksum for page %" PRIu8 " block %" PRIu8 ": 0x%" PRIx8 "\r\n", currFlashPage, currFlashBlockIndex, newChecksum);

    ThisThread::sleep_for(50ms); //Wait for BQ34Z100 to process, may be totally overkill
}


//Warning: change page first before reading data
//Copies a page in flash into the "flashbytes" array
void BQ34Z100::readFlash()
{
	//Request read flash
	_i2c.write(GAUGE_ADDRESS | 0);
	_i2c.write(static_cast<uint8_t>(Command::BlockData)); //Command to read Data Flash
	_i2c.stop();

	_i2c.start();
	_i2c.write(GAUGE_ADDRESS | 1);
	//Read all bytes from page (which is 32 bytes long)
    //_i2c.read(GAUGE_ADDRESS, reinterpret_cast<char*>(flashbytes), 32);
    for (int i = 0; i<32; i++)
	{
		//Store in flashbytes (memory)
		flashbytes[i] = _i2c.read(i < 31);
	}

	_i2c.stop();

    uint8_t expectedChecksum = read(Command::BlockDataCheckSum, 1);

    if(expectedChecksum != calcChecksum())
    {
        printf("ERROR: Checksum of flash memory block does not match.  I2C read was likely corrupted.");
    }

    printf("Page %" PRIu8 " block %" PRIu8 " contents:", currFlashPage, currFlashBlockIndex);
    for(size_t byteIndex = 0; byteIndex < 32; ++byteIndex)
    {
        printf(" %" PRIx8, flashbytes[byteIndex]);
    }
    printf("\r\n");
	printf("Checksum: 0x%" PRIx8 "\r\n", expectedChecksum);


	ThisThread::sleep_for(10ms); //Is this necessary?
}

//Writes new data, first to flashbytes array, then updates on the sensor as well
//Note: Requires update of the checksum after changes are made
//See pg. 25 of datasheet for reference
//Input: subclass ID (index)
//Input: offset
//Input: length of write
void BQ34Z100::writeFlash(uint8_t index, uint32_t value, int len)
{
	//Has to be a number between 0 to 31
	//Use changePage to set the offset correctly beforehand
	if (index > 31) index = index % 32;

	//Write to I2C bus and change flashbytes at the same time
	//Necessary for checksum calculation at the end
	if (len == 1)
	{
		flashbytes[index] = value;
		write(static_cast<Command>(static_cast<uint8_t>(Command::BlockData) + index), (unsigned char)value);
		printf("Flash[%d] <- 0x%" PRIx8 "\n", index, flashbytes[index]);

	} else if (len > 1)
	{
		//Process every byte except the last
		for (int i = 0; i<len-1; i++)
		{
			flashbytes[index+i] = value >> 8*((len-1)-i);
			printf("Flash[%d] <- 0x%" PRIx8 "\n", index + i, flashbytes[index+i]);
			write(static_cast<Command>(static_cast<uint8_t>(Command::BlockData) + index + i), flashbytes[index+i]);
		}

		//Last byte (lower byte)
		flashbytes[index+len-1] = value & 0xFF;
		printf("Flash[%d] <- 0x%" PRIx8 "\n", index + (len - 1), flashbytes[index+len-1]);
		write(static_cast<Command>(static_cast<uint8_t>(Command::BlockData) + index + len - 1), value & 0xFF);
	}
}

uint8_t* BQ34Z100::getFlashBytes()
{
	return flashbytes;
}

//Edits the design capacity and design energy of the battery
void BQ34Z100::changePage48()
{
	changePage(48, 0); //Block 48, offset 0
	readFlash();
	writeFlash(11, DESIGNCAP, 2);
	writeFlash(13, DESIGNENERGY, 2);
	updateChecksum();
	ThisThread::sleep_for(300ms);
}

//This function sets certain settings, such as enabling the external voltage divider,
//enablibg calibration, enabling internal temp sensor, and setting the number of cells in the bat.
//and selecting the correct LED config mode
void BQ34Z100::changePage64()
{
	changePage(64, 0); //Block 64, offset 0
	readFlash();

	//Edit pack configuration register (has an upper and lower byte)
	//Enables external voltage divider use
	uint8_t packConfig_high = flashbytes[0];
	uint8_t packConfig_low = flashbytes[1];
	if (VOLTSEL)
	{
		packConfig_high |= 0x08; //00001000
	}

	//Also allow calibration by switching the CAL_EN bit to 1
	packConfig_high |= 0x40; //01000000

	//Set temps bit.
	packConfig_low &= ~(1);
	packConfig_low |= USE_EXTERNAL_THERMISTOR;

	writeFlash(0, packConfig_high, 1);
	writeFlash(1, packConfig_low, 1);

	// Disable fast convergence as recommended in the calibration procedure
	uint8_t packConfigB = flashbytes[2];
	packConfigB &= ~1;
	writeFlash(2, packConfigB, 1);

	//Update LED config and number of cells in battery
	writeFlash(4, LEDCONFIG, 1);
	writeFlash(7, 0x04, 1);
	updateChecksum();
	ThisThread::sleep_for(300ms);
}

void BQ34Z100::changePage80()
{
	changePage(80, 0);
	readFlash();

	writeFlash(0, LOADSELECT, 1);
	writeFlash(1, LOADMODE, 1);
	writeFlash(10, 10, 2);
	updateChecksum();
	ThisThread::sleep_for(300ms);

	changePage(80, 53);
	readFlash();
	//Update cell terminate voltage
	writeFlash(53, ZEROCHARGEVOLT, 2);
	updateChecksum();
	ThisThread::sleep_for(300ms);
}

void BQ34Z100::changePage82()
{
	changePage(82, 0);
	readFlash();
	//Update QMax cell 0
	writeFlash(0, QMAX0, 2);
	updateChecksum();
	ThisThread::sleep_for(300ms);
}

//Input: The actual battery voltage, measure with multimeter (mV)
//Output: The new voltage divider ratio written
uint16_t BQ34Z100::calibrateVoltage(uint16_t currentVoltage)
{
	changePage(104, 0);
	readFlash();
	//Gets the voltage divider value stored in flash
	uint16_t flashVoltage = (uint16_t)(flashbytes[14] << 8);
	flashVoltage |= (uint16_t)(flashbytes[15]);

	float readVoltage = (float)getVoltage();
	float newSetting = ((float)(currentVoltage)/readVoltage)*(float)(flashVoltage);
	uint16_t writeSetting; //This 2 byte integer will be written to chip
	if (newSetting>65535.0f) writeSetting=65535;
	else if (newSetting < 0.0f) writeSetting=0;
	else writeSetting=(uint16_t)(newSetting);
	writeFlash(14, writeSetting, 2);
	updateChecksum();
	ThisThread::sleep_for(10ms);

	// also change the "Flash Update OK Cell Volt" as the datasheet says to:
    changePage(68, 0);
    readFlash();

    int16_t oldUpdateOK = 0;
    oldUpdateOK |= (static_cast<uint16_t>(flashbytes[0]) << 8);
    oldUpdateOK |= flashbytes[1];

    int16_t newUpdateOK = static_cast<int16_t>(round(FLASH_UPDATE_OK_VOLT * CELLCOUNT * (5000.0f/writeSetting)));
    printf("Changing Flash Update OK Voltage from %" PRIi16 " to %" PRIi16 "\r\n", oldUpdateOK, newUpdateOK);

    writeFlash(0, newUpdateOK, 2);
    updateChecksum();

	//Test output
	printf("Register (voltage divider): %d\r\n", flashVoltage);
	printf("New Ratio: %f\r\n", ((float)(currentVoltage)/readVoltage));
	printf("READ VOLTAGE (mv): %f\r\n", readVoltage);
	return (uint16_t)newSetting;
}

//If too much calibration is done, you may want to reset the Voltage divider
//Sets voltage divider register to custom value (in mV)
//Recommended: 1.5x the current battery voltage
void BQ34Z100::setVoltageDivider(uint16_t newVoltage)
{
    changePage(104, 0);
    readFlash();
    writeFlash(14, newVoltage, 2);
    updateChecksum();
    ThisThread::sleep_for(300ms);
}

//Calibrates the current output to match the calibration current input (calCurrent)
void BQ34Z100::calibrateShunt(int16_t calCurrent)
{
	if (calCurrent < 0) calCurrent = -calCurrent;

	int16_t currentReading = getCurrent();
	if (currentReading < 0) currentReading = -currentReading;

	changePage(104, 0);
	readFlash();
	ThisThread::sleep_for(30ms);

	// read and convert CC Gain
	uint32_t currentGainDF = ((uint32_t)flashbytes[0]) << 24 | ((uint32_t)flashbytes[1]) << 16 | ((uint32_t)flashbytes[2]) << 8 | (uint32_t)flashbytes[3];
	float currentGainResistance = (4.768f/xemicsToFloat(currentGainDF));

	uint32_t currentDeltaDF = ((uint32_t)flashbytes[4]) << 24 | ((uint32_t)flashbytes[5]) << 16 | ((uint32_t)flashbytes[6]) << 8 | (uint32_t)flashbytes[7];
	float currentDeltaResistance = (5677445/xemicsToFloat(currentGainDF));

	float newGain = (((float)currentReading)/((float)calCurrent)) * currentGainResistance;

	uint32_t newGainDF = floatToXemics(4.768 / newGain);
    float DeltaDF = floatToXemics(5677445/newGain);

    printf("currentGainDF = 0x%" PRIx32 ", currentGainResistance = %f, currentDeltaDF = %" PRIx32 ", currentDeltaResistance = %f, newGain = %f, newGainDF = 0x%" PRIx32 "\n",
           currentGainDF, currentGainResistance, currentDeltaDF, currentDeltaResistance, newGain, newGainDF);

    writeFlash(0, newGainDF, 4);
	writeFlash(4, DeltaDF, 4);

	updateChecksum();
	ThisThread::sleep_for(300ms);
}

void BQ34Z100::setSenseResistor() {
    changePage(104, 0);
    ThisThread::sleep_for(30ms);

    //Constants use to convert mOhm to the BQ34Z100 data flash units
    uint32_t GainDF = floatToXemics(/*4.768/SENSE_RES*/0.4768);
    uint32_t DeltaDF = floatToXemics(5677445/SENSE_RES);
    writeFlash(0, GainDF, 4);
	writeFlash(4, DeltaDF, 4);

	printf("newGain = %f, GainDF = 0x%" PRIx32 "\n",
	       SENSE_RES, GainDF);

    updateChecksum();
	ThisThread::sleep_for(300ms);
}

uint16_t BQ34Z100::readDeviceType()
{
	return readControlCommand(Control::DEVICE_TYPE);
}

uint16_t BQ34Z100::readFWVersion() {
    return readControlCommand(Control::FW_VERSION);
}

uint16_t BQ34Z100::readHWVersion() {
    return readControlCommand(Control::HW_VERSION);
}

uint8_t BQ34Z100::calcChecksum() {
    uint8_t chkSum = 0;
    //Perform 8 bit summation of the entire BlockData() on byte-per-byte
    //basis. Checksum = (255-sum)
    //For every byte in flashbytes, add to sum
    for (int i = 0; i<32; i++)
    {
        chkSum += flashbytes[i];
    }

    chkSum = 255 - chkSum;

    return chkSum;
}

uint32_t BQ34Z100::floatToXemics(float value) {
	int iByte1, iByte2, iByte3, iByte4, iExp;
	bool bNegative = false;
	float fMantissa;
	// Don't blow up with logs of zero
	if (value == 0) value = 0.00001F;
	if (value < 0)
	{
		bNegative = true;
		value = -value;
	}
	// find the correct exponent
	iExp = (int)((log(value) / log(2)) + 1);// remember - log of any base is ln(x)/ln(base)

	// MS byte is the exponent + 0x80
	iByte1 = iExp + 128;

	// Divide input by this exponent to get mantissa
	fMantissa = value / (pow(2, iExp));

	// Scale it up
	fMantissa = fMantissa / (pow(2, -24));

	// Split the mantissa into 3 bytes
	iByte2 = (int)(fMantissa / (pow(2, 16)));

	iByte3 = (int)((fMantissa - (iByte2 * (pow(2, 16)))) / (pow(2, 8)));

	iByte4 = (int)(fMantissa - (iByte2 * (pow(2, 16))) - (iByte3 * (pow(2, 8))));

	// subtract the sign bit if number is positive
	if (bNegative == false)
	{
		iByte2 = iByte2 & 0x7F;
	}
	return (uint32_t)((uint32_t)iByte1 << 24 | (uint32_t)iByte2 << 16 | (uint32_t)iByte3 << 8 | (uint32_t)iByte4);
}

float BQ34Z100::xemicsToFloat(uint32_t xemics)
{
	bool bIsPositive = false;
	float fExponent, fResult;
	uint8_t vMSByte = (uint8_t)(xemics >> 24);
	uint8_t vMidHiByte = (uint8_t)(xemics >> 16);
	uint8_t vMidLoByte = (uint8_t)(xemics >> 8);
	uint8_t vLSByte = (uint8_t)xemics;
	// Get the sign, its in the 0x00 80 00 00 bit
	if ((vMidHiByte & 128) == 0)
	{ bIsPositive = true; }

	// Get the exponent, it's 2^(MSbyte - 0x80)
	fExponent = pow(2, (vMSByte - 128));
	// Or in 0x80 to the MidHiByte
	vMidHiByte = (uint8_t)(vMidHiByte | 128);
	// get value out of midhi byte
	fResult = (vMidHiByte) * 65536;
	// add in midlow byte
	fResult = fResult + (vMidLoByte * 256);
	// add in LS byte
	fResult = fResult + vLSByte;
	// multiply by 2^-24 to get the actual fraction
	fResult = fResult * pow(2, -24);
	// multiply fraction by the ‘exponent’ part
	fResult = fResult * fExponent;
	// Make negative if necessary
	if (bIsPositive)
		return fResult;
	else
		return -fResult;
}

uint8_t BQ34Z100::getUpdateStatus() {
	changePage(82, 0);
	readFlash();
	return flashbytes[4];
}

std::pair<uint16_t, uint16_t> BQ34Z100::getFlags() {
	uint16_t flags = read(Command::Flags, 2);
	uint16_t flagsB = read(Command::FlagsB, 2);
	return std::make_pair(flags, flagsB);
}