While looking at implementing disk hotplug for ahci, I saw that the ahci spec suggests putting disconnected ports in listen mode, which allows the controller to put the phy in a low power state while still allowing it to generate interrupts when a device is attached. As a first step towards hotplug, the diff below implements this, and also stops freeing disconnected ports, since they may become active later on.
In listen mode, ahci_ata_probe will always return ATA_T_PORT_NONE, so no commands will be issued to the port. Resetting the port exits listen mode. Some rough testing here shows that placing an otherwise active port in listen mode reduces power draw by around 0.1W (assuming we can believe acpibat sensors), so if your laptop has an extra sata phy for some reason, it might get you a couple more minutes of battery life, for the cost of a few kb of memory. I've tested this on intel, amd and asmedia ahcis and haven't seen any ill effects. Index: ahci.c =================================================================== RCS file: /cvs/src/sys/dev/ic/ahci.c,v retrieving revision 1.29 diff -u -p -r1.29 ahci.c --- ahci.c 5 Mar 2017 09:55:16 -0000 1.29 +++ ahci.c 27 May 2017 06:42:36 -0000 @@ -81,6 +81,7 @@ int ahci_port_clo(struct ahci_port *); int ahci_port_softreset(struct ahci_port *); int ahci_port_portreset(struct ahci_port *, int); int ahci_port_signature(struct ahci_port *); +int ahci_port_listenmode(struct ahci_port *); int ahci_pmp_port_softreset(struct ahci_port *, int); int ahci_pmp_port_portreset(struct ahci_port *, int); int ahci_pmp_port_probe(struct ahci_port *ap, int pmp_port); @@ -186,6 +187,7 @@ ahci_attach(struct ahci_softc *sc) printf("\n"); sc->sc_cap = ahci_read(sc, AHCI_REG_CAP); + sc->sc_cap2 = ahci_read(sc, AHCI_REG_CAP2); sc->sc_ncmds = AHCI_REG_CAP_NCS(sc->sc_cap); #ifdef AHCI_DEBUG if (ahcidebug & AHCI_D_VERBOSE) { @@ -210,7 +212,7 @@ ahci_attach(struct ahci_softc *sc) DEVNAME(sc), sc->sc_cap, AHCI_FMT_CAP, AHCI_REG_CAP_NP(sc->sc_cap), sc->sc_ncmds, gen); printf("%s: extended capabilities 0x%b\n", DEVNAME(sc), - ahci_read(sc, AHCI_REG_CAP2), AHCI_FMT_CAP2); + sc->sc_cap2, AHCI_FMT_CAP2); } #endif @@ -628,7 +631,11 @@ nomem: "on port %d\n", DEVNAME(sc), port); break; } - goto freeport; + + rc = ahci_port_listenmode(ap); + if (rc != 0) + goto freeport; + goto flushport; case EBUSY: printf("%s: device on port %d didn't come ready, " @@ -676,6 +689,7 @@ nomem: rc = ENXIO; /* couldn't start port */ } +flushport: /* Flush interrupts for port */ ahci_pwrite(ap, AHCI_PREG_IS, ahci_pread(ap, AHCI_PREG_IS)); ahci_write(sc, AHCI_REG_IS, 1 << port); @@ -807,7 +821,10 @@ ahci_port_init(struct ahci_softc *sc, u_ "on port %d\n", DEVNAME(sc), port); break; } - goto reterr; + rc = ahci_port_listenmode(ap); + if (rc == 0) + goto reterr; + goto flushport; case EBUSY: printf("%s: device on port %d didn't come ready, " @@ -857,6 +874,7 @@ ahci_port_init(struct ahci_softc *sc, u_ rc = ENXIO; /* couldn't start port */ } +flushport: /* Flush interrupts for port */ ahci_pwrite(ap, AHCI_PREG_IS, ahci_pread(ap, AHCI_PREG_IS)); ahci_write(sc, AHCI_REG_IS, 1 << port); @@ -933,6 +951,39 @@ ahci_port_stop(struct ahci_port *ap, int return (0); } +/* place port in listen mode (10.10.1) */ +int +ahci_port_listenmode(struct ahci_port *ap) +{ + int rc; + uint32_t cmd, ipm; + + rc = ahci_port_stop(ap, 1); + if (rc != 0) + return (rc); + + cmd = ahci_pread(ap, AHCI_PREG_CMD); + cmd &= ~(AHCI_PREG_CMD_CLO | AHCI_PREG_CMD_PMA | AHCI_PREG_CMD_ICC); + ahci_pwrite(ap, AHCI_PREG_CMD, cmd); + + /* disable IPM and DET */ + ipm = AHCI_PREG_SCTL_IPM_DISABLED; + if (ap->ap_sc->sc_cap2 & AHCI_REG_CAP2_SDS) + ipm |= AHCI_PREG_SCTL_IPM_NODEVSLP; + ahci_pwrite(ap, AHCI_PREG_SCTL, ipm); + + delay(10000); + + /* disable SUD */ + cmd = ahci_pread(ap, AHCI_PREG_CMD); + cmd &= ~AHCI_PREG_CMD_SUD; + ahci_pwrite(ap, AHCI_PREG_CMD, cmd); + + ap->ap_state = AP_S_LISTEN; + + return (0); +} + /* AHCI command list override -> forcibly clear TFD.STS.{BSY,DRQ} */ int ahci_port_clo(struct ahci_port *ap) @@ -1407,6 +1458,18 @@ ahci_port_portreset(struct ahci_port *ap s = splbio(); DPRINTF(AHCI_D_VERBOSE, "%s: port reset\n", PORTNAME(ap)); + /* exit listen mode if required */ + if (ap->ap_state == AP_S_LISTEN) { + cmd = ahci_pread(ap, AHCI_PREG_CMD); + cmd |= AHCI_PREG_CMD_SUD | AHCI_PREG_CMD_POD | + AHCI_PREG_CMD_ICC_ACTIVE; + ahci_pwrite(ap, AHCI_PREG_CMD, cmd); + + ahci_pwait_clr(ap, AHCI_PREG_CMD, AHCI_PREG_CMD_ICC, 1); + ahci_flush_tfd(ap); + ap->ap_state = AP_S_NORMAL; + } + /* Save previous command register state */ cmd = ahci_pread(ap, AHCI_PREG_CMD) & ~AHCI_PREG_CMD_ICC; @@ -2726,7 +2872,7 @@ ahci_ata_probe(void *xsc, int port, int struct ahci_softc *sc = xsc; struct ahci_port *ap = sc->sc_ports[port]; - if (ap == NULL) + if (ap == NULL || ap->ap_state == AP_S_LISTEN) return (ATA_PORT_T_NONE); if (lun != 0) { Index: ahcireg.h =================================================================== RCS file: /cvs/src/sys/dev/ic/ahcireg.h,v retrieving revision 1.5 diff -u -p -r1.5 ahcireg.h --- ahcireg.h 11 Feb 2015 07:13:44 -0000 1.5 +++ ahcireg.h 27 May 2017 06:42:36 -0000 @@ -201,6 +201,7 @@ #define AHCI_PREG_SCTL_IPM_NOPARTIAL 0x100 #define AHCI_PREG_SCTL_IPM_NOSLUMBER 0x200 #define AHCI_PREG_SCTL_IPM_DISABLED 0x300 +#define AHCI_PREG_SCTL_IPM_NODEVSLP 0x400 #define AHCI_PREG_SERR 0x30 /* SATA Error */ #define AHCI_PREG_SERR_ERR(_r) ((_r) & 0xffff) #define AHCI_PREG_SERR_ERR_I (1<<0) /* Recovered Data Integrity */ Index: ahcivar.h =================================================================== RCS file: /cvs/src/sys/dev/ic/ahcivar.h,v retrieving revision 1.9 diff -u -p -r1.9 ahcivar.h --- ahcivar.h 3 Dec 2014 04:33:06 -0000 1.9 +++ ahcivar.h 27 May 2017 06:42:36 -0000 @@ -85,6 +86,7 @@ struct ahci_port { #define AP_S_PMP_PORT_PROBE 2 #define AP_S_ERROR_RECOVERY 3 #define AP_S_FATAL_ERROR 4 +#define AP_S_LISTEN 5 int ap_pmp_ports; int ap_port; @@ -131,6 +133,7 @@ struct ahci_softc { struct atascsi *sc_atascsi; u_int32_t sc_cap; + u_int32_t sc_cap2; #ifdef AHCI_COALESCE u_int32_t sc_ccc_mask;