/*- * Copyright (c) 2016 Landon Fuller * Copyright (c) 2010 Broadcom Corporation. * Copyright (c) 2017 The FreeBSD Foundation * All rights reserved. * * Portions of this software were developed by Landon Fuller * under sponsorship from the FreeBSD Foundation. * * Portions of this file were derived from the siutils.c source distributed with * the Asus RT-N16 firmware source code release. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * $Id: siutils.c,v 1.821.2.48 2011-02-11 20:59:28 Exp $ */ #include __FBSDID("$FreeBSD$"); #include #include #include #include #include #include #include #include #include #include #include #include #include "bhnd_chipc_if.h" #include "bhnd_pwrctl_if.h" #include "bhnd_pwrctl_hostb_if.h" #include "bhnd_pwrctl_private.h" /* * ChipCommon Power Control. * * Provides a runtime interface to device clocking and power management on * legacy non-PMU chipsets. */ typedef enum { BHND_PWRCTL_WAR_UP, /**< apply attach/resume workarounds */ BHND_PWRCTL_WAR_RUN, /**< apply running workarounds */ BHND_PWRCTL_WAR_DOWN, /**< apply detach/suspend workarounds */ } bhnd_pwrctl_wars; static int bhnd_pwrctl_updateclk(struct bhnd_pwrctl_softc *sc, bhnd_pwrctl_wars wars); static struct bhnd_device_quirk pwrctl_quirks[]; /* Supported parent core device identifiers */ static const struct bhnd_device pwrctl_devices[] = { BHND_DEVICE(BCM, CC, "ChipCommon Power Control", pwrctl_quirks), BHND_DEVICE_END }; /* Device quirks table */ static struct bhnd_device_quirk pwrctl_quirks[] = { BHND_CORE_QUIRK (HWREV_LTE(5), PWRCTL_QUIRK_PCICLK_CTL), BHND_CORE_QUIRK (HWREV_RANGE(6, 9), PWRCTL_QUIRK_SLOWCLK_CTL), BHND_CORE_QUIRK (HWREV_RANGE(10, 19), PWRCTL_QUIRK_INSTACLK_CTL), BHND_DEVICE_QUIRK_END }; static int bhnd_pwrctl_probe(device_t dev) { const struct bhnd_device *id; struct chipc_caps *ccaps; device_t chipc; /* Look for compatible chipc parent */ chipc = device_get_parent(dev); if (device_get_devclass(chipc) != devclass_find("bhnd_chipc")) return (ENXIO); if (device_get_driver(chipc) != &bhnd_chipc_driver) return (ENXIO); /* Verify chipc capability flags */ ccaps = BHND_CHIPC_GET_CAPS(chipc); if (ccaps->pmu || !ccaps->pwr_ctrl) return (ENXIO); /* Check for chipc device match */ id = bhnd_device_lookup(chipc, pwrctl_devices, sizeof(pwrctl_devices[0])); if (id == NULL) return (ENXIO); device_set_desc(dev, id->desc); return (BUS_PROBE_NOWILDCARD); } static int bhnd_pwrctl_attach(device_t dev) { struct bhnd_pwrctl_softc *sc; const struct bhnd_chipid *cid; struct chipc_softc *chipc_sc; bhnd_devclass_t hostb_class; device_t hostb_dev; device_t bus; int error; sc = device_get_softc(dev); sc->dev = dev; sc->chipc_dev = device_get_parent(dev); sc->quirks = bhnd_device_quirks(sc->chipc_dev, pwrctl_devices, sizeof(pwrctl_devices[0])); bus = device_get_parent(sc->chipc_dev); /* On devices that lack a slow clock source, HT must always be * enabled. */ hostb_class = BHND_DEVCLASS_INVALID; hostb_dev = bhnd_bus_find_hostb_device(device_get_parent(sc->chipc_dev)); if (hostb_dev != NULL) hostb_class = bhnd_get_class(hostb_dev); cid = bhnd_get_chipid(sc->chipc_dev); switch (cid->chip_id) { case BHND_CHIPID_BCM4311: if (cid->chip_rev <= 1 && hostb_class == BHND_DEVCLASS_PCI) sc->quirks |= PWRCTL_QUIRK_FORCE_HT; break; case BHND_CHIPID_BCM4321: if (hostb_class == BHND_DEVCLASS_PCIE || hostb_class == BHND_DEVCLASS_PCI) sc->quirks |= PWRCTL_QUIRK_FORCE_HT; break; case BHND_CHIPID_BCM4716: if (hostb_class == BHND_DEVCLASS_PCIE) sc->quirks |= PWRCTL_QUIRK_FORCE_HT; break; } /* Fetch core register block from ChipCommon parent */ chipc_sc = device_get_softc(sc->chipc_dev); sc->res = chipc_sc->core; PWRCTL_LOCK_INIT(sc); STAILQ_INIT(&sc->clkres_list); /* Initialize power control */ PWRCTL_LOCK(sc); if ((error = bhnd_pwrctl_init(sc))) { PWRCTL_UNLOCK(sc); goto cleanup; } /* Apply default clock transitions */ if ((error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_UP))) { PWRCTL_UNLOCK(sc); goto cleanup; } PWRCTL_UNLOCK(sc); /* Register as the bus PWRCTL provider */ if ((error = bhnd_register_provider(dev, BHND_SERVICE_PWRCTL))) { device_printf(sc->dev, "failed to register PWRCTL with bus : " "%d\n", error); goto cleanup; } return (0); cleanup: PWRCTL_LOCK_DESTROY(sc); return (error); } static int bhnd_pwrctl_detach(device_t dev) { struct bhnd_pwrctl_softc *sc; struct bhnd_pwrctl_clkres *clkres, *crnext; int error; sc = device_get_softc(dev); if ((error = bhnd_deregister_provider(dev, BHND_SERVICE_ANY))) return (error); /* Update clock state */ PWRCTL_LOCK(sc); error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_DOWN); PWRCTL_UNLOCK(sc); if (error) return (error); STAILQ_FOREACH_SAFE(clkres, &sc->clkres_list, cr_link, crnext) free(clkres, M_DEVBUF); PWRCTL_LOCK_DESTROY(sc); return (0); } static int bhnd_pwrctl_suspend(device_t dev) { struct bhnd_pwrctl_softc *sc; int error; sc = device_get_softc(dev); /* Update clock state */ PWRCTL_LOCK(sc); error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_DOWN); PWRCTL_UNLOCK(sc); return (error); } static int bhnd_pwrctl_resume(device_t dev) { struct bhnd_pwrctl_softc *sc; int error; sc = device_get_softc(dev); PWRCTL_LOCK(sc); /* Re-initialize power control registers */ if ((error = bhnd_pwrctl_init(sc))) { device_printf(sc->dev, "PWRCTL init failed: %d\n", error); goto cleanup; } /* Restore clock state */ if ((error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_UP))) { device_printf(sc->dev, "clock state restore failed: %d\n", error); goto cleanup; } cleanup: PWRCTL_UNLOCK(sc); return (error); } static int bhnd_pwrctl_get_clock_latency(device_t dev, bhnd_clock clock, u_int *latency) { struct bhnd_pwrctl_softc *sc = device_get_softc(dev); switch (clock) { case BHND_CLOCK_HT: PWRCTL_LOCK(sc); *latency = bhnd_pwrctl_fast_pwrup_delay(sc); PWRCTL_UNLOCK(sc); return (0); default: return (ENODEV); } } static int bhnd_pwrctl_get_clock_freq(device_t dev, bhnd_clock clock, u_int *freq) { struct bhnd_pwrctl_softc *sc = device_get_softc(dev); switch (clock) { case BHND_CLOCK_ALP: BPMU_LOCK(sc); *freq = bhnd_pwrctl_getclk_speed(sc); BPMU_UNLOCK(sc); return (0); case BHND_CLOCK_HT: case BHND_CLOCK_ILP: case BHND_CLOCK_DYN: default: return (ENODEV); } } /** * Find the clock reservation associated with @p owner, if any. * * @param sc Driver instance state. * @param owner The owning device. */ static struct bhnd_pwrctl_clkres * bhnd_pwrctl_find_res(struct bhnd_pwrctl_softc *sc, device_t owner) { struct bhnd_pwrctl_clkres *clkres; PWRCTL_LOCK_ASSERT(sc, MA_OWNED); STAILQ_FOREACH(clkres, &sc->clkres_list, cr_link) { if (clkres->owner == owner) return (clkres); } /* not found */ return (NULL); } /** * Enumerate all active clock requests, compute the minimum required clock, * and issue any required clock transition. * * @param sc Driver instance state. * @param wars Work-around state. */ static int bhnd_pwrctl_updateclk(struct bhnd_pwrctl_softc *sc, bhnd_pwrctl_wars wars) { struct bhnd_pwrctl_clkres *clkres; bhnd_clock clock; PWRCTL_LOCK_ASSERT(sc, MA_OWNED); /* Nothing to update on fixed clock devices */ if (PWRCTL_QUIRK(sc, FIXED_CLK)) return (0); /* Default clock target */ clock = BHND_CLOCK_DYN; /* Apply quirk-specific overrides to the clock target */ switch (wars) { case BHND_PWRCTL_WAR_UP: /* Force HT clock */ if (PWRCTL_QUIRK(sc, FORCE_HT)) clock = BHND_CLOCK_HT; break; case BHND_PWRCTL_WAR_RUN: /* Cannot transition clock if FORCE_HT */ if (PWRCTL_QUIRK(sc, FORCE_HT)) return (0); break; case BHND_PWRCTL_WAR_DOWN: /* Leave default clock unmodified to permit * transition back to BHND_CLOCK_DYN on FORCE_HT devices. */ break; } /* Determine required clock */ STAILQ_FOREACH(clkres, &sc->clkres_list, cr_link) clock = bhnd_clock_max(clock, clkres->clock); /* Map to supported clock setting */ switch (clock) { case BHND_CLOCK_DYN: case BHND_CLOCK_ILP: clock = BHND_CLOCK_DYN; break; case BHND_CLOCK_ALP: /* In theory FORCE_ALP is supported by the hardware, but * there are currently no known use-cases for it; mapping * to HT is still valid, and allows us to punt on determing * where FORCE_ALP is supported and functional */ clock = BHND_CLOCK_HT; break; case BHND_CLOCK_HT: break; default: device_printf(sc->dev, "unknown clock: %#x\n", clock); return (ENODEV); } /* Issue transition */ return (bhnd_pwrctl_setclk(sc, clock)); } /* BHND_PWRCTL_REQUEST_CLOCK() */ static int bhnd_pwrctl_request_clock(device_t dev, device_t child, bhnd_clock clock) { struct bhnd_pwrctl_softc *sc; struct bhnd_pwrctl_clkres *clkres; int error; sc = device_get_softc(dev); error = 0; PWRCTL_LOCK(sc); clkres = bhnd_pwrctl_find_res(sc, child); /* BHND_CLOCK_DYN discards the clock reservation entirely */ if (clock == BHND_CLOCK_DYN) { /* nothing to clean up? */ if (clkres == NULL) { PWRCTL_UNLOCK(sc); return (0); } /* drop reservation and apply clock transition */ STAILQ_REMOVE(&sc->clkres_list, clkres, bhnd_pwrctl_clkres, cr_link); if ((error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_RUN))) { device_printf(dev, "clock transition failed: %d\n", error); /* restore reservation */ STAILQ_INSERT_TAIL(&sc->clkres_list, clkres, cr_link); PWRCTL_UNLOCK(sc); return (error); } /* deallocate orphaned reservation */ free(clkres, M_DEVBUF); PWRCTL_UNLOCK(sc); return (0); } /* create (or update) reservation */ if (clkres == NULL) { clkres = malloc(sizeof(struct bhnd_pwrctl_clkres), M_DEVBUF, M_NOWAIT); if (clkres == NULL) return (ENOMEM); clkres->owner = child; clkres->clock = clock; STAILQ_INSERT_TAIL(&sc->clkres_list, clkres, cr_link); } else { KASSERT(clkres->owner == child, ("invalid owner")); clkres->clock = clock; } /* apply clock transition */ error = bhnd_pwrctl_updateclk(sc, BHND_PWRCTL_WAR_RUN); if (error) { STAILQ_REMOVE(&sc->clkres_list, clkres, bhnd_pwrctl_clkres, cr_link); free(clkres, M_DEVBUF); } PWRCTL_UNLOCK(sc); return (error); } static device_method_t bhnd_pwrctl_methods[] = { /* Device interface */ DEVMETHOD(device_probe, bhnd_pwrctl_probe), DEVMETHOD(device_attach, bhnd_pwrctl_attach), DEVMETHOD(device_detach, bhnd_pwrctl_detach), DEVMETHOD(device_suspend, bhnd_pwrctl_suspend), DEVMETHOD(device_resume, bhnd_pwrctl_resume), /* BHND PWRCTL interface */ DEVMETHOD(bhnd_pwrctl_request_clock, bhnd_pwrctl_request_clock), DEVMETHOD(bhnd_pwrctl_get_clock_freq, bhnd_pwrctl_get_clock_freq), DEVMETHOD(bhnd_pwrctl_get_clock_latency, bhnd_pwrctl_get_clock_latency), DEVMETHOD_END }; DEFINE_CLASS_0(bhnd_pwrctl, bhnd_pwrctl_driver, bhnd_pwrctl_methods, sizeof(struct bhnd_pwrctl_softc)); EARLY_DRIVER_MODULE(bhnd_pwrctl, bhnd_chipc, bhnd_pwrctl_driver, bhnd_pmu_devclass, NULL, NULL, BUS_PASS_TIMER + BUS_PASS_ORDER_MIDDLE); MODULE_DEPEND(bhnd_pwrctl, bhnd, 1, 1, 1); MODULE_VERSION(bhnd_pwrctl, 1);