diff options
author | Linus Torvalds <torvalds@woody.linux-foundation.org> | 2007-10-29 12:12:34 -0700 |
---|---|---|
committer | Linus Torvalds <torvalds@woody.linux-foundation.org> | 2007-10-29 12:12:34 -0700 |
commit | 3529a233421fc43fa7bfdf7a4317daf28348a23d (patch) | |
tree | 520558ffb70e4f4743495ad7a62d4c775c0c1ea3 /drivers | |
parent | 00cda56d39f013cce60f44f1e3da19b87eba5d85 (diff) | |
parent | 31556594f913fa81d008cecfe46d7211c919a853 (diff) | |
download | op-kernel-dev-3529a233421fc43fa7bfdf7a4317daf28348a23d.zip op-kernel-dev-3529a233421fc43fa7bfdf7a4317daf28348a23d.tar.gz |
Merge branch 'alpm' of master.kernel.org:/pub/scm/linux/kernel/git/jgarzik/libata-dev
* 'alpm' of master.kernel.org:/pub/scm/linux/kernel/git/jgarzik/libata-dev:
[libata] AHCI: add hw link power management support
[libata] Link power management infrastructure
Diffstat (limited to 'drivers')
-rw-r--r-- | drivers/ata/ahci.c | 157 | ||||
-rw-r--r-- | drivers/ata/libata-core.c | 196 | ||||
-rw-r--r-- | drivers/ata/libata-eh.c | 4 | ||||
-rw-r--r-- | drivers/ata/libata-scsi.c | 68 | ||||
-rw-r--r-- | drivers/ata/libata.h | 2 |
5 files changed, 424 insertions, 3 deletions
diff --git a/drivers/ata/ahci.c b/drivers/ata/ahci.c index c8ab947..ed9b407 100644 --- a/drivers/ata/ahci.c +++ b/drivers/ata/ahci.c @@ -49,6 +49,9 @@ #define DRV_NAME "ahci" #define DRV_VERSION "3.0" +static int ahci_enable_alpm(struct ata_port *ap, + enum link_pm policy); +static void ahci_disable_alpm(struct ata_port *ap); enum { AHCI_PCI_BAR = 5, @@ -99,6 +102,7 @@ enum { HOST_CAP_SSC = (1 << 14), /* Slumber capable */ HOST_CAP_PMP = (1 << 17), /* Port Multiplier support */ HOST_CAP_CLO = (1 << 24), /* Command List Override support */ + HOST_CAP_ALPM = (1 << 26), /* Aggressive Link PM support */ HOST_CAP_SSS = (1 << 27), /* Staggered Spin-up */ HOST_CAP_SNTF = (1 << 29), /* SNotification register */ HOST_CAP_NCQ = (1 << 30), /* Native Command Queueing */ @@ -155,6 +159,8 @@ enum { PORT_IRQ_PIOS_FIS | PORT_IRQ_D2H_REG_FIS, /* PORT_CMD bits */ + PORT_CMD_ASP = (1 << 27), /* Aggressive Slumber/Partial */ + PORT_CMD_ALPE = (1 << 26), /* Aggressive Link PM enable */ PORT_CMD_ATAPI = (1 << 24), /* Device is ATAPI */ PORT_CMD_PMP = (1 << 17), /* PMP attached */ PORT_CMD_LIST_ON = (1 << 15), /* cmd list DMA engine running */ @@ -178,13 +184,14 @@ enum { AHCI_HFLAG_MV_PATA = (1 << 4), /* PATA port */ AHCI_HFLAG_NO_MSI = (1 << 5), /* no PCI MSI */ AHCI_HFLAG_NO_PMP = (1 << 6), /* no PMP */ + AHCI_HFLAG_NO_HOTPLUG = (1 << 7), /* ignore PxSERR.DIAG.N */ /* ap->flags bits */ - AHCI_FLAG_NO_HOTPLUG = (1 << 24), /* ignore PxSERR.DIAG.N */ AHCI_FLAG_COMMON = ATA_FLAG_SATA | ATA_FLAG_NO_LEGACY | ATA_FLAG_MMIO | ATA_FLAG_PIO_DMA | - ATA_FLAG_ACPI_SATA | ATA_FLAG_AN, + ATA_FLAG_ACPI_SATA | ATA_FLAG_AN | + ATA_FLAG_IPM, AHCI_LFLAG_COMMON = ATA_LFLAG_SKIP_D2H_BSY, }; @@ -254,6 +261,11 @@ static int ahci_pci_device_suspend(struct pci_dev *pdev, pm_message_t mesg); static int ahci_pci_device_resume(struct pci_dev *pdev); #endif +static struct class_device_attribute *ahci_shost_attrs[] = { + &class_device_attr_link_power_management_policy, + NULL +}; + static struct scsi_host_template ahci_sht = { .module = THIS_MODULE, .name = DRV_NAME, @@ -271,6 +283,7 @@ static struct scsi_host_template ahci_sht = { .slave_configure = ata_scsi_slave_config, .slave_destroy = ata_scsi_slave_destroy, .bios_param = ata_std_bios_param, + .shost_attrs = ahci_shost_attrs, }; static const struct ata_port_operations ahci_ops = { @@ -302,6 +315,8 @@ static const struct ata_port_operations ahci_ops = { .port_suspend = ahci_port_suspend, .port_resume = ahci_port_resume, #endif + .enable_pm = ahci_enable_alpm, + .disable_pm = ahci_disable_alpm, .port_start = ahci_port_start, .port_stop = ahci_port_stop, @@ -836,6 +851,130 @@ static void ahci_power_up(struct ata_port *ap) writel(cmd | PORT_CMD_ICC_ACTIVE, port_mmio + PORT_CMD); } +static void ahci_disable_alpm(struct ata_port *ap) +{ + struct ahci_host_priv *hpriv = ap->host->private_data; + void __iomem *port_mmio = ahci_port_base(ap); + u32 cmd; + struct ahci_port_priv *pp = ap->private_data; + + /* IPM bits should be disabled by libata-core */ + /* get the existing command bits */ + cmd = readl(port_mmio + PORT_CMD); + + /* disable ALPM and ASP */ + cmd &= ~PORT_CMD_ASP; + cmd &= ~PORT_CMD_ALPE; + + /* force the interface back to active */ + cmd |= PORT_CMD_ICC_ACTIVE; + + /* write out new cmd value */ + writel(cmd, port_mmio + PORT_CMD); + cmd = readl(port_mmio + PORT_CMD); + + /* wait 10ms to be sure we've come out of any low power state */ + msleep(10); + + /* clear out any PhyRdy stuff from interrupt status */ + writel(PORT_IRQ_PHYRDY, port_mmio + PORT_IRQ_STAT); + + /* go ahead and clean out PhyRdy Change from Serror too */ + ahci_scr_write(ap, SCR_ERROR, ((1 << 16) | (1 << 18))); + + /* + * Clear flag to indicate that we should ignore all PhyRdy + * state changes + */ + hpriv->flags &= ~AHCI_HFLAG_NO_HOTPLUG; + + /* + * Enable interrupts on Phy Ready. + */ + pp->intr_mask |= PORT_IRQ_PHYRDY; + writel(pp->intr_mask, port_mmio + PORT_IRQ_MASK); + + /* + * don't change the link pm policy - we can be called + * just to turn of link pm temporarily + */ +} + +static int ahci_enable_alpm(struct ata_port *ap, + enum link_pm policy) +{ + struct ahci_host_priv *hpriv = ap->host->private_data; + void __iomem *port_mmio = ahci_port_base(ap); + u32 cmd; + struct ahci_port_priv *pp = ap->private_data; + u32 asp; + + /* Make sure the host is capable of link power management */ + if (!(hpriv->cap & HOST_CAP_ALPM)) + return -EINVAL; + + switch (policy) { + case MAX_PERFORMANCE: + case NOT_AVAILABLE: + /* + * if we came here with NOT_AVAILABLE, + * it just means this is the first time we + * have tried to enable - default to max performance, + * and let the user go to lower power modes on request. + */ + ahci_disable_alpm(ap); + return 0; + case MIN_POWER: + /* configure HBA to enter SLUMBER */ + asp = PORT_CMD_ASP; + break; + case MEDIUM_POWER: + /* configure HBA to enter PARTIAL */ + asp = 0; + break; + default: + return -EINVAL; + } + + /* + * Disable interrupts on Phy Ready. This keeps us from + * getting woken up due to spurious phy ready interrupts + * TBD - Hot plug should be done via polling now, is + * that even supported? + */ + pp->intr_mask &= ~PORT_IRQ_PHYRDY; + writel(pp->intr_mask, port_mmio + PORT_IRQ_MASK); + + /* + * Set a flag to indicate that we should ignore all PhyRdy + * state changes since these can happen now whenever we + * change link state + */ + hpriv->flags |= AHCI_HFLAG_NO_HOTPLUG; + + /* get the existing command bits */ + cmd = readl(port_mmio + PORT_CMD); + + /* + * Set ASP based on Policy + */ + cmd |= asp; + + /* + * Setting this bit will instruct the HBA to aggressively + * enter a lower power link state when it's appropriate and + * based on the value set above for ASP + */ + cmd |= PORT_CMD_ALPE; + + /* write out new cmd value */ + writel(cmd, port_mmio + PORT_CMD); + cmd = readl(port_mmio + PORT_CMD); + + /* IPM bits should be set by libata-core */ + return 0; +} + #ifdef CONFIG_PM static void ahci_power_down(struct ata_port *ap) { @@ -1504,6 +1643,17 @@ static void ahci_port_intr(struct ata_port *ap) if (unlikely(resetting)) status &= ~PORT_IRQ_BAD_PMP; + /* If we are getting PhyRdy, this is + * just a power state change, we should + * clear out this, plus the PhyRdy/Comm + * Wake bits from Serror + */ + if ((hpriv->flags & AHCI_HFLAG_NO_HOTPLUG) && + (status & PORT_IRQ_PHYRDY)) { + status &= ~PORT_IRQ_PHYRDY; + ahci_scr_write(ap, SCR_ERROR, ((1 << 16) | (1 << 18))); + } + if (unlikely(status & PORT_IRQ_ERROR)) { ahci_error_intr(ap, status); return; @@ -2151,6 +2301,9 @@ static int ahci_init_one(struct pci_dev *pdev, const struct pci_device_id *ent) ata_port_pbar_desc(ap, AHCI_PCI_BAR, 0x100 + ap->port_no * 0x80, "port"); + /* set initial link pm policy */ + ap->pm_policy = NOT_AVAILABLE; + /* standard SATA port setup */ if (hpriv->port_map & (1 << i)) ap->ioaddr.cmd_addr = port_mmio; diff --git a/drivers/ata/libata-core.c b/drivers/ata/libata-core.c index e512903..63035d7 100644 --- a/drivers/ata/libata-core.c +++ b/drivers/ata/libata-core.c @@ -620,6 +620,177 @@ void ata_dev_disable(struct ata_device *dev) } } +static int ata_dev_set_dipm(struct ata_device *dev, enum link_pm policy) +{ + struct ata_link *link = dev->link; + struct ata_port *ap = link->ap; + u32 scontrol; + unsigned int err_mask; + int rc; + + /* + * disallow DIPM for drivers which haven't set + * ATA_FLAG_IPM. This is because when DIPM is enabled, + * phy ready will be set in the interrupt status on + * state changes, which will cause some drivers to + * think there are errors - additionally drivers will + * need to disable hot plug. + */ + if (!(ap->flags & ATA_FLAG_IPM) || !ata_dev_enabled(dev)) { + ap->pm_policy = NOT_AVAILABLE; + return -EINVAL; + } + + /* + * For DIPM, we will only enable it for the + * min_power setting. + * + * Why? Because Disks are too stupid to know that + * If the host rejects a request to go to SLUMBER + * they should retry at PARTIAL, and instead it + * just would give up. So, for medium_power to + * work at all, we need to only allow HIPM. + */ + rc = sata_scr_read(link, SCR_CONTROL, &scontrol); + if (rc) + return rc; + + switch (policy) { + case MIN_POWER: + /* no restrictions on IPM transitions */ + scontrol &= ~(0x3 << 8); + rc = sata_scr_write(link, SCR_CONTROL, scontrol); + if (rc) + return rc; + + /* enable DIPM */ + if (dev->flags & ATA_DFLAG_DIPM) + err_mask = ata_dev_set_feature(dev, + SETFEATURES_SATA_ENABLE, SATA_DIPM); + break; + case MEDIUM_POWER: + /* allow IPM to PARTIAL */ + scontrol &= ~(0x1 << 8); + scontrol |= (0x2 << 8); + rc = sata_scr_write(link, SCR_CONTROL, scontrol); + if (rc) + return rc; + + /* disable DIPM */ + if (ata_dev_enabled(dev) && (dev->flags & ATA_DFLAG_DIPM)) + err_mask = ata_dev_set_feature(dev, + SETFEATURES_SATA_DISABLE, SATA_DIPM); + break; + case NOT_AVAILABLE: + case MAX_PERFORMANCE: + /* disable all IPM transitions */ + scontrol |= (0x3 << 8); + rc = sata_scr_write(link, SCR_CONTROL, scontrol); + if (rc) + return rc; + + /* disable DIPM */ + if (ata_dev_enabled(dev) && (dev->flags & ATA_DFLAG_DIPM)) + err_mask = ata_dev_set_feature(dev, + SETFEATURES_SATA_DISABLE, SATA_DIPM); + break; + } + + /* FIXME: handle SET FEATURES failure */ + (void) err_mask; + + return 0; +} + +/** + * ata_dev_enable_pm - enable SATA interface power management + * @device - device to enable ipm for + * @policy - the link power management policy + * + * Enable SATA Interface power management. This will enable + * Device Interface Power Management (DIPM) for min_power + * policy, and then call driver specific callbacks for + * enabling Host Initiated Power management. + * + * Locking: Caller. + * Returns: -EINVAL if IPM is not supported, 0 otherwise. + */ +void ata_dev_enable_pm(struct ata_device *dev, enum link_pm policy) +{ + int rc = 0; + struct ata_port *ap = dev->link->ap; + + /* set HIPM first, then DIPM */ + if (ap->ops->enable_pm) + rc = ap->ops->enable_pm(ap, policy); + if (rc) + goto enable_pm_out; + rc = ata_dev_set_dipm(dev, policy); + +enable_pm_out: + if (rc) + ap->pm_policy = MAX_PERFORMANCE; + else + ap->pm_policy = policy; + return /* rc */; /* hopefully we can use 'rc' eventually */ +} + +/** + * ata_dev_disable_pm - disable SATA interface power management + * @device - device to enable ipm for + * + * Disable SATA Interface power management. This will disable + * Device Interface Power Management (DIPM) without changing + * policy, call driver specific callbacks for disabling Host + * Initiated Power management. + * + * Locking: Caller. + * Returns: void + */ +static void ata_dev_disable_pm(struct ata_device *dev) +{ + struct ata_port *ap = dev->link->ap; + + ata_dev_set_dipm(dev, MAX_PERFORMANCE); + if (ap->ops->disable_pm) + ap->ops->disable_pm(ap); +} + +void ata_lpm_schedule(struct ata_port *ap, enum link_pm policy) +{ + ap->pm_policy = policy; + ap->link.eh_info.action |= ATA_EHI_LPM; + ap->link.eh_info.flags |= ATA_EHI_NO_AUTOPSY; + ata_port_schedule_eh(ap); +} + +static void ata_lpm_enable(struct ata_host *host) +{ + struct ata_link *link; + struct ata_port *ap; + struct ata_device *dev; + int i; + + for (i = 0; i < host->n_ports; i++) { + ap = host->ports[i]; + ata_port_for_each_link(link, ap) { + ata_link_for_each_dev(dev, link) + ata_dev_disable_pm(dev); + } + } +} + +static void ata_lpm_disable(struct ata_host *host) +{ + int i; + + for (i = 0; i < host->n_ports; i++) { + struct ata_port *ap = host->ports[i]; + ata_lpm_schedule(ap, ap->pm_policy); + } +} + + /** * ata_devchk - PATA device presence detection * @ap: ATA channel to examine @@ -2101,6 +2272,13 @@ int ata_dev_configure(struct ata_device *dev) if (dev->flags & ATA_DFLAG_LBA48) dev->max_sectors = ATA_MAX_SECTORS_LBA48; + if (!(dev->horkage & ATA_HORKAGE_IPM)) { + if (ata_id_has_hipm(dev->id)) + dev->flags |= ATA_DFLAG_HIPM; + if (ata_id_has_dipm(dev->id)) + dev->flags |= ATA_DFLAG_DIPM; + } + if (dev->horkage & ATA_HORKAGE_DIAGNOSTIC) { /* Let the user know. We don't want to disallow opens for rescue purposes, or in case the vendor is just a blithering @@ -2126,6 +2304,13 @@ int ata_dev_configure(struct ata_device *dev) dev->max_sectors = min_t(unsigned int, ATA_MAX_SECTORS_128, dev->max_sectors); + if (ata_dev_blacklisted(dev) & ATA_HORKAGE_IPM) { + dev->horkage |= ATA_HORKAGE_IPM; + + /* reset link pm_policy for this port to no pm */ + ap->pm_policy = MAX_PERFORMANCE; + } + if (ap->ops->dev_config) ap->ops->dev_config(dev); @@ -6362,6 +6547,12 @@ int ata_host_suspend(struct ata_host *host, pm_message_t mesg) { int rc; + /* + * disable link pm on all ports before requesting + * any pm activity + */ + ata_lpm_enable(host); + rc = ata_host_request_pm(host, mesg, 0, ATA_EHI_QUIET, 1); if (rc == 0) host->dev->power.power_state = mesg; @@ -6384,6 +6575,9 @@ void ata_host_resume(struct ata_host *host) ata_host_request_pm(host, PMSG_ON, ATA_EH_SOFTRESET, ATA_EHI_NO_AUTOPSY | ATA_EHI_QUIET, 0); host->dev->power.power_state = PMSG_ON; + + /* reenable link pm */ + ata_lpm_disable(host); } #endif @@ -6926,6 +7120,7 @@ int ata_host_register(struct ata_host *host, struct scsi_host_template *sht) struct ata_port *ap = host->ports[i]; ata_scsi_scan_host(ap, 1); + ata_lpm_schedule(ap, ap->pm_policy); } return 0; @@ -7322,7 +7517,6 @@ const struct ata_port_info ata_dummy_port_info = { * likely to change as new drivers are added and updated. * Do not depend on ABI/API stability. */ - EXPORT_SYMBOL_GPL(sata_deb_timing_normal); EXPORT_SYMBOL_GPL(sata_deb_timing_hotplug); EXPORT_SYMBOL_GPL(sata_deb_timing_long); diff --git a/drivers/ata/libata-eh.c b/drivers/ata/libata-eh.c index ec55d63..fefea74 100644 --- a/drivers/ata/libata-eh.c +++ b/drivers/ata/libata-eh.c @@ -2628,6 +2628,10 @@ int ata_eh_recover(struct ata_port *ap, ata_prereset_fn_t prereset, ehc->i.flags &= ~ATA_EHI_SETMODE; } + if (ehc->i.action & ATA_EHI_LPM) + ata_link_for_each_dev(dev, link) + ata_dev_enable_pm(dev, ap->pm_policy); + /* this link is okay now */ ehc->i.flags = 0; continue; diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c index f752edd..93bd36c 100644 --- a/drivers/ata/libata-scsi.c +++ b/drivers/ata/libata-scsi.c @@ -110,6 +110,74 @@ static struct scsi_transport_template ata_scsi_transport_template = { }; +static const struct { + enum link_pm value; + const char *name; +} link_pm_policy[] = { + { NOT_AVAILABLE, "max_performance" }, + { MIN_POWER, "min_power" }, + { MAX_PERFORMANCE, "max_performance" }, + { MEDIUM_POWER, "medium_power" }, +}; + +const char *ata_scsi_lpm_get(enum link_pm policy) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(link_pm_policy); i++) + if (link_pm_policy[i].value == policy) + return link_pm_policy[i].name; + + return NULL; +} + +static ssize_t ata_scsi_lpm_put(struct class_device *class_dev, + const char *buf, size_t count) +{ + struct Scsi_Host *shost = class_to_shost(class_dev); + struct ata_port *ap = ata_shost_to_port(shost); + enum link_pm policy = 0; + int i; + + /* + * we are skipping array location 0 on purpose - this + * is because a value of NOT_AVAILABLE is displayed + * to the user as max_performance, but when the user + * writes "max_performance", they actually want the + * value to match MAX_PERFORMANCE. + */ + for (i = 1; i < ARRAY_SIZE(link_pm_policy); i++) { + const int len = strlen(link_pm_policy[i].name); + if (strncmp(link_pm_policy[i].name, buf, len) == 0 && + buf[len] == '\n') { + policy = link_pm_policy[i].value; + break; + } + } + if (!policy) + return -EINVAL; + + ata_lpm_schedule(ap, policy); + return count; +} + +static ssize_t +ata_scsi_lpm_show(struct class_device *class_dev, char *buf) +{ + struct Scsi_Host *shost = class_to_shost(class_dev); + struct ata_port *ap = ata_shost_to_port(shost); + const char *policy = + ata_scsi_lpm_get(ap->pm_policy); + + if (!policy) + return -EINVAL; + + return snprintf(buf, 23, "%s\n", policy); +} +CLASS_DEVICE_ATTR(link_power_management_policy, S_IRUGO | S_IWUSR, + ata_scsi_lpm_show, ata_scsi_lpm_put); +EXPORT_SYMBOL_GPL(class_device_attr_link_power_management_policy); + static void ata_scsi_invalid_field(struct scsi_cmnd *cmd, void (*done)(struct scsi_cmnd *)) { diff --git a/drivers/ata/libata.h b/drivers/ata/libata.h index 90df58a..0e6cf3a 100644 --- a/drivers/ata/libata.h +++ b/drivers/ata/libata.h @@ -101,6 +101,8 @@ extern int sata_link_init_spd(struct ata_link *link); extern int ata_task_ioctl(struct scsi_device *scsidev, void __user *arg); extern int ata_cmd_ioctl(struct scsi_device *scsidev, void __user *arg); extern struct ata_port *ata_port_alloc(struct ata_host *host); +extern void ata_dev_enable_pm(struct ata_device *dev, enum link_pm policy); +extern void ata_lpm_schedule(struct ata_port *ap, enum link_pm); /* libata-acpi.c */ #ifdef CONFIG_ATA_ACPI |