/*
 * Processor idle mode module for AMD SMP systems
 *
 * Copyright (C) 2002 Tony Lindgren <tony@atomide.com>
 *
 * Using this module saves about 70 - 90W of energy in the idle mode compared
 * to the default idle mode. Waking up from the idle mode is fast to keep the
 * system response time good. Currently no CPU load calculation is done, the
 * system exits the idle mode if the idle function runs twice on the same
 * processor in a row. This only works on SMP systems, but maybe the idle mode
 * enabling can be integrated to ACPI to provide C2 mode at some point.
 *
 * NOTE: Currently there's a bug somewhere where the reading the
 *       P_LVL2 for the first time causes the system to sleep instead of 
 *       idling. This means that you need to hit the power button once to
 *       wake the system after loading the module for the first time after
 *       reboot. After that the system idles as supposed.
 *
 *
 * Influenced by Vcool, and LVCool. Rewrote everything from scratch to
 * use the PCI features in Linux, and to support SMP systems.
 * 
 * Currently only tested on TYAN S2460 (760MP) system. Adding support 
 * for other Athlon SMP or single processor systems should be easy if 
 * desired.
 *
 * This software is licensed under GNU General Public License Version 2 
 * as specified in file COPYING in the Linux kernel source tree main 
 * directory.

 Compile command:
   gcc -D__KERNEL__ -I/usr/src/linux/include -Wall -Wstrict-prototypes \
   -Wno-trigraphs -O2 -fomit-frame-pointer -fno-strict-aliasing -fno-common \
   -pipe -mpreferred-stack-boundary=2 -march=athlon -DMODULE -DMODVERSIONS \
   -include /usr/src/linux/include/linux/modversions.h -c amd-idle.c

 */

#include <linux/config.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/pci.h>
#include <linux/delay.h>

extern void default_idle(void);
static void amd_smp_idle(void);
static int amd_idle_main(void);
static int __devinit amd_nb_init(struct pci_dev *pdev,
				 const struct pci_device_id *ent);
static void amd_nb_remove(struct pci_dev *pdev);
static int __devinit amd_sb_init(struct pci_dev *pdev,
				 const struct pci_device_id *ent);
static void amd_sb_remove(struct pci_dev *pdev);

#define DEBUG 1
#define VERSION	"20020702"
#define AMD762		0x700c
#define AMD765_766	0x7413
#define CUR_PR	smp_processor_id()

struct pci_dev *pdev_nb;
struct pci_dev *pdev_sb;

struct idle_cfg {
	unsigned int status_reg;
	unsigned int idle_reg;
	unsigned int slp_reg;
	unsigned int resume_reg;
	void (*orig_idle) (void);
	void (*curr_idle) (void);
	int last_pr;
};
static struct idle_cfg amd_idle_cfg;

struct cpu_idle_state {
	int idle;
	int count;
};
static struct cpu_idle_state prs[2];

static struct pci_device_id amd_nb_tbl[] __devinitdata = {
	{PCI_VENDOR_ID_AMD, AMD762, PCI_ANY_ID, PCI_ANY_ID,},
	{0,}
};

static struct pci_device_id amd_sb_tbl[] __devinitdata = {
	{PCI_VENDOR_ID_AMD, AMD765_766, PCI_ANY_ID, PCI_ANY_ID,},
	{0,}
};

static struct pci_driver amd_nb_driver = {
	name:"amd-smp-idle-nb",
	id_table:amd_nb_tbl,
	probe:amd_nb_init,
	remove:__devexit_p(amd_nb_remove),
};

static struct pci_driver amd_sb_driver = {
	name:"amd-smp-idle-sb",
	id_table:amd_sb_tbl,
	probe:amd_sb_init,
	remove:__devexit_p(amd_sb_remove),
};

static int __devinit
amd_nb_init(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	pdev_nb = pdev;
	printk(KERN_INFO "amd-smp-idle: Initializing northbridge %s\n",
	       pdev_nb->name);

	return 0;
}

static void __devexit
amd_nb_remove(struct pci_dev *pdev)
{
}

static int __devinit
amd_sb_init(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	pdev_sb = pdev;
	printk(KERN_INFO "amd-smp-idle: Initializing southbridge %s\n",
	       pdev_sb->name);

	return 0;
}

static void __devexit
amd_sb_remove(struct pci_dev *pdev)
{
}

/*
 * Configures the southbridge to support idle calls, and gets
 * the processor idle call register location.
 */
static int
sb_idle_amd_766(int enable)
{
	unsigned int regdword;
	unsigned short regshort;
	unsigned char regbyte;

#define DCSTOP_EN	(1 << 1)
#define STPCLK_EN	(1 << 2)
#define CPUSTP_EN	(1 << 3)
#define PCISTP_EN	(1 << 4)
#define CPUSLP_EN	(1 << 5)
#define SUSPND_EN	(1 << 6)

#define C2_REGS		0
#define C3_REGS		8
#define POS_REGS	16	

	/* Get the address for pm status, P_LVL2, etc */
	pci_read_config_dword(pdev_sb, 0x58, &regdword);
	regdword &= 0xff80;
	amd_idle_cfg.status_reg = regdword + 0x00;
	amd_idle_cfg.slp_reg = regdword + 0x04;
	amd_idle_cfg.idle_reg = regdword + 0x14;
	amd_idle_cfg.resume_reg = regdword + 0x16;

	/* Set C2 options in C3A50, page 63 in AMD-766 doc */
	pci_read_config_dword(pdev_sb, 0x50, &regdword);

	regdword &= ~((DCSTOP_EN | CPUSTP_EN | PCISTP_EN | 
		       SUSPND_EN) << C2_REGS);

	regdword |= (STPCLK_EN << C2_REGS);	/* ~ 20 Watt savings max */
	regdword |= (CPUSLP_EN << C2_REGS);	/* Additional ~ 70 Watts max! */

	pci_write_config_dword(pdev_sb, 0x50, regdword);

	/* Clear W4SG, set STPGNT and PMIOEN at C3A41 */
	pci_read_config_byte(pdev_sb, 0x41, &regbyte);
	regbyte &= ~(1 << 0);
	regbyte |= ((1 << 1) | (1 << 7));
	pci_write_config_byte(pdev_sb, 0x41, regbyte);

	return 0;
}

/*
 * Configures the northbridge to support idle calls
 */
static int
nb_idle_amd_762(int enable)
{
	unsigned int regdword;

	/* Enable STPGNT in BIU Status/Control for cpu0 */
	pci_read_config_dword(pdev_nb, 0x60, &regdword);
	regdword |= (1 << 17);
	pci_write_config_dword(pdev_nb, 0x60, regdword);

	/* Enable STPGNT in BIU Status/Control for cpu1 */
	pci_read_config_dword(pdev_nb, 0x68, &regdword);
	regdword |= (1 << 17);
	pci_write_config_dword(pdev_nb, 0x68, regdword);

	/* DRAM refresh enable */
	pci_read_config_dword(pdev_nb, 0x58, &regdword);
	regdword &= ~(1 << 19);
	pci_write_config_dword(pdev_nb, 0x58, regdword);

	/* Self refresh enable */
	pci_read_config_dword(pdev_nb, 0x70, &regdword);
	regdword |= (1 << 18);
	pci_write_config_dword(pdev_nb, 0x70, regdword);

	return 0;
}

/*
 * Idle loop for single processor systems
 */
void
amd_idle(void)
{
	// FIXME: Optionally add non-smp idle loop here
}

/*
 * Idle loop for SMP systems, supports currently only 2 processors.
 */
static void
amd_smp_idle(void)
{

#define LAZY_IDLE_DELAY	800	/* 0: Best savings,  3000: More responsive */

	/*
	 * Exit idle mode immediately if the CPU does not change.
	 * Usually that means that we have some load on another CPU.
	 */
	if (prs[0].idle && prs[1].idle && amd_idle_cfg.last_pr == CUR_PR) {
		prs[0].idle = 0;
		prs[1].idle = 0;
		amd_idle_cfg.last_pr = CUR_PR;
		return;
	}

	prs[CUR_PR].count++;

	/* Don't start the idle mode immediately */
	if (prs[CUR_PR].count >= LAZY_IDLE_DELAY) {

		/* Put the current processor into idle mode */
		prs[CUR_PR].idle = 1;

		/* Only idle if both processors are idle */
		if (prs[0].idle && prs[1].idle)
			inb(amd_idle_cfg.idle_reg);

		prs[CUR_PR].count = 0;

	}
	amd_idle_cfg.last_pr = CUR_PR;
}

/*
 * Finds and initializes the bridges, and then sets the idle function
 */
static int
amd_idle_main(void)
{
	int found;

	/* Find northbridge */
	found = pci_module_init(&amd_nb_driver);
	if (found < 0) {
		printk(KERN_ERR "amd-smp-idle: Could not find northbridge\n");
		return 1;
	}

	/* Find southbridge */
	found = pci_module_init(&amd_sb_driver);
	if (found < 0) {
		printk(KERN_ERR "amd-smp-idle: Could not find southbridge\n");
		pci_unregister_driver(&amd_nb_driver);
		return 1;
	}

	/* Init southbridge */
	switch (pdev_sb->device) {
	case AMD765_766:	/* AMD-765 or 766 */
		sb_idle_amd_766(1);
		break;
	default:
		printk(KERN_ERR "amd-smp-idle: No southbridge to initialize\n");
		break;
	}

	/* Init northbridge and queue the new idle function */
	switch (pdev_nb->device) {
	case AMD762:
		nb_idle_amd_762(1);
		amd_idle_cfg.curr_idle = amd_smp_idle;
		break;
	default:
		printk(KERN_ERR "amd-smp-idle: No northbridge to initialize\n");
		break;
	}

	if (!amd_idle_cfg.curr_idle) {
		printk(KERN_ERR "amd-smp-idle: Idle function not changed\n");
		return 1;
	}

	amd_idle_cfg.orig_idle = pm_idle;
	pm_idle = amd_idle_cfg.curr_idle;

	return 0;
}

static int __init
amd_idle_init(void)
{
	printk(KERN_INFO "amd-smp-idle: AMD processor idle module version %s\n",
	       VERSION);
	return amd_idle_main();
}

static void __exit
amd_idle_cleanup(void)
{
	pm_idle = amd_idle_cfg.orig_idle;

	/* 
	 * FIXME: We want to wait until all CPUs have set the new
	 * idle function, otherwise we will oops. This may not be
	 * the right way to do it, but seems to work.
	 */
	schedule();
	mdelay(1000);

	pci_unregister_driver(&amd_nb_driver);
	pci_unregister_driver(&amd_sb_driver);

}

MODULE_LICENSE("GPL");
module_init(amd_idle_init);
module_exit(amd_idle_cleanup);