blob: 0c349e6380a1f250ea8dd5541c19158f15fc9b1e [file] [log] [blame]
/*
* Copyright (c) 2021 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT nxp_pcal6408a
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include "gpio_utils.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pcal6408a, CONFIG_GPIO_LOG_LEVEL);
enum {
PCAL6408A_REG_INPUT_PORT = 0x00,
PCAL6408A_REG_OUTPUT_PORT = 0x01,
PCAL6408A_REG_POLARITY_INVERSION = 0x02,
PCAL6408A_REG_CONFIGURATION = 0x03,
PCAL6408A_REG_OUTPUT_DRIVE_STRENGTH_0 = 0x40,
PCAL6408A_REG_OUTPUT_DRIVE_STRENGTH_1 = 0x41,
PCAL6408A_REG_INPUT_LATCH = 0x42,
PCAL6408A_REG_PULL_UP_DOWN_ENABLE = 0x43,
PCAL6408A_REG_PULL_UP_DOWN_SELECT = 0x44,
PCAL6408A_REG_INTERRUPT_MASK = 0x45,
PCAL6408A_REG_INTERRUPT_STATUS = 0x46,
PCAL6408A_REG_OUTPUT_PORT_CONFIGURATION = 0x4f,
};
struct pcal6408a_pins_cfg {
uint8_t configured_as_inputs;
uint8_t outputs_high;
uint8_t pull_ups_selected;
uint8_t pulls_enabled;
};
struct pcal6408a_triggers {
uint8_t masked;
uint8_t dual_edge;
uint8_t on_low;
};
struct pcal6408a_drv_data {
/* gpio_driver_data needs to be first */
struct gpio_driver_data common;
sys_slist_t callbacks;
struct k_sem lock;
struct k_work work;
const struct device *dev;
struct gpio_callback int_gpio_cb;
struct pcal6408a_pins_cfg pins_cfg;
struct pcal6408a_triggers triggers;
uint8_t input_port_last;
};
struct pcal6408a_drv_cfg {
/* gpio_driver_config needs to be first */
struct gpio_driver_config common;
struct i2c_dt_spec i2c;
const struct device *int_gpio_dev;
gpio_pin_t int_gpio_pin;
gpio_dt_flags_t int_gpio_flags;
const struct device *reset_gpio_dev;
gpio_pin_t reset_gpio_pin;
gpio_dt_flags_t reset_gpio_flags;
};
static int pcal6408a_pins_cfg_apply(const struct device *dev,
struct pcal6408a_pins_cfg pins_cfg)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
int rc;
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_PULL_UP_DOWN_SELECT,
pins_cfg.pull_ups_selected);
if (rc != 0) {
LOG_ERR("%s: failed to select pull-up/pull-down resistors: %d",
dev->name, rc);
return -EIO;
}
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_PULL_UP_DOWN_ENABLE,
pins_cfg.pulls_enabled);
if (rc != 0) {
LOG_ERR("%s: failed to enable pull-up/pull-down resistors: %d",
dev->name, rc);
return -EIO;
}
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_OUTPUT_PORT, pins_cfg.outputs_high);
if (rc != 0) {
LOG_ERR("%s: failed to set outputs: %d",
dev->name, rc);
return -EIO;
}
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_CONFIGURATION,
pins_cfg.configured_as_inputs);
if (rc != 0) {
LOG_ERR("%s: failed to configure pins: %d",
dev->name, rc);
return -EIO;
}
return 0;
}
static int pcal6408a_pin_configure(const struct device *dev,
gpio_pin_t pin,
gpio_flags_t flags)
{
struct pcal6408a_drv_data *drv_data = dev->data;
struct pcal6408a_pins_cfg pins_cfg;
gpio_flags_t flags_io;
int rc;
/* This device does not support open-source outputs, and open-drain
* outputs can be only configured port-wise.
*/
if ((flags & GPIO_SINGLE_ENDED) != 0) {
return -ENOTSUP;
}
/* Pins in this device can be either inputs or outputs and cannot be
* completely disconnected.
*/
flags_io = (flags & (GPIO_INPUT | GPIO_OUTPUT));
if (flags_io == (GPIO_INPUT | GPIO_OUTPUT) ||
flags_io == GPIO_DISCONNECTED) {
return -ENOTSUP;
}
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
pins_cfg = drv_data->pins_cfg;
if ((flags & (GPIO_PULL_UP | GPIO_PULL_DOWN)) != 0) {
if ((flags & GPIO_PULL_UP) != 0) {
pins_cfg.pull_ups_selected |= BIT(pin);
} else {
pins_cfg.pull_ups_selected &= ~BIT(pin);
}
pins_cfg.pulls_enabled |= BIT(pin);
} else {
pins_cfg.pulls_enabled &= ~BIT(pin);
}
if ((flags & GPIO_OUTPUT) != 0) {
if ((flags & GPIO_OUTPUT_INIT_LOW) != 0) {
pins_cfg.outputs_high &= ~BIT(pin);
} else if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0) {
pins_cfg.outputs_high |= BIT(pin);
}
pins_cfg.configured_as_inputs &= ~BIT(pin);
} else {
pins_cfg.configured_as_inputs |= BIT(pin);
}
rc = pcal6408a_pins_cfg_apply(dev, pins_cfg);
if (rc == 0) {
drv_data->pins_cfg = pins_cfg;
}
k_sem_give(&drv_data->lock);
return rc;
}
static int pcal6408a_process_input(const struct device *dev,
gpio_port_value_t *value)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
struct pcal6408a_drv_data *drv_data = dev->data;
int rc;
uint8_t int_sources;
uint8_t input_port;
rc = i2c_reg_read_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_INTERRUPT_STATUS, &int_sources);
if (rc != 0) {
LOG_ERR("%s: failed to read interrupt sources: %d",
dev->name, rc);
return -EIO;
}
/* This read also clears the generated interrupt if any. */
rc = i2c_reg_read_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_INPUT_PORT, &input_port);
if (rc != 0) {
LOG_ERR("%s: failed to read input port: %d",
dev->name, rc);
return -EIO;
}
if (value) {
*value = input_port;
}
/* It may happen that some inputs change their states between above
* reads of the interrupt status and input port registers. Such changes
* will not be noted in `int_sources`, thus to correctly detect them,
* the current state of inputs needs to be additionally compared with
* the one read last time, and any differences need to be added to
* `int_sources`.
*/
int_sources |= ((input_port ^ drv_data->input_port_last)
& ~drv_data->triggers.masked);
drv_data->input_port_last = input_port;
if (int_sources) {
uint8_t dual_edge_triggers = drv_data->triggers.dual_edge;
uint8_t falling_edge_triggers =
(~dual_edge_triggers & drv_data->triggers.on_low);
uint8_t fired_triggers = 0;
/* For dual edge triggers, react to all state changes. */
fired_triggers |= (int_sources & dual_edge_triggers);
/* For single edge triggers, fire callbacks only for the pins
* that transitioned to their configured target state (0 for
* falling edges, 1 otherwise, hence the XOR operation below).
*/
fired_triggers |= ((input_port & int_sources)
^ falling_edge_triggers);
gpio_fire_callbacks(&drv_data->callbacks, dev, fired_triggers);
}
return 0;
}
static void pcal6408a_work_handler(struct k_work *work)
{
struct pcal6408a_drv_data *drv_data = CONTAINER_OF(work,
struct pcal6408a_drv_data, work);
k_sem_take(&drv_data->lock, K_FOREVER);
(void)pcal6408a_process_input(drv_data->dev, NULL);
k_sem_give(&drv_data->lock);
}
static void pcal6408a_int_gpio_handler(const struct device *dev,
struct gpio_callback *gpio_cb,
uint32_t pins)
{
ARG_UNUSED(dev);
ARG_UNUSED(pins);
struct pcal6408a_drv_data *drv_data = CONTAINER_OF(gpio_cb,
struct pcal6408a_drv_data, int_gpio_cb);
k_work_submit(&drv_data->work);
}
static int pcal6408a_port_get_raw(const struct device *dev,
gpio_port_value_t *value)
{
struct pcal6408a_drv_data *drv_data = dev->data;
int rc;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
/* Reading of the input port also clears the generated interrupt,
* thus the configured callbacks must be fired also here if needed.
*/
rc = pcal6408a_process_input(dev, value);
k_sem_give(&drv_data->lock);
return rc;
}
static int pcal6408a_port_set_raw(const struct device *dev,
uint8_t mask,
uint8_t value,
uint8_t toggle)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
struct pcal6408a_drv_data *drv_data = dev->data;
int rc;
uint8_t output;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
output = (drv_data->pins_cfg.outputs_high & ~mask);
output |= (value & mask);
output ^= toggle;
/*
* No need to limit `out` to only pins configured as outputs,
* as the chip anyway ignores all other bits in the register.
*/
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_OUTPUT_PORT, output);
if (rc == 0) {
drv_data->pins_cfg.outputs_high = output;
}
k_sem_give(&drv_data->lock);
if (rc != 0) {
LOG_ERR("%s: failed to write output port: %d", dev->name, rc);
return -EIO;
}
return 0;
}
static int pcal6408a_port_set_masked_raw(const struct device *dev,
gpio_port_pins_t mask,
gpio_port_value_t value)
{
return pcal6408a_port_set_raw(dev, (uint8_t)mask, (uint8_t)value, 0);
}
static int pcal6408a_port_set_bits_raw(const struct device *dev,
gpio_port_pins_t pins)
{
return pcal6408a_port_set_raw(dev, (uint8_t)pins, (uint8_t)pins, 0);
}
static int pcal6408a_port_clear_bits_raw(const struct device *dev,
gpio_port_pins_t pins)
{
return pcal6408a_port_set_raw(dev, (uint8_t)pins, 0, 0);
}
static int pcal6408a_port_toggle_bits(const struct device *dev,
gpio_port_pins_t pins)
{
return pcal6408a_port_set_raw(dev, 0, 0, (uint8_t)pins);
}
static int pcal6408a_triggers_apply(const struct device *dev,
struct pcal6408a_triggers triggers)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
int rc;
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_INPUT_LATCH, ~(triggers.masked));
if (rc != 0) {
LOG_ERR("%s: failed to configure input latch: %d",
dev->name, rc);
return -EIO;
}
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_INTERRUPT_MASK, triggers.masked);
if (rc != 0) {
LOG_ERR("%s: failed to configure interrupt mask: %d",
dev->name, rc);
return -EIO;
}
return 0;
}
static int pcal6408a_pin_interrupt_configure(const struct device *dev,
gpio_pin_t pin,
enum gpio_int_mode mode,
enum gpio_int_trig trig)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
struct pcal6408a_drv_data *drv_data = dev->data;
struct pcal6408a_triggers triggers;
int rc;
if (!drv_cfg->int_gpio_dev) {
return -ENOTSUP;
}
/* This device supports only edge-triggered interrupts. */
if (mode == GPIO_INT_MODE_LEVEL) {
return -ENOTSUP;
}
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
triggers = drv_data->triggers;
if (mode == GPIO_INT_MODE_DISABLED) {
triggers.masked |= BIT(pin);
} else {
triggers.masked &= ~BIT(pin);
}
if (trig == GPIO_INT_TRIG_BOTH) {
triggers.dual_edge |= BIT(pin);
} else {
triggers.dual_edge &= ~BIT(pin);
if (trig == GPIO_INT_TRIG_LOW) {
triggers.on_low |= BIT(pin);
} else {
triggers.on_low &= ~BIT(pin);
}
}
rc = pcal6408a_triggers_apply(dev, triggers);
if (rc == 0) {
drv_data->triggers = triggers;
}
k_sem_give(&drv_data->lock);
return rc;
}
static int pcal6408a_manage_callback(const struct device *dev,
struct gpio_callback *callback,
bool set)
{
struct pcal6408a_drv_data *drv_data = dev->data;
return gpio_manage_callback(&drv_data->callbacks, callback, set);
}
static int pcal6408a_init(const struct device *dev)
{
const struct pcal6408a_drv_cfg *drv_cfg = dev->config;
struct pcal6408a_drv_data *drv_data = dev->data;
const struct pcal6408a_pins_cfg initial_pins_cfg = {
.configured_as_inputs = 0xff,
.outputs_high = 0,
.pull_ups_selected = 0,
.pulls_enabled = 0,
};
const struct pcal6408a_triggers initial_triggers = {
.masked = 0xff,
};
int rc;
if (!device_is_ready(drv_cfg->i2c.bus)) {
LOG_ERR("%s is not ready", drv_cfg->i2c.bus->name);
return -ENODEV;
}
/* If the RESET line is available, use it to reset the expander.
* Otherwise, write reset values to registers that are not used by
* this driver.
*/
if (drv_cfg->reset_gpio_dev) {
if (!device_is_ready(drv_cfg->reset_gpio_dev)) {
LOG_ERR("%s is not ready",
drv_cfg->reset_gpio_dev->name);
return -ENODEV;
}
rc = gpio_pin_configure(drv_cfg->reset_gpio_dev,
drv_cfg->reset_gpio_pin,
drv_cfg->reset_gpio_flags |
GPIO_OUTPUT_ACTIVE);
if (rc != 0) {
LOG_ERR("%s: failed to configure RESET line: %d",
dev->name, rc);
return -EIO;
}
/* RESET signal needs to be active for a minimum of 30 ns. */
k_busy_wait(1);
rc = gpio_pin_set(drv_cfg->reset_gpio_dev,
drv_cfg->reset_gpio_pin, 0);
if (rc != 0) {
LOG_ERR("%s: failed to deactivate RESET line: %d",
dev->name, rc);
return -EIO;
}
/* Give the expander at least 200 ns to recover after reset. */
k_busy_wait(1);
} else {
static const uint8_t reset_state[][2] = {
{ PCAL6408A_REG_POLARITY_INVERSION, 0 },
{ PCAL6408A_REG_OUTPUT_DRIVE_STRENGTH_0, 0xff },
{ PCAL6408A_REG_OUTPUT_DRIVE_STRENGTH_1, 0xff },
{ PCAL6408A_REG_OUTPUT_PORT_CONFIGURATION, 0 },
};
for (int i = 0; i < ARRAY_SIZE(reset_state); ++i) {
rc = i2c_reg_write_byte_dt(&drv_cfg->i2c, reset_state[i][0],
reset_state[i][1]);
if (rc != 0) {
LOG_ERR("%s: failed to reset register %02x: %d",
dev->name, reset_state[i][0], rc);
return -EIO;
}
}
}
/* Set initial configuration of the pins. */
rc = pcal6408a_pins_cfg_apply(dev, initial_pins_cfg);
if (rc != 0) {
return rc;
}
drv_data->pins_cfg = initial_pins_cfg;
/* Read initial state of the input port register. */
rc = i2c_reg_read_byte_dt(&drv_cfg->i2c, PCAL6408A_REG_INPUT_PORT,
&drv_data->input_port_last);
if (rc != 0) {
LOG_ERR("%s: failed to initially read input port: %d",
dev->name, rc);
return -EIO;
}
/* Set initial state of the interrupt related registers. */
rc = pcal6408a_triggers_apply(dev, initial_triggers);
if (rc != 0) {
return rc;
}
drv_data->triggers = initial_triggers;
/* If the INT line is available, configure the callback for it. */
if (drv_cfg->int_gpio_dev) {
if (!device_is_ready(drv_cfg->int_gpio_dev)) {
LOG_ERR("%s is not ready",
drv_cfg->int_gpio_dev->name);
return -ENODEV;
}
rc = gpio_pin_configure(drv_cfg->int_gpio_dev,
drv_cfg->int_gpio_pin,
drv_cfg->int_gpio_flags | GPIO_INPUT);
if (rc != 0) {
LOG_ERR("%s: failed to configure INT line: %d",
dev->name, rc);
return -EIO;
}
rc = gpio_pin_interrupt_configure(drv_cfg->int_gpio_dev,
drv_cfg->int_gpio_pin,
GPIO_INT_EDGE_TO_ACTIVE);
if (rc != 0) {
LOG_ERR("%s: failed to configure INT interrupt: %d",
dev->name, rc);
return -EIO;
}
gpio_init_callback(&drv_data->int_gpio_cb,
pcal6408a_int_gpio_handler,
BIT(drv_cfg->int_gpio_pin));
rc = gpio_add_callback(drv_cfg->int_gpio_dev,
&drv_data->int_gpio_cb);
if (rc != 0) {
LOG_ERR("%s: failed to add INT callback: %d",
dev->name, rc);
return -EIO;
}
}
/* Device configured, unlock it so that it can be used. */
k_sem_give(&drv_data->lock);
return 0;
}
static const struct gpio_driver_api pcal6408a_drv_api = {
.pin_configure = pcal6408a_pin_configure,
.port_get_raw = pcal6408a_port_get_raw,
.port_set_masked_raw = pcal6408a_port_set_masked_raw,
.port_set_bits_raw = pcal6408a_port_set_bits_raw,
.port_clear_bits_raw = pcal6408a_port_clear_bits_raw,
.port_toggle_bits = pcal6408a_port_toggle_bits,
.pin_interrupt_configure = pcal6408a_pin_interrupt_configure,
.manage_callback = pcal6408a_manage_callback,
};
#define INIT_INT_GPIO_FIELDS(idx) \
COND_CODE_1(DT_INST_NODE_HAS_PROP(idx, int_gpios), \
( \
.int_gpio_dev = DEVICE_DT_GET( \
DT_GPIO_CTLR(DT_DRV_INST(idx), int_gpios)), \
.int_gpio_pin = DT_INST_GPIO_PIN(idx, int_gpios), \
.int_gpio_flags = DT_INST_GPIO_FLAGS(idx, int_gpios), \
), ())
#define INIT_RESET_GPIO_FIELDS(idx) \
COND_CODE_1(DT_INST_NODE_HAS_PROP(idx, reset_gpios), \
( \
.reset_gpio_dev = DEVICE_DT_GET( \
DT_GPIO_CTLR(DT_DRV_INST(idx), reset_gpios)), \
.reset_gpio_pin = DT_INST_GPIO_PIN(idx, reset_gpios), \
.reset_gpio_flags = DT_INST_GPIO_FLAGS(idx, reset_gpios), \
), ())
#define GPIO_PCAL6408A_INST(idx) \
static const struct pcal6408a_drv_cfg pcal6408a_cfg##idx = { \
.common = { \
.port_pin_mask = \
GPIO_PORT_PIN_MASK_FROM_DT_INST(idx), \
}, \
.i2c = I2C_DT_SPEC_INST_GET(idx), \
INIT_INT_GPIO_FIELDS(idx) \
INIT_RESET_GPIO_FIELDS(idx) \
}; \
static struct pcal6408a_drv_data pcal6408a_data##idx = { \
.lock = Z_SEM_INITIALIZER(pcal6408a_data##idx.lock, 1, 1), \
.work = Z_WORK_INITIALIZER(pcal6408a_work_handler), \
.dev = DEVICE_DT_INST_GET(idx), \
}; \
DEVICE_DT_INST_DEFINE(idx, pcal6408a_init, \
NULL, \
&pcal6408a_data##idx, &pcal6408a_cfg##idx, \
POST_KERNEL, \
CONFIG_GPIO_PCAL6408A_INIT_PRIORITY, \
&pcal6408a_drv_api);
DT_INST_FOREACH_STATUS_OKAY(GPIO_PCAL6408A_INST)