x86, olpc: Add XO-1 suspend/resume support
authorDaniel Drake <dsd@laptop.org>
Sat, 25 Jun 2011 16:34:11 +0000 (17:34 +0100)
committerH. Peter Anvin <hpa@linux.intel.com>
Wed, 6 Jul 2011 21:44:32 +0000 (14:44 -0700)
Add code needed for basic suspend/resume of the XO-1 laptop.
Based on earlier work by Jordan Crouse, Andres Salomon, and others.

This patch incorporates all earlier feedback from Thomas Gleixner. To
clarify a certain point (now more obvious in the code itself):
On resume, OpenFirmware returns execution to Linux in protected mode
with a kernel-compatible GDT already set up. The changes and
simplifications suggested have all been included.

Signed-off-by: Daniel Drake <dsd@laptop.org>
Link: http://lkml.kernel.org/r/1309019658-1712-5-git-send-email-dsd@laptop.org
Acked-by: Andres Salomon <dilinger@queued.net>
Signed-off-by: H. Peter Anvin <hpa@linux.intel.com>
arch/x86/Kconfig
arch/x86/include/asm/olpc.h
arch/x86/platform/olpc/Makefile
arch/x86/platform/olpc/olpc-xo1-pm.c
arch/x86/platform/olpc/xo1-wakeup.S [new file with mode: 0644]
include/linux/cs5535.h

index 29615ee688a590e2ab86fd7bb6729f603c2b2df7..f473151ac991586e6bb0aadddffe7d0d0d3c90ce 100644 (file)
@@ -2075,10 +2075,10 @@ config OLPC
 
 config OLPC_XO1_PM
        bool "OLPC XO-1 Power Management"
-       depends on OLPC && MFD_CS5535
+       depends on OLPC && MFD_CS5535 && PM_SLEEP
        select MFD_CORE
        ---help---
-         Add support for poweroff of the OLPC XO-1 laptop.
+         Add support for poweroff and suspend of the OLPC XO-1 laptop.
 
 endif # X86_32
 
index 5ca6801b75f3ad1d907419f48ebe40ae9ef199f0..10ea59594b71abf6acbc75e55cca495918fb20ac 100644 (file)
@@ -76,6 +76,12 @@ static inline int olpc_has_dcon(void)
 
 #endif
 
+#ifdef CONFIG_OLPC_XO1_PM
+extern void do_olpc_suspend_lowlevel(void);
+extern void olpc_xo1_pm_wakeup_set(u16 value);
+extern void olpc_xo1_pm_wakeup_clear(u16 value);
+#endif
+
 extern int pci_olpc_init(void);
 
 /* EC related functions */
@@ -88,9 +94,12 @@ extern int olpc_ec_mask_unset(uint8_t bits);
 
 /* EC commands */
 
-#define EC_FIRMWARE_REV                0x08
-#define EC_WLAN_ENTER_RESET    0x35
-#define EC_WLAN_LEAVE_RESET    0x25
+#define EC_FIRMWARE_REV                        0x08
+#define EC_WAKE_UP_WLAN                        0x24
+#define EC_WLAN_LEAVE_RESET            0x25
+#define EC_SET_SCI_INHIBIT             0x32
+#define EC_SET_SCI_INHIBIT_RELEASE     0x34
+#define EC_WLAN_ENTER_RESET            0x35
 
 /* SCI source values */
 
index cd250387d4bb2a829e441bf140ab0e397f81904a..1ae7bed89821985c15183389e7d38a656a301587 100644 (file)
@@ -1,2 +1,2 @@
 obj-$(CONFIG_OLPC)             += olpc.o olpc_ofw.o olpc_dt.o
-obj-$(CONFIG_OLPC_XO1_PM)              += olpc-xo1-pm.o
+obj-$(CONFIG_OLPC_XO1_PM)              += olpc-xo1-pm.o xo1-wakeup.o
index a2a59d36824dc815a391bfcea7d73fd427807594..6f3855a5a2f783dce23282d5a1fa08bf03d9e4b4 100644 (file)
@@ -16,6 +16,7 @@
 #include <linux/platform_device.h>
 #include <linux/pm.h>
 #include <linux/mfd/core.h>
+#include <linux/suspend.h>
 
 #include <asm/io.h>
 #include <asm/olpc.h>
 static unsigned long acpi_base;
 static unsigned long pms_base;
 
+static u16 wakeup_mask = CS5536_PM_PWRBTN;
+
+static struct {
+       unsigned long address;
+       unsigned short segment;
+} ofw_bios_entry = { 0xF0000 + PAGE_OFFSET, __KERNEL_CS };
+
+/* Set bits in the wakeup mask */
+void olpc_xo1_pm_wakeup_set(u16 value)
+{
+       wakeup_mask |= value;
+}
+EXPORT_SYMBOL_GPL(olpc_xo1_pm_wakeup_set);
+
+/* Clear bits in the wakeup mask */
+void olpc_xo1_pm_wakeup_clear(u16 value)
+{
+       wakeup_mask &= ~value;
+}
+EXPORT_SYMBOL_GPL(olpc_xo1_pm_wakeup_clear);
+
+static int xo1_power_state_enter(suspend_state_t pm_state)
+{
+       unsigned long saved_sci_mask;
+       int r;
+
+       /* Only STR is supported */
+       if (pm_state != PM_SUSPEND_MEM)
+               return -EINVAL;
+
+       r = olpc_ec_cmd(EC_SET_SCI_INHIBIT, NULL, 0, NULL, 0);
+       if (r)
+               return r;
+
+       /*
+        * Save SCI mask (this gets lost since PM1_EN is used as a mask for
+        * wakeup events, which is not necessarily the same event set)
+        */
+       saved_sci_mask = inl(acpi_base + CS5536_PM1_STS);
+       saved_sci_mask &= 0xffff0000;
+
+       /* Save CPU state */
+       do_olpc_suspend_lowlevel();
+
+       /* Resume path starts here */
+
+       /* Restore SCI mask (using dword access to CS5536_PM1_EN) */
+       outl(saved_sci_mask, acpi_base + CS5536_PM1_STS);
+
+       /* Tell the EC to stop inhibiting SCIs */
+       olpc_ec_cmd(EC_SET_SCI_INHIBIT_RELEASE, NULL, 0, NULL, 0);
+
+       /*
+        * Tell the wireless module to restart USB communication.
+        * Must be done twice.
+        */
+       olpc_ec_cmd(EC_WAKE_UP_WLAN, NULL, 0, NULL, 0);
+       olpc_ec_cmd(EC_WAKE_UP_WLAN, NULL, 0, NULL, 0);
+
+       return 0;
+}
+
+asmlinkage int xo1_do_sleep(u8 sleep_state)
+{
+       void *pgd_addr = __va(read_cr3());
+
+       /* Program wakeup mask (using dword access to CS5536_PM1_EN) */
+       outl(wakeup_mask << 16, acpi_base + CS5536_PM1_STS);
+
+       __asm__("movl %0,%%eax" : : "r" (pgd_addr));
+       __asm__("call *(%%edi); cld"
+               : : "D" (&ofw_bios_entry));
+       __asm__("movb $0x34, %al\n\t"
+               "outb %al, $0x70\n\t"
+               "movb $0x30, %al\n\t"
+               "outb %al, $0x71\n\t");
+       return 0;
+}
+
 static void xo1_power_off(void)
 {
        printk(KERN_INFO "OLPC XO-1 power off sequence...\n");
@@ -43,6 +123,17 @@ static void xo1_power_off(void)
        outl(0x00002000, acpi_base + CS5536_PM1_CNT);
 }
 
+static int xo1_power_state_valid(suspend_state_t pm_state)
+{
+       /* suspend-to-RAM only */
+       return pm_state == PM_SUSPEND_MEM;
+}
+
+static const struct platform_suspend_ops xo1_suspend_ops = {
+       .valid = xo1_power_state_valid,
+       .enter = xo1_power_state_enter,
+};
+
 static int __devinit xo1_pm_probe(struct platform_device *pdev)
 {
        struct resource *res;
@@ -68,6 +159,7 @@ static int __devinit xo1_pm_probe(struct platform_device *pdev)
 
        /* If we have both addresses, we can override the poweroff hook */
        if (pms_base && acpi_base) {
+               suspend_set_ops(&xo1_suspend_ops);
                pm_power_off = xo1_power_off;
                printk(KERN_INFO "OLPC XO-1 support registered\n");
        }
diff --git a/arch/x86/platform/olpc/xo1-wakeup.S b/arch/x86/platform/olpc/xo1-wakeup.S
new file mode 100644 (file)
index 0000000..948deb2
--- /dev/null
@@ -0,0 +1,124 @@
+.text
+#include <linux/linkage.h>
+#include <asm/segment.h>
+#include <asm/page.h>
+#include <asm/pgtable_32.h>
+
+       .macro writepost,value
+               movb $0x34, %al
+               outb %al, $0x70
+               movb $\value, %al
+               outb %al, $0x71
+       .endm
+
+wakeup_start:
+       # OFW lands us here, running in protected mode, with a
+       # kernel-compatible GDT already setup.
+
+       # Clear any dangerous flags
+       pushl $0
+       popfl
+
+       writepost 0x31
+
+       # Set up %cr3
+       movl $initial_page_table - __PAGE_OFFSET, %eax
+       movl %eax, %cr3
+
+       movl saved_cr4, %eax
+       movl %eax, %cr4
+
+       movl saved_cr0, %eax
+       movl %eax, %cr0
+
+       # Control registers were modified, pipeline resync is needed
+       jmp 1f
+1:
+
+       movw    $__KERNEL_DS, %ax
+       movw    %ax, %ss
+       movw    %ax, %ds
+       movw    %ax, %es
+       movw    %ax, %fs
+       movw    %ax, %gs
+
+       lgdt    saved_gdt
+       lidt    saved_idt
+       lldt    saved_ldt
+       ljmp    $(__KERNEL_CS),$1f
+1:
+       movl    %cr3, %eax
+       movl    %eax, %cr3
+       wbinvd
+
+       # Go back to the return point
+       jmp ret_point
+
+save_registers:
+       sgdt  saved_gdt
+       sidt  saved_idt
+       sldt  saved_ldt
+
+       pushl %edx
+       movl %cr4, %edx
+       movl %edx, saved_cr4
+
+       movl %cr0, %edx
+       movl %edx, saved_cr0
+
+       popl %edx
+
+       movl %ebx, saved_context_ebx
+       movl %ebp, saved_context_ebp
+       movl %esi, saved_context_esi
+       movl %edi, saved_context_edi
+
+       pushfl
+       popl saved_context_eflags
+
+       ret
+
+restore_registers:
+       movl saved_context_ebp, %ebp
+       movl saved_context_ebx, %ebx
+       movl saved_context_esi, %esi
+       movl saved_context_edi, %edi
+
+       pushl saved_context_eflags
+       popfl
+
+       ret
+
+ENTRY(do_olpc_suspend_lowlevel)
+       call    save_processor_state
+       call    save_registers
+
+       # This is the stack context we want to remember
+       movl %esp, saved_context_esp
+
+       pushl   $3
+       call    xo1_do_sleep
+
+       jmp     wakeup_start
+       .p2align 4,,7
+ret_point:
+       movl    saved_context_esp, %esp
+
+       writepost 0x32
+
+       call    restore_registers
+       call    restore_processor_state
+       ret
+
+.data
+saved_gdt:             .long   0,0
+saved_idt:             .long   0,0
+saved_ldt:             .long   0
+saved_cr4:             .long   0
+saved_cr0:             .long   0
+saved_context_esp:     .long   0
+saved_context_edi:     .long   0
+saved_context_esi:     .long   0
+saved_context_ebx:     .long   0
+saved_context_ebp:     .long   0
+saved_context_eflags:  .long   0
index e46b8b0f90575baa715040b5c61210aa04fb993a..2facf16f017b50b34cf1d8151d12ad61e776d0e9 100644 (file)
@@ -70,6 +70,9 @@
 #define CS5536_PM1_CNT         0x08
 #define CS5536_PM_GPE0_STS     0x18
 
+/* CS5536_PM1_EN bits */
+#define CS5536_PM_PWRBTN       (1 << 8)
+
 /* VSA2 magic values */
 #define VSA_VRC_INDEX          0xAC1C
 #define VSA_VRC_DATA           0xAC1E