diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig index 35a3ed1e1a15..886af1626e65 100644 --- a/drivers/staging/Kconfig +++ b/drivers/staging/Kconfig @@ -69,6 +69,8 @@ source "drivers/staging/rt2870/Kconfig" source "drivers/staging/comedi/Kconfig" +source "drivers/staging/olpc_dcon/Kconfig" + source "drivers/staging/asus_oled/Kconfig" source "drivers/staging/panel/Kconfig" diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile index 6221ffd3407f..e6f88b9af290 100644 --- a/drivers/staging/Makefile +++ b/drivers/staging/Makefile @@ -18,6 +18,7 @@ obj-$(CONFIG_BRCM80211) += brcm80211/ obj-$(CONFIG_RT2860) += rt2860/ obj-$(CONFIG_RT2870) += rt2870/ obj-$(CONFIG_COMEDI) += comedi/ +obj-$(CONFIG_FB_OLPC_DCON) += olpc_dcon/ obj-$(CONFIG_ASUS_OLED) += asus_oled/ obj-$(CONFIG_PANEL) += panel/ obj-$(CONFIG_R8187SE) += rtl8187se/ diff --git a/drivers/staging/olpc_dcon/Kconfig b/drivers/staging/olpc_dcon/Kconfig new file mode 100644 index 000000000000..cfdec44591b2 --- /dev/null +++ b/drivers/staging/olpc_dcon/Kconfig @@ -0,0 +1,8 @@ +config FB_OLPC_DCON + tristate "One Laptop Per Child Display CONtroller support" + depends on OLPC + select I2C + ---help--- + Add support for the OLPC XO DCON controller. This controller is + only available on OLPC platforms. Unless you have one of these + platforms, you will want to say 'N'. diff --git a/drivers/staging/olpc_dcon/Makefile b/drivers/staging/olpc_dcon/Makefile new file mode 100644 index 000000000000..cd8f2898947d --- /dev/null +++ b/drivers/staging/olpc_dcon/Makefile @@ -0,0 +1 @@ +obj-$(CONFIG_FB_OLPC_DCON) += olpc_dcon.o diff --git a/drivers/staging/olpc_dcon/olpc_dcon.c b/drivers/staging/olpc_dcon/olpc_dcon.c new file mode 100644 index 000000000000..688cf0f69524 --- /dev/null +++ b/drivers/staging/olpc_dcon/olpc_dcon.c @@ -0,0 +1,900 @@ +/* + * Mainly by David Woodhouse, somewhat modified by Jordan Crouse + * + * Copyright © 2006-2007 Red Hat, Inc. + * Copyright © 2006-2007 Advanced Micro Devices, Inc. + * Copyright © 2009 VIA Technology, Inc. + * Copyright (c) 2010 Andres Salomon + * + * This program is free software. You can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public + * License as published by the Free Software Foundation. + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "olpc_dcon.h" + +/* Module definitions */ + +static int resumeline = 898; +module_param(resumeline, int, 0444); + +static int noinit; +module_param(noinit, int, 0444); + +/* Default off since it doesn't work on DCON ASIC in B-test OLPC board */ +static int useaa = 1; +module_param(useaa, int, 0444); + +struct dcon_platform_data { + int (*init)(void); + void (*bus_stabilize_wiggle)(void); + void (*set_dconload)(int); + int (*read_status)(void); +}; + +static struct dcon_platform_data *pdata; + +/* I2C structures */ + +static struct i2c_driver dcon_driver; +static struct i2c_client *dcon_client; + +/* Platform devices */ +static struct platform_device *dcon_device; + +/* Backlight device */ +static struct backlight_device *dcon_bl_dev; + +static struct fb_info *fbinfo; + +/* set this to 1 while controlling fb blank state from this driver */ +static int ignore_fb_events = 0; + +/* Current source, initialized at probe time */ +static int dcon_source; + +/* Desired source */ +static int dcon_pending; + +/* Current output type */ +static int dcon_output = DCON_OUTPUT_COLOR; + +/* Current sleep status (not yet implemented) */ +static int dcon_sleep_val = DCON_ACTIVE; + +/* Shadow register for the DCON_REG_MODE register */ +static unsigned short dcon_disp_mode; + +/* Variables used during switches */ +static int dcon_switched; +static struct timespec dcon_irq_time; +static struct timespec dcon_load_time; + +static DECLARE_WAIT_QUEUE_HEAD(dcon_wait_queue); + +static unsigned short normal_i2c[] = { 0x0d, I2C_CLIENT_END }; + +#define dcon_write(reg,val) i2c_smbus_write_word_data(dcon_client,reg,val) +#define dcon_read(reg) i2c_smbus_read_word_data(dcon_client,reg) + +/* The current backlight value - this saves us some smbus traffic */ +static int bl_val = -1; + +/* ===== API functions - these are called by a variety of users ==== */ + +static int dcon_hw_init(struct i2c_client *client, int is_init) +{ + uint16_t ver; + int rc = 0; + + ver = i2c_smbus_read_word_data(client, DCON_REG_ID); + if ((ver >> 8) != 0xDC) { + printk(KERN_ERR "olpc-dcon: DCON ID not 0xDCxx: 0x%04x " + "instead.\n", ver); + rc = -ENXIO; + goto err; + } + + if (is_init) { + printk(KERN_INFO "olpc-dcon: Discovered DCON version %x\n", + ver & 0xFF); + if ((rc = pdata->init()) != 0) { + printk(KERN_ERR "olpc-dcon: Unable to init.\n"); + goto err; + } + } + + if (ver < 0xdc02 && !noinit) { + /* Initialize the DCON registers */ + + /* Start with work-arounds for DCON ASIC */ + i2c_smbus_write_word_data(client, 0x4b, 0x00cc); + i2c_smbus_write_word_data(client, 0x4b, 0x00cc); + i2c_smbus_write_word_data(client, 0x4b, 0x00cc); + i2c_smbus_write_word_data(client, 0x0b, 0x007a); + i2c_smbus_write_word_data(client, 0x36, 0x025c); + i2c_smbus_write_word_data(client, 0x37, 0x025e); + + /* Initialise SDRAM */ + + i2c_smbus_write_word_data(client, 0x3b, 0x002b); + i2c_smbus_write_word_data(client, 0x41, 0x0101); + i2c_smbus_write_word_data(client, 0x42, 0x0101); + } + else if (!noinit) { + /* SDRAM setup/hold time */ + i2c_smbus_write_word_data(client, 0x3a, 0xc040); + i2c_smbus_write_word_data(client, 0x41, 0x0000); + i2c_smbus_write_word_data(client, 0x41, 0x0101); + i2c_smbus_write_word_data(client, 0x42, 0x0101); + } + + /* Colour swizzle, AA, no passthrough, backlight */ + if (is_init) { + dcon_disp_mode = MODE_PASSTHRU | MODE_BL_ENABLE | MODE_CSWIZZLE; + if (useaa) + dcon_disp_mode |= MODE_COL_AA; + } + i2c_smbus_write_word_data(client, DCON_REG_MODE, dcon_disp_mode); + + + /* Set the scanline to interrupt on during resume */ + i2c_smbus_write_word_data(client, DCON_REG_SCAN_INT, resumeline); + +err: + return rc; +} + +/* + * The smbus doesn't always come back due to what is believed to be + * hardware (power rail) bugs. For older models where this is known to + * occur, our solution is to attempt to wait for the bus to stabilize; + * if it doesn't happen, cut power to the dcon, repower it, and wait + * for the bus to stabilize. Rinse, repeat until we have a working + * smbus. For newer models, we simply BUG(); we want to know if this + * still happens despite the power fixes that have been made! + */ +static int dcon_bus_stabilize(struct i2c_client *client, int is_powered_down) +{ + unsigned long timeout; + int x; + +power_up: + if (is_powered_down) { + x = 1; + if ((x = olpc_ec_cmd(0x26, (unsigned char *) &x, 1, NULL, 0))) { + printk(KERN_WARNING "olpc-dcon: unable to force dcon " + "to power up: %d!\n", x); + return x; + } + msleep(10); /* we'll be conservative */ + } + + pdata->bus_stabilize_wiggle(); + + for (x = -1, timeout = 50; timeout && x < 0; timeout--) { + msleep(1); + x = dcon_read(DCON_REG_ID); + } + if (x < 0) { + printk(KERN_ERR "olpc-dcon: unable to stabilize dcon's " + "smbus, reasserting power and praying.\n"); + BUG_ON(olpc_board_at_least(olpc_board(BOARD_XO_1_C2))); + x = 0; + olpc_ec_cmd(0x26, (unsigned char *) &x, 1, NULL, 0); + msleep(100); + is_powered_down = 1; + goto power_up; /* argh, stupid hardware.. */ + } + + if (is_powered_down) + return dcon_hw_init(client, 0); + return 0; +} + + +/* Backlight notes - turning off the backlight enable bit in the DCON + * doesn't save us any power over just pushing the BL to zero, so we + * don't use that bit in this code. + */ + +static int dcon_get_backlight(void) +{ + if (dcon_client == NULL) + return 0; + + if (bl_val == -1) + bl_val = dcon_read(DCON_REG_BRIGHT) & 0x0F; + + return bl_val; +} + + +static void dcon_set_backlight_hw(int level) +{ + bl_val = level & 0x0F; + dcon_write(DCON_REG_BRIGHT, bl_val); + + /* Purposely turn off the backlight when we go to level 0 */ + if (bl_val == 0) { + dcon_disp_mode &= ~MODE_BL_ENABLE; + dcon_write(DCON_REG_MODE, dcon_disp_mode); + } else if (!(dcon_disp_mode & MODE_BL_ENABLE)) { + dcon_disp_mode |= MODE_BL_ENABLE; + dcon_write(DCON_REG_MODE, dcon_disp_mode); + } +} + +static void dcon_set_backlight(int level) +{ + if (dcon_client == NULL) + return; + + if (bl_val == (level & 0x0F)) + return; + + dcon_set_backlight_hw(level); +} + +/* Set the output type to either color or mono */ + +static int dcon_set_output(int arg) +{ + if (dcon_output == arg) + return 0; + + dcon_output = arg; + + if (arg == DCON_OUTPUT_MONO) { + dcon_disp_mode &= ~(MODE_CSWIZZLE | MODE_COL_AA); + dcon_disp_mode |= MODE_MONO_LUMA; + } + else { + dcon_disp_mode &= ~(MODE_MONO_LUMA); + dcon_disp_mode |= MODE_CSWIZZLE; + if (useaa) + dcon_disp_mode |= MODE_COL_AA; + } + + dcon_write(DCON_REG_MODE, dcon_disp_mode); + return 0; +} + +/* For now, this will be really stupid - we need to address how + * DCONLOAD works in a sleep and account for it accordingly + */ + +static void dcon_sleep(int state) +{ + int x; + + /* Turn off the backlight and put the DCON to sleep */ + + if (state == dcon_sleep_val) + return; + + if (!olpc_board_at_least(olpc_board(BOARD_XO_1_C2))) + return; + + if (state == DCON_SLEEP) { + x = 0; + if ((x = olpc_ec_cmd(0x26, (unsigned char *) &x, 1, NULL, 0))) + printk(KERN_WARNING "olpc-dcon: unable to force dcon " + "to power down: %d!\n", x); + else + dcon_sleep_val = state; + } + else { + /* Only re-enable the backlight if the backlight value is set */ + if (bl_val != 0) + dcon_disp_mode |= MODE_BL_ENABLE; + + if ((x=dcon_bus_stabilize(dcon_client, 1))) + printk(KERN_WARNING "olpc-dcon: unable to reinit dcon" + " hardware: %d!\n", x); + else + dcon_sleep_val = state; + + /* Restore backlight */ + dcon_set_backlight_hw(bl_val); + } + + /* We should turn off some stuff in the framebuffer - but what? */ +} + +/* the DCON seems to get confused if we change DCONLOAD too + * frequently -- i.e., approximately faster than frame time. + * normally we don't change it this fast, so in general we won't + * delay here. + */ +void dcon_load_holdoff(void) +{ + struct timespec delta_t, now; + while(1) { + getnstimeofday(&now); + delta_t = timespec_sub(now, dcon_load_time); + if (delta_t.tv_sec != 0 || + delta_t.tv_nsec > NSEC_PER_MSEC * 20) { + break; + } + mdelay(4); + } +} +/* Set the source of the display (CPU or DCON) */ + +static void dcon_source_switch(struct work_struct *work) +{ + DECLARE_WAITQUEUE(wait, current); + int source = dcon_pending; + + if (dcon_source == source) + return; + + dcon_load_holdoff(); + + dcon_switched = 0; + + switch (source) { + case DCON_SOURCE_CPU: + printk("dcon_source_switch to CPU\n"); + /* Enable the scanline interrupt bit */ + if (dcon_write(DCON_REG_MODE, dcon_disp_mode | MODE_SCAN_INT)) + printk(KERN_ERR "olpc-dcon: couldn't enable scanline interrupt!\n"); + else { + /* Wait up to one second for the scanline interrupt */ + wait_event_timeout(dcon_wait_queue, dcon_switched == 1, HZ); + } + + if (!dcon_switched) + printk(KERN_ERR "olpc-dcon: Timeout entering CPU mode; expect a screen glitch.\n"); + + /* Turn off the scanline interrupt */ + if (dcon_write(DCON_REG_MODE, dcon_disp_mode)) + printk(KERN_ERR "olpc-dcon: couldn't disable scanline interrupt!\n"); + + /* + * Ideally we'd like to disable interrupts here so that the + * fb unblanking and DCON turn on happen at a known time value; + * however, we can't do that right now with fb_blank + * messing with semaphores. + * + * For now, we just hope.. + */ + acquire_console_sem(); + ignore_fb_events = 1; + if (fb_blank(fbinfo, FB_BLANK_UNBLANK)) { + ignore_fb_events = 0; + release_console_sem(); + printk(KERN_ERR "olpc-dcon: Failed to enter CPU mode\n"); + dcon_pending = DCON_SOURCE_DCON; + return; + } + ignore_fb_events = 0; + release_console_sem(); + + /* And turn off the DCON */ + pdata->set_dconload(1); + getnstimeofday(&dcon_load_time); + + printk(KERN_INFO "olpc-dcon: The CPU has control\n"); + break; + case DCON_SOURCE_DCON: + { + int t; + struct timespec delta_t; + + printk("dcon_source_switch to DCON\n"); + + add_wait_queue(&dcon_wait_queue, &wait); + set_current_state(TASK_UNINTERRUPTIBLE); + + /* Clear DCONLOAD - this implies that the DCON is in control */ + pdata->set_dconload(0); + getnstimeofday(&dcon_load_time); + + t = schedule_timeout(HZ/2); + remove_wait_queue(&dcon_wait_queue, &wait); + set_current_state(TASK_RUNNING); + + if (!dcon_switched) { + printk(KERN_ERR "olpc-dcon: Timeout entering DCON mode; expect a screen glitch.\n"); + } else { + /* sometimes the DCON doesn't follow its own rules, + * and doesn't wait for two vsync pulses before + * ack'ing the frame load with an IRQ. the result + * is that the display shows the *previously* + * loaded frame. we can detect this by looking at + * the time between asserting DCONLOAD and the IRQ -- + * if it's less than 20msec, then the DCON couldn't + * have seen two VSYNC pulses. in that case we + * deassert and reassert, and hope for the best. + * see http://dev.laptop.org/ticket/9664 + */ + delta_t = timespec_sub(dcon_irq_time, dcon_load_time); + if (dcon_switched && delta_t.tv_sec == 0 && + delta_t.tv_nsec < NSEC_PER_MSEC * 20) { + printk(KERN_ERR "olpc-dcon: missed loading, retrying\n"); + pdata->set_dconload(1); + mdelay(41); + pdata->set_dconload(0); + getnstimeofday(&dcon_load_time); + mdelay(41); + } + } + + acquire_console_sem(); + ignore_fb_events = 1; + if (fb_blank(fbinfo, FB_BLANK_POWERDOWN)) + printk(KERN_ERR "olpc-dcon: couldn't blank fb!\n"); + ignore_fb_events = 0; + release_console_sem(); + + printk(KERN_INFO "olpc-dcon: The DCON has control\n"); + break; + } + default: + BUG(); + } + + dcon_source = source; +} + +static DECLARE_WORK(dcon_work, dcon_source_switch); + +static void dcon_set_source(int arg) +{ + if (dcon_pending == arg) + return; + + dcon_pending = arg; + + if ((dcon_source != arg) && !work_pending(&dcon_work)) + schedule_work(&dcon_work); +} + +static void dcon_set_source_sync(int arg) +{ + dcon_set_source(arg); + flush_scheduled_work(); +} + +static int dconbl_set(struct backlight_device *dev) { + + int level = dev->props.brightness; + + if (dev->props.power != FB_BLANK_UNBLANK) + level = 0; + + dcon_set_backlight(level); + return 0; +} + +static int dconbl_get(struct backlight_device *dev) { + return dcon_get_backlight(); +} + +static ssize_t dcon_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%4.4X\n", dcon_disp_mode); +} + +static ssize_t dcon_sleep_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + + return sprintf(buf, "%d\n", dcon_sleep_val); +} + +static ssize_t dcon_freeze_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", dcon_source == DCON_SOURCE_DCON ? 1 : 0); +} + +static ssize_t dcon_output_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", dcon_output); +} + +static ssize_t dcon_resumeline_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", resumeline); +} + +static int _strtoul(const char *buf, int len, unsigned int *val) +{ + + char *endp; + unsigned int output = simple_strtoul(buf, &endp, 0); + int size = endp - buf; + + if (*endp && isspace(*endp)) + size++; + + if (size != len) + return -EINVAL; + + *val = output; + return 0; +} + +static ssize_t dcon_output_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int output; + int rc = -EINVAL; + + if (_strtoul(buf, count, &output)) + return -EINVAL; + + if (output == DCON_OUTPUT_COLOR || output == DCON_OUTPUT_MONO) { + dcon_set_output(output); + rc = count; + } + + return rc; +} + +static ssize_t dcon_freeze_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int output; + + if (_strtoul(buf, count, &output)) + return -EINVAL; + + printk("dcon_freeze_store: %d\n", output); + + switch (output) { + case 0: + dcon_set_source(DCON_SOURCE_CPU); + break; + case 1: + dcon_set_source_sync(DCON_SOURCE_DCON); + break; + case 2: // normally unused + dcon_set_source(DCON_SOURCE_DCON); + break; + default: + return -EINVAL; + } + + return count; +} + +static ssize_t dcon_resumeline_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int rl; + int rc = -EINVAL; + + if (_strtoul(buf, count, &rl)) + return rc; + + resumeline = rl; + dcon_write(DCON_REG_SCAN_INT, resumeline); + rc = count; + + return rc; +} + +static ssize_t dcon_sleep_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + int output; + + if (_strtoul(buf, count, &output)) + return -EINVAL; + + dcon_sleep(output ? DCON_SLEEP : DCON_ACTIVE); + return count; +} + +static struct device_attribute dcon_device_files[] = { + __ATTR(mode, 0444, dcon_mode_show, NULL), + __ATTR(sleep, 0644, dcon_sleep_show, dcon_sleep_store), + __ATTR(freeze, 0644, dcon_freeze_show, dcon_freeze_store), + __ATTR(output, 0644, dcon_output_show, dcon_output_store), + __ATTR(resumeline, 0644, dcon_resumeline_show, dcon_resumeline_store), +}; + +static struct backlight_ops dcon_bl_ops = { + .get_brightness = dconbl_get, + .update_status = dconbl_set +}; + + +static int dcon_reboot_notify(struct notifier_block *nb, unsigned long foo, void *bar) +{ + if (dcon_client == NULL) + return 0; + + /* Turn off the DCON. Entirely. */ + dcon_write(DCON_REG_MODE, 0x39); + dcon_write(DCON_REG_MODE, 0x32); + return 0; +} + +static int dcon_conswitch_notify(struct notifier_block *nb, + unsigned long mode, void *dummy) +{ + if (mode == CONSOLE_EVENT_SWITCH_TEXT) + dcon_sleep(DCON_ACTIVE); + + return 0; +} + +static struct notifier_block dcon_nb = { + .notifier_call = dcon_reboot_notify, + .priority = -1, +}; + +static struct notifier_block dcon_console_nb = { + .notifier_call = dcon_conswitch_notify, + .priority = -1, +}; + +static int unfreeze_on_panic(struct notifier_block *nb, unsigned long e, void *p) +{ + pdata->set_dconload(1); + return NOTIFY_DONE; +} + +static struct notifier_block dcon_panic_nb = { + .notifier_call = unfreeze_on_panic, +}; + +/* when framebuffer sleeps due to external source (e.g. user idle), power down + * the DCON. also power up when the framebuffer comes back to life. */ +static int fb_notifier_callback(struct notifier_block *self, unsigned long event, void *data) +{ + struct fb_event *evdata = data; + int *blank = (int *) evdata->data; + if (event != FB_EVENT_BLANK || ignore_fb_events) + return 0; + dcon_sleep((*blank) ? DCON_SLEEP : DCON_ACTIVE); + return 0; +} + +static struct notifier_block fb_nb = { + .notifier_call = fb_notifier_callback, +}; + +static int dcon_detect(struct i2c_client *client, struct i2c_board_info *info) +{ + strlcpy(info->type, "olpc_dcon", I2C_NAME_SIZE); + + return 0; +} + +static int dcon_probe(struct i2c_client *client, const struct i2c_device_id *id) +{ + int rc, i; + + if (num_registered_fb >= 1) + fbinfo = registered_fb[0]; + + rc = dcon_hw_init(client, 1); + if (rc) + goto einit; + + /* Add the DCON device */ + + dcon_device = platform_device_alloc("dcon", -1); + + if (dcon_device == NULL) { + printk("dcon: Unable to create the DCON device\n"); + rc = -ENOMEM; + goto eirq; + } + /* Place holder...*/ + i2c_set_clientdata(client, dcon_device); + + if ((rc = platform_device_add(dcon_device))) { + printk("dcon: Unable to add the DCON device\n"); + goto edev; + } + + for(i = 0; i < ARRAY_SIZE(dcon_device_files); i++) + device_create_file(&dcon_device->dev, &dcon_device_files[i]); + + /* Add the backlight device for the DCON */ + + dcon_client = client; + + dcon_bl_dev = backlight_device_register("dcon-bl", &dcon_device->dev, + NULL, &dcon_bl_ops, NULL); + + if (IS_ERR(dcon_bl_dev)) { + printk("Could not register the backlight device for the DCON (%ld)\n", PTR_ERR(dcon_bl_dev)); + dcon_bl_dev = NULL; + } + else { + dcon_bl_dev->props.max_brightness = 15; + dcon_bl_dev->props.power = FB_BLANK_UNBLANK; + dcon_bl_dev->props.brightness = dcon_get_backlight(); + + backlight_update_status(dcon_bl_dev); + } + + register_reboot_notifier(&dcon_nb); + console_event_register(&dcon_console_nb); + atomic_notifier_chain_register(&panic_notifier_list, &dcon_panic_nb); + fb_register_client(&fb_nb); + + return 0; + + edev: + platform_device_unregister(dcon_device); + dcon_device = NULL; + i2c_set_clientdata(client, NULL); + eirq: + free_irq(DCON_IRQ, &dcon_driver); + einit: + return rc; +} + +static int dcon_remove(struct i2c_client *client) +{ + dcon_client = NULL; + + fb_unregister_client(&fb_nb); + unregister_reboot_notifier(&dcon_nb); + console_event_unregister(&dcon_console_nb); + atomic_notifier_chain_unregister(&panic_notifier_list, &dcon_panic_nb); + + free_irq(DCON_IRQ, &dcon_driver); + + if (dcon_bl_dev != NULL) + backlight_device_unregister(dcon_bl_dev); + + if (dcon_device != NULL) + platform_device_unregister(dcon_device); + cancel_work_sync(&dcon_work); + + i2c_set_clientdata(client, NULL); + + return 0; +} + +#ifdef CONFIG_PM +static int dcon_suspend(struct i2c_client *client, pm_message_t state) +{ + if (dcon_sleep_val == DCON_ACTIVE) { + /* Set up the DCON to have the source */ + dcon_set_source_sync(DCON_SOURCE_DCON); + } + + return 0; +} + +static int dcon_resume(struct i2c_client *client) +{ + if (dcon_sleep_val == DCON_ACTIVE) { + dcon_bus_stabilize(client, 0); + dcon_set_source(DCON_SOURCE_CPU); + } + + return 0; +} + +#endif + + +static irqreturn_t dcon_interrupt(int irq, void *id) +{ + int status = pdata->read_status(); + + if (status == -1) + return IRQ_NONE; + + switch (status & 3) { + case 3: + printk(KERN_DEBUG "olpc-dcon: DCONLOAD_MISSED interrupt\n"); + break; + + case 2: /* switch to DCON mode */ + case 1: /* switch to CPU mode */ + dcon_switched = 1; + getnstimeofday(&dcon_irq_time); + wake_up(&dcon_wait_queue); + break; + + case 0: + /* workaround resume case: the DCON (on 1.5) doesn't + * ever assert status 0x01 when switching to CPU mode + * during resume. this is because DCONLOAD is de-asserted + * _immediately_ upon exiting S3, so the actual release + * of the DCON happened long before this point. + * see http://dev.laptop.org/ticket/9869 + */ + if (dcon_source != dcon_pending && !dcon_switched) { + dcon_switched = 1; + getnstimeofday(&dcon_irq_time); + wake_up(&dcon_wait_queue); + printk(KERN_DEBUG "olpc-dcon: switching w/ status 0/0\n"); + } else { + printk(KERN_DEBUG "olpc-dcon: scanline interrupt w/CPU\n"); + } + } + + return IRQ_HANDLED; +} + +static struct i2c_device_id dcon_idtable[] = { + { "olpc_dcon", 0 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, dcon_idtable); + +static struct i2c_driver dcon_driver = { + .driver = { + .name = "olpc_dcon", + }, + .class = I2C_CLASS_DDC | I2C_CLASS_HWMON, + .id_table = dcon_idtable, + .probe = dcon_probe, + .remove = __devexit_p(dcon_remove), + .detect = dcon_detect, + .address_list = normal_i2c, +#ifdef CONFIG_PM + .suspend = dcon_suspend, + .resume = dcon_resume, +#endif +}; + +#if defined(CONFIG_OLPC_XO_1) +#include "olpc_dcon_xo_1.c" +#elif defined(CONFIG_OLPC_XO_1_5) +#include "olpc_dcon_xo_1_5.c" +#else +#error "Trying to build OLPC DCON driver but kernel not configured for XO" +#endif + +static int __init olpc_dcon_init(void) +{ +#ifdef CONFIG_OLPC_XO_1 + pdata = &dcon_pdata_xo_1; +#else + pdata = &dcon_pdata_xo_1_5; +#endif + + i2c_add_driver(&dcon_driver); + return 0; +} + +static void __exit olpc_dcon_exit(void) +{ + i2c_del_driver(&dcon_driver); +} + +module_init(olpc_dcon_init); +module_exit(olpc_dcon_exit); + +MODULE_LICENSE("GPL"); diff --git a/drivers/staging/olpc_dcon/olpc_dcon.h b/drivers/staging/olpc_dcon/olpc_dcon.h new file mode 100644 index 000000000000..6453ca4ba0ee --- /dev/null +++ b/drivers/staging/olpc_dcon/olpc_dcon.h @@ -0,0 +1,75 @@ +#ifndef OLPC_DCON_H_ +#define OLPC_DCON_H_ + +/* DCON registers */ + +#define DCON_REG_ID 0 +#define DCON_REG_MODE 1 + +#define MODE_PASSTHRU (1<<0) +#define MODE_SLEEP (1<<1) +#define MODE_SLEEP_AUTO (1<<2) +#define MODE_BL_ENABLE (1<<3) +#define MODE_BLANK (1<<4) +#define MODE_CSWIZZLE (1<<5) +#define MODE_COL_AA (1<<6) +#define MODE_MONO_LUMA (1<<7) +#define MODE_SCAN_INT (1<<8) +#define MODE_CLOCKDIV (1<<9) +#define MODE_DEBUG (1<<14) +#define MODE_SELFTEST (1<<15) + +#define DCON_REG_HRES 2 +#define DCON_REG_HTOTAL 3 +#define DCON_REG_HSYNC_WIDTH 4 +#define DCON_REG_VRES 5 +#define DCON_REG_VTOTAL 6 +#define DCON_REG_VSYNC_WIDTH 7 +#define DCON_REG_TIMEOUT 8 +#define DCON_REG_SCAN_INT 9 +#define DCON_REG_BRIGHT 10 + +/* GPIO registers (CS5536) */ + +#define MSR_LBAR_GPIO 0x5140000C + +#define GPIOx_OUT_VAL 0x00 +#define GPIOx_OUT_EN 0x04 +#define GPIOx_IN_EN 0x20 +#define GPIOx_INV_EN 0x24 +#define GPIOx_IN_FLTR_EN 0x28 +#define GPIOx_EVNTCNT_EN 0x2C +#define GPIOx_READ_BACK 0x30 +#define GPIOx_EVNT_EN 0x38 +#define GPIOx_NEGEDGE_EN 0x44 +#define GPIOx_NEGEDGE_STS 0x4C +#define GPIO_FLT7_AMNT 0xD8 +#define GPIO_MAP_X 0xE0 +#define GPIO_MAP_Y 0xE4 +#define GPIO_FE7_SEL 0xF7 + + +/* Status values */ + +#define DCONSTAT_SCANINT 0 +#define DCONSTAT_SCANINT_DCON 1 +#define DCONSTAT_DISPLAYLOAD 2 +#define DCONSTAT_MISSED 3 + +/* Source values */ + +#define DCON_SOURCE_DCON 0 +#define DCON_SOURCE_CPU 1 + +/* Output values */ +#define DCON_OUTPUT_COLOR 0 +#define DCON_OUTPUT_MONO 1 + +/* Sleep values */ +#define DCON_ACTIVE 0 +#define DCON_SLEEP 1 + +/* Interrupt */ +#define DCON_IRQ 6 + +#endif diff --git a/drivers/staging/olpc_dcon/olpc_dcon_xo_1.c b/drivers/staging/olpc_dcon/olpc_dcon_xo_1.c new file mode 100644 index 000000000000..779fb7d7b30c --- /dev/null +++ b/drivers/staging/olpc_dcon/olpc_dcon_xo_1.c @@ -0,0 +1,171 @@ +/* + * Mainly by David Woodhouse, somewhat modified by Jordan Crouse + * + * Copyright © 2006-2007 Red Hat, Inc. + * Copyright © 2006-2007 Advanced Micro Devices, Inc. + * Copyright © 2009 VIA Technology, Inc. + * Copyright (c) 2010 Andres Salomon + * + * This program is free software. You can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public + * License as published by the Free Software Foundation. + */ + +#include + +#include "olpc_dcon.h" + +/* Base address of the GPIO registers */ +static unsigned long gpio_base; + +/* + * List of GPIOs that we care about: + * (in) GPIO12 -- DCONBLANK + * (in) GPIO[56] -- DCONSTAT[01] + * (out) GPIO11 -- DCONLOAD + */ + +#define IN_GPIOS ((1<<5) | (1<<6) | (1<<7) | (1<<12)) +#define OUT_GPIOS (1<<11) + +static int dcon_init_xo_1(void) +{ + unsigned long lo, hi; + unsigned char lob; + + rdmsr(MSR_LBAR_GPIO, lo, hi); + + /* Check the mask and whether GPIO is enabled (sanity check) */ + if (hi != 0x0000f001) { + printk(KERN_ERR "GPIO not enabled -- cannot use DCON\n"); + return -ENODEV; + } + + /* Mask off the IO base address */ + gpio_base = lo & 0x0000ff00; + + /* Turn off the event enable for GPIO7 just to be safe */ + outl(1 << (16+7), gpio_base + GPIOx_EVNT_EN); + + /* Set the directions for the GPIO pins */ + outl(OUT_GPIOS | (IN_GPIOS << 16), gpio_base + GPIOx_OUT_EN); + outl(IN_GPIOS | (OUT_GPIOS << 16), gpio_base + GPIOx_IN_EN); + + /* Set up the interrupt mappings */ + + /* Set the IRQ to pair 2 */ + geode_gpio_event_irq(OLPC_GPIO_DCON_IRQ, 2); + + /* Enable group 2 to trigger the DCON interrupt */ + geode_gpio_set_irq(2, DCON_IRQ); + + /* Select edge level for interrupt (in PIC) */ + lob = inb(0x4d0); + lob &= ~(1 << DCON_IRQ); + outb(lob, 0x4d0); + + /* Register the interupt handler */ + if (request_irq(DCON_IRQ, &dcon_interrupt, 0, "DCON", &dcon_driver)) + return -EIO; + + /* Clear INV_EN for GPIO7 (DCONIRQ) */ + outl((1<<(16+7)), gpio_base + GPIOx_INV_EN); + + /* Enable filter for GPIO12 (DCONBLANK) */ + outl(1<<(12), gpio_base + GPIOx_IN_FLTR_EN); + + /* Disable filter for GPIO7 */ + outl(1<<(16+7), gpio_base + GPIOx_IN_FLTR_EN); + + /* Disable event counter for GPIO7 (DCONIRQ) and GPIO12 (DCONBLANK) */ + + outl(1<<(16+7), gpio_base + GPIOx_EVNTCNT_EN); + outl(1<<(16+12), gpio_base + GPIOx_EVNTCNT_EN); + + /* Add GPIO12 to the Filter Event Pair #7 */ + outb(12, gpio_base + GPIO_FE7_SEL); + + /* Turn off negative Edge Enable for GPIO12 */ + outl(1<<(16+12), gpio_base + GPIOx_NEGEDGE_EN); + + /* Enable negative Edge Enable for GPIO7 */ + outl(1<<7, gpio_base + GPIOx_NEGEDGE_EN); + + /* Zero the filter amount for Filter Event Pair #7 */ + outw(0, gpio_base + GPIO_FLT7_AMNT); + + /* Clear the negative edge status for GPIO7 and GPIO12 */ + outl((1<<7) | (1<<12), gpio_base+0x4c); + + /* FIXME: Clear the posiitive status as well, just to be sure */ + outl((1<<7) | (1<<12), gpio_base+0x48); + + /* Enable events for GPIO7 (DCONIRQ) and GPIO12 (DCONBLANK) */ + outl((1<<(7))|(1<<12), gpio_base + GPIOx_EVNT_EN); + + /* Determine the current state by reading the GPIO bit */ + /* Earlier stages of the boot process have established the state */ + dcon_source = inl(gpio_base + GPIOx_OUT_VAL) & (1<<11) + ? DCON_SOURCE_CPU + : DCON_SOURCE_DCON; + dcon_pending = dcon_source; + + return 0; +} + +static void dcon_wiggle_xo_1(void) +{ + int x; + + /* + * According to HiMax, when powering the DCON up we should hold + * SMB_DATA high for 8 SMB_CLK cycles. This will force the DCON + * state machine to reset to a (sane) initial state. Mitch Bradley + * did some testing and discovered that holding for 16 SMB_CLK cycles + * worked a lot more reliably, so that's what we do here. + * + * According to the cs5536 spec, to set GPIO14 to SMB_CLK we must + * simultaneously set AUX1 IN/OUT to GPIO14; ditto for SMB_DATA and + * GPIO15. + */ + geode_gpio_set(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_OUTPUT_VAL); + geode_gpio_set(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_OUTPUT_ENABLE); + geode_gpio_clear(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_OUTPUT_AUX1); + geode_gpio_clear(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_OUTPUT_AUX2); + geode_gpio_clear(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_INPUT_AUX1); + + for (x = 0; x < 16; x++) { + udelay(5); + geode_gpio_clear(OLPC_GPIO_SMB_CLK, GPIO_OUTPUT_VAL); + udelay(5); + geode_gpio_set(OLPC_GPIO_SMB_CLK, GPIO_OUTPUT_VAL); + } + udelay(5); + geode_gpio_set(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_OUTPUT_AUX1); + geode_gpio_set(OLPC_GPIO_SMB_CLK|OLPC_GPIO_SMB_DATA, GPIO_INPUT_AUX1); +} + +static void dcon_set_dconload_1(int val) +{ + if (val) + outl(1<<11, gpio_base + GPIOx_OUT_VAL); + else + outl(1<<(11 + 16), gpio_base + GPIOx_OUT_VAL); +} + +static int dcon_read_status_xo_1(void) +{ + int status = inl(gpio_base + GPIOx_READ_BACK) >> 5; + + /* Clear the negative edge status for GPIO7 */ + outl(1 << 7, gpio_base + GPIOx_NEGEDGE_STS); + + return status; +} + +static struct dcon_platform_data dcon_pdata_xo_1 = { + .init = dcon_init_xo_1, + .bus_stabilize_wiggle = dcon_wiggle_xo_1, + .set_dconload = dcon_set_dconload_1, + .read_status = dcon_read_status_xo_1, +}; diff --git a/drivers/staging/olpc_dcon/olpc_dcon_xo_1_5.c b/drivers/staging/olpc_dcon/olpc_dcon_xo_1_5.c new file mode 100644 index 000000000000..cca6a235ef96 --- /dev/null +++ b/drivers/staging/olpc_dcon/olpc_dcon_xo_1_5.c @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2009,2010 One Laptop per Child + * + * This program is free software. You can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public + * License as published by the Free Software Foundation. + */ + +#include + +/* Hardware setup on the XO 1.5: + * DCONLOAD connects to + * VX855_GPO12 (not nCR_PWOFF) (rev A) + * VX855_GPIO1 (not SMBCK2) (rev B) + * DCONBLANK connects to VX855_GPIO8 (not SSPICLK) unused in driver + * DCONSTAT0 connects to VX855_GPI10 (not SSPISDI) + * DCONSTAT1 connects to VX855_GPI11 (not nSSPISS) + * DCONIRQ connects to VX855_GPIO12 (on B3. on B2, it goes to + * SMBALRT, which doesn't work.) + * DCONSMBDATA connects to VX855 graphics CRTSPD + * DCONSMBCLK connects to VX855 graphics CRTSPCLK + */ + +#define TEST_B2 0 // define to test B3 paths on a modded B2 board + +#define VX855_GENL_PURPOSE_OUTPUT 0x44c // PMIO_Rx4c-4f +#define VX855_GPI_STATUS_CHG 0x450 // PMIO_Rx50 +#define VX855_GPI_SCI_SMI 0x452 // PMIO_Rx52 +#define BIT_GPIO12 0x40 + +#define PREFIX "OLPC DCON:" + +/* + there is no support here for DCONIRQ on 1.5 boards earlier than + B3. the issue is that the DCONIRQ signal on earlier boards is + routed to SMBALRT, which turns out to to be a level sensitive + interrupt. the DCONIRQ signal is far too short (11usec) to + be detected reliably in that case. including support for + DCONIRQ functions no better than none at all. +*/ + +static struct dcon_platform_data dcon_pdata_xo_1_5; + +static void dcon_clear_irq(void) +{ + if (TEST_B2 || olpc_board_at_least(olpc_board(BOARD_XO_1_5_B3))) { + // irq status will appear in PMIO_Rx50[6] (RW1C) on gpio12 + outb(BIT_GPIO12, VX855_GPI_STATUS_CHG); + } +} + +static int dcon_was_irq(void) +{ + u_int8_t tmp; + + if (TEST_B2 || olpc_board_at_least(olpc_board(BOARD_XO_1_5_B3))) { + // irq status will appear in PMIO_Rx50[6] on gpio12 + tmp = inb(VX855_GPI_STATUS_CHG); + return !!(tmp & BIT_GPIO12); + } + + return 0; +} + +static int dcon_init_xo_1_5(void) +{ + unsigned int irq; + u_int8_t tmp; + struct pci_dev *pdev; + + + pdev = pci_get_device(PCI_VENDOR_ID_VIA, + PCI_DEVICE_ID_VIA_VX855, NULL); + if (!pdev) { + printk(KERN_ERR "cannot find VX855 PCI ID\n"); + return 1; + } + + if (olpc_board_at_least(olpc_board(BOARD_XO_1_5_B1))) { + pci_read_config_byte(pdev, 0x95, &tmp); + pci_write_config_byte(pdev, 0x95, tmp|0x0c); + } else { + /* Set GPO12 to GPO mode, not nCR_PWOFF */ + pci_read_config_byte(pdev, 0x9b, &tmp); + pci_write_config_byte(pdev, 0x9b, tmp|0x01); + } + + /* Set GPIO8 to GPIO mode, not SSPICLK */ + pci_read_config_byte(pdev, 0xe3, &tmp); + pci_write_config_byte(pdev, 0xe3, tmp | 0x04); + + /* Set GPI10/GPI11 to GPI mode, not SSPISDI/SSPISS */ + pci_read_config_byte(pdev, 0xe4, &tmp); + pci_write_config_byte(pdev, 0xe4, tmp|0x08); + + if (TEST_B2 || olpc_board_at_least(olpc_board(BOARD_XO_1_5_B3))) { + // clear PMU_RxE1[6] to select SCI on GPIO12 + // clear PMU_RxE0[6] to choose falling edge + pci_read_config_byte(pdev, 0xe1, &tmp); + pci_write_config_byte(pdev, 0xe1, tmp & ~BIT_GPIO12); + pci_read_config_byte(pdev, 0xe0, &tmp); + pci_write_config_byte(pdev, 0xe0, tmp & ~BIT_GPIO12); + + dcon_clear_irq(); + + // set PMIO_Rx52[6] to enable SCI/SMI on gpio12 + outb(inb(VX855_GPI_SCI_SMI)|BIT_GPIO12, VX855_GPI_SCI_SMI); + + } + + /* Determine the current state of DCONLOAD, likely set by firmware */ + if (olpc_board_at_least(olpc_board(BOARD_XO_1_5_B1))) { + // GPIO1 + dcon_source = (inl(VX855_GENL_PURPOSE_OUTPUT) & 0x1000) ? + DCON_SOURCE_CPU : DCON_SOURCE_DCON; + } else { + // GPO12 + dcon_source = (inl(VX855_GENL_PURPOSE_OUTPUT) & 0x04000000) ? + DCON_SOURCE_CPU : DCON_SOURCE_DCON; + } + dcon_pending = dcon_source; + + pci_dev_put(pdev); + + /* we're sharing the IRQ with ACPI */ + irq = acpi_gbl_FADT.sci_interrupt; + if (request_irq(irq, &dcon_interrupt, IRQF_SHARED, "DCON", &dcon_driver)) { + printk(KERN_ERR PREFIX "DCON (IRQ%d) allocation failed\n", irq); + return 1; + } + + + return 0; +} + +static void set_i2c_line(int sda, int scl) +{ + unsigned char tmp; + unsigned int port = 0x26; + + /* FIXME: This directly accesses the CRT GPIO controller !!! */ + outb(port, 0x3c4); + tmp = inb(0x3c5); + + if (scl) + tmp |= 0x20; + else + tmp &= ~0x20; + + if (sda) + tmp |= 0x10; + else + tmp &= ~0x10; + + tmp |= 0x01; + + outb(port, 0x3c4); + outb(tmp, 0x3c5); +} + + +static void dcon_wiggle_xo_1_5(void) +{ + int x; + + /* + * According to HiMax, when powering the DCON up we should hold + * SMB_DATA high for 8 SMB_CLK cycles. This will force the DCON + * state machine to reset to a (sane) initial state. Mitch Bradley + * did some testing and discovered that holding for 16 SMB_CLK cycles + * worked a lot more reliably, so that's what we do here. + */ + set_i2c_line(1, 1); + + for (x = 0; x < 16; x++) { + udelay(5); + set_i2c_line(1, 0); + udelay(5); + set_i2c_line(1, 1); + } + udelay(5); + + if (TEST_B2 || olpc_board_at_least(olpc_board(BOARD_XO_1_5_B3))) { + // set PMIO_Rx52[6] to enable SCI/SMI on gpio12 + outb(inb(VX855_GPI_SCI_SMI)|BIT_GPIO12, VX855_GPI_SCI_SMI); + } +} + +static void dcon_set_dconload_xo_1_5(int val) +{ + if (olpc_board_at_least(olpc_board(BOARD_XO_1_5_B1))) { + gpio_set_value(VX855_GPIO(1), val); + } else { + gpio_set_value(VX855_GPO(12), val); + } +} + +static int dcon_read_status_xo_1_5(void) +{ + int status; + + if (!dcon_was_irq()) + return -1; + + // i believe this is the same as "inb(0x44b) & 3" + status = gpio_get_value(VX855_GPI(10)); + status |= gpio_get_value(VX855_GPI(11)) << 1; + + dcon_clear_irq(); + + return status; +} + +static struct dcon_platform_data dcon_pdata_xo_1_5 = { + .init = dcon_init_xo_1_5, + .bus_stabilize_wiggle = dcon_wiggle_xo_1_5, + .set_dconload = dcon_set_dconload_xo_1_5, + .read_status = dcon_read_status_xo_1_5, +};