// SPDX-License-Identifier: GPL-2.0 /* * Copyright (c) 2020 MediaTek Inc. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "apu_log.h" #include "apusys_power_ctl.h" #include "apusys_power_cust.h" #include "apusys_power_debug.h" #include "apu_platform_resource.h" #include "hal_config_power.h" #include "apu_power_api.h" #include "pmic_api_buck.h" #include "apusys_power_rule_check.h" // #include "apusys_dbg.h" #include "apusys_power.h" #ifdef APUPWR_TAG_TP #include "apu_power_tag.h" #endif int g_pwr_log_level = APUSYS_PWR_LOG_ERR; int g_pm_procedure; int power_on_off_stress; static int apu_power_counter; static int apusys_power_broken; static uint64_t power_info_id; static uint8_t power_info_force_print; bool apusys_power_check(void) { #if defined(CONFIG_MACH_MT6885) char *pwr_ptr; bool pwr_status = true; pwr_ptr = strstr(saved_command_line, "apusys_status=normal"); if (pwr_ptr == 0) { pwr_status = false; LOG_ERR("apusys power disable !!, pwr_status=%d\n", pwr_status); } LOG_INF("apusys power check, pwr_status=%d\n", pwr_status); return pwr_status; #else return true; #endif } EXPORT_SYMBOL(apusys_power_check); struct power_device { enum DVFS_USER dev_usr; int is_power_on; struct platform_device *pdev; struct list_head list; }; struct power_callback_device { enum POWER_CALLBACK_USER power_callback_usr; void (*power_on_callback)(void *para); void (*power_off_callback)(void *para); struct list_head list; }; static LIST_HEAD(power_device_list); static LIST_HEAD(power_callback_device_list); static struct mutex power_device_list_mtx; static struct mutex power_ctl_mtx; static struct mutex power_opp_mtx; static spinlock_t power_info_lock; static int power_callback_counter; static struct task_struct *power_task_handle; static uint64_t timestamp; static struct hal_param_init_power init_power_data; static struct workqueue_struct *wq; static void d_work_power_info_func(struct work_struct *work); static void d_work_power_init_func(struct work_struct *work); static DECLARE_WORK(d_work_power_info, d_work_power_info_func); static DECLARE_WORK(d_work_power_init, d_work_power_init_func); struct delayed_work d_work_power; #ifdef CONFIG_PM_SLEEP struct wakeup_source *pwr_wake_lock; #endif static void apu_pwr_wake_lock(void) { #ifdef CONFIG_PM_SLEEP __pm_stay_awake(pwr_wake_lock); #endif } static void apu_pwr_wake_unlock(void) { #ifdef CONFIG_PM_SLEEP __pm_relax(pwr_wake_lock); #endif } static void apu_pwr_wake_init(void) { #ifdef CONFIG_PM_SLEEP pwr_wake_lock = wakeup_source_register(NULL, "apupwr_wakelock"); if (IS_ERR_OR_NULL(pwr_wake_lock)) pr_info("%s: wakeup_source_register fail\n", __func__); #endif } /** * print_time() - Brief description of print_time. * @ts: nanosecond * @buf: buffer to put [%5lu.%06lu] * * Transfer nanoseconds to format as [second.micro_second]. * * Return: length of valid string **/ static size_t print_time(u64 ts, char *buf) { unsigned long rem_nsec; rem_nsec = do_div(ts, 1000000000); if (!buf) return snprintf(NULL, 0, "[%5lu.000000] ", (unsigned long)ts); return sprintf(buf, "[%5lu.%06lu] ", (unsigned long)ts, rem_nsec / 1000); } uint64_t apu_get_power_info(int force) { uint64_t ret = 0; spin_lock(&power_info_lock); power_info_id = sched_clock(); ret = power_info_id; power_info_force_print = force; spin_unlock(&power_info_lock); queue_work(wq, &d_work_power_info); return ret; } EXPORT_SYMBOL(apu_get_power_info); void apu_power_reg_dump(void) { mutex_lock(&power_ctl_mtx); // keep 26M vcore clk make we can dump reg directly hal_config_power(PWR_CMD_REG_DUMP, VPU0, NULL); mutex_unlock(&power_ctl_mtx); } EXPORT_SYMBOL(apu_power_reg_dump); void apu_set_vcore_boost(bool enable) { } EXPORT_SYMBOL(apu_set_vcore_boost); void apu_qos_set_vcore(int target_volt) { #ifdef MTK_FPGA_PORTING LOG_WRN("%s FPGA porting bypass DVFS\n", __func__); #else #if !BYPASS_DVFS_CTL mutex_lock(&power_opp_mtx); #if SUPPORT_VCORE_TO_IPUIF // set opp value apusys_set_apu_vcore(target_volt); #endif // change regulator and clock if need wake_up_process(power_task_handle); mutex_unlock(&power_opp_mtx); #else LOG_WRN("%s bypass since not support DVFS\n", __func__); #endif #endif } EXPORT_SYMBOL(apu_qos_set_vcore); static struct power_device *find_out_device_by_user(enum DVFS_USER user) { struct power_device *pwr_dev = NULL; if (!list_empty(&power_device_list)) { list_for_each_entry(pwr_dev, &power_device_list, list) { if (pwr_dev && pwr_dev->dev_usr == user) return pwr_dev; } } else { LOG_ERR("%s empty list\n", __func__); } return NULL; } bool apu_get_power_on_status(enum DVFS_USER user) { bool power_on_status; struct power_device *pwr_dev = find_out_device_by_user(user); if (pwr_dev == NULL) return false; power_on_status = pwr_dev->is_power_on; return power_on_status; } EXPORT_SYMBOL(apu_get_power_on_status); #if !BYPASS_POWER_CTL static void power_callback_caller(int power_on) { struct power_callback_device *pwr_dev = NULL; LOG_DBG("%s begin (%d)\n", __func__, power_on); if (!list_empty(&power_callback_device_list)) { list_for_each_entry(pwr_dev, &power_callback_device_list, list) { LOG_DBG("%s calling %d in state %d\n", __func__, pwr_dev->power_callback_usr, power_on); if (power_on) { if (pwr_dev && pwr_dev->power_on_callback) pwr_dev->power_on_callback(NULL); } else { if (pwr_dev && pwr_dev->power_off_callback) pwr_dev->power_off_callback(NULL); } } } LOG_DBG("%s end (%d)\n", __func__, power_on); } #endif static struct power_callback_device* find_out_callback_device_by_user(enum POWER_CALLBACK_USER user) { struct power_callback_device *pwr_dev = NULL; if (!list_empty(&power_callback_device_list)) { list_for_each_entry(pwr_dev, &power_callback_device_list, list) { if (pwr_dev && pwr_dev->power_callback_usr == user) return pwr_dev; } } else { LOG_ERR("%s empty list\n", __func__); } return NULL; } int apu_device_power_suspend(enum DVFS_USER user, int is_suspend) { int ret = 0; #if !BYPASS_POWER_OFF char time_stmp[32]; struct power_device *pwr_dev = NULL; #if TIME_PROFILING struct profiling_timestamp power_profiling; power_profiling.begin = sched_clock(); #endif pwr_dev = find_out_device_by_user(user); if (pwr_dev == NULL) { LOG_ERR("%s fail, dev of user %d is NULL\n", __func__, user); return -1; } LOG_DBG("%s waiting for lock, user = %d\n", __func__, user); mutex_lock(&power_ctl_mtx); if (is_suspend) g_pm_procedure = 1; if (apusys_power_broken) { mutex_unlock(&power_ctl_mtx); hal_config_power(PWR_CMD_DUMP_FAIL_STATE, VPU0, NULL); LOG_ERR( "APUPWR_BROKEN, user:%d fail to pwr off, is_suspend:%d\n", user, is_suspend); return -ENODEV; } if (pwr_dev->is_power_on == 0) { mutex_unlock(&power_ctl_mtx); LOG_ERR( "APUPWR_OFF_FAIL, not allow user:%d to pwr off twice, is_suspend:%d\n", user, is_suspend); return -ECANCELED; } LOG_DBG("%s for user:%d, cnt:%d, is_suspend:%d\n", __func__, user, power_callback_counter, is_suspend); if (!g_pm_procedure) apu_pwr_wake_lock(); power_callback_counter--; if (power_callback_counter == 0) { // call passive power off function list #if !BYPASS_POWER_CTL power_callback_caller(0); #endif } // for debug // dump_stack(); #if !BYPASS_POWER_CTL ret = apusys_power_off(user); #endif if (!ret) { pwr_dev->is_power_on = 0; } else { apusys_power_broken = 1; power_callback_counter++; } if (!g_pm_procedure) apu_pwr_wake_unlock(); mutex_unlock(&power_ctl_mtx); // for pwr saving, get pwr info in case pwr ctl fail or enable debug log if (g_pwr_log_level >= APUSYS_PWR_LOG_WARN || ret) { /* prepare time stamp format */ memset(time_stmp, 0, sizeof(time_stmp)); print_time(apu_get_power_info(0), time_stmp); LOG_PM( "%s for user:%d, ret:%d, cnt:%d, is_suspend:%d, info_id: %s\n", __func__, user, ret, power_callback_counter, is_suspend, ret ? 0 : time_stmp); } #else LOG_WRN("%s by user:%d bypass\n", __func__, user); #endif // BYPASS_POWER_OFF if (ret) { hal_config_power(PWR_CMD_DUMP_FAIL_STATE, VPU0, NULL); #ifndef APUSYS_POWER_BRINGUP // apusys_reg_dump(); #endif apu_aee_warn("APUPWR_OFF_FAIL", "user:%d, is_suspend:%d\n", user, is_suspend); return -ENODEV; } else { #if TIME_PROFILING power_profiling.end = sched_clock(); apu_profiling(&power_profiling, __func__); #endif return 0; } } EXPORT_SYMBOL(apu_device_power_suspend); int apu_device_power_off(enum DVFS_USER user) { return apu_device_power_suspend(user, 0); } EXPORT_SYMBOL(apu_device_power_off); int apu_device_power_on(enum DVFS_USER user) { struct power_device *pwr_dev = NULL; int ret = 0; char time_stmp[32]; #if TIME_PROFILING struct profiling_timestamp power_profiling; power_profiling.begin = sched_clock(); #endif pwr_dev = find_out_device_by_user(user); if (pwr_dev == NULL) { LOG_ERR("%s fail, dev of user %d is NULL\n", __func__, user); return -1; } LOG_DBG("%s waiting for lock, user:%d\n", __func__, user); mutex_lock(&power_ctl_mtx); g_pm_procedure = 0; if (apusys_power_broken) { mutex_unlock(&power_ctl_mtx); hal_config_power(PWR_CMD_DUMP_FAIL_STATE, VPU0, NULL); LOG_ERR("APUPWR_BROKEN, user:%d fail to pwr on\n", user); return -ENODEV; } if (pwr_dev->is_power_on == 1) { mutex_unlock(&power_ctl_mtx); #if !BYPASS_POWER_OFF LOG_ERR("APUPWR_ON_FAIL, not allow user:%d to pwr on twice\n", user); return -ECANCELED; #else LOG_WRN("%s by user:%d bypass\n", __func__, user); return 0; #endif } LOG_DBG("%s for user:%d, cnt:%d\n", __func__, user, power_callback_counter); if (!g_pm_procedure) apu_pwr_wake_lock(); // for debug // dump_stack(); #if !BYPASS_POWER_CTL ret = apusys_power_on(user); #endif if (!ret) { pwr_dev->is_power_on = 1; power_callback_counter++; } else { apusys_power_broken = 1; } if (power_callback_counter == 1) { // call passive power on function list #if !BYPASS_POWER_CTL power_callback_caller(1); #endif } if (!g_pm_procedure) apu_pwr_wake_unlock(); mutex_unlock(&power_ctl_mtx); // for pwr saving, get pwr info in case pwr ctl fail or enable debug log if (g_pwr_log_level >= APUSYS_PWR_LOG_WARN || ret) { /* prepare time stamp format */ memset(time_stmp, 0, sizeof(time_stmp)); print_time(apu_get_power_info(0), time_stmp); LOG_PM("%s for user:%d, ret:%d, cnt:%d, info_id: %s\n", __func__, user, ret, power_callback_counter, ret ? 0 : time_stmp); } if (ret) { hal_config_power(PWR_CMD_DUMP_FAIL_STATE, VPU0, NULL); #ifndef APUSYS_POWER_BRINGUP // apusys_reg_dump(); #endif apu_aee_warn("APUPWR_ON_FAIL", "user:%d\n", user); return -ENODEV; } else { #if TIME_PROFILING power_profiling.end = sched_clock(); apu_profiling(&power_profiling, __func__); #endif return 0; } } EXPORT_SYMBOL(apu_device_power_on); int apu_power_device_register(enum DVFS_USER user, struct platform_device *pdev) { struct power_device *pwr_dev = NULL; pwr_dev = kzalloc(sizeof(struct power_device), GFP_KERNEL); if (pwr_dev == NULL) { LOG_ERR("%s fail in dvfs user %d\n", __func__, user); return -1; } pwr_dev->dev_usr = user; pwr_dev->is_power_on = 0; pwr_dev->pdev = pdev; /* add to device link list */ mutex_lock(&power_device_list_mtx); list_add_tail(&pwr_dev->list, &power_device_list); // call apusys_power_init in probe #if 0 if (apu_power_counter == 0) { // prepare clock and get regulator handle apusys_power_init(user, (void *)&init_power_data); } #endif apu_power_counter++; mutex_unlock(&power_device_list_mtx); LOG_INF("%s add dvfs user %d success\n", __func__, user); return 0; } EXPORT_SYMBOL(apu_power_device_register); int apu_power_callback_device_register(enum POWER_CALLBACK_USER user, void (*power_on_callback)(void *para), void (*power_off_callback)(void *para)) { struct power_callback_device *pwr_dev = NULL; pwr_dev = kzalloc(sizeof(struct power_callback_device), GFP_KERNEL); if (pwr_dev == NULL) { LOG_ERR("%s fail in power callback user %d\n", __func__, user); return -1; } pwr_dev->power_callback_usr = user; pwr_dev->power_on_callback = power_on_callback; pwr_dev->power_off_callback = power_off_callback; /* add to device link list */ mutex_lock(&power_device_list_mtx); list_add_tail(&pwr_dev->list, &power_callback_device_list); mutex_unlock(&power_device_list_mtx); LOG_INF("%s add power callback user %d success\n", __func__, user); return 0; } EXPORT_SYMBOL(apu_power_callback_device_register); void apu_power_device_unregister(enum DVFS_USER user) { struct power_device *pwr_dev = find_out_device_by_user(user); mutex_lock(&power_device_list_mtx); apu_power_counter--; if (apu_power_counter == 0) apusys_power_uninit(user); /* remove from device link list */ list_del_init(&pwr_dev->list); kfree(pwr_dev); mutex_unlock(&power_device_list_mtx); LOG_INF("%s remove dvfs user %d success\n", __func__, user); } EXPORT_SYMBOL(apu_power_device_unregister); void apu_power_callback_device_unregister(enum POWER_CALLBACK_USER user) { struct power_callback_device *pwr_dev = find_out_callback_device_by_user(user); mutex_lock(&power_device_list_mtx); /* remove from device link list */ list_del_init(&pwr_dev->list); kfree(pwr_dev); mutex_unlock(&power_device_list_mtx); LOG_INF("%s remove power callback user %d success\n", __func__, user); } EXPORT_SYMBOL(apu_power_callback_device_unregister); static void d_work_power_info_func(struct work_struct *work) { struct apu_power_info info = {0}; spin_lock(&power_info_lock); info.id = power_info_id; info.force_print = power_info_force_print; spin_unlock(&power_info_lock); info.type = 1; hal_config_power(PWR_CMD_GET_POWER_INFO, VPU0, &info); } #if DEFAULT_POWER_ON static void default_power_on_func(void) { int dev_id = 0; LOG_ERR("### apusys power on all device ###\n"); for (dev_id = 0 ; dev_id < APUSYS_DVFS_USER_NUM ; dev_id++) { apu_power_counter++; apusys_power_on(dev_id); udelay(200); } apu_get_power_info(1); } #endif static void d_work_power_init_func(struct work_struct *work) { int ret = 0; ret = apusys_power_init(VPU0, (void *)&init_power_data); if (ret == -EPROBE_DEFER) { INIT_DEFERRABLE_WORK(&d_work_power, d_work_power_init_func); queue_delayed_work(wq, &d_work_power, msecs_to_jiffies(100)); return; } #if DEFAULT_POWER_ON default_power_on_func(); #endif // DEFAULT_POWER_ON } static int apusys_power_task(void *arg) { int keep_loop = 0; struct apu_power_info info = {0}; unsigned long rem_nsec; set_current_state(TASK_INTERRUPTIBLE); LOG_INF("%s first time wakeup and enter sleep now\n", __func__); schedule(); for (;;) { if (kthread_should_stop()) break; #if 1 mutex_lock(&power_opp_mtx); keep_loop = apusys_check_opp_change(); mutex_unlock(&power_opp_mtx); #else keep_loop = 1; mdelay(1000); #endif if (keep_loop) { timestamp = sched_clock(); info.id = timestamp; info.type = 0; // call dvfs API and bring timestamp to id rem_nsec = do_div(timestamp, 1000000000); LOG_INF("%s call DVFS handler, id:[%5lu.%06lu]\n", __func__, (unsigned long)timestamp, rem_nsec / 1000); apusys_dvfs_policy(info.id); hal_config_power(PWR_CMD_GET_POWER_INFO, VPU0, &info); #if SUPPORT_VCORE_TO_IPUIF apusys_ipuif_opp_change(); #endif #if DVFS_ASSERTION_CHECK apu_power_assert_check(&info); #endif #ifdef APUPWR_TASK_DEBOUNCE task_debounce(); #endif } else { LOG_DBG("%s enter sleep\n", __func__); set_current_state(TASK_INTERRUPTIBLE); schedule(); } } LOG_WRN("%s task stop\n", __func__); return 0; } void apu_device_set_opp(enum DVFS_USER user, uint8_t opp) { #ifdef MTK_FPGA_PORTING LOG_WRN("%s FPGA porting bypass DVFS\n", __func__); #else #if !BYPASS_DVFS_CTL if (user >= 0 && user < APUSYS_DVFS_USER_NUM && opp < APUSYS_MAX_NUM_OPPS) { mutex_lock(&power_opp_mtx); // set opp value apusys_set_opp(user, opp); // change regulator and clock if need wake_up_process(power_task_handle); mutex_unlock(&power_opp_mtx); } #else LOG_WRN("%s bypass since not support DVFS\n", __func__); #endif #endif } EXPORT_SYMBOL(apu_device_set_opp); /** * apu_profiling() - Brief description of apu_profiling. * @profile: struct profiling_timestamp with begin/end timestamp. * @tag: the string need to print prefix. * * Calcualte abs(end-begin) and show result in unit of us with "tag" prefix. * * Return: void **/ void apu_profiling(struct profiling_timestamp *profile, const char *tag) { u64 nanosec = 0; u64 time = 0; time = abs(profile->end - profile->begin), nanosec = do_div(time, 1000000000); pr_info("%s: %s take %lu (us)\n", __func__, tag, ((unsigned long)nanosec / 1000)); } EXPORT_SYMBOL(apu_profiling); void event_trigger_dvfs_policy(void) { wake_up_process(power_task_handle); } static int apu_power_probe(struct platform_device *pdev) { int ret = 0; int err = 0; if (!apusys_power_check()) return 0; ret = init_platform_resource(pdev, &init_power_data); if (ret) goto err_exit; wq = create_workqueue("apu_pwr_drv_wq"); if (IS_ERR(wq)) { LOG_ERR("%s create power driver wq fail\n", __func__); goto err_exit; } power_task_handle = kthread_create(apusys_power_task, (void *)NULL, "apu_pwr_policy"); if (IS_ERR(power_task_handle)) { LOG_ERR("%s create power task fail\n", __func__); goto err_exit; } mutex_init(&power_device_list_mtx); mutex_init(&power_ctl_mtx); mutex_init(&power_opp_mtx); spin_lock_init(&power_info_lock); apu_pwr_wake_init(); queue_work(wq, &d_work_power_init); queue_work(wq, &d_work_power_info); wake_up_process(power_task_handle); #if AUTO_BUCK_OFF_SUSPEND // buck auto power off in suspend pmic_buck_vproc1_lp(SRCLKEN0, 1, 1, HW_OFF); pmic_buck_vproc2_lp(SRCLKEN0, 1, 1, HW_OFF); pmic_ldo_vsram_md_lp(SRCLKEN0, 1, 1, HW_OFF); #endif #if AUTO_BUCK_OFF_DEEPIDLE // buck auto power off in deeep idle pmic_buck_vproc1_lp(SRCLKEN2, 1, 1, HW_OFF); pmic_buck_vproc2_lp(SRCLKEN2, 1, 1, HW_OFF); pmic_ldo_vsram_md_lp(SRCLKEN2, 1, 1, HW_OFF); #endif apusys_power_debugfs_init(); #if defined(CONFIG_MACH_MT6877) apusys_power_create_procfs(); #endif #ifdef APUPWR_TAG_TP apupwr_init_drv_tags(); #endif return 0; err_exit: if (power_task_handle != NULL) { kfree(power_task_handle); power_task_handle = NULL; } if (wq != NULL) { flush_workqueue(wq); destroy_workqueue(wq); kfree(wq); wq = NULL; } return err; } static int apu_power_suspend(struct platform_device *pdev, pm_message_t mesg) { struct hal_param_pm pm; enum DVFS_USER user; LOG_PM("%s begin\n", __func__); if (power_on_off_stress) { LOG_PM("%s, power_on_off_stress: %d\n", __func__, power_on_off_stress); for (user = VPU0; user < APUSYS_DVFS_USER_NUM; user++) apu_device_power_suspend(user, 1); } mutex_lock(&power_ctl_mtx); pm.is_suspend = 1; hal_config_power(PWR_CMD_PM_HANDLER, VPU0, &pm); mutex_unlock(&power_ctl_mtx); LOG_PM("%s end\n", __func__); return 0; } static int apu_power_resume(struct platform_device *pdev) { struct hal_param_pm pm; enum DVFS_USER user; LOG_PM("%s begin\n", __func__); mutex_lock(&power_ctl_mtx); pm.is_suspend = 0; hal_config_power(PWR_CMD_PM_HANDLER, VPU0, &pm); g_pm_procedure = 0; mutex_unlock(&power_ctl_mtx); if (power_on_off_stress) { LOG_PM("%s, power_on_off_stress: %d\n", __func__, power_on_off_stress); for (user = VPU0; user < APUSYS_DVFS_USER_NUM; user++) apu_device_power_on(user); } LOG_PM("%s end\n", __func__); return 0; } static int apu_power_remove(struct platform_device *pdev) { apusys_power_debugfs_exit(); #ifdef APUPWR_TAG_TP apupwr_exit_drv_tags(); #endif if (power_task_handle) kthread_stop(power_task_handle); mutex_destroy(&power_opp_mtx); mutex_destroy(&power_ctl_mtx); mutex_destroy(&power_device_list_mtx); if (wq) { flush_workqueue(wq); destroy_workqueue(wq); kfree(wq); wq = NULL; } return 0; } static const struct of_device_id apu_power_of_match[] = { { .compatible = "mediatek,apusys_power" }, { /* end of list */}, }; static struct platform_driver apu_power_driver = { .probe = apu_power_probe, .remove = apu_power_remove, .suspend = apu_power_suspend, .resume = apu_power_resume, .driver = { .name = "apusys_power", .of_match_table = apu_power_of_match, }, }; static int __init apu_power_drv_init(void) { return platform_driver_register(&apu_power_driver); } module_init(apu_power_drv_init) static void __exit apu_power_drv_exit(void) { platform_driver_unregister(&apu_power_driver); } module_exit(apu_power_drv_exit) MODULE_LICENSE("GPL v2"); MODULE_DESCRIPTION("apu power driver");