blob: 81898e83382b40871ffe146221edf9dc325cc143 [file] [log] [blame]
/*
* Copyright (c) 2018 Peter Bigot Consulting, LLC
* Copyright (c) 2018 Aapo Vienamo
* Copyright (c) 2019 Nordic Semiconductor ASA
* Copyright (c) 2019 Vestas Wind Systems A/S
* Copyright (c) 2020 ZedBlox Ltd.
* Copyright (c) 2021 Laird Connectivity
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pca953x, CONFIG_GPIO_LOG_LEVEL);
#include <zephyr/drivers/gpio/gpio_utils.h>
/* PCA953X Register addresses */
#define PCA953X_INPUT_PORT 0x00
#define PCA953X_OUTPUT_PORT 0x01
#define PCA953X_CONFIGURATION 0x03
#define REG_INPUT_LATCH_PORT0 0x42
#define REG_INT_MASK_PORT0 0x45
/* Number of pins supported by the device */
#define NUM_PINS 8
/* Max to select all pins supported on the device. */
#define ALL_PINS ((uint8_t)BIT_MASK(NUM_PINS))
/** Cache of the output configuration and data of the pins. */
struct pca953x_pin_state {
uint8_t dir;
uint8_t input;
uint8_t output;
};
struct pca953x_irq_state {
uint8_t rising;
uint8_t falling;
};
/** Runtime driver data */
struct pca953x_drv_data {
/* gpio_driver_data needs to be first */
struct gpio_driver_data common;
struct pca953x_pin_state pin_state;
struct k_sem lock;
struct gpio_callback gpio_cb;
struct k_work work;
struct pca953x_irq_state irq_state;
const struct device *dev;
/* user ISR cb */
sys_slist_t cb;
};
/** Configuration data */
struct pca953x_config {
/* gpio_driver_config needs to be first */
struct gpio_driver_config common;
struct i2c_dt_spec i2c;
const struct gpio_dt_spec gpio_int;
bool interrupt_enabled;
int interrupt_mask;
int input_latch;
};
/**
* @brief Gets the state of input pins of the PCA953X I/O Port and
* stores in driver data struct.
*
* @param dev Pointer to the device structure for the driver instance.
*
* @retval 0 If successful.
* @retval Negative value for error code.
*/
static int update_input(const struct device *dev)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
uint8_t input_states;
int rc = 0;
rc = i2c_reg_read_byte_dt(&cfg->i2c, PCA953X_INPUT_PORT, &input_states);
if (rc == 0) {
drv_data->pin_state.input = input_states;
}
return rc;
}
/**
* @brief Handles interrupt triggered by the interrupt pin of PCA953X I/O Port.
*
* If nint_gpios is configured in device tree then this will be triggered each
* time a gpio configured as an input changes state. The gpio input states are
* read in this function which clears the interrupt.
*
* @param dev Pointer to the device structure for the driver instance.
*/
static void gpio_pca953x_handle_interrupt(const struct device *dev)
{
struct pca953x_drv_data *drv_data = dev->data;
struct pca953x_irq_state *irq_state = &drv_data->irq_state;
int rc;
uint8_t previous_state;
uint8_t current_state;
uint8_t transitioned_pins;
uint8_t interrupt_status = 0;
k_sem_take(&drv_data->lock, K_FOREVER);
/* Any interrupts enabled? */
if (!irq_state->rising && !irq_state->falling) {
rc = -EINVAL;
goto out;
}
/* Store previous input state then read new value */
previous_state = drv_data->pin_state.input;
rc = update_input(dev);
if (rc != 0) {
goto out;
}
/* Find out which input pins have changed state */
current_state = drv_data->pin_state.input;
transitioned_pins = previous_state ^ current_state;
/* Mask gpio transactions with rising/falling edge interrupt config */
interrupt_status = (irq_state->rising & transitioned_pins &
current_state);
interrupt_status |= (irq_state->falling & transitioned_pins &
previous_state);
out:
k_sem_give(&drv_data->lock);
if ((rc == 0) && (interrupt_status)) {
gpio_fire_callbacks(&drv_data->cb, dev, interrupt_status);
}
}
/**
* @brief Work handler for PCA953X interrupt
*
* @param work Work struct that contains pointer to interrupt handler function
*/
static void gpio_pca953x_work_handler(struct k_work *work)
{
struct pca953x_drv_data *drv_data =
CONTAINER_OF(work, struct pca953x_drv_data, work);
gpio_pca953x_handle_interrupt(drv_data->dev);
}
/**
* @brief ISR for interrupt pin of PCA953X
*
* @param dev Pointer to the device structure for the driver instance.
* @param gpio_cb Pointer to callback function struct
* @param pins Bitmask of pins that triggered interrupt
*/
static void gpio_pca953x_init_cb(const struct device *dev,
struct gpio_callback *gpio_cb, uint32_t pins)
{
struct pca953x_drv_data *drv_data =
CONTAINER_OF(gpio_cb, struct pca953x_drv_data, gpio_cb);
ARG_UNUSED(pins);
k_work_submit(&drv_data->work);
}
static int gpio_pca953x_config(const struct device *dev, gpio_pin_t pin,
gpio_flags_t flags)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
struct pca953x_pin_state *pins = &drv_data->pin_state;
int rc = 0;
bool data_first = false;
/* Can't do I2C 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;
}
/* The PCA953X has no internal pull up support */
if (((flags & GPIO_PULL_UP) != 0) || ((flags & GPIO_PULL_DOWN) != 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);
/* Ensure either Output or Input is specified */
if ((flags & GPIO_OUTPUT) != 0) {
pins->dir &= ~BIT(pin);
if ((flags & GPIO_OUTPUT_INIT_LOW) != 0) {
pins->output &= ~BIT(pin);
data_first = true;
} else if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0) {
pins->output |= BIT(pin);
data_first = true;
}
} else if ((flags & GPIO_INPUT) != 0) {
pins->dir |= BIT(pin);
} else {
rc = -ENOTSUP;
goto out;
}
/* Set output values */
if (data_first) {
rc = i2c_reg_write_byte_dt(&cfg->i2c, PCA953X_OUTPUT_PORT, pins->output);
}
if (rc == 0) {
/* Set pin directions */
rc = i2c_reg_write_byte_dt(&cfg->i2c, PCA953X_CONFIGURATION, pins->dir);
}
if (rc == 0) {
/* Refresh input status */
rc = update_input(dev);
}
out:
k_sem_give(&drv_data->lock);
return rc;
}
static int gpio_pca953x_port_read(const struct device *dev,
gpio_port_value_t *value)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
uint8_t input_pin_data;
int rc = 0;
/* Can't do I2C bus operations from an ISR */
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
/* Read Input Register */
rc = i2c_reg_read_byte_dt(&cfg->i2c, PCA953X_INPUT_PORT, &input_pin_data);
LOG_DBG("read %x got %d", input_pin_data, rc);
if (rc == 0) {
drv_data->pin_state.input = input_pin_data;
*value = (gpio_port_value_t)(drv_data->pin_state.input);
}
k_sem_give(&drv_data->lock);
return rc;
}
static int gpio_pca953x_port_write(const struct device *dev,
gpio_port_pins_t mask,
gpio_port_value_t value,
gpio_port_value_t toggle)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
uint8_t *outp = &drv_data->pin_state.output;
int rc;
uint8_t orig_out;
uint8_t out;
/* Can't do I2C bus operations from an ISR */
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
orig_out = *outp;
out = ((orig_out & ~mask) | (value & mask)) ^ toggle;
rc = i2c_reg_write_byte_dt(&cfg->i2c, PCA953X_OUTPUT_PORT, out);
if (rc == 0) {
*outp = out;
}
k_sem_give(&drv_data->lock);
LOG_DBG("write %x msk %08x val %08x => %x: %d", orig_out, mask,
value, out, rc);
return rc;
}
static int gpio_pca953x_port_set_masked(const struct device *dev,
gpio_port_pins_t mask,
gpio_port_value_t value)
{
return gpio_pca953x_port_write(dev, mask, value, 0);
}
static int gpio_pca953x_port_set_bits(const struct device *dev,
gpio_port_pins_t pins)
{
return gpio_pca953x_port_write(dev, pins, pins, 0);
}
static int gpio_pca953x_port_clear_bits(const struct device *dev,
gpio_port_pins_t pins)
{
return gpio_pca953x_port_write(dev, pins, 0, 0);
}
static int gpio_pca953x_port_toggle_bits(const struct device *dev,
gpio_port_pins_t pins)
{
return gpio_pca953x_port_write(dev, 0, 0, pins);
}
static int gpio_pca953x_pin_interrupt_configure(const struct device *dev,
gpio_pin_t pin,
enum gpio_int_mode mode,
enum gpio_int_trig trig)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
struct pca953x_irq_state *irq = &drv_data->irq_state;
if (!cfg->interrupt_enabled) {
return -ENOTSUP;
}
/* Device does not support level-triggered interrupts. */
if (mode == GPIO_INT_MODE_LEVEL) {
return -ENOTSUP;
}
k_sem_take(&drv_data->lock, K_FOREVER);
if (mode == GPIO_INT_MODE_DISABLED) {
irq->falling &= ~BIT(pin);
irq->rising &= ~BIT(pin);
} else { /* GPIO_INT_MODE_EDGE */
if (trig == GPIO_INT_TRIG_BOTH) {
irq->falling |= BIT(pin);
irq->rising |= BIT(pin);
} else if (trig == GPIO_INT_TRIG_LOW) {
irq->falling |= BIT(pin);
irq->rising &= ~BIT(pin);
} else if (trig == GPIO_INT_TRIG_HIGH) {
irq->falling &= ~BIT(pin);
irq->rising |= BIT(pin);
}
}
k_sem_give(&drv_data->lock);
return 0;
}
static int gpio_pca953x_manage_callback(const struct device *dev,
struct gpio_callback *callback,
bool set)
{
struct pca953x_drv_data *data = dev->data;
return gpio_manage_callback(&data->cb, callback, set);
}
/**
* @brief Initialization function of PCA953X
*
* This sets initial input/ output configuration and output states.
* The interrupt is configured if this is enabled.
*
* @param dev Device struct
* @return 0 if successful, failed otherwise.
*/
static int gpio_pca953x_init(const struct device *dev)
{
const struct pca953x_config *cfg = dev->config;
struct pca953x_drv_data *drv_data = dev->data;
int rc = 0;
if (!device_is_ready(cfg->i2c.bus)) {
LOG_ERR("I2C bus device not found");
goto out;
}
/* Do an initial read, this clears the interrupt pin and sets
* up the initial value of the pin state input data.
*/
rc = update_input(dev);
if (rc) {
goto out;
}
if (cfg->interrupt_enabled) {
if (!gpio_is_ready_dt(&cfg->gpio_int)) {
LOG_ERR("Cannot get pointer to gpio interrupt device");
rc = -EINVAL;
goto out;
}
drv_data->dev = dev;
k_work_init(&drv_data->work, gpio_pca953x_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_pca953x_init_cb,
BIT(cfg->gpio_int.pin));
rc = gpio_add_callback(cfg->gpio_int.port,
&drv_data->gpio_cb);
if (rc) {
goto out;
}
/* This may not present on all variants of device */
if (cfg->input_latch > -1) {
rc = i2c_reg_write_byte_dt(&cfg->i2c, REG_INPUT_LATCH_PORT0,
cfg->input_latch);
}
if (cfg->interrupt_mask > -1) {
rc = i2c_reg_write_byte_dt(&cfg->i2c, REG_INT_MASK_PORT0,
cfg->interrupt_mask);
}
}
out:
if (rc) {
LOG_ERR("%s init failed: %d", dev->name, rc);
} else {
LOG_INF("%s init ok", dev->name);
}
return rc;
}
static const struct gpio_driver_api api_table = {
.pin_configure = gpio_pca953x_config,
.port_get_raw = gpio_pca953x_port_read,
.port_set_masked_raw = gpio_pca953x_port_set_masked,
.port_set_bits_raw = gpio_pca953x_port_set_bits,
.port_clear_bits_raw = gpio_pca953x_port_clear_bits,
.port_toggle_bits = gpio_pca953x_port_toggle_bits,
.pin_interrupt_configure = gpio_pca953x_pin_interrupt_configure,
.manage_callback = gpio_pca953x_manage_callback,
};
#define GPIO_PCA953X_INIT(n) \
static const struct pca953x_config pca953x_cfg_##n = { \
.i2c = I2C_DT_SPEC_INST_GET(n), \
.common = { \
.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(n), \
}, \
.interrupt_enabled = DT_INST_NODE_HAS_PROP(n, nint_gpios), \
.gpio_int = GPIO_DT_SPEC_INST_GET_OR(n, nint_gpios, {0}), \
.interrupt_mask = DT_INST_PROP_OR(n, interrupt_mask, -1), \
.input_latch = DT_INST_PROP_OR(n, input_latch, -1), \
}; \
\
static struct pca953x_drv_data pca953x_drvdata_##n = { \
.lock = Z_SEM_INITIALIZER(pca953x_drvdata_##n.lock, 1, 1), \
.pin_state.dir = ALL_PINS, \
.pin_state.output = ALL_PINS, \
}; \
DEVICE_DT_INST_DEFINE(n, \
gpio_pca953x_init, \
NULL, \
&pca953x_drvdata_##n, \
&pca953x_cfg_##n, \
POST_KERNEL, \
CONFIG_GPIO_PCA953X_INIT_PRIORITY, \
&api_table);
#define DT_DRV_COMPAT ti_tca9538
DT_INST_FOREACH_STATUS_OKAY(GPIO_PCA953X_INIT)