#include <sys/modctl.h>
#include <sys/conf.h>
#include <sys/devops.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/bitext.h>
#include <sys/sysmacros.h>
#include <sys/i2c/client.h>
#include <eedev.h>
#define AT24C_MAX_IO 128
static uint32_t at24c_write_to_count = 5;
static uint32_t at24c_write_delay_us = 5000;
typedef struct at24c_ident {
const char *ati_name;
const char *ati_compat;
size_t ati_size;
size_t ati_page;
bool ati_addr16;
} at24c_ident_t;
typedef struct at24c {
dev_info_t *at_dip;
kmutex_t at_mutex;
const at24c_ident_t *at_ident;
size_t at_naddrs;
i2c_client_t **at_clients;
i2c_reg_hdl_t **at_regs;
eedev_hdl_t *at_eedev;
uint8_t at_buf[AT24C_MAX_IO];
} at24c_t;
static const at24c_ident_t at24c_idents[] = {
{
.ati_name = "at24c01",
.ati_compat = "atmel,at24c01",
.ati_size = 128,
.ati_page = 8,
.ati_addr16 = false
}, {
.ati_name = "at24c02",
.ati_compat = "atmel,at24c02",
.ati_size = 256,
.ati_page = 8,
.ati_addr16 = false
}, {
.ati_name = "at24c04",
.ati_compat = "atmel,at24c04",
.ati_size = 512,
.ati_page = 16,
.ati_addr16 = false
}, {
.ati_name = "at24c08",
.ati_compat = "atmel,at24c08",
.ati_size = 1024,
.ati_page = 16,
.ati_addr16 = false
}, {
.ati_name = "at24c16",
.ati_compat = "atmel,at24c16",
.ati_size = 2 * 1024,
.ati_page = 16,
.ati_addr16 = false
}, {
.ati_name = "at24c32",
.ati_compat = "atmel,at24c32",
.ati_size = 4 * 1024,
.ati_page = 32,
.ati_addr16 = true
}, {
.ati_name = "at24c64",
.ati_compat = "atmel,at24c64",
.ati_size = 8 * 1024,
.ati_page = 32,
.ati_addr16 = true
}, {
.ati_name = "at24c128",
.ati_compat = "atmel,at24c128",
.ati_size = 16 * 1024,
.ati_page = 64,
.ati_addr16 = true
}, {
.ati_name = "at24c256",
.ati_compat = "atmel,at24c256",
.ati_size = 32 * 1024,
.ati_page = 64,
.ati_addr16 = true
}, {
.ati_name = "at24c512",
.ati_compat = "atmel,at24c512",
.ati_size = 64 * 1024,
.ati_page = 128,
.ati_addr16 = true
}, {
.ati_name = "at24c1024",
.ati_compat = "atmel,at24c1024",
.ati_size = 128 * 1024,
.ati_page = 256,
.ati_addr16 = true
}
};
static void
at24c_page_to_addr(at24c_t *at, uint32_t page, uint32_t pageoff,
uint32_t *clino, uint32_t *addrp)
{
uint32_t addr = page * at->at_ident->ati_page + pageoff;
uint32_t addrlen = at->at_ident->ati_addr16 ? 1 << 16 : 1 << 8;
*clino = addr / addrlen;
*addrp = addr % addrlen;
VERIFY3U(*clino, <, at->at_naddrs);
}
static int
at24c_read(void *arg, struct uio *uio, uint32_t page, uint32_t pageoff,
uint32_t nbytes)
{
int ret;
uint32_t idx, reg;
i2c_error_t err;
at24c_t *at = arg;
at24c_page_to_addr(at, page, pageoff, &idx, ®);
mutex_enter(&at->at_mutex);
if (i2c_reg_get(NULL, at->at_regs[idx], reg, at->at_buf, nbytes,
&err)) {
ret = uiomove(at->at_buf, nbytes, UIO_READ, uio);
} else {
dev_err(at->at_dip, CE_WARN, "!failed to read %u bytes on "
"client %u, addr 0x%x: 0x%x/0x%x", nbytes, idx, reg,
err.i2c_error, err.i2c_ctrl);
ret = EIO;
}
mutex_exit(&at->at_mutex);
return (ret);
}
static bool
at24c_poll_nack(const i2c_error_t *err)
{
if (err->i2c_error != I2C_CORE_E_CONTROLLER) {
return (false);
}
return (err->i2c_ctrl == I2C_CTRL_E_ADDR_NACK ||
err->i2c_ctrl == I2C_CTRL_E_NACK);
}
static int
at24c_write(void *arg, struct uio *uio, uint32_t page, uint32_t pageoff,
uint32_t nbytes)
{
int ret = EIO;
uint32_t idx, reg;
i2c_error_t err;
at24c_t *at = arg;
at24c_page_to_addr(at, page, pageoff, &idx, ®);
mutex_enter(&at->at_mutex);
ret = uiomove(at->at_buf, nbytes, UIO_WRITE, uio);
if (ret != 0) {
mutex_exit(&at->at_mutex);
return (ret);
}
if (!i2c_reg_put(NULL, at->at_regs[idx], reg, at->at_buf, nbytes,
&err)) {
dev_err(at->at_dip, CE_WARN, "!failed to write %u bytes on "
"client %u, addr 0x%x: 0x%x/0x%x", nbytes, idx, reg,
err.i2c_error, err.i2c_ctrl);
goto done;
}
for (uint32_t i = 0; i < at24c_write_to_count; i++) {
uint8_t val;
delay(drv_usectohz(at24c_write_delay_us));
if (i2c_reg_get(NULL, at->at_regs[idx], 0, &val, sizeof (val),
&err)) {
ret = 0;
break;
}
if (!at24c_poll_nack(&err)) {
dev_err(at->at_dip, CE_WARN, "!failed to read after "
"write on client %u, addr 0x%x: 0x%x/0x%x", idx,
reg, err.i2c_error, err.i2c_ctrl);
goto done;
}
}
if (ret != 0) {
dev_err(at->at_dip, CE_WARN, "!timed out waiting for write ack "
"on client %u, addr 0x%x: 0x%x/0x%x", idx, reg,
err.i2c_error, err.i2c_ctrl);
}
done:
mutex_exit(&at->at_mutex);
if (ret != 0) {
uio->uio_resid += nbytes;
uio->uio_loffset -= nbytes;
}
return (ret);
}
static const eedev_ops_t at24c_eedev_ops = {
.eo_read = at24c_read,
.eo_write = at24c_write
};
static bool
at24c_ident(at24c_t *at)
{
const char *bind = ddi_binding_name(at->at_dip);
const char *name = ddi_node_name(at->at_dip);
for (size_t i = 0; i < ARRAY_SIZE(at24c_idents); i++) {
if (strcmp(bind, at24c_idents[i].ati_name) == 0 ||
strcmp(bind, at24c_idents[i].ati_compat) == 0 ||
strcmp(name, at24c_idents[i].ati_name) == 0 ||
strcmp(name, at24c_idents[i].ati_compat) == 0) {
at->at_ident = &at24c_idents[i];
return (true);
}
}
dev_err(at->at_dip, CE_WARN, "failed to match against node name %s "
"and binding name %s", name, bind);
return (false);
}
static bool
at24c_i2c_init(at24c_t *at)
{
i2c_errno_t err;
uint8_t addrlen = at->at_ident->ati_addr16 ? 16 : 8;
at->at_naddrs = at->at_ident->ati_size / (1 << addrlen);
if ((at->at_ident->ati_size % (1 << addrlen)) != 0)
at->at_naddrs++;
VERIFY3U(at->at_naddrs, >, 0);
at->at_clients = kmem_zalloc(at->at_naddrs * sizeof (i2c_client_t *),
KM_SLEEP);
at->at_regs = kmem_zalloc(at->at_naddrs * sizeof (i2c_reg_hdl_t *),
KM_SLEEP);
if ((err = i2c_client_init(at->at_dip, 0, &at->at_clients[0])) !=
I2C_CORE_E_OK) {
dev_err(at->at_dip, CE_WARN, "failed to create i2c client: "
"0x%x", err);
return (false);
}
for (size_t i = 1; i < at->at_naddrs; i++) {
i2c_addr_t addr = *i2c_client_addr((at->at_clients[0]));
addr.ia_addr += i;
if ((err = i2c_client_claim_addr(at->at_dip, &addr, 0,
&at->at_clients[i])) != I2C_CORE_E_OK) {
dev_err(at->at_dip, CE_WARN, "failed to claim client "
"%zu address 0x%x: 0x%x", i, addr.ia_addr, err);
return (false);
}
}
i2c_reg_acc_attr_t attr;
bzero(&attr, sizeof (attr));
attr.i2cacc_version = I2C_REG_ACC_ATTR_V0;
if (at->at_ident->ati_addr16) {
attr.i2cacc_addr_len = 2;
attr.i2cacc_addr_endian = DDI_STRUCTURE_BE_ACC;
} else {
attr.i2cacc_addr_len = 1;
}
attr.i2cacc_addr_max = (1 << addrlen) - 1;
attr.i2cacc_reg_len = 1;
for (size_t i = 0; i < at->at_naddrs; i++) {
if ((err = i2c_reg_handle_init(at->at_clients[i], &attr,
&at->at_regs[i])) != I2C_CORE_E_OK) {
dev_err(at->at_dip, CE_WARN, "failed to create "
"register handle %zu: %s (0x%x)", i,
i2c_client_errtostr(at->at_clients[i], err), err);
return (false);
}
}
return (true);
}
static bool
at24c_eedev_init(at24c_t *at)
{
int ret;
eedev_reg_t reg;
bzero(®, sizeof (reg));
reg.ereg_vers = EEDEV_REG_VERS0;
reg.ereg_size = at->at_ident->ati_size;
reg.ereg_seg = at->at_ident->ati_page;
reg.ereg_read_gran = 1;
reg.ereg_write_gran = 1;
reg.ereg_max_read = MIN(i2c_reg_max_read(at->at_regs[0]), AT24C_MAX_IO);
reg.ereg_max_write = MIN(i2c_reg_max_write(at->at_regs[0]),
AT24C_MAX_IO);
reg.ereg_max_write = MIN(reg.ereg_max_write, at->at_ident->ati_page);
reg.ereg_dip = at->at_dip;
reg.ereg_driver = at;
reg.ereg_ops = &at24c_eedev_ops;
if ((ret = eedev_create(®, &at->at_eedev)) != 0) {
dev_err(at->at_dip, CE_WARN, "failed to create eedev device: "
"%d", ret);
return (false);
}
return (true);
}
static void
at24c_cleanup(at24c_t *at)
{
eedev_fini(at->at_eedev);
for (size_t i = 0; i < at->at_naddrs; i++) {
i2c_reg_handle_destroy(at->at_regs[i]);
i2c_client_destroy(at->at_clients[i]);
}
if (at->at_naddrs > 0) {
kmem_free(at->at_regs, sizeof (i2c_reg_hdl_t *) *
at->at_naddrs);
kmem_free(at->at_clients, sizeof (i2c_client_t *) *
at->at_naddrs);
}
mutex_destroy(&at->at_mutex);
ddi_set_driver_private(at->at_dip, NULL);
at->at_dip = NULL;
kmem_free(at, sizeof (at24c_t));
}
static int
at24c_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
at24c_t *at;
switch (cmd) {
case DDI_ATTACH:
break;
case DDI_RESUME:
return (DDI_SUCCESS);
default:
return (DDI_FAILURE);
}
at = kmem_zalloc(sizeof (at24c_t), KM_SLEEP);
at->at_dip = dip;
ddi_set_driver_private(dip, at);
mutex_init(&at->at_mutex, NULL, MUTEX_DRIVER, NULL);
if (!at24c_ident(at))
goto cleanup;
if (!at24c_i2c_init(at))
goto cleanup;
if (!at24c_eedev_init(at))
goto cleanup;
return (DDI_SUCCESS);
cleanup:
at24c_cleanup(at);
return (DDI_FAILURE);
}
static int
at24c_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
at24c_t *at;
switch (cmd) {
case DDI_DETACH:
break;
case DDI_SUSPEND:
return (DDI_SUCCESS);
default:
return (DDI_FAILURE);
}
at = ddi_get_driver_private(dip);
if (at == NULL) {
dev_err(dip, CE_WARN, "asked to detach, but missing private "
"data");
return (DDI_FAILURE);
}
VERIFY3P(at->at_dip, ==, dip);
at24c_cleanup(at);
return (DDI_SUCCESS);
}
static struct dev_ops at24c_dev_ops = {
.devo_rev = DEVO_REV,
.devo_refcnt = 0,
.devo_identify = nulldev,
.devo_probe = nulldev,
.devo_attach = at24c_attach,
.devo_detach = at24c_detach,
.devo_reset = nodev,
.devo_quiesce = ddi_quiesce_not_needed
};
static struct modldrv at24c_modldrv = {
.drv_modops = &mod_driverops,
.drv_linkinfo = "SPD5118 driver",
.drv_dev_ops = &at24c_dev_ops
};
static struct modlinkage at24c_modlinkage = {
.ml_rev = MODREV_1,
.ml_linkage = { &at24c_modldrv, NULL }
};
int
_init(void)
{
return (mod_install(&at24c_modlinkage));
}
int
_info(struct modinfo *modinfop)
{
return (mod_info(&at24c_modlinkage, modinfop));
}
int
_fini(void)
{
return (mod_remove(&at24c_modlinkage));
}