[PATCH] [RFC] firewire: Add ohci1394 PCI runtime power management support

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



OHCI firewire controllers with 1394a or later phys support the generation
of PMEs from D3hot when a cable is connected or disconnected. This patch
adds support for enabling this functionality, along with hooking it into
the PCI runtime PM framework. The device will be kept awake if a cable
is connected to any of its ports, and put to sleep if no connection is
present.

For sanity purposes, the code ensures that the phy is recent enough to
implement the required functionality. It also ensures that all ports have
the interrupt enabled - a VIA chip I've tested appears to only allow
int_enable to be set on one port at a time, which is inadequate for the
purposes of this code.

This depends on the PCI runtime PM code, which is not yet upstream. I'm
sending this out now for sanity checking.

Signed-off-by: Matthew Garrett <mjg@xxxxxxxxxx>
---
 drivers/firewire/ohci.c |  283 +++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 238 insertions(+), 45 deletions(-)

diff --git a/drivers/firewire/ohci.c b/drivers/firewire/ohci.c
index a61571c..3f90de9 100644
--- a/drivers/firewire/ohci.c
+++ b/drivers/firewire/ohci.c
@@ -35,6 +35,7 @@
 #include <linux/moduleparam.h>
 #include <linux/pci.h>
 #include <linux/pci_ids.h>
+#include <linux/pm_runtime.h>
 #include <linux/spinlock.h>
 #include <linux/string.h>
 
@@ -184,6 +185,7 @@ struct fw_ohci {
 	dma_addr_t self_id_bus;
 	__le32 *self_id_cpu;
 	struct tasklet_struct bus_reset_tasklet;
+	struct tasklet_struct phy_tasklet;
 	int node_id;
 	int generation;
 	int request_generation;	/* for timestamping incoming requests */
@@ -217,6 +219,8 @@ struct fw_ohci {
 	u64 ir_context_channels;
 	u32 ir_context_mask;
 	struct iso_context *ir_context_list;
+
+	bool runtime_pm;
 };
 
 static inline struct fw_ohci *fw_ohci(struct fw_card *card)
@@ -452,25 +456,81 @@ static inline void flush_writes(const struct fw_ohci *ohci)
 	reg_read(ohci, OHCI1394_Version);
 }
 
-static int ohci_update_phy_reg(struct fw_card *card, int addr,
-			       int clear_bits, int set_bits)
+static int ohci_read_phy_reg(struct fw_card *card, int addr, u32 *val)
 {
 	struct fw_ohci *ohci = fw_ohci(card);
-	u32 val, old;
+	int i;
 
 	reg_write(ohci, OHCI1394_PhyControl, OHCI1394_PhyControl_Read(addr));
 	flush_writes(ohci);
-	msleep(2);
-	val = reg_read(ohci, OHCI1394_PhyControl);
-	if ((val & OHCI1394_PhyControl_ReadDone) == 0) {
-		fw_error("failed to set phy reg bits.\n");
+
+	for (i = 0; i < OHCI_LOOP_COUNT; i++) {
+		*val = reg_read(ohci, OHCI1394_PhyControl);
+		if (*val & OHCI1394_PhyControl_ReadDone)
+			break;
+		mdelay(1);
+	}
+
+	if (i == OHCI_LOOP_COUNT) {
+		fw_error("Get PHY Reg timeout [0x%08x/0x%08x/%d]",
+			 *val, *val & OHCI1394_PhyControl_ReadDone, i);
 		return -EBUSY;
 	}
 
-	old = OHCI1394_PhyControl_ReadData(val);
-	old = (old & ~clear_bits) | set_bits;
+	*val = OHCI1394_PhyControl_ReadData(*val);
+
+	return 0;
+}
+
+static int ohci_update_phy_reg(struct fw_card *card, int addr,
+			       int clear_bits, int set_bits)
+{
+	struct fw_ohci *ohci = fw_ohci(card);
+	u32 r;
+	int i;
+
+	if (ohci_read_phy_reg(card, addr, &r))
+		return -EBUSY;
+
+	r = (r & ~clear_bits) | set_bits;
 	reg_write(ohci, OHCI1394_PhyControl,
-		  OHCI1394_PhyControl_Write(addr, old));
+		  OHCI1394_PhyControl_Write(addr, r));
+
+	for (i = 0; i < OHCI_LOOP_COUNT; i++) {
+		r = reg_read(ohci, OHCI1394_PhyControl);
+		if (!(r & OHCI1394_PhyControl_WriteDone))
+			break;
+	}
+
+	if (i == OHCI_LOOP_COUNT) {
+		fw_error("Set PHY Reg timeout [0x%08x/0x%08x/%d]",
+			 r, r & OHCI1394_PhyControl_WriteDone, i);
+		return -EBUSY;
+	}
+
+	return 0;
+}
+
+static int ohci_read_port_reg(struct fw_card *card, int port,
+			      int addr, u32 *val)
+{
+	if (ohci_update_phy_reg(card, 7, 0xff, port))
+		return -EBUSY;
+
+	if (ohci_read_phy_reg(card, 8+addr, val))
+		return -EBUSY;
+
+	return 0;
+}
+
+static int ohci_write_port_reg(struct fw_card *card, int port,
+			       int addr, int clear_bits, int set_bits)
+{
+	if (ohci_update_phy_reg(card, 7, 0xff, port))
+		return -EBUSY;
+
+	if (ohci_update_phy_reg(card, 8+addr, clear_bits, set_bits))
+		return -EBUSY;
 
 	return 0;
 }
@@ -1250,6 +1310,45 @@ static void at_context_transmit(struct context *ctx, struct fw_packet *packet)
 
 }
 
+static int ohci_port_is_connected(struct fw_card *card)
+{
+	int i, total_ports;
+	u32 val;
+
+	if (ohci_read_phy_reg(card, 2, &val))
+		return -EBUSY;
+
+	total_ports = val & 0x1f;
+
+	for (i = 0; i < total_ports; i++) {
+		ohci_read_port_reg(card, i, 0, &val);
+		if (val & 0x04)
+			return 1;
+	}
+
+	return 0;
+}
+
+static int ohci_clear_port_event(struct fw_card *card)
+{
+
+	return ohci_update_phy_reg(card, 5, 0, 0x04);
+}
+
+static void phy_tasklet(unsigned long data)
+{
+	struct fw_ohci *ohci = (struct fw_ohci *)data;
+	u32 val;
+
+	ohci_read_phy_reg(&ohci->card, 5, &val);
+
+	if (val & 0x04)
+		ohci_clear_port_event(&ohci->card);
+
+	if (!ohci_port_is_connected(&ohci->card))
+		pm_schedule_suspend(ohci->card.device, 1000);
+}
+
 static void bus_reset_tasklet(unsigned long data)
 {
 	struct fw_ohci *ohci = (struct fw_ohci *)data;
@@ -1259,14 +1358,17 @@ static void bus_reset_tasklet(unsigned long data)
 	void *free_rom = NULL;
 	dma_addr_t free_rom_bus = 0;
 
+	/* Cancel any pending suspend requests */
+	pm_runtime_resume(ohci->card.device);
+
 	reg = reg_read(ohci, OHCI1394_NodeID);
 	if (!(reg & OHCI1394_NodeID_idValid)) {
 		fw_notify("node ID not valid, new bus reset in progress\n");
-		return;
+		goto out;
 	}
 	if ((reg & OHCI1394_NodeID_nodeNumber) == 63) {
 		fw_notify("malconfigured bus\n");
-		return;
+		goto out;
 	}
 	ohci->node_id = reg & (OHCI1394_NodeID_busNumber |
 			       OHCI1394_NodeID_nodeNumber);
@@ -1274,7 +1376,7 @@ static void bus_reset_tasklet(unsigned long data)
 	reg = reg_read(ohci, OHCI1394_SelfIDCount);
 	if (reg & OHCI1394_SelfIDCount_selfIDError) {
 		fw_notify("inconsistent self IDs\n");
-		return;
+		goto out;
 	}
 	/*
 	 * The count in the SelfIDCount register is the number of
@@ -1285,7 +1387,7 @@ static void bus_reset_tasklet(unsigned long data)
 	self_id_count = (reg >> 3) & 0xff;
 	if (self_id_count == 0 || self_id_count > 252) {
 		fw_notify("inconsistent self IDs\n");
-		return;
+		goto out;
 	}
 	generation = (cond_le32_to_cpu(ohci->self_id_cpu[0]) >> 16) & 0xff;
 	rmb();
@@ -1293,7 +1395,7 @@ static void bus_reset_tasklet(unsigned long data)
 	for (i = 1, j = 0; j < self_id_count; i += 2, j++) {
 		if (ohci->self_id_cpu[i] != ~ohci->self_id_cpu[i + 1]) {
 			fw_notify("inconsistent self IDs\n");
-			return;
+			goto out;
 		}
 		ohci->self_id_buffer[j] =
 				cond_le32_to_cpu(ohci->self_id_cpu[i]);
@@ -1318,7 +1420,7 @@ static void bus_reset_tasklet(unsigned long data)
 	if (new_generation != generation) {
 		fw_notify("recursive bus reset detected, "
 			  "discarding self ids\n");
-		return;
+		goto out;
 	}
 
 	/* FIXME: Document how the locking works. */
@@ -1379,6 +1481,11 @@ static void bus_reset_tasklet(unsigned long data)
 
 	fw_core_handle_bus_reset(&ohci->card, ohci->node_id, generation,
 				 self_id_count, ohci->self_id_buffer);
+out:
+	if (!ohci_port_is_connected(&ohci->card))
+		pm_schedule_suspend(ohci->card.device, 1000);
+
+	return;
 }
 
 static irqreturn_t irq_handler(int irq, void *data)
@@ -1396,6 +1503,9 @@ static irqreturn_t irq_handler(int irq, void *data)
 	reg_write(ohci, OHCI1394_IntEventClear, event & ~OHCI1394_busReset);
 	log_irqs(event);
 
+	if (event & OHCI1394_phy)
+		tasklet_schedule(&ohci->phy_tasklet);
+
 	if (event & OHCI1394_selfIDComplete)
 		tasklet_schedule(&ohci->bus_reset_tasklet);
 
@@ -1488,6 +1598,58 @@ static void copy_config_rom(__be32 *dest, const __be32 *src, size_t length)
 		memset(&dest[length], 0, CONFIG_ROM_SIZE - size);
 }
 
+static int is_extended_phy(struct fw_card *card)
+{
+	u32 val;
+
+	if (ohci_read_phy_reg(card, 2, &val))
+		return -EBUSY;
+
+	if ((val & 0xE0) == 0xE0)
+		return 1;
+
+	return 0;
+}
+
+static int ohci_configure_wakeup(struct fw_card *card)
+{
+	struct fw_ohci *ohci = fw_ohci(card);
+	int i, total_ports;
+	u32 val;
+
+	if (ohci_read_phy_reg(card, 2, &val))
+		return -EBUSY;
+
+	total_ports = val & 0x1f;
+
+	/* Attempt to enable port interrupts */
+	for (i = 0; i < total_ports; i++) {
+		ohci_write_port_reg(card, i, 1, 0, 0x10);
+		ohci_read_port_reg(card, i, 1, &val);
+		if (!(val & 0x10)) {
+			fw_error("Failed to enable interrupts on port %d\n", i);
+			return -ENODEV;
+		}
+	}
+
+	/*
+	 * Some cards appear to only support int_enable on a single port.
+	 * Check that it's still set on everything
+	 */
+
+	for (i = 0; i < total_ports; i++) {
+		ohci_read_port_reg(card, i, 1, &val);
+		if (!(val & 0x10)) {
+			fw_error("int_enable on port %d didn't stick\n", i);
+			return -ENODEV;
+		}
+	}
+
+	ohci->runtime_pm = 1;
+
+	return 0;
+}
+
 static int ohci_enable(struct fw_card *card,
 		       const __be32 *config_rom, size_t length)
 {
@@ -1555,7 +1717,7 @@ static int ohci_enable(struct fw_card *card,
 		  OHCI1394_postedWriteErr | OHCI1394_cycleTooLong |
 		  OHCI1394_cycleInconsistent |
 		  OHCI1394_cycle64Seconds | OHCI1394_regAccessFail |
-		  OHCI1394_masterIntEnable);
+		  OHCI1394_masterIntEnable | OHCI1394_phy);
 	if (param_debug & OHCI_PARAM_DEBUG_BUSRESETS)
 		reg_write(ohci, OHCI1394_IntMaskSet, OHCI1394_busReset);
 
@@ -1604,6 +1766,7 @@ static int ohci_enable(struct fw_card *card,
 	ohci->next_header = ohci->next_config_rom[0];
 	ohci->next_config_rom[0] = 0;
 	reg_write(ohci, OHCI1394_ConfigROMhdr, 0);
+
 	reg_write(ohci, OHCI1394_BusOptions,
 		  be32_to_cpu(ohci->next_config_rom[2]));
 	reg_write(ohci, OHCI1394_ConfigROMmap, ohci->next_config_rom_bus);
@@ -1624,6 +1787,11 @@ static int ohci_enable(struct fw_card *card,
 		  OHCI1394_HCControl_BIBimageValid);
 	flush_writes(ohci);
 
+	if (is_extended_phy(card)) {
+		ohci_configure_wakeup(card);
+		ohci_clear_port_event(card);
+	}
+
 	/*
 	 * We are ready to go, initiate bus reset to finish the
 	 * initialization.
@@ -2455,6 +2623,9 @@ static int __devinit pci_probe(struct pci_dev *dev,
 	tasklet_init(&ohci->bus_reset_tasklet,
 		     bus_reset_tasklet, (unsigned long)ohci);
 
+	tasklet_init(&ohci->phy_tasklet,
+		     phy_tasklet, (unsigned long)ohci);
+
 	err = pci_request_region(dev, 0, ohci_driver_name);
 	if (err) {
 		fw_error("MMIO resource unavailable\n");
@@ -2548,6 +2719,11 @@ static int __devinit pci_probe(struct pci_dev *dev,
 	if (err)
 		goto fail_self_id;
 
+	if (pci_dev_run_wake(dev) && ohci->runtime_pm) {
+		pm_runtime_set_active(&dev->dev);
+		pm_runtime_enable(&dev->dev);
+	}
+
 	fw_notify("Added fw-ohci device %s, OHCI version %x.%x\n",
 		  dev_name(&dev->dev), version >> 16, version & 0xff);
 
@@ -2580,9 +2756,17 @@ static int __devinit pci_probe(struct pci_dev *dev,
 
 static void pci_remove(struct pci_dev *dev)
 {
-	struct fw_ohci *ohci;
+	struct fw_ohci *ohci = pci_get_drvdata(dev);
+
+	pm_runtime_get_sync(&dev->dev);
+
+	if (pci_dev_run_wake(dev) && ohci->runtime_pm) {
+		pm_runtime_disable(&dev->dev);
+		pm_runtime_set_suspended(&dev->dev);
+	}
+
+	pm_runtime_put_noidle(&dev->dev);
 
-	ohci = pci_get_drvdata(dev);
 	reg_write(ohci, OHCI1394_IntMaskClear, ~0);
 	flush_writes(ohci);
 	fw_core_remove_card(&ohci->card);
@@ -2619,42 +2803,41 @@ static void pci_remove(struct pci_dev *dev)
 }
 
 #ifdef CONFIG_PM
-static int pci_suspend(struct pci_dev *dev, pm_message_t state)
+static int ohci_pci_suspend(struct device *dev)
 {
-	struct fw_ohci *ohci = pci_get_drvdata(dev);
-	int err;
+	struct pci_dev *pdev = to_pci_dev(dev);
+	struct fw_ohci *ohci = pci_get_drvdata(pdev);
 
 	software_reset(ohci);
-	free_irq(dev->irq, ohci);
-	err = pci_save_state(dev);
-	if (err) {
-		fw_error("pci_save_state failed\n");
-		return err;
-	}
-	err = pci_set_power_state(dev, pci_choose_state(dev, state));
-	if (err)
-		fw_error("pci_set_power_state failed with %d\n", err);
-	ohci_pmac_off(dev);
+
+	ohci_pmac_off(pdev);
 
 	return 0;
 }
 
-static int pci_resume(struct pci_dev *dev)
+static int ohci_pci_runtime_suspend(struct device *dev)
 {
-	struct fw_ohci *ohci = pci_get_drvdata(dev);
-	int err;
+	struct pci_dev *pdev = to_pci_dev(dev);
+	struct fw_ohci *ohci = pci_get_drvdata(pdev);
 
-	ohci_pmac_on(dev);
-	pci_set_power_state(dev, PCI_D0);
-	pci_restore_state(dev);
-	err = pci_enable_device(dev);
-	if (err) {
-		fw_error("pci_enable_device failed\n");
-		return err;
-	}
+	ohci_clear_port_event(&ohci->card);
+	return ohci_pci_suspend(dev);
+}
+
+static int ohci_pci_resume(struct device *dev)
+{
+	struct pci_dev *pdev = to_pci_dev(dev);
+	struct fw_ohci *ohci = pci_get_drvdata(pdev);
+
+	ohci_pmac_on(pdev);
 
 	return ohci_enable(&ohci->card, NULL, 0);
 }
+
+static int ohci_pci_runtime_idle(struct device *dev)
+{
+	return -EBUSY;
+}
 #endif
 
 static struct pci_device_id pci_table[] = {
@@ -2664,14 +2847,24 @@ static struct pci_device_id pci_table[] = {
 
 MODULE_DEVICE_TABLE(pci, pci_table);
 
+static struct dev_pm_ops ohci_pci_pm_ops = {
+	.suspend = ohci_pci_suspend,
+	.resume = ohci_pci_resume,
+	.freeze = ohci_pci_suspend,
+	.thaw = ohci_pci_resume,
+	.poweroff = ohci_pci_suspend,
+	.runtime_suspend = ohci_pci_runtime_suspend,
+	.runtime_resume = ohci_pci_resume,
+	.runtime_idle = ohci_pci_runtime_idle,
+};
+
 static struct pci_driver fw_ohci_pci_driver = {
 	.name		= ohci_driver_name,
 	.id_table	= pci_table,
 	.probe		= pci_probe,
 	.remove		= pci_remove,
 #ifdef CONFIG_PM
-	.resume		= pci_resume,
-	.suspend	= pci_suspend,
+	.driver.pm	= &ohci_pci_pm_ops,
 #endif
 };
 
-- 
1.6.5.2

--
To unsubscribe from this list: send the line "unsubscribe linux-acpi" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html

[Index of Archives]     [Linux IBM ACPI]     [Linux Power Management]     [Linux Kernel]     [Linux Laptop]     [Kernel Newbies]     [Share Photos]     [Security]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Samba]     [Video 4 Linux]     [Device Mapper]     [Linux Resources]

  Powered by Linux