diff options
author | Andriy Skulysh <askulysh@gmail.com> | 2006-09-27 09:20:22 +0200 |
---|---|---|
committer | Paul Mundt <lethal@linux-sh.org> | 2006-09-27 09:20:22 +0200 |
commit | 3aa770e7972723f479122cf66b529017d2175289 (patch) | |
tree | f70d870381cec8034693704e346c53b311db688f /arch | |
parent | sh: Free up some and document PTEL flags. (diff) | |
download | linux-3aa770e7972723f479122cf66b529017d2175289.tar.xz linux-3aa770e7972723f479122cf66b529017d2175289.zip |
sh: APM/PM support.
This adds some simple PM stubs and the basic APM interfaces,
primarily for use by hp6xx, where the existing userland
expects it.
Signed-off-by: Andriy Skulysh <askulysh@gmail.com>
Signed-off-by: Paul Mundt <lethal@linux-sh.org>
Diffstat (limited to 'arch')
-rw-r--r-- | arch/sh/Kconfig | 10 | ||||
-rw-r--r-- | arch/sh/boards/hp6xx/Makefile | 5 | ||||
-rw-r--r-- | arch/sh/boards/hp6xx/hp6xx_apm.c | 123 | ||||
-rw-r--r-- | arch/sh/boards/hp6xx/pm.c | 88 | ||||
-rw-r--r-- | arch/sh/boards/hp6xx/pm_wakeup.S | 58 | ||||
-rw-r--r-- | arch/sh/boards/hp6xx/setup.c | 14 | ||||
-rw-r--r-- | arch/sh/kernel/Makefile | 2 | ||||
-rw-r--r-- | arch/sh/kernel/apm.c | 539 | ||||
-rw-r--r-- | arch/sh/kernel/entry.S | 4 | ||||
-rw-r--r-- | arch/sh/kernel/pm.c | 88 | ||||
-rw-r--r-- | arch/sh/kernel/sh_ksyms.c | 4 | ||||
-rw-r--r-- | arch/sh/kernel/time.c | 25 | ||||
-rw-r--r-- | arch/sh/kernel/timers/timer-tmu.c | 18 |
13 files changed, 973 insertions, 5 deletions
diff --git a/arch/sh/Kconfig b/arch/sh/Kconfig index fe982f0eba23..d1d724447826 100644 --- a/arch/sh/Kconfig +++ b/arch/sh/Kconfig @@ -638,6 +638,16 @@ source "fs/Kconfig.binfmt" endmenu +menu "Power management options (EXPERIMENTAL)" +depends on EXPERIMENTAL + +source kernel/power/Kconfig + +config APM + bool "Advanced Power Management Emulation" + depends on PM +endmenu + source "net/Kconfig" source "drivers/Kconfig" diff --git a/arch/sh/boards/hp6xx/Makefile b/arch/sh/boards/hp6xx/Makefile index 927fe0aa5dfa..fdf98ffcf437 100644 --- a/arch/sh/boards/hp6xx/Makefile +++ b/arch/sh/boards/hp6xx/Makefile @@ -2,5 +2,8 @@ # Makefile for the HP6xx specific parts of the kernel # -obj-y := mach.o setup.o +obj-y := mach.o setup.o +obj-$(CONFIG_PM) += pm.o pm_wakeup.o +obj-$(CONFIG_APM) += hp6xx_apm.o + diff --git a/arch/sh/boards/hp6xx/hp6xx_apm.c b/arch/sh/boards/hp6xx/hp6xx_apm.c new file mode 100644 index 000000000000..ad0e712c29f6 --- /dev/null +++ b/arch/sh/boards/hp6xx/hp6xx_apm.c @@ -0,0 +1,123 @@ +/* + * bios-less APM driver for hp680 + * + * Copyright 2005 (c) Andriy Skulysh <askulysh@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License. + */ +#include <linux/config.h> +#include <linux/module.h> +#include <linux/apm_bios.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <asm/io.h> +#include <asm/apm.h> +#include <asm/adc.h> +#include <asm/hp6xx/hp6xx.h> + +#define SH7709_PGDR 0xa400012c + +#define APM_CRITICAL 10 +#define APM_LOW 30 + +#define HP680_BATTERY_MAX 875 +#define HP680_BATTERY_MIN 600 +#define HP680_BATTERY_AC_ON 900 + +#define MODNAME "hp6x0_apm" + +static int hp6x0_apm_get_info(char *buf, char **start, off_t fpos, int length) +{ + u8 pgdr; + char *p; + int battery_status; + int battery_flag; + int ac_line_status; + int time_units = APM_BATTERY_LIFE_UNKNOWN; + + int battery = adc_single(ADC_CHANNEL_BATTERY); + int backup = adc_single(ADC_CHANNEL_BACKUP); + int charging = adc_single(ADC_CHANNEL_CHARGE); + int percentage; + + percentage = 100 * (battery - HP680_BATTERY_MIN) / + (HP680_BATTERY_MAX - HP680_BATTERY_MIN); + + ac_line_status = (battery > HP680_BATTERY_AC_ON) ? + APM_AC_ONLINE : APM_AC_OFFLINE; + + p = buf; + + pgdr = ctrl_inb(SH7709_PGDR); + if (pgdr & PGDR_MAIN_BATTERY_OUT) { + battery_status = APM_BATTERY_STATUS_NOT_PRESENT; + battery_flag = 0x80; + percentage = -1; + } else if (charging < 8 ) { + battery_status = APM_BATTERY_STATUS_CHARGING; + battery_flag = 0x08; + ac_line_status = 0xff; + } else if (percentage <= APM_CRITICAL) { + battery_status = APM_BATTERY_STATUS_CRITICAL; + battery_flag = 0x04; + } else if (percentage <= APM_LOW) { + battery_status = APM_BATTERY_STATUS_LOW; + battery_flag = 0x02; + } else { + battery_status = APM_BATTERY_STATUS_HIGH; + battery_flag = 0x01; + } + + p += sprintf(p, "1.0 1.2 0x%02x 0x%02x 0x%02x 0x%02x %d%% %d %s\n", + APM_32_BIT_SUPPORT, + ac_line_status, + battery_status, + battery_flag, + percentage, + time_units, + "min"); + p += sprintf(p, "bat=%d backup=%d charge=%d\n", + battery, backup, charging); + + return p - buf; +} + +static irqreturn_t hp6x0_apm_interrupt(int irq, void *dev, struct pt_regs *regs) +{ + if (!apm_suspended) + apm_queue_event(APM_USER_SUSPEND); + + return IRQ_HANDLED; +} + +static int __init hp6x0_apm_init(void) +{ + int ret; + + ret = request_irq(HP680_BTN_IRQ, hp6x0_apm_interrupt, + SA_INTERRUPT, MODNAME, 0); + if (unlikely(ret < 0)) { + printk(KERN_ERR MODNAME ": IRQ %d request failed\n", + HP680_BTN_IRQ); + return ret; + } + + apm_get_info = hp6x0_apm_get_info; + + return ret; +} + +static void __exit hp6x0_apm_exit(void) +{ + free_irq(HP680_BTN_IRQ, 0); + apm_get_info = 0; +} + +module_init(hp6x0_apm_init); +module_exit(hp6x0_apm_exit); + +MODULE_AUTHOR("Adriy Skulysh"); +MODULE_DESCRIPTION("hp6xx Advanced Power Management"); +MODULE_LICENSE("GPL"); diff --git a/arch/sh/boards/hp6xx/pm.c b/arch/sh/boards/hp6xx/pm.c new file mode 100644 index 000000000000..0e501bcbd7a9 --- /dev/null +++ b/arch/sh/boards/hp6xx/pm.c @@ -0,0 +1,88 @@ +/* + * hp6x0 Power Management Routines + * + * Copyright (c) 2006 Andriy Skulysh <askulsyh@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License. + */ +#include <linux/config.h> +#include <linux/init.h> +#include <linux/suspend.h> +#include <linux/errno.h> +#include <linux/time.h> +#include <asm/io.h> +#include <asm/hd64461.h> +#include <asm/hp6xx/hp6xx.h> +#include <asm/cpu/dac.h> +#include <asm/pm.h> + +#define STBCR 0xffffff82 +#define STBCR2 0xffffff88 + +static int hp6x0_pm_enter(suspend_state_t state) +{ + u8 stbcr, stbcr2; +#ifdef CONFIG_HD64461_ENABLER + u8 scr; + u16 hd64461_stbcr; +#endif + + if (state != PM_SUSPEND_MEM) + return -EINVAL; + +#ifdef CONFIG_HD64461_ENABLER + outb(0, HD64461_PCC1CSCIER); + + scr = inb(HD64461_PCC1SCR); + scr |= HD64461_PCCSCR_VCC1; + outb(scr, HD64461_PCC1SCR); + + hd64461_stbcr = inw(HD64461_STBCR); + hd64461_stbcr |= HD64461_STBCR_SPC1ST; + outw(hd64461_stbcr, HD64461_STBCR); +#endif + + ctrl_outb(0x1f, DACR); + + stbcr = ctrl_inb(STBCR); + ctrl_outb(0x01, STBCR); + + stbcr2 = ctrl_inb(STBCR2); + ctrl_outb(0x7f , STBCR2); + + outw(0xf07f, HD64461_SCPUCR); + + pm_enter(); + + outw(0, HD64461_SCPUCR); + ctrl_outb(stbcr, STBCR); + ctrl_outb(stbcr2, STBCR2); + +#ifdef CONFIG_HD64461_ENABLER + hd64461_stbcr = inw(HD64461_STBCR); + hd64461_stbcr &= ~HD64461_STBCR_SPC1ST; + outw(hd64461_stbcr, HD64461_STBCR); + + outb(0x4c, HD64461_PCC1CSCIER); + outb(0x00, HD64461_PCC1CSCR); +#endif + + return 0; +} + +/* + * Set to PM_DISK_FIRMWARE so we can quickly veto suspend-to-disk. + */ +static struct pm_ops hp6x0_pm_ops = { + .pm_disk_mode = PM_DISK_FIRMWARE, + .enter = hp6x0_pm_enter, +}; + +static int __init hp6x0_pm_init(void) +{ + pm_set_ops(&hp6x0_pm_ops); + return 0; +} + +late_initcall(hp6x0_pm_init); diff --git a/arch/sh/boards/hp6xx/pm_wakeup.S b/arch/sh/boards/hp6xx/pm_wakeup.S new file mode 100644 index 000000000000..45e9bf0b9115 --- /dev/null +++ b/arch/sh/boards/hp6xx/pm_wakeup.S @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2006 Andriy Skulysh <askulsyh@gmail.com> + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file "COPYING" in the main directory of this archive + * for more details. + * + */ + +#include <linux/linkage.h> +#include <asm/cpu/mmu_context.h> + +#define k0 r0 +#define k1 r1 +#define k2 r2 +#define k3 r3 +#define k4 r4 + +/* + * Kernel mode register usage: + * k0 scratch + * k1 scratch + * k2 scratch (Exception code) + * k3 scratch (Return address) + * k4 scratch + * k5 reserved + * k6 Global Interrupt Mask (0--15 << 4) + * k7 CURRENT_THREAD_INFO (pointer to current thread info) + */ + +ENTRY(wakeup_start) +! clear STBY bit + mov #-126, k2 + and #127, k0 + mov.b k0, @k2 +! enable refresh + mov.l 5f, k1 + mov.w 6f, k0 + mov.w k0, @k1 +! jump to handler + mov.l 2f, k2 + mov.l 3f, k3 + mov.l @k2, k2 + + mov.l 4f, k1 + jmp @k1 + nop + + .align 2 +1: .long EXPEVT +2: .long INTEVT +3: .long ret_from_irq +4: .long handle_exception +5: .long 0xffffff68 +6: .word 0x0524 + +ENTRY(wakeup_end) + nop diff --git a/arch/sh/boards/hp6xx/setup.c b/arch/sh/boards/hp6xx/setup.c index 71f315663cc9..5fc00f0727c4 100644 --- a/arch/sh/boards/hp6xx/setup.c +++ b/arch/sh/boards/hp6xx/setup.c @@ -15,6 +15,9 @@ #include <asm/hp6xx/hp6xx.h> #include <asm/cpu/dac.h> +#define SCPCR 0xa4000116 +#define SCPDR 0xa4000136 + const char *get_system_type(void) { return "HP6xx"; @@ -24,6 +27,7 @@ int __init platform_setup(void) { u8 v8; u16 v; + v = inw(HD64461_STBCR); v |= HD64461_STBCR_SURTST | HD64461_STBCR_SIRST | HD64461_STBCR_STM1ST | HD64461_STBCR_STM0ST | @@ -50,5 +54,15 @@ int __init platform_setup(void) v8 &= ~DACR_DAE; ctrl_outb(v8,DACR); + v8 = ctrl_inb(SCPDR); + v8 |= SCPDR_TS_SCAN_X | SCPDR_TS_SCAN_Y; + v8 &= ~SCPDR_TS_SCAN_ENABLE; + ctrl_outb(v8, SCPDR); + + v = ctrl_inw(SCPCR); + v &= ~SCPCR_TS_MASK; + v |= SCPCR_TS_ENABLE; + ctrl_outw(v, SCPCR); + return 0; } diff --git a/arch/sh/kernel/Makefile b/arch/sh/kernel/Makefile index 2de82b66af4a..0e2148f63e7e 100644 --- a/arch/sh/kernel/Makefile +++ b/arch/sh/kernel/Makefile @@ -18,3 +18,5 @@ obj-$(CONFIG_SH_CPU_FREQ) += cpufreq.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_EARLY_PRINTK) += early_printk.o obj-$(CONFIG_KEXEC) += machine_kexec.o relocate_kernel.o +obj-$(CONFIG_APM) += apm.o +obj-$(CONFIG_PM) += pm.o diff --git a/arch/sh/kernel/apm.c b/arch/sh/kernel/apm.c new file mode 100644 index 000000000000..871e7d640002 --- /dev/null +++ b/arch/sh/kernel/apm.c @@ -0,0 +1,539 @@ +/* + * bios-less APM driver for hp680 + * + * Copyright 2005 (c) Andriy Skulysh <askulysh@gmail.com> + * + * based on ARM APM driver by + * 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/config.h> +#include <linux/module.h> +#include <linux/poll.h> +#include <linux/timer.h> +#include <linux/slab.h> +#include <linux/proc_fs.h> +#include <linux/miscdevice.h> +#include <linux/apm_bios.h> +#include <linux/pm.h> +#include <linux/pm_legacy.h> +#include <asm/apm.h> + +#define MODNAME "apm" + +/* + * The apm_bios device is one of the misc char devices. + * This is its minor number. + */ +#define APM_MINOR_DEV 134 + +/* + * 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_DONE 4 /* suspend completed */ + + struct apm_queue queue; +}; + +/* + * Local variables + */ +static int suspends_pending; + +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 DECLARE_COMPLETION(kapmd_exit); +static DEFINE_SPINLOCK(kapmd_queue_lock); +static struct apm_queue kapmd_queue; + +int apm_suspended; +EXPORT_SYMBOL(apm_suspended); + +/* Platform-specific apm_read_proc(). */ +int (*apm_get_info)(char *buf, char **start, off_t fpos, int length); +EXPORT_SYMBOL(apm_get_info); + +/* + * 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_one_user(struct apm_user *as, apm_event_t event) +{ + if (as->suser && as->writer) { + switch (event) { + case APM_SYS_SUSPEND: + case APM_USER_SUSPEND: + /* + * If this user already has a suspend pending, + * don't queue another one. + */ + if (as->suspend_state != SUSPEND_NONE) + return; + + as->suspend_state = SUSPEND_PENDING; + suspends_pending++; + break; + } + } + queue_add_event(&as->queue, event); +} + +static void queue_event(apm_event_t event, struct apm_user *sender) +{ + struct apm_user *as; + + down_read(&user_list_lock); + + list_for_each_entry(as, &apm_user_list, list) + if (as != sender && as->reader) + queue_event_one_user(as, event); + + up_read(&user_list_lock); + wake_up_interruptible(&apm_waitqueue); +} + +/** + * 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) +{ + spin_lock_irq(&kapmd_queue_lock); + queue_add_event(&kapmd_queue, event); + spin_unlock_irq(&kapmd_queue_lock); + + wake_up_interruptible(&kapmd_wait); +} +EXPORT_SYMBOL(apm_queue_event); + +static void apm_suspend(void) +{ + struct apm_user *as; + int err; + + apm_suspended = 1; + 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, NULL); + + /* + * Finally, wake up anyone who is sleeping on the suspend. + */ + down_read(&user_list_lock); + list_for_each_entry(as, &apm_user_list, list) { + as->suspend_result = err; + as->suspend_state = SUSPEND_DONE; + } + up_read(&user_list_lock); + + wake_up(&apm_suspend_waitqueue); + apm_suspended = 0; +} + +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; + + if (event == APM_SYS_SUSPEND || event == APM_USER_SUSPEND) + as->suspend_state = SUSPEND_READ; + + 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: + as->suspend_result = -EINTR; + + if (as->suspend_state == SUSPEND_READ) { + /* + * 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--; + } else { + /* + * 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. + */ + queue_event(APM_USER_SUSPEND, as); + } + + /* + * If there are no further acknowledges required, suspend + * the system. + */ + if (suspends_pending == 0) + apm_suspend(); + + /* + * Wait for the suspend/resume to complete. If there are + * pending acknowledges, we wait here for them. + * + * Note that 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; + + /* + * Note: do not allow a thread which is acking the suspend + * to escape until the resume is complete. + */ + if (as->suspend_state == SUSPEND_ACKED) + wait_event(apm_suspend_waitqueue, + as->suspend_state == SUSPEND_DONE); + else + wait_event_interruptible(apm_suspend_waitqueue, + as->suspend_state == SUSPEND_DONE); + + current->flags = flags; + err = as->suspend_result; + as->suspend_state = SUSPEND_NONE; + break; + } + + return err; +} + +static int apm_release(struct inode * inode, struct file * filp) +{ + struct apm_user *as = filp->private_data; + 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. + */ + if (as->suspend_state != SUSPEND_NONE) { + suspends_pending -= 1; + if (suspends_pending == 0) + 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_read_proc(char *buf, char **start, off_t fpos, int length) +{ + if (likely(apm_get_info)) + return apm_get_info(buf, start, fpos, length); + + return -EINVAL; +} +#endif + +static int kapmd(void *arg) +{ + daemonize("kapmd"); + current->flags |= PF_NOFREEZE; + + do { + apm_event_t event; + + wait_event_interruptible(kapmd_wait, + !queue_empty(&kapmd_queue) || !pm_active); + + if (!pm_active) + 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, NULL); + break; + + case APM_USER_SUSPEND: + case APM_SYS_SUSPEND: + queue_event(event, NULL); + if (suspends_pending == 0) + apm_suspend(); + break; + + case APM_CRITICAL_SUSPEND: + apm_suspend(); + break; + } + } while (1); + + complete_and_exit(&kapmd_exit, 0); +} + +static int __init apm_init(void) +{ + int ret; + + pm_active = 1; + + ret = kernel_thread(kapmd, NULL, CLONE_KERNEL); + if (unlikely(ret < 0)) { + pm_active = 0; + return ret; + } + + create_proc_info_entry("apm", 0, NULL, apm_read_proc); + + ret = misc_register(&apm_device); + if (unlikely(ret != 0)) { + remove_proc_entry("apm", NULL); + + pm_active = 0; + wake_up(&kapmd_wait); + wait_for_completion(&kapmd_exit); + } + + return ret; +} + +static void __exit apm_exit(void) +{ + misc_deregister(&apm_device); + remove_proc_entry("apm", NULL); + + pm_active = 0; + wake_up(&kapmd_wait); + wait_for_completion(&kapmd_exit); +} + +module_init(apm_init); +module_exit(apm_exit); + +MODULE_AUTHOR("Stephen Rothwell, Andriy Skulysh"); +MODULE_DESCRIPTION("Advanced Power Management"); +MODULE_LICENSE("GPL"); diff --git a/arch/sh/kernel/entry.S b/arch/sh/kernel/entry.S index 306eb8d83de5..b087d34dba35 100644 --- a/arch/sh/kernel/entry.S +++ b/arch/sh/kernel/entry.S @@ -308,7 +308,7 @@ ENTRY(exception_error) .align 2 ret_from_exception: preempt_stop() -ret_from_irq: +ENTRY(ret_from_irq) ! mov #OFF_SR, r0 mov.l @(r0,r15), r0 ! get status register @@ -704,7 +704,7 @@ interrupt: ! ! .align 2 -handle_exception: +ENTRY(handle_exception) ! Using k0, k1 for scratch registers (r0_bank1, r1_bank), ! save all registers onto stack. ! diff --git a/arch/sh/kernel/pm.c b/arch/sh/kernel/pm.c new file mode 100644 index 000000000000..10ab62c9aede --- /dev/null +++ b/arch/sh/kernel/pm.c @@ -0,0 +1,88 @@ +/* + * Generic Power Management Routine + * + * Copyright (c) 2006 Andriy Skulysh <askulsyh@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License. + */ +#include <linux/suspend.h> +#include <linux/delay.h> +#include <linux/gfp.h> +#include <asm/freq.h> +#include <asm/io.h> +#include <asm/watchdog.h> +#include <asm/pm.h> + +#define INTR_OFFSET 0x600 + +#define STBCR 0xffffff82 +#define STBCR2 0xffffff88 + +#define STBCR_STBY 0x80 +#define STBCR_MSTP2 0x04 + +#define MCR 0xffffff68 +#define RTCNT 0xffffff70 + +#define MCR_RMODE 2 +#define MCR_RFSH 4 + +void pm_enter(void) +{ + u8 stbcr, csr; + u16 frqcr, mcr; + u32 vbr_new, vbr_old; + + set_bl_bit(); + + /* set wdt */ + csr = sh_wdt_read_csr(); + csr &= ~WTCSR_TME; + csr |= WTCSR_CKS_4096; + sh_wdt_write_csr(csr); + csr = sh_wdt_read_csr(); + sh_wdt_write_cnt(0); + + /* disable PLL1 */ + frqcr = ctrl_inw(FRQCR); + frqcr &= ~(FRQCR_PLLEN | FRQCR_PSTBY); + ctrl_outw(frqcr, FRQCR); + + /* enable standby */ + stbcr = ctrl_inb(STBCR); + ctrl_outb(stbcr | STBCR_STBY | STBCR_MSTP2, STBCR); + + /* set self-refresh */ + mcr = ctrl_inw(MCR); + ctrl_outw(mcr & ~MCR_RFSH, MCR); + + /* set interrupt handler */ + asm volatile("stc vbr, %0" : "=r" (vbr_old)); + vbr_new = get_zeroed_page(GFP_ATOMIC); + udelay(50); + memcpy((void*)(vbr_new + INTR_OFFSET), + &wakeup_start, &wakeup_end - &wakeup_start); + asm volatile("ldc %0, vbr" : : "r" (vbr_new)); + + ctrl_outw(0, RTCNT); + ctrl_outw(mcr | MCR_RFSH | MCR_RMODE, MCR); + + cpu_sleep(); + + asm volatile("ldc %0, vbr" : : "r" (vbr_old)); + + free_page(vbr_new); + + /* enable PLL1 */ + frqcr = ctrl_inw(FRQCR); + frqcr |= FRQCR_PSTBY; + ctrl_outw(frqcr, FRQCR); + udelay(50); + frqcr |= FRQCR_PLLEN; + ctrl_outw(frqcr, FRQCR); + + ctrl_outb(stbcr, STBCR); + + clear_bl_bit(); +} diff --git a/arch/sh/kernel/sh_ksyms.c b/arch/sh/kernel/sh_ksyms.c index b73feafcd06e..fd73ab0326e9 100644 --- a/arch/sh/kernel/sh_ksyms.c +++ b/arch/sh/kernel/sh_ksyms.c @@ -116,6 +116,10 @@ EXPORT_SYMBOL(__down_trylock); EXPORT_SYMBOL(synchronize_irq); #endif +#ifdef CONFIG_PM +EXPORT_SYMBOL(pm_suspend); +#endif + EXPORT_SYMBOL(csum_partial); #ifdef CONFIG_IPV6 EXPORT_SYMBOL(csum_ipv6_magic); diff --git a/arch/sh/kernel/time.c b/arch/sh/kernel/time.c index a1589f85499d..c8db6ca4f9d1 100644 --- a/arch/sh/kernel/time.c +++ b/arch/sh/kernel/time.c @@ -143,8 +143,33 @@ void handle_timer_tick(struct pt_regs *regs) } } +#ifdef CONFIG_PM +int timer_suspend(struct sys_device *dev, pm_message_t state) +{ + struct sys_timer *sys_timer = container_of(dev, struct sys_timer, dev); + + sys_timer->ops->stop(); + + return 0; +} + +int timer_resume(struct sys_device *dev) +{ + struct sys_timer *sys_timer = container_of(dev, struct sys_timer, dev); + + sys_timer->ops->start(); + + return 0; +} +#else +#define timer_suspend NULL +#define timer_resume NULL +#endif + static struct sysdev_class timer_sysclass = { set_kset_name("timer"), + .suspend = timer_suspend, + .resume = timer_resume, }; static int __init timer_init_sysfs(void) diff --git a/arch/sh/kernel/timers/timer-tmu.c b/arch/sh/kernel/timers/timer-tmu.c index d4212add53b2..ea4bdf8cdf0d 100644 --- a/arch/sh/kernel/timers/timer-tmu.c +++ b/arch/sh/kernel/timers/timer-tmu.c @@ -188,6 +188,18 @@ static struct clk tmu0_clk = { .ops = &tmu_clk_ops, }; +static int tmu_timer_start(void) +{ + ctrl_outb(TMU_TSTR_INIT, TMU_TSTR); + return 0; +} + +static int tmu_timer_stop(void) +{ + ctrl_outb(0, TMU_TSTR); + return 0; +} + static int tmu_timer_init(void) { unsigned long interval; @@ -197,7 +209,7 @@ static int tmu_timer_init(void) tmu0_clk.parent = clk_get("module_clk"); /* Start TMU0 */ - ctrl_outb(0, TMU_TSTR); + tmu_timer_stop(); #if !defined(CONFIG_CPU_SUBTYPE_SH7300) && !defined(CONFIG_CPU_SUBTYPE_SH7760) ctrl_outb(TMU_TOCR_INIT, TMU_TOCR); #endif @@ -211,13 +223,15 @@ static int tmu_timer_init(void) ctrl_outl(interval, TMU0_TCOR); ctrl_outl(interval, TMU0_TCNT); - ctrl_outb(TMU_TSTR_INIT, TMU_TSTR); + tmu_timer_start(); return 0; } struct sys_timer_ops tmu_timer_ops = { .init = tmu_timer_init, + .start = tmu_timer_start, + .stop = tmu_timer_stop, .get_frequency = tmu_timer_get_frequency, .get_offset = tmu_timer_get_offset, }; |