365 lines
8.4 KiB
C
365 lines
8.4 KiB
C
|
/*
|
||
|
* PowerMac G5 SMU driver
|
||
|
*
|
||
|
* Copyright 2004 J. Mayer <l_indien@magic.fr>
|
||
|
* Copyright 2005 Benjamin Herrenschmidt, IBM Corp.
|
||
|
*
|
||
|
* Released under the term of the GNU GPL v2.
|
||
|
*/
|
||
|
|
||
|
/*
|
||
|
* For now, this driver includes:
|
||
|
* - RTC get & set
|
||
|
* - reboot & shutdown commands
|
||
|
* all synchronous with IRQ disabled (ugh)
|
||
|
*
|
||
|
* TODO:
|
||
|
* rework in a way the PMU driver works, that is asynchronous
|
||
|
* with a queue of commands. I'll do that as soon as I have an
|
||
|
* SMU based machine at hand. Some more cleanup is needed too,
|
||
|
* like maybe fitting it into a platform device, etc...
|
||
|
* Also check what's up with cache coherency, and if we really
|
||
|
* can't do better than flushing the cache, maybe build a table
|
||
|
* of command len/reply len like the PMU driver to only flush
|
||
|
* what is actually necessary.
|
||
|
* --BenH.
|
||
|
*/
|
||
|
|
||
|
#include <linux/config.h>
|
||
|
#include <linux/types.h>
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/device.h>
|
||
|
#include <linux/dmapool.h>
|
||
|
#include <linux/bootmem.h>
|
||
|
#include <linux/vmalloc.h>
|
||
|
#include <linux/highmem.h>
|
||
|
#include <linux/jiffies.h>
|
||
|
#include <linux/interrupt.h>
|
||
|
#include <linux/rtc.h>
|
||
|
|
||
|
#include <asm/byteorder.h>
|
||
|
#include <asm/io.h>
|
||
|
#include <asm/prom.h>
|
||
|
#include <asm/machdep.h>
|
||
|
#include <asm/pmac_feature.h>
|
||
|
#include <asm/smu.h>
|
||
|
#include <asm/sections.h>
|
||
|
#include <asm/abs_addr.h>
|
||
|
|
||
|
#define DEBUG_SMU 1
|
||
|
|
||
|
#ifdef DEBUG_SMU
|
||
|
#define DPRINTK(fmt, args...) do { printk(KERN_DEBUG fmt , ##args); } while (0)
|
||
|
#else
|
||
|
#define DPRINTK(fmt, args...) do { } while (0)
|
||
|
#endif
|
||
|
|
||
|
/*
|
||
|
* This is the command buffer passed to the SMU hardware
|
||
|
*/
|
||
|
struct smu_cmd_buf {
|
||
|
u8 cmd;
|
||
|
u8 length;
|
||
|
u8 data[0x0FFE];
|
||
|
};
|
||
|
|
||
|
struct smu_device {
|
||
|
spinlock_t lock;
|
||
|
struct device_node *of_node;
|
||
|
int db_ack; /* doorbell ack GPIO */
|
||
|
int db_req; /* doorbell req GPIO */
|
||
|
u32 __iomem *db_buf; /* doorbell buffer */
|
||
|
struct smu_cmd_buf *cmd_buf; /* command buffer virtual */
|
||
|
u32 cmd_buf_abs; /* command buffer absolute */
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* I don't think there will ever be more than one SMU, so
|
||
|
* for now, just hard code that
|
||
|
*/
|
||
|
static struct smu_device *smu;
|
||
|
|
||
|
/*
|
||
|
* SMU low level communication stuff
|
||
|
*/
|
||
|
static inline int smu_cmd_stat(struct smu_cmd_buf *cmd_buf, u8 cmd_ack)
|
||
|
{
|
||
|
rmb();
|
||
|
return cmd_buf->cmd == cmd_ack && cmd_buf->length != 0;
|
||
|
}
|
||
|
|
||
|
static inline u8 smu_save_ack_cmd(struct smu_cmd_buf *cmd_buf)
|
||
|
{
|
||
|
return (~cmd_buf->cmd) & 0xff;
|
||
|
}
|
||
|
|
||
|
static void smu_send_cmd(struct smu_device *dev)
|
||
|
{
|
||
|
/* SMU command buf is currently cacheable, we need a physical
|
||
|
* address. This isn't exactly a DMA mapping here, I suspect
|
||
|
* the SMU is actually communicating with us via i2c to the
|
||
|
* northbridge or the CPU to access RAM.
|
||
|
*/
|
||
|
writel(dev->cmd_buf_abs, dev->db_buf);
|
||
|
|
||
|
/* Ring the SMU doorbell */
|
||
|
pmac_do_feature_call(PMAC_FTR_WRITE_GPIO, NULL, dev->db_req, 4);
|
||
|
pmac_do_feature_call(PMAC_FTR_READ_GPIO, NULL, dev->db_req, 4);
|
||
|
}
|
||
|
|
||
|
static int smu_cmd_done(struct smu_device *dev)
|
||
|
{
|
||
|
unsigned long wait = 0;
|
||
|
int gpio;
|
||
|
|
||
|
/* Check the SMU doorbell */
|
||
|
do {
|
||
|
gpio = pmac_do_feature_call(PMAC_FTR_READ_GPIO,
|
||
|
NULL, dev->db_ack);
|
||
|
if ((gpio & 7) == 7)
|
||
|
return 0;
|
||
|
udelay(100);
|
||
|
} while(++wait < 10000);
|
||
|
|
||
|
printk(KERN_ERR "SMU timeout !\n");
|
||
|
return -ENXIO;
|
||
|
}
|
||
|
|
||
|
static int smu_do_cmd(struct smu_device *dev)
|
||
|
{
|
||
|
int rc;
|
||
|
u8 cmd_ack;
|
||
|
|
||
|
DPRINTK("SMU do_cmd %02x len=%d %02x\n",
|
||
|
dev->cmd_buf->cmd, dev->cmd_buf->length,
|
||
|
dev->cmd_buf->data[0]);
|
||
|
|
||
|
cmd_ack = smu_save_ack_cmd(dev->cmd_buf);
|
||
|
|
||
|
/* Clear cmd_buf cache lines */
|
||
|
flush_inval_dcache_range((unsigned long)dev->cmd_buf,
|
||
|
((unsigned long)dev->cmd_buf) +
|
||
|
sizeof(struct smu_cmd_buf));
|
||
|
smu_send_cmd(dev);
|
||
|
rc = smu_cmd_done(dev);
|
||
|
if (rc == 0)
|
||
|
rc = smu_cmd_stat(dev->cmd_buf, cmd_ack) ? 0 : -1;
|
||
|
|
||
|
DPRINTK("SMU do_cmd %02x len=%d %02x => %d (%02x)\n",
|
||
|
dev->cmd_buf->cmd, dev->cmd_buf->length,
|
||
|
dev->cmd_buf->data[0], rc, cmd_ack);
|
||
|
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
/* RTC low level commands */
|
||
|
static inline int bcd2hex (int n)
|
||
|
{
|
||
|
return (((n & 0xf0) >> 4) * 10) + (n & 0xf);
|
||
|
}
|
||
|
|
||
|
static inline int hex2bcd (int n)
|
||
|
{
|
||
|
return ((n / 10) << 4) + (n % 10);
|
||
|
}
|
||
|
|
||
|
#if 0
|
||
|
static inline void smu_fill_set_pwrup_timer_cmd(struct smu_cmd_buf *cmd_buf)
|
||
|
{
|
||
|
cmd_buf->cmd = 0x8e;
|
||
|
cmd_buf->length = 8;
|
||
|
cmd_buf->data[0] = 0x00;
|
||
|
memset(cmd_buf->data + 1, 0, 7);
|
||
|
}
|
||
|
|
||
|
static inline void smu_fill_get_pwrup_timer_cmd(struct smu_cmd_buf *cmd_buf)
|
||
|
{
|
||
|
cmd_buf->cmd = 0x8e;
|
||
|
cmd_buf->length = 1;
|
||
|
cmd_buf->data[0] = 0x01;
|
||
|
}
|
||
|
|
||
|
static inline void smu_fill_dis_pwrup_timer_cmd(struct smu_cmd_buf *cmd_buf)
|
||
|
{
|
||
|
cmd_buf->cmd = 0x8e;
|
||
|
cmd_buf->length = 1;
|
||
|
cmd_buf->data[0] = 0x02;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
static inline void smu_fill_set_rtc_cmd(struct smu_cmd_buf *cmd_buf,
|
||
|
struct rtc_time *time)
|
||
|
{
|
||
|
cmd_buf->cmd = 0x8e;
|
||
|
cmd_buf->length = 8;
|
||
|
cmd_buf->data[0] = 0x80;
|
||
|
cmd_buf->data[1] = hex2bcd(time->tm_sec);
|
||
|
cmd_buf->data[2] = hex2bcd(time->tm_min);
|
||
|
cmd_buf->data[3] = hex2bcd(time->tm_hour);
|
||
|
cmd_buf->data[4] = time->tm_wday;
|
||
|
cmd_buf->data[5] = hex2bcd(time->tm_mday);
|
||
|
cmd_buf->data[6] = hex2bcd(time->tm_mon) + 1;
|
||
|
cmd_buf->data[7] = hex2bcd(time->tm_year - 100);
|
||
|
}
|
||
|
|
||
|
static inline void smu_fill_get_rtc_cmd(struct smu_cmd_buf *cmd_buf)
|
||
|
{
|
||
|
cmd_buf->cmd = 0x8e;
|
||
|
cmd_buf->length = 1;
|
||
|
cmd_buf->data[0] = 0x81;
|
||
|
}
|
||
|
|
||
|
static void smu_parse_get_rtc_reply(struct smu_cmd_buf *cmd_buf,
|
||
|
struct rtc_time *time)
|
||
|
{
|
||
|
time->tm_sec = bcd2hex(cmd_buf->data[0]);
|
||
|
time->tm_min = bcd2hex(cmd_buf->data[1]);
|
||
|
time->tm_hour = bcd2hex(cmd_buf->data[2]);
|
||
|
time->tm_wday = bcd2hex(cmd_buf->data[3]);
|
||
|
time->tm_mday = bcd2hex(cmd_buf->data[4]);
|
||
|
time->tm_mon = bcd2hex(cmd_buf->data[5]) - 1;
|
||
|
time->tm_year = bcd2hex(cmd_buf->data[6]) + 100;
|
||
|
}
|
||
|
|
||
|
int smu_get_rtc_time(struct rtc_time *time)
|
||
|
{
|
||
|
unsigned long flags;
|
||
|
int rc;
|
||
|
|
||
|
if (smu == NULL)
|
||
|
return -ENODEV;
|
||
|
|
||
|
memset(time, 0, sizeof(struct rtc_time));
|
||
|
spin_lock_irqsave(&smu->lock, flags);
|
||
|
smu_fill_get_rtc_cmd(smu->cmd_buf);
|
||
|
rc = smu_do_cmd(smu);
|
||
|
if (rc == 0)
|
||
|
smu_parse_get_rtc_reply(smu->cmd_buf, time);
|
||
|
spin_unlock_irqrestore(&smu->lock, flags);
|
||
|
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
int smu_set_rtc_time(struct rtc_time *time)
|
||
|
{
|
||
|
unsigned long flags;
|
||
|
int rc;
|
||
|
|
||
|
if (smu == NULL)
|
||
|
return -ENODEV;
|
||
|
|
||
|
spin_lock_irqsave(&smu->lock, flags);
|
||
|
smu_fill_set_rtc_cmd(smu->cmd_buf, time);
|
||
|
rc = smu_do_cmd(smu);
|
||
|
spin_unlock_irqrestore(&smu->lock, flags);
|
||
|
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
void smu_shutdown(void)
|
||
|
{
|
||
|
const unsigned char *command = "SHUTDOWN";
|
||
|
unsigned long flags;
|
||
|
|
||
|
if (smu == NULL)
|
||
|
return;
|
||
|
|
||
|
spin_lock_irqsave(&smu->lock, flags);
|
||
|
smu->cmd_buf->cmd = 0xaa;
|
||
|
smu->cmd_buf->length = strlen(command);
|
||
|
strcpy(smu->cmd_buf->data, command);
|
||
|
smu_do_cmd(smu);
|
||
|
for (;;)
|
||
|
;
|
||
|
spin_unlock_irqrestore(&smu->lock, flags);
|
||
|
}
|
||
|
|
||
|
void smu_restart(void)
|
||
|
{
|
||
|
const unsigned char *command = "RESTART";
|
||
|
unsigned long flags;
|
||
|
|
||
|
if (smu == NULL)
|
||
|
return;
|
||
|
|
||
|
spin_lock_irqsave(&smu->lock, flags);
|
||
|
smu->cmd_buf->cmd = 0xaa;
|
||
|
smu->cmd_buf->length = strlen(command);
|
||
|
strcpy(smu->cmd_buf->data, command);
|
||
|
smu_do_cmd(smu);
|
||
|
for (;;)
|
||
|
;
|
||
|
spin_unlock_irqrestore(&smu->lock, flags);
|
||
|
}
|
||
|
|
||
|
int smu_present(void)
|
||
|
{
|
||
|
return smu != NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
int smu_init (void)
|
||
|
{
|
||
|
struct device_node *np;
|
||
|
u32 *data;
|
||
|
|
||
|
np = of_find_node_by_type(NULL, "smu");
|
||
|
if (np == NULL)
|
||
|
return -ENODEV;
|
||
|
|
||
|
if (smu_cmdbuf_abs == 0) {
|
||
|
printk(KERN_ERR "SMU: Command buffer not allocated !\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
smu = alloc_bootmem(sizeof(struct smu_device));
|
||
|
if (smu == NULL)
|
||
|
return -ENOMEM;
|
||
|
memset(smu, 0, sizeof(*smu));
|
||
|
|
||
|
spin_lock_init(&smu->lock);
|
||
|
smu->of_node = np;
|
||
|
/* smu_cmdbuf_abs is in the low 2G of RAM, can be converted to a
|
||
|
* 32 bits value safely
|
||
|
*/
|
||
|
smu->cmd_buf_abs = (u32)smu_cmdbuf_abs;
|
||
|
smu->cmd_buf = (struct smu_cmd_buf *)abs_to_virt(smu_cmdbuf_abs);
|
||
|
|
||
|
np = of_find_node_by_name(NULL, "smu-doorbell");
|
||
|
if (np == NULL) {
|
||
|
printk(KERN_ERR "SMU: Can't find doorbell GPIO !\n");
|
||
|
goto fail;
|
||
|
}
|
||
|
data = (u32 *)get_property(np, "reg", NULL);
|
||
|
of_node_put(np);
|
||
|
if (data == NULL) {
|
||
|
printk(KERN_ERR "SMU: Can't find doorbell GPIO address !\n");
|
||
|
goto fail;
|
||
|
}
|
||
|
|
||
|
/* Current setup has one doorbell GPIO that does both doorbell
|
||
|
* and ack. GPIOs are at 0x50, best would be to find that out
|
||
|
* in the device-tree though.
|
||
|
*/
|
||
|
smu->db_req = 0x50 + *data;
|
||
|
smu->db_ack = 0x50 + *data;
|
||
|
|
||
|
/* Doorbell buffer is currently hard-coded, I didn't find a proper
|
||
|
* device-tree entry giving the address. Best would probably to use
|
||
|
* an offset for K2 base though, but let's do it that way for now.
|
||
|
*/
|
||
|
smu->db_buf = ioremap(0x8000860c, 0x1000);
|
||
|
if (smu->db_buf == NULL) {
|
||
|
printk(KERN_ERR "SMU: Can't map doorbell buffer pointer !\n");
|
||
|
goto fail;
|
||
|
}
|
||
|
|
||
|
sys_ctrler = SYS_CTRLER_SMU;
|
||
|
return 0;
|
||
|
|
||
|
fail:
|
||
|
smu = NULL;
|
||
|
return -ENXIO;
|
||
|
|
||
|
}
|