/*
 * longrun - Transmeta(TM) Crusoe(TM) LongRun(TM) utility
 *
 * Copyright (C) 2001  Transmeta Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#define VERSION "0.9"
#define REVDATE "2001-02-14"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/io.h>
#include <sys/sysmacros.h>
#define __USE_UNIX98	/* for pread/pwrite */
#include <unistd.h>

#define MSR_DEVICE "/dev/cpu/0/msr"
#define MSR_TMx86_LONGRUN	0x80868010
#define MSR_TMx86_LONGRUN_FLAGS	0x80868011

#define LONGRUN_MASK(x)		((x) & 0x0000007f)
#define LONGRUN_RESERVED(x)	((x) & 0xffffff80)
#define LONGRUN_WRITE(x, y)	(LONGRUN_RESERVED(x) | LONGRUN_MASK(y))

#define CPUID_DEVICE "/dev/cpu/0/cpuid"
#define CPUID_TMx86_VENDOR_ID		0x80860000
#define CPUID_TMx86_PROCESSOR_INFO	0x80860001
#define CPUID_TMx86_LONGRUN_STATUS	0x80860007
#define CPUID_TMx86_FEATURE_LONGRUN(x)	((x) & 0x02)

/* Advanced Thermal Management */
#define LR_NORTHBRIDGE "/proc/bus/pci/00/00.0"
#define ATM_ADDRESS	0xa8
#define ATM_EN(x)	(((x) & 0x10) >> 4)
#define ATM_LVL(x)	(((x) & 0x0e) >> 1)
#define LR_EN(x)	((x) & 0x01)

char *progname;			/* name of program */

int msr_fd;			/* MSR file descriptor */
char *msr_device;		/* MSR device name */

int cpuid_fd;			/* CPUID file descriptor */
char *cpuid_device;		/* CPUID device name */

int opt_verbose = 0;		/* verbosity */

void usage(int status) {
	FILE *stream = status ? stderr : stdout;

	fprintf(stream,
		"%s %s (%s)\n"
		"usage: %s [-c device] [-m device] [-hlpv] [-f flag] [-s low high]\n"
		" -c device     set CPUID device\n"
		" -m device     set MSR device\n"
		" -h            print this help\n"
		" -l            list LongRun information about available performance levels\n"
		" -p            print current LongRun settings and status\n"
		" -v            be more verbose\n"
		" -f [flag]     set a LongRun mode flag\n"
		" -s low high   set current LongRun performance window (0 to 100)\n"
		"\n"
		"supported flags:\n"
		" economy       set economy mode (turn off performance mode)\n"
		" performance   set performance mode (turn off economy mode)\n",
		progname, VERSION, REVDATE, progname);

	exit(status);
}

char *my_basename (const char *filename)
{
	char *base = strrchr (filename, '/');

	return base ? base + 1 : (char *) filename;
}

void error_warn (const char *fmt, ...)
{
	va_list arg_ptr;

	va_start(arg_ptr, fmt);
	fprintf(stderr, "%s: ", progname);
	vfprintf(stderr, fmt, arg_ptr);
	fprintf(stderr, ": %s\n", strerror(errno));
	va_end(arg_ptr);
}

void error_die (const char *fmt, ...)
{
	va_list arg_ptr;

	va_start(arg_ptr, fmt);
	fprintf(stderr, "%s: ", progname);
	vfprintf(stderr, fmt, arg_ptr);
	fprintf(stderr, ": %s\n", strerror(errno));
	va_end(arg_ptr);
	exit(1);
}

/* read Advanced Thermal Management definitions register */
void read_atm(uint8_t *atm)
{
	int nb;

	nb = open(LR_NORTHBRIDGE, O_RDONLY);
	if (nb < 0) {
		error_warn("error opening %s", LR_NORTHBRIDGE);
		if (errno == ENOENT) {
			fprintf(stderr, "make sure /proc is mounted\n");
		}
		exit(1);
	}
	if (pread(nb, atm, 1, ATM_ADDRESS) != 1) {
		error_die("error reading %s", LR_NORTHBRIDGE);
	}
	close(nb);
}

/* note: if an output is NULL, then don't set it */
void read_msr(long address, int *lower, int *upper)
{
	uint32_t data[2];

	if (pread(msr_fd, &data, 8, address) != 8) {
		error_die("error reading %s", msr_device);
	}

	if (lower) *lower = data[0];
	if (upper) *upper = data[1];
}

void write_msr(long address, int lower, int upper)
{
	uint32_t data[2];

	data[0] = (uint32_t) lower;
	data[1] = (uint32_t) upper;

	if (pwrite(msr_fd, &data, 8, address) != 8) {
		error_die("error writing %s", msr_device);
	}
}

/* note: if an output is NULL, then don't set it */
void read_cpuid(long address, int *eax, int *ebx, int *ecx, int *edx)
{
	uint32_t data[4];

	if (pread(cpuid_fd, &data, 16, address) != 16) {
		error_die("error reading %s", cpuid_device);
	}

	if (eax) *eax = data[0];
	if (ebx) *ebx = data[1];
	if (ecx) *ecx = data[2];
	if (edx) *edx = data[3];
}

void check_cpu()
{
	int eax, ebx, ecx, edx;

	/* test for "TransmetaCPU" */
	read_cpuid(CPUID_TMx86_VENDOR_ID, &eax, &ebx, &ecx, &edx);
	if (ebx != 0x6e617254 || ecx != 0x55504361 || edx != 0x74656d73) {
		fprintf(stderr, "%s: not a Transmeta x86 CPU\n", progname);
		exit(1);
	}

	/* test for LongRun feature flag */
	read_cpuid(CPUID_TMx86_PROCESSOR_INFO, &eax, &ebx, &ecx, &edx);
	if (!CPUID_TMx86_FEATURE_LONGRUN(edx)) {
		printf("LongRun: unsupported\n");
		exit(0);
	}
}

void list_longrun()
{
	int i, save_lower, save_upper, pct_in, pct_last, steps, freq_max;
	int eax, ebx, ecx;
	float power_max, power_ratio;
	int *perf, *mhz, *volts;

	/* save current settings */
	read_msr(MSR_TMx86_LONGRUN, &save_lower, &save_upper);

	/* find freq_max */
	read_cpuid(CPUID_TMx86_PROCESSOR_INFO, 0, 0, &freq_max, 0);

	/* setup for probing */
	write_msr(MSR_TMx86_LONGRUN, LONGRUN_WRITE(save_lower, 0),
		  LONGRUN_WRITE(save_upper, 0));
	read_cpuid(CPUID_TMx86_LONGRUN_STATUS, &eax, 0, 0, 0);
	steps = (((freq_max - eax)) / (100.0 / 3.0) + 0.5); /* 33 MHz steps */
	perf = malloc(sizeof(int) * (steps + 1));
	mhz = malloc(sizeof(int) * (steps + 1));
	volts = malloc(sizeof(int) * (steps + 1));
	for (i = 0; i <= steps; i++) {
		mhz[i] = 0;
	}

	/* probe */
	pct_last = -1;
	for (i = 0; i <= steps; i++) {
		pct_in = ((float) i * (100.0 / (float) steps));
		write_msr(MSR_TMx86_LONGRUN, LONGRUN_WRITE(save_lower, pct_in),
			  LONGRUN_WRITE(save_upper, pct_in));
		read_cpuid(CPUID_TMx86_LONGRUN_STATUS, &eax, &ebx, &ecx, 0);
		if (opt_verbose)
			printf("# set %d, got %d\n", pct_in, ecx);
		if (pct_last < ecx) {
			perf[i] = ecx;
			mhz[i] = eax;
			volts[i] = ebx;
		}
		if (ecx == 100)
			break;
		if (ecx > pct_in)
			i++;
		pct_last = ecx;
	}

	/* find power_max */
	power_max = mhz[i] * volts[i] * volts[i];

	/* print results */
	printf("# %%   MHz  Volts  usage\n");
	for (i = 0; i <= steps; i++) {
		if (mhz[i]) {
			power_ratio = mhz[i] * volts[i] * volts[i] / power_max;
			printf("%3d %5d %6.3f %6.3f\n",
			       perf[i], mhz[i], volts[i] / 1000.0, power_ratio);
		}
	}

	free(perf);
	free(mhz);
	free(volts);

	/* restore current settings */
	write_msr(MSR_TMx86_LONGRUN, save_lower, save_upper);
}

void get_longrun(int *low, int *high)
{
	read_msr(MSR_TMx86_LONGRUN, low, high);
	/* only look at desired bits */
	*low = LONGRUN_MASK(*low);
	*high = LONGRUN_MASK(*high);
}

void set_longrun(int low, int high)
{
	int lower, upper;

	read_msr(MSR_TMx86_LONGRUN, &lower, &upper);
	write_msr(MSR_TMx86_LONGRUN, LONGRUN_WRITE(lower, low),
		  LONGRUN_WRITE(upper, high));
	if (opt_verbose) {
		printf("Setting performance window: %d to %d\n", low, high);
	}
}

void set_longrun_flag(char *flag)
{
	int lower, upper;

	read_msr(MSR_TMx86_LONGRUN_FLAGS, &lower, &upper);
	if (!strcmp(flag, "economy")) {
		write_msr(MSR_TMx86_LONGRUN_FLAGS, lower & 0xfffffffe, upper);
		if (opt_verbose) {
			printf("Setting flag: economy\n");
		}
	}
	else if (!strcmp(flag, "performance")) {
		write_msr(MSR_TMx86_LONGRUN_FLAGS, (lower & 0xffffffff) | 0x1,
			  upper);
		if (opt_verbose) {
			printf("Setting flag: performance\n");
		}
	}
	else {
		usage(1);
	}
}

void print_longrun()
{
	uint8_t atm;		/* Advanced Thermal Management */
	int lower, upper, percent;

	/* read the current performance level first */
	read_cpuid(CPUID_TMx86_LONGRUN_STATUS, 0, 0, &percent, 0);

	read_atm(&atm);

	printf("LongRun: %s\n", LR_EN(atm) ? "enabled" : "disabled");
	printf("LongRun Thermal Extensions (LTX): %s\n", ATM_EN(atm) ? "active" : "inactive");
	if (ATM_EN(atm)) {
		printf("LTX setting: ");
		switch(ATM_LVL(atm)) {
		case 0:
		case 1:
			printf("reserved\n"); break;
		case 2:
			printf("75%% reduction\n"); break;
		case 3:
			printf("62.5%% reduction\n"); break;
		case 4:
			printf("50%% reduction\n"); break;
		case 5:
			printf("37.5%% reduction\n"); break;
		case 6:
			printf("25%% reduction\n"); break;
		case 7:
			printf("12.5%% reduction\n"); break;
		}
	}
	get_longrun(&lower, &upper);
	printf("Current performance window: %d to %d\n", lower, upper);
	printf("Current performance level: %d\n", percent);
	read_msr(MSR_TMx86_LONGRUN_FLAGS, &lower, &upper);
	printf("LongRun flags: %s\n", (lower & 1) ? "performance" : "economy");
}

int main(int argc, char *argv[])
{
	int low, high;
	int g;
	char *opt_flag = NULL;
	int opt_list = 0;
	int opt_print = 0;
	int opt_set = 0;

	if (argc)
		progname = my_basename(argv[0]);
	else
		progname = "longrun";

	msr_device = MSR_DEVICE;
	cpuid_device = CPUID_DEVICE;

	/* command line options */
	while ((g = getopt(argc, argv, "c:f:m:hlpsv")) != EOF) {
		switch (g) {
		case 'c':
			cpuid_device = optarg;
			break;
		case 'f':
			opt_flag = optarg;
			break;
		case 'm':
			msr_device = optarg;
			break;
		case 'h':
			usage(0);
			break;
		case 'l':
			opt_list = 1;
			break;
		case 'p':
			opt_print = 1;
			break;
		case 's':
			opt_set = 1;
			break;
		case 'v':
			opt_verbose++;
			break;
		}
	}

	if (opt_list + opt_print + opt_set + (opt_flag ? 1 : 0) != 1)
		usage(1);

	if (opt_set && (optind + 2 != argc))
		usage(1);

	if (geteuid()) {
		fprintf(stderr, "%s: must be run as root\n", progname);
		exit(1);
	}

	if ((cpuid_fd = open(cpuid_device, O_RDWR)) < 0) {
		error_warn("error opening %s", cpuid_device);
		if (errno == ENODEV) {
			fprintf(stderr, "make sure your kernel was compiled with CONFIG_X86_CPUID=y\n");
		}
		exit(1);
	}

	if ((msr_fd = open(msr_device, O_RDWR)) < 0) {
		error_warn("error opening %s", msr_device);
		if (errno == ENODEV) {
			fprintf(stderr, "make sure your kernel was compiled with CONFIG_X86_MSR=y\n");
		}
		exit(1);
	}

	check_cpu();

	if (opt_list) {
		list_longrun();
		exit(0);
	}
	if (opt_set) {
		low = strtol(argv[optind], NULL, 10);
		high = strtol(argv[optind + 1], NULL, 10);
		set_longrun(low, high);
	}
	if (opt_flag) {
		set_longrun_flag(opt_flag);
	}
	if (opt_print || opt_verbose) {
		print_longrun();
	}

	exit(0);
}
