nuttx/drivers/ioexpander/pcf8575.c
Michal Lenc bf90361895 ioexpander: add support for pcf8575 expander
This adds support for 16 bit io expander pcf8575. The driver is basically
the same as for pcf8574 however it is for 16 bits instead of 8 bits. Since
pcf8575 and pcf8574 can be both used on the same i2c bus the driver for
pcf8575 is implemented in a different file to reduce the code size
and complexity and to have less if statements in the driver.

Signed-off-by: Michal Lenc <michallenc@seznam.cz>
2023-08-03 06:52:27 -07:00

682 lines
19 KiB
C

/****************************************************************************
* drivers/ioexpander/pcf8575.c
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>
#include <sys/param.h>
#include <nuttx/kmalloc.h>
#include <nuttx/ioexpander/ioexpander.h>
#include <nuttx/ioexpander/pcf8575.h>
#include "pcf8575.h"
#ifdef CONFIG_IOEXPANDER_PCF8575
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
# error PCF8575 cannot work with CONFIG_IOEXPANDER_INT_ENABLE
#endif
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
/* PCF8575xx Helpers */
static int pcf8575_read(FAR struct pcf8575_dev_s *priv,
FAR uint16_t *portval);
static int pcf8575_write(struct pcf8575_dev_s *priv, uint16_t portval);
/* I/O Expander Methods */
static int pcf8575_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
int dir);
static int pcf8575_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
int opt, void *regval);
static int pcf8575_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
bool value);
static int pcf8575_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
FAR bool *value);
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int pcf8575_multiwritepin(FAR struct ioexpander_dev_s *dev,
FAR uint8_t *pins, FAR bool *values, int count);
static int pcf8575_multireadpin(FAR struct ioexpander_dev_s *dev,
FAR uint8_t *pins, FAR bool *values, int count);
#endif
/****************************************************************************
* Private Data
****************************************************************************/
#ifndef CONFIG_PCF8575_MULTIPLE
/* If only a single device is supported, then the driver state structure may
* as well be pre-allocated.
*/
static struct pcf8575_dev_s g_pcf8575;
#endif
/* I/O expander vtable */
static const struct ioexpander_ops_s g_pcf8575_ops =
{
pcf8575_direction,
pcf8575_option,
pcf8575_writepin,
pcf8575_readpin,
pcf8575_readpin
#ifdef CONFIG_IOEXPANDER_MULTIPIN
, pcf8575_multiwritepin
, pcf8575_multireadpin
, pcf8575_multireadpin
#endif
};
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: pcf8575_read
*
* Description:
* Read the PCF8575 16-bit value from a PCF8575xx port
*
* Primitive I2C read operation for the PCF8575. The PCF8575 is
* 'interesting' in that it doesn't really have a data direction register,
* but instead the outputs are current-limited when high, so by setting an
* IO line high, you are also making it an input. Consequently, before
* using this method, you'll need to perform a pca8574_write() setting the
* bits you are interested in reading to 1's, then call this method.
*
****************************************************************************/
static int pcf8575_read(FAR struct pcf8575_dev_s *priv,
FAR uint16_t *portval)
{
struct i2c_msg_s msg;
uint8_t buffer[2];
int ret;
DEBUGASSERT(priv != NULL && priv->i2c != NULL && priv->config != NULL);
/* Setup for the transfer */
msg.frequency = priv->config->frequency,
msg.addr = priv->config->address,
msg.flags = I2C_M_READ;
msg.buffer = buffer;
msg.length = 2;
/* Then perform the transfer. */
ret = I2C_TRANSFER(priv->i2c, &msg, 1);
*portval = buffer[0] & 0xff;
*portval |= (buffer[1] << 8) & 0xff00;
return (ret >= 0) ? OK : ret;
}
/****************************************************************************
* Name: pcf8575_write
*
* Description:
* Write a 16-bit value to a PCF8575xx port
*
* Primitive I2C write operation for the PCA8574. The I2C interface
* simply sets the state of the 8 IO lines in the PCA8574 port.
*
****************************************************************************/
static int pcf8575_write(struct pcf8575_dev_s *priv, uint16_t portval)
{
struct i2c_msg_s msg;
uint8_t buffer[2];
int ret;
DEBUGASSERT(priv != NULL && priv->i2c != NULL && priv->config != NULL);
/* Setup for the transfer */
buffer[0] = (uint8_t)(portval & 0xff);
buffer[1] = (uint8_t)((portval & 0xff00) >> 8);
msg.frequency = priv->config->frequency,
msg.addr = priv->config->address;
msg.flags = 0;
msg.buffer = buffer;
msg.length = 2;
/* Then perform the transfer. */
ret = I2C_TRANSFER(priv->i2c, &msg, 1);
return (ret >= 0) ? OK : ret;
}
/****************************************************************************
* Name: pcf8575_direction
*
* Description:
* Set the direction of an ioexpander pin. Required.
*
* The PCF8575 is 'interesting' in that it doesn't really have a data
* direction register, but instead the outputs are current-limited when
* high, so by setting an IO line high, you are also making it an input.
* Consequently, before using this method, you'll need to perform a
* pcf8575_write() setting the bits you are interested in reading to 1's,
* before calling pcf8575_read().
*
* Input Parameters:
* dev - Device-specific state data
* pin - The index of the pin to alter in this call
* dir - One of the IOEXPANDER_DIRECTION_ macros
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
static int pcf8575_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
int direction)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
int ret;
if (direction != IOEXPANDER_DIRECTION_IN &&
direction != IOEXPANDER_DIRECTION_OUT)
{
return -EINVAL;
}
if (pin > 15)
{
return -ENXIO;
}
DEBUGASSERT(priv != NULL && priv->config != NULL);
gpioinfo("I2C addr=%02x pin=%u direction=%s\n",
priv->config->address, pin,
(direction == IOEXPANDER_DIRECTION_IN) ? "IN" : "OUT");
/* Get exclusive access to the I/O Expander */
ret = nxmutex_lock(&priv->lock);
if (ret < 0)
{
return ret;
}
/* Set a bit in inpins if the pin is an input. Clear the bit in
* inpins if the pin is an output.
*/
if (direction == IOEXPANDER_DIRECTION_IN)
{
priv->inpins |= (1 << pin);
priv->outstate &= ~(1 << pin);
}
else
{
priv->inpins &= ~(1 << pin);
}
/* Write the OR of the set of input pins and the set of output pins.
* In order to read input pins, we have to write a '1' to put the
* pin in the current limiting state.
*/
ret = pcf8575_write(priv, priv->inpins | priv->outstate);
nxmutex_unlock(&priv->lock);
return ret;
}
/****************************************************************************
* Name: pcf8575_option
*
* Description:
* Set pin options. Required.
* Since all IO expanders have various pin options, this API allows setting
* pin options in a flexible way.
*
* Input Parameters:
* dev - Device-specific state data
* pin - The index of the pin to alter in this call
* opt - One of the IOEXPANDER_OPTION_ macros
* val - The option's value
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
static int pcf8575_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
int opt, FAR void *value)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
int ret = OK;
DEBUGASSERT(priv != NULL && priv->config != NULL);
gpioinfo("I2C addr=%02x pin=%u option=%u\n",
priv->config->address, pin, opt);
return ret;
}
/****************************************************************************
* Name: pcf8575_writepin
*
* Description:
* Set the pin level. Required.
*
* Input Parameters:
* dev - Device-specific state data
* pin - The index of the pin to alter in this call
* val - The pin level. Usually TRUE will set the pin high,
* except if OPTION_INVERT has been set on this pin.
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
static int pcf8575_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
bool value)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
int ret;
if (pin > 15)
{
return -ENXIO;
}
DEBUGASSERT(priv != NULL && priv->config != NULL);
gpioinfo("I2C addr=%02x pin=%u value=%u\n",
priv->config->address, pin, value);
/* Get exclusive access to the I/O Expander */
ret = nxmutex_lock(&priv->lock);
if (ret < 0)
{
return ret;
}
/* Make sure that this is an output pin */
if ((priv->inpins & (1 << pin)) != 0)
{
gpioerr("ERROR: pin%u is an input\n", pin);
nxmutex_unlock(&priv->lock);
return -EINVAL;
}
/* Set/clear a bit in outstate. */
if (value)
{
priv->outstate |= (1 << pin);
}
else
{
priv->outstate &= ~(1 << pin);
}
/* Write the OR of the set of input pins and the set of output pins.
* In order to set the new output value.
*/
ret = pcf8575_write(priv, priv->inpins | priv->outstate);
nxmutex_unlock(&priv->lock);
return ret;
}
/****************************************************************************
* Name: pcf8575_readpin
*
* Description:
* Read the actual PIN level. This can be different from the last value
* written to this pin. Required.
*
* The PCF8575 is 'interesting' in that it doesn't really have a data
* direction register, but instead the outputs are current-limited when
* high, so by setting an IO line high, you are also making it an input.
* Consequently, before using this method, you'll need to perform a
* pca8574_write() setting the bits you are interested in reading to 1's,
* before calling pca8574_read().
*
* Input Parameters:
* dev - Device-specific state data
* pin - The index of the pin
* valptr - Pointer to a buffer where the pin level is stored. Usually TRUE
* if the pin is high, except if OPTION_INVERT has been set on
* this pin.
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
static int pcf8575_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
FAR bool *value)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
uint16_t regval;
int ret;
if (pin > 15)
{
return -ENXIO;
}
DEBUGASSERT(priv != NULL && priv->config != NULL && value != NULL);
gpioinfo("I2C addr=%02x, pin=%u\n", priv->config->address, pin);
/* Get exclusive access to the I/O Expander */
ret = nxmutex_lock(&priv->lock);
if (ret < 0)
{
return ret;
}
/* Is the pin an output? */
if ((priv->inpins & (1 << pin)) == 0)
{
/* We cannot read the value on pin directly. Just Return the last
* value that we wrote to the pin.
*/
*value = ((priv->outstate & (1 << pin)) != 0);
nxmutex_unlock(&priv->lock);
return OK;
}
/* It is an input pin. Read the input register for this pin
*
* The Input Port Register reflects the incoming logic levels of the pins,
* regardless of whether the pin is defined as an input or an output by
* the Configuration Register. They act only on read operation.
*/
ret = pcf8575_read(priv, &regval);
if (ret < 0)
{
gpioerr("ERROR: Failed to read port register: %d\n", ret);
goto errout_with_lock;
}
/* Return 0 or 1 to indicate the state of pin */
*value = (bool)((regval >> (pin & 0xf)) & 1);
ret = OK;
errout_with_lock:
nxmutex_unlock(&priv->lock);
return ret;
}
/****************************************************************************
* Name: pcf8575_multiwritepin
*
* Description:
* Set the pin level for multiple pins. This routine may be faster than
* individual pin accesses. Optional.
*
* Input Parameters:
* dev - Device-specific state data
* pins - The list of pin indexes to alter in this call
* val - The list of pin levels.
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int pcf8575_multiwritepin(FAR struct ioexpander_dev_s *dev,
FAR uint8_t *pins, FAR bool *values,
int count)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
uint8_t pin;
int ret;
int i;
DEBUGASSERT(priv != NULL && priv->config != NULL &&
pins != NULL && values != NULL);
gpioinfo("I2C addr=%02x count=%d\n", priv->config->address, count);
/* Get exclusive access to the I/O Expander */
ret = nxmutex_lock(&priv->lock);
if (ret < 0)
{
return ret;
}
/* Process each pin setting */
for (i = 0; i < count; i++)
{
/* Make sure that this is an output pin */
pin = pins[i];
if (pin > 15)
{
nxmutex_unlock(&priv->lock);
return -ENXIO;
}
gpioinfo("%d. pin=%u value=%u\n", pin, values[i]);
if ((priv->inpins & (1 << pin)) != 0)
{
gpioerr("ERROR: pin%u is an input\n", pin);
continue;
}
/* Set/clear a bit in outstate. */
if (values[i])
{
priv->outstate |= (1 << pin);
}
else
{
priv->outstate &= ~(1 << pin);
}
}
/* Write the OR of the set of input pins and the set of output pins.
* In order to set the new output value.
*/
ret = pcf8575_write(priv, priv->inpins | priv->outstate);
nxmutex_unlock(&priv->lock);
return ret;
}
#endif
/****************************************************************************
* Name: pcf8575_multireadpin
*
* Description:
* Read the actual level for multiple pins. This routine may be faster than
* individual pin accesses. Optional.
*
* Input Parameters:
* dev - Device-specific state data
* pin - The list of pin indexes to read
* valptr - Pointer to a buffer where the pin levels are stored.
*
* Returned Value:
* 0 on success, else a negative error code
*
****************************************************************************/
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int pcf8575_multireadpin(FAR struct ioexpander_dev_s *dev,
FAR uint8_t *pins, FAR bool *values,
int count)
{
FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev;
uint8_t regval;
uint8_t pin;
int ret;
int i;
DEBUGASSERT(priv != NULL && priv->config != NULL &&
pins != NULL && values != NULL);
gpioinfo("I2C addr=%02x, count=%d\n", priv->config->address, count);
/* Get exclusive access to the I/O Expander */
ret = nxmutex_lock(&priv->lock);
if (ret < 0)
{
return ret;
}
/* Read the input register for this pin
*
* The Input Port Register reflects the incoming logic levels of the pins,
* regardless of whether the pin is defined as an input or an output by
* the Configuration Register. They act only on read operation.
*/
ret = pcf8575_read(priv, &regval);
if (ret < 0)
{
gpioerr("ERROR: Failed to read port register: %d\n", ret);
goto errout_with_lock;
}
/* Return the requested pin values */
for (i = 0; i < count; i++)
{
/* Make sure that this is an output pin */
pin = pins[i];
if (pin > 15)
{
nxmutex_unlock(&priv->lock);
return -ENXIO;
}
/* Is the pin an output? */
if ((priv->inpins & (1 << pin)) == 0)
{
/* We cannot read the value on pin directly. Just Return the last
* value that we wrote to the pin.
*/
values[i] = ((priv->outstate & (1 << pin)) != 0);
}
else
{
values[i] = ((regval & (1 << pin)) != 0);
}
gpioinfo("%d. pin=%u value=%u\n", pin, values[i]);
}
ret = OK;
errout_with_lock:
nxmutex_unlock(&priv->lock);
return ret;
}
#endif
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: pcf8575_initialize
*
* Description:
* Instantiate and configure the PCF8575xx device driver to use the
* provided I2C device instance.
*
* Input Parameters:
* i2c - An I2C driver instance
* minor - The device i2c address
* config - Persistent board configuration data
*
* Returned Value:
* an ioexpander_dev_s instance on success, NULL on failure.
*
****************************************************************************/
FAR struct ioexpander_dev_s *pcf8575_initialize(FAR struct i2c_master_s *i2c,
FAR struct pcf8575_config_s *config)
{
FAR struct pcf8575_dev_s *priv;
#ifdef CONFIG_PCF8575_MULTIPLE
/* Allocate the device state structure */
priv = (FAR struct pcf8575_dev_s *)
kmm_zalloc(sizeof(struct pcf8575_dev_s));
if (!priv)
{
gpioerr("ERROR: Failed to allocate driver instance\n");
return NULL;
}
#else
/* Use the one-and-only I/O Expander driver instance */
priv = &g_pcf8575;
#endif
/* Initialize the device state structure */
priv->dev.ops = &g_pcf8575_ops;
priv->i2c = i2c;
priv->config = config;
nxmutex_init(&priv->lock);
return &priv->dev;
}
#endif /* CONFIG_IOEXPANDER_PCF8575 */