summaryrefslogtreecommitdiff
path: root/arch/arm/mach-sa1100/h3xxx-sleeve.c
diff options
context:
space:
mode:
Diffstat (limited to 'arch/arm/mach-sa1100/h3xxx-sleeve.c')
-rw-r--r--arch/arm/mach-sa1100/h3xxx-sleeve.c341
1 files changed, 341 insertions, 0 deletions
diff --git a/arch/arm/mach-sa1100/h3xxx-sleeve.c b/arch/arm/mach-sa1100/h3xxx-sleeve.c
new file mode 100644
index 000000000000..ed0edaf43d9c
--- /dev/null
+++ b/arch/arm/mach-sa1100/h3xxx-sleeve.c
@@ -0,0 +1,341 @@
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/ipaq-micro.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <linux/ipaq-sleeve.h>
+
+#define SLEEVE_POWER_DELAY_MS 10
+#define SLEEVE_NVRAM_POWER_MS 5
+
+struct opt_info {
+ struct device *dev;
+ struct ipaq_option_id id;
+ struct delayed_work detect_work;
+ bool detect_disabled;
+ bool sleeve_changed;
+ bool sleeve_present;
+ bool sleeve_wakeup;
+ struct gpio_desc *detect;
+ struct gpio_desc *nvram_on;
+ struct gpio_desc *opt_on;
+ struct gpio_desc *opt_reset;
+};
+
+static int ipaq_option_spi_read(void *buf, unsigned int addr, size_t len)
+{
+ struct device *mdev = bus_find_device_by_name(&platform_bus_type, NULL,
+ "ipaq-h3xxx-micro");
+ struct ipaq_micro *micro;
+ struct ipaq_micro_msg msg;
+
+ memset(buf, 0, len);
+
+ if (!mdev || !mdev->driver)
+ return -EINVAL;
+
+ micro = dev_get_drvdata(mdev);
+ if (!micro)
+ return -EINVAL;
+
+ memset(&msg, 0, sizeof(msg));
+
+ while (len) {
+ size_t this_len = len;
+
+ if (this_len > 8)
+ this_len = 8;
+
+ msg.id = MSG_SPI_READ;
+ msg.tx_len = 3;
+ msg.tx_data[0] = addr;
+ msg.tx_data[1] = addr >> 8;
+ msg.tx_data[2] = this_len;
+
+ ipaq_micro_tx_msg_sync(micro, &msg);
+
+ memcpy(buf, msg.rx_data, msg.rx_len);
+
+ buf += msg.rx_len;
+ len -= msg.rx_len;
+ addr += msg.rx_len;
+ }
+
+ return 0;
+}
+
+static int ipaq_option_read_id(struct opt_info *opt, struct ipaq_option_id *id)
+{
+ __le16 vendor, device;
+ int ret;
+
+ /* Enable the CF bus */
+ gpiod_set_value_cansleep(opt->nvram_on, 1);
+
+ /* Wait for power to stabilise */
+ msleep(SLEEVE_NVRAM_POWER_MS);
+
+ ret = ipaq_option_spi_read(&vendor, 6, 2);
+ if (ret)
+ goto err;
+
+ ret = ipaq_option_spi_read(&device, 8, 2);
+ if (ret)
+ goto err;
+
+ id->vendor = le16_to_cpu(vendor);
+ id->device = le16_to_cpu(device);
+
+ err:
+ gpiod_set_value_cansleep(opt->nvram_on, 0);
+ return ret;
+}
+
+static void ipaq_option_power_on(struct opt_info *opt, bool noirq)
+{
+ gpiod_set_value(opt->opt_reset, 1);
+ gpiod_set_value(opt->opt_on, 1);
+ gpiod_set_value(opt->opt_reset, 0);
+
+ if (noirq)
+ mdelay(SLEEVE_POWER_DELAY_MS);
+ else
+ msleep(SLEEVE_POWER_DELAY_MS);
+}
+
+static void ipaq_option_power_off(struct opt_info *opt, bool noirq)
+{
+ gpiod_set_value(opt->opt_reset, 1);
+ gpiod_set_value(opt->opt_on, 0);
+}
+
+static void ipaq_option_insert(struct opt_info *opt)
+{
+ int ret;
+
+ ret = ipaq_option_read_id(opt, &opt->id);
+ if (ret)
+ return;
+
+ dev_info(opt->dev, "Option pack %04x:%04x detected\n",
+ opt->id.vendor, opt->id.device);
+
+ ipaq_option_power_on(opt, false);
+
+ if (!opt->sleeve_present)
+ ipaq_option_device_add(opt->dev, opt->id);
+ opt->sleeve_present = true;
+}
+
+static void ipaq_option_remove(struct opt_info *opt)
+{
+ if (opt->sleeve_present)
+ ipaq_option_device_del();
+ opt->sleeve_present = false;
+
+ ipaq_option_power_off(opt, false);
+}
+
+static void ipaq_option_work(struct work_struct *work)
+{
+ struct opt_info *opt = container_of(work, struct opt_info, detect_work.work);
+ int present = gpiod_get_value_cansleep(opt->detect);
+
+ if (opt->sleeve_changed)
+ ipaq_option_remove(opt);
+
+ if (present)
+ ipaq_option_insert(opt);
+ else
+ ipaq_option_remove(opt);
+}
+
+static irqreturn_t ipaq_option_irq(int irq, void *data)
+{
+ struct opt_info *opt = data;
+
+ dev_info(opt->dev, "irq: option pack: %u\n", gpiod_get_value(opt->detect));
+
+ if (!opt->detect_disabled)
+ mod_delayed_work(system_wq, &opt->detect_work,
+ msecs_to_jiffies(100));
+
+ return IRQ_HANDLED;
+}
+
+static int ipaq_h3xxx_sleeve_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct opt_info *opt;
+ int irq, ret;
+
+ opt = devm_kzalloc(dev, sizeof(*opt), GFP_KERNEL);
+ if (!opt)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, opt);
+ opt->dev = dev;
+ INIT_DELAYED_WORK(&opt->detect_work, ipaq_option_work);
+
+ opt->nvram_on = devm_gpiod_get(dev, "nvram-on", GPIOD_OUT_LOW);
+ if (IS_ERR(opt->nvram_on))
+ return PTR_ERR(opt->nvram_on);
+
+ opt->opt_on = devm_gpiod_get(dev, "option-on", GPIOD_OUT_LOW);
+ if (IS_ERR(opt->opt_on))
+ return PTR_ERR(opt->opt_on);
+
+ opt->opt_reset = devm_gpiod_get(dev, "option-reset", GPIOD_OUT_LOW);
+ if (IS_ERR(opt->opt_reset))
+ return PTR_ERR(opt->opt_reset);
+
+ opt->detect = devm_gpiod_get_optional(dev, "option-detect", GPIOD_IN);
+ if (IS_ERR(opt->detect))
+ return PTR_ERR(opt->detect);
+
+ if (opt->detect) {
+ irq = gpiod_to_irq(opt->detect);
+ if (irq < 0)
+ return irq;
+
+ ret = devm_request_irq(dev, irq, ipaq_option_irq,
+ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
+ dev_name(dev), opt);
+ if (ret)
+ return ret;
+ }
+
+ schedule_delayed_work(&opt->detect_work, 0);
+ flush_delayed_work(&opt->detect_work);
+
+ return 0;
+}
+
+static int ipaq_h3xxx_sleeve_remove(struct platform_device *pdev)
+{
+ struct opt_info *opt = platform_get_drvdata(pdev);
+
+ opt->detect_disabled = true;
+ cancel_delayed_work_sync(&opt->detect_work);
+
+ ipaq_option_remove(opt);
+
+ return 0;
+}
+
+static int ipaq_h3xxx_check_wakeup(struct device *dev, void *data)
+{
+ if (device_may_wakeup(dev))
+ return 1;
+
+ return device_for_each_child(dev, data, ipaq_h3xxx_check_wakeup);
+}
+
+static int ipaq_h3xxx_sleeve_suspend(struct device *dev)
+{
+ struct opt_info *opt = dev_get_drvdata(dev);
+ int may_wakeup;
+
+ opt->detect_disabled = true;
+ cancel_delayed_work_sync(&opt->detect_work);
+
+ /* Disable sleeve power if device isn't a wakeup source */
+ may_wakeup = device_for_each_child(dev, NULL, ipaq_h3xxx_check_wakeup);
+
+ opt->sleeve_wakeup = may_wakeup;
+
+ return 0;
+}
+
+static int ipaq_h3xxx_sleeve_suspend_noirq(struct device *dev)
+{
+ struct opt_info *opt = dev_get_drvdata(dev);
+
+ if (!opt->sleeve_wakeup && opt->sleeve_present) {
+ ipaq_option_power_off(opt, true);
+
+ /*
+ * Give the power time to collapse before bringing the
+ * reset signal to logic 0.
+ */
+ mdelay(1);
+ gpiod_set_raw_value(opt->opt_reset, 0);
+ }
+
+ return 0;
+}
+
+static int ipaq_h3xxx_sleeve_resume_noirq(struct device *dev)
+{
+ struct opt_info *opt = dev_get_drvdata(dev);
+
+ if (!opt->sleeve_wakeup && gpiod_get_value(opt->detect)) {
+ /*
+ * We can't check the ID of the sleeve here, so assume that
+ * it is the same sleeve inserted, and power it up. This
+ * avoids an eject/insert cycle on any inserted PCMCIA card.
+ */
+ ipaq_option_power_on(opt, true);
+ }
+
+ return 0;
+}
+
+static int ipaq_h3xxx_sleeve_resume(struct device *dev)
+{
+ struct opt_info *opt = dev_get_drvdata(dev);
+ struct ipaq_option_id id;
+ int ret;
+
+ if (opt->sleeve_present && gpiod_get_value_cansleep(opt->detect)) {
+ ret = ipaq_option_read_id(opt, &id);
+ if (ret) {
+ dev_warn(opt->dev, "unable to read sleeve id: %d\n",
+ ret);
+ opt->sleeve_changed = true;
+ goto finish;
+ }
+
+ if (opt->id.vendor != id.vendor ||
+ opt->id.device != id.device) {
+ /* sleeve changed */
+ dev_warn(opt->dev, "sleeve changed\n");
+ opt->sleeve_changed = true;
+ } else {
+ dev_info(opt->dev, "same sleeve type inserted\n");
+ opt->detect_disabled = false;
+ return 0;
+ }
+ }
+
+finish:
+ opt->detect_disabled = false;
+ schedule_delayed_work(&opt->detect_work, 0);
+ flush_delayed_work(&opt->detect_work);
+
+ return 0;
+}
+
+static struct dev_pm_ops ipaq_h3xxx_sleeve_pm = {
+ SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(ipaq_h3xxx_sleeve_suspend_noirq,
+ ipaq_h3xxx_sleeve_resume_noirq)
+ SET_SYSTEM_SLEEP_PM_OPS(ipaq_h3xxx_sleeve_suspend,
+ ipaq_h3xxx_sleeve_resume)
+};
+
+static struct platform_driver ipaq_h3xxx_sleeve = {
+ .driver = {
+ .name = "ipaq-h3xxx-sleeve",
+ .pm = &ipaq_h3xxx_sleeve_pm,
+ },
+ .probe = ipaq_h3xxx_sleeve_probe,
+ .remove = ipaq_h3xxx_sleeve_remove,
+};
+
+module_platform_driver(ipaq_h3xxx_sleeve);
+
+MODULE_ALIAS("platform:ipaq-h3xxx-sleeve");
+MODULE_LICENSE("GPL");