2007-02-09 17:08:57 +00:00
/*
* 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>
2007-07-17 04:03:35 -07:00
# include <linux/freezer.h>
2007-02-09 17:08:57 +00:00
# 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 .
*/
flags = current - > flags ;
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 .
*/
flags = current - > flags ;
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 50 ms .
*/
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 ;
}
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 ) ;