/*- * Copyright (c) 2012 * Ben Gray . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include __FBSDID("$FreeBSD$"); /* * Texas Instruments TWL4030/TWL5030/TWL60x0/TPS659x0 Power Management. * * This driver covers the external clocks, allows for enabling & * disabling their output. * * * * FLATTENED DEVICE TREE (FDT) * Startup override settings can be specified in the FDT, if they are they * should be under the twl parent device and take the following form: * * external-clocks = "name1", "state1", * "name2", "state2", * etc; * * Each override should be a pair, the first entry is the name of the clock * the second is the state to set, possible strings are either "on" or "off". * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "twl.h" #include "twl_clks.h" static int twl_clks_debug = 1; /* * Power Groups bits for the 4030 and 6030 devices */ #define TWL4030_P3_GRP 0x80 /* Peripherals, power group */ #define TWL4030_P2_GRP 0x40 /* Modem power group */ #define TWL4030_P1_GRP 0x20 /* Application power group (FreeBSD control) */ #define TWL6030_P3_GRP 0x04 /* Modem power group */ #define TWL6030_P2_GRP 0x02 /* Connectivity power group */ #define TWL6030_P1_GRP 0x01 /* Application power group (FreeBSD control) */ /* * Register offsets within a clk regulator register set */ #define TWL_CLKS_GRP 0x00 /* Regulator GRP register */ #define TWL_CLKS_STATE 0x02 /* TWL6030 only */ /** * Support voltage regulators for the different IC's */ struct twl_clock { const char *name; uint8_t subdev; uint8_t regbase; }; static const struct twl_clock twl4030_clocks[] = { { "32kclkout", 0, 0x8e }, { NULL, 0, 0x00 } }; static const struct twl_clock twl6030_clocks[] = { { "clk32kg", 0, 0xbc }, { "clk32kao", 0, 0xb9 }, { "clk32kaudio", 0, 0xbf }, { NULL, 0, 0x00 } }; #define TWL_CLKS_MAX_NAMELEN 32 struct twl_clk_entry { LIST_ENTRY(twl_clk_entry) link; struct sysctl_oid *oid; char name[TWL_CLKS_MAX_NAMELEN]; uint8_t sub_dev; /* the sub-device number for the clock */ uint8_t reg_off; /* register base address of the clock */ }; struct twl_clks_softc { device_t sc_dev; /* twl_clk device */ device_t sc_pdev; /* parent device (twl) */ struct sx sc_sx; /* internal locking */ struct intr_config_hook sc_init_hook; LIST_HEAD(twl_clk_list, twl_clk_entry) sc_clks_list; }; /** * Macros for driver shared locking */ #define TWL_CLKS_XLOCK(_sc) sx_xlock(&(_sc)->sc_sx) #define TWL_CLKS_XUNLOCK(_sc) sx_xunlock(&(_sc)->sc_sx) #define TWL_CLKS_SLOCK(_sc) sx_slock(&(_sc)->sc_sx) #define TWL_CLKS_SUNLOCK(_sc) sx_sunlock(&(_sc)->sc_sx) #define TWL_CLKS_LOCK_INIT(_sc) sx_init(&(_sc)->sc_sx, "twl_clks") #define TWL_CLKS_LOCK_DESTROY(_sc) sx_destroy(&(_sc)->sc_sx); #define TWL_CLKS_ASSERT_LOCKED(_sc) sx_assert(&(_sc)->sc_sx, SA_LOCKED); #define TWL_CLKS_LOCK_UPGRADE(_sc) \ do { \ while (!sx_try_upgrade(&(_sc)->sc_sx)) \ pause("twl_clks_ex", (hz / 100)); \ } while(0) #define TWL_CLKS_LOCK_DOWNGRADE(_sc) sx_downgrade(&(_sc)->sc_sx); /** * twl_clks_read_1 - read single register from the TWL device * twl_clks_write_1 - writes a single register in the TWL device * @sc: device context * @clk: the clock device we're reading from / writing to * @off: offset within the clock's register set * @val: the value to write or a pointer to a variable to store the result * * RETURNS: * Zero on success or an error code on failure. */ static inline int twl_clks_read_1(struct twl_clks_softc *sc, struct twl_clk_entry *clk, uint8_t off, uint8_t *val) { return (twl_read(sc->sc_pdev, clk->sub_dev, clk->reg_off + off, val, 1)); } static inline int twl_clks_write_1(struct twl_clks_softc *sc, struct twl_clk_entry *clk, uint8_t off, uint8_t val) { return (twl_write(sc->sc_pdev, clk->sub_dev, clk->reg_off + off, &val, 1)); } /** * twl_clks_is_enabled - determines if a clock is enabled * @dev: TWL CLK device * @name: the name of the clock * @enabled: upon return will contain the 'enabled' state * * LOCKING: * Internally the function takes and releases the TWL lock. * * RETURNS: * Zero on success or a negative error code on failure. */ int twl_clks_is_enabled(device_t dev, const char *name, int *enabled) { struct twl_clks_softc *sc = device_get_softc(dev); struct twl_clk_entry *clk; int found = 0; int err; uint8_t grp, state; TWL_CLKS_SLOCK(sc); LIST_FOREACH(clk, &sc->sc_clks_list, link) { if (strcmp(clk->name, name) == 0) { found = 1; break; } } if (!found) { TWL_CLKS_SUNLOCK(sc); return (EINVAL); } if (twl_is_4030(sc->sc_pdev)) { err = twl_clks_read_1(sc, clk, TWL_CLKS_GRP, &grp); if (!err) *enabled = (grp & TWL4030_P1_GRP); } else if (twl_is_6030(sc->sc_pdev) || twl_is_6025(sc->sc_pdev)) { TWL_CLKS_LOCK_UPGRADE(sc); /* Check the clock is in the application group */ if (twl_is_6030(sc->sc_pdev)) { err = twl_clks_read_1(sc, clk, TWL_CLKS_GRP, &grp); if (err) { TWL_CLKS_LOCK_DOWNGRADE(sc); goto done; } if (!(grp & TWL6030_P1_GRP)) { TWL_CLKS_LOCK_DOWNGRADE(sc); *enabled = 0; /* disabled */ goto done; } } /* Read the application mode state and verify it's ON */ err = twl_clks_read_1(sc, clk, TWL_CLKS_STATE, &state); if (!err) *enabled = ((state & 0x0C) == 0x04); TWL_CLKS_LOCK_DOWNGRADE(sc); } else { err = EINVAL; } done: TWL_CLKS_SUNLOCK(sc); return (err); } /** * twl_clks_set_state - enables/disables a clock output * @sc: device context * @clk: the clock entry to enable/disable * @enable: non-zero the clock is enabled, zero the clock is disabled * * LOCKING: * The TWL CLK lock must be held before this function is called. * * RETURNS: * Zero on success or an error code on failure. */ static int twl_clks_set_state(struct twl_clks_softc *sc, struct twl_clk_entry *clk, int enable) { int xlocked; int err; uint8_t grp; TWL_CLKS_ASSERT_LOCKED(sc); /* Upgrade the lock to exclusive because about to perform read-mod-write */ xlocked = sx_xlocked(&sc->sc_sx); if (!xlocked) TWL_CLKS_LOCK_UPGRADE(sc); err = twl_clks_read_1(sc, clk, TWL_CLKS_GRP, &grp); if (err) goto done; if (twl_is_4030(sc->sc_pdev)) { /* On the TWL4030 we just need to ensure the clock is in the right * power domain, don't need to turn on explicitly like TWL6030. */ if (enable) grp |= TWL4030_P1_GRP; else grp &= ~(TWL4030_P1_GRP | TWL4030_P2_GRP | TWL4030_P3_GRP); err = twl_clks_write_1(sc, clk, TWL_CLKS_GRP, grp); } else if (twl_is_6030(sc->sc_pdev) || twl_is_6025(sc->sc_pdev)) { /* Make sure the clock belongs to at least the APP power group */ if (twl_is_6030(sc->sc_pdev) && !(grp & TWL6030_P1_GRP)) { grp |= TWL6030_P1_GRP; err = twl_clks_write_1(sc, clk, TWL_CLKS_GRP, grp); if (err) goto done; } /* On TWL6030 we need to make sure we disable power for all groups */ if (twl_is_6030(sc->sc_pdev)) grp = TWL6030_P1_GRP | TWL6030_P2_GRP | TWL6030_P3_GRP; else grp = 0x00; /* Set the state of the clock */ if (enable) err = twl_clks_write_1(sc, clk, TWL_CLKS_STATE, (grp << 5) | 0x01); else err = twl_clks_write_1(sc, clk, TWL_CLKS_STATE, (grp << 5)); } else { err = EINVAL; } done: if (!xlocked) TWL_CLKS_LOCK_DOWNGRADE(sc); if ((twl_clks_debug > 1) && !err) device_printf(sc->sc_dev, "%s : %sabled\n", clk->name, enable ? "en" : "dis"); return (err); } /** * twl_clks_disable - disables a clock output * @dev: TWL clk device * @name: the name of the clock * * LOCKING: * Internally the function takes and releases the TWL lock. * * RETURNS: * Zero on success or an error code on failure. */ int twl_clks_disable(device_t dev, const char *name) { struct twl_clks_softc *sc = device_get_softc(dev); struct twl_clk_entry *clk; int err = EINVAL; TWL_CLKS_SLOCK(sc); LIST_FOREACH(clk, &sc->sc_clks_list, link) { if (strcmp(clk->name, name) == 0) { err = twl_clks_set_state(sc, clk, 0); break; } } TWL_CLKS_SUNLOCK(sc); return (err); } /** * twl_clks_enable - enables a clock output * @dev: TWL clk device * @name: the name of the clock * * LOCKING: * Internally the function takes and releases the TWL CLKS lock. * * RETURNS: * Zero on success or an error code on failure. */ int twl_clks_enable(device_t dev, const char *name) { struct twl_clks_softc *sc = device_get_softc(dev); struct twl_clk_entry *clk; int err = EINVAL; TWL_CLKS_SLOCK(sc); LIST_FOREACH(clk, &sc->sc_clks_list, link) { if (strcmp(clk->name, name) == 0) { err = twl_clks_set_state(sc, clk, 1); break; } } TWL_CLKS_SUNLOCK(sc); return (err); } /** * twl_clks_sysctl_clock - reads the state of the clock * @SYSCTL_HANDLER_ARGS: arguments for the callback * * Returns the clock status; disabled is zero and enabled is non-zero. * * LOCKING: * It's expected the TWL lock is held while this function is called. * * RETURNS: * EIO if device is not present, otherwise 0 is returned. */ static int twl_clks_sysctl_clock(SYSCTL_HANDLER_ARGS) { struct twl_clks_softc *sc = (struct twl_clks_softc*)arg1; int err; int enabled = 0; if ((err = twl_clks_is_enabled(sc->sc_dev, oidp->oid_name, &enabled)) != 0) return err; return sysctl_handle_int(oidp, &enabled, 0, req); } /** * twl_clks_add_clock - adds single clock sysctls for the device * @sc: device soft context * @name: the name of the regulator * @nsub: the number of the subdevice * @regbase: the base address of the clocks registers * * Adds a single clock to the device and also a sysctl interface for * querying it's status. * * LOCKING: * It's expected the exclusive lock is held while this function is called. * * RETURNS: * Pointer to the new clock entry on success, otherwise NULL on failure. */ static struct twl_clk_entry* twl_clks_add_clock(struct twl_clks_softc *sc, const char *name, uint8_t nsub, uint8_t regbase) { struct sysctl_ctx_list *ctx = device_get_sysctl_ctx(sc->sc_dev); struct sysctl_oid *tree = device_get_sysctl_tree(sc->sc_dev); struct twl_clk_entry *new; TWL_CLKS_ASSERT_LOCKED(sc); new = malloc(sizeof(struct twl_clk_entry), M_DEVBUF, M_NOWAIT | M_ZERO); if (new == NULL) return (NULL); strncpy(new->name, name, TWL_CLKS_MAX_NAMELEN); new->name[TWL_CLKS_MAX_NAMELEN - 1] = '\0'; new->sub_dev = nsub; new->reg_off = regbase; /* Add a sysctl entry for the clock */ new->oid = SYSCTL_ADD_PROC(ctx, SYSCTL_CHILDREN(tree), OID_AUTO, name, CTLTYPE_INT | CTLFLAG_RD, sc, 0, twl_clks_sysctl_clock, "I", "external clock"); /* Finally add the regulator to list of supported regulators */ LIST_INSERT_HEAD(&sc->sc_clks_list, new, link); return (new); } /** * twl_clks_add_clocks - populates the internal list of clocks * @sc: device soft context * @chip: the name of the chip used in the hints * @clks the list of clocks supported by the device * * Loops over the list of clocks and adds them to the device context. Also * scans the FDT to determine if there are any clocks that should be * enabled/disabled automatically. * * LOCKING: * Internally takes the exclusive lock while adding the clocks to the * device context. * * RETURNS: * Always returns 0. */ static int twl_clks_add_clocks(struct twl_clks_softc *sc, const struct twl_clock *clks) { int err; const struct twl_clock *walker; struct twl_clk_entry *entry; phandle_t child; char rnames[256]; char *name, *state; int len = 0, prop_len; int enable; TWL_CLKS_XLOCK(sc); /* Add the regulators from the list */ walker = &clks[0]; while (walker->name != NULL) { /* Add the regulator to the list */ entry = twl_clks_add_clock(sc, walker->name, walker->subdev, walker->regbase); if (entry == NULL) continue; walker++; } /* Check for any FDT settings that need to be applied */ child = ofw_bus_get_node(sc->sc_pdev); if (child) { prop_len = OF_getprop(child, "external-clocks", rnames, sizeof(rnames)); while (len < prop_len) { name = rnames + len; len += strlen(name) + 1; if ((len >= prop_len) || (name[0] == '\0')) break; state = rnames + len; len += strlen(state) + 1; if (state[0] == '\0') break; enable = !strncmp(state, "on", 2); LIST_FOREACH(entry, &sc->sc_clks_list, link) { if (strcmp(entry->name, name) == 0) { twl_clks_set_state(sc, entry, enable); break; } } } } TWL_CLKS_XUNLOCK(sc); if (twl_clks_debug) { LIST_FOREACH(entry, &sc->sc_clks_list, link) { err = twl_clks_is_enabled(sc->sc_dev, entry->name, &enable); if (!err) device_printf(sc->sc_dev, "%s : %s\n", entry->name, enable ? "on" : "off"); } } return (0); } /** * twl_clks_init - initialises the list of clocks * @dev: the twl_clks device * * This function is called as an intrhook once interrupts have been enabled, * this is done so that the driver has the option to enable/disable a clock * based on settings providied in the FDT. * * LOCKING: * May takes the exclusive lock in the function. */ static void twl_clks_init(void *dev) { struct twl_clks_softc *sc; sc = device_get_softc((device_t)dev); if (twl_is_4030(sc->sc_pdev)) twl_clks_add_clocks(sc, twl4030_clocks); else if (twl_is_6030(sc->sc_pdev) || twl_is_6025(sc->sc_pdev)) twl_clks_add_clocks(sc, twl6030_clocks); config_intrhook_disestablish(&sc->sc_init_hook); } static int twl_clks_probe(device_t dev) { if (twl_is_4030(device_get_parent(dev))) device_set_desc(dev, "TI TWL4030 PMIC External Clocks"); else if (twl_is_6025(device_get_parent(dev)) || twl_is_6030(device_get_parent(dev))) device_set_desc(dev, "TI TWL6025/TWL6030 PMIC External Clocks"); else return (ENXIO); return (0); } static int twl_clks_attach(device_t dev) { struct twl_clks_softc *sc; sc = device_get_softc(dev); sc->sc_dev = dev; sc->sc_pdev = device_get_parent(dev); TWL_CLKS_LOCK_INIT(sc); LIST_INIT(&sc->sc_clks_list); sc->sc_init_hook.ich_func = twl_clks_init; sc->sc_init_hook.ich_arg = dev; if (config_intrhook_establish(&sc->sc_init_hook) != 0) return (ENOMEM); return (0); } static int twl_clks_detach(device_t dev) { struct twl_clks_softc *sc; struct twl_clk_entry *clk; struct twl_clk_entry *tmp; sc = device_get_softc(dev); TWL_CLKS_XLOCK(sc); LIST_FOREACH_SAFE(clk, &sc->sc_clks_list, link, tmp) { LIST_REMOVE(clk, link); sysctl_remove_oid(clk->oid, 1, 0); free(clk, M_DEVBUF); } TWL_CLKS_XUNLOCK(sc); TWL_CLKS_LOCK_DESTROY(sc); return (0); } static device_method_t twl_clks_methods[] = { DEVMETHOD(device_probe, twl_clks_probe), DEVMETHOD(device_attach, twl_clks_attach), DEVMETHOD(device_detach, twl_clks_detach), {0, 0}, }; static driver_t twl_clks_driver = { "twl_clks", twl_clks_methods, sizeof(struct twl_clks_softc), }; static devclass_t twl_clks_devclass; DRIVER_MODULE(twl_clks, twl, twl_clks_driver, twl_clks_devclass, 0, 0); MODULE_VERSION(twl_clks, 1);