aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/macintosh/smu.c
blob: fb535737d17d114244481fea4ba7421a768ce1ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
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;

}