| /**************************************************************************** |
| * 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 const uint8_t *pins, FAR const bool *values, int count); |
| static int pcf8575_multireadpin(FAR struct ioexpander_dev_s *dev, |
| FAR const 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, ®val); |
| 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 const uint8_t *pins, |
| FAR const 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", i, 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 const uint8_t *pins, FAR bool *values, |
| int count) |
| { |
| FAR struct pcf8575_dev_s *priv = (FAR struct pcf8575_dev_s *)dev; |
| uint16_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, ®val); |
| 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", i, 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 */ |