#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: drivebay.c,v 1.4 2025/08/18 05:29:04 macallan Exp $");
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/conf.h>
#include <sys/bus.h>
#include <sys/sysctl.h>
#include <dev/ofw/openfirm.h>
#include <dev/i2c/i2cvar.h>
#ifdef DRIVEBAY_DEBUG
#define DPRINTF printf
#else
#define DPRINTF if (0) printf
#endif
#define PCAGPIO_INPUT 0x00
#define PCAGPIO_OUTPUT 0x01
#define PCAGPIO_REVERT 0x02
#define PCAGPIO_CONFIG 0x03
#define O_POWER 0x01
#define O_FAIL 0x02
#define O_INUSE 0x04
#define I_PRESENT 0x08
#define I_SWITCH 0x10
#define I_RESET 0x20
#define I_POWER 0x40
#define I_INUSE 0x80
static int drivebay_match(device_t, cfdata_t, void *);
static void drivebay_attach(device_t, device_t, void *);
static int drivebay_detach(device_t, int);
struct drivebay_softc {
device_t sc_dev;
i2c_tag_t sc_i2c;
i2c_addr_t sc_addr;
uint32_t sc_state, sc_input, sc_last_update;
#ifdef DRIVEBAY_DEBUG
uint32_t sc_dir, sc_in;
#endif
};
static void drivebay_writereg(struct drivebay_softc *, int, uint32_t);
static uint32_t drivebay_readreg(struct drivebay_softc *, int);
static int sysctl_power(SYSCTLFN_ARGS);
static int sysctl_fail(SYSCTLFN_ARGS);
static int sysctl_inuse(SYSCTLFN_ARGS);
static int sysctl_present(SYSCTLFN_ARGS);
static int sysctl_switch(SYSCTLFN_ARGS);
CFATTACH_DECL_NEW(drivebay, sizeof(struct drivebay_softc),
drivebay_match, drivebay_attach, drivebay_detach, NULL);
static const struct device_compatible_entry compat_data[] = {
{ .compat = "drivebay-i2c-gpio", .value = 0 },
DEVICE_COMPAT_EOL
};
static int
drivebay_match(device_t parent, cfdata_t match, void *aux)
{
struct i2c_attach_args *ia = aux;
int match_result;
if (iic_use_direct_match(ia, match, compat_data, &match_result))
return 2 * match_result;
return 0;
}
#ifdef DRIVEBAY_DEBUG
static void
printdir(struct drivebay_softc *sc, uint32_t val, uint32_t mask, char letter)
{
char flags[17], bits[17];
uint32_t bit = 0x80;
int i, cnt = 8;
val &= mask;
for (i = 0; i < cnt; i++) {
flags[i] = (mask & bit) ? letter : '-';
bits[i] = (val & bit) ? 'X' : ' ';
bit = bit >> 1;
}
flags[cnt] = 0;
bits[cnt] = 0;
printf("%s: dir: %s\n", device_xname(sc->sc_dev), flags);
printf("%s: lvl: %s\n", device_xname(sc->sc_dev), bits);
}
#endif
static void
drivebay_attach(device_t parent, device_t self, void *aux)
{
struct drivebay_softc *sc = device_private(self);
struct i2c_attach_args *ia = aux;
const struct sysctlnode *me = NULL, *node = NULL;
int ret;
sc->sc_dev = self;
sc->sc_i2c = ia->ia_tag;
sc->sc_addr = ia->ia_addr;
sc->sc_last_update = 0xffffffff;
aprint_naive("\n");
aprint_normal(": PCA9554\n");
drivebay_writereg(sc, PCAGPIO_CONFIG, 0xf8);
sc->sc_state = drivebay_readreg(sc, PCAGPIO_OUTPUT);
ret = sysctl_createv(NULL, 0, NULL, &me,
CTLFLAG_READWRITE,
CTLTYPE_NODE, device_xname(sc->sc_dev), NULL,
NULL, 0, NULL, 0,
CTL_HW, CTL_CREATE, CTL_EOL);
ret = sysctl_createv(NULL, 0, NULL, &node,
CTLFLAG_READWRITE | CTLFLAG_OWNDESC,
CTLTYPE_INT, "power", "drive power",
sysctl_power, 1, (void *)sc, 0,
CTL_HW, me->sysctl_num, CTL_CREATE, CTL_EOL);
ret = sysctl_createv(NULL, 0, NULL, &node,
CTLFLAG_READWRITE | CTLFLAG_OWNDESC,
CTLTYPE_INT, "fail", "drive fail LED",
sysctl_fail, 1, (void *)sc, 0,
CTL_HW, me->sysctl_num, CTL_CREATE, CTL_EOL);
ret = sysctl_createv(NULL, 0, NULL, &node,
CTLFLAG_READWRITE | CTLFLAG_OWNDESC,
CTLTYPE_INT, "inuse", "drive in use LED",
sysctl_inuse, 1, (void *)sc, 0,
CTL_HW, me->sysctl_num, CTL_CREATE, CTL_EOL);
ret = sysctl_createv(NULL, 0, NULL, &node,
CTLFLAG_READONLY | CTLFLAG_OWNDESC,
CTLTYPE_INT, "present", "drive present",
sysctl_present, 1, (void *)sc, 0,
CTL_HW, me->sysctl_num, CTL_CREATE, CTL_EOL);
ret = sysctl_createv(NULL, 0, NULL, &node,
CTLFLAG_READONLY | CTLFLAG_OWNDESC,
CTLTYPE_INT, "switch", "drive switch",
sysctl_switch, 1, (void *)sc, 0,
CTL_HW, me->sysctl_num, CTL_CREATE, CTL_EOL);
__USE(ret);
#ifdef DRIVEBAY_DEBUG
uint32_t in, out;
sc->sc_dir = drivebay_readreg(sc, PCAGPIO_CONFIG);
sc->sc_in = drivebay_readreg(sc, PCAGPIO_INPUT);
in = sc->sc_in;
out = sc->sc_state;
out &= ~sc->sc_dir;
in &= sc->sc_dir;
printdir(sc, in, sc->sc_dir, 'I');
printdir(sc, out, ~sc->sc_dir, 'O');
#endif
}
static int
drivebay_detach(device_t self, int flags)
{
return 0;
}
static void
drivebay_writereg(struct drivebay_softc *sc, int reg, uint32_t val)
{
uint8_t cmd;
uint8_t creg;
iic_acquire_bus(sc->sc_i2c, 0);
cmd = reg;
creg = (uint8_t)val;
iic_exec(sc->sc_i2c, I2C_OP_WRITE_WITH_STOP,
sc->sc_addr, &cmd, 1, &creg, 1, 0);
if (reg == PCAGPIO_OUTPUT) sc->sc_state = val;
iic_release_bus(sc->sc_i2c, 0);
}
static uint32_t
drivebay_readreg(struct drivebay_softc *sc, int reg)
{
uint8_t cmd;
uint8_t creg;
uint32_t ret;
if (reg == PCAGPIO_INPUT) {
if (time_uptime32 == sc->sc_last_update)
return sc->sc_input;
}
iic_acquire_bus(sc->sc_i2c, 0);
cmd = reg;
iic_exec(sc->sc_i2c, I2C_OP_READ_WITH_STOP,
sc->sc_addr, &cmd, 1, &creg, 1, 0);
ret = creg;
iic_release_bus(sc->sc_i2c, 0);
if (reg == PCAGPIO_INPUT) {
sc->sc_last_update = time_uptime32;
sc->sc_input = ret;
}
return ret;
}
static int
sysctl_power(SYSCTLFN_ARGS)
{
struct sysctlnode node = *rnode;
struct drivebay_softc *sc = node.sysctl_data;
int reg = drivebay_readreg(sc, PCAGPIO_INPUT);
int power = (reg & I_POWER) != 0;
if (newp) {
node.sysctl_data = &power;
if (sysctl_lookup(SYSCTLFN_CALL(&node)) == 0) {
power = *(int *)node.sysctl_data;
reg = sc->sc_state;
if (power != 0) reg |= O_POWER;
else reg &= ~O_POWER;
if (reg != sc->sc_state) {
drivebay_writereg(sc, PCAGPIO_OUTPUT, reg);
sc->sc_state = reg;
}
return 0;
}
return EINVAL;
} else {
node.sysctl_data = &power;
node.sysctl_size = 4;
return (sysctl_lookup(SYSCTLFN_CALL(&node)));
}
return 0;
}
static int
sysctl_inuse(SYSCTLFN_ARGS)
{
struct sysctlnode node = *rnode;
struct drivebay_softc *sc = node.sysctl_data;
int reg = drivebay_readreg(sc, PCAGPIO_INPUT);
int inuse = (reg & I_INUSE) != 0;
if (newp) {
node.sysctl_data = &inuse;
if (sysctl_lookup(SYSCTLFN_CALL(&node)) == 0) {
inuse = *(int *)node.sysctl_data;
reg = sc->sc_state;
if (inuse != 0) reg |= O_INUSE;
else reg &= ~O_INUSE;
if (reg != sc->sc_state) {
drivebay_writereg(sc, PCAGPIO_OUTPUT, reg);
sc->sc_state = reg;
}
return 0;
}
return EINVAL;
} else {
node.sysctl_data = &inuse;
node.sysctl_size = 4;
return (sysctl_lookup(SYSCTLFN_CALL(&node)));
}
return 0;
}
static int
sysctl_fail(SYSCTLFN_ARGS)
{
struct sysctlnode node = *rnode;
struct drivebay_softc *sc = node.sysctl_data;
int reg = sc->sc_state;
int fail = (reg & O_FAIL) != 0;
if (newp) {
node.sysctl_data = &fail;
if (sysctl_lookup(SYSCTLFN_CALL(&node)) == 0) {
fail = *(int *)node.sysctl_data;
reg = sc->sc_state;
if (fail != 0) reg |= O_FAIL;
else reg &= ~O_FAIL;
if (reg != sc->sc_state) {
drivebay_writereg(sc, PCAGPIO_OUTPUT, reg);
sc->sc_state = reg;
}
return 0;
}
return EINVAL;
} else {
node.sysctl_data = &fail;
node.sysctl_size = 4;
return (sysctl_lookup(SYSCTLFN_CALL(&node)));
}
return 0;
}
static int
sysctl_present(SYSCTLFN_ARGS)
{
struct sysctlnode node = *rnode;
struct drivebay_softc *sc = node.sysctl_data;
int reg = drivebay_readreg(sc, PCAGPIO_INPUT);
int present = (reg & I_PRESENT) == 0;
if (newp) {
return EINVAL;
} else {
node.sysctl_data = &present;
node.sysctl_size = 4;
return (sysctl_lookup(SYSCTLFN_CALL(&node)));
}
return 0;
}
static int
sysctl_switch(SYSCTLFN_ARGS)
{
struct sysctlnode node = *rnode;
struct drivebay_softc *sc = node.sysctl_data;
int reg = drivebay_readreg(sc, PCAGPIO_INPUT);
int sw = (reg & I_SWITCH) == 0;
if (newp) {
return EINVAL;
} else {
node.sysctl_data = &sw;
node.sysctl_size = 4;
return (sysctl_lookup(SYSCTLFN_CALL(&node)));
}
return 0;
}