| /* |
| * 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) |