Program Listing for File FuelGauge.cpp¶
↰ Return to documentation for file (components/ESC/src/FuelGauge.cpp
)
#include "FuelGauge.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_err.h>
#include <esp_log.h>
#include <esp_system.h>
#include <ErrUtil.h>
#include "BQ27441_Constants.h"
namespace esc {
static const char* TAG = "ESC";
static const i2c_port_t I2C_PORT = I2C_NUM_0;
static const int I2C_FREQUENCY = 100000;
static const gpio_num_t I2C_SDA_PIN = GPIO_NUM_23;
static const gpio_num_t I2C_SCL_PIN = GPIO_NUM_22;
static const TickType_t I2C_TIMEOUT = 2000;
template <typename T>
static T constrain(T value, T min, T max)
{
if (value < min) { return min; }
if (value > max) { return max; }
return value;
}
FuelGauge::FuelGauge()
: m_conf{},
m_seal_again(false),
m_manual_config(false)
{
m_conf.mode = I2C_MODE_MASTER;
m_conf.sda_io_num = I2C_SDA_PIN;
m_conf.sda_pullup_en = GPIO_PULLUP_DISABLE;
m_conf.scl_io_num = I2C_SCL_PIN;
m_conf.scl_pullup_en = GPIO_PULLUP_DISABLE;
m_conf.master.clk_speed = I2C_FREQUENCY;
}
esp_err_t FuelGauge::temperature(TempMeasure type, std::uint16_t* temp)
{
switch (type)
{
case TempMeasure::Battery:
ESP_ERR_TRY(readWord(TEMP, temp));
break;
case TempMeasure::Internal:
ESP_ERR_TRY(readWord(INT_TEMP, temp));
break;
}
return ESP_OK;
}
esp_err_t FuelGauge::voltage(std::uint16_t* voltage)
{
return readWord(VOLTAGE, voltage);
}
esp_err_t FuelGauge::avgCurrent(std::int16_t* avg_current)
{
return readWord(AVG_CURRENT, reinterpret_cast<std::uint16_t*>(avg_current));
}
esp_err_t FuelGauge::avgPower(std::int16_t* avg_power)
{
return readWord(AVG_POWER, reinterpret_cast<std::uint16_t*>(avg_power));
}
esp_err_t FuelGauge::soc(SocMeasure type, std::uint16_t* soc)
{
switch (type) {
case SocMeasure::Filtered:
return readWord(SOC, soc);
break;
case SocMeasure::Unfiltered:
return readWord(SOC_UNFL, soc);
break;
default:
return ESP_FAIL;
break;
}
return ESP_OK;
}
esp_err_t FuelGauge::setCapacity(std::uint16_t capacity)
{
// Write to STATE subclass (82) of FuelGauge extended memory.
// Offset 0x0A (10)
// Design capacity is a 2-byte piece of data - MSB first
std::uint8_t cap_msb = capacity >> 8;
std::uint8_t cap_lsb = capacity & 0x00FF;
std::uint8_t capacity_data[2] = {cap_msb, cap_lsb};
return writeExtendedData(ID_STATE, 10, capacity_data, 2);
}
esp_err_t FuelGauge::status(std::uint16_t* result)
{
return readControlWord(STATUS, result);
}
esp_err_t FuelGauge::flags(std::uint16_t* result)
{
return readWord(FLAGS, result);
}
esp_err_t FuelGauge::GPOUTPolarity(bool* value)
{
if (value == nullptr) return ESP_FAIL;
std::uint16_t opconfig = 0;
ESP_ERR_TRY(opConfig(&opconfig));
*value = (opconfig & OPCONFIG_GPIOPOL) == OPCONFIG_GPIOPOL;
return ESP_OK;
}
esp_err_t FuelGauge::setGPOUTPolarity(bool active_high)
{
std::uint16_t old_opconfig = 0;
ESP_ERR_TRY(opConfig(&old_opconfig));
bool already_high = (active_high && (old_opconfig & OPCONFIG_GPIOPOL));
bool already_low = (!active_high && !(old_opconfig & OPCONFIG_GPIOPOL));
if (already_high || already_low) return ESP_OK;
uint16_t new_opconfig = old_opconfig;
if (active_high) {
new_opconfig |= OPCONFIG_GPIOPOL;
} else {
new_opconfig &= ~(OPCONFIG_GPIOPOL);
}
ESP_ERR_TRY(writeOpConfig(new_opconfig));
return ESP_OK;
}
esp_err_t FuelGauge::GPOUTFunction(bool* function)
{
if (function == nullptr) return ESP_FAIL;
std::uint16_t opconfig = 0;
ESP_ERR_TRY(opConfig(&opconfig));
*function = ((opconfig & OPCONFIG_BATLOWEN) == OPCONFIG_BATLOWEN);
return ESP_OK;
}
esp_err_t FuelGauge::setGPOUTFunction(bool function)
{
std::uint16_t old_opconfig = 0;
ESP_ERR_TRY(opConfig(&old_opconfig));
bool is_bat_low = (function && (old_opconfig & OPCONFIG_BATLOWEN));
bool is_soc_int = (!function && !(old_opconfig & OPCONFIG_BATLOWEN));
if (is_bat_low || is_soc_int) return ESP_OK;
std::uint16_t new_opconfig = old_opconfig;
if (function == BAT_LOW) {
new_opconfig |= OPCONFIG_BATLOWEN;
} else {
new_opconfig &= ~(OPCONFIG_BATLOWEN);
}
ESP_ERR_TRY(writeOpConfig(new_opconfig));
return ESP_OK;
}
esp_err_t FuelGauge::sociDelta(std::uint8_t* value)
{
if (value == nullptr) return ESP_FAIL;
std::uint8_t soci = 0;
ESP_ERR_TRY(readExtendedData(ID_STATE, 26, &soci));
return ESP_OK;
}
esp_err_t FuelGauge::setSOCIDelta(std::uint8_t delta)
{
std::uint8_t soci = constrain<std::uint8_t>(delta, 0, 100);
return writeExtendedData(ID_STATE, 26, &soci, 1);
}
esp_err_t FuelGauge::pulseGPOUT()
{
return executeControlWord(PULSE_SOC_INT);
}
esp_err_t FuelGauge::sealed(bool* is_sealed)
{
std::uint16_t stat = 0;
ESP_ERR_TRY(status(&stat));
*is_sealed = (stat & STATUS_SS) == STATUS_SS;
return ESP_OK;
}
esp_err_t FuelGauge::seal(std::uint16_t* result)
{
return readControlWord(SEALED, result);
}
esp_err_t FuelGauge::unseal(std::uint16_t* result)
{
// To unseal the FuelGauge, write the key to the control
// command. Then immediately write the same key to control again.
ESP_ERR_TRY(readControlWord(UNSEAL_KEY, result));
return readControlWord(UNSEAL_KEY, result);
}
esp_err_t FuelGauge::enterConfig(bool manual_config)
{
bool is_sealed = false;
ESP_ERR_TRY(sealed(&is_sealed));
if (is_sealed) {
// Must be unsealed before making changes
std::uint16_t res = 0;
ESP_ERR_TRY(unseal(&res));
m_seal_again = true;
// Wait 2 s to get values updated.
vTaskDelay(pdMS_TO_TICKS(2000));
}
ESP_ERR_TRY(executeControlWord(SET_CFG_UPDATE));
std::int16_t timeout = I2C_TIMEOUT;
while (timeout != 0) {
vTaskDelay(1);
std::uint16_t flags_res = 0;
ESP_ERR_TRY(flags(&flags_res));
if (flags_res & FLAG_CFGUPMODE) {
// We are now in CFG update mode.
m_manual_config = manual_config;
return ESP_OK;
}
timeout -= 1;
}
// Timeout reached, no change in mode.
return ESP_FAIL;
}
esp_err_t FuelGauge::exitConfig(bool resim)
{
// There are two methods for exiting config mode:
// 1. Execute the EXIT_CFGUPDATE command
// 2. Execute the SOFT_RESET command
// EXIT_CFGUPDATE exits config mode _without_ an OCV (open-circuit voltage)
// measurement, and without resimulating to update unfiltered-SoC and SoC.
// If a new OCV measurement or resimulation is desired, SOFT_RESET or
// EXIT_RESIM should be used to exit config mode.
esp_err_t err = ESP_FAIL;
if (resim) {
err = softReset();
if (err != ESP_OK) return err;
std::int16_t timeout = I2C_TIMEOUT;
while (timeout != 0) {
vTaskDelay(1);
std::uint16_t flags_res = 0;
err = flags(&flags_res);
if (err != ESP_OK) return err;
if (!(flags_res & FLAG_CFGUPMODE)) {
if (m_seal_again) {
// Seal back up if we IC was sealed coming in
std::uint16_t seal_res = 0;
err = seal(&seal_res);
if (err != ESP_OK) return err;
// Wait 2 s to get values updated.
vTaskDelay(pdMS_TO_TICKS(2000));
}
m_manual_config = false;
return ESP_OK;
}
timeout -= 1;
}
} else {
m_manual_config = false;
err = executeControlWord(EXIT_CFGUPDATE);
if (err != ESP_OK) return err;
return ESP_OK;
}
return ESP_FAIL;
}
esp_err_t FuelGauge::opConfig(std::uint16_t* result)
{
return readWord(EXTENDED_OPCONFIG, result);
}
esp_err_t FuelGauge::writeOpConfig(std::uint16_t value)
{
std::uint8_t op_config_msb = value >> 8;
std::uint8_t op_config_lsb = value & 0x00FF;
std::uint8_t op_config_data[2] = {op_config_msb, op_config_lsb};
// OpConfig register location: ID_REGISTERS id, offset 0
return writeExtendedData(ID_REGISTERS, 0, op_config_data, 2);
}
esp_err_t FuelGauge::blockDataControl()
{
std::uint8_t enable_byte = 0x00;
return i2cWriteBytes(EXTENDED_CONTROL, &enable_byte, 1);
}
esp_err_t FuelGauge::blockDataClass(std::uint8_t id)
{
return i2cWriteBytes(EXTENDED_DATACLASS, &id, 1);
}
esp_err_t FuelGauge::blockDataOffset(std::uint8_t offset)
{
return i2cWriteBytes(EXTENDED_DATABLOCK, &offset, 1);
}
esp_err_t FuelGauge::blockDataChecksum(std::uint8_t* csum)
{
if (csum == nullptr) return ESP_FAIL;
std::uint8_t new_csum = 0;
ESP_ERR_TRY(i2cReadBytes(EXTENDED_CHECKSUM, &new_csum, 1));
*csum = new_csum;
return ESP_OK;
}
esp_err_t FuelGauge::readBlockData(std::uint8_t offset, std::uint8_t* result)
{
if (result == nullptr) return ESP_FAIL;
std::uint8_t ret = 0;
std::uint8_t address = EXTENDED_BLOCKDATA + offset;
ESP_ERR_TRY(i2cReadBytes(address, &ret, 1));
*result = ret;
return ESP_OK;
}
esp_err_t FuelGauge::writeBlockData(std::uint8_t offset, std::uint8_t data)
{
std::uint8_t address = EXTENDED_BLOCKDATA + offset;
return i2cWriteBytes(address, &data, 1);
}
esp_err_t FuelGauge::computeBlockChecksum(std::uint8_t* checksum)
{
if (checksum == nullptr) return ESP_FAIL;
std::uint8_t data[32] = {0};
ESP_ERR_TRY(i2cReadBytes(EXTENDED_BLOCKDATA, data, 32));
std::uint8_t ret = 0;
for (int i = 0; i < 32; i++) {
ret += data[i];
}
*checksum = 255 - ret;
return ESP_OK;
}
esp_err_t FuelGauge::writeBlockChecksum(std::uint8_t csum)
{
return i2cWriteBytes(EXTENDED_CHECKSUM, &csum, 1);
}
esp_err_t FuelGauge::readExtendedData(std::uint8_t class_id,
std::uint8_t offset,
std::uint8_t* result)
{
if (!m_manual_config) ESP_ERR_TRY(enterConfig(false));
// Enable block data memory control
ESP_ERR_TRY(blockDataControl());
// Write class ID using DataBlockClass()
ESP_ERR_TRY(blockDataClass(class_id));
// Write 32-bit block offset (usually 0)
ESP_ERR_TRY(blockDataOffset(offset / 32));
std::uint8_t new_csum = 0;
// Compute checksum going in
ESP_ERR_TRY(computeBlockChecksum(&new_csum));
std::uint8_t old_csum = 0;
ESP_ERR_TRY(blockDataChecksum(&old_csum));
// Read from offset (limit to 0-31)
ESP_ERR_TRY(readBlockData(offset % 32, result));
if (!m_manual_config) ESP_ERR_TRY(exitConfig(false));
return ESP_OK;
}
esp_err_t FuelGauge::writeExtendedData(std::uint8_t class_id,
std::uint8_t offset,
std::uint8_t* data,
std::uint8_t len)
{
if (len == 0) return ESP_FAIL;
if (len > 32) {
ESP_LOGE(TAG, "max write length is 32");
return ESP_FAIL;
}
if (data == nullptr) return ESP_FAIL;
if (!m_manual_config) ESP_ERR_TRY(enterConfig(false));
// Enable block data memory control
ESP_ERR_TRY(blockDataControl());
// Write class ID using DataBlockClass()
ESP_ERR_TRY(blockDataClass(class_id));
// Write 32-bit block offset (usually 0)
ESP_ERR_TRY(blockDataOffset(offset / 32));
vTaskDelay(pdMS_TO_TICKS(2000));
// Compute checksum going in
std::uint8_t computed_csum = 0;
ESP_ERR_TRY(computeBlockChecksum(&computed_csum));
std::uint8_t old_csum = 0;
ESP_ERR_TRY(blockDataChecksum(&old_csum));
// Write data bytes:
for (int i = 0; i < len; i++) {
// Write to offset, mod 32 if offset is greater than 32
// The blockDataOffset above sets the 32-bit block
ESP_ERR_TRY(writeBlockData((offset % 32) + i, data[i]));
}
vTaskDelay(pdMS_TO_TICKS(2000));
// Write new checksum using BlockDataChecksum (0x60)
std::uint8_t new_csum = 0;
ESP_ERR_TRY(computeBlockChecksum(&new_csum));
ESP_ERR_TRY(writeBlockChecksum(new_csum));
vTaskDelay(pdMS_TO_TICKS(2000));
if (!m_manual_config) ESP_ERR_TRY(exitConfig(false));
return ESP_OK;
}
esp_err_t FuelGauge::softReset()
{
return executeControlWord(SOFT_RESET);
}
esp_err_t FuelGauge::readControlWord(std::uint16_t function,
std::uint16_t* result)
{
if (result == nullptr) return ESP_FAIL;
std::uint8_t sub_command_lsb = static_cast<std::uint8_t>(function & 0x00FF);
std::uint8_t sub_command_msb = static_cast<std::uint8_t>(function >> 8);
std::uint8_t command[2] = {sub_command_lsb, sub_command_msb};
ESP_ERR_TRY(i2cWriteBytes(0, command, 2));
std::uint8_t data[2] = {0};
ESP_ERR_TRY(i2cReadBytes(0, data, 2));
std::uint16_t d0 = static_cast<std::uint16_t>(data[1]) << 8;
std::uint16_t d1 = static_cast<std::uint16_t>(data[0]);
*result = (d1 | d0);
return ESP_OK;
}
esp_err_t FuelGauge::executeControlWord(std::uint16_t function)
{
std::uint8_t sub_command_msb = (function >> 8);
std::uint8_t sub_command_lsb = (function & 0x00FF);
std::uint8_t command[2] = {sub_command_lsb, sub_command_msb};
ESP_ERR_TRY(i2cWriteBytes(0, command, 2));
return ESP_OK;
}
esp_err_t FuelGauge::readWord(std::uint8_t command, std::uint16_t* word)
{
if (word == nullptr) return ESP_FAIL;
std::uint8_t data[2] = {0};
ESP_ERR_TRY(i2cReadBytes(command, data, 2));
std::uint16_t d0 = static_cast<std::uint16_t>(data[1]) << 8;
std::uint16_t d1 = static_cast<std::uint16_t>(data[0]);
*word = (d1 | d0);
return ESP_OK;
}
esp_err_t FuelGauge::i2cWriteBytes(std::uint8_t sub_address,
std::uint8_t* bytes,
std::size_t count)
{
if (bytes == nullptr) return ESP_FAIL;
if (count == 0) return ESP_FAIL;
ESP_ERR_TRY(i2cInit());
ESP_ERR_TRY(i2c_set_timeout(I2C_PORT, 14000));
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd,
(FUELGAUGE_ADDRESS << 1) | I2C_MASTER_WRITE,
I2C_MASTER_ACK);
i2c_master_write_byte(cmd, sub_address, I2C_MASTER_ACK);
i2c_master_write(cmd, bytes, count, I2C_MASTER_ACK);
i2c_master_stop(cmd);
ESP_ERR_TRY(i2c_master_cmd_begin(I2C_PORT, cmd, I2C_TIMEOUT));
i2c_cmd_link_delete(cmd);
ESP_ERR_TRY(i2cDelete());
return ESP_OK;
}
esp_err_t FuelGauge::i2cReadBytes(std::uint8_t sub_address,
std::uint8_t* bytes,
std::size_t count)
{
if (bytes == nullptr) return ESP_FAIL;
if (count == 0) return ESP_FAIL;
ESP_ERR_TRY(i2cInit());
ESP_ERR_TRY(i2c_set_timeout(I2C_PORT, 14000));
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd,
(FUELGAUGE_ADDRESS << 1) | I2C_MASTER_WRITE,
I2C_MASTER_ACK);
i2c_master_write_byte(cmd, sub_address, I2C_MASTER_ACK);
i2c_master_start(cmd);
i2c_master_write_byte(cmd,
(FUELGAUGE_ADDRESS << 1) | I2C_MASTER_READ,
I2C_MASTER_ACK);
i2c_master_read(cmd, bytes, count, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
ESP_ERR_TRY(i2c_master_cmd_begin(I2C_PORT, cmd, I2C_TIMEOUT));
i2c_cmd_link_delete(cmd);
ESP_ERR_TRY(i2cDelete());
return ESP_OK;
}
esp_err_t FuelGauge::i2cInit()
{
ESP_ERR_TRY(i2c_param_config(I2C_PORT, &m_conf));
ESP_ERR_TRY(i2c_driver_install(I2C_PORT, m_conf.mode, 0, 0, 0));
return ESP_OK;
}
esp_err_t FuelGauge::i2cDelete()
{
ESP_ERR_TRY(i2c_driver_delete(I2C_PORT));
return ESP_OK;
}
} // namespace esc