/*****************************************************************************/
/*
 *      rovclock.c  --  Radeon overclocking utility.
 *
 *      Copyright (C) 2005-2006  Sebastian Witt (se.witt@gmx.net)
 *
 *      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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
 /*****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <sys/io.h>
#include <sys/mman.h>

#include "pci.h"
#include "radeon.h"

#define VERSION "0.6e"

#define PCI_VENDOR_ID_ATI 0x1002
#define XTAL 2700

typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

/* PLL info block from video BIOS */
struct pll_block {
        u8 clock_chip_type;
        u8 struct_size;
        u8 accelerator_entry;
        u8 VGA_entry;
        u16 VGA_table_offset;
        u16 POST_table_offset;
        u16 XCLK;
        u16 MCLK;
        u8 num_PLL_blocks;
        u8 size_PLL_blocks;
        u16 PCLK_ref_freq;
        u16 PCLK_ref_divider;
        u32 PCLK_min_freq;
        u32 PCLK_max_freq;
        u16 MCLK_ref_freq;
        u16 MCLK_ref_divider;
        u32 MCLK_min_freq;
        u32 MCLK_max_freq;
        u16 XCLK_ref_freq;
        u16 XCLK_ref_divider;
        u32 XCLK_min_freq;
        u32 XCLK_max_freq;
} __attribute__ ((packed));

/* PLL register data */
struct pll_data {
		u8 M;
		u8 Nm;
		u8 Ns;
		u8 X;
} __attribute__ ((packed));

/* Memory config locations */
struct mem_t {
	u16 reg;
	u32 mask;
	u8  shift;
	u8  offset;
	u16 value;
	char *description;
};

struct mem_t mem_timings[] = {
	{EXT_MEM_CNTL, 0x00000007,  0, 3,  3, "tRcdRD:  "},
	{EXT_MEM_CNTL, 0x00000070,  4, 1,  2, "tRcdWR:  "},
	{EXT_MEM_CNTL, 0x00000700,  8, 3,  3, "tRP:     "},
	{EXT_MEM_CNTL, 0x00007800, 11, 6,  5, "tRAS:    "},
	{EXT_MEM_CNTL, 0x00038000, 15, 1,  2, "tRRD:    "},
	{EXT_MEM_CNTL, 0x000c0000, 18, 1,  2, "tR2W-CL: "},
	{EXT_MEM_CNTL, 0x00700000, 20, 1,  2, "tWR:     "},
	{EXT_MEM_CNTL, 0x07000000, 24, 0,  2, "tW2R:    "},
	{EXT_MEM_CNTL, 0x08000000, 27, 0,  1, "tW2Rsb:  "},	// same bank: 1 = tWR, 0 = tW2R
	{EXT_MEM_CNTL, 0x30000000, 28, 1,  1, "tR2R:    "},
	{MEM_REFRESH_CNTL, 0x0000f000, 12, 13,  7, "tRFC:    "},
	{MEM_SDRAM_MODE_REG, 0x000f0000, 16, 0, 2, "tWL(0.5):"},
	{MEM_SDRAM_MODE_REG, 0x00700000, 20, 0, 4, "tCAS:    "},
	{MEM_SDRAM_MODE_REG, 0x00800000, 23, 0, 0, "tCMD:    "},
	{MEM_SDRAM_MODE_REG, 0x01000000, 24, 0, 1, "tSTR:    "},
	{0x0000, 0x00000000, 0, 0, 0, ""},
};

/* Program data */
struct rovclock_data {
	u32 pci_id;
	u32 pci_bus;
	u32 pci_dev;
	u32 iobase;
	u32 xtal;
	u32 cfreq;
	u32 mfreq;
	u32 ref_div;
	struct pll_data pllreg;
	struct pll_block pll;
	u32 mem_size;
	u8 mem_channels;
	u8 mem_cdch_only;
};
	
/* PCI read/write functions */
unsigned int pci_read(u8 bus, u8 device, u8 func, u8 addr)
{
	outl(0x80000000 | (bus << 16) | 
		 (PCI_DEVFN(device, func) << 8) |
		 (addr & ~3), 0xcf8);
	return inl(0xcfc);
}

void pci_write(u8 bus, u8 device, u8 func, u8 addr, u32 data)
{
	outl(0x80000000 | (bus << 16) | 
		 (PCI_DEVFN(device, func) << 8) |
		 (addr & ~3), 0xcf8);
	outl(data, 0xcfc);
}

/* Register read/write functions */
u32 reg_read(struct rovclock_data *rovclock, u32 addr)
{
	return inl(rovclock->iobase + addr);
}

void reg_write(struct rovclock_data *rovclock, u32 addr, u32 data)
{
	outl(data, rovclock->iobase + addr);
}

/* PLL read/write functions */
u32 pll_read(struct rovclock_data *rovclock, u32 addr)
{
	outb(addr & 0x0000003f, rovclock->iobase + CLOCK_CNTL_INDEX);
	return inl(rovclock->iobase + CLOCK_CNTL_DATA);
}

void pll_write(struct rovclock_data *rovclock, u32 addr, u32 data)
{
	outb((addr & 0x3f) | PLL_WR_EN, rovclock->iobase + CLOCK_CNTL_INDEX);
	outl(data, rovclock->iobase + CLOCK_CNTL_DATA);
}

/* MM read/write functions */
u32 mm_read(struct rovclock_data *rovclock, u32 addr)
{
	outl(addr, rovclock->iobase + MM_INDEX);
	return inl(rovclock->iobase + MM_DATA);
}

void mm_write(struct rovclock_data *rovclock, u32 addr, u32 data)
{
	outl(addr, rovclock->iobase + MM_INDEX);
	outl(data, rovclock->iobase + MM_DATA);
}

int round_div(int num, int den)
{
    return (num + (den / 2)) / den;
}

/* Print usage */
void usage(void)
{
	printf("Usage: rovclock -i                 (info)\n");
	printf("                -c [MHz]           (set core clock)\n");
	printf("                -m [MHz]           (set mem clock)\n");
	printf("                -x [kHz]           (xtal frequency, default 2700)\n");
	printf("                -t [name]:[val]    (set memory timings)\n");
}

/* Get memory info */
void get_mem_info(struct rovclock_data *rovclock)
{
	u32 temp;
	u8 count;
	
	rovclock->mem_size = reg_read(rovclock, CONFIG_MEMSIZE);
	temp = mm_read(rovclock, MEM_CNTL);
	rovclock->mem_channels = temp & R300_MEM_NUM_CHANNELS_MASK;
	rovclock->mem_cdch_only = (temp & R300_MEM_USE_CD_CH_ONLY) ? 1 : 0;

	for (count = 0; mem_timings[count].reg != 0; count++)
		mem_timings[count].value = (mm_read(rovclock, mem_timings[count].reg) & mem_timings[count].mask) >> mem_timings[count].shift;
}

/* Display memory info */
void mem_info(struct rovclock_data *rovclock)
{
	u8 count;
	
	printf("Memory size: %d kB\n", rovclock->mem_size/1024);
	printf("Memory channels: %d, CD,CH only: %d\n", rovclock->mem_channels, rovclock->mem_cdch_only);

	for (count = 0; mem_timings[count].reg != 0x00; count++)
		printf("%s %d\n", mem_timings[count].description, mem_timings[count].value + mem_timings[count].offset);
}

/* Set memory timings */
void mem_set_timings(struct rovclock_data *rovclock, char *string)
{
	u32 val, temp;
	char *vp;
	u8 count;
	
	if ((vp = strchr(string, ':')) == NULL)
		return;
	
	if (*(vp+1) == '\0')
		return;
	else
		*(vp++) = '\0';
	
	val = atoi(vp);
	
	for (count = 0; mem_timings[count].reg != 0x00; count++) {
		if (!strncmp(mem_timings[count].description, string, strlen(string))) {
			temp = mm_read(rovclock, mem_timings[count].reg) & ~mem_timings[count].mask;
			temp |= ((val - mem_timings[count].offset) << mem_timings[count].shift) & mem_timings[count].mask;
			mm_write(rovclock, mem_timings[count].reg, temp);
			return;
		}
	}

	fprintf(stderr, "%s not found...\n", string);
}

/* Print PLL info */
void pll_info(struct rovclock_data *rovclock)
{
	/* Get reference divider */
	rovclock->ref_div = pll_read(rovclock, PPLL_REF_DIV) & PPLL_REF_DIV_MASK;
	
	/* Calc. core and memory clock */
	*(u32 *)(&rovclock->pllreg) = pll_read(rovclock, M_SPLL_REF_FB_DIV);
	
	rovclock->cfreq = round_div(rovclock->pllreg.Ns * rovclock->xtal, rovclock->pllreg.M);
	rovclock->mfreq = round_div(rovclock->pllreg.Nm * rovclock->xtal, rovclock->pllreg.M);
	
	//printf("Ns: 0x%02x, Nm: 0x%02x, M: 0x%02x\n", Ns, Nm, M);
	printf("XTAL: %d.%d MHz, RefDiv: %d\n\n", rovclock->xtal / 100, rovclock->xtal % 100, rovclock->ref_div);
	printf("Core: %d.%d MHz, Mem: %d.%d MHz\n", rovclock->cfreq / 100, 
						rovclock->cfreq % 100, 
						rovclock->mfreq / 100, 
						rovclock->mfreq % 100);
}

/* Set new frequency */
void pll_set_freq(struct rovclock_data *rovclock)
{	
	*(u32 *)(&rovclock->pllreg) = pll_read(rovclock, M_SPLL_REF_FB_DIV);
	
	/* Calc. new PLL value */
	rovclock->pllreg.Ns = round_div(rovclock->cfreq * 100 * rovclock->pllreg.M, rovclock->xtal);
	rovclock->pllreg.Nm = round_div(rovclock->mfreq * 100 * rovclock->pllreg.M, rovclock->xtal);
	
	//printf("%x\n", *(int *)&rovclock->pllreg);
	
	pll_write(rovclock, M_SPLL_REF_FB_DIV, *(u32 *)&rovclock->pllreg);
}

/* Search for ATI card on PCI bus */
int find_card(struct rovclock_data *rovclock)
{
	FILE *proc;
	char temp;
	u32 id;
	
	/* Check /proc/bus/pci/devices first */
	if ((proc = fopen("/proc/bus/pci/devices", "r")) != NULL) {
		while (fscanf(proc, "%x\t%x", &rovclock->pci_bus, &id) == 2) {
			if ((id >> 16) == PCI_VENDOR_ID_ATI) {
				rovclock->pci_dev = (rovclock->pci_bus & 0xff) >> 3;
				rovclock->pci_bus = rovclock->pci_bus >> 8;
				rovclock->pci_id = (id << 16) | (id >> 16);
				
				/* Check for display device */
				id = pci_read(rovclock->pci_bus, rovclock->pci_dev, 0, PCI_CLASS_REVISION) >> 24;
				if (id == PCI_BASE_CLASS_DISPLAY) {
					fclose(proc);
					return 0;
				}
			}
			while ((temp = fgetc(proc)) != EOF)
				if (temp == '\n')
					break;
		}
	}
	if ((rovclock->pci_id & 0xffff) != PCI_VENDOR_ID_ATI) {
		fprintf(stderr, "Search in /proc/bus/pci/devices failed, scanning the PCI bus.\n");

		/* Find card by scanning the PCI devices, check from bus 1 to 9 for ATI device */
		rovclock->pci_dev = 0;
		for (rovclock->pci_bus = 1; rovclock->pci_bus < 10; rovclock->pci_bus++) {
			if (((rovclock->pci_id = pci_read(rovclock->pci_bus, 0, 0, 0)) & 0xffff) == PCI_VENDOR_ID_ATI) {
				id = pci_read(rovclock->pci_bus, rovclock->pci_dev, 0, PCI_CLASS_REVISION) >> 24;
				if (id == PCI_BASE_CLASS_DISPLAY)
					break;
			}
		}
	
		if ((rovclock->pci_id & 0xffff) != PCI_VENDOR_ID_ATI) {
			fprintf(stderr, "No ATI card found.\n");
			return -1;
		}
	}
	
	return 0;
}

/* Get card I/O base */
int get_iobase(struct rovclock_data *rovclock)
{
	u32 count;
	
	/* Get I/O base */
	if (!(pci_read(rovclock->pci_bus, rovclock->pci_dev, 0, PCI_COMMAND) & PCI_COMMAND_IO)) {
		fprintf(stderr, "I/O space not enabled, aborting.\n");
		return -1;
	}
	for (count = PCI_BASE_ADDRESS_0; count <= PCI_BASE_ADDRESS_5; count += 4) {
		rovclock->iobase = pci_read(rovclock->pci_bus, rovclock->pci_dev, 0, count);
		if (rovclock->iobase & PCI_BASE_ADDRESS_SPACE_IO)
			break;
	}
	if (!(rovclock->iobase & PCI_BASE_ADDRESS_SPACE_IO)) {
		fprintf(stderr, "I/O base address not found, aborting.\n");
		return -1;
	}
	rovclock->iobase &= PCI_BASE_ADDRESS_IO_MASK;
	
	printf("I/O base address: 0x%04x\n", rovclock->iobase);
	
	return 0;
}
	
/* Get PLL info from video BIOS */
int get_pll_info(struct rovclock_data *rovclock)
{
	FILE *mem;
	u32 bios_seg;
	u8 *rom;
	u32 fp_bios;
	u16 pll_info_start;
	
	/* Access memory for video BIOS */
	if ((mem = fopen("/dev/mem", "r")) == NULL) {
		fprintf(stderr, "Error opening /dev/mem !\n");
		return -1;
	}
	
	for (bios_seg = 0xc0000; bios_seg < 0xf0000; bios_seg += 0x1000) {
		if ((rom = mmap(NULL, 0x1000, PROT_READ, MAP_SHARED, fileno(mem), bios_seg)) == MAP_FAILED) {
			fprintf(stderr, "mmap() failed.\n");
			return -2;
		}
		/* Check signature */
		if (*(u16 *)rom == 0xaa55) {
			printf("Video BIOS shadow found @ 0x%x\n", bios_seg);
			break;
		}
		else {
			munmap(rom, 0x1000);
			rom = NULL;
		}
	}
	
	if (rom == NULL)
		fprintf(stderr, "Video BIOS signature not found.\n");
	else {
		/* Get flat panel info offset */
		fp_bios = *(u32 *)(rom + 0x48);
		//printf("Flat panel info at offset 0x%x\n", fp_bios);
		
		/* Get PLL info block offset */
		pll_info_start = *(u16 *)(rom + fp_bios + 0x30);
		//printf("PLL info at offset 0x%x\n", pll_info_start);
		
		/* Copy PLL info */
		memcpy(&rovclock->pll, (rom + pll_info_start), sizeof(struct pll_block));
		
		/* Unmap & close memory */
		munmap(rom, 0x1000);
		fclose(mem);
	}
	
	return 0;
}

/* Main program */
int main(int argc, char **argv)
{
	int opt, mode;
	struct rovclock_data rovclock;
	
	/* Set default XTAL frequency */
	rovclock.xtal = XTAL;
	
	printf("Radeon overclock %s by Hasw (hasw@hasw.net)\n\n", VERSION);
	
	if (argc < 2) {
		usage();
		return 0;
	}
	
	/* Get I/O permission */
	if (iopl(3) != 0) {
		fprintf(stderr, "Error getting I/O permissions (root?).\n");
		return 1;
	}
	
	/* Detect card */
	if (find_card(&rovclock))
		return 2;
	
	printf("Found ATI card on %02x:%02x, device id: 0x%04x\n", rovclock.pci_bus, rovclock.pci_dev, rovclock.pci_id >> 16);

	/* Get I/O base */
	if (get_iobase(&rovclock))
		return 3;
	
	/* Get PLL info */
	if (!get_pll_info(&rovclock)) {
		/* Get reference clock */
		if ((rovclock.pll.PCLK_ref_freq < 1000) || (rovclock.pll.PCLK_ref_freq > 5000))
			fprintf(stderr, "Invalid reference clock from BIOS: %d.%d MHz\n", rovclock.pll.PCLK_ref_freq / 100, rovclock.pll.PCLK_ref_freq % 100);
		else {
			rovclock.xtal = rovclock.pll.PCLK_ref_freq;
			printf("Reference clock from BIOS: %d.%d MHz\n", rovclock.xtal / 100, rovclock.xtal % 100);
		
			//printf("BIOS default divider: %d/%d/%d\n", pll->PCLK_ref_divider, pll->MCLK_ref_divider, pll->XCLK_ref_divider);
		}
	}
	
	mode = 0;
	rovclock.cfreq = 270;
	rovclock.mfreq = 270;
	get_mem_info(&rovclock);

	/* Parse command line options */
	while ((opt = getopt(argc, argv, "ic:m:x:t:")) > 0) {
		switch (opt) {
			case 'i':	mem_info(&rovclock);
					pll_info(&rovclock);
					break;
			case 'x':	rovclock.xtal = atoi(optarg);
					if (!rovclock.xtal) {
						fprintf(stderr, "Invalid xtal value: %s\n", optarg);
						return 5;
					}
					break;
			case 'c':	rovclock.cfreq = atoi(optarg);
					if (!rovclock.cfreq) {
						fprintf(stderr, "Invalid value: %s\n", optarg);
						return 5;
					}
					mode |= 1;
					break;
			case 'm':	rovclock.mfreq = atoi(optarg);
					if (!rovclock.mfreq) {
						fprintf(stderr, "Invalid value: %s\n", optarg);
						return 5;
					}
					mode |= 1;
					break;
			case 't':	mem_set_timings(&rovclock, optarg);
					break;
			default:	usage();
					return 5;
					break;
		}
	}
	
	/* Now we got a optional xtal frequency */
	if (mode & 1)
		pll_set_freq(&rovclock);
	
	return 0;
}


