blob: 827adab00e2a28ac90641ff11095003ce8d45b45 [file] [log] [blame]
/*
* Copyright (c) 2009, Code Aurora Forum. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that 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.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/kernel_stat.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
#include "avs.h"
#define AVSDSCR_INPUT 0x01004860 /* magic # from circuit designer */
#define TSCSR_INPUT 0x00000001 /* enable temperature sense */
#define TEMPRS 16 /* total number of temperature regions */
#define GET_TEMPR() (avs_get_tscsr() >> 28) /* scale TSCSR[CTEMP] to regions */
struct mutex avs_lock;
static struct avs_state_s
{
u32 freq_cnt; /* Frequencies supported list */
short *avs_v; /* Dyanmically allocated storage for
* 2D table of voltages over temp &
* freq. Used as a set of 1D tables.
* Each table is for a single temp.
* For usage see avs_get_voltage
*/
int (*set_vdd) (int); /* Function Ptr for setting voltage */
int changing; /* Clock frequency is changing */
u32 freq_idx; /* Current frequency index */
int vdd; /* Current ACPU voltage */
} avs_state;
/*
* Update the AVS voltage vs frequency table, for current temperature
* Adjust based on the AVS delay circuit hardware status
*/
static void avs_update_voltage_table(short *vdd_table)
{
u32 avscsr;
int cpu;
int vu;
int l2;
int i;
u32 cur_freq_idx;
short cur_voltage;
cur_freq_idx = avs_state.freq_idx;
cur_voltage = avs_state.vdd;
avscsr = avs_test_delays();
AVSDEBUG("avscsr=%x, avsdscr=%x\n", avscsr, avs_get_avsdscr());
/*
* Read the results for the various unit's AVS delay circuits
* 2=> up, 1=>down, 0=>no-change
*/
cpu = ((avscsr >> 23) & 2) + ((avscsr >> 16) & 1);
vu = ((avscsr >> 28) & 2) + ((avscsr >> 21) & 1);
l2 = ((avscsr >> 29) & 2) + ((avscsr >> 22) & 1);
if ((cpu == 3) || (vu == 3) || (l2 == 3)) {
printk(KERN_ERR "AVS: Dly Synth O/P error\n");
} else if ((cpu == 2) || (l2 == 2) || (vu == 2)) {
/*
* even if one oscillator asks for up, increase the voltage,
* as its an indication we are running outside the
* critical acceptable range of v-f combination.
*/
AVSDEBUG("cpu=%d l2=%d vu=%d\n", cpu, l2, vu);
AVSDEBUG("Voltage up at %d\n", cur_freq_idx);
if (cur_voltage >= VOLTAGE_MAX)
printk(KERN_ERR
"AVS: Voltage can not get high enough!\n");
/* Raise the voltage for all frequencies */
for (i = 0; i < avs_state.freq_cnt; i++) {
vdd_table[i] = cur_voltage + VOLTAGE_STEP;
if (vdd_table[i] > VOLTAGE_MAX)
vdd_table[i] = VOLTAGE_MAX;
}
} else if ((cpu == 1) && (l2 == 1) && (vu == 1)) {
if ((cur_voltage - VOLTAGE_STEP >= VOLTAGE_MIN) &&
(cur_voltage <= vdd_table[cur_freq_idx])) {
vdd_table[cur_freq_idx] = cur_voltage - VOLTAGE_STEP;
AVSDEBUG("Voltage down for %d and lower levels\n",
cur_freq_idx);
/* clamp to this voltage for all lower levels */
for (i = 0; i < cur_freq_idx; i++) {
if (vdd_table[i] > vdd_table[cur_freq_idx])
vdd_table[i] = vdd_table[cur_freq_idx];
}
}
}
}
/*
* Return the voltage for the target performance freq_idx and optionally
* use AVS hardware to check the present voltage freq_idx
*/
static short avs_get_target_voltage(int freq_idx, bool update_table)
{
unsigned cur_tempr = GET_TEMPR();
unsigned temp_index = cur_tempr*avs_state.freq_cnt;
/* Table of voltages vs frequencies for this temp */
short *vdd_table = avs_state.avs_v + temp_index;
if (update_table)
avs_update_voltage_table(vdd_table);
return vdd_table[freq_idx];
}
/*
* Set the voltage for the freq_idx and optionally
* use AVS hardware to update the voltage
*/
static int avs_set_target_voltage(int freq_idx, bool update_table)
{
int rc = 0;
int new_voltage = avs_get_target_voltage(freq_idx, update_table);
if (avs_state.vdd != new_voltage) {
AVSDEBUG("AVS setting V to %d mV @%d\n",
new_voltage, freq_idx);
rc = avs_state.set_vdd(new_voltage);
if (rc)
return rc;
avs_state.vdd = new_voltage;
}
return rc;
}
/*
* Notify avs of clk frquency transition begin & end
*/
int avs_adjust_freq(u32 freq_idx, int begin)
{
int rc = 0;
if (!avs_state.set_vdd) {
/* AVS not initialized */
return 0;
}
if (freq_idx >= avs_state.freq_cnt) {
AVSDEBUG("Out of range :%d\n", freq_idx);
return -EINVAL;
}
mutex_lock(&avs_lock);
if ((begin && (freq_idx > avs_state.freq_idx)) ||
(!begin && (freq_idx < avs_state.freq_idx))) {
/* Update voltage before increasing frequency &
* after decreasing frequency
*/
rc = avs_set_target_voltage(freq_idx, 0);
if (rc)
goto aaf_out;
avs_state.freq_idx = freq_idx;
}
avs_state.changing = begin;
aaf_out:
mutex_unlock(&avs_lock);
return rc;
}
static struct delayed_work avs_work;
static struct workqueue_struct *kavs_wq;
#define AVS_DELAY ((CONFIG_HZ * 50 + 999) / 1000)
static void do_avs_timer(struct work_struct *work)
{
int cur_freq_idx;
mutex_lock(&avs_lock);
if (!avs_state.changing) {
/* Only adjust the voltage if clk is stable */
cur_freq_idx = avs_state.freq_idx;
avs_set_target_voltage(cur_freq_idx, 1);
}
mutex_unlock(&avs_lock);
queue_delayed_work_on(0, kavs_wq, &avs_work, AVS_DELAY);
}
static void __init avs_timer_init(void)
{
INIT_DELAYED_WORK_DEFERRABLE(&avs_work, do_avs_timer);
queue_delayed_work_on(0, kavs_wq, &avs_work, AVS_DELAY);
}
static void __exit avs_timer_exit(void)
{
cancel_delayed_work(&avs_work);
}
static int __init avs_work_init(void)
{
kavs_wq = create_workqueue("avs");
if (!kavs_wq) {
printk(KERN_ERR "AVS initialization failed\n");
return -EFAULT;
}
avs_timer_init();
return 1;
}
static void __exit avs_work_exit(void)
{
avs_timer_exit();
destroy_workqueue(kavs_wq);
}
int __init avs_init(int (*set_vdd)(int), u32 freq_cnt, u32 freq_idx)
{
int i;
mutex_init(&avs_lock);
if (freq_cnt == 0)
return -EINVAL;
avs_state.freq_cnt = freq_cnt;
if (freq_idx >= avs_state.freq_cnt)
return -EINVAL;
avs_state.avs_v = kmalloc(TEMPRS * avs_state.freq_cnt *
sizeof(avs_state.avs_v[0]), GFP_KERNEL);
if (avs_state.avs_v == 0)
return -ENOMEM;
for (i = 0; i < TEMPRS*avs_state.freq_cnt; i++)
avs_state.avs_v[i] = VOLTAGE_MAX;
avs_reset_delays(AVSDSCR_INPUT);
avs_set_tscsr(TSCSR_INPUT);
avs_state.set_vdd = set_vdd;
avs_state.changing = 0;
avs_state.freq_idx = -1;
avs_state.vdd = -1;
avs_adjust_freq(freq_idx, 0);
avs_work_init();
return 0;
}
void __exit avs_exit()
{
avs_work_exit();
kfree(avs_state.avs_v);
}