/**************************************************************************** * * Copyright (c) 2012-2018 PX4 Development Team. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name PX4 nor the names of its contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * ****************************************************************************/ /** * @file batt_smbus.cpp * * Driver for a battery monitor connected via SMBus (I2C). * Designed for BQ40Z50-R1/R2 * * @author Randy Mackay * @author Alex Klimaj * @author Mark Sauder * @author Jacob Dahl */ #include #include "batt_smbus.h" BATT_SMBUS::BATT_SMBUS(device::Device *interface, const char *path) : CDev(path), _interface(interface), _batt_topic(nullptr), _batt_orb_id(nullptr), _batt_capacity(0), _batt_startup_capacity(0), _cycle_count(0), _serial_number(0), _crit_thr(0.0f), _emergency_thr(0.0f), _low_thr(0.0f), _manufacturer_name(nullptr) { } BATT_SMBUS::~BATT_SMBUS() { // Ensure we are truly inactive. stop(); if (_manufacturer_name != nullptr) { delete[] _manufacturer_name; } PX4_WARN("Smart battery driver stopped"); int battsource = 0; param_set(param_find("BAT_SOURCE"), &battsource); } int BATT_SMBUS::block_read(const uint8_t cmd_code, void *data, const unsigned length) { unsigned byte_count = 0; // Length of data (32max). byte_count(1), cmd_code(2), pec(1) uint8_t rx_data[DATA_BUFFER_SIZE + 4]; // If this is a ManufacturerBlockAccess() command then the first 2 data bytes will be the cmd_code. if (cmd_code == BATT_SMBUS_MANUFACTURER_BLOCK_ACCESS) { _interface->read(cmd_code, rx_data, length + 4); byte_count = rx_data[0]; // addr1, addr2, byte_count, memcpy(data, &rx_data[3], byte_count); } else { // byte_count(1) + data[32max] + pec(1) _interface->read(cmd_code, rx_data, length + 2); byte_count = rx_data[0]; memcpy(data, &rx_data[1], byte_count); } // addr(wr), cmd_code, addr(r), byte_count, rx_data[] uint8_t device_address = _interface->get_device_address(); uint8_t full_data_packet[DATA_BUFFER_SIZE + 4] = {0}; full_data_packet[0] = (device_address << 1) | 0x00; full_data_packet[1] = cmd_code; full_data_packet[2] = (device_address << 1) | 0x01; full_data_packet[3] = byte_count; memcpy(&full_data_packet[4], &rx_data[1], byte_count); uint8_t pec = get_pec(full_data_packet, byte_count + 4); // First byte is byte count, followed by data. if (pec != ((uint8_t *)rx_data)[byte_count + 1]) { PX4_INFO("bad PEC from block_read"); return PX4_ERROR; } else { return PX4_OK; } } int BATT_SMBUS::block_write(const uint8_t cmd_code, void *data, const unsigned byte_count) { // cmd code, byte count, data[byte_count], pec uint8_t buf[byte_count + 2]; buf[0] = cmd_code; buf[1] = (uint8_t)byte_count; memcpy(&buf[2], data, byte_count); uint8_t pec = get_pec(buf, sizeof(buf)); buf[byte_count + 2] = pec; unsigned i = 0; // If block_write fails, try up to 10 times. while (i < 10) { if (PX4_OK != _interface->write(0, buf, sizeof(buf))) { i++; PX4_WARN("block_write failed: %d", i); usleep(100000); } else { return PX4_OK; } } return PX4_ERROR; } void BATT_SMBUS::cycle() { // Get the current time. uint64_t now = hrt_absolute_time(); // Read data from sensor. battery_status_s new_report = {}; // Set time of reading. new_report.timestamp = now; // Don't publish if any reads fail. bool success = true; // Temporary variable for storing SMBUS reads. uint16_t tmp; if (read_word(BATT_SMBUS_VOLTAGE, &tmp) == PX4_OK) { new_report.connected = true; // Convert millivolts to volts. new_report.voltage_v = ((float)tmp) / 1000.0f; new_report.voltage_filtered_v = new_report.voltage_v; // Read current. if (read_word(BATT_SMBUS_CURRENT, &tmp) == PX4_OK) { new_report.current_a = (-1.0f * ((float)(*(int16_t *)&tmp)) / 1000.0f); new_report.current_filtered_a = new_report.current_a; } else { success = false; } // Read average current. if (read_word(BATT_SMBUS_AVERAGE_CURRENT, &tmp) == PX4_OK) { new_report.average_current_a = (-1.0f * ((float)(*(int16_t *)&tmp)) / 1000.0f); } else { success = false; } // Read run time to empty. if (read_word(BATT_SMBUS_RUN_TIME_TO_EMPTY, &tmp) == PX4_OK) { new_report.run_time_to_empty = tmp; } else { success = false; } // Read average time to empty. if (read_word(BATT_SMBUS_AVERAGE_TIME_TO_EMPTY, &tmp) == PX4_OK) { new_report.average_time_to_empty = tmp; } else { success = false; } // Read remaining capacity. if (read_word(BATT_SMBUS_REMAINING_CAPACITY, &tmp) == PX4_OK) { if (tmp > _batt_capacity) { PX4_WARN("Remaining capacity greater than total: Capacity:%hu \tRemaining Capacity:%hu", (uint16_t)_batt_capacity, (uint16_t)tmp); _batt_capacity = (uint16_t)tmp; } // Calculate remaining capacity percent with complementary filter new_report.remaining = ((float)_last_report.remaining * 0.8f) + (0.2f * (1.0f - (((float)_batt_capacity - (float)tmp) / (float)_batt_capacity))); // Calculate total discharged amount. new_report.discharged_mah = (float)_batt_startup_capacity - (float)tmp; // Check if remaining % is out of range. if ((new_report.remaining > 1.00f) || (new_report.remaining <= 0.00f)) { new_report.warning = battery_status_s::BATTERY_WARNING_EMERGENCY; PX4_INFO("Percent out of range: %4.2f", (double)new_report.remaining); } // Check if discharged amount is greater than the starting capacity. else if (new_report.discharged_mah > (float)_batt_startup_capacity) { new_report.warning = battery_status_s::BATTERY_WARNING_EMERGENCY; PX4_INFO("Discharged greater than startup capacity: %4.2f", (double)new_report.discharged_mah); } // Propagate warning state. else { if (new_report.remaining > _low_thr) { new_report.warning = battery_status_s::BATTERY_WARNING_NONE; } else if (new_report.remaining > _crit_thr) { new_report.warning = battery_status_s::BATTERY_WARNING_LOW; } else if (new_report.remaining > _emergency_thr) { new_report.warning = battery_status_s::BATTERY_WARNING_CRITICAL; } else { uint64_t timer = hrt_absolute_time() - now; new_report.warning = battery_status_s::BATTERY_WARNING_EMERGENCY; /* Only warn every 5 seconds */ if (timer > 5000000) { PX4_WARN("Battery Warning Emergency: %4.2f", (double)new_report.remaining); } } } } else { success = false; } // Read battery temperature and covert to Celsius. if (read_word(BATT_SMBUS_TEMP, &tmp) == PX4_OK) { new_report.temperature = (float)(((float)tmp / 10.0f) + CONSTANTS_ABSOLUTE_NULL_CELSIUS); } else { success = false; } new_report.capacity = _batt_capacity; new_report.cycle_count = _cycle_count; new_report.serial_number = _serial_number; // Publish to orb. if (_batt_topic != nullptr && success) { orb_publish(_batt_orb_id, _batt_topic, &new_report); // Copy report for test(). _last_report = new_report; } else { _batt_topic = orb_advertise(_batt_orb_id, &new_report); if (_batt_topic == nullptr) { PX4_ERR("ADVERT FAIL"); return; } } } // Schedule a fresh cycle call when the measurement is done. work_queue(HPWORK, &_work, (worker_t)&BATT_SMBUS::cycle_trampoline, this, USEC2TICK(BATT_SMBUS_MEASUREMENT_INTERVAL_US)); } void BATT_SMBUS::cycle_trampoline(void *arg) { BATT_SMBUS *dev = (BATT_SMBUS *)arg; dev->cycle(); } int BATT_SMBUS::dataflash_read(uint16_t &address, void *data) { uint8_t code = BATT_SMBUS_MANUFACTURER_BLOCK_ACCESS; // address is 2 bytes block_write(code, &address, 2); // @NOTE: The data buffer MUST be 32 bytes. block_read(code, data, DATA_BUFFER_SIZE); // for debug only: print out the receieved buffer for (unsigned i = 0; i < DATA_BUFFER_SIZE ; i++) { PX4_INFO("%d", ((uint8_t *)data)[i]); } return PX4_OK; } int BATT_SMBUS::dataflash_write(uint16_t &address, void *data, const unsigned length) { uint8_t code = BATT_SMBUS_MANUFACTURER_BLOCK_ACCESS; // @NOTE: The data buffer can be 1 - 32 bytes. The address field is 2 bytes. uint8_t tx_buf[DATA_BUFFER_SIZE + 2] = {0}; tx_buf[0] = ((uint8_t *)&address)[0]; tx_buf[1] = ((uint8_t *)&address)[1]; memcpy(&tx_buf[2], data, length); // code (1), byte_count (1), addr(2), data(32) + pec block_write(code, tx_buf, length + 2); return PX4_OK; } uint8_t BATT_SMBUS::get_pec(uint8_t *buff, const uint8_t len) { // Initialise CRC to zero. uint8_t crc = 0; uint8_t shift_register = 0; bool invert_crc; // Calculate crc for each byte in the stream. for (uint8_t i = 0; i < len; i++) { // Load next data byte into the shift register shift_register = buff[i]; // Calculate crc for each bit in the current byte. for (uint8_t j = 0; j < 8; j++) { invert_crc = (crc ^ shift_register) & 0x80; crc <<= 1; shift_register <<= 1; if (invert_crc) { crc ^= BATT_SMBUS_PEC_POLYNOMIAL; } } } return crc; } int BATT_SMBUS::get_startup_info() { int result = PX4_ERROR; const unsigned name_length = 22; // Try and get battery SBS info. if (_manufacturer_name == nullptr) { char man_name[name_length] = {0}; result = manufacturer_name((uint8_t *)man_name, sizeof(man_name)); if (PX4_OK != result) { PX4_WARN("Failed to get manufacturer name"); return PX4_ERROR; } _manufacturer_name = new char[sizeof(man_name)]; strcpy(_manufacturer_name, man_name); } // Temporary variable for storing SMBUS reads. uint16_t tmp = 0; // Read battery serial number on startup. if (read_word(BATT_SMBUS_SERIAL_NUMBER, &tmp) == PX4_OK) { _serial_number = tmp; result = PX4_OK; } // Read battery capacity on startup. if (read_word(BATT_SMBUS_REMAINING_CAPACITY, &tmp) == PX4_OK) { _batt_startup_capacity = tmp; result = PX4_OK; } // Read battery cycle count on startup. if (read_word(BATT_SMBUS_CYCLE_COUNT, &tmp) == PX4_OK) { _cycle_count = tmp; result = PX4_OK; } // Read battery design capacity on startup. if (read_word(BATT_SMBUS_FULL_CHARGE_CAPACITY, &tmp) == PX4_OK) { _batt_capacity = tmp; result = PX4_OK; } // Read battery threshold params on startup. param_get(param_find("BAT_CRIT_THR"), &_crit_thr); param_get(param_find("BAT_LOW_THR"), &_low_thr); param_get(param_find("BAT_EMERGEN_THR"), &_emergency_thr); return result; } uint16_t BATT_SMBUS::get_serial_number() { uint16_t serial_num = 0; if (read_word(BATT_SMBUS_SERIAL_NUMBER, &serial_num) == PX4_OK) { return serial_num; } return PX4_ERROR; } int BATT_SMBUS::info() { print_message(_last_report); return PX4_OK; } int BATT_SMBUS::init() { if (PX4_OK != CDev::init()) { PX4_ERR("CDev init failed"); return PX4_ERROR; } // Find the battery on the bus and read startup info. if (search_addresses() != PX4_OK) { PX4_ERR("Failed to init I2C"); return PX4_ERROR; } // Retry up to 10 times to read startup info. for (unsigned i = 0; i < 10; i++) { if (PX4_OK == get_startup_info()) { break; } if (i == 9) { PX4_ERR("Failed to get battery startup info"); return PX4_ERROR; } } // Initialize the orb ID. _batt_orb_id = ORB_ID(battery_status); // Start the work queue. start(); return PX4_OK; } int BATT_SMBUS::manufacture_date(void *man_date) { uint8_t code = BATT_SMBUS_MANUFACTURE_DATE; int result = read_word(code, man_date); if (PX4_OK != result) { PX4_WARN("Could not read manufacturer name"); return PX4_ERROR; } return PX4_OK; } int BATT_SMBUS::manufacturer_name(uint8_t *man_name, const uint8_t length) { uint8_t code = BATT_SMBUS_MANUFACTURER_NAME; uint8_t rx_buf[21] = {0}; // Returns 21 bytes, add 1 byte for null terminator. int result = block_read(code, rx_buf, length - 1); memcpy(man_name, rx_buf, sizeof(rx_buf)); man_name[21] = '\0'; if (PX4_OK != result) { PX4_WARN("Could not read manufacturer name"); return PX4_ERROR; } return PX4_OK; } int BATT_SMBUS::manufacturer_read(const uint16_t cmd_code, void *data, const unsigned length) { uint8_t code = BATT_SMBUS_MANUFACTURER_BLOCK_ACCESS; uint8_t address[2] = {0}; address[0] = ((uint8_t *)&cmd_code)[0]; address[1] = ((uint8_t *)&cmd_code)[1]; block_write(code, address, sizeof(address)); // returns the 2 bytes of addr info + data[] block_read(code, data, length); uint8_t rx_buf[DATA_BUFFER_SIZE] = {0}; memcpy(rx_buf, data, DATA_BUFFER_SIZE); PX4_INFO("Received data: %d %d %d %d %d %d %d %d %d %d %d %d", rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3], rx_buf[4], rx_buf[5], rx_buf[6], rx_buf[7], rx_buf[8], rx_buf[9], rx_buf[10], rx_buf[11]); return PX4_OK; } int BATT_SMBUS::manufacturer_write(const uint16_t cmd_code, void *data, const unsigned length) { uint8_t code = BATT_SMBUS_MANUFACTURER_BLOCK_ACCESS; uint8_t address[2] = {0}; address[0] = ((uint8_t *)&cmd_code)[0]; address[1] = ((uint8_t *)&cmd_code)[1]; uint8_t tx_buf[DATA_BUFFER_SIZE + 2] = {0}; memcpy(tx_buf, address, 2); memcpy(&tx_buf[2], data, length); block_write(code, tx_buf, length + 2); return PX4_OK; } int BATT_SMBUS::read_word(const uint8_t cmd_code, void *data) { // 2 data bytes + pec byte int result = _interface->read(cmd_code, data, 3); if (PX4_OK == result) { // Check PEC. uint8_t addr = (_interface->get_device_address() << 1); uint8_t full_data_packet[5]; full_data_packet[0] = addr | 0x00; full_data_packet[1] = cmd_code; full_data_packet[2] = addr | 0x01; memcpy(&full_data_packet[3], data, 2); uint8_t pec = get_pec(full_data_packet, sizeof(full_data_packet)); if (pec == ((uint8_t *)data)[2]) { return PX4_OK; } else { return PX4_ERROR; } } return PX4_ERROR; } int BATT_SMBUS::search_addresses() { uint16_t tmp; _interface->set_device_address(BATT_SMBUS_ADDR); if (PX4_OK != read_word(BATT_SMBUS_VOLTAGE, &tmp)) { // Search through all valid SMBus addresses. for (uint8_t i = BATT_SMBUS_ADDR_MIN; i < BATT_SMBUS_ADDR_MAX; i++) { _interface->set_device_address(i); if (read_word(BATT_SMBUS_VOLTAGE, &tmp) == PX4_OK) { if (tmp > 0) { break; } } if (i == BATT_SMBUS_ADDR_MAX - 1) { PX4_WARN("No smart batteries found."); return PX4_ERROR; } } } PX4_INFO("Smart battery found at 0x%x", _interface->get_device_address()); PX4_INFO("Smart battery connected"); return PX4_OK; } void BATT_SMBUS::start() { // Schedule a cycle to start things. work_queue(HPWORK, &_work, (worker_t)&BATT_SMBUS::cycle_trampoline, this, 1); } void BATT_SMBUS::stop() { // Cancel the work queue. work_cancel(HPWORK, &_work); } int BATT_SMBUS::unseal() { // See pg85 of bq40z50 technical reference. uint16_t keys[2] = {0x0414, 0x3672}; if (PX4_OK != manufacturer_write(keys[0], &keys[1], 2)) { PX4_INFO("Failed to unseal device."); return PX4_ERROR; } return PX4_OK; }