/* * sb_tx.c * Samsung Mobile Wireless TX Driver * * Copyright (C) 2021 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. */ #include #include #include #include #include "sec_battery.h" #include "sec_charging_common.h" #include "sb_pass_through.h" #define pt_log(str, ...) pr_info("[PASS-THROUGH]:%s: "str, __func__, ##__VA_ARGS__) #define PT_MODULE_NAME "pass-through" #define IV_VOTE_NAME "IV" #define ICL_VOTE_NAME "ICL" #define FCC_VOTE_NAME "FCC" #define CHGEN_VOTE_NAME "CHGEN" struct sb_pt { struct notifier_block nb; struct mutex mlock; struct wakeup_source *ws; struct workqueue_struct *wq; struct delayed_work start_work; struct delayed_work adjust_work; struct delayed_work step_work; /* state flags */ int user_mode; int chg_src; int step; int ref_cap; int adj_state; unsigned int adj_cnt; unsigned int adj_op_cnt; /* battery status */ int cable_type; int batt_status; int dc_status; /* dt data */ bool is_enabled; unsigned int start_delay; unsigned int init_delay; unsigned int adj_delay; unsigned int adj_max_cnt; unsigned int min_cap; unsigned int fixed_sc_cap; unsigned int max_icl; unsigned int vfloat; char *sc_name; char *dc_name; char *fg_name; }; enum pt_step { PT_STEP_NONE = 0, PT_STEP_INIT, PT_STEP_PRESET, PT_STEP_ADJUST, PT_STEP_MONITOR, PT_STEP_RESET, }; static const char *get_step_str(int step) { switch (step) { case PT_STEP_NONE: return "None"; case PT_STEP_INIT: return "Init"; case PT_STEP_PRESET: return "Preset"; case PT_STEP_ADJUST: return "Adjust"; case PT_STEP_MONITOR: return "Monitor"; case PT_STEP_RESET: return "Reset"; } return "Unknown"; } static void set_misc_event(bool state) { struct sbn_bit_event misc; misc.value = (state) ? BATT_MISC_EVENT_PASS_THROUGH : 0; misc.mask = BATT_MISC_EVENT_PASS_THROUGH; sb_notify_call(SB_NOTIFY_EVENT_MISC, cast_to_sb_pdata(&misc)); } static int set_dc_ta_volt(struct sb_pt *pt, int value) { union power_supply_propval val = { value, }; return psy_do_property(pt->dc_name, set, POWER_SUPPLY_EXT_PROP_PASS_THROUGH_MODE_TA_VOL, val); } /* don't call the mutex lock in static functions */ static bool check_state(struct sb_pt *pt) { if (!pt) return false; if (!pt->is_enabled) return false; if (pt->user_mode == PTM_NONE) return false; if (!is_pd_apdo_wire_type(pt->cable_type)) return false; if (pt->batt_status != POWER_SUPPLY_STATUS_CHARGING) return false; if (pt->step == PT_STEP_NONE) { union power_supply_propval value = { 0, }; value.intval = SEC_FUELGAUGE_CAPACITY_TYPE_DYNAMIC_SCALE; psy_do_property(pt->fg_name, get, POWER_SUPPLY_PROP_CAPACITY, value); if (pt->min_cap >= value.intval) return false; } return true; } static bool check_preset_state(int dc_status) { return (dc_status == SEC_DIRECT_CHG_MODE_DIRECT_ON) || (dc_status == SEC_DIRECT_CHG_MODE_DIRECT_DONE) || (dc_status == SEC_DIRECT_CHG_MODE_DIRECT_BYPASS); } #define CAP_HIGH (1) #define CAP_NORMAL (0) #define CAP_LOW (-1) static int check_cap(struct sb_pt *pt) { union power_supply_propval value = {0, }; int ncap1, ncap2, rcap1, rcap2; int delta_cap = 0; value.intval = SEC_FUELGAUGE_CAPACITY_TYPE_DYNAMIC_SCALE; psy_do_property(pt->fg_name, get, POWER_SUPPLY_PROP_CAPACITY, value); ncap1 = (value.intval / 10); ncap2 = (value.intval % 10); rcap1 = (pt->ref_cap / 10); rcap2 = (pt->ref_cap % 10); value.intval = SEC_BATTERY_CURRENT_MA; psy_do_property(pt->fg_name, get, POWER_SUPPLY_PROP_CURRENT_NOW, value); if (ncap1 == rcap1) delta_cap = (((ncap2 >= 6) && (value.intval > 0)) ? CAP_HIGH : (((ncap2 <= 4) && (value.intval < 0)) ? CAP_LOW : CAP_NORMAL)); else if (ncap1 > rcap1) delta_cap = (value.intval > 0) ? CAP_HIGH : CAP_NORMAL; else delta_cap = (value.intval < 0) ? CAP_LOW : CAP_NORMAL; pt_log("Now Cap(%03d.%d%%), Ref Cap(%03d.%d%%), Current(%04dmA), delta(%d)\n", ncap1, ncap2, rcap1, rcap2, value.intval, delta_cap); return delta_cap; } static void clear_state(struct sb_pt *pt, int init_step) { if (pt->step == PT_STEP_NONE) return; pt_log("latest step = %d, init_step = %d\n", pt->step, init_step); sec_votef(IV_VOTE_NAME, VOTER_PASS_THROUGH, false, 0); sec_votef(ICL_VOTE_NAME, VOTER_PASS_THROUGH, false, 0); sec_votef(FCC_VOTE_NAME, VOTER_PASS_THROUGH, false, 0); sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, false, 0); pt->chg_src = SEC_CHARGING_SOURCE_SWITCHING; pt->adj_state = CAP_NORMAL; pt->adj_cnt = 0; pt->adj_op_cnt = 0; if (init_step == PT_STEP_NONE) { pt->ref_cap = 0; /* set charging off before re-starting dc */ sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, true, SEC_BAT_CHG_MODE_CHARGING_OFF); sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, false, 0); /* clear event */ set_misc_event(false); } pt->step = init_step; } static void cb_start_work(struct work_struct *work) { struct sb_pt *pt = container_of(work, struct sb_pt, start_work.work); mutex_lock(&pt->mlock); pt_log("now step = %s\n", get_step_str(pt->step)); if (!check_state(pt)) goto end_work; if (pt->step == PT_STEP_NONE) pt->step = PT_STEP_INIT; if (pt->step != PT_STEP_INIT) goto end_work; __pm_wakeup_event(pt->ws, msecs_to_jiffies(1000)); queue_delayed_work(pt->wq, &pt->step_work, 0); end_work: mutex_unlock(&pt->mlock); } #define PT_ADJ_OP_MAX_CNT 100 #define PT_ADJ_CURR 500 static void cb_adjust_work(struct work_struct *work) { struct sb_pt *pt = container_of(work, struct sb_pt, adjust_work.work); union power_supply_propval value = {0, }; int now_state = CAP_NORMAL; mutex_lock(&pt->mlock); if (!check_state(pt)) goto end_work; if (pt->step != PT_STEP_ADJUST) goto end_work; if (pt->chg_src == SEC_CHARGING_SOURCE_SWITCHING) { pt->step = PT_STEP_MONITOR; goto end_work; } if (pt->adj_op_cnt++ >= PT_ADJ_OP_MAX_CNT) { pt_log("over working(%d) !!\n", pt->adj_op_cnt); pt->step = PT_STEP_MONITOR; goto end_work; } now_state = check_cap(pt); if (now_state != pt->adj_state) { pt->adj_cnt = 0; pt->adj_state = now_state; goto re_work; } value.intval = SEC_BATTERY_CURRENT_MA; psy_do_property(pt->fg_name, get, POWER_SUPPLY_PROP_CURRENT_NOW, value); switch (pt->adj_state) { case CAP_LOW: if (value.intval < (-PT_ADJ_CURR)) { pt->adj_cnt = 0; set_dc_ta_volt(pt, CAP_LOW); } else if (value.intval > (PT_ADJ_CURR)) { pt->adj_cnt = 0; } else { pt->adj_cnt++; } break; case CAP_HIGH: if (value.intval > (PT_ADJ_CURR)) { pt->adj_cnt = 0; set_dc_ta_volt(pt, CAP_HIGH); } else if (value.intval < (-PT_ADJ_CURR)) { pt->adj_cnt = 0; } else { pt->adj_cnt++; } break; case CAP_NORMAL: if (value.intval < (-PT_ADJ_CURR)) { pt->adj_cnt = 0; set_dc_ta_volt(pt, CAP_LOW); } else if (value.intval > (PT_ADJ_CURR)) { pt->adj_cnt = 0; set_dc_ta_volt(pt, CAP_HIGH); } else { pt->adj_cnt++; } break; default: break; } pt_log("ADJ STATE(%d, %d), CURR(%d), CNT(%d)\n", pt->adj_state, now_state, value.intval, pt->adj_cnt); if (pt->adj_cnt >= pt->adj_max_cnt) { pt->step = PT_STEP_MONITOR; goto end_work; } re_work: mutex_unlock(&pt->mlock); queue_delayed_work(pt->wq, &pt->adjust_work, msecs_to_jiffies(pt->adj_delay)); return; end_work: pt->adj_state = CAP_NORMAL; pt->adj_cnt = 0; pt->adj_op_cnt = 0; mutex_unlock(&pt->mlock); __pm_relax(pt->ws); } static void cb_step_work(struct work_struct *work) { struct sb_pt *pt = container_of(work, struct sb_pt, step_work.work); sb_pt_monitor(pt, pt->chg_src); } static void push_start_work(struct sb_pt *pt, unsigned int delay) { unsigned int work_state; if (!check_state(pt)) return; work_state = work_busy(&pt->start_work.work); pt_log("work_state = 0x%x, delay = %d\n", work_state, delay); if (!(work_state & (WORK_BUSY_PENDING | WORK_BUSY_RUNNING))) { __pm_wakeup_event(pt->ws, msecs_to_jiffies(delay + 1000)); queue_delayed_work(pt->wq, &pt->start_work, msecs_to_jiffies(delay)); } } static ssize_t show_attrs(struct device *dev, struct device_attribute *attr, char *buf); static ssize_t store_attrs(struct device *dev, struct device_attribute *attr, const char *buf, size_t count); #define PT_SYSFS_ATTR(_name) \ { \ .attr = {.name = #_name, .mode = 0664}, \ .show = show_attrs, \ .store = store_attrs, \ } static struct device_attribute pt_attr[] = { PT_SYSFS_ATTR(pass_through), }; enum sb_pt_attrs { PASS_THROUGH = 0, }; static ssize_t show_attrs(struct device *dev, struct device_attribute *attr, char *buf) { struct sb_pt *pt = sb_sysfs_get_pdata(PT_MODULE_NAME); const ptrdiff_t offset = attr - pt_attr; ssize_t count = 0; switch (offset) { case PASS_THROUGH: count += scnprintf(buf + count, PAGE_SIZE - count, "%d\n", pt->user_mode); break; default: break; } return count; } static ssize_t store_attrs(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct sb_pt *pt = sb_sysfs_get_pdata(PT_MODULE_NAME); const ptrdiff_t offset = attr - pt_attr; switch (offset) { case PASS_THROUGH: { int x = 0; if (sscanf(buf, "%10d\n", &x) == 1) { mutex_lock(&pt->mlock); pt_log("user_mode = %d <-> %d, %s\n", x, pt->user_mode, ((x != PTM_NONE) ? "enabled" : "disabled")); x = (x) ? PTM_2TO1 : PTM_NONE; if (pt->user_mode != x) { pt->user_mode = x; if (pt->step != PT_STEP_NONE) clear_state(pt, PT_STEP_NONE); if (pt->user_mode) push_start_work(pt, pt->start_delay); } mutex_unlock(&pt->mlock); } } break; default: break; } return count; } static int sb_noti_handler(struct notifier_block *nb, unsigned long action, void *data) { return 0; } static int parse_dt(struct sb_pt *pt, struct device *parent) { #if defined(CONFIG_OF) struct device_node *np; int ret = 0; if (!parent) return -EINVAL; np = of_find_node_by_name(NULL, PT_MODULE_NAME); if (!np) { pt_log("failed to find root node\n"); return -ENODEV; } pt->is_enabled = true; sb_of_parse_u32(np, pt, start_delay, 5000); sb_of_parse_u32(np, pt, init_delay, 5000); sb_of_parse_u32(np, pt, adj_delay, 500); sb_of_parse_u32(np, pt, adj_max_cnt, 3); sb_of_parse_u32(np, pt, min_cap, 200); sb_of_parse_u32(np, pt, fixed_sc_cap, 900); sb_of_parse_u32(np, pt, max_icl, 3000); sb_of_parse_u32(np, pt, vfloat, 4400); np = of_find_node_by_name(NULL, "battery"); if (np) { ret = of_property_read_string(np, "battery,fuelgauge_name", (const char **)&pt->fg_name); if (ret) pt_log("failed to get fg name in battery dt (ret = %d)\n", ret); } ret = of_property_read_string(parent->of_node, "charger,main_charger", (const char **)&pt->sc_name); if (ret) pt_log("failed to get sc name in dc drv (ret = %d)\n", ret); ret = of_property_read_string(parent->of_node, "charger,direct_charger", (const char **)&pt->dc_name); if (ret) pt_log("failed to get dc name in dc drv (ret = %d)\n", ret); #endif return 0; } struct sb_pt *sb_pt_init(struct device *parent) { struct sb_pt *pt; int ret = 0; pt = kzalloc(sizeof(struct sb_pt), GFP_KERNEL); if (!pt) return ERR_PTR(-ENOMEM); ret = parse_dt(pt, parent); pt_log("parse_dt ret = %s\n", (ret) ? "fail" : "success"); if (ret) goto failed_dt; pt->wq = create_singlethread_workqueue(PT_MODULE_NAME); if (!pt->wq) { ret = -ENOMEM; goto failed_wq; } pt->ws = wakeup_source_register(parent, PT_MODULE_NAME); INIT_DELAYED_WORK(&pt->start_work, cb_start_work); INIT_DELAYED_WORK(&pt->adjust_work, cb_adjust_work); INIT_DELAYED_WORK(&pt->step_work, cb_step_work); mutex_init(&pt->mlock); pt->chg_src = SEC_CHARGING_SOURCE_SWITCHING; pt->step = PT_STEP_NONE; pt->ref_cap = 0; pt->user_mode = PTM_NONE; pt->adj_state = CAP_NORMAL; pt->adj_cnt = 0; pt->adj_op_cnt = 0; ret = sb_sysfs_add_attrs(PT_MODULE_NAME, pt, pt_attr, ARRAY_SIZE(pt_attr)); pt_log("sb_sysfs_add_attrs ret = %s\n", (ret) ? "fail" : "success"); ret = sb_notify_register(&pt->nb, sb_noti_handler, PT_MODULE_NAME, SB_DEV_MODULE); pt_log("sb_notify_register ret = %s\n", (ret) ? "fail" : "success"); return pt; failed_wq: failed_dt: kfree(pt); return ERR_PTR(ret); } EXPORT_SYMBOL(sb_pt_init); int sb_pt_psy_set_property(struct sb_pt *pt, enum power_supply_property psp, const union power_supply_propval *value) { if (!pt) return 0; switch ((int)psp) { case POWER_SUPPLY_PROP_STATUS: pt->batt_status = value->intval; break; case POWER_SUPPLY_PROP_ONLINE: pt->cable_type = value->intval; mutex_lock(&pt->mlock); if (!is_pd_apdo_wire_type(pt->cable_type) && (pt->step != PT_STEP_NONE)) clear_state(pt, PT_STEP_NONE); mutex_unlock(&pt->mlock); break; case POWER_SUPPLY_EXT_PROP_DIRECT_CHARGER_MODE: /* Caution : dead lock */ pt->dc_status = value->intval; break; default: break; } return 0; } EXPORT_SYMBOL(sb_pt_psy_set_property); int sb_pt_psy_get_property(struct sb_pt *pt, enum power_supply_property psp, union power_supply_propval *value) { int ret = 0; if (!pt) return 0; switch (psp) { case POWER_SUPPLY_PROP_STATUS: mutex_lock(&pt->mlock); if ((pt->step != PT_STEP_NONE) && (pt->chg_src == SEC_CHARGING_SOURCE_SWITCHING)) { union power_supply_propval val; val.intval = 0; psy_do_property(pt->sc_name, get, psp, val); if (val.intval == POWER_SUPPLY_STATUS_FULL) { ret = -EBUSY; pt_log("prevent charging status\n"); value->intval = POWER_SUPPLY_STATUS_CHARGING; } } mutex_unlock(&pt->mlock); break; case POWER_SUPPLY_PROP_HEALTH: mutex_lock(&pt->mlock); if (pt->step != PT_STEP_NONE) { union power_supply_propval val; val.intval = 0; psy_do_property(pt->dc_name, get, psp, val); if (val.intval == POWER_SUPPLY_EXT_HEALTH_DC_ERR) { pt_log("clear pt state because of DC err(%d)\n", val.intval); clear_state(pt, PT_STEP_NONE); } } mutex_unlock(&pt->mlock); break; default: break; } return ret; } EXPORT_SYMBOL(sb_pt_psy_get_property); int sb_pt_monitor(struct sb_pt *pt, int chg_src) { if (!pt) return -EINVAL; mutex_lock(&pt->mlock); if (!check_state(pt)) { clear_state(pt, PT_STEP_NONE); goto end_monitor; } pt_log("start - step = %s, chg_src = %d, dc_status = %d\n", get_step_str(pt->step), chg_src, pt->dc_status); switch (pt->step) { case PT_STEP_NONE: push_start_work(pt, pt->start_delay); pt->chg_src = chg_src; break; case PT_STEP_INIT: if (pt->ref_cap <= 0) { union power_supply_propval value = { 0, }; value.intval = SEC_FUELGAUGE_CAPACITY_TYPE_DYNAMIC_SCALE; psy_do_property(pt->fg_name, get, POWER_SUPPLY_PROP_CAPACITY, value); pt->ref_cap = value.intval; pt_log("update ref_cap = %d\n", pt->ref_cap); if ((chg_src == SEC_CHARGING_SOURCE_DIRECT) && (pt->ref_cap > pt->fixed_sc_cap)) { /* reset chg src to switching charger */ sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, true, SEC_BAT_CHG_MODE_CHARGING_OFF); pt->step = PT_STEP_PRESET; break; } } if (chg_src == SEC_CHARGING_SOURCE_SWITCHING) { pt->step = PT_STEP_PRESET; } else { if (check_preset_state(pt->dc_status)) pt->step = PT_STEP_PRESET; else push_start_work(pt, pt->init_delay); } pt->chg_src = chg_src; break; case PT_STEP_PRESET: { union power_supply_propval value = { 0, }; int iv, icl, fcc, chgen; if ((chg_src == SEC_CHARGING_SOURCE_DIRECT) && (pt->ref_cap > pt->fixed_sc_cap)) chg_src = SEC_CHARGING_SOURCE_SWITCHING; pt->chg_src = chg_src; pt->step = PT_STEP_ADJUST; if (chg_src == SEC_CHARGING_SOURCE_SWITCHING) { iv = SEC_INPUT_VOLTAGE_5V; icl = fcc = pt->max_icl; } else { iv = SEC_INPUT_VOLTAGE_APDO; icl = fcc = pt->max_icl * pt->user_mode; } chgen = SEC_BAT_CHG_MODE_PASS_THROUGH; sec_votef(IV_VOTE_NAME, VOTER_PASS_THROUGH, true, iv); sec_votef(ICL_VOTE_NAME, VOTER_PASS_THROUGH, true, icl); sec_votef(FCC_VOTE_NAME, VOTER_PASS_THROUGH, true, fcc); sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, true, chgen); if (chg_src == SEC_CHARGING_SOURCE_SWITCHING) { value.intval = pt->user_mode; psy_do_property(pt->sc_name, set, POWER_SUPPLY_EXT_PROP_PASS_THROUGH_MODE, value); /* skip adjust work */ pt->step = PT_STEP_MONITOR; } else { value.intval = pt->user_mode; psy_do_property(pt->dc_name, set, POWER_SUPPLY_EXT_PROP_PASS_THROUGH_MODE, value); value.intval = pt->vfloat; psy_do_property(pt->dc_name, set, POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, value); /* set adj state */ pt->adj_state = check_cap(pt); /* start adj work */ __pm_stay_awake(pt->ws); queue_delayed_work(pt->wq, &pt->adjust_work, msecs_to_jiffies(pt->adj_delay)); } set_misc_event(true); } break; case PT_STEP_ADJUST: /* not working */ break; case PT_STEP_MONITOR: { int cap_state = CAP_NORMAL; if (pt->chg_src != chg_src) { clear_state(pt, PT_STEP_INIT); break; } cap_state = check_cap(pt); if (pt->chg_src == SEC_CHARGING_SOURCE_SWITCHING) { if (cap_state == CAP_LOW) sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, true, SEC_BAT_CHG_MODE_CHARGING); else sec_votef(CHGEN_VOTE_NAME, VOTER_PASS_THROUGH, true, SEC_BAT_CHG_MODE_PASS_THROUGH); } else { set_dc_ta_volt(pt, cap_state); } } break; case PT_STEP_RESET: break; default: break; } end_monitor: pt_log("end - step = %s\n", get_step_str(pt->step)); mutex_unlock(&pt->mlock); return 0; } EXPORT_SYMBOL(sb_pt_monitor); int sb_pt_check_chg_src(struct sb_pt *pt, int chg_src) { if (!pt) return chg_src; if ((pt->step != PT_STEP_NONE) && (pt->ref_cap > pt->fixed_sc_cap)) return SEC_CHARGING_SOURCE_SWITCHING; return chg_src; } EXPORT_SYMBOL(sb_pt_check_chg_src);