From patchwork Thu Feb 23 17:38:11 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Enric Balletbo i Serra X-Patchwork-Id: 9588729 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id BEE3860578 for ; Thu, 23 Feb 2017 17:40:43 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id B2A6C28527 for ; Thu, 23 Feb 2017 17:40:43 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id A798F286FD; Thu, 23 Feb 2017 17:40:43 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-6.9 required=2.0 tests=BAYES_00, RCVD_IN_DNSWL_HI, UNPARSEABLE_RELAY autolearn=unavailable version=3.3.1 Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id E57C928527 for ; Thu, 23 Feb 2017 17:40:42 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1751237AbdBWRk2 (ORCPT ); Thu, 23 Feb 2017 12:40:28 -0500 Received: from bhuna.collabora.co.uk ([46.235.227.227]:56253 "EHLO bhuna.collabora.co.uk" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1751169AbdBWRkN (ORCPT ); Thu, 23 Feb 2017 12:40:13 -0500 Received: from [127.0.0.1] (localhost [127.0.0.1]) (Authenticated sender: eballetbo) with ESMTPSA id 278C626064D From: Enric Balletbo i Serra To: Sebastian Reichel Cc: linux-kernel@vger.kernel.org, linux-pm@vger.kernel.org Subject: [PATCH 1/2] power: Add support for the Toby Churchill SBS battery monitor driver. Date: Thu, 23 Feb 2017 18:38:11 +0100 Message-Id: <20170223173812.10740-1-enric.balletbo@collabora.com> X-Mailer: git-send-email 2.9.3 MIME-Version: 1.0 Sender: linux-pm-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-pm@vger.kernel.org X-Virus-Scanned: ClamAV using ClamSMTP Some of the Toby Churchill devices come with a smart battery and one AVR XMEGA Microcontroller monitor its state. This patch adds the driver for this battery monitor. Signed-off-by: Enric Balletbo i Serra --- drivers/power/supply/Kconfig | 10 + drivers/power/supply/Makefile | 2 + drivers/power/supply/xmega16d4_battery.c | 353 +++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 drivers/power/supply/xmega16d4_battery.c diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index 76806a0..f95f8d5 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -511,4 +511,14 @@ config AXP20X_POWER This driver provides support for the power supply features of AXP20x PMIC. +config BATTERY_XMEGA16D4 + tristate "XMEGA16D4 Battery Gauge Driver" + depends on SPI + help + Say Y here to include support for XMEGA16D4 Battery Gauge. The + driver reports the charge count, and measures the voltage and + the current. + + This adds support for XMEGA16D4 battery. + endif # POWER_SUPPLY diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index 36c599d..abae887 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -72,3 +72,5 @@ obj-$(CONFIG_CHARGER_TPS65090) += tps65090-charger.o obj-$(CONFIG_CHARGER_TPS65217) += tps65217_charger.o obj-$(CONFIG_AXP288_FUEL_GAUGE) += axp288_fuel_gauge.o obj-$(CONFIG_AXP288_CHARGER) += axp288_charger.o +obj-$(CONFIG_BATTERY_XMEGA16D4) += xmega16d4_battery.o + diff --git a/drivers/power/supply/xmega16d4_battery.c b/drivers/power/supply/xmega16d4_battery.c new file mode 100644 index 0000000..8c6b11a --- /dev/null +++ b/drivers/power/supply/xmega16d4_battery.c @@ -0,0 +1,353 @@ +/* + * Battery monitor driver for SL50 Toby Churchill SBS Batteries + * + * Copyright (c) 2017, Collabora Ltd. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include + +#define SBS_MEMORY_MAP_SIZE 128 +/* Charging voltage, 2 bytes */ +#define SBS_CHARGING_VOLTAGE 0x0a +/* Design voltage, 2 bytes */ +#define SBS_DESIGN_VOLTAGE 0x0c +/* Fast charging current, 2 bytes */ +#define SBS_FAST_CHARGING_CURRENT 0x0e +/* Max T, Low T, 2 bytes */ +#define SBS_MAX_LOW_TEMPERATURE 0x10 +/* Pack capacity, 2 bytes */ +#define SBS_PACK_CAPACITY 0x12 +/* Serial number, 2 bytes */ +#define SBS_SERIAL_NUMBER 0x18 +/* Manufacturer name, 16 bytes */ +#define SBS_MANUFACTURER_NAME 0x20 +/* Model name, 16 bytes */ +#define SBS_MODEL_NAME 0x30 +/* Device chemistry, 5 bytes */ +#define SBS_DEVICE_CHEMISTRY 0x40 +/* Cycle count, 2 bytes */ +#define SBS_CYCLE_COUNT 0x50 +/* Voltage now, 2 bytes */ +#define SBS_VOLTAGE_NOW 0x70 +/* Current now, 2 bytes */ +#define SBS_CURRENT_NOW 0x72 +/* Battery Status, 2 bytes */ +#define SBS_BATTERY_STATUS 0x74 +# define BATTERY_STATUS_CHARGING 0 +# define BATTERY_STATUS_DISCHARGING BIT(6) +# define BATTERY_STATUS_FULLY_CHARGED BIT(5) +/* State of charge in percentage, 1 byte */ +#define SBS_STATE_OF_CHARGE 0x76 + +/* MM SIZE + START(u16) + CHECKSUM(u16) */ +#define SPI_MSG_LENGTH (SBS_MEMORY_MAP_SIZE + 4) +#define SPI_MSG_DATA_BP 2 +/* MSB checksum byte position */ +#define SPI_MSG_CSUM_BP (2 + SBS_MEMORY_MAP_SIZE) +#define SPI_MSG_START_TOKEN 0xb00b + +struct xmega16d4_battery_data { + struct spi_device *spi; + struct power_supply *bat; + + struct mutex work_lock; /* protect work data */ + struct delayed_work bat_work; + + u8 map[SBS_MEMORY_MAP_SIZE]; + + char model_name[16]; + char manufacturer_name[16]; + char serial_number[5]; + + int technology; + int voltage_uV; /* units of uV */ + int current_uA; /* units of uA */ + int rated_capacity; /* units of µAh */ + int cycle_count; + int rem_capacity; /* percentage */ + int life_sec; /* units of seconds */ + int status; /* state of charge */ +}; + +#define MAX_KEYLENGTH 256 +struct battery_property_map { + int value; + char const *key; +}; + +static struct battery_property_map map_technology[] = { + { POWER_SUPPLY_TECHNOLOGY_NiMH, "NiMH" }, + { POWER_SUPPLY_TECHNOLOGY_LION, "LION" }, + { POWER_SUPPLY_TECHNOLOGY_LIPO, "LIPO" }, + { POWER_SUPPLY_TECHNOLOGY_LiFe, "LiFe" }, + { POWER_SUPPLY_TECHNOLOGY_NiCd, "NiCd" }, + { POWER_SUPPLY_TECHNOLOGY_LiMn, "LiMn" }, + { -1, NULL }, +}; + +static int map_get_value(struct battery_property_map *map, const char *key, + int def_val) +{ + char buf[MAX_KEYLENGTH]; + int cr; + + strncpy(buf, key, MAX_KEYLENGTH); + buf[MAX_KEYLENGTH - 1] = '\0'; + + cr = strnlen(buf, MAX_KEYLENGTH) - 1; + if (buf[cr] == '\n') + buf[cr] = '\0'; + + while (map->key) { + if (strncasecmp(map->key, buf, MAX_KEYLENGTH) == 0) + return map->value; + map++; + } + + return def_val; +} + +static int xmega16d4_battery_read_status(struct xmega16d4_battery_data *data) +{ + int i; + int csum = 0; + u8 buf[SBS_MEMORY_MAP_SIZE], technology[5]; + unsigned int uval; + int sval; + struct spi_device *spi = data->spi; + + for (i = 0; i < SBS_MEMORY_MAP_SIZE; i++) { + spi_write(spi, &i, 1); + spi_read(spi, &buf[i], 1); + } + + print_hex_dump(KERN_DEBUG, ": ", DUMP_PREFIX_OFFSET, 16, 1, + buf, SBS_MEMORY_MAP_SIZE, false); + + /* Calculate the data checksum */ + for (i = 0; i < SBS_MEMORY_MAP_SIZE - 2; i++) + csum += buf[i]; + + /* Verify the checksum */ + uval = (buf[SBS_MEMORY_MAP_SIZE - 2] << 8) | + buf[SBS_MEMORY_MAP_SIZE - 1]; + if (csum != uval) { + dev_dbg(&spi->dev, + "message received with invalid checksum (%d != %d)\n", + csum, uval); + return -ENOMSG; + } + + /* Update memory map with the new data */ + memcpy(data->map, buf, SBS_MEMORY_MAP_SIZE); + + strncpy(data->model_name, &data->map[SBS_MODEL_NAME], 16); + + strncpy(data->manufacturer_name, &data->map[SBS_MANUFACTURER_NAME], + 16); + + strncpy(technology, &data->map[SBS_DEVICE_CHEMISTRY], 5); + + uval = (u16)((data->map[SBS_SERIAL_NUMBER + 1] << 8) | + data->map[SBS_SERIAL_NUMBER]); + snprintf(data->serial_number, ARRAY_SIZE(data->serial_number), "%04d", + uval); + + data->technology = map_get_value(map_technology, technology, + POWER_SUPPLY_TECHNOLOGY_UNKNOWN); + + data->voltage_uV = (u16)((data->map[SBS_VOLTAGE_NOW + 1] << 8) | + data->map[SBS_VOLTAGE_NOW]); + data->voltage_uV *= 1000; /* convert from mV to uV */ + + sval = (s16)((data->map[SBS_CURRENT_NOW + 1] << 8) | + data->map[SBS_CURRENT_NOW]); + data->current_uA = sval; + data->current_uA *= 1000; /* convert from mA to uA */ + + data->rated_capacity = (u16)((data->map[SBS_PACK_CAPACITY + 1] << 8) | + data->map[SBS_PACK_CAPACITY]); + data->rated_capacity *= 1000; /* convert from mAh to uAh */ + + uval = (u16)((data->map[SBS_BATTERY_STATUS + 1] << 8) | + data->map[SBS_BATTERY_STATUS]); + if (uval == BATTERY_STATUS_CHARGING) + data->status = POWER_SUPPLY_STATUS_CHARGING; + else if (uval == BATTERY_STATUS_DISCHARGING) + data->status = POWER_SUPPLY_STATUS_DISCHARGING; + else if (uval == BATTERY_STATUS_FULLY_CHARGED) + data->status = POWER_SUPPLY_STATUS_FULL; + else + data->status = POWER_SUPPLY_STATUS_UNKNOWN; + + data->cycle_count = (u16)((data->map[SBS_CYCLE_COUNT + 1] << 8) | + data->map[SBS_CYCLE_COUNT]); + + data->rem_capacity = data->map[SBS_STATE_OF_CHARGE]; + + uval = (data->rem_capacity * (data->rated_capacity / 1000)) / 100; + if (data->current_uA) + data->life_sec = (3600l * uval) / (data->current_uA / 1000); + + return 0; +} + +static void xmega16d4_battery_work(struct work_struct *work) +{ + struct xmega16d4_battery_data *data = container_of(work, + struct xmega16d4_battery_data, bat_work.work); + + /* Update values */ + mutex_lock(&data->work_lock); + xmega16d4_battery_read_status(data); + mutex_unlock(&data->work_lock); + + schedule_delayed_work(&data->bat_work, HZ * 60); +} + +static int xmega16d4_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct xmega16d4_battery_data *data = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = data->status; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = data->voltage_uV; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = data->current_uA; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = data->rated_capacity; + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + val->intval = data->life_sec; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = data->rem_capacity; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = data->technology; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = data->model_name; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = data->manufacturer_name; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = data->serial_number; + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property xmega16d4_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TECHNOLOGY, + /* Properties of type `const char *' */ + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, +}; + +static const struct power_supply_desc xmega16d4_battery_desc = { + .name = "battery-monitor", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = xmega16d4_battery_props, + .num_properties = ARRAY_SIZE(xmega16d4_battery_props), + .get_property = xmega16d4_battery_get_property, +}; + +static int xmega16d4_battery_probe(struct spi_device *spi) +{ + struct xmega16d4_battery_data *data; + struct power_supply_config psy_cfg = {}; + + data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->spi = spi; + psy_cfg.of_node = spi->dev.of_node; + psy_cfg.drv_data = data; + + mutex_init(&data->work_lock); + + INIT_DELAYED_WORK(&data->bat_work, xmega16d4_battery_work); + + spi_set_drvdata(spi, data); + + /* Get initial status */ + if (xmega16d4_battery_read_status(data)) + return -ENODEV; + + data->bat = devm_power_supply_register(&spi->dev, + &xmega16d4_battery_desc, + &psy_cfg); + if (IS_ERR(data->bat)) + return PTR_ERR(data->bat); + + schedule_delayed_work(&data->bat_work, 0); + + return 0; +} + +static int xmega16d4_battery_remove(struct spi_device *spi) +{ + struct xmega16d4_battery_data *data = spi_get_drvdata(spi); + + cancel_delayed_work_sync(&data->bat_work); + + return 0; +} + +static const struct of_device_id xmega16d4_battery_of_match[] = { + { .compatible = "tcl,xmega16d4-battery", }, + { /* sentinel */ }, +}; + +static struct spi_driver xmega16d4_battery_driver = { + .driver = { + .name = "xmega16d4-battery", + .owner = THIS_MODULE, + .of_match_table = of_match_ptr(xmega16d4_battery_of_match), + }, + .probe = xmega16d4_battery_probe, + .remove = xmega16d4_battery_remove, +}; +module_spi_driver(xmega16d4_battery_driver); + +MODULE_ALIAS("spi:xmega16d4-battery"); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Enric Balletbo Serra "); +MODULE_DESCRIPTION("xmega16d4 battery monitor driver");