blob: a6d50f4fabd7a9b3bf557af01c2b1c040c10c925 [file] [log] [blame]
Linus Torvalds1da177e2005-04-16 15:20:36 -07001/* APM emulation layer for PowerMac
2 *
3 * Copyright 2001 Benjamin Herrenschmidt (benh@kernel.crashing.org)
4 *
5 * Lots of code inherited from apm.c, see appropriate notice in
6 * arch/i386/kernel/apm.c
7 *
8 * This program is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the
10 * Free Software Foundation; either version 2, or (at your option) any
11 * later version.
12 *
13 * This program is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 *
19 */
20
Linus Torvalds1da177e2005-04-16 15:20:36 -070021#include <linux/module.h>
22
23#include <linux/poll.h>
24#include <linux/types.h>
25#include <linux/stddef.h>
26#include <linux/timer.h>
27#include <linux/fcntl.h>
28#include <linux/slab.h>
29#include <linux/stat.h>
30#include <linux/proc_fs.h>
31#include <linux/miscdevice.h>
32#include <linux/apm_bios.h>
33#include <linux/init.h>
34#include <linux/sched.h>
35#include <linux/pm.h>
36#include <linux/kernel.h>
37#include <linux/smp_lock.h>
38
39#include <linux/adb.h>
40#include <linux/pmu.h>
41
42#include <asm/system.h>
43#include <asm/uaccess.h>
44#include <asm/machdep.h>
45
46#undef DEBUG
47
48#ifdef DEBUG
49#define DBG(args...) printk(KERN_DEBUG args)
50//#define DBG(args...) xmon_printf(args)
51#else
52#define DBG(args...) do { } while (0)
53#endif
54
55/*
56 * The apm_bios device is one of the misc char devices.
57 * This is its minor number.
58 */
59#define APM_MINOR_DEV 134
60
61/*
62 * Maximum number of events stored
63 */
64#define APM_MAX_EVENTS 20
65
66#define FAKE_APM_BIOS_VERSION 0x0101
67
68#define APM_USER_NOTIFY_TIMEOUT (5*HZ)
69
70/*
71 * The per-file APM data
72 */
73struct apm_user {
74 int magic;
75 struct apm_user * next;
76 int suser: 1;
77 int suspend_waiting: 1;
78 int suspends_pending;
79 int suspends_read;
80 int event_head;
81 int event_tail;
82 apm_event_t events[APM_MAX_EVENTS];
83};
84
85/*
86 * The magic number in apm_user
87 */
88#define APM_BIOS_MAGIC 0x4101
89
90/*
91 * Local variables
92 */
93static int suspends_pending;
94
95static DECLARE_WAIT_QUEUE_HEAD(apm_waitqueue);
96static DECLARE_WAIT_QUEUE_HEAD(apm_suspend_waitqueue);
97static struct apm_user * user_list;
98
99static int apm_notify_sleep(struct pmu_sleep_notifier *self, int when);
100static struct pmu_sleep_notifier apm_sleep_notifier = {
101 apm_notify_sleep,
102 SLEEP_LEVEL_USERLAND,
103};
104
105static char driver_version[] = "0.5"; /* no spaces */
106
107#ifdef DEBUG
108static char * apm_event_name[] = {
109 "system standby",
110 "system suspend",
111 "normal resume",
112 "critical resume",
113 "low battery",
114 "power status change",
115 "update time",
116 "critical suspend",
117 "user standby",
118 "user suspend",
119 "system standby resume",
120 "capabilities change"
121};
122#define NR_APM_EVENT_NAME \
123 (sizeof(apm_event_name) / sizeof(apm_event_name[0]))
124
125#endif
126
127static int queue_empty(struct apm_user *as)
128{
129 return as->event_head == as->event_tail;
130}
131
132static apm_event_t get_queued_event(struct apm_user *as)
133{
134 as->event_tail = (as->event_tail + 1) % APM_MAX_EVENTS;
135 return as->events[as->event_tail];
136}
137
138static void queue_event(apm_event_t event, struct apm_user *sender)
139{
140 struct apm_user * as;
141
142 DBG("apm_emu: queue_event(%s)\n", apm_event_name[event-1]);
143 if (user_list == NULL)
144 return;
145 for (as = user_list; as != NULL; as = as->next) {
146 if (as == sender)
147 continue;
148 as->event_head = (as->event_head + 1) % APM_MAX_EVENTS;
149 if (as->event_head == as->event_tail) {
150 static int notified;
151
152 if (notified++ == 0)
153 printk(KERN_ERR "apm_emu: an event queue overflowed\n");
154 as->event_tail = (as->event_tail + 1) % APM_MAX_EVENTS;
155 }
156 as->events[as->event_head] = event;
157 if (!as->suser)
158 continue;
159 switch (event) {
160 case APM_SYS_SUSPEND:
161 case APM_USER_SUSPEND:
162 as->suspends_pending++;
163 suspends_pending++;
164 break;
165 case APM_NORMAL_RESUME:
166 as->suspend_waiting = 0;
167 break;
168 }
169 }
170 wake_up_interruptible(&apm_waitqueue);
171}
172
173static int check_apm_user(struct apm_user *as, const char *func)
174{
175 if ((as == NULL) || (as->magic != APM_BIOS_MAGIC)) {
176 printk(KERN_ERR "apm_emu: %s passed bad filp\n", func);
177 return 1;
178 }
179 return 0;
180}
181
182static ssize_t do_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
183{
184 struct apm_user * as;
185 size_t i;
186 apm_event_t event;
187 DECLARE_WAITQUEUE(wait, current);
188
189 as = fp->private_data;
190 if (check_apm_user(as, "read"))
191 return -EIO;
192 if (count < sizeof(apm_event_t))
193 return -EINVAL;
194 if (queue_empty(as)) {
195 if (fp->f_flags & O_NONBLOCK)
196 return -EAGAIN;
197 add_wait_queue(&apm_waitqueue, &wait);
198repeat:
199 set_current_state(TASK_INTERRUPTIBLE);
200 if (queue_empty(as) && !signal_pending(current)) {
201 schedule();
202 goto repeat;
203 }
204 set_current_state(TASK_RUNNING);
205 remove_wait_queue(&apm_waitqueue, &wait);
206 }
207 i = count;
208 while ((i >= sizeof(event)) && !queue_empty(as)) {
209 event = get_queued_event(as);
210 DBG("apm_emu: do_read, returning: %s\n", apm_event_name[event-1]);
211 if (copy_to_user(buf, &event, sizeof(event))) {
212 if (i < count)
213 break;
214 return -EFAULT;
215 }
216 switch (event) {
217 case APM_SYS_SUSPEND:
218 case APM_USER_SUSPEND:
219 as->suspends_read++;
220 break;
221 }
222 buf += sizeof(event);
223 i -= sizeof(event);
224 }
225 if (i < count)
226 return count - i;
227 if (signal_pending(current))
228 return -ERESTARTSYS;
229 return 0;
230}
231
232static unsigned int do_poll(struct file *fp, poll_table * wait)
233{
234 struct apm_user * as;
235
236 as = fp->private_data;
237 if (check_apm_user(as, "poll"))
238 return 0;
239 poll_wait(fp, &apm_waitqueue, wait);
240 if (!queue_empty(as))
241 return POLLIN | POLLRDNORM;
242 return 0;
243}
244
245static int do_ioctl(struct inode * inode, struct file *filp,
246 u_int cmd, u_long arg)
247{
248 struct apm_user * as;
249 DECLARE_WAITQUEUE(wait, current);
250
251 as = filp->private_data;
252 if (check_apm_user(as, "ioctl"))
253 return -EIO;
254 if (!as->suser)
255 return -EPERM;
256 switch (cmd) {
257 case APM_IOC_SUSPEND:
258 /* If a suspend message was sent to userland, we
259 * consider this as a confirmation message
260 */
261 if (as->suspends_read > 0) {
262 as->suspends_read--;
263 as->suspends_pending--;
264 suspends_pending--;
265 } else {
266 // Route to PMU suspend ?
267 break;
268 }
269 as->suspend_waiting = 1;
270 add_wait_queue(&apm_waitqueue, &wait);
271 DBG("apm_emu: ioctl waking up sleep waiter !\n");
272 wake_up(&apm_suspend_waitqueue);
273 mb();
274 while(as->suspend_waiting && !signal_pending(current)) {
275 set_current_state(TASK_INTERRUPTIBLE);
276 schedule();
277 }
278 set_current_state(TASK_RUNNING);
279 remove_wait_queue(&apm_waitqueue, &wait);
280 break;
281 default:
282 return -EINVAL;
283 }
284 return 0;
285}
286
287static int do_release(struct inode * inode, struct file * filp)
288{
289 struct apm_user * as;
290
291 as = filp->private_data;
292 if (check_apm_user(as, "release"))
293 return 0;
294 filp->private_data = NULL;
295 lock_kernel();
296 if (as->suspends_pending > 0) {
297 suspends_pending -= as->suspends_pending;
298 if (suspends_pending <= 0)
299 wake_up(&apm_suspend_waitqueue);
300 }
301 if (user_list == as)
302 user_list = as->next;
303 else {
304 struct apm_user * as1;
305
306 for (as1 = user_list;
307 (as1 != NULL) && (as1->next != as);
308 as1 = as1->next)
309 ;
310 if (as1 == NULL)
311 printk(KERN_ERR "apm: filp not in user list\n");
312 else
313 as1->next = as->next;
314 }
315 unlock_kernel();
316 kfree(as);
317 return 0;
318}
319
320static int do_open(struct inode * inode, struct file * filp)
321{
322 struct apm_user * as;
323
Robert P. J. Day5cbded52006-12-13 00:35:56 -0800324 as = kmalloc(sizeof(*as), GFP_KERNEL);
Linus Torvalds1da177e2005-04-16 15:20:36 -0700325 if (as == NULL) {
326 printk(KERN_ERR "apm: cannot allocate struct of size %d bytes\n",
327 sizeof(*as));
328 return -ENOMEM;
329 }
330 as->magic = APM_BIOS_MAGIC;
331 as->event_tail = as->event_head = 0;
332 as->suspends_pending = 0;
333 as->suspends_read = 0;
334 /*
335 * XXX - this is a tiny bit broken, when we consider BSD
336 * process accounting. If the device is opened by root, we
337 * instantly flag that we used superuser privs. Who knows,
338 * we might close the device immediately without doing a
339 * privileged operation -- cevans
340 */
341 as->suser = capable(CAP_SYS_ADMIN);
342 as->next = user_list;
343 user_list = as;
344 filp->private_data = as;
345
346 DBG("apm_emu: opened by %s, suser: %d\n", current->comm, (int)as->suser);
347
348 return 0;
349}
350
351/* Wait for all clients to ack the suspend request. APM API
352 * doesn't provide a way to NAK, but this could be added
353 * here.
354 */
355static int wait_all_suspend(void)
356{
357 DECLARE_WAITQUEUE(wait, current);
358
359 add_wait_queue(&apm_suspend_waitqueue, &wait);
360 DBG("apm_emu: wait_all_suspend(), suspends_pending: %d\n", suspends_pending);
361 while(suspends_pending > 0) {
362 set_current_state(TASK_UNINTERRUPTIBLE);
363 schedule();
364 }
365 set_current_state(TASK_RUNNING);
366 remove_wait_queue(&apm_suspend_waitqueue, &wait);
367
368 DBG("apm_emu: wait_all_suspend() - complete !\n");
369
370 return 1;
371}
372
373static int apm_notify_sleep(struct pmu_sleep_notifier *self, int when)
374{
375 switch(when) {
376 case PBOOK_SLEEP_REQUEST:
377 queue_event(APM_SYS_SUSPEND, NULL);
378 if (!wait_all_suspend())
379 return PBOOK_SLEEP_REFUSE;
380 break;
381 case PBOOK_SLEEP_REJECT:
382 case PBOOK_WAKE:
383 queue_event(APM_NORMAL_RESUME, NULL);
384 break;
385 }
386 return PBOOK_SLEEP_OK;
387}
388
389#define APM_CRITICAL 10
390#define APM_LOW 30
391
392static int apm_emu_get_info(char *buf, char **start, off_t fpos, int length)
393{
394 /* Arguments, with symbols from linux/apm_bios.h. Information is
395 from the Get Power Status (0x0a) call unless otherwise noted.
396
397 0) Linux driver version (this will change if format changes)
398 1) APM BIOS Version. Usually 1.0, 1.1 or 1.2.
399 2) APM flags from APM Installation Check (0x00):
400 bit 0: APM_16_BIT_SUPPORT
401 bit 1: APM_32_BIT_SUPPORT
402 bit 2: APM_IDLE_SLOWS_CLOCK
403 bit 3: APM_BIOS_DISABLED
404 bit 4: APM_BIOS_DISENGAGED
405 3) AC line status
406 0x00: Off-line
407 0x01: On-line
408 0x02: On backup power (BIOS >= 1.1 only)
409 0xff: Unknown
410 4) Battery status
411 0x00: High
412 0x01: Low
413 0x02: Critical
414 0x03: Charging
415 0x04: Selected battery not present (BIOS >= 1.2 only)
416 0xff: Unknown
417 5) Battery flag
418 bit 0: High
419 bit 1: Low
420 bit 2: Critical
421 bit 3: Charging
422 bit 7: No system battery
423 0xff: Unknown
424 6) Remaining battery life (percentage of charge):
425 0-100: valid
426 -1: Unknown
427 7) Remaining battery life (time units):
428 Number of remaining minutes or seconds
429 -1: Unknown
430 8) min = minutes; sec = seconds */
431
Olaf Hering08124f92005-10-28 17:46:51 -0700432 unsigned short ac_line_status;
433 unsigned short battery_status = 0;
Linus Torvalds1da177e2005-04-16 15:20:36 -0700434 unsigned short battery_flag = 0xff;
435 int percentage = -1;
436 int time_units = -1;
437 int real_count = 0;
438 int i;
439 char * p = buf;
440 char charging = 0;
441 long charge = -1;
442 long amperage = 0;
443 unsigned long btype = 0;
444
445 ac_line_status = ((pmu_power_flags & PMU_PWR_AC_PRESENT) != 0);
446 for (i=0; i<pmu_battery_count; i++) {
447 if (pmu_batteries[i].flags & PMU_BATT_PRESENT) {
Olaf Hering08124f92005-10-28 17:46:51 -0700448 battery_status++;
Linus Torvalds1da177e2005-04-16 15:20:36 -0700449 if (percentage < 0)
450 percentage = 0;
451 if (charge < 0)
452 charge = 0;
453 percentage += (pmu_batteries[i].charge * 100) /
454 pmu_batteries[i].max_charge;
455 charge += pmu_batteries[i].charge;
456 amperage += pmu_batteries[i].amperage;
457 if (btype == 0)
458 btype = (pmu_batteries[i].flags & PMU_BATT_TYPE_MASK);
459 real_count++;
460 if ((pmu_batteries[i].flags & PMU_BATT_CHARGING))
461 charging++;
462 }
463 }
Olaf Hering08124f92005-10-28 17:46:51 -0700464 if (0 == battery_status)
465 ac_line_status = 1;
466 battery_status = 0xff;
Linus Torvalds1da177e2005-04-16 15:20:36 -0700467 if (real_count) {
468 if (amperage < 0) {
469 if (btype == PMU_BATT_TYPE_SMART)
470 time_units = (charge * 59) / (amperage * -1);
471 else
472 time_units = (charge * 16440) / (amperage * -60);
473 }
474 percentage /= real_count;
475 if (charging > 0) {
476 battery_status = 0x03;
477 battery_flag = 0x08;
478 } else if (percentage <= APM_CRITICAL) {
479 battery_status = 0x02;
480 battery_flag = 0x04;
481 } else if (percentage <= APM_LOW) {
482 battery_status = 0x01;
483 battery_flag = 0x02;
484 } else {
485 battery_status = 0x00;
486 battery_flag = 0x01;
487 }
488 }
489 p += sprintf(p, "%s %d.%d 0x%02x 0x%02x 0x%02x 0x%02x %d%% %d %s\n",
490 driver_version,
491 (FAKE_APM_BIOS_VERSION >> 8) & 0xff,
492 FAKE_APM_BIOS_VERSION & 0xff,
493 0,
494 ac_line_status,
495 battery_status,
496 battery_flag,
497 percentage,
498 time_units,
499 "min");
500
501 return p - buf;
502}
503
Arjan van de Venfa027c22007-02-12 00:55:33 -0800504static const struct file_operations apm_bios_fops = {
Linus Torvalds1da177e2005-04-16 15:20:36 -0700505 .owner = THIS_MODULE,
506 .read = do_read,
507 .poll = do_poll,
508 .ioctl = do_ioctl,
509 .open = do_open,
510 .release = do_release,
511};
512
513static struct miscdevice apm_device = {
514 APM_MINOR_DEV,
515 "apm_bios",
516 &apm_bios_fops
517};
518
519static int __init apm_emu_init(void)
520{
521 struct proc_dir_entry *apm_proc;
522
523 if (sys_ctrler != SYS_CTRLER_PMU) {
524 printk(KERN_INFO "apm_emu: Requires a machine with a PMU.\n");
525 return -ENODEV;
526 }
527
528 apm_proc = create_proc_info_entry("apm", 0, NULL, apm_emu_get_info);
529 if (apm_proc)
530 apm_proc->owner = THIS_MODULE;
531
Neil Horman5d469ec2006-12-06 20:37:08 -0800532 if (misc_register(&apm_device) != 0)
533 printk(KERN_INFO "Could not create misc. device for apm\n");
Linus Torvalds1da177e2005-04-16 15:20:36 -0700534
535 pmu_register_sleep_notifier(&apm_sleep_notifier);
536
537 printk(KERN_INFO "apm_emu: APM Emulation %s initialized.\n", driver_version);
538
539 return 0;
540}
541
542static void __exit apm_emu_exit(void)
543{
544 pmu_unregister_sleep_notifier(&apm_sleep_notifier);
545 misc_deregister(&apm_device);
546 remove_proc_entry("apm", NULL);
547
548 printk(KERN_INFO "apm_emu: APM Emulation removed.\n");
549}
550
551module_init(apm_emu_init);
552module_exit(apm_emu_exit);
553
554MODULE_AUTHOR("Benjamin Herrenschmidt");
555MODULE_DESCRIPTION("APM emulation layer for PowerMac");
556MODULE_LICENSE("GPL");
557