1
linux/drivers/char/apm-emulation.c
Ralf Baechle 7726942fb1 [APM] Add shared version of APM emulation
Currently ARM and MIPS both have nearly identical copies of the APM
emulation code in their arch code.  Add yet another copy of it to
drivers char and make it selectable through SYS_SUPPORTS_APM_EMULATION.

Signed-off-by: Ralf Baechle <ralf@linux-mips.org>
2007-02-09 17:08:57 +00:00

673 lines
15 KiB
C

/*
* bios-less APM driver for ARM Linux
* Jamey Hicks <jamey@crl.dec.com>
* adapted from the APM BIOS driver for Linux by Stephen Rothwell (sfr@linuxcare.com)
*
* APM 1.2 Reference:
* Intel Corporation, Microsoft Corporation. Advanced Power Management
* (APM) BIOS Interface Specification, Revision 1.2, February 1996.
*
* [This document is available from Microsoft at:
* http://www.microsoft.com/hwdev/busbios/amp_12.htm]
*/
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#include <linux/miscdevice.h>
#include <linux/apm_bios.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/apm-emulation.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <asm/system.h>
/*
* The apm_bios device is one of the misc char devices.
* This is its minor number.
*/
#define APM_MINOR_DEV 134
/*
* See Documentation/Config.help for the configuration options.
*
* Various options can be changed at boot time as follows:
* (We allow underscores for compatibility with the modules code)
* apm=on/off enable/disable APM
*/
/*
* Maximum number of events stored
*/
#define APM_MAX_EVENTS 16
struct apm_queue {
unsigned int event_head;
unsigned int event_tail;
apm_event_t events[APM_MAX_EVENTS];
};
/*
* The per-file APM data
*/
struct apm_user {
struct list_head list;
unsigned int suser: 1;
unsigned int writer: 1;
unsigned int reader: 1;
int suspend_result;
unsigned int suspend_state;
#define SUSPEND_NONE 0 /* no suspend pending */
#define SUSPEND_PENDING 1 /* suspend pending read */
#define SUSPEND_READ 2 /* suspend read, pending ack */
#define SUSPEND_ACKED 3 /* suspend acked */
#define SUSPEND_WAIT 4 /* waiting for suspend */
#define SUSPEND_DONE 5 /* suspend completed */
struct apm_queue queue;
};
/*
* Local variables
*/
static int suspends_pending;
static int apm_disabled;
static struct task_struct *kapmd_tsk;
static DECLARE_WAIT_QUEUE_HEAD(apm_waitqueue);
static DECLARE_WAIT_QUEUE_HEAD(apm_suspend_waitqueue);
/*
* This is a list of everyone who has opened /dev/apm_bios
*/
static DECLARE_RWSEM(user_list_lock);
static LIST_HEAD(apm_user_list);
/*
* kapmd info. kapmd provides us a process context to handle
* "APM" events within - specifically necessary if we're going
* to be suspending the system.
*/
static DECLARE_WAIT_QUEUE_HEAD(kapmd_wait);
static DEFINE_SPINLOCK(kapmd_queue_lock);
static struct apm_queue kapmd_queue;
static DEFINE_MUTEX(state_lock);
static const char driver_version[] = "1.13"; /* no spaces */
/*
* Compatibility cruft until the IPAQ people move over to the new
* interface.
*/
static void __apm_get_power_status(struct apm_power_info *info)
{
}
/*
* This allows machines to provide their own "apm get power status" function.
*/
void (*apm_get_power_status)(struct apm_power_info *) = __apm_get_power_status;
EXPORT_SYMBOL(apm_get_power_status);
/*
* APM event queue management.
*/
static inline int queue_empty(struct apm_queue *q)
{
return q->event_head == q->event_tail;
}
static inline apm_event_t queue_get_event(struct apm_queue *q)
{
q->event_tail = (q->event_tail + 1) % APM_MAX_EVENTS;
return q->events[q->event_tail];
}
static void queue_add_event(struct apm_queue *q, apm_event_t event)
{
q->event_head = (q->event_head + 1) % APM_MAX_EVENTS;
if (q->event_head == q->event_tail) {
static int notified;
if (notified++ == 0)
printk(KERN_ERR "apm: an event queue overflowed\n");
q->event_tail = (q->event_tail + 1) % APM_MAX_EVENTS;
}
q->events[q->event_head] = event;
}
static void queue_event(apm_event_t event)
{
struct apm_user *as;
down_read(&user_list_lock);
list_for_each_entry(as, &apm_user_list, list) {
if (as->reader)
queue_add_event(&as->queue, event);
}
up_read(&user_list_lock);
wake_up_interruptible(&apm_waitqueue);
}
/*
* queue_suspend_event - queue an APM suspend event.
*
* Check that we're in a state where we can suspend. If not,
* return -EBUSY. Otherwise, queue an event to all "writer"
* users. If there are no "writer" users, return '1' to
* indicate that we can immediately suspend.
*/
static int queue_suspend_event(apm_event_t event, struct apm_user *sender)
{
struct apm_user *as;
int ret = 1;
mutex_lock(&state_lock);
down_read(&user_list_lock);
/*
* If a thread is still processing, we can't suspend, so reject
* the request.
*/
list_for_each_entry(as, &apm_user_list, list) {
if (as != sender && as->reader && as->writer && as->suser &&
as->suspend_state != SUSPEND_NONE) {
ret = -EBUSY;
goto out;
}
}
list_for_each_entry(as, &apm_user_list, list) {
if (as != sender && as->reader && as->writer && as->suser) {
as->suspend_state = SUSPEND_PENDING;
suspends_pending++;
queue_add_event(&as->queue, event);
ret = 0;
}
}
out:
up_read(&user_list_lock);
mutex_unlock(&state_lock);
wake_up_interruptible(&apm_waitqueue);
return ret;
}
static void apm_suspend(void)
{
struct apm_user *as;
int err = pm_suspend(PM_SUSPEND_MEM);
/*
* Anyone on the APM queues will think we're still suspended.
* Send a message so everyone knows we're now awake again.
*/
queue_event(APM_NORMAL_RESUME);
/*
* Finally, wake up anyone who is sleeping on the suspend.
*/
mutex_lock(&state_lock);
down_read(&user_list_lock);
list_for_each_entry(as, &apm_user_list, list) {
if (as->suspend_state == SUSPEND_WAIT ||
as->suspend_state == SUSPEND_ACKED) {
as->suspend_result = err;
as->suspend_state = SUSPEND_DONE;
}
}
up_read(&user_list_lock);
mutex_unlock(&state_lock);
wake_up(&apm_suspend_waitqueue);
}
static ssize_t apm_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
{
struct apm_user *as = fp->private_data;
apm_event_t event;
int i = count, ret = 0;
if (count < sizeof(apm_event_t))
return -EINVAL;
if (queue_empty(&as->queue) && fp->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(apm_waitqueue, !queue_empty(&as->queue));
while ((i >= sizeof(event)) && !queue_empty(&as->queue)) {
event = queue_get_event(&as->queue);
ret = -EFAULT;
if (copy_to_user(buf, &event, sizeof(event)))
break;
mutex_lock(&state_lock);
if (as->suspend_state == SUSPEND_PENDING &&
(event == APM_SYS_SUSPEND || event == APM_USER_SUSPEND))
as->suspend_state = SUSPEND_READ;
mutex_unlock(&state_lock);
buf += sizeof(event);
i -= sizeof(event);
}
if (i < count)
ret = count - i;
return ret;
}
static unsigned int apm_poll(struct file *fp, poll_table * wait)
{
struct apm_user *as = fp->private_data;
poll_wait(fp, &apm_waitqueue, wait);
return queue_empty(&as->queue) ? 0 : POLLIN | POLLRDNORM;
}
/*
* apm_ioctl - handle APM ioctl
*
* APM_IOC_SUSPEND
* This IOCTL is overloaded, and performs two functions. It is used to:
* - initiate a suspend
* - acknowledge a suspend read from /dev/apm_bios.
* Only when everyone who has opened /dev/apm_bios with write permission
* has acknowledge does the actual suspend happen.
*/
static int
apm_ioctl(struct inode * inode, struct file *filp, u_int cmd, u_long arg)
{
struct apm_user *as = filp->private_data;
unsigned long flags;
int err = -EINVAL;
if (!as->suser || !as->writer)
return -EPERM;
switch (cmd) {
case APM_IOC_SUSPEND:
mutex_lock(&state_lock);
as->suspend_result = -EINTR;
if (as->suspend_state == SUSPEND_READ) {
int pending;
/*
* If we read a suspend command from /dev/apm_bios,
* then the corresponding APM_IOC_SUSPEND ioctl is
* interpreted as an acknowledge.
*/
as->suspend_state = SUSPEND_ACKED;
suspends_pending--;
pending = suspends_pending == 0;
mutex_unlock(&state_lock);
/*
* If there are no further acknowledges required,
* suspend the system.
*/
if (pending)
apm_suspend();
/*
* Wait for the suspend/resume to complete. If there
* are pending acknowledges, we wait here for them.
*
* Note: we need to ensure that the PM subsystem does
* not kick us out of the wait when it suspends the
* threads.
*/
flags = current->flags;
current->flags |= PF_NOFREEZE;
wait_event(apm_suspend_waitqueue,
as->suspend_state == SUSPEND_DONE);
} else {
as->suspend_state = SUSPEND_WAIT;
mutex_unlock(&state_lock);
/*
* Otherwise it is a request to suspend the system.
* Queue an event for all readers, and expect an
* acknowledge from all writers who haven't already
* acknowledged.
*/
err = queue_suspend_event(APM_USER_SUSPEND, as);
if (err < 0) {
/*
* Avoid taking the lock here - this
* should be fine.
*/
as->suspend_state = SUSPEND_NONE;
break;
}
if (err > 0)
apm_suspend();
/*
* Wait for the suspend/resume to complete. If there
* are pending acknowledges, we wait here for them.
*
* Note: we need to ensure that the PM subsystem does
* not kick us out of the wait when it suspends the
* threads.
*/
flags = current->flags;
current->flags |= PF_NOFREEZE;
wait_event_interruptible(apm_suspend_waitqueue,
as->suspend_state == SUSPEND_DONE);
}
current->flags = flags;
mutex_lock(&state_lock);
err = as->suspend_result;
as->suspend_state = SUSPEND_NONE;
mutex_unlock(&state_lock);
break;
}
return err;
}
static int apm_release(struct inode * inode, struct file * filp)
{
struct apm_user *as = filp->private_data;
int pending = 0;
filp->private_data = NULL;
down_write(&user_list_lock);
list_del(&as->list);
up_write(&user_list_lock);
/*
* We are now unhooked from the chain. As far as new
* events are concerned, we no longer exist. However, we
* need to balance suspends_pending, which means the
* possibility of sleeping.
*/
mutex_lock(&state_lock);
if (as->suspend_state != SUSPEND_NONE) {
suspends_pending -= 1;
pending = suspends_pending == 0;
}
mutex_unlock(&state_lock);
if (pending)
apm_suspend();
kfree(as);
return 0;
}
static int apm_open(struct inode * inode, struct file * filp)
{
struct apm_user *as;
as = kzalloc(sizeof(*as), GFP_KERNEL);
if (as) {
/*
* XXX - this is a tiny bit broken, when we consider BSD
* process accounting. If the device is opened by root, we
* instantly flag that we used superuser privs. Who knows,
* we might close the device immediately without doing a
* privileged operation -- cevans
*/
as->suser = capable(CAP_SYS_ADMIN);
as->writer = (filp->f_mode & FMODE_WRITE) == FMODE_WRITE;
as->reader = (filp->f_mode & FMODE_READ) == FMODE_READ;
down_write(&user_list_lock);
list_add(&as->list, &apm_user_list);
up_write(&user_list_lock);
filp->private_data = as;
}
return as ? 0 : -ENOMEM;
}
static struct file_operations apm_bios_fops = {
.owner = THIS_MODULE,
.read = apm_read,
.poll = apm_poll,
.ioctl = apm_ioctl,
.open = apm_open,
.release = apm_release,
};
static struct miscdevice apm_device = {
.minor = APM_MINOR_DEV,
.name = "apm_bios",
.fops = &apm_bios_fops
};
#ifdef CONFIG_PROC_FS
/*
* Arguments, with symbols from linux/apm_bios.h.
*
* 0) Linux driver version (this will change if format changes)
* 1) APM BIOS Version. Usually 1.0, 1.1 or 1.2.
* 2) APM flags from APM Installation Check (0x00):
* bit 0: APM_16_BIT_SUPPORT
* bit 1: APM_32_BIT_SUPPORT
* bit 2: APM_IDLE_SLOWS_CLOCK
* bit 3: APM_BIOS_DISABLED
* bit 4: APM_BIOS_DISENGAGED
* 3) AC line status
* 0x00: Off-line
* 0x01: On-line
* 0x02: On backup power (BIOS >= 1.1 only)
* 0xff: Unknown
* 4) Battery status
* 0x00: High
* 0x01: Low
* 0x02: Critical
* 0x03: Charging
* 0x04: Selected battery not present (BIOS >= 1.2 only)
* 0xff: Unknown
* 5) Battery flag
* bit 0: High
* bit 1: Low
* bit 2: Critical
* bit 3: Charging
* bit 7: No system battery
* 0xff: Unknown
* 6) Remaining battery life (percentage of charge):
* 0-100: valid
* -1: Unknown
* 7) Remaining battery life (time units):
* Number of remaining minutes or seconds
* -1: Unknown
* 8) min = minutes; sec = seconds
*/
static int apm_get_info(char *buf, char **start, off_t fpos, int length)
{
struct apm_power_info info;
char *units;
int ret;
info.ac_line_status = 0xff;
info.battery_status = 0xff;
info.battery_flag = 0xff;
info.battery_life = -1;
info.time = -1;
info.units = -1;
if (apm_get_power_status)
apm_get_power_status(&info);
switch (info.units) {
default: units = "?"; break;
case 0: units = "min"; break;
case 1: units = "sec"; break;
}
ret = sprintf(buf, "%s 1.2 0x%02x 0x%02x 0x%02x 0x%02x %d%% %d %s\n",
driver_version, APM_32_BIT_SUPPORT,
info.ac_line_status, info.battery_status,
info.battery_flag, info.battery_life,
info.time, units);
return ret;
}
#endif
static int kapmd(void *arg)
{
do {
apm_event_t event;
int ret;
wait_event_interruptible(kapmd_wait,
!queue_empty(&kapmd_queue) || kthread_should_stop());
if (kthread_should_stop())
break;
spin_lock_irq(&kapmd_queue_lock);
event = 0;
if (!queue_empty(&kapmd_queue))
event = queue_get_event(&kapmd_queue);
spin_unlock_irq(&kapmd_queue_lock);
switch (event) {
case 0:
break;
case APM_LOW_BATTERY:
case APM_POWER_STATUS_CHANGE:
queue_event(event);
break;
case APM_USER_SUSPEND:
case APM_SYS_SUSPEND:
ret = queue_suspend_event(event, NULL);
if (ret < 0) {
/*
* We were busy. Try again in 50ms.
*/
queue_add_event(&kapmd_queue, event);
msleep(50);
}
if (ret > 0)
apm_suspend();
break;
case APM_CRITICAL_SUSPEND:
apm_suspend();
break;
}
} while (1);
return 0;
}
static int __init apm_init(void)
{
int ret;
if (apm_disabled) {
printk(KERN_NOTICE "apm: disabled on user request.\n");
return -ENODEV;
}
kapmd_tsk = kthread_create(kapmd, NULL, "kapmd");
if (IS_ERR(kapmd_tsk)) {
ret = PTR_ERR(kapmd_tsk);
kapmd_tsk = NULL;
return ret;
}
kapmd_tsk->flags |= PF_NOFREEZE;
wake_up_process(kapmd_tsk);
#ifdef CONFIG_PROC_FS
create_proc_info_entry("apm", 0, NULL, apm_get_info);
#endif
ret = misc_register(&apm_device);
if (ret != 0) {
remove_proc_entry("apm", NULL);
kthread_stop(kapmd_tsk);
}
return ret;
}
static void __exit apm_exit(void)
{
misc_deregister(&apm_device);
remove_proc_entry("apm", NULL);
kthread_stop(kapmd_tsk);
}
module_init(apm_init);
module_exit(apm_exit);
MODULE_AUTHOR("Stephen Rothwell");
MODULE_DESCRIPTION("Advanced Power Management");
MODULE_LICENSE("GPL");
#ifndef MODULE
static int __init apm_setup(char *str)
{
while ((str != NULL) && (*str != '\0')) {
if (strncmp(str, "off", 3) == 0)
apm_disabled = 1;
if (strncmp(str, "on", 2) == 0)
apm_disabled = 0;
str = strchr(str, ',');
if (str != NULL)
str += strspn(str, ", \t");
}
return 1;
}
__setup("apm=", apm_setup);
#endif
/**
* apm_queue_event - queue an APM event for kapmd
* @event: APM event
*
* Queue an APM event for kapmd to process and ultimately take the
* appropriate action. Only a subset of events are handled:
* %APM_LOW_BATTERY
* %APM_POWER_STATUS_CHANGE
* %APM_USER_SUSPEND
* %APM_SYS_SUSPEND
* %APM_CRITICAL_SUSPEND
*/
void apm_queue_event(apm_event_t event)
{
unsigned long flags;
spin_lock_irqsave(&kapmd_queue_lock, flags);
queue_add_event(&kapmd_queue, event);
spin_unlock_irqrestore(&kapmd_queue_lock, flags);
wake_up_interruptible(&kapmd_wait);
}
EXPORT_SYMBOL(apm_queue_event);