diff --git a/sys/arm64/conf/std.nxp b/sys/arm64/conf/std.nxp --- a/sys/arm64/conf/std.nxp +++ b/sys/arm64/conf/std.nxp @@ -7,6 +7,7 @@ # I2C device pca954x # NPX I2C bus multiplexer / switches +device pca963x # NXP Fm+ I2C-bus LED driver device pcf8563 # NXP Real-time clock/calendar device tca64xx # NXP I2C gpio expander device pcf85063 # NXP Real-time clock diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -1820,6 +1820,7 @@ dev/iicbus/sensor/htu21.c optional htu21 dev/iicbus/sensor/lm75.c optional lm75 dev/iicbus/sensor/max44009.c optional max44009 +dev/iicbus/gpio/pca963x.c optional pca963x dev/iicbus/gpio/pcf8574.c optional pcf8574 dev/iicbus/gpio/tca64xx.c optional tca64xx fdt gpio dev/iicbus/pmic/fan53555.c optional fan53555 fdt | tcs4525 fdt diff --git a/sys/dev/iicbus/gpio/pca963x.c b/sys/dev/iicbus/gpio/pca963x.c new file mode 100644 --- /dev/null +++ b/sys/dev/iicbus/gpio/pca963x.c @@ -0,0 +1,546 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 Bjoern A. Zeeb + * + * 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 THE 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 THE 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. + */ + +/* + * Highly simplified driver for PCA963x done on 9633. + * Ignoring most (PWM, GROUP, etc.) features. + * The only thing we do take care off is getting in/out of low-power mode + * (MODE1 SLEEP) if no LEDs are on or one is turned on. + * We export the LEDs as GPIO pins to enable/disable them at full brightness. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include "gpio_if.h" + +#define PCA963X_MODE1 0x00 +#define PCA963X_MODE1_SLEEP (1 << 4) +#define PCA963X_MODE2 0x01 +#define PCA963X_MODE2_INVRT (1 << 4) + +/* LEDOUT register address is configured in pca963x_descr "led_out_reg" */ +#define PCA963X_LEDOUT_LDR00 0x00 +#define PCA963X_LEDOUT_LDR00_STATE 0 /* off, power up */ +#define PCA963X_LEDOUT_LDR01 0x01 +#define PCA963X_LEDOUT_LDR01_STATE 1 /* full on */ +#define PCA963X_LEDOUT_LDR10 0x02 +#define PCA963X_LEDOUT_LDR10_STATE 0 /* PWMx controlled (unsupported) */ +#define PCA963X_LEDOUT_LDR11 0x03 +#define PCA963X_LEDOUT_LDR11_STATE 0 /* PWMx/GRPPWM controlled (unsupported) */ + +#define PCA963X_PINS_PER_REG 4 + +struct pca963x_descr { + const char *partname; + const char *description; + uint8_t led_out_reg; + uint8_t num_pins; + uint32_t pin_caps; +}; + +static struct pca963x_descr pca9633 = { + .partname = "pca9633", + .description = "PCA9633 4-bit Fm+ I2C-bus LED driver", + .num_pins = 4, + .led_out_reg = 0x08, +}; + + +static struct ofw_compat_data compat_data[] = { + { "nxp,pca9633", (uintptr_t)&pca9633 }, + { 0, 0 } +}; + +struct pca963x_softc { + device_t dev; + device_t busdev; + struct mtx mtx; + uint32_t addr; + bool sleep; + BITSET_DEFINE_VAR() *ledset; + + const struct pca963x_descr *descr; +}; + +static MALLOC_DEFINE(M_PCA963X, "pca963x", "pca963x I2C-bus LED driver"); +static int +pca963x_read(device_t dev, uint8_t reg, uint8_t *data) +{ + struct pca963x_softc *sc; + struct iic_msg msgs[2]; + int error; + + if (data == NULL) + return (EINVAL); + + sc = device_get_softc(dev); + + msgs[0].slave = sc->addr; + msgs[0].flags = IIC_M_WR | IIC_M_NOSTOP; + msgs[0].len = 1; + msgs[0].buf = ® + + msgs[1].slave = sc->addr; + msgs[1].flags = IIC_M_RD; + msgs[1].len = 1; + msgs[1].buf = data; + + error = iicbus_transfer_excl(dev, msgs, 2, IIC_WAIT); + return (iic2errno(error)); +} + +static int +pca963x_write(device_t dev, uint8_t reg, uint8_t val) +{ + struct pca963x_softc *sc; + struct iic_msg msg; + uint8_t buf[2]; + int error; + + sc = device_get_softc(dev); + + buf[0] = reg; + buf[1] = val; + + msg.slave = sc->addr; + msg.flags = IIC_M_WR; + msg.len = 2; + msg.buf = buf; + + error = iicbus_transfer_excl(dev, &msg, 1, IIC_WAIT); + return (iic2errno(error)); +} + +static void +pca963x_update_led_state_locked(device_t dev, uint32_t pin, uint8_t nval) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + if (nval == 0) + BIT_CLR(sc->descr->num_pins, pin, sc->ledset); + else + BIT_SET(sc->descr->num_pins, pin, sc->ledset); +} + +static void +pca963x_update_mode1_sleep_locked(device_t dev) +{ + struct pca963x_softc *sc; + int error; + uint8_t val; + bool empty; + + sc = device_get_softc(dev); + empty = BIT_EMPTY(sc->descr->num_pins, sc->ledset); + + if (sc->sleep == empty) + return; + + /* Update cached state and toggle oscillator state. */ + sc->sleep = empty; + + error = pca963x_read(dev, PCA963X_MODE1, &val); + if (error != 0) { + device_printf(dev, "%s: failed to read MODE1: %d\n", + __func__, error); + return; + } + if (sc->sleep) + val |= PCA963X_MODE1_SLEEP; + else + val &= ~PCA963X_MODE1_SLEEP; + error = pca963x_write(dev, PCA963X_MODE1, val); + if (error != 0) + device_printf(dev, "%s: failed to toggle MODE1 SLEEP: %d\n", + __func__, error); +} + + +static device_t +pca963x_get_bus(device_t dev) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + return (sc->busdev); +} + +static int +pca963x_pin_max(device_t dev, int *maxpin) +{ + struct pca963x_softc *sc; + + if (maxpin == NULL) + return (EINVAL); + + sc = device_get_softc(dev); + *maxpin = sc->descr->num_pins - 1; + return (0); +} + +static int +pca963x_pin_getcaps(device_t dev, uint32_t pin, uint32_t *caps) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins || caps == NULL) + return (EINVAL); + + *caps = sc->descr->pin_caps; + return (0); +} + +static int +pca963x_pin_getflags(device_t dev, uint32_t pin, uint32_t *pflags) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins || pflags == NULL) + return (EINVAL); + + *pflags = GPIO_PIN_OUTPUT; + + return (0); +} + +static int +pca963x_pin_setflags(device_t dev, uint32_t pin, uint32_t flags) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + + if (pin >= sc->descr->num_pins) + return (EINVAL); + + return (ENODEV); +} + +static int +pca963x_pin_getname(device_t dev, uint32_t pin, char *name) +{ + struct pca963x_softc *sc; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins || name == NULL) + return (EINVAL); + + snprintf(name, GPIOMAXNAME, "gpio_P%d%d", pin / PCA963X_PINS_PER_REG, + pin % PCA963X_PINS_PER_REG); + + return (0); +} + +static int +pca963x_pin_get(device_t dev, uint32_t pin, unsigned int *pval) +{ + struct pca963x_softc *sc; + int error; + uint8_t addr, bitshift, val; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins || pval == NULL) + return (EINVAL); + + addr = sc->descr->led_out_reg + (pin / PCA963X_PINS_PER_REG); + + mtx_lock(&sc->mtx); + error = pca963x_read(dev, addr, &val); + mtx_unlock(&sc->mtx); + if (error != 0) + return (error); + + bitshift = 2 * (pin % PCA963X_PINS_PER_REG); + switch ((val >> bitshift) & 0x3) { + case PCA963X_LEDOUT_LDR00: + *pval = PCA963X_LEDOUT_LDR00_STATE; + break; + case PCA963X_LEDOUT_LDR01: + *pval = PCA963X_LEDOUT_LDR01_STATE; + break; + case PCA963X_LEDOUT_LDR10: + *pval = PCA963X_LEDOUT_LDR10_STATE; /* unsupported, hence defined to off */ + break; + case PCA963X_LEDOUT_LDR11: + *pval = PCA963X_LEDOUT_LDR11_STATE; /* unsupported, hence defined to off */ + break; + } + + return (0); +} + +static int +pca963x_pin_set(device_t dev, uint32_t pin, unsigned int nval) +{ + struct pca963x_softc *sc; + int error; + uint8_t addr, bitshift, val; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins || nval > PCA963X_LEDOUT_LDR11) + return (EINVAL); + + addr = sc->descr->led_out_reg + (pin / PCA963X_PINS_PER_REG); + + mtx_lock(&sc->mtx); + error = pca963x_read(dev, addr, &val); + if (error != 0) + goto fail; + + bitshift = 2 * (pin % PCA963X_PINS_PER_REG); + + switch ((val >> bitshift) & 0x3) { + case PCA963X_LEDOUT_LDR00: + nval = PCA963X_LEDOUT_LDR00_STATE; + break; + case PCA963X_LEDOUT_LDR01: + nval = PCA963X_LEDOUT_LDR01_STATE; + break; + case PCA963X_LEDOUT_LDR10: + nval = PCA963X_LEDOUT_LDR10_STATE; /* unsupported, hence always defined to off */ + break; + case PCA963X_LEDOUT_LDR11: + nval = PCA963X_LEDOUT_LDR11_STATE; /* unsupported, hence always defined to off */ + break; + default: + error = EINVAL; + goto fail; + /* NOTREACHED */ + } + val &= ~(0x3 << bitshift); + val |= (nval & 0x3) << bitshift; + error = pca963x_write(dev, addr, val); + + pca963x_update_led_state_locked(dev, pin, nval); + pca963x_update_mode1_sleep_locked(dev); +fail: + mtx_unlock(&sc->mtx); + return (error); +} + +static int +pca963x_pin_toggle(device_t dev, uint32_t pin) +{ + struct pca963x_softc *sc; + int error; + uint8_t addr, bitshift, nval, val; + + sc = device_get_softc(dev); + if (pin >= sc->descr->num_pins) + return (EINVAL); + + addr = sc->descr->led_out_reg + (pin / PCA963X_PINS_PER_REG); + + mtx_lock(&sc->mtx); + error = pca963x_read(dev, addr, &val); + if (error != 0) + goto fail; + + bitshift = 2 * (pin % PCA963X_PINS_PER_REG); + + switch ((val >> bitshift) & 0x3) { + case PCA963X_LEDOUT_LDR00: /* off -> on */ + nval = PCA963X_LEDOUT_LDR01_STATE; + break; + case PCA963X_LEDOUT_LDR01: /* on -> off */ + nval = PCA963X_LEDOUT_LDR00_STATE; + break; + case PCA963X_LEDOUT_LDR10: + nval = PCA963X_LEDOUT_LDR10_STATE; /* unsupported, hence always defined to off */ + break; + case PCA963X_LEDOUT_LDR11: + nval = PCA963X_LEDOUT_LDR11_STATE; /* unsupported, hence always defined to off */ + break; + default: + error = EINVAL; + goto fail; + /* NOTREACHED */ + } + val &= ~(0x3 << bitshift); + val |= (nval & 0x3) << bitshift; + error = pca963x_write(dev, addr, val); + + pca963x_update_led_state_locked(dev, pin, nval); + pca963x_update_mode1_sleep_locked(dev); +fail: + mtx_unlock(&sc->mtx); + return (error); +} + +static int +pca963x_probe(device_t dev) +{ + const struct ofw_compat_data *compat_ptr; + const struct pca963x_descr *descr; + + if (!ofw_bus_status_okay(dev)) + return (ENXIO); + + compat_ptr = ofw_bus_search_compatible(dev, compat_data); + if (compat_ptr->ocd_data == 0) + return (ENXIO); + + descr = (const struct pca963x_descr *)compat_ptr->ocd_data; + device_set_desc(dev, descr->description); + return (BUS_PROBE_DEFAULT); +} + +static int +pca963x_attach(device_t dev) +{ + struct pca963x_softc *sc; + const struct ofw_compat_data *compat_ptr; + const struct pca963x_descr *descr; + int i; + + sc = device_get_softc(dev); + compat_ptr = ofw_bus_search_compatible(dev, compat_data); + descr = (const struct pca963x_descr *)compat_ptr->ocd_data; + + mtx_init(&sc->mtx, "pca963x_gpio", "gpio", MTX_DEF); + sc->dev = dev; + sc->addr = iicbus_get_addr(dev); + sc->descr = descr; + sc->ledset = BITSET_ALLOC(sc->descr->num_pins, M_PCA963X, + M_WAITOK | M_ZERO); + + /* Turn everything off for starters... */ + mtx_lock(&sc->mtx); + /* + * Turn oscillator off (LPM, should be default), disable auto-incr., + * turn off SUB[123] and ALLCALL i2c (sub-)addresses. + */ + sc->sleep = true; + pca963x_write(dev, PCA963X_MODE1, PCA963X_MODE1_SLEEP); + + /* Turn all LEDs off. */ + for (i = 0; i < (sc->descr->num_pins / PCA963X_PINS_PER_REG); i++) + pca963x_write(dev, sc->descr->led_out_reg + i, 0x00); + mtx_unlock(&sc->mtx); + + /* + * We should probably add a pin for each num_pins manually if + * configured from FDT so we can add names, etc. too and only + * attach the ones actually connected. + * Until we have a better LED infrastructure let's stay with + * one pin for one/off and all pins exposed. + */ + + sc->busdev = gpiobus_attach_bus(dev); + if (sc->busdev == NULL) { + device_printf(dev, "Could not create busdev child\n"); + mtx_destroy(&sc->mtx); + return (ENXIO); + } + + OF_device_register_xref(OF_xref_from_node(ofw_bus_get_node(dev)), dev); + + + return (0); +} + +static int +pca963x_detach(device_t dev) +{ + struct pca963x_softc *sc; + int i; + + sc = device_get_softc(dev); + + /* Turn everything off again for desert... */ + mtx_lock(&sc->mtx); + /* + * Turn oscillator off (LPM, should be default), disable auto-incr., + * turn off SUB[123] and ALLCALL i2c (sub-)addresses. + */ + sc->sleep = true; + pca963x_write(dev, PCA963X_MODE1, PCA963X_MODE1_SLEEP); + + /* Turn all LEDs off. */ + for (i = 0; i < (sc->descr->num_pins / PCA963X_PINS_PER_REG); i++) + pca963x_write(dev, sc->descr->led_out_reg + i, 0x00); + mtx_unlock(&sc->mtx); + + if (sc->busdev != NULL) + gpiobus_detach_bus(sc->busdev); + if (sc->ledset) + BITSET_FREE(sc->ledset, M_PCA963X); + + mtx_destroy(&sc->mtx); + + return (0); +} + +static device_method_t pca963x_methods[] = { + DEVMETHOD(device_probe, pca963x_probe), + DEVMETHOD(device_attach, pca963x_attach), + DEVMETHOD(device_detach, pca963x_detach), + + /* GPIO methods */ + DEVMETHOD(gpio_get_bus, pca963x_get_bus), + DEVMETHOD(gpio_pin_max, pca963x_pin_max), + DEVMETHOD(gpio_pin_getcaps, pca963x_pin_getcaps), + DEVMETHOD(gpio_pin_getflags, pca963x_pin_getflags), + DEVMETHOD(gpio_pin_setflags, pca963x_pin_setflags), + DEVMETHOD(gpio_pin_getname, pca963x_pin_getname), + DEVMETHOD(gpio_pin_get, pca963x_pin_get), + DEVMETHOD(gpio_pin_set, pca963x_pin_set), + DEVMETHOD(gpio_pin_toggle, pca963x_pin_toggle), + + DEVMETHOD_END +}; + +static driver_t pca963x_driver = { + "gpio", + pca963x_methods, + sizeof(struct pca963x_softc) +}; + +DRIVER_MODULE(pca963x, iicbus, pca963x_driver, 0, 0); +MODULE_VERSION(pca963x, 1);