// SPDX-License-Identifier: GPL-2.0 /* * Copyright (c) 2019 MediaTek Inc. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pmic_lbat_service.h" #define USER_NAME_MAXLEN 30 #define USER_SIZE 16 #define THD_VOLT_MAX 5400 #define THD_VOLT_MIN 2650 #define VOLT_FULL 1800 #define LBAT_RES 12 /* DEBT_SEL: {0: 1, 1: 2, 2: 4, 3: 8}*/ #define DEF_DEBT_MAX_SEL 3 #define DEF_DEBT_MIN_SEL 0 /* DET_PRD_SEL: {0: 15, 1: 30, 2: 45, 3: 60}*/ #define DEF_DET_PRD_SEL 0 #define DEF_R_RATIO_0 7 #define DEF_R_RATIO_1 2 #define LBAT_SERVICE_DBG 0 enum lbat_thd_type { LBAT_HV, LBAT_LV, }; struct lbat_thd_t { unsigned int thd_volt; struct lbat_user *user; struct list_head list; }; #if 0 struct lbat_user { char name[USER_NAME_MAXLEN]; struct lbat_thd_t *hv_thd; struct lbat_thd_t *lv1_thd; struct lbat_thd_t *lv2_thd; void (*callback)(unsigned int thd_volt); unsigned int deb_cnt; struct lbat_thd_t *deb_thd_ptr; unsigned int hv_deb_prd; unsigned int hv_deb_times; unsigned int lv_deb_prd; unsigned int lv_deb_times; struct timer_list deb_timer; struct work_struct deb_work; }; #endif struct reg_t { unsigned int addr; unsigned int mask; }; struct lbat_regs_t { struct reg_t en; struct reg_t debt_max; struct reg_t debt_min; struct reg_t det_prd; struct reg_t max_en; struct reg_t volt_max; struct reg_t min_en; struct reg_t volt_min; struct reg_t adc_out; }; struct lbat_regs_t mt6359_lbat_regs = { .en = {MT6359_AUXADC_LBAT0, 0x1}, .debt_max = {MT6359_AUXADC_LBAT1, 0xC}, .debt_min = {MT6359_AUXADC_LBAT1, 0x30}, .det_prd = {MT6359_AUXADC_LBAT1, 0x3}, .max_en = {MT6359_AUXADC_LBAT2, 0x3000}, .volt_max = {MT6359_AUXADC_LBAT2, 0xFFF}, .min_en = {MT6359_AUXADC_LBAT3, 0x3000}, .volt_min = {MT6359_AUXADC_LBAT3, 0xFFF}, .adc_out = {MT6359_AUXADC_LBAT7, 0xFFF}, }; static DEFINE_MUTEX(lbat_mutex); static struct list_head lbat_hv_list = LIST_HEAD_INIT(lbat_hv_list); static struct list_head lbat_lv_list = LIST_HEAD_INIT(lbat_lv_list); /* workqueue for SW de-bounce */ static struct workqueue_struct *lbat_wq; struct regmap *regmap; const struct lbat_regs_t *lbat_regs; static struct lbat_thd_t *cur_hv_ptr; static struct lbat_thd_t *cur_lv_ptr; static struct lbat_user *lbat_user_table[USER_SIZE]; static unsigned int user_count; static unsigned int r_ratio[2]; static unsigned int VOLT_TO_RAW(unsigned int volt) { return (volt << LBAT_RES) / (VOLT_FULL * r_ratio[0] / r_ratio[1]); } static void lbat_max_en_setting(bool en) { unsigned int val; val = en ? lbat_regs->max_en.mask : 0; regmap_update_bits(regmap, lbat_regs->max_en.addr, lbat_regs->max_en.mask, val); } static void lbat_min_en_setting(bool en) { unsigned int val; val = en ? lbat_regs->min_en.mask : 0; regmap_update_bits(regmap, lbat_regs->min_en.addr, lbat_regs->min_en.mask, val); } static void lbat_irq_enable(void) { if (cur_hv_ptr != NULL) lbat_max_en_setting(true); if (cur_lv_ptr != NULL) lbat_min_en_setting(true); regmap_write(regmap, lbat_regs->en.addr, 1); } static void lbat_irq_disable(void) { regmap_write(regmap, lbat_regs->en.addr, 0); lbat_max_en_setting(false); lbat_min_en_setting(false); } static int hv_list_cmp(void *priv, struct list_head *a, struct list_head *b) { struct lbat_thd_t *thd_a, *thd_b; thd_a = list_entry(a, struct lbat_thd_t, list); thd_b = list_entry(b, struct lbat_thd_t, list); return thd_a->thd_volt - thd_b->thd_volt; } static int lv_list_cmp(void *priv, struct list_head *a, struct list_head *b) { struct lbat_thd_t *thd_a, *thd_b; thd_a = list_entry(a, struct lbat_thd_t, list); thd_b = list_entry(b, struct lbat_thd_t, list); return thd_b->thd_volt - thd_a->thd_volt; } static void modify_lbat_list(enum lbat_thd_type type, struct lbat_thd_t *thd) { switch (type) { case LBAT_HV: list_add(&thd->list, &lbat_hv_list); list_sort(NULL, &lbat_hv_list, hv_list_cmp); thd = list_first_entry(&lbat_hv_list, struct lbat_thd_t, list); if (cur_hv_ptr != thd) { cur_hv_ptr = thd; regmap_update_bits(regmap, lbat_regs->volt_max.addr, lbat_regs->volt_max.mask, VOLT_TO_RAW(cur_hv_ptr->thd_volt)); } break; case LBAT_LV: list_add(&thd->list, &lbat_lv_list); list_sort(NULL, &lbat_lv_list, lv_list_cmp); thd = list_first_entry(&lbat_lv_list, struct lbat_thd_t, list); if (cur_lv_ptr != thd) { cur_lv_ptr = thd; regmap_update_bits(regmap, lbat_regs->volt_min.addr, lbat_regs->volt_min.mask, VOLT_TO_RAW(cur_lv_ptr->thd_volt)); } break; } } /* * After execute lbat_user's callback, set next thd node to wait event */ static void lbat_set_next_thd(struct lbat_user *user, struct lbat_thd_t *thd) { if (thd == user->hv_thd) { modify_lbat_list(LBAT_LV, user->lv1_thd); if (user->lv2_thd && !list_empty(&user->lv2_thd->list)) list_del_init(&user->lv2_thd->list); } else if (thd == user->lv1_thd) { modify_lbat_list(LBAT_HV, user->hv_thd); if (user->lv2_thd && list_empty(&user->lv2_thd->list)) modify_lbat_list(LBAT_LV, user->lv2_thd); } } /* * Execute user's callback and set its next threshold if reach deb_times, * otherwise ignore this event and reset lbat_list */ static void lbat_deb_handler(struct work_struct *work) { enum lbat_thd_type type; unsigned int deb_times; struct lbat_user *user = container_of(work, struct lbat_user, deb_work); mutex_lock(&lbat_mutex); if (user->deb_thd_ptr == user->hv_thd) { type = LBAT_HV; deb_times = user->hv_deb_times; } else { type = LBAT_LV; deb_times = user->lv_deb_times; } if (user->deb_cnt >= deb_times) { /* execute user's callback after de-bounce */ user->callback(user->deb_thd_ptr->thd_volt); lbat_set_next_thd(user, user->deb_thd_ptr); } else { /* ignore this event and reset lbat_list */ modify_lbat_list(type, user->deb_thd_ptr); } /* de-bounce done, reset deb_cnt and deb_thd_ptr */ user->deb_cnt = 0; user->deb_thd_ptr = NULL; lbat_irq_disable(); udelay(200); lbat_irq_enable(); mutex_unlock(&lbat_mutex); } static void lbat_timer_func(struct timer_list *t) { unsigned int deb_prd = 0; unsigned int deb_times = 0; struct lbat_user *user = from_timer(user, t, deb_timer); if (user->deb_thd_ptr == user->hv_thd) { /* LBAT user HV de-bounce */ if (lbat_read_volt() < user->deb_thd_ptr->thd_volt) { /* queue deb_work to reset lbat_list */ goto wq_handler; } deb_prd = user->hv_deb_prd; deb_times = user->hv_deb_times; } else if (user->deb_thd_ptr == user->lv1_thd || user->deb_thd_ptr == user->lv2_thd) { /* LBAT user LV de-bounce */ if (lbat_read_volt() > user->deb_thd_ptr->thd_volt) { /* queue deb_work to reset lbat_list */ goto wq_handler; } deb_prd = user->lv_deb_prd; deb_times = user->lv_deb_times; } else { pr_notice("[%s] LBAT debounce threshold not match\n", __func__); return; } user->deb_cnt++; #if LBAT_SERVICE_DBG pr_info("[%s] name:%s, thd_volt:%d, de-bounce times:%d\n", __func__, user->name, user->deb_thd_ptr->thd_volt, user->deb_cnt); #endif if (user->deb_cnt < deb_times) { mod_timer(&user->deb_timer, jiffies + msecs_to_jiffies(deb_prd)); return; } wq_handler: /* queue deb_work to execute user's callback or reset lbat_list */ queue_work(lbat_wq, &user->deb_work); } static void lbat_user_init_timer(struct lbat_user *user) { user->deb_cnt = 0; user->hv_deb_prd = 0; user->hv_deb_times = 0; user->lv_deb_prd = 0; user->lv_deb_times = 0; timer_setup(&user->deb_timer, lbat_timer_func, 0); } static int lbat_user_update(struct lbat_user *user) { /* * add lv_thd to lbat_lv_list * and assign first entry of lv_list to cur_lv_ptr */ modify_lbat_list(LBAT_LV, user->lv1_thd); if (user_count == 0) lbat_irq_enable(); lbat_user_table[user_count++] = user; return 0; } static struct lbat_thd_t *lbat_thd_init(unsigned int thd_volt, struct lbat_user *user) { struct lbat_thd_t *thd; if (thd_volt == 0) return NULL; thd = kzalloc(sizeof(*thd), GFP_KERNEL); if (thd == NULL) return NULL; thd->thd_volt = thd_volt; thd->user = user; INIT_LIST_HEAD(&thd->list); return thd; } struct lbat_user *lbat_user_register(const char *name, unsigned int hv_thd_volt, unsigned int lv1_thd_volt, unsigned int lv2_thd_volt, void (*callback)(unsigned int thd_volt)) { int ret = 0; struct lbat_user *user; if (!regmap) return ERR_PTR(-EPROBE_DEFER); mutex_lock(&lbat_mutex); user = kzalloc(sizeof(*user), GFP_KERNEL); if (user == NULL) { ret = -ENOMEM; goto out; } strncpy(user->name, name, USER_NAME_MAXLEN - 1); if (hv_thd_volt >= THD_VOLT_MAX || lv1_thd_volt <= THD_VOLT_MIN) { ret = -EINVAL; goto out; } else if (hv_thd_volt < lv1_thd_volt || lv1_thd_volt < lv2_thd_volt) { ret = -EINVAL; goto out; } else if (callback == NULL) { ret = -EINVAL; goto out; } user->hv_thd = lbat_thd_init(hv_thd_volt, user); user->lv1_thd = lbat_thd_init(lv1_thd_volt, user); user->lv2_thd = lbat_thd_init(lv2_thd_volt, user); if (!user->hv_thd || !user->lv1_thd || !user->lv2_thd) { ret = -EINVAL; goto out; } user->callback = callback; lbat_user_init_timer(user); INIT_WORK(&user->deb_work, lbat_deb_handler); pr_info("[%s] hv=%d, lv1=%d, lv2=%d\n", __func__, hv_thd_volt, lv1_thd_volt, lv2_thd_volt); ret = lbat_user_update(user); out: if (ret) { pr_notice("[%s] error ret=%d\n", __func__, ret); if (ret == -EINVAL) kfree(user); return ERR_PTR(ret); } mutex_unlock(&lbat_mutex); return user; } EXPORT_SYMBOL(lbat_user_register); int lbat_user_set_debounce(struct lbat_user *user, unsigned int hv_deb_prd, unsigned int hv_deb_times, unsigned int lv_deb_prd, unsigned int lv_deb_times) { if (IS_ERR(user)) return PTR_ERR(user); user->hv_deb_prd = hv_deb_prd; user->hv_deb_times = hv_deb_times; user->lv_deb_prd = lv_deb_prd; user->lv_deb_times = lv_deb_times; return 0; } EXPORT_SYMBOL(lbat_user_set_debounce); unsigned int lbat_read_raw(void) { unsigned int adc_out = 0; if (!regmap) return 0; regmap_read(regmap, lbat_regs->adc_out.addr, &adc_out); adc_out &= lbat_regs->adc_out.mask; return adc_out; } EXPORT_SYMBOL(lbat_read_raw); unsigned int lbat_read_volt(void) { unsigned int raw_data = lbat_read_raw(); return (raw_data * VOLT_FULL * r_ratio[0] / r_ratio[1]) >> LBAT_RES; } EXPORT_SYMBOL(lbat_read_volt); static irqreturn_t bat_h_int_handler(int irq, void *data) { struct lbat_user *user; if (cur_hv_ptr == NULL) { lbat_max_en_setting(0); return IRQ_NONE; } mutex_lock(&lbat_mutex); pr_info("[%s] cur_thd_volt=%d\n", __func__, cur_hv_ptr->thd_volt); user = cur_hv_ptr->user; list_del_init(&cur_hv_ptr->list); if (user->hv_deb_times) { user->deb_cnt = 0; user->deb_thd_ptr = cur_hv_ptr; mod_timer(&user->deb_timer, jiffies + msecs_to_jiffies(user->hv_deb_prd)); } else { user->callback(cur_hv_ptr->thd_volt); lbat_set_next_thd(user, cur_hv_ptr); } /* Since cur_hv_ptr is removed, assign new thd for cur_hv_ptr */ if (list_empty(&lbat_hv_list)) { cur_hv_ptr = NULL; goto out; } cur_hv_ptr = list_first_entry(&lbat_hv_list, struct lbat_thd_t, list); regmap_update_bits(regmap, lbat_regs->volt_max.addr, lbat_regs->volt_max.mask, VOLT_TO_RAW(cur_hv_ptr->thd_volt)); out: lbat_irq_disable(); udelay(200); lbat_irq_enable(); mutex_unlock(&lbat_mutex); return IRQ_HANDLED; } static irqreturn_t bat_l_int_handler(int irq, void *data) { struct lbat_user *user; if (cur_lv_ptr == NULL) { lbat_min_en_setting(0); return IRQ_NONE; } mutex_lock(&lbat_mutex); pr_info("[%s] cur_thd_volt=%d\n", __func__, cur_lv_ptr->thd_volt); user = cur_lv_ptr->user; list_del_init(&cur_lv_ptr->list); if (user->lv_deb_times) { user->deb_cnt = 0; user->deb_thd_ptr = cur_lv_ptr; mod_timer(&user->deb_timer, jiffies + msecs_to_jiffies(user->lv_deb_prd)); } else { user->callback(cur_lv_ptr->thd_volt); lbat_set_next_thd(user, cur_lv_ptr); } /* Since cur_lv_ptr is removed, assign new thd for cur_lv_ptr */ if (list_empty(&lbat_lv_list)) { cur_lv_ptr = NULL; goto out; } cur_lv_ptr = list_first_entry(&lbat_lv_list, struct lbat_thd_t, list); regmap_update_bits(regmap, lbat_regs->volt_min.addr, lbat_regs->volt_min.mask, VOLT_TO_RAW(cur_lv_ptr->thd_volt)); out: lbat_irq_disable(); udelay(200); lbat_irq_enable(); mutex_unlock(&lbat_mutex); return IRQ_HANDLED; } static int pmic_lbat_service_probe(struct platform_device *pdev) { int ret, irq; unsigned int val; struct device_node *np; struct mt6397_chip *chip; chip = dev_get_drvdata(pdev->dev.parent); regmap = chip->regmap; lbat_regs = of_device_get_match_data(&pdev->dev); /* Selects debounce as 8 */ val = DEF_DEBT_MAX_SEL << (ffs(lbat_regs->debt_max.mask) - 1); regmap_update_bits(regmap, lbat_regs->debt_max.addr, lbat_regs->debt_max.mask, val); /* Selects debounce as 1 */ val = DEF_DEBT_MIN_SEL << (ffs(lbat_regs->debt_min.mask) - 1); regmap_update_bits(regmap, lbat_regs->debt_min.addr, lbat_regs->debt_min.mask, val); /* Set LBAT_PRD as 15ms */ val = DEF_DET_PRD_SEL << (ffs(lbat_regs->det_prd.mask) - 1); regmap_update_bits(regmap, lbat_regs->det_prd.addr, lbat_regs->det_prd.mask, val); irq = platform_get_irq_byname(pdev, "bat_h"); if (irq < 0) { dev_notice(&pdev->dev, "failed to get bat_h irq, ret=%d\n", irq); return irq; } ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, bat_h_int_handler, IRQF_TRIGGER_NONE, "bat_h", NULL); if (ret < 0) dev_notice(&pdev->dev, "request bat_h irq fail\n"); irq = platform_get_irq_byname(pdev, "bat_l"); if (irq < 0) { dev_notice(&pdev->dev, "failed to get bat_l irq, ret=%d\n", irq); return irq; } ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, bat_l_int_handler, IRQF_TRIGGER_NONE, "bat_l", NULL); if (ret < 0) dev_notice(&pdev->dev, "request bat_l irq fail\n"); lbat_wq = create_singlethread_workqueue("lbat_service"); /* get LBAT r_ratio */ np = of_find_node_by_name(pdev->dev.parent->of_node, "batadc"); if (!np) { dev_notice(&pdev->dev, "get batadc node fail\n"); r_ratio[0] = DEF_R_RATIO_0; r_ratio[1] = DEF_R_RATIO_1; return 0; } ret = of_property_read_u32_array(np, "resistance-ratio", r_ratio, 2); dev_info(&pdev->dev, "r_ratio = %d/%d\n", r_ratio[0], r_ratio[1]); return ret; } static int __maybe_unused lbat_service_suspend(struct device *d) { lbat_irq_disable(); return 0; } static int __maybe_unused lbat_service_resume(struct device *d) { lbat_irq_enable(); return 0; } static SIMPLE_DEV_PM_OPS(lbat_service_pm_ops, lbat_service_suspend, lbat_service_resume); static const struct of_device_id lbat_service_of_match[] = { { .compatible = "mediatek,mt6359-lbat_service", .data = &mt6359_lbat_regs, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, lbat_service_of_match); static struct platform_driver pmic_lbat_service_driver = { .driver = { .name = "pmic_lbat_service", .of_match_table = lbat_service_of_match, .pm = &lbat_service_pm_ops, }, .probe = pmic_lbat_service_probe, }; module_platform_driver(pmic_lbat_service_driver);