| /* |
| * smb347_charger.c |
| * Samsung SMB347 Charger Driver |
| * |
| * Copyright (C) 2012 Samsung Electronics |
| * |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| */ |
| #define DEBUG |
| |
| #include <linux/battery/sec_charger.h> |
| static int smb347_i2c_write(struct i2c_client *client, |
| int reg, u8 *buf) |
| { |
| int ret; |
| ret = i2c_smbus_write_i2c_block_data(client, reg, 1, buf); |
| if (ret < 0) |
| dev_err(&client->dev, "%s: Error(%d)\n", __func__, ret); |
| return ret; |
| } |
| |
| static int smb347_i2c_read(struct i2c_client *client, |
| int reg, u8 *buf) |
| { |
| int ret; |
| ret = i2c_smbus_read_i2c_block_data(client, reg, 1, buf); |
| if (ret < 0) |
| dev_err(&client->dev, "%s: Error(%d)\n", __func__, ret); |
| return ret; |
| } |
| |
| static void smb347_i2c_write_array(struct i2c_client *client, |
| u8 *buf, int size) |
| { |
| int i; |
| for (i = 0; i < size; i += 3) |
| smb347_i2c_write(client, (u8) (*(buf + i)), (buf + i) + 1); |
| } |
| |
| static void smb347_set_command(struct i2c_client *client, |
| int reg, int datum) |
| { |
| int val; |
| u8 data = 0; |
| val = smb347_i2c_read(client, reg, &data); |
| if (val >= 0) { |
| dev_dbg(&client->dev, "%s : reg(0x%02x): 0x%02x", |
| __func__, reg, data); |
| if (data != datum) { |
| data = datum; |
| if (smb347_i2c_write(client, reg, &data) < 0) |
| dev_err(&client->dev, |
| "%s : error!\n", __func__); |
| val = smb347_i2c_read(client, reg, &data); |
| if (val >= 0) |
| dev_dbg(&client->dev, " => 0x%02x\n", data); |
| } |
| } |
| } |
| |
| static void smb347_test_read(struct i2c_client *client) |
| { |
| u8 data = 0; |
| u32 addr = 0; |
| for (addr = 0; addr <= 0x0f; addr++) { |
| smb347_i2c_read(client, addr, &data); |
| dev_dbg(&client->dev, |
| "smb347 addr : 0x%02x data : 0x%02x\n", addr, data); |
| } |
| for (addr = 0x30; addr <= 0x3f; addr++) { |
| smb347_i2c_read(client, addr, &data); |
| dev_dbg(&client->dev, |
| "smb347 addr : 0x%02x data : 0x%02x\n", addr, data); |
| } |
| } |
| |
| static void smb347_read_regs(struct i2c_client *client, char *str) |
| { |
| u8 data = 0; |
| u32 addr = 0; |
| |
| for (addr = 0; addr <= 0x0f; addr++) { |
| smb347_i2c_read(client, addr, &data); |
| sprintf(str+strlen(str), "0x%x, ", data); |
| } |
| |
| /* "#" considered as new line in application */ |
| sprintf(str+strlen(str), "#"); |
| |
| for (addr = 0x30; addr <= 0x3f; addr++) { |
| smb347_i2c_read(client, addr, &data); |
| sprintf(str+strlen(str), "0x%x, ", data); |
| } |
| } |
| |
| static int smb347_read_reg(struct i2c_client *client, int reg) |
| { |
| int ret; |
| |
| ret = i2c_smbus_read_byte_data(client, reg); |
| |
| if (ret < 0) { |
| pr_err("%s: err %d, try again!\n", __func__, ret); |
| ret = i2c_smbus_read_byte_data(client, reg); |
| if (ret < 0) |
| pr_err("%s: err %d\n", __func__, ret); |
| } |
| |
| return ret; |
| } |
| |
| static int smb347_get_charging_status(struct i2c_client *client) |
| { |
| int status = POWER_SUPPLY_STATUS_UNKNOWN; |
| u8 data_a = 0; |
| u8 data_b = 0; |
| u8 data_c = 0; |
| u8 data_d = 0; |
| u8 data_e = 0; |
| |
| smb347_i2c_read(client, SMB347_STATUS_A, &data_a); |
| dev_info(&client->dev, |
| "%s : charger status A(0x%02x)\n", __func__, data_a); |
| smb347_i2c_read(client, SMB347_STATUS_B, &data_b); |
| dev_info(&client->dev, |
| "%s : charger status B(0x%02x)\n", __func__, data_b); |
| smb347_i2c_read(client, SMB347_STATUS_C, &data_c); |
| dev_info(&client->dev, |
| "%s : charger status C(0x%02x)\n", __func__, data_c); |
| smb347_i2c_read(client, SMB347_STATUS_D, &data_d); |
| dev_info(&client->dev, |
| "%s : charger status D(0x%02x)\n", __func__, data_d); |
| smb347_i2c_read(client, SMB347_STATUS_E, &data_e); |
| dev_info(&client->dev, |
| "%s : charger status E(0x%02x)\n", __func__, data_e); |
| |
| /* At least one charge cycle terminated, |
| * Charge current < Termination Current |
| */ |
| if ((data_c & 0x20) == 0x20) { |
| /* top-off by full charging */ |
| status = POWER_SUPPLY_STATUS_FULL; |
| goto charging_status_end; |
| } |
| |
| /* Is enabled ? */ |
| if (data_c & 0x01) { |
| /* check for 0x06 : no charging (0b00) */ |
| /* not charging */ |
| if (!(data_c & 0x06)) { |
| status = POWER_SUPPLY_STATUS_NOT_CHARGING; |
| goto charging_status_end; |
| } else { |
| status = POWER_SUPPLY_STATUS_CHARGING; |
| goto charging_status_end; |
| } |
| } else |
| status = POWER_SUPPLY_STATUS_DISCHARGING; |
| charging_status_end: |
| return (int)status; |
| } |
| |
| static int smb347_get_charging_health(struct i2c_client *client) |
| { |
| int health = POWER_SUPPLY_HEALTH_GOOD; |
| u8 data_a = 0; |
| u8 data_b = 0; |
| u8 data_c = 0; |
| u8 data_d = 0; |
| u8 data_e = 0; |
| |
| smb347_i2c_read(client, SMB347_STATUS_A, &data_a); |
| dev_info(&client->dev, |
| "%s : charger status A(0x%02x)\n", __func__, data_a); |
| smb347_i2c_read(client, SMB347_STATUS_B, &data_b); |
| dev_info(&client->dev, |
| "%s : charger status B(0x%02x)\n", __func__, data_b); |
| smb347_i2c_read(client, SMB347_STATUS_C, &data_c); |
| dev_info(&client->dev, |
| "%s : charger status C(0x%02x)\n", __func__, data_c); |
| smb347_i2c_read(client, SMB347_STATUS_D, &data_d); |
| dev_info(&client->dev, |
| "%s : charger status D(0x%02x)\n", __func__, data_d); |
| smb347_i2c_read(client, SMB347_STATUS_E, &data_e); |
| dev_info(&client->dev, |
| "%s : charger status E(0x%02x)\n", __func__, data_e); |
| |
| /* Is enabled ? */ |
| if (data_c & 0x01) { |
| if (!(data_a & 0x02)) /* Input current is NOT OK */ |
| health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; |
| } |
| return (int)health; |
| } |
| |
| static void smb347_allow_volatile_writes(struct i2c_client *client) |
| { |
| int val, reg; |
| u8 data; |
| reg = SMB347_COMMAND_A; |
| val = smb347_i2c_read(client, reg, &data); |
| if ((val >= 0) && !(data & 0x80)) { |
| dev_dbg(&client->dev, |
| "%s : reg(0x%02x): 0x%02x", __func__, reg, data); |
| data |= (0x1 << 7); |
| if (smb347_i2c_write(client, reg, &data) < 0) |
| dev_err(&client->dev, "%s : error!\n", __func__); |
| val = smb347_i2c_read(client, reg, &data); |
| if (val >= 0) { |
| data = (u8) data; |
| dev_dbg(&client->dev, " => 0x%02x\n", data); |
| } |
| } |
| } |
| |
| static u8 smb347_get_float_voltage_data( |
| int float_voltage) |
| { |
| u8 data; |
| |
| if (float_voltage < 3500) |
| float_voltage = 3500; |
| |
| data = (float_voltage - 3500) / 20; |
| |
| return data; |
| } |
| |
| static u8 smb347_get_input_current_limit_data( |
| struct sec_charger_info *charger, int input_current) |
| { |
| u8 data; |
| |
| if (input_current <= 300) |
| data = 0x0; |
| else if (input_current <= 500) |
| data = 0x1; |
| else if (input_current <= 700) |
| data = 0x2; |
| else if (input_current <= 900) |
| data = 0x3; |
| else if (input_current <= 1200) |
| data = 0x4; |
| else if (input_current <= 1500) |
| data = 0x5; |
| else if (input_current <= 1800) |
| data = 0x6; |
| else if (input_current <= 2000) |
| data = 0x7; |
| else if (input_current <= 2200) |
| data = 0x8; |
| else if (input_current <= 2500) |
| data = 0x9; |
| else |
| data = 0; |
| |
| return data; |
| } |
| |
| static u8 smb347_get_termination_current_limit_data( |
| int termination_current) |
| { |
| u8 data; |
| |
| if (termination_current <= 37) |
| data = 0x0; |
| else if (termination_current <= 50) |
| data = 0x1; |
| else if (termination_current <= 100) |
| data = 0x2; |
| else if (termination_current <= 150) |
| data = 0x3; |
| else if (termination_current <= 200) |
| data = 0x4; |
| else if (termination_current <= 250) |
| data = 0x5; |
| else if (termination_current <= 500) |
| data = 0x6; |
| else if (termination_current <= 600) |
| data = 0x7; |
| else |
| data = 0; |
| |
| return data; |
| } |
| |
| static u8 smb347_get_fast_charging_current_data( |
| int fast_charging_current) |
| { |
| u8 data; |
| |
| if (fast_charging_current <= 700) |
| data = 0x0; |
| else if (fast_charging_current <= 900) |
| data = 0x1; |
| else if (fast_charging_current <= 1200) |
| data = 0x2; |
| else if (fast_charging_current <= 1500) |
| data = 0x3; |
| else if (fast_charging_current <= 1800) |
| data = 0x4; |
| else if (fast_charging_current <= 2000) |
| data = 0x5; |
| else if (fast_charging_current <= 2200) |
| data = 0x6; |
| else if (fast_charging_current <= 2500) |
| data = 0x7; |
| else |
| data = 0; |
| |
| return data << 5; |
| } |
| |
| static void smb347_charger_function_conrol( |
| struct i2c_client *client) |
| { |
| struct sec_charger_info *charger = i2c_get_clientdata(client); |
| u8 data; |
| |
| if (charger->charging_current < 0) { |
| dev_dbg(&client->dev, |
| "%s : OTG is activated. Ignore command!\n", __func__); |
| return; |
| } |
| smb347_allow_volatile_writes(client); |
| |
| if (charger->cable_type == |
| POWER_SUPPLY_TYPE_BATTERY) { |
| /* turn off charger */ |
| smb347_set_command(client, |
| SMB347_COMMAND_A, 0x80); |
| |
| /* high current mode for system current */ |
| smb347_set_command(client, |
| SMB347_COMMAND_B, 0x01); |
| } else { |
| /* Pre-charge curr 250mA */ |
| dev_dbg(&client->dev, |
| "%s : fast charging current (%dmA)\n", |
| __func__, charger->charging_current); |
| dev_dbg(&client->dev, |
| "%s : termination current (%dmA)\n", |
| __func__, charger->pdata->charging_current[ |
| charger->cable_type].full_check_current_1st); |
| data = 0x1c; |
| data |= smb347_get_fast_charging_current_data( |
| charger->charging_current); |
| data |= smb347_get_termination_current_limit_data( |
| charger->pdata->charging_current[ |
| charger->cable_type].full_check_current_1st); |
| smb347_set_command(client, |
| SMB347_CHARGE_CURRENT, data); |
| |
| /* Pin enable control */ |
| /* DCIN Input Pre-bias Enable */ |
| data = 0x01; |
| if (charger->pdata->chg_gpio_en) |
| data |= 0x40; |
| if (charger->pdata->chg_polarity_en) |
| data |= 0x20; |
| smb347_set_command(client, |
| SMB347_PIN_ENABLE_CONTROL, data); |
| |
| /* Input current limit */ |
| dev_dbg(&client->dev, "%s : input current (%dmA)\n", |
| __func__, charger->pdata->charging_current |
| [charger->cable_type].input_current_limit); |
| data = 0; |
| data = smb347_get_input_current_limit_data( |
| charger, |
| charger->pdata->charging_current |
| [charger->cable_type].input_current_limit); |
| smb347_set_command(client, |
| SMB347_INPUT_CURRENTLIMIT, data); |
| |
| /* |
| * Input to System FET by Register |
| * Enable AICL, VCHG |
| * Max System voltage =Vflt + 0.1v |
| * Input Source Priority : USBIN |
| */ |
| if (charger->pdata->chg_functions_setting & |
| SEC_CHARGER_NO_GRADUAL_CHARGING_CURRENT) |
| /* disable AICL */ |
| smb347_set_command(client, |
| SMB347_VARIOUS_FUNCTIONS, 0x85); |
| else |
| /* enable AICL */ |
| smb347_set_command(client, |
| SMB347_VARIOUS_FUNCTIONS, 0x95); |
| |
| /* Float voltage, Vprechg : 2.4V */ |
| dev_dbg(&client->dev, "%s : float voltage (%dmV)\n", |
| __func__, charger->pdata->chg_float_voltage); |
| data = 0; |
| data |= smb347_get_float_voltage_data( |
| charger->pdata->chg_float_voltage); |
| smb347_set_command(client, |
| SMB347_FLOAT_VOLTAGE, data); |
| |
| /* Charge control |
| * Automatic Recharge disable, |
| * Current Termination disable, |
| * BMD disable, Recharge Threshold =50mV, |
| * APSD disable */ |
| data = 0xC0; |
| switch (charger->pdata->full_check_type) { |
| case SEC_BATTERY_FULLCHARGED_CHGGPIO: |
| case SEC_BATTERY_FULLCHARGED_CHGINT: |
| case SEC_BATTERY_FULLCHARGED_CHGPSY: |
| /* Enable Current Termination */ |
| data &= 0xBF; |
| break; |
| default: |
| break; |
| } |
| smb347_set_command(client, |
| SMB347_CHARGE_CONTROL, data); |
| |
| /* STAT, Timer control : STAT active low, |
| * Complete time out 1527min. |
| */ |
| smb347_set_command(client, |
| SMB347_STAT_TIMERS_CONTROL, 0x1A); |
| |
| /* Pin/Enable |
| * USB 5/1/HC Dual state |
| * DCIN pre-bias Enable |
| */ |
| smb347_set_command(client, |
| SMB347_PIN_ENABLE_CONTROL, 0x09); |
| |
| /* Therm control : |
| * Therm monitor disable, |
| * Minimum System Voltage 3.60V |
| */ |
| smb347_set_command(client, |
| SMB347_THERM_CONTROL_A, 0x7F); |
| |
| /* USB selection : USB2.0(100mA/500mA), |
| * INOK polarity Active low |
| */ |
| smb347_set_command(client, |
| SMB347_SYSOK_USB30_SELECTION, 0x08); |
| |
| /* Other control |
| * Low batt detection disable |
| * Minimum System Voltage 3.60V |
| */ |
| smb347_set_command(client, |
| SMB347_OTHER_CONTROL_A, 0x00); |
| |
| /* OTG tlim therm control */ |
| smb347_set_command(client, |
| SMB347_OTG_TLIM_THERM_CONTROL, 0x3F); |
| |
| /* Limit cell temperature */ |
| smb347_set_command(client, |
| SMB347_LIMIT_CELL_TEMPERATURE_MONITOR, 0x01); |
| |
| /* Fault interrupt : Clear */ |
| smb347_set_command(client, |
| SMB347_FAULT_INTERRUPT, 0x00); |
| |
| /* STATUS ingerrupt : Clear */ |
| smb347_set_command(client, |
| SMB347_STATUS_INTERRUPT, 0x00); |
| |
| /* turn on charger */ |
| smb347_set_command(client, |
| SMB347_COMMAND_A, 0xC2); |
| |
| /* HC or USB5 mode */ |
| switch (charger->cable_type) { |
| case POWER_SUPPLY_TYPE_MAINS: |
| case POWER_SUPPLY_TYPE_MISC: |
| /* High-current mode */ |
| data = 0x01; |
| break; |
| case POWER_SUPPLY_TYPE_USB: |
| case POWER_SUPPLY_TYPE_USB_DCP: |
| case POWER_SUPPLY_TYPE_USB_CDP: |
| case POWER_SUPPLY_TYPE_USB_ACA: |
| /* USB5 */ |
| data = 0x02; |
| break; |
| default: |
| /* USB1 */ |
| data = 0x00; |
| break; |
| } |
| smb347_set_command(client, |
| SMB347_COMMAND_B, data); |
| } |
| } |
| |
| static void smb347_charger_otg_conrol( |
| struct i2c_client *client) |
| { |
| struct sec_charger_info *charger = i2c_get_clientdata(client); |
| smb347_allow_volatile_writes(client); |
| if (charger->cable_type == |
| POWER_SUPPLY_TYPE_BATTERY) { |
| /* turn off charger */ |
| smb347_set_command(client, |
| SMB347_COMMAND_A, 0x80); |
| } else { |
| /* turn on OTG */ |
| smb347_set_command(client, |
| SMB347_COMMAND_A, (0x1 << 4)); |
| } |
| } |
| |
| static int smb347_check_charging_status(struct i2c_client *client) |
| { |
| int val, reg; |
| u8 data = 0; |
| int ret = -1; |
| |
| reg = SMB347_STATUS_C; /* SMB328A_BATTERY_CHARGING_STATUS_C */ |
| val = smb347_read_reg(client, reg); |
| if (val >= 0) { |
| data = (u8) val; |
| pr_debug("%s : reg (0x%x) = 0x%x\n", __func__, reg, data); |
| |
| ret = (data & (0x3 << 1)) >> 1; |
| pr_debug("%s : status = 0x%x\n", __func__, data); |
| } |
| |
| return ret; |
| } |
| |
| bool sec_hal_chg_init(struct i2c_client *client) |
| { |
| smb347_test_read(client); |
| return true; |
| } |
| |
| bool sec_hal_chg_suspend(struct i2c_client *client) |
| { |
| return true; |
| } |
| |
| bool sec_hal_chg_resume(struct i2c_client *client) |
| { |
| return true; |
| } |
| |
| bool sec_hal_chg_get_property(struct i2c_client *client, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct sec_charger_info *charger = i2c_get_clientdata(client); |
| u8 data; |
| switch (psp) { |
| case POWER_SUPPLY_PROP_STATUS: |
| val->intval = smb347_get_charging_status(client); |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| switch (smb347_check_charging_status(client)) { |
| case 0: |
| val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; |
| break; |
| case 1: |
| val->intval = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; |
| break; |
| case 2: |
| val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; |
| break; |
| case 3: |
| val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
| break; |
| default: |
| pr_err("%s : get charge type error!\n", __func__); |
| return -EINVAL; |
| } |
| break; |
| case POWER_SUPPLY_PROP_HEALTH: |
| val->intval = smb347_get_charging_health(client); |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| if (charger->charging_current) { |
| smb347_i2c_read(client, SMB347_STATUS_B, &data); |
| if (data & 0x20) |
| switch (data & 0x18) { |
| case 0: |
| val->intval = 100; |
| break; |
| case 1: |
| val->intval = 150; |
| break; |
| case 2: |
| val->intval = 200; |
| break; |
| case 3: |
| val->intval = 250; |
| break; |
| } |
| else |
| switch (data & 0x07) { |
| case 0: |
| val->intval = 700; |
| break; |
| case 1: |
| val->intval = 900; |
| break; |
| case 2: |
| val->intval = 1200; |
| break; |
| case 3: |
| val->intval = 1500; |
| break; |
| case 4: |
| val->intval = 1800; |
| break; |
| case 5: |
| val->intval = 2000; |
| break; |
| case 6: |
| val->intval = 2200; |
| break; |
| case 7: |
| val->intval = 2500; |
| break; |
| } |
| } else |
| val->intval = 0; |
| dev_dbg(&client->dev, |
| "%s : set-current(%dmA), current now(%dmA)\n", |
| __func__, charger->charging_current, val->intval); |
| break; |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| bool sec_hal_chg_set_property(struct i2c_client *client, |
| enum power_supply_property psp, |
| const union power_supply_propval *val) |
| { |
| struct sec_charger_info *charger = i2c_get_clientdata(client); |
| |
| switch (psp) { |
| /* val->intval : type */ |
| case POWER_SUPPLY_PROP_ONLINE: |
| /* val->intval : charging current */ |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| if (charger->charging_current < 0) |
| smb347_charger_otg_conrol(client); |
| else if (charger->charging_current > 0) |
| smb347_charger_function_conrol(client); |
| else { |
| smb347_charger_function_conrol(client); |
| smb347_charger_otg_conrol(client); |
| } |
| smb347_test_read(client); |
| break; |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| ssize_t sec_hal_chg_show_attrs(struct device *dev, |
| const ptrdiff_t offset, char *buf) |
| { |
| struct power_supply *psy = dev_get_drvdata(dev); |
| struct sec_charger_info *chg = |
| container_of(psy, struct sec_charger_info, psy_chg); |
| int i = 0; |
| char *str = NULL; |
| |
| switch (offset) { |
| case CHG_DATA: |
| i += scnprintf(buf + i, PAGE_SIZE - i, "%x\n", |
| chg->reg_data); |
| break; |
| case CHG_REGS: |
| str = kzalloc(sizeof(char)*1024, GFP_KERNEL); |
| if (!str) |
| return -ENOMEM; |
| |
| smb347_read_regs(chg->client, str); |
| i += scnprintf(buf + i, PAGE_SIZE - i, "%s\n", |
| str); |
| |
| kfree(str); |
| break; |
| default: |
| i = -EINVAL; |
| break; |
| } |
| |
| return i; |
| } |
| |
| ssize_t sec_hal_chg_store_attrs(struct device *dev, |
| const ptrdiff_t offset, |
| const char *buf, size_t count) |
| { |
| struct power_supply *psy = dev_get_drvdata(dev); |
| struct sec_charger_info *chg = |
| container_of(psy, struct sec_charger_info, psy_chg); |
| int ret = 0; |
| int x = 0; |
| u8 data = 0; |
| |
| switch (offset) { |
| case CHG_REG: |
| if (sscanf(buf, "%x\n", &x) == 1) { |
| chg->reg_addr = x; |
| smb347_i2c_read(chg->client, |
| chg->reg_addr, &data); |
| chg->reg_data = data; |
| dev_dbg(dev, "%s: (read) addr = 0x%x, data = 0x%x\n", |
| __func__, chg->reg_addr, chg->reg_data); |
| ret = count; |
| } |
| break; |
| case CHG_DATA: |
| if (sscanf(buf, "%x\n", &x) == 1) { |
| data = (u8)x; |
| dev_dbg(dev, "%s: (write) addr = 0x%x, data = 0x%x\n", |
| __func__, chg->reg_addr, data); |
| smb347_i2c_write(chg->client, |
| chg->reg_addr, &data); |
| ret = count; |
| } |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |