blob: 6c10649863ba1c07a07627a8ed4d6938d6bcc5ce [file] [log] [blame]
/*
* Copyright (c) 2025 Michael Estes
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT nxp_pcal9722
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pcal9722, CONFIG_GPIO_LOG_LEVEL);
#include <zephyr/drivers/gpio/gpio_utils.h>
/* PCAL9722 Register addresses */
#define PCAL9722_INPUT_PORT0 0x00
#define PCAL9722_OUTPUT_PORT0 0x04
#define PCAL9722_CONFIG_PORT0 0x0C
#define PCAL9722_PULL_EN0 0x4C
#define PCAL9722_PULL_SEL0 0x50
#define PCAL9722_IRQMASK_PORT0 0x54
#define PCAL9722_IRQ_STAT_PORT0 0x58
#define PCAL9722_IRQEDGE_PORT0_A 0x60
#define PCAL9722_IRQ_CLEAR_PORT0 0x68
/* Bit instructs PCAL9722 to auto-increment register address between SPI bytes */
#define PCAL9722_AUTO_INC BIT(7)
/* Base address of the PCAL9722 sent as the first byte in every SPI transceive */
#define PCAL9722_ADDRESS 0x40
/* Set this bit in PCAL9722_ADDRESS to address another PCAL9722 device on the same chip select */
#define ADDRESS_BIT (BIT(1))
#define PCAL9722_READ_CMD BIT(0)
#define NUM_PINS 22
#define ALL_PINS ((uint32_t)BIT_MASK(NUM_PINS))
#define PCAL9722_ENABLE_IRQ_SUPPORT DT_ANY_INST_HAS_PROP_STATUS_OKAY(irq_gpios)
struct pcal9722_drv_data {
/* gpio_driver_data needs to be first */
struct gpio_driver_data common;
struct k_sem *lock;
struct gpio_callback gpio_cb;
struct k_work work;
const struct device *dev;
sys_slist_t cb;
};
struct pcal9722_config {
/* gpio_driver_config needs to be first */
struct gpio_driver_config common;
struct spi_dt_spec spi;
#if PCAL9722_ENABLE_IRQ_SUPPORT
const struct gpio_dt_spec gpio_int;
#endif
const uint8_t addr;
};
static int gpio_pcal9722_reg_read(const struct pcal9722_config *cfg, uint8_t reg, uint8_t *val,
uint8_t len)
{
uint8_t cmd_buf[2 + len];
cmd_buf[0] = cfg->addr | PCAL9722_READ_CMD;
cmd_buf[1] = reg;
const struct spi_buf tx_buf = {.buf = cmd_buf, .len = ARRAY_SIZE(cmd_buf)};
const struct spi_buf rx_buf = {.buf = cmd_buf, .len = ARRAY_SIZE(cmd_buf)};
const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1};
const struct spi_buf_set rx = {.buffers = &rx_buf, .count = 1};
int ret;
ret = spi_transceive_dt(&cfg->spi, &tx, &rx);
if (ret < 0) {
return ret;
}
memcpy(val, &cmd_buf[2], len);
return 0;
}
static int gpio_pcal9722_reg_write(const struct pcal9722_config *cfg, uint8_t reg, uint8_t *val,
uint8_t len)
{
uint8_t cmd_buf[2] = {cfg->addr, PCAL9722_AUTO_INC | reg};
const struct spi_buf tx_buf[2] = {
{.buf = cmd_buf, .len = sizeof(cmd_buf)},
{.buf = val, .len = len},
};
const struct spi_buf_set tx = {.buffers = &tx_buf[0], .count = 2};
return spi_transceive_dt(&cfg->spi, &tx, NULL);
}
static int gpio_pcal9722_config(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags)
{
const struct pcal9722_config *cfg = dev->config;
struct pcal9722_drv_data *drv_data = dev->data;
uint32_t pullen = 0;
uint32_t pull = 0;
uint32_t dir = 0;
uint32_t val = 0;
int rc = 0;
/* Can't do SPI bus operations from an ISR */
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
/* Single Ended lines (Open drain and open source) not supported */
if ((flags & GPIO_SINGLE_ENDED) != 0) {
return -ENOTSUP;
}
/* Simultaneous input & output mode not supported */
if (((flags & GPIO_INPUT) != 0) && ((flags & GPIO_OUTPUT) != 0)) {
return -ENOTSUP;
}
k_sem_take(drv_data->lock, K_FOREVER);
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_CONFIG_PORT0, (uint8_t *)&dir, sizeof(dir));
if (rc != 0) {
goto out;
}
dir = sys_le32_to_cpu(dir) & ALL_PINS;
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&val, sizeof(val));
if (rc != 0) {
goto out;
}
val = sys_le32_to_cpu(val) & ALL_PINS;
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_PULL_EN0, (uint8_t *)&pullen, sizeof(pullen));
if (rc != 0) {
goto out;
}
pullen = sys_le32_to_cpu(pullen) & ALL_PINS;
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_PULL_SEL0, (uint8_t *)&pull, sizeof(pull));
if (rc != 0) {
goto out;
}
pull = sys_le32_to_cpu(pull) & ALL_PINS;
/* Ensure either Output or Input is specified */
WRITE_BIT(dir, pin, (flags & GPIO_INPUT));
if (flags & GPIO_OUTPUT_INIT_LOW || flags & GPIO_OUTPUT_INIT_HIGH) {
WRITE_BIT(val, pin, (flags & GPIO_OUTPUT_INIT_HIGH));
}
if (flags & GPIO_PULL_UP || flags & GPIO_PULL_DOWN) {
WRITE_BIT(pullen, pin, 1);
WRITE_BIT(pull, pin, (flags & GPIO_PULL_UP));
} else {
WRITE_BIT(pullen, pin, 0);
}
pull = sys_cpu_to_le32(pull & ALL_PINS);
pullen = sys_cpu_to_le32(pull & ALL_PINS);
val = sys_cpu_to_le32(val & ALL_PINS);
dir = sys_cpu_to_le32(dir & ALL_PINS);
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&val, 3);
if (rc != 0) {
goto out;
}
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_CONFIG_PORT0, (uint8_t *)&dir, 3);
if (rc != 0) {
goto out;
}
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_PULL_SEL0, (uint8_t *)&pull, 3);
if (rc != 0) {
goto out;
}
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_PULL_EN0, (uint8_t *)&pullen, 3);
if (rc != 0) {
goto out;
}
out:
k_sem_give(drv_data->lock);
return rc;
}
static int gpio_pcal9722_port_read(const struct device *dev, gpio_port_value_t *value)
{
const struct pcal9722_config *cfg = dev->config;
struct pcal9722_drv_data *drv_data = dev->data;
uint32_t data = 0;
int rc = 0;
/* Can't do SPI bus operations from an ISR */
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(drv_data->lock, K_FOREVER);
/* Read Input Register */
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_INPUT_PORT0, (uint8_t *)&data, 3);
data = sys_le32_to_cpu(data) & ALL_PINS;
LOG_DBG("read 0x%06X got %d", data, rc);
if (rc == 0) {
*value = (gpio_port_value_t)(data);
}
k_sem_give(drv_data->lock);
return rc;
}
static int gpio_pcal9722_port_write(const struct device *dev, gpio_port_pins_t mask,
gpio_port_value_t value, gpio_port_value_t toggle)
{
const struct pcal9722_config *cfg = dev->config;
struct pcal9722_drv_data *drv_data = dev->data;
uint32_t data = 0;
int rc = 0;
/* Can't do SPI bus operations from an ISR */
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(drv_data->lock, K_FOREVER);
gpio_pcal9722_reg_read(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&data, 3);
data = sys_le32_to_cpu(data) & ALL_PINS;
data = sys_cpu_to_le32((((data & ~mask) | (value & mask)) ^ toggle) & ALL_PINS);
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&data, 3);
k_sem_give(drv_data->lock);
LOG_DBG("write %x msk %08x val %08x => %x: %d", data, mask, value, data, rc);
return rc;
}
static int gpio_pcal9722_port_set_masked(const struct device *dev, gpio_port_pins_t mask,
gpio_port_value_t value)
{
return gpio_pcal9722_port_write(dev, mask, value, 0);
}
static int gpio_pcal9722_port_set_bits(const struct device *dev, gpio_port_pins_t pins)
{
return gpio_pcal9722_port_write(dev, pins, pins, 0);
}
static int gpio_pcal9722_port_clear_bits(const struct device *dev, gpio_port_pins_t pins)
{
return gpio_pcal9722_port_write(dev, pins, 0, 0);
}
static int gpio_pcal9722_port_toggle_bits(const struct device *dev, gpio_port_pins_t pins)
{
return gpio_pcal9722_port_write(dev, 0, 0, pins);
}
#if PCAL9722_ENABLE_IRQ_SUPPORT
static int gpio_pcal9722_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin,
enum gpio_int_mode mode, enum gpio_int_trig trig)
{
const struct pcal9722_config *cfg = dev->config;
struct pcal9722_drv_data *drv_data = dev->data;
uint64_t irqedge = 0;
uint32_t irqmask = 0;
int rc = 0;
k_sem_take(drv_data->lock, K_FOREVER);
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQEDGE_PORT0_A, (uint8_t *)&irqedge, 6);
if (rc != 0) {
goto out;
}
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQMASK_PORT0, (uint8_t *)&irqmask, 3);
if (rc != 0) {
goto out;
}
irqedge = sys_le64_to_cpu(irqedge) & BIT64_MASK(NUM_PINS * 2);
irqmask = sys_le32_to_cpu(irqmask) & ALL_PINS;
if (mode == GPIO_INT_MODE_DISABLED) {
WRITE_BIT(irqmask, pin, 1);
} else if (mode == GPIO_INT_MODE_LEVEL) {
WRITE_BIT(irqmask, pin, 0);
irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
} else { /* GPIO_INT_MODE_EDGE */
WRITE_BIT(irqmask, pin, 0);
if (trig == GPIO_INT_TRIG_BOTH) {
/* Set both edge trigger bits for this pin */
irqedge |= (GENMASK64(1, 0) << (pin * 2));
} else if (trig == GPIO_INT_TRIG_LOW) {
/* Set the falling-edge bit, clear the rising edge bit */
irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
irqedge |= (BIT64(1) << (pin * 2));
} else { /* GPIO_INT_TRIG_HIGH */
/* Set the rising-edge bit, clear the falling-edge bit */
irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
irqedge |= (BIT64(0) << (pin * 2));
}
}
irqedge = sys_cpu_to_le64(irqedge & BIT64_MASK(NUM_PINS * 2));
irqmask = sys_cpu_to_le32(irqmask & ALL_PINS);
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQMASK_PORT0, (uint8_t *)&irqmask, 3);
if (rc != 0) {
goto out;
}
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQEDGE_PORT0_A, (uint8_t *)&irqedge, 6);
out:
k_sem_give(drv_data->lock);
return rc;
}
static int gpio_pcal9722_manage_callback(const struct device *dev, struct gpio_callback *callback,
bool set)
{
struct pcal9722_drv_data *data = dev->data;
return gpio_manage_callback(&data->cb, callback, set);
}
static void gpio_pcal9722_handle_interrupt(const struct device *dev)
{
const struct pcal9722_config *cfg = dev->config;
struct pcal9722_drv_data *drv_data = dev->data;
int rc = 0;
uint32_t irq_status = 0;
k_sem_take(drv_data->lock, K_FOREVER);
rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQ_STAT_PORT0, (uint8_t *)&irq_status, 3);
if (rc) {
goto out;
}
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQ_CLEAR_PORT0, (uint8_t *)&irq_status, 3);
if (rc != 0) {
goto out;
}
irq_status = sys_le32_to_cpu(irq_status) & ALL_PINS;
out:
k_sem_give(drv_data->lock);
if ((rc == 0) && (irq_status)) {
gpio_fire_callbacks(&drv_data->cb, dev, irq_status);
}
}
static void gpio_pcal9722_work_handler(struct k_work *work)
{
struct pcal9722_drv_data *drv_data = CONTAINER_OF(work, struct pcal9722_drv_data, work);
gpio_pcal9722_handle_interrupt(drv_data->dev);
}
static void gpio_pcal9722_init_cb(const struct device *dev, struct gpio_callback *gpio_cb,
uint32_t pins)
{
struct pcal9722_drv_data *drv_data =
CONTAINER_OF(gpio_cb, struct pcal9722_drv_data, gpio_cb);
ARG_UNUSED(pins);
k_work_submit(&drv_data->work);
}
#endif /* PCAL9722_ENABLE_IRQ_SUPPORT */
static int gpio_pcal9722_init(const struct device *dev)
{
const struct pcal9722_config *cfg = dev->config;
#if PCAL9722_ENABLE_IRQ_SUPPORT
struct pcal9722_drv_data *drv_data = dev->data;
#endif
uint32_t pins = 0;
int rc = 0;
if (!device_is_ready(cfg->spi.bus)) {
LOG_ERR("SPI bus not ready");
goto out;
}
/* Clear interrupts */
pins = sys_cpu_to_le32(ALL_PINS);
rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQ_CLEAR_PORT0, (uint8_t *)&pins, 3);
if (rc != 0) {
goto out;
}
#if PCAL9722_ENABLE_IRQ_SUPPORT
if (!gpio_is_ready_dt(&cfg->gpio_int)) {
LOG_ERR("Interrupt GPIO not ready");
rc = -EINVAL;
goto out;
}
drv_data->dev = dev;
k_work_init(&drv_data->work, gpio_pcal9722_work_handler);
rc = gpio_pin_configure_dt(&cfg->gpio_int, GPIO_INPUT);
if (rc) {
goto out;
}
rc = gpio_pin_interrupt_configure_dt(&cfg->gpio_int, GPIO_INT_EDGE_TO_ACTIVE);
if (rc) {
goto out;
}
gpio_init_callback(&drv_data->gpio_cb, gpio_pcal9722_init_cb, BIT(cfg->gpio_int.pin));
rc = gpio_add_callback(cfg->gpio_int.port, &drv_data->gpio_cb);
if (rc) {
goto out;
}
#endif /* PCAL9722_ENABLE_IRQ_SUPPORT */
out:
if (rc) {
LOG_ERR("%s failed to initialize: %d", dev->name, rc);
}
return rc;
}
static DEVICE_API(gpio, api_table) = {
.pin_configure = gpio_pcal9722_config,
.port_get_raw = gpio_pcal9722_port_read,
.port_set_masked_raw = gpio_pcal9722_port_set_masked,
.port_set_bits_raw = gpio_pcal9722_port_set_bits,
.port_clear_bits_raw = gpio_pcal9722_port_clear_bits,
.port_toggle_bits = gpio_pcal9722_port_toggle_bits,
#if PCAL9722_ENABLE_IRQ_SUPPORT
.pin_interrupt_configure = gpio_pcal9722_pin_interrupt_configure,
.manage_callback = gpio_pcal9722_manage_callback,
#endif
};
#if PCAL9722_ENABLE_IRQ_SUPPORT
#define IRQ_GPIO(n) .gpio_int = GPIO_DT_SPEC_INST_GET(n, irq_gpios)
#else
#define IRQ_GPIO(n)
#endif
#define GPIO_PCAL9722_INIT(n) \
static const struct pcal9722_config pcal9722_cfg_##n = { \
.spi = SPI_DT_SPEC_INST_GET(n, SPI_OP_MODE_MASTER | SPI_WORD_SET(8)), \
.common = \
{ \
.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(n), \
}, \
.addr = DT_INST_PROP(n, addr), \
IRQ_GPIO(n)}; \
static K_SEM_DEFINE(pcal9722_drvdata_##n_lock, 1, 1); \
static struct pcal9722_drv_data pcal9722_drvdata_##n = { \
.lock = &pcal9722_drvdata_##n_lock}; \
DEVICE_DT_INST_DEFINE(n, gpio_pcal9722_init, NULL, &pcal9722_drvdata_##n, \
&pcal9722_cfg_##n, POST_KERNEL, CONFIG_GPIO_PCAL9722_INIT_PRIORITY, \
&api_table);
DT_INST_FOREACH_STATUS_OKAY(GPIO_PCAL9722_INIT)