509 lines
13 KiB
C
509 lines
13 KiB
C
/*
|
|
* 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)
|