diff --git a/drivers/Kconfig b/drivers/Kconfig index 145f9f7d35..eead0d8c2e 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -42,6 +42,7 @@ rsource "ds3231/Kconfig" rsource "ds3234/Kconfig" rsource "edbg_eui/Kconfig" rsource "io1_xplained/Kconfig" +rsource "pcf857x/Kconfig" rsource "tps6274x/Kconfig" rsource "uart_half_duplex/Kconfig" rsource "usbdev_mock/Kconfig" diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index 9e1bf442ff..671c92e326 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -113,6 +113,10 @@ ifneq (,$(filter nrf24l01p_ng_%,$(USEMODULE))) USEMODULE += nrf24l01p_ng endif +ifneq (,$(filter pcf857%,$(USEMODULE))) + USEMODULE += pcf857x +endif + ifneq (,$(filter periph_ptp_timer periph_ptp_speed_adjustment,$(FEATURES_USED))) FEATURES_REQUIRED += periph_ptp endif diff --git a/drivers/include/pcf857x.h b/drivers/include/pcf857x.h new file mode 100644 index 0000000000..e93f0c89c1 --- /dev/null +++ b/drivers/include/pcf857x.h @@ -0,0 +1,600 @@ +/* + * Copyright (C) 2018 Gunar Schorcht + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup drivers_pcf857x PCF857X I2C I/O expanders + * @ingroup drivers_misc + * @ingroup drivers_saul + * @brief Device driver for Texas Instruments PCF857X I2C I/O expanders + * + * + * \section Device driver for Texas Instruments PCF857X I2C I/O expanders + * + * ## Overview + * + * Texas Instruments PCF857X I2C I/O expanders provide general purpose I/O + * extension via I2C bus. The driver supports the following PCF857X I2C I/O + * expander variants: + * + *
+ * Expander | Type | Pseudomodule to be used + * :--------|:------------------------|:----------------------- + * PCF8574 | 8-bit I2C I/O expander | `pcf8574` + * PCF8574A | 8-bit I2C I/O expander | `pcf8574a` + * PCF8575 | 16-bit I2C I/O expander | `pcf8575` + *

+ * + * For each of these PCF857X I2C I/O expanders variants, the driver defines + * a separate pseudomodule. Multiple PCF857X I2C I/O expanders and different + * variants can be used at the same time. Either the board definition or the + * application must specify used PCF857X I/O expander variants by a list of + * used pseudomodules. For example, to use a PCF8574A and a PCF8575 I/O + * expander in one application, the make command would be: + * + * USEMODULE="pcf8574a pcf8575" make -C tests/driver_pcf857x BOARD=... + * + * At least one PCF857X I2C I/O expander variant has to be specified. The + * driver module `pcf857x` is then enabled implicitly. + * + * @note While PCF8575 is working in I2C fast mode with up to 400 kHz clock + * frequency, PCF8574 and PCF8574A are only specified for I2C normal mode + * with up to 100 kHz clock frequency. However, they seem also to work at + * 400 kHz clock frequency. + * + * The driver interface is kept as compatible as possible with the peripheral + * GPIO interface. The only differences are that + * + * - functions have the prefix `pcf857x_` and + * - functions require an additional parameter, the pointer to the expander + * device of type #pcf857x_t. + * + * ## Defined pseudomodules + * + * The functionality of the driver is controlled by the use of pseudomodules. + * The following pseudomodules are defined: + *
+ * Pseudomoule | Functionality + * :--------------------|:--------------------- + * `pcf8574` | support of PCF8574 enabled + * `pcf8574a` | support of PCF8574A enabled + * `pcf8575` | support of PCF8575 enabled + * `pcf857x_irq` | support of interrupts enabled with medium event priority + * `pcf857x_irq_low` | support of interrupts enabled with low event priority + * `pcf857x_irq_medium` | support of interrupts enabled with medium event priority + * `pcf857x_irq_low` | support of interrupts enabled with high event priority + *

+ * + * @note At least one of the modules `pcf8574`, `pcf8574a` and `pcf8575` has to + * be used. + * + * ## Expander GPIOs + * + * The PCF857X expander devices provide a GPIO expansion over the I2C + * interface with either + * + * - 1 port with 8 quasi-parallel input/output (I/O) pins (PCF8574, PCF8574A) or + * - 2 ports with 8 quasi-parallel input/output (I/O) pins each (PCF8575). + * + * Each quasi-bidirectional expander I/O pin can be used as an input or output + * without the use of a data-direction control signal. Output pins are latched + * and have high-current drive capability for directly driving LEDs. + * The quasi-bidirectional expander I/O pins without direction control + * work as follows: + * + * - INPUT: + * Writing 1 to an expander pin configures the pin as an input, which is + * pulled up to HIGH by a very weak 100 μA pull-up. When reading the pin, + * its value then depends on the actual voltage. This corresponds to the + * behavior of the #GPIO_IN_PU mode. + * - OUTPUT: + * Writing 0 to an expander pin configures the pin as an output and + * actively drives the pin to LOW. This corresponds to the behavior of + * the #GPIO_OD_PU mode. + * + * @note Since the expander I/O pins are quasi-bidirectional without direction + * control, the only actively driven level is the output LOW. Therefore the + * driver physically supports only the modes #GPIO_IN_PU and #GPIO_OD_PU. + * The other logically identical modes #GPIO_IN, #GPIO_OUT and #GPIO_OD are + * emulated. Please keep this in mind when connecting these pins to other + * open-drain output pins that do not generate active signals. The #GPIO_IN_PD + * mode is not supported. + * + * After the initialization with the #pcf857x_init function, all + * expander I/O pins are in input mode and pulled-up to HIGH. + * + * The expander I/O pins can be addressed as GPIO pins using the following + * scheme: + * + *
+ * PCF857X pin label | Port | Pin | RIOT symbol | Remark + * ----------------- |:----:|:---:|:--------------------------|:----------------- + * P00 | 0 | 0 | `PCF857X_GPIO_PIN(0, 0)` | PCF8574, PCF8574A and PCF8575 + * P01 | 0 | 1 | `PCF857X_GPIO_PIN(0, 1)` | PCF8574, PCF8574A and PCF8575 + * ... | ... | ... | ... | ... + * P07 | 0 | 7 | `PCF857X_GPIO_PIN(0, 7)` | PCF8574, PCF8574A and PCF8575 + * P10 | 0 | 8 | `PCF857X_GPIO_PIN(0, 8)` | PCF8575 only + * P11 | 0 | 9 | `PCF857X_GPIO_PIN(0, 9)` | PCF8575 only + * ... | ... | ... | ... | ... + * P17 | 0 | 15 | `PCF857X_GPIO_PIN(0, 15)` | PCF8575 only + *
+ * + * ## Interrupts + * + * PCF857X expanders have an open-drain, low-active interrupt (INT) signal, + * which generates an interrupt by any rising or falling edge of the expander + * pins in the input mode. Using this expander interrupt signal, the following + * features become available: + * + * - An interrupt service function can be attached to an expander input pin with + * the #pcf857x_gpio_init_int function. This interrupt service function is + * then called on any rising and/or falling edge of the expander input pin. + * + * - In addition, the driver uses the interrupt on changes of an expander + * input pin to internally maintain the current state of all expander + * input pins. Using this internal current state of the expander input + * pins avoids reading all expander input pins via I2C every time the input + * value of a single expander GPIO pin is read with #pcf857x_gpio_read. + * + * Since interrupts are handled in the context of a separate event thread (see + * section [The Interrupt Context Problem](#pcf857x_interrupt_context_problem)) + * enabling interrupts requires more RAM. Therefore interrupts have to be + * explicitly enabled with the module `pcf857x_irq_`. + * `priority` can be one of `low`, `medium` or `highest`, which correspond to + * the priority of the event thread that processes the interrupts. If only the + * module `pcf857x_irq` is used without specifying the priority, the interrupt + * handling is enabled with a medium priority of the event thread. For more + * information on the priorities check the @ref sys_event module. + * + * Furthermore, the MCU GPIO pin to which the PCF857X `INT` signal is + * connected has to be defined by the default configuration parameter + * #PCF857X_PARAM_INT_PIN (pcf857x_params_t::int_pin) either in the + * configuration parameter file or at the command line, for example: + * + * CFLAGS="-DPCF857X_PARAM_INT_PIN=\(GPIO_PIN\(0,6\)\)" \ + * USEMODULE="pcf8575 pcf857x_irq_medium" make -C tests/driver_pcf857x BOARD=... + * + *
+ * @note If an output of the expander is connected to an input of the same + * expander, there is no interrupt triggered by the input when the + * output changes. Therefore, a write operation to an output with + * #pcf857x_gpio_write, #pcf857x_gpio_clear, #pcf857x_gpio_set or + * #pcf857x_gpio_toggle leads to an additional read-after-write operation, + * if interrupts are used.

+ * The use of interrupts therefore increases the read performance considerably, + * since I2C read operations are required only when the inputs change. But the + * write performance is reduced to the half. + * + * ## The Interrupt Context Problem {#pcf857x_interrupt_context_problem} + * + * Handling an interrupt of a PCF857x expander requires the driver to access + * the device directly via I2C. However, the mutex-based synchronization of + * I2C accesses does not work in the interrupt context. Therefore the ISR must + * not access the PCF857x expander device directly. Rather, the ISR must only + * indicate the occurrence of the interrupt which has to be handled + * asynchronously in thread context. + * + * For this purpose an event thread module is used when interrupts are + * enabled by the module `pcf857x_irq_`. The driver then + * handles the interrupts in the context of the event thread with given + * `priority`. For more information on the priorities check + * the @ref sys_event module. + * + * ## SAUL Capabilities + * + * The driver provides SAUL capabilities that are compatible to the SAUL + * capabilities of peripheral GPIOs. Each PCF857X expander I/O pin can be + * mapped directly to SAUL by defining an according entry in + * \c PCF857X_SAUL_GPIO_PARAMS. Please refer file + * `$RIOTBASE/drivers/pcf857x/include/pcf857x_params.h` for an example. + * + * @note Module `saul_gpio` has to be added to the + * project to enable SAUL capabilities of the PCF857X driver, e.g.: + * + * USEMODULE="pcf8575 saul_gpio" make -C tests/saul BOARD=... + * + * ## Using Multiple Devices + * + * It is possible to use multiple devices and different variants of PCF857X + * I/O expanders at the same time. Either the board definition or the + * application must specify used PCF857X I/O expander variants by a list of + * used pseudomodules. For example, to use a PCF8574A and a PCF8575 I/O + * expander in one application, the make command would be: + * + * USEMODULE="pcf8574a pcf8575" make -C tests/driver_pcf857x BOARD=... + * + * Furthermore, used devices have to be configured by defining the + * configuration parameter set `pcf857x_params` of type #pcf857x_params_t. + * The default configuration for one device is defined in + * `drivers/pcf857x/pcf857x_params.h`. Either the board definition or the + * application can override it by placing a file `pcf857x_params.h` in the + * board definition directory or the application directory `$APPDIR`. + * For example, the definition of the configuration parameter array for the + * two devices above could be: + * + * static const pcf857x_params_t pcf857x_params[] = { + * { + * .dev = I2C_DEV(0), + * .addr = 0, + * .exp = PCF857X_EXP_PCF8574A, + * .int_pin = GPIO_PIN(0,1), + * }, + * { + * .dev = I2C_DEV(0), + * .addr = 0, + * .exp = PCF857X_EXP_PCF8575, + * .int_pin = GPIO_PIN(0,2), + * }, + * }; + * + * @{ + * + * @author Gunar Schorcht + * @file + */ + +#ifndef PCF857X_H +#define PCF857X_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + +#include "kernel_defines.h" +#include "periph/gpio.h" +#include "periph/i2c.h" + +#if IS_USED(MODULE_SAUL_GPIO) || DOXYGEN +#include "saul/periph.h" +#endif /* MODULE_SAUL_GPIO */ + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN +#include "event.h" +#endif /* MODULE_PCF857X_IRQ */ + +#if !IS_USED(MODULE_PCF8574) && !IS_USED(MODULE_PCF8574A) && !IS_USED(MODULE_PCF8575) +#error "Please provide a list of pcf857x variants used by the application (pcf8574, pcf8574a or pcf8575)" +#endif + +/** + * @name PCF857X I2C slave addresses + * + * PCF857X I2C slave addresses are defined as an offset to a base address, + * which depends on the expander used. The address offset is in the range + * of 0 to 7. + * @{ + */ +#define PCF8575_BASE_ADDR (0x20) /**< PCF8575 I2C slave base address. + Addresses are then in range from + 0x20 to 0x27 */ +#define PCF8574_BASE_ADDR (0x20) /**< PCF8574 I2C slave base address. + Addresses are then in range from + 0x20 to 0x27 */ +#define PCF8574A_BASE_ADDR (0x38) /**< PCF8574A I2C slave base address. + Addresses are then in range from + 0x38 to 0x3f */ +/** @} */ + +/** + * @name PCF857X I/O expander pin number + * @{ + */ +#define PCF8575_GPIO_PIN_NUM (16) /**< PCF8575 has 16 I/O pins */ +#define PCF8574_GPIO_PIN_NUM (8) /**< PCF8574 has 8 I/O pins */ +#define PCF8574A_GPIO_PIN_NUM (8) /**< PCF8574A has 8 I/O pins */ +/** @} */ + +/** conversion of (port x : pin y) to a pin number */ +#define PCF857X_GPIO_PIN(x,y) ((gpio_t)((x << 3) | y)) + +/** + * @name Module dependent definitions and declarations + * @{ + */ +#if IS_USED(MODULE_PCF8575) || DOXYGEN + +/** + * @brief Maximum number of GPIO pins + * + * Defines the maximum number of GPIO pins of all PCF857X I/O expanders + * used. If a PCF8575 is used, the maximum number is 16 I/O pins. + */ +#define PCF857X_GPIO_PIN_NUM (16) + +/** + * @brief Data type that can mask all expander pins + * + * If a PCF8575 is used, the 16 I/O pins have to be masked. + */ +typedef uint16_t pcf857x_data_t; + +#else /* MODULE_PCF8575 || DOXYGEN */ + +#define PCF857X_GPIO_PIN_NUM (8) /**< PCF8574, PCF8574 provide 8 I/O pins */ +typedef uint8_t pcf857x_data_t; /**< type that can mask all expander pins */ + +#endif /* MODULE_PCF8575 || DOXYGEN */ +/** @} */ + +/** Definition of PCF857X driver error codes */ +typedef enum { + PCF857X_OK, /**< success */ + PCF857X_ERROR_I2C, /**< I2C communication error */ + PCF857X_ERROR_INV_EXP, /**< invalid expander variant */ + PCF857X_ERROR_INV_MODE, /**< invalid pin mode */ + PCF857X_ERROR_INV_FLANK, /**< invalid interrupt flank */ + PCF857X_ERROR_INT_PIN, /**< interrupt pin initialization failed */ +} pcf857x_error_codes_t; + +/** + * @brief Definition of PCF857X expander variants + * + * It is used in configuration parameters to specify the PCF857X expander + * used by device. + * + * @note Expander variants known by the driver depend on enabled pseudomodules + * `pcf8574`, `pcf8574a` and `pcf8575`. + */ +typedef enum { +#if IS_USED(MODULE_PCF8574) || DOXYGEN + PCF857X_EXP_PCF8574, /**< PCF8574 8 bit I/O expander used */ +#endif +#if IS_USED(MODULE_PCF8574A) || DOXYGEN + PCF857X_EXP_PCF8574A, /**< PCF8574A 8 bit I/O expander */ +#endif +#if IS_USED(MODULE_PCF8575) || DOXYGEN + PCF857X_EXP_PCF8575, /**< PCF8575 16 bit I/O expander */ +#endif + PCF857X_EXP_MAX, +} pcf857x_exp_t; + +/** + * @brief PCF857X device initialization parameters + */ +typedef struct { + + i2c_t dev; /**< I2C device (default I2C_DEV(0)) */ + uint16_t addr; /**< I2C slave address offset to the PCF7857X base + address (default 0) */ + pcf857x_exp_t exp; /**< PCF857X expander variant used by the device + (default depends on used pseudomodules */ + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN + gpio_t int_pin; /**< MCU GPIO pin or #GPIO_UNDEF if not used (default). + Using interrupt pin has the advantage that inputs + have to be read from expander only if any input + value changes. + @note To use interrupts for expander inputs, this + pin has to be defined. */ +#endif /* MODULE_PCF857X_IRQ */ +} pcf857x_params_t; + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN +/** + * @brief IRQ event type + * + * Handling an interrupt of a PCF857x expander requires the driver to access + * the device directly via I2C. However, the mutex-based synchronization of + * I2C accesses does not work in the interrupt context. Therefore the ISR must + * not access the PCF857x expander device directly. Rather, the ISR must only + * indicate the occurrence of the interrupt which has to be handled + * asynchronously in the thread context. + * + * The type defines the data structure that is part of each device data + * structure to indicate that an interrupt of the device occurred. Since there + * is only one interrupt source, only one interrupt can be pending per device. + * Thus, only one object of this type per device is required. + */ +typedef struct { + event_t event; /**< inherited event data structure */ + void *dev; /**< PCF857X device reference */ +} pcf857x_irq_event_t; + +#endif /* MODULE_PCF857X_IRQ */ + +/** + * @brief PCF857X device data structure type + */ +typedef struct { + pcf857x_params_t params; /**< device initialization parameters */ + + uint8_t pin_num; /**< number of I/O pins, depends on used expander + variant */ + pcf857x_data_t modes; /**< expander pin modes */ + pcf857x_data_t in; /**< expander input pin values */ + pcf857x_data_t out; /**< expander output pin values */ + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN + gpio_isr_ctx_t isr[PCF857X_GPIO_PIN_NUM]; /**< ISR with arg for each expander pin */ + gpio_flank_t flank[PCF857X_GPIO_PIN_NUM]; /**< interrupt flank for each expander pin */ + bool enabled[PCF857X_GPIO_PIN_NUM]; /**< enabled flag for each expander pin */ + pcf857x_irq_event_t irq_event; /**< IRQ event object used for the device */ +#endif /* MODULE_PCF857X_IRQ */ + +} pcf857x_t; + +#if IS_USED(MODULE_SAUL_GPIO) || DOXYGEN +/** + * @brief PCF857X configuration structure for mapping expander pins to SAUL + * + * This data structure is an extension of the GPIO configuration structure for + * mapping GPIOs to SAUL. The only additional information required is a + * reference to the according PCF857X device. + * + * @note To use PCF857X with SAUL, module `saul_gpio` has to be added to the + * project. + */ +typedef struct { + unsigned int dev; /**< PCF857X device index */ + saul_gpio_params_t gpio; /**< GPIO configuration for mapping to SAUL */ +} pcf857x_saul_gpio_params_t; +#endif + +/** + * @brief Initialize the PCF857X I/O expander + * + * All expander pins are set to be input and are pulled up. + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] params configuration parameters, see #pcf857x_params_t + * + * @pre If the interrupt handling is enabled by one of the modules + * `pcf857x_irq*`, the MCU GPIO pin for the interrupt signal + * has to be defined by the default configuration parameter + * #PCF857X_PARAM_INT_PIN (pcf857x_params_t::int_pin). + * + * @retval PCF857X_OK on success + * @retval PCF857X_ERROR_* a negative error code on error, + * see #pcf857x_error_codes_t + */ +int pcf857x_init(pcf857x_t *dev, const pcf857x_params_t *params); + +/** + * @brief Initialize a PCF857X pin + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to initialize, use PCF857X_GPIO_PIN(x,y) to specify + * @param[in] mode mode of the pin, see #gpio_t + * + * @note + * - Since the expander I/O pins are quasi-bidirectional without direction + * control, the only actively driven level is the output LOW. Therefore + * the driver physically supports only the modes #GPIO_IN_PU and + * #GPIO_OD_PU. The other logically identical modes #GPIO_IN, #GPIO_OUT + * and #GPIO_OD are emulated. For the #GPIO_IN_PU mode the function returns + * with #PCF857X_ERROR_INV_MODE. + * - After initialization in #GPIO_OUT mode the pin is actively driven LOW, + * after initialization in all other modes the pin is pulled-up to HIGH. + * + * @retval PCF857X_OK on success + * @retval PCF857X_ERROR_* a negative error code on error, + * see #pcf857x_error_codes_t + */ +int pcf857x_gpio_init(pcf857x_t *dev, gpio_t pin, gpio_mode_t mode); + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN +/** + * @brief Initialize a PCF857X pin for external interrupt usage + * + * The registered callback function will be called in interrupt context every + * time the defined flank(s) are detected. Therefore, it MUST NOT be blocking + * or time-consuming. + * + * The interrupt is activated automatically after the initialization. + * + * @pre The MCU GPIO pin for the interrupt signal has to be defined by the + * default configuration parameter #PCF857X_PARAM_INT_PIN + * (pcf857x_params_t::int_pin). + * + * @note + * - This function is only available if interrupt handling is enabled by one + * of the modules `pcf857x_irq*` + * - Since the expander I/O pins are quasi-bidirectional without direction + * control, the only actively driven level is the output LOW. Therefore + * the driver physically supports only the modes #GPIO_IN_PU and + * #GPIO_OD_PU. The other logically identical modes #GPIO_IN, #GPIO_OUT + * and #GPIO_OD are emulated. For the #GPIO_IN_PU mode the function returns + * with #PCF857X_ERROR_INV_MODE. + * - After initialization in #GPIO_OUT mode the pin is actively driven LOW, + * after initialization in all other modes the pin is pulled-up to HIGH. + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to initialize, use PCF857X_GPIO_PIN(x,y) to specify + * @param[in] mode mode of the pin, see #gpio_t + * @param[in] flank define the active flanks, see #gpio_flank_t + * @param[in] isr ISR that is called back from interrupt context + * @param[in] arg optional argument passed to the callback + * + * @retval PCF857X_OK on success + * @retval PCF857X_ERROR_* a negative error code on error, + * see #pcf857x_error_codes_t + */ +int pcf857x_gpio_init_int(pcf857x_t *dev, gpio_t pin, + gpio_mode_t mode, + gpio_flank_t flank, + gpio_cb_t isr, + void *arg); +#endif /* MODULE_PCF857X_IRQ || DOXYGEN */ + +/** + * @brief Get the value from PCF857X input pin + * + * @note If the PCF857X interrupt is used, the read operation does not perform + * an I2C read operation since the last input pin value is already read. + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to read, use PCF857X_GPIO_PIN(x,y) to specify + */ +int pcf857x_gpio_read(pcf857x_t *dev, gpio_t pin); + +/** + * @brief Write the value to PCF857X input pin + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to write, use PCF857X_GPIO_PIN(x,y) to specify + * @param[in] value value to write + */ +void pcf857x_gpio_write(pcf857x_t *dev, gpio_t pin, int value); + +/** + * @brief Clear the PCF857X output pin + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to clear, use PCF857X_GPIO_PIN(x,y) to specify + */ +void pcf857x_gpio_clear(pcf857x_t *dev, gpio_t pin); + +/** + * @brief Set the PCF857X output pin + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to set, use PCF857X_GPIO_PIN(x,y) to specify + */ +void pcf857x_gpio_set(pcf857x_t *dev, gpio_t pin); + +/** + * @brief Toggle the value of the PCF857X output pin + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to toggle, use PCF857X_GPIO_PIN(x,y) to specify + */ +void pcf857x_gpio_toggle(pcf857x_t *dev, gpio_t pin); + +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN +/** + * @brief Enable pin interrupt + * + * @note This function is only available if interrupt handling is enabled + * by one of the modules `pcf857x_irq*` + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to enable the interrupt for + */ +void pcf857x_gpio_irq_enable(pcf857x_t *dev, gpio_t pin); + +/** + * @brief Disable pin interrupt + * + * @note This function is only available if interrupt handling is enabled + * by one of the modules `pcf857x_irq*` + * + * @param[in] dev descriptor of PCF857X I/O expander device + * @param[in] pin pin to enable the interrupt for + */ +void pcf857x_gpio_irq_disable(pcf857x_t *dev, gpio_t pin); +#endif /* MODULE_PCF857X_IRQ || DOXYGEN */ + +#ifdef __cplusplus +} +#endif + +#endif /* PCF857X_H */ +/** @} */ diff --git a/drivers/pcf857x/Kconfig b/drivers/pcf857x/Kconfig new file mode 100644 index 0000000000..f60a2ab858 --- /dev/null +++ b/drivers/pcf857x/Kconfig @@ -0,0 +1,65 @@ +# Copyright (c) 2021 Gunar Schorcht +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +menuconfig MODULE_PCF857X + bool "PCF857x Remote I/O Expander for I2C Bus" + depends on HAS_PERIPH_GPIO + depends on HAS_PERIPH_I2C + depends on TEST_KCONFIG + select MODULE_PERIPH_GPIO + select MODULE_PERIPH_I2C + help + Driver for Texas Instruments PCF857X I2C I/O expanders. + The driver supports the PCF8574, PCF8574A, and PCF8575 variants. + Select the variants used by your application. + +if MODULE_PCF857X + +config MODULE_PCF8574 + bool "PCF8574 Remote 8-Bit I/O is used" + +config MODULE_PCF8574A + bool "PCF8574A Remote 8-Bit I/O is used" + +config MODULE_PCF8575 + bool "PCF8575 Remote 16-Bit I/O is used" + default y + +config MODULE_PCF857X_IRQ + bool "Interrupt support for PCF857x I/O Expander pins" + depends on MODULE_PCF857X + depends on HAS_PERIPH_GPIO_IRQ + select MODULE_PERIPH_GPIO_IRQ + select MODULE_EVENT + select MODULE_EVENT_THREAD + help + To use the IRQs the MODULE_EVENT_THREAD symbol should be set. + +choice + bool "Thread priority" + depends on MODULE_PCF857X_IRQ + default MODULE_PCF857X_IRQ_MEDIUM + help + To process IRQs an event thread is used. The MODULE_EVENT_THREAD + symbol should be set. Choose a priority for the thread that + processes the IRQs. The default is medium priority. + +config MODULE_PCF857X_IRQ_LOW + bool "Low" + select MODULE_EVENT_THREAD_LOW + +config MODULE_PCF857X_IRQ_MEDIUM + bool "Medium" + select MODULE_EVENT_THREAD_MEDIUM + +config MODULE_PCF857X_IRQ_HIGHEST + bool "Highest" + select MODULE_EVENT_THREAD_HIGHEST + +endchoice + +endif # MODULE_PCF857X diff --git a/drivers/pcf857x/Makefile b/drivers/pcf857x/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/pcf857x/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/pcf857x/Makefile.dep b/drivers/pcf857x/Makefile.dep new file mode 100644 index 0000000000..090294112c --- /dev/null +++ b/drivers/pcf857x/Makefile.dep @@ -0,0 +1,19 @@ +FEATURES_REQUIRED += periph_gpio +FEATURES_REQUIRED += periph_i2c + +_PCF857X_IRQ_MODULE := $(filter pcf857x_irq_%,$(USEMODULE)) +ifneq (,$(_PCF857X_IRQ_MODULE)) + # pull in the correspondant event_thread_ module + USEMODULE += $(_PCF857X_IRQ_MODULE:pcf857x_irq_%=event_thread_%) + USEMODULE += pcf857x_irq +else + ifneq (,$(filter pcf857x_irq,$(USEMODULE))) + # pull in the pcf857x_irq_medium module as default if pcf857x_irq is used. + USEMODULE += pcf857x_irq_medium + endif +endif + +ifneq (,$(filter pcf857x_irq,$(USEMODULE))) + FEATURES_REQUIRED += periph_gpio_irq + USEMODULE += pcf857x +endif diff --git a/drivers/pcf857x/Makefile.include b/drivers/pcf857x/Makefile.include new file mode 100644 index 0000000000..bddc48997f --- /dev/null +++ b/drivers/pcf857x/Makefile.include @@ -0,0 +1,11 @@ +# include variants of PCF857X drivers as pseudo modules +PSEUDOMODULES += pcf8574 +PSEUDOMODULES += pcf8574a +PSEUDOMODULES += pcf8575 +PSEUDOMODULES += pcf857x_irq +PSEUDOMODULES += pcf857x_irq_low +PSEUDOMODULES += pcf857x_irq_medium +PSEUDOMODULES += pcf857x_irq_highest + +USEMODULE_INCLUDES_pcf857x := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_pcf857x) diff --git a/drivers/pcf857x/include/pcf857x_params.h b/drivers/pcf857x/include/pcf857x_params.h new file mode 100644 index 0000000000..b260cd0247 --- /dev/null +++ b/drivers/pcf857x/include/pcf857x_params.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 Gunar Schorcht + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_pcf857x + * @brief Default configuration for Texas Instruments PCF857X I2C I/O expanders + * @author Gunar Schorcht + * @file + * @{ + */ + +#ifndef PCF857X_PARAMS_H +#define PCF857X_PARAMS_H + +#include "board.h" +#include "pcf857x.h" +#include "saul_reg.h" +#include "saul/periph.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters + * @{ + */ +#ifndef PCF857X_PARAM_DEV +/** device is I2C_DEV(0) */ +#define PCF857X_PARAM_DEV I2C_DEV(0) +#endif + +#ifndef PCF857X_PARAM_ADDR +/** I2C slave address offset is 0 */ +#define PCF857X_PARAM_ADDR (0) +#endif + +#ifndef PCF857X_PARAM_EXP +/** PCF857X expander variant used depends on enabled pseudomodules*/ +#if IS_USED(MODULE_PCF8575) || DOXYGEN +#define PCF857X_PARAM_EXP (PCF857X_EXP_PCF8575) +#elif IS_USED(MODULE_PCF8574) +#define PCF857X_PARAM_EXP (PCF857X_EXP_PCF8574) +#elif IS_USED(MODULE_PCF8574A) +#define PCF857X_PARAM_EXP (PCF857X_EXP_PCF8574A) +#endif +#endif /* PCF857X_PARAM_EXP */ + +#ifndef PCF857X_PARAM_INT_PIN +/** MCU interrupt pin */ +#define PCF857X_PARAM_INT_PIN (GPIO_UNDEF) +#endif + +#ifndef PCF857X_PARAMS +#if IS_USED(MODULE_PCF857X_IRQ) || DOXYGEN +/** Default configuration parameter set */ +#define PCF857X_PARAMS { \ + .dev = PCF857X_PARAM_DEV, \ + .addr = PCF857X_PARAM_ADDR, \ + .exp = PCF857X_PARAM_EXP, \ + .int_pin = PCF857X_PARAM_INT_PIN, \ + }, +#else +#define PCF857X_PARAMS { \ + .dev = PCF857X_PARAM_DEV, \ + .addr = PCF857X_PARAM_ADDR, \ + .exp = PCF857X_PARAM_EXP, \ + }, +#endif +#endif /* PCF857X_PARAMS */ + +#ifndef PCF857X_SAUL_GPIO_PARAMS +/** Example for mapping expander pins to SAUL */ +#define PCF857X_SAUL_GPIO_PARAMS { \ + .dev = 0, \ + .gpio = { \ + .name = "P00 Output", \ + .pin = PCF857X_GPIO_PIN(0, 0), \ + .mode = GPIO_OUT, \ + .flags = SAUL_GPIO_INIT_CLEAR, \ + } \ + }, \ + { \ + .dev = 0, \ + .gpio = { \ + .name = "P01 Input", \ + .pin = PCF857X_GPIO_PIN(0, 1), \ + .mode = GPIO_IN, \ + .flags = 0, \ + } \ + }, +#endif +/**@}*/ + +/** + * @brief Allocate some memory to store the actual configuration + */ +static const pcf857x_params_t pcf857x_params[] = +{ + PCF857X_PARAMS +}; + +#if IS_USED(MODULE_SAUL_GPIO) || DOXYGEN +/** + * @brief Additional meta information to keep in the SAUL registry + */ +static const pcf857x_saul_gpio_params_t pcf857x_saul_gpio_params[] = +{ + PCF857X_SAUL_GPIO_PARAMS +}; +#endif /* MODULE_SAUL_GPIO || DOXYGEN */ + +#ifdef __cplusplus +} +#endif + +#endif /* PCF857X_PARAMS_H */ +/** @} */ diff --git a/drivers/pcf857x/pcf857x.c b/drivers/pcf857x/pcf857x.c new file mode 100644 index 0000000000..f7f4e8b224 --- /dev/null +++ b/drivers/pcf857x/pcf857x.c @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2018 Gunar Schorcht + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_pcf857x + * @brief Device driver for Texas Instruments PCF857X I2C I/O expanders + * @author Gunar Schorcht + * @file + * @{ + */ + +#include +#include + +#include "pcf857x.h" + +#include "irq.h" +#include "log.h" +#include "thread.h" + +#if IS_USED(MODULE_PCF857X_IRQ) +#include "event/thread.h" +#endif + +#define ENABLE_DEBUG 0 +#include "debug.h" + +#if ENABLE_DEBUG + +#define DEBUG_DEV(f, d, ...) \ + DEBUG("[pcf857x] %s i2c dev=%d addr=%02x: " f "\n", \ + __func__, d->params.dev, d->params.addr, ## __VA_ARGS__) + +#else /* ENABLE_DEBUG */ + +#define DEBUG_DEV(f, d, ...) + +#endif /* ENABLE_DEBUG */ + +#if IS_USED(MODULE_PCF857X_IRQ_LOW) +#define PCF857X_EVENT_PRIO EVENT_PRIO_LOWEST +#elif IS_USED(MODULE_PCF857X_IRQ_MEDIUM) +#define PCF857X_EVENT_PRIO EVENT_PRIO_MEDIUM +#elif IS_USED(MODULE_PCF857X_IRQ_HIGHEST) +#define PCF857X_EVENT_PRIO EVENT_PRIO_HIGHEST +#endif + +/** Forward declaration of functions for internal use */ + +static inline void _acquire(const pcf857x_t *dev); +static inline void _release(const pcf857x_t *dev); +static int _read(const pcf857x_t *dev, pcf857x_data_t *data); +static int _write(const pcf857x_t *dev, pcf857x_data_t data); + +#if IS_USED(MODULE_PCF857X_IRQ) + +/* interrutp service routine for IRQs */ +static void _irq_isr(void *arg); + +/* declaration of IRQ handler function */ +static void _irq_handler(event_t *event); + +/* internal update function */ +static void _update_state(pcf857x_t* dev); + +#endif /* MODULE_PCF857X_IRQ */ + +int pcf857x_init(pcf857x_t *dev, const pcf857x_params_t *params) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(params != NULL); + assert(params->exp < PCF857X_EXP_MAX); +#if IS_USED(MODULE_PCF857X_IRQ) + assert(gpio_is_valid(params->int_pin)); +#endif + + DEBUG_DEV("params=%p", dev, params); + + /* init device data structure */ + dev->params = *params; + + switch (params->exp) { +#if IS_USED(MODULE_PCF8574) + /**< PCF8574 8 bit I/O expander used */ + case PCF857X_EXP_PCF8574: dev->pin_num = PCF8574_GPIO_PIN_NUM; + dev->params.addr += PCF8574_BASE_ADDR; + break; +#endif +#if IS_USED(MODULE_PCF8574A) + /**< PCF8574A 8 bit I/O expander */ + case PCF857X_EXP_PCF8574A: dev->pin_num = PCF8574A_GPIO_PIN_NUM; + dev->params.addr += PCF8574A_BASE_ADDR; + break; +#endif +#if IS_USED(MODULE_PCF8575) + /**< PCF8575 16 bit I/O expander */ + case PCF857X_EXP_PCF8575: dev->pin_num = PCF8575_GPIO_PIN_NUM; + dev->params.addr += PCF8575_BASE_ADDR; + break; +#endif + default: return -PCF857X_ERROR_INV_EXP; + } + +#if IS_USED(MODULE_PCF857X_IRQ) + /* initialize the IRQ event object used for delaying interrupts */ + dev->irq_event.event.handler = _irq_handler; + dev->irq_event.dev = dev; + + for (unsigned i = 0; i < dev->pin_num; i++) { + dev->isr[i].cb = NULL; + dev->isr[i].arg = NULL; + dev->enabled[i] = false; + } + + /* initialize the interrupt pin */ + if (gpio_init_int(dev->params.int_pin, + GPIO_IN_PU, GPIO_FALLING, _irq_isr, (void*)dev)) { + return -PCF857X_ERROR_INT_PIN; + } +#endif /* MODULE_PCF857X_IRQ */ + + int res = PCF857X_OK; + + _acquire(dev); + + /* write 1 to all pins to switch them to INPUTS pulled up to HIGH */ + dev->out = ~0; + res |= _write(dev, dev->out); + + /* initial read all pins */ + res |= _read(dev, &dev->in); + + /* set all pin modes to INPUT and set internal output data to 1 (HIGH) */ + dev->modes = ~0; + + _release(dev); + + return PCF857X_OK; +} + +int pcf857x_gpio_init(pcf857x_t *dev, gpio_t pin, gpio_mode_t mode) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(pin < dev->pin_num); + + DEBUG_DEV("pin=%u mode=%u", dev, pin, mode); + + /* + * Since the LOW output is the only actively driven level possible with + * this expander, only in the case of GPIO_OUT we write a 0 to the pin + * to configure the pin as an output and actively drive it LOW. In all + * other modes, the pin is configured as an input and pulled-up to HIGH + * with the weak pull-up to emulate them. + */ + switch (mode) { + case GPIO_IN_PD: DEBUG_DEV("gpio mode GPIO_IN_PD not supported", dev, mode); + return -PCF857X_ERROR_INV_MODE; + case GPIO_OUT: dev->modes &= ~(1 << pin); /* set mode bit to 0 */ + dev->out &= ~(1 << pin); /* set output bit to 0 */ + break; + default: dev->modes |= (1 << pin); /* set mode bit to 1 */ + dev->out |= (1 << pin); /* set output bit to 1 */ + break; + } + + int res; + + /* write the mode */ + pcf857x_data_t data = dev->modes | dev->out; + _acquire(dev); + if ((res = _write(dev, data)) != PCF857X_OK) { + _release(dev); + return res; + } + +#if IS_USED(MODULE_PCF857X_IRQ) + /* reset the callback in case the port used external interrupts before */ + dev->isr[pin].cb = NULL; + dev->isr[pin].arg = NULL; + dev->enabled[pin] = false; + + /* + * If an output of the expander is connected to an input of the same + * expander, there is no interrupt triggered by the input when the + * output changes. + * Therefore, we have to read input pins after the write operation to + * update the input pin state in the device data structure and to trigger + * an ISR if necessary. + * + * @note _update_state releases the bus. + */ + _update_state(dev); +#else + /* read to update the internal input state */ + res = _read(dev, &dev->in); + _release(dev); +#endif + return res; +} + +#if IS_USED(MODULE_PCF857X_IRQ) +int pcf857x_gpio_init_int(pcf857x_t *dev, gpio_t pin, + gpio_mode_t mode, + gpio_flank_t flank, + gpio_cb_t isr, + void *arg) +{ + int res = PCF857X_OK; + + /* initialize the pin */ + if ((res = pcf857x_gpio_init(dev, pin, mode)) != PCF857X_OK) { + return res; + } + + switch (flank) { + case GPIO_FALLING: + case GPIO_RISING: + case GPIO_BOTH: dev->isr[pin].cb = isr; + dev->isr[pin].arg = arg; + dev->flank[pin] = flank; + dev->enabled[pin] = true; + break; + default: DEBUG_DEV("invalid flank %d for pin %d", dev, flank, pin); + return -PCF857X_ERROR_INV_FLANK; + } + + return PCF857X_OK; +} + +void pcf857x_gpio_irq_enable(pcf857x_t *dev, gpio_t pin) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(pin < dev->pin_num); + + DEBUG_DEV("pin=%u", dev, pin); + dev->enabled[pin] = true; +} + +void pcf857x_gpio_irq_disable(pcf857x_t *dev, gpio_t pin) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(pin < dev->pin_num); + + DEBUG_DEV("pin=%u", dev, pin); + dev->enabled[pin] = false; +} +#endif + +int pcf857x_gpio_read(pcf857x_t *dev, gpio_t pin) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(pin < dev->pin_num); + + DEBUG_DEV("pin=%u", dev, pin); + + /* + * If we use the interrupt, we always have an up-to-date input snapshot + * stored in the device data structure and which can be used directly. + * Otherwise we have to read the pins first. + */ +#if !IS_USED(MODULE_PCF857X_IRQ) + _acquire(dev); + _read(dev, &dev->in); + _release(dev); +#endif + return (dev->in &(1 << pin)) ? 1 : 0; +} + +void pcf857x_gpio_write(pcf857x_t *dev, gpio_t pin, int value) +{ + /* some parameter sanity checks */ + assert(dev != NULL); + assert(pin < dev->pin_num); + + DEBUG_DEV("pin=%u value=%d", dev, pin, value); + + /* set pin bit value */ + if (value) { + dev->out |= (1 << pin); + } + else { + dev->out &= ~(1 << pin); + } + + /* update pin values */ + pcf857x_data_t data = dev->modes | dev->out; + _acquire(dev); + _write(dev, data); +#if IS_USED(MODULE_PCF857X_IRQ) + /* + * If an output of the expander is connected to an input of the same + * expander, there is no interrupt triggered by the input when the + * output changes. + * Therefore, we have to read input pins after the write operation to + * update the input pin state in the device data structure and to trigger + * an ISR if necessary. + * + * @note _update_state releases the bus. + */ + _update_state(dev); +#else + _release(dev); +#endif +} + +void pcf857x_gpio_clear(pcf857x_t *dev, gpio_t pin) +{ + DEBUG_DEV("pin=%u", dev, pin); + return pcf857x_gpio_write(dev, pin, 0); +} + +void pcf857x_gpio_set(pcf857x_t *dev, gpio_t pin) +{ + DEBUG_DEV("pin=%u", dev, pin); + return pcf857x_gpio_write(dev, pin, 1); +} + +void pcf857x_gpio_toggle(pcf857x_t *dev, gpio_t pin) +{ + DEBUG_DEV("pin=%u", dev, pin); + return pcf857x_gpio_write(dev, pin, (dev->out & (1 << pin)) ? 0 : 1); +} + +/** Functions for internal use only */ + +#if IS_USED(MODULE_PCF857X_IRQ) + +/* interrupt service routine for IRQs */ +static void _irq_isr(void *arg) +{ + assert(arg != NULL); + + /* just indicate that an interrupt occurred and return */ + event_post(PCF857X_EVENT_PRIO, (event_t*)&((pcf857x_t*)arg)->irq_event); +} + +/* handle one IRQ event of device referenced by the event */ +static void _irq_handler(event_t* event) +{ + pcf857x_irq_event_t* irq_event = (pcf857x_irq_event_t*)event; + + assert(irq_event != NULL); + _acquire(irq_event->dev); + /* _update_state releases the bus */ + _update_state(irq_event->dev); +} + +/* + * @warning: It is expected that the I2C bus is already acquired when the + * function is called. However, it is released by this function + * before the function returns. + */ +static void _update_state(pcf857x_t* dev) +{ + assert(dev != NULL); + DEBUG_DEV("", dev); + + /* save old input values */ + pcf857x_data_t old_in = dev->in; + pcf857x_data_t new_in; + + /* read in new input values and release the bus */ + if (_read(dev, &dev->in)) { + _release(dev); + return; + } + _release(dev); + + new_in = dev->in; + + /* iterate over all pins to check whether ISR has to be called */ + for (unsigned i = 0; i < dev->pin_num; i++) { + pcf857x_data_t mask = 1 << i; + + /* + * if pin is input, interrupt is enabled, has an ISR registered + * and the input value changed + */ + if (((dev->modes & mask) != 0) && dev->enabled[i] && + (dev->isr[i].cb != NULL) && ((old_in ^ new_in) & mask)) { + /* check for the flank and the activated flank mode */ + if ((dev->flank[i] == GPIO_BOTH) || /* no matter what flank */ + ((new_in & mask) == 0 && /* falling flank */ + (dev->flank[i] == GPIO_FALLING)) || + ((new_in & mask) == mask && /* rising flank */ + (dev->flank[i] == GPIO_RISING))) { + + /* call the ISR */ + dev->isr[i].cb(dev->isr[i].arg); + } + } + } +} +#endif /* MODULE_PCF857X_IRQ */ + +static inline void _acquire(const pcf857x_t *dev) +{ + assert(dev != NULL); + i2c_acquire(dev->params.dev); +} + +static inline void _release(const pcf857x_t *dev) +{ + assert(dev != NULL); + i2c_release(dev->params.dev); +} + +static int _read(const pcf857x_t *dev, pcf857x_data_t *data) +{ + assert(dev != NULL); + assert(data != NULL); + + uint8_t bytes[2]; + size_t len = (dev->pin_num == 8) ? 1 : 2; + + int res = i2c_read_bytes(dev->params.dev, dev->params.addr, bytes, len, 0); + + if (res != 0) { + DEBUG_DEV("could not read data, reason %d (%s)", + dev, res, strerror(res * -1)); + return -PCF857X_ERROR_I2C; + } + + if (dev->pin_num == 8) { + *data = bytes[0]; + DEBUG_DEV("data=%02x", dev, *data); + } + else { + *data = (bytes[1] << 8) | bytes[0]; + DEBUG_DEV("data=%04x", dev, *data); + } + + return PCF857X_OK; +} + +static int _write(const pcf857x_t *dev, pcf857x_data_t data) +{ + assert(dev != NULL); + + uint8_t bytes[2]; + size_t len; + + if (dev->pin_num == 8) { + DEBUG_DEV("data=%02x", dev, data & 0xff); + + bytes[0] = data & 0xff; + len = 1; + } + else { + DEBUG_DEV("data=%04x", dev, data); + + bytes[0] = data & 0xff; + bytes[1] = data >> 8; + len = 2; + } + + int res = i2c_write_bytes(dev->params.dev, dev->params.addr, bytes, len, 0); + + if (res != 0) { + DEBUG_DEV("could not write data, reason %d (%s)", + dev, res, strerror(res * -1)); + return -PCF857X_ERROR_I2C; + } + + return PCF857X_OK; +}