/* * Tegra Graphics Host Unit clock scaling * * Copyright (c) 2010-2014, NVIDIA Corporation. All rights reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dev.h" #include "chip_support.h" #include "nvhost_acm.h" #include "nvhost_scale.h" #include "host1x/host1x_actmon.h" static ssize_t nvhost_scale_load_show(struct device *dev, struct device_attribute *attr, char *buf) { struct nvhost_device_data *pdata = dev_get_drvdata(dev); struct nvhost_device_profile *profile = pdata->power_profile; u32 busy_time; ssize_t res; actmon_op().read_avg_norm(profile->actmon, &busy_time); res = snprintf(buf, PAGE_SIZE, "%u\n", busy_time); return res; } static DEVICE_ATTR(load, S_IRUGO, nvhost_scale_load_show, NULL); /* * nvhost_scale_make_freq_table(profile) * * This function initialises the frequency table for the given device profile */ static int nvhost_scale_make_freq_table(struct nvhost_device_profile *profile) { unsigned long *freqs; int num_freqs, err; unsigned long max_freq = clk_round_rate(profile->clk, UINT_MAX); unsigned long min_freq = clk_round_rate(profile->clk, 0); err = tegra_dvfs_get_freqs(clk_get_parent(profile->clk), &freqs, &num_freqs); if (err) return -ENOSYS; /* check for duplicate frequencies at higher end */ while ((num_freqs >= 2 && freqs[num_freqs - 2] == freqs[num_freqs - 1]) || (num_freqs && max_freq < freqs[num_freqs - 1])) num_freqs--; /* check low end */ while ((num_freqs >= 2 && freqs[0] == freqs[1]) || (num_freqs && freqs[0] < min_freq)) { freqs++; num_freqs--; } if (!num_freqs) dev_warn(&profile->pdev->dev, "dvfs table had no applicable frequencies!\n"); profile->devfreq_profile.freq_table = (unsigned long *)freqs; profile->devfreq_profile.max_state = num_freqs; return 0; } /* * nvhost_scale_target(dev, *freq, flags) * * This function scales the clock */ static int nvhost_scale_target(struct device *dev, unsigned long *freq, u32 flags) { struct nvhost_device_data *pdata = dev_get_drvdata(dev); struct nvhost_device_profile *profile = pdata->power_profile; if (!tegra_is_clk_enabled(profile->clk)) { *freq = profile->devfreq_profile.freq_table[0]; return 0; } *freq = clk_round_rate(clk_get_parent(profile->clk), *freq); if (clk_get_rate(profile->clk) == *freq) return 0; nvhost_module_set_devfreq_rate(profile->pdev, 0, *freq); if (pdata->scaling_post_cb) pdata->scaling_post_cb(profile, *freq); *freq = clk_get_rate(profile->clk); return 0; } /* * nvhost_scale_qos_notify() * * This function is called when the minimum QoS requirement for the device * has changed. The function calls postscaling callback if it is defined. */ static int nvhost_scale_qos_notify(struct notifier_block *nb, unsigned long n, void *p) { struct nvhost_device_profile *profile = container_of(nb, struct nvhost_device_profile, qos_notify_block); struct nvhost_device_data *pdata = platform_get_drvdata(profile->pdev); unsigned long freq; if (!pdata->scaling_post_cb) return NOTIFY_OK; /* get the frequency requirement. if devfreq is enabled, check if it * has higher demand than qos */ freq = clk_round_rate(clk_get_parent(profile->clk), pm_qos_request(pdata->qos_id)); if (pdata->power_manager) freq = max(pdata->power_manager->previous_freq, freq); pdata->scaling_post_cb(profile, freq); return NOTIFY_OK; } /* * update_load_estimate(profile) * * Update load estimate using busy/idle flag. */ static void update_load_estimate(struct nvhost_device_profile *profile, bool busy) { ktime_t t; unsigned long dt; t = ktime_get(); dt = ktime_us_delta(t, profile->last_event_time); profile->dev_stat.total_time += dt; profile->last_event_time = t; if (profile->busy) profile->dev_stat.busy_time += dt; profile->busy = busy; } /* * update_load_estimate_actmon(profile) * * Update load estimate using hardware actmon. The actmon value is normalised * based on the time it was asked last time. */ static void update_load_estimate_actmon(struct nvhost_device_profile *profile) { ktime_t t; unsigned long dt; u32 busy_time; t = ktime_get(); dt = ktime_us_delta(t, profile->last_event_time); profile->dev_stat.total_time = dt; profile->last_event_time = t; actmon_op().read_avg_norm(profile->actmon, &busy_time); profile->dev_stat.busy_time = (busy_time * dt) / 1000; } /* * nvhost_scale_notify(pdev, busy) * * Calling this function informs that the device is idling (..or busy). This * data is used to estimate the current load */ static void nvhost_scale_notify(struct platform_device *pdev, bool busy) { struct nvhost_device_data *pdata = platform_get_drvdata(pdev); struct nvhost_device_profile *profile = pdata->power_profile; struct devfreq *devfreq = pdata->power_manager; /* Is the device profile initialised? */ if (!profile) return; /* inform edp about new constraint */ if (pdata->gpu_edp_device) { u32 avg = 0; actmon_op().read_avg_norm(profile->actmon, &avg); BUG(); /* the next line passes a bogus frequency */ tegra_edp_notify_gpu_load(avg, 0); } /* If defreq is disabled, set the freq to max or min */ if (!devfreq) { unsigned long freq = busy ? UINT_MAX : 0; nvhost_scale_target(&pdev->dev, &freq, 0); return; } mutex_lock(&devfreq->lock); if (!profile->actmon) update_load_estimate(profile, busy); profile->dev_stat.busy = busy; update_devfreq(devfreq); mutex_unlock(&devfreq->lock); } void nvhost_scale_notify_idle(struct platform_device *pdev) { nvhost_scale_notify(pdev, false); } void nvhost_scale_notify_busy(struct platform_device *pdev) { nvhost_scale_notify(pdev, true); } /* * nvhost_scale_get_dev_status(dev, *stat) * * This function queries the current device status. */ static int nvhost_scale_get_dev_status(struct device *dev, struct devfreq_dev_status *stat) { struct nvhost_device_data *pdata = dev_get_drvdata(dev); struct nvhost_device_profile *profile = pdata->power_profile; /* Make sure there are correct values for the current frequency */ profile->dev_stat.current_frequency = clk_get_rate(profile->clk); if (profile->actmon) update_load_estimate_actmon(profile); /* Copy the contents of the current device status */ *stat = profile->dev_stat; /* Finally, clear out the local values */ profile->dev_stat.total_time = 0; profile->dev_stat.busy_time = 0; return 0; } /* * nvhost_scale_init(pdev) */ void nvhost_scale_init(struct platform_device *pdev) { struct nvhost_device_data *pdata = platform_get_drvdata(pdev); struct nvhost_device_profile *profile; int err; if (pdata->power_profile) return; profile = kzalloc(sizeof(struct nvhost_device_profile), GFP_KERNEL); if (!profile) return; pdata->power_profile = profile; profile->pdev = pdev; profile->clk = pdata->clk[0]; profile->dev_stat.busy = false; /* Create frequency table */ err = nvhost_scale_make_freq_table(profile); if (err || !profile->devfreq_profile.max_state) goto err_get_freqs; /* Initialize actmon */ if (pdata->actmon_enabled) { if (device_create_file(&pdev->dev, &dev_attr_load)) goto err_create_sysfs_entry; profile->actmon = kzalloc(sizeof(struct host1x_actmon), GFP_KERNEL); if (!profile->actmon) goto err_allocate_actmon; profile->actmon->host = nvhost_get_host(pdev); profile->actmon->regs = nvhost_get_host(pdev)->aperture + pdata->actmon_regs; actmon_op().init(profile->actmon); actmon_op().debug_init(profile->actmon, pdata->debugfs); actmon_op().deinit(profile->actmon); } if (pdata->devfreq_governor) { struct devfreq *devfreq; profile->devfreq_profile.initial_freq = profile->devfreq_profile.freq_table[0]; profile->devfreq_profile.target = nvhost_scale_target; profile->devfreq_profile.get_dev_status = nvhost_scale_get_dev_status; devfreq = devfreq_add_device(&pdev->dev, &profile->devfreq_profile, pdata->devfreq_governor, NULL); if (IS_ERR(devfreq)) devfreq = NULL; pdata->power_manager = devfreq; } /* Should we register QoS callback for this device? */ if (pdata->qos_id < PM_QOS_NUM_CLASSES && pdata->qos_id != PM_QOS_RESERVED) { profile->qos_notify_block.notifier_call = &nvhost_scale_qos_notify; pm_qos_add_notifier(pdata->qos_id, &profile->qos_notify_block); } return; err_get_freqs: err_allocate_actmon: device_remove_file(&pdev->dev, &dev_attr_load); err_create_sysfs_entry: kfree(pdata->power_profile); pdata->power_profile = NULL; } /* * nvhost_scale_deinit(dev) * * Stop scaling for the given device. */ void nvhost_scale_deinit(struct platform_device *pdev) { struct nvhost_device_data *pdata = platform_get_drvdata(pdev); struct nvhost_device_profile *profile = pdata->power_profile; if (!profile) return; if (pdata->power_manager) devfreq_remove_device(pdata->power_manager); if (pdata->actmon_enabled) device_remove_file(&pdev->dev, &dev_attr_load); kfree(profile->devfreq_profile.freq_table); kfree(profile->actmon); kfree(profile); pdata->power_profile = NULL; } /* * nvhost_scale_hw_init(dev) * * Initialize hardware portion of the device */ int nvhost_scale_hw_init(struct platform_device *pdev) { struct nvhost_device_data *pdata = platform_get_drvdata(pdev); struct nvhost_device_profile *profile = pdata->power_profile; if (profile && profile->actmon) actmon_op().init(profile->actmon); return 0; } /* * nvhost_scale_hw_deinit(dev) * * Deinitialize the hw partition related to scaling */ void nvhost_scale_hw_deinit(struct platform_device *pdev) { struct nvhost_device_data *pdata = platform_get_drvdata(pdev); struct nvhost_device_profile *profile = pdata->power_profile; if (profile && profile->actmon) actmon_op().deinit(profile->actmon); }