summaryrefslogtreecommitdiff
path: root/drivers/macintosh/smu.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/macintosh/smu.c')
-rw-r--r--drivers/macintosh/smu.c364
1 files changed, 364 insertions, 0 deletions
diff --git a/drivers/macintosh/smu.c b/drivers/macintosh/smu.c
new file mode 100644
index 000000000000..fb535737d17d
--- /dev/null
+++ b/drivers/macintosh/smu.c
@@ -0,0 +1,364 @@
+/*
+ * 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;
+
+}