/* * Copyright (c) 2018 Samsung Electronics Co., Ltd. * http://www.samsung.com/ * * HAFM-TB(AFM with HIU TB) support * Auther : PARK CHOONGHOON (choong.park@samsung.com) * 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 #include #include #include #include #include "exynos-hiu.h" #include "../../cpufreq/exynos-ff.h" #include "../../cpufreq/exynos-acme.h" static struct exynos_hiu_data *data; static void hiu_stats_create_table(struct cpufreq_policy *policy); #define POLL_PERIOD 100 /****************************************************************/ /* HIU HELPER FUNCTION */ /****************************************************************/ static unsigned int hiu_get_freq_level(unsigned int freq) { int level; struct hiu_stats *stats = data->stats; if (unlikely(!stats)) return 0; for (level = 0; level < stats->last_level; level++) if (stats->freq_table[level] == freq) return level + data->level_offset; return -EINVAL; } static unsigned int hiu_get_power_budget(unsigned int freq) { return data->sw_pbl; } static void hiu_update_reg(int offset, int mask, int shift, unsigned int val) { unsigned int reg_val; reg_val = __raw_readl(data->base + offset); reg_val &= ~(mask << shift); reg_val |= val << shift; __raw_writel(reg_val, data->base + offset); } static unsigned int hiu_read_reg(int offset, int mask, int shift) { unsigned int reg_val; reg_val = __raw_readl(data->base + offset); return (reg_val >> shift) & mask; } static unsigned int hiu_get_act_dvfs(void) { return hiu_read_reg(HIUTOPCTL1, ACTDVFS_MASK, ACTDVFS_SHIFT); } static void hiu_control_err_interrupts(int enable) { if (enable) hiu_update_reg(HIUTOPCTL1, ENB_ERR_INTERRUPTS_MASK, 0, ENB_ERR_INTERRUPTS_MASK); else hiu_update_reg(HIUTOPCTL1, ENB_ERR_INTERRUPTS_MASK, 0, 0); } static void hiu_control_mailbox(int enable) { hiu_update_reg(HIUTOPCTL1, ENB_SR1INTR_MASK, ENB_SR1INTR_SHIFT, !!enable); hiu_update_reg(HIUTOPCTL1, ENB_ACPM_COMM_MASK, ENB_ACPM_COMM_SHIFT, !!enable); } static void hiu_set_limit_dvfs(unsigned int freq) { unsigned int level; level = hiu_get_freq_level(freq); hiu_update_reg(HIUTOPCTL2, LIMITDVFS_MASK, LIMITDVFS_SHIFT, level); } static void hiu_set_tb_dvfs(unsigned int freq) { unsigned int level; level = hiu_get_freq_level(freq); hiu_update_reg(HIUTBCTL, TBDVFS_MASK, TBDVFS_SHIFT, level); } static void hiu_control_tb(int enable) { hiu_update_reg(HIUTBCTL, TB_ENB_MASK, TB_ENB_SHIFT, !!enable); } static void hiu_control_pc(int enable) { hiu_update_reg(HIUTBCTL, PC_DISABLE_MASK, PC_DISABLE_SHIFT, !enable); } static void hiu_set_boost_level_inc(void) { unsigned int inc; struct device_node *dn = data->dn; if (!of_property_read_u32(dn, "bl1-inc", &inc)) hiu_update_reg(HIUTBCTL, B1_INC_MASK, B1_INC_SHIFT, inc); if (!of_property_read_u32(dn, "bl2-inc", &inc)) hiu_update_reg(HIUTBCTL, B2_INC_MASK, B2_INC_SHIFT, inc); if (!of_property_read_u32(dn, "bl3-inc", &inc)) hiu_update_reg(HIUTBCTL, B3_INC_MASK, B3_INC_SHIFT, inc); } static void hiu_set_tb_ps_cfg_each(int index, unsigned int cfg_val) { int offset; offset = HIUTBPSCFG_BASE + index * HIUTBPSCFG_OFFSET; hiu_update_reg(offset, HIUTBPSCFG_MASK, 0, cfg_val); } static int hiu_set_tb_ps_cfg(void) { int size, index; unsigned int val; struct hiu_cfg *table; struct device_node *dn = data->dn; size = of_property_count_u32_elems(dn, "config-table"); if (size < 0) return size; table = kzalloc(sizeof(struct hiu_cfg) * size / 4, GFP_KERNEL); if (!table) return -ENOMEM; of_property_read_u32_array(dn, "config-table", (unsigned int *)table, size); for (index = 0; index < size / 4; index++) { val = 0; val |= table[index].power_borrowed << PB_SHIFT; val |= table[index].boost_level << BL_SHIFT; val |= table[index].power_budget_limit << PBL_SHIFT; val |= table[index].power_threshold_inc << TBPWRTHRESH_INC_SHIFT; hiu_set_tb_ps_cfg_each(index, val); } kfree(table); return 0; } static bool check_hiu_sr1_irq_pending(void) { return !!hiu_read_reg(HIUTOPCTL1, HIU_MBOX_RESPONSE_MASK, SR1INTR_SHIFT); } static void clear_hiu_sr1_irq_pending(void) { hiu_update_reg(HIUTOPCTL1, HIU_MBOX_RESPONSE_MASK, SR1INTR_SHIFT, 0); } static bool check_hiu_mailbox_err_pending(void) { return !!hiu_read_reg(HIUTOPCTL1, HIU_MBOX_ERR_MASK, HIU_MBOX_ERR_SHIFT); } static unsigned int get_hiu_mailbox_err(void) { return hiu_read_reg(HIUTOPCTL1, HIU_MBOX_ERR_MASK, HIU_MBOX_ERR_SHIFT); } static void hiu_mailbox_err_handler(void) { unsigned int err, val; err = get_hiu_mailbox_err(); if (err & SR1UXPERR_MASK) pr_err("exynos-hiu: unexpected error occurs\n"); if (err & SR1SNERR_MASK) { val = __raw_readl(data->base + HIUTOPCTL2); val = (val >> SEQNUM_SHIFT) & SEQNUM_MASK; pr_err("exynos-hiu: erroneous sequence num %d\n", val); } if (err & SR1TIMEOUT_MASK) pr_err("exynos-hiu: TIMEOUT on SR1 write\n"); if (err & SR0RDERR_MASK) pr_err("exynos-hiu: SR0 read twice or more\n"); } static bool check_hiu_req_freq_updated(unsigned int req_freq) { unsigned int cur_level, cur_freq; cur_level = hiu_get_act_dvfs(); cur_freq = data->stats->freq_table[cur_level - data->level_offset]; /* * If req_freq == boost_threshold, HIU could request turbo boost * That's why in case of req_freq == boost_threshold, * requested frequency update is consdered as done, * if act_dvfs is larger than or equal to boost threshold */ if (req_freq == data->boost_threshold) return cur_freq >= data->boost_threshold; return cur_freq == req_freq; } static bool check_hiu_normal_req_done(unsigned int req_freq) { return check_hiu_sr1_irq_pending() && check_hiu_req_freq_updated(req_freq); } static bool check_hiu_need_register_restore(void) { return !hiu_read_reg(HIUTOPCTL1, ENB_SR1INTR_MASK, ENB_SR1INTR_SHIFT); } static int request_dvfs_on_sr0(unsigned int req_freq) { unsigned int val, level, budget; /* Get dvfs level */ level = hiu_get_freq_level(req_freq); if (level < 0) return -EINVAL; /* Get power budget */ budget = hiu_get_power_budget(req_freq); if (budget < 0) return -EINVAL; /* write REQDVFS & REQPBL to HIU SFR */ val = __raw_readl(data->base + HIUTOPCTL2); val &= ~(REQDVFS_MASK << REQDVFS_SHIFT | REQPBL_MASK << REQPBL_SHIFT); val |= (level << REQDVFS_SHIFT | budget << REQPBL_SHIFT); __raw_writel(val, data->base + HIUTOPCTL2); return 0; } /****************************************************************/ /* HIU API */ /****************************************************************/ static int hiu_sr1_check_loop(void *unused); static void __exynos_hiu_update_data(struct cpufreq_policy *policy); int exynos_hiu_set_freq(unsigned int id, unsigned int req_freq) { bool need_update_cur_freq = true; if (unlikely(!data)) return -ENODEV; if (!data->enabled) return -ENODEV; pr_debug("exynos-hiu: update data->cur_freq:%d\n", data->cur_freq); mutex_lock(&data->lock); if (check_hiu_need_register_restore()) __exynos_hiu_update_data(NULL); /* PM QoS could make req_freq bigger than boost_threshold */ if (req_freq >= data->boost_threshold){ /* * 1) If turbo boost is already activated * just update cur_freq and return. * 2) If not, req_freq should be boost_threshold; * DO NOT allow req_freq to be bigger than boost_threshold. */ if (data->cur_freq >= data->boost_threshold) { data->cur_freq = req_freq; mutex_unlock(&data->lock); return 0; } else { data->cur_freq = req_freq; req_freq = data->boost_threshold; need_update_cur_freq = false; } } /* Write req_freq on SR0 to request DVFS */ request_dvfs_on_sr0(req_freq); if (data->operation_mode == POLLING_MODE) { while (!check_hiu_normal_req_done(req_freq) && !check_hiu_mailbox_err_pending()) usleep_range(POLL_PERIOD, 2 * POLL_PERIOD); if (check_hiu_mailbox_err_pending()) { hiu_mailbox_err_handler(); BUG_ON(1); } if (need_update_cur_freq) data->cur_freq = req_freq; clear_hiu_sr1_irq_pending(); if (req_freq == data->boost_threshold && !data->boosting_activated) { data->boosting_activated = true; wake_up(&data->polling_wait); } } mutex_unlock(&data->lock); pr_debug("exynos-hiu: set REQDVFS to HIU : %ukHz\n", req_freq); return 0; } int exynos_hiu_get_freq(unsigned int id) { if (unlikely(!data)) return -ENODEV; return data->cur_freq; } int exynos_hiu_get_max_freq(void) { if (unlikely(!data)) return -1; return data->clipped_freq; } unsigned int exynos_pstate_get_boost_freq(int cpu) { if (!cpumask_test_cpu(cpu, &data->cpus)) return 0; return data->boost_max; } /****************************************************************/ /* HIU SR1 WRITE HANDLER */ /****************************************************************/ static void exynos_hiu_work(struct work_struct *work) { unsigned int boost_freq, level; struct cpufreq_policy *policy; struct hiu_stats *stats = data->stats; struct cpumask *mask; level = hiu_get_act_dvfs(); boost_freq = stats->freq_table[level - data->level_offset]; /* * Only when TB bit is set, this work callback is called. * However, while this callback is waiting to start, * well... turbo boost could be released. * So, acting frequency coulde be lower than turbo boost threshold. * This condition code is for treating that case. */ if (boost_freq < data->boost_threshold) goto done; policy = cpufreq_cpu_get(cpumask_first(cpu_coregroup_mask(0))); if (!policy) { pr_debug("Failed to get CPUFreq policy in HIU work\n"); goto done; } __cpufreq_driver_target(policy, boost_freq, CPUFREQ_RELATION_H | CPUFREQ_HW_DVFS_REQ); cpufreq_cpu_put(policy); done: data->hwidvfs_done = true; wake_up(&data->hwidvfs_wait); } static irqreturn_t exynos_hiu_irq_handler(int irq, void *id) { schedule_work_on(data->cpu, &data->work); return IRQ_HANDLED; } static bool hiu_need_hw_request(void) { unsigned int cur_level, cur_freq; cur_level = hiu_get_act_dvfs(); cur_freq = data->stats->freq_table[cur_level - data->level_offset]; return cur_freq >= data->boost_threshold; } static int hiu_sr1_check_loop(void *unused) { wait: wait_event(data->polling_wait, data->boosting_activated); poll: mutex_lock(&data->lock); if (data->cur_freq < data->boost_threshold) goto done; while (!check_hiu_sr1_irq_pending() && !check_hiu_mailbox_err_pending()) { mutex_unlock(&data->lock); usleep_range(POLL_PERIOD, 2 * POLL_PERIOD); mutex_lock(&data->lock); if (data->cur_freq < data->boost_threshold) goto done; } if (check_hiu_mailbox_err_pending()) { hiu_mailbox_err_handler(); BUG_ON(1); } if (hiu_need_hw_request()) { schedule_work_on(cpumask_first(cpu_coregroup_mask(0)), &data->work); clear_hiu_sr1_irq_pending(); mutex_unlock(&data->lock); wait_event(data->hwidvfs_wait, data->hwidvfs_done); data->hwidvfs_done = false; goto poll; } done: data->boosting_activated = false; mutex_unlock(&data->lock); goto wait; /* NEVER come here */ return 0; } /****************************************************************/ /* EXTERNAL EVENT HANDLER */ /****************************************************************/ static void __exynos_hiu_update_data(struct cpufreq_policy *policy) { /* Explicitly disable the whole HW */ /* ex) hiu_control_pc, tb(0), hiu_control_mailbox(0) */ /* Set dvfs limit and TB threshold */ hiu_set_limit_dvfs(data->clipped_freq); hiu_set_tb_dvfs(data->boost_threshold); /* Initialize TB level offset */ hiu_set_boost_level_inc(); /* Initialize TB power state config */ hiu_set_tb_ps_cfg(); /* Enable TB */ hiu_control_pc(data->pc_enabled); hiu_control_tb(data->tb_enabled); /* Enable error interrupts */ hiu_control_err_interrupts(1); /* Enable mailbox communication with ACPM */ hiu_control_mailbox(1); } static int exynos_hiu_update_data(struct cpufreq_policy *policy) { if (!cpumask_test_cpu(data->cpu, policy->cpus)) return 0; data->clipped_freq = data->boost_max; hiu_stats_create_table(policy); __exynos_hiu_update_data(policy); data->enabled = true; pr_info("exynos-hiu: HIU data structure update complete\n"); return 0; } static struct exynos_cpufreq_ready_block exynos_hiu_ready = { .update = exynos_hiu_update_data, }; static bool check_hiu_need_boost_thrott(void) { return data->cur_freq > data->boost_threshold && data->cur_freq > data->clipped_freq; } static int exynos_hiu_policy_callback(struct notifier_block *nb, unsigned long event, void *info) { struct cpufreq_policy *policy = info; if (policy->cpu != data->cpu) return NOTIFY_DONE; if (policy->max == data->clipped_freq) return NOTIFY_DONE; switch (event) { case CPUFREQ_NOTIFY: /* Note : MUST write LIMIT_DVFS to HIU SFR */ mutex_lock(&data->lock); if (policy->max >= data->boost_threshold) { data->clipped_freq = policy->max; hiu_set_limit_dvfs(data->clipped_freq); } mutex_unlock(&data->lock); pr_debug("exynos-hiu: update clipped freq:%d\n", data->clipped_freq); if (check_hiu_need_boost_thrott()) atomic_inc(&boost_throttling); break; default: ; } return NOTIFY_OK; } static struct notifier_block exynos_hiu_policy_notifier = { .notifier_call = exynos_hiu_policy_callback, }; static int exynos_hiu_transition_callback(struct notifier_block *nb, unsigned long event, void *info) { struct cpufreq_freqs *freq = info; int cpu = freq->cpu; if (cpu != data->cpu) return NOTIFY_DONE; if (event != CPUFREQ_POSTCHANGE) return NOTIFY_DONE; if (atomic_read(&boost_throttling) && data->cur_freq <= data->clipped_freq) { atomic_dec(&boost_throttling); } return NOTIFY_OK; } static struct notifier_block exynos_hiu_transition_notifier = { .notifier_call = exynos_hiu_transition_callback, .priority = INT_MIN, }; /****************************************************************/ /* SYSFS INTERFACE */ /****************************************************************/ static ssize_t hiu_enable_show(struct device *dev, struct device_attribute *devattr, char *buf) { return snprintf(buf, PAGE_SIZE, "%d\n", data->enabled); } static ssize_t hiu_enable_store(struct device *dev, struct device_attribute *devattr, const char *buf, size_t count) { unsigned int input; if (kstrtos32(buf, 10, &input)) return -EINVAL; return count; } static ssize_t hiu_boosted_show(struct device *dev, struct device_attribute *devattr, char *buf) { unsigned int boosted = hiu_read_reg(HIUTBCTL, BOOSTED_MASK, BOOSTED_SHIFT); return snprintf(buf, PAGE_SIZE, "%d\n", boosted); } static ssize_t hiu_boost_threshold_show(struct device *dev, struct device_attribute *devattr, char *buf) { return snprintf(buf, PAGE_SIZE, "%d\n", data->boost_threshold); } static ssize_t hiu_dvfs_limit_show(struct device *dev, struct device_attribute *devattr, char *buf) { unsigned int dvfs_limit = hiu_read_reg(HIUTOPCTL2, LIMITDVFS_MASK, LIMITDVFS_SHIFT); return snprintf(buf, PAGE_SIZE, "%d\n", dvfs_limit); } static ssize_t hiu_dvfs_limit_store(struct device *dev, struct device_attribute *devattr, const char *buf, size_t count) { unsigned int input; if (kstrtos32(buf, 10, &input)) return -EINVAL; hiu_update_reg(HIUTOPCTL2, LIMITDVFS_MASK, LIMITDVFS_SHIFT, input); return count; } static DEVICE_ATTR(enabled, 0644, hiu_enable_show, hiu_enable_store); static DEVICE_ATTR(boosted, 0444, hiu_boosted_show, NULL); static DEVICE_ATTR(boost_threshold, 0444, hiu_boost_threshold_show, NULL); static DEVICE_ATTR(dvfs_limit, 0644, hiu_dvfs_limit_show, hiu_dvfs_limit_store); static struct attribute *exynos_hiu_attrs[] = { &dev_attr_enabled.attr, &dev_attr_boosted.attr, &dev_attr_boost_threshold.attr, &dev_attr_dvfs_limit.attr, NULL, }; static struct attribute_group exynos_hiu_attr_group = { .name = "hiu", .attrs = exynos_hiu_attrs, }; /****************************************************************/ /* INITIALIZE EXYNOS HIU DRIVER */ /****************************************************************/ static int hiu_dt_parsing(struct device_node *dn) { const char *buf; int ret = 0; ret |= of_property_read_u32(dn, "operation-mode", &data->operation_mode); ret |= of_property_read_u32(dn, "boot-freq", &data->cur_freq); ret |= of_property_read_u32(dn, "boost-threshold", &data->boost_threshold); ret |= of_property_read_u32(dn, "boost-max", &data->boost_max); ret |= of_property_read_u32(dn, "sw-pbl", &data->sw_pbl); ret |= of_property_read_string(dn, "sibling-cpus", &buf); if (ret) return ret; if (of_property_read_bool(dn, "pc-enabled")) data->pc_enabled = true; if (of_property_read_bool(dn, "tb-enabled")) data->tb_enabled = true; cpulist_parse(buf, &data->cpus); cpumask_and(&data->cpus, &data->cpus, cpu_possible_mask); if (cpumask_weight(&data->cpus) == 0) return -ENODEV; data->cpu = cpumask_first(&data->cpus); return 0; } static void hiu_stats_create_table(struct cpufreq_policy *policy) { unsigned int i = 0, count = 0, alloc_size; struct hiu_stats *stats; struct cpufreq_frequency_table *pos, *table; table = policy->freq_table; if (unlikely(!table)) return; stats = kzalloc(sizeof(*stats), GFP_KERNEL); if (!stats) return; cpufreq_for_each_valid_entry(pos, table) count++; alloc_size = count * (sizeof(unsigned int) + sizeof(u64)); stats->freq_table = kzalloc(alloc_size, GFP_KERNEL); if (!stats->freq_table) goto free_stat; stats->time_in_state = (unsigned long long *)(stats->freq_table + count); stats->last_level = count; cpufreq_for_each_valid_entry(pos, table) stats->freq_table[i++] = pos->frequency; data->stats = stats; cpufreq_for_each_valid_entry(pos, table) { data->level_offset = pos->driver_data; break; } return; free_stat: kfree(stats); } static int exynos_hiu_probe(struct platform_device *pdev) { struct task_struct *polling_thread; struct device_node *dn = pdev->dev.of_node; int ret; data = kzalloc(sizeof(struct exynos_hiu_data), GFP_KERNEL); if (!data) return -ENOMEM; mutex_init(&data->lock); platform_set_drvdata(pdev, data); data->base = ioremap(GCU_BASE, SZ_4K); ret = hiu_dt_parsing(dn); if (ret) { dev_err(&pdev->dev, "Failed to parse HIU data\n"); return -ENODEV; } data->dn = dn; if (data->operation_mode == INTERRUPT_MODE) { data->irq = irq_of_parse_and_map(dn, 0); if (data->irq <= 0) { dev_err(&pdev->dev, "Failed to get IRQ\n"); return -ENODEV; } ret = devm_request_irq(&pdev->dev, data->irq, exynos_hiu_irq_handler, IRQF_TRIGGER_RISING, dev_name(&pdev->dev), data); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ handler: %d\n", data->irq); return -ENODEV; } } else { init_waitqueue_head(&data->polling_wait); data->boosting_activated = false; polling_thread = kthread_create(hiu_sr1_check_loop, NULL, "hiu_polling"); kthread_bind_mask(polling_thread, cpu_coregroup_mask(0)); wake_up_process(polling_thread); } cpufreq_register_notifier(&exynos_hiu_policy_notifier, CPUFREQ_POLICY_NOTIFIER); cpufreq_register_notifier(&exynos_hiu_transition_notifier, CPUFREQ_TRANSITION_NOTIFIER); INIT_WORK(&data->work, exynos_hiu_work); init_waitqueue_head(&data->hwidvfs_wait); ret = sysfs_create_group(&pdev->dev.kobj, &exynos_hiu_attr_group); if (ret) dev_err(&pdev->dev, "Failed to create Exynos HIU attr group"); exynos_cpufreq_ready_list_add(&exynos_hiu_ready); dev_info(&pdev->dev, "HIU Handler initialization complete\n"); return 0; } static int exynos_hiu_suspend(struct platform_device *pdev, pm_message_t state) { /* HACK : disable turbo boost */ return 0; } static int exynos_hiu_resume(struct platform_device *pdev) { /* HACK : enable turbo boost */ return 0; } static const struct of_device_id of_exynos_hiu_match[] = { { .compatible = "samsung,exynos-hiu", }, { }, }; static const struct platform_device_id exynos_hiu_ids[] = { { "exynos-hiu", }, { } }; static struct platform_driver exynos_hiu_driver = { .driver = { .name = "exynos-hiu", .owner = THIS_MODULE, .of_match_table = of_exynos_hiu_match, }, .probe = exynos_hiu_probe, .suspend = exynos_hiu_suspend, .resume = exynos_hiu_resume, .id_table = exynos_hiu_ids, }; int __init exynos_hiu_init(void) { return platform_driver_register(&exynos_hiu_driver); } arch_initcall(exynos_hiu_init);