From f93f9b77902f8c7c1325ac7232d1f607411ba3db Mon Sep 17 00:00:00 2001 From: Schorcht Date: Thu, 6 Dec 2018 09:47:24 +0100 Subject: [PATCH] drivers: support for PCA9685 PWMs --- drivers/Makefile.dep | 6 + drivers/Makefile.include | 4 + drivers/include/pca9685.h | 322 ++++++++++++++++++++ drivers/pca9685/Makefile | 1 + drivers/pca9685/include/pca9685_params.h | 143 +++++++++ drivers/pca9685/include/pca9685_regs.h | 98 ++++++ drivers/pca9685/pca9685.c | 369 +++++++++++++++++++++++ drivers/pca9685/pca9685_saul.c | 36 +++ 8 files changed, 979 insertions(+) create mode 100644 drivers/include/pca9685.h create mode 100644 drivers/pca9685/Makefile create mode 100644 drivers/pca9685/include/pca9685_params.h create mode 100644 drivers/pca9685/include/pca9685_regs.h create mode 100644 drivers/pca9685/pca9685.c create mode 100644 drivers/pca9685/pca9685_saul.c diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index dc39930244..1c7868f3f9 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -388,6 +388,12 @@ ifneq (,$(filter nvram_spi,$(USEMODULE))) USEMODULE += xtimer endif +ifneq (,$(filter pca9685,$(USEMODULE))) + FEATURES_REQUIRED += periph_gpio + FEATURES_REQUIRED += periph_i2c + USEMODULE += xtimer +endif + ifneq (,$(filter pcd8544,$(USEMODULE))) FEATURES_REQUIRED += periph_gpio FEATURES_REQUIRED += periph_spi diff --git a/drivers/Makefile.include b/drivers/Makefile.include index 4f1efdde79..7816c9ca8c 100644 --- a/drivers/Makefile.include +++ b/drivers/Makefile.include @@ -210,6 +210,10 @@ ifneq (,$(filter nrf24l01p,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/nrf24l01p/include endif +ifneq (,$(filter pca9685,$(USEMODULE))) + USEMODULE_INCLUDES += $(RIOTBASE)/drivers/pca9685/include +endif + ifneq (,$(filter pcd8544,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/pcd8544/include endif diff --git a/drivers/include/pca9685.h b/drivers/include/pca9685.h new file mode 100644 index 0000000000..0703d3ecd7 --- /dev/null +++ b/drivers/include/pca9685.h @@ -0,0 +1,322 @@ +/* + * 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_pca9685 PCA9685 I2C PWM controller + * @ingroup drivers_actuators + * @ingroup drivers_saul + * @brief Device driver for the NXP PCA9685 + * + * ## Overview + * + * The driver supports the NXP PCA9685 16-channel, 12-bit PWM LED controller + * connected to I2C. Although the controller is optimized for LED control, + * the 12-bit resolution also allows the control of servos with a resolution + * of 4 us at 60 Hz refresh signal. + * + * The following features of the PCA9685 are supported by the driver: + * + * - 16 channels with 12-bit resolution + * - Refresh rates from 24 Hz to 1526 Hz with internal 25 MHz oscillator + * - Totem pole outputs with 25 mA as sink and 10 mA as source at 5V + * - Software programmable open-drain output selection + * - Inverted outputs + * - Active LOW Output Enable (OE) input pin + * - External clock input with max. 50 MHz + * + * ## Usage + * + * The driver interface is kept as compatible as possible with the peripheral + * PWM interface. The only differences are that + * + * - functions have the prefix `pca9685_` and + * - functions require an additional parameter, the pointer to the PWM + * device of type #pca9685_t. + * + * Please refer the test application in `tests/driver_pca9685` for an example + * on how to use the driver. + * + * ## SAUL Capabilities + * + * The driver provides SAUL capabilities that are compatible to the SAUL + * actuators of type #SAUL_ACT_SERVO. + * + * Each PCA9685 channel can be mapped directly to SAUL by defining an + * according entry in \c PCA9685_SAUL_PWM_PARAMS. Please refer file + * `$RIOTBASE/drivers/pca9685/include/pca9685_params.h` for an example. + * + * pca9685_saul_pwm_params_t pca9685_saul_pwm_params[] = { + * { + * .name = "PCA9685-0:0", + * .dev = 0, + * .channel = 0, + * .initial = (PCA9685_PARAM_RES >> 1), + * }, + * { + * .name = "PCA9685-0:1", + * .dev = 0, + * .channel = 1, + * .initial = (PCA9685_PARAM_RES >> 2), + * }, + * { + * .name = "PCA9685-0:2", + * .dev = 0, + * .channel = 2, + * .initial = (PCA9685_PARAM_RES >> 3), + * }, + * }; + * + * For each PWM channel that should be used with SAUL, an entry with a name, + * the device, the channel, and the initial value has to be defined as shown + * above. + * + * @{ + * + * @author Gunar Schorcht + * @file + */ + +#ifndef PCA9685_H +#define PCA9685_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + +#include "periph/gpio.h" +#include "periph/i2c.h" +#include "periph/pwm.h" + +/** + * @name PCA9685 I2C slave addresses + * + * PCA9685 offers 64 possible hardware-programmable I2C slave addresses. + * Therefore the I2C slave address is defined as an offset in the range + * from 0 to 63 to a base address #PCA9685_I2C_BASE_ADDR. PCA9685 I2C + * slave addresses are then in the range from #PCA9685_I2C_BASE_ADDR + 0 to + * #PCA9685_I2C_BASE_ADDR + 63 + * + * Four I2C slave addresses have special meaning when they are enabled, the + * All Call address (enabled by default) and three Sub Call addresses + * disabled by default). These addresses can be used to address either all or + * sub groups of PCF9695 controllers at the same time. + * + * @{ + */ +#define PCA9685_I2C_BASE_ADDR (0x40) /**< I2C slave base address */ +#define PCA9685_I2C_ALLCALLADDR (0x70) /**< Default All Call adress */ +#define PCA9685_I2C_SUBADR1 (0x71) /**< Default Sub Call adress 1 */ +#define PCA9685_I2C_SUBADR2 (0x72) /**< Default Sub Call adress 2 */ +#define PCA9685_I2C_SUBADR3 (0x73) /**< Default Sub Call adress 3 */ +/** @} */ + +/** + * @brief Number of PWM channels provided by PCA9685 + */ +#define PCA9685_CHANNEL_NUM (16U) + +/** + * @brief Internal PCA9685 channel resolution is 12-bit + */ +#define PCA9685_RESOLUTION (1 << 12) + +/** + * @brief Internal PCA9685 oscilator frequency is 25 MHz + */ +#define PCA9685_OSC_FREQ (25000000) + +/** + * @brief Maximum external clock frequency is 50 MHz + */ +#define PCA9685_EXT_FERQ_MAX (50000000) + +/** + * @brief PCA9685 driver error codes + */ +typedef enum { + PCA9685_OK = 0, /**< Success */ + PCA9685_ERROR_I2C = 1, /**< I2C communication error */ +} pca9685_error_t; + +/** + * @brief PCA9685 output driver mode + * + * The output driver mode defines how the outputs are configured. + */ +typedef enum { + PCA9685_OPEN_DRAIN = 0, /**< Open-drain structure ouptut */ + PCA9685_TOTEM_POLE = 1, /**< Totem pole structure output */ +} pca9685_out_drv_t; + +/** + * @brief PCA9685 output-not-enabled mode + * + * The output-not-enabled mode defines how the outputs behave when the + * active LOW output enable pin /OE is HIGH. + */ +typedef enum { + PCA9685_OFF = 0, /**< If /OE pin is HIGH, outputs are LOW */ + PCA9685_OUT_DRV = 1, /**< Outputs depend on the output driver mode + pca9685_params_t::out_drv. If /OE pin is HIGH, + it is high-impedance for PCA9685_OPEN_DRAIN, + and HIGH for PCA9685_TOTEM_POLE */ + PCA9685_HIHGH_Z = 2, /**< If /OE pin is HIGH, outputs are high-impedance */ +} pca9685_out_ne_t; + +/** + * @brief PCA9685 device initialization parameters + */ +typedef struct { + + i2c_t i2c_dev; /**< I2C device, default I2C_DEV(0) */ + uint8_t i2c_addr; /**< I2C slave address */ + + pwm_mode_t mode; /**< PWM mode for all channels: #PWM_LEFT, #PWM_CENTER, + #PWM_RIGHT supported, (default PWM_CENTER) */ + uint32_t freq; /**< PWM frequency in Hz (default 100) */ + uint16_t res; /**< PWM resolution (default 4096) */ + + bool inv; /**< Invert outputs, e.g., for LEDs (default yes) */ + + uint32_t ext_freq; /**< If not 0, EXTCLK pin is used with this frequency */ + gpio_t oe_pin; /**< Active LOW output enable pin /OE. If #GPIO_UNDEF, + the pin is not used. (default #GPIO_UNDEF). */ + pca9685_out_drv_t out_drv; /**< Output driver mode */ + pca9685_out_ne_t out_ne; /**< Output-not-enabled mode */ + +} pca9685_params_t; + +/** + * @brief PCA9685 PWM device data structure type + */ +typedef struct { + + pca9685_params_t params; /**< Device initialization parameters */ + bool powered_on; /**< Devices is powered on if true */ + +} pca9685_t; + +#if MODULE_SAUL || DOXYGEN +/** + * @brief PCA9685 configuration structure for mapping PWM channels to SAUL + */ +typedef struct { + const char *name; /**< name of the PCA9685 device */ + unsigned int dev; /**< index of the PCA9685 device */ + uint8_t channel; /**< channel of the PCA9685 device */ + uint16_t initial; /**< initial duty-cycle value*/ +} pca9685_saul_pwm_params_t; +#endif + +/** + * @brief Initialize the PCA9685 PWM device driver + * + * The function initializes the driver. After calling this function, the PWM + * device is in low-power sleep mode (powered off), all outputs off. Before + * the PWM device can be used, it has to be initialized with #pca9685_pwm_init. + * + * @param[in] dev Device descriptor of the PCA9685 to be initialized + * @param[in] params Configuration parameters, see #pca9685_params_t + * + * @retval PCA9685_OK on success + * @retval PCA9685_ERROR_* a negative error code on error, see + * #pca9685_error_t + */ +int pca9685_init(pca9685_t *dev, const pca9685_params_t *params); + +/** + * @brief Initialize the PCA9685 PWM device + * + * The function initializes the PWM device with the given parameters that are + * used for all channels. After calling this funcion, the PWM device is + * operational (powered on). That is, all outputs are active with the given + * parameters and the same duty cycle value as before the call. + * + * @note + * - PCA9685 works with internally with a resolution of 12 bit = 4096. + * Using a resolution that is not a power of two, will cause inaccuracy due + * to aligment errors when scaling down the internal resolution to the + * configured resolution. + * - Frequencies from 24 Hz to 1526 Hz can be used with PCF9865. + * + * @param[in] dev Device descriptor of the PCA9685 + * @param[in] mode PWM mode, left, right or center aligned + * @param[in] freq PWM frequency in Hz [24...1526] + * @param[in] res PWM resolution [2...4096], should be a power of two + * + * @retval >0 actual frequency on success + * @retval 0 on error + */ +uint32_t pca9685_pwm_init(pca9685_t *dev, pwm_mode_t mode, uint32_t freq, + uint16_t res); + +/** + * @brief Set the duty-cycle for a given channel or all channels of the + * given PCA9685 PWM device + * + * The duty-cycle is set in relation to the chosen resolution of the given + * device. If value > resolution, value is set to resolution. + * + * If the given channel is #PCA9685_CHANNEL_NUM, all channels are set to the + * same duty cycle at the same time with only one I2C bus access. + * + * @param[in] dev Device descriptor of the PCA9685 + * @param[in] channel Channel of PCA9685 to set, if #PCA9685_CHANNEL_NUM + * all channels are set to the desired duty-cycle + * @param[in] value Desired duty-cycle to set + */ +void pca9685_pwm_set(pca9685_t *dev, uint8_t channel, uint16_t value); + +/** + * @brief Resume PWM generation on the given PCA9685 device + * + * When this function is called, the given PWM device is powered on and + * continues its previously configured operation. The duty cycle of each channel + * will be the value that was last set. + * + * This function must not be called before the PWM device was initialized. + * + * @param[in] dev Device descriptor of the PCA9685 + */ +void pca9685_pwm_poweron(pca9685_t *dev); + +/** + * @brief Stop the PWM generation on the given PCA9685 device + * + * This function switches the PCA9685 into sleep mode which turns off the + * internal oscillator. This disables the PWM generation on all configured. + * If the active LOW output enable pin /OE is used, the signal is set to HIGH. + * Dependent on the pca9685_params_t::out_drv and pca9685_params_t::out_ne + * parameters, the outputs are set 0, 1 or high-impedance. All channel + * duty-cycle values are preserved. + * + * @param[in] dev Device descriptor of the PCA9685 + */ +void pca9685_pwm_poweroff(pca9685_t *dev); + +/** + * @brief Get the number of available channels of the given PCA9685 device + * @param[in] dev Device descriptor of the PCA9685 + * @return Number of channels available + */ +static inline uint8_t pca9685_pwm_channels(pca9685_t *dev) +{ + (void)dev; + return PCA9685_CHANNEL_NUM; +} + +#ifdef __cplusplus +} +#endif + +#endif /* PCA9685_H */ +/** @} */ diff --git a/drivers/pca9685/Makefile b/drivers/pca9685/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/pca9685/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/pca9685/include/pca9685_params.h b/drivers/pca9685/include/pca9685_params.h new file mode 100644 index 0000000000..d607de4d96 --- /dev/null +++ b/drivers/pca9685/include/pca9685_params.h @@ -0,0 +1,143 @@ +/* + * 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_pca9685 + * @brief Default configuration for the PCA9685 I2C PWM controller + * @author Gunar Schorcht + * @file + * @{ + */ + +#ifndef PCA9685_PARAMS_H +#define PCA9685_PARAMS_H + +#include "board.h" +#include "saul_reg.h" +#include "pca9685.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters + * @{ + */ +#ifndef PCA9685_PARAM_DEV +/** device is I2C_DEV(0) */ +#define PCA9685_PARAM_DEV I2C_DEV(0) +#endif + +#ifndef PCA9685_PARAM_ADDR +/** device address is PCA9685_I2C_ADDR */ +#define PCA9685_PARAM_ADDR (PCA9685_I2C_BASE_ADDR + 0) +#endif + +#ifndef PCA9685_PARAM_INV +/** Invert outputs: yes */ +#define PCA9685_PARAM_INV (false) +#endif + +#ifndef PCA9685_PARAM_MODE +/** PWM mode for all channels: PWM_LEFT */ +#define PCA9685_PARAM_MODE (PWM_LEFT) +#endif + +#ifndef PCA9685_PARAM_FREQ +/** PWM frequency in Hz: 100 */ +#define PCA9685_PARAM_FREQ (100) +#endif + +#ifndef PCA9685_PARAM_RES +/** PWM resolution: 4096 */ +#define PCA9685_PARAM_RES (4096) +#endif + +#ifndef PCA9685_PARAM_OE_PIN +/** Output enable pin: not used */ +#define PCA9685_PARAM_OE_PIN (GPIO_UNDEF) +#endif + +#ifndef PCA9685_PARAM_EXT_FREQ +/** EXTCLK frequency and pin: not used */ +#define PCA9685_PARAM_EXT_FREQ (0) +#endif + +#ifndef PCA9685_PARAM_OUT_DRV +/** Output driver mode: totem pole */ +#define PCA9685_PARAM_OUT_DRV (PCA9685_TOTEM_POLE) +#endif + +#ifndef PCA9685_PARAM_OUT_NE +/** Output driver mode: totem pole */ +#define PCA9685_PARAM_OUT_NE (PCA9685_OFF) +#endif + +#ifndef PCA9685_PARAMS +#define PCA9685_PARAMS { \ + .i2c_dev = PCA9685_PARAM_DEV, \ + .i2c_addr = PCA9685_PARAM_ADDR, \ + .inv = PCA9685_PARAM_INV, \ + .mode = PCA9685_PARAM_MODE, \ + .freq = PCA9685_PARAM_FREQ, \ + .res = PCA9685_PARAM_RES, \ + .ext_freq = PCA9685_PARAM_EXT_FREQ, \ + .oe_pin = PCA9685_PARAM_OE_PIN, \ + .out_drv = PCA9685_PARAM_OUT_DRV, \ + .out_ne = PCA9685_PARAM_OUT_NE, \ + } +#endif /* PCA9685_PARAMS */ + +#ifndef PCA9685_SAUL_PWM_PARAMS +/** Example for mapping PWM channels to SAUL */ +#define PCA9685_SAUL_PWM_PARAMS { \ + .name = "PCA9685-0:0", \ + .dev = 0, \ + .channel = 0, \ + .initial = (PCA9685_PARAM_RES >> 1), \ + }, \ + { \ + .name = "PCA9685-0:1", \ + .dev = 0, \ + .channel = 1, \ + .initial = (PCA9685_PARAM_RES >> 2), \ + }, \ + { \ + .name = "PCA9685-0:1", \ + .dev = 0, \ + .channel = 2, \ + .initial = (PCA9685_PARAM_RES >> 3), \ + }, +#endif /* PCA9685_PARAMS */ +/**@}*/ + +/** + * @brief Allocate some memory to store the actual configuration + */ +static const pca9685_params_t pca9685_params[] = +{ + PCA9685_PARAMS +}; + +#if MODULE_SAUL || DOXYGEN +/** + * @brief Additional meta information to keep in the SAUL registry + */ +static const pca9685_saul_pwm_params_t pca9685_saul_pwm_params[] = +{ + PCA9685_SAUL_PWM_PARAMS +}; +#endif /* MODULE_SAUL || DOXYGEN */ + +#ifdef __cplusplus +} +#endif + +#endif /* PCA9685_PARAMS_H */ +/** @} */ diff --git a/drivers/pca9685/include/pca9685_regs.h b/drivers/pca9685/include/pca9685_regs.h new file mode 100644 index 0000000000..1ddee9db8e --- /dev/null +++ b/drivers/pca9685/include/pca9685_regs.h @@ -0,0 +1,98 @@ +/* + * 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_pca9685 + * @brief Register definitions for the PCA9685 I2C PWM controller + * @author Gunar Schorcht + * @file + * @{ + */ + +#ifndef PCA9685_REGS_H +#define PCA9685_REGS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @name Register addresses + * @{ + */ +#define PCA9685_REG_MODE1 (0x00) /**< Mode1 register */ +#define PCA9685_REG_MODE2 (0x01) /**< Mode2 register */ +#define PCA9685_REG_SUBADR1 (0x02) /**< I2C bus subaddress 1 register */ +#define PCA9685_REG_SUBADR2 (0x03) /**< I2C bus subaddress 2 register */ +#define PCA9685_REG_SUBADR3 (0x04) /**< I2C bus subaddress 3 register */ +#define PCA9685_REG_ALLCALLADDR (0x05) /**< LED All Call I 2 C-bus address */ +#define PCA9685_REG_LED0_ON_L (0x06) /**< LED0 ON control low byte */ + +#define PCA9685_REG_ALL_LED_ON_L (0xfa) /**< Load all LEDn_OFF register low byte */ +#define PCA9685_REG_ALL_LED_ON_H (0xfb) /**< Load all LEDn_OFF register high byte */ +#define PCA9685_REG_ALL_LED_OFF_L (0xfc) /**< Load all LEDn_OFF register low byte */ +#define PCA9685_REG_ALL_LED_OFF_H (0xfd) /**< Load all LEDn_OFF register high byte */ + +#define PCA9685_REG_ALL_LED_ON (0xfa) /**< Load all LEDn_OFF register word */ +#define PCA9685_REG_ALL_LED_OFF (0xfc) /**< Load all LEDn_OFF register word */ + +#define PCA9685_REG_PRE_SCALE (0xfe) /**< Prescaler for PWM output frequency */ +#define PCA9685_REG_TEST_MODE (0xff) /**< Enter test mode register */ + +#define PCA9685_REG_LED_ON(n) (0x06 + (n << 2)) /**< LEDn ON control word */ +#define PCA9685_REG_LED_OFF(n) (0x08 + (n << 2)) /**< LEDn OFF control word */ + +#define PCA9685_REG_LED_ON_L(n) (0x06 + (n << 2)) /**< LEDn ON control low byte */ +#define PCA9685_REG_LED_ON_H(n) (0x07 + (n << 2)) /**< LEDn ON control high byte */ +#define PCA9685_REG_LED_OFF_L(n) (0x08 + (n << 2)) /**< LEDn OFF control low byte */ +#define PCA9685_REG_LED_OFF_H(n) (0x09 + (n << 2)) /**< LEDn OFF control high byte */ +/** @} */ + +/** + * @name Register structures + * @{ + */ + +/* PCA9685_REG_MODE1 */ +#define PCA9685_MODE1_RESTART (0x80) /**< State of restart logic, write 1 to clear */ +#define PCA9685_MODE1_EXTCLK (0x40) /**< Use EXTCLK pin */ +#define PCA9685_MODE1_AI (0x20) /**< Enable register auto-increment*/ +#define PCA9685_MODE1_SLEEP (0x10) /**< Enter low power mode, PWM is off */ +#define PCA9685_MODE1_SUB1 (0x08) /**< Enable I2C subaddress 1 */ +#define PCA9685_MODE1_SUB2 (0x04) /**< Enable I2C subaddress 2 */ +#define PCA9685_MODE1_SUB3 (0x02) /**< Enable I2C subaddress 3 */ +#define PCA9685_MODE1_ALLCALL (0x01) /**< Enable I2C all call address */ + +/* PCA9685_REG_MODE2 */ +#define PCA9685_MODE2_INVERT (0x10) /**< Invert outputs */ +#define PCA9685_MODE2_OCH (0x08) /**< Output change change configuration */ +#define PCA9685_MODE2_OUTDRV (0x04) /**< Output driver configuration */ +#define PCA9685_MODE2_OUTNE (0x03) /**< Output enabled configuration */ +/** @} */ + +/** + * @name Register value definitions + * @{ + */ +#define PCA9685_LED_ON (0x1000) /* LEDs on word */ +#define PCA9685_LED_OFF (0x1000) /* LEDs off word */ +#define PCA9685_LED_ON_H (0x10) /* LEDs on high byte */ +#define PCA9685_LED_OFF_H (0x10) /* LEDs off high byte */ +#define PCA9685_ALL_LED_ON (0x1000) /* All LEDs on word */ +#define PCA9685_ALL_LED_OFF (0x1000) /* All LEDs off word */ +#define PCA9685_ALL_LED_ON_H (0x10) /* All LEDs on high byte */ +#define PCA9685_ALL_LED_OFF_H (0x10) /* All LEDs off word */ +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* PCA9685_REGS_H */ +/** @} */ diff --git a/drivers/pca9685/pca9685.c b/drivers/pca9685/pca9685.c new file mode 100644 index 0000000000..5c8bda9a06 --- /dev/null +++ b/drivers/pca9685/pca9685.c @@ -0,0 +1,369 @@ +/* + * 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_pca9685 + * @brief Device driver for the PCA9685 I2C PWM controller + * @author Gunar Schorcht + * @file + * @{ + */ + +#include +#include +#include + +#include "pca9685_regs.h" +#include "pca9685.h" + +#include "irq.h" +#include "log.h" +#include "xtimer.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#if ENABLE_DEBUG + +#define ASSERT_PARAM(cond) \ + do { \ + if (!(cond)) { \ + DEBUG("[pca9685] %s: %s\n", \ + __func__, "parameter condition (" #cond ") not fulfilled"); \ + assert(cond); \ + } \ + } while(0) + +#define DEBUG_DEV(f, d, ...) \ + DEBUG("[pca9685] %s i2c dev=%d addr=%02x: " f "\n", \ + __func__, d->params.i2c_dev, dev->params.i2c_addr, ## __VA_ARGS__) + +#else /* ENABLE_DEBUG */ + +#define ASSERT_PARAM(cond) assert(cond) +#define DEBUG_DEV(f, d, ...) + +#endif /* ENABLE_DEBUG */ +#define ERROR_DEV(f, d, ...) \ + LOG_ERROR("[pca9685] %s i2c dev=%d addr=%02x: " f "\n", \ + __func__, d->params.i2c_dev, dev->params.i2c_addr, ## __VA_ARGS__) + +#define EXEC_RET(f) \ + do { \ + int _r; \ + if ((_r = f) != PCA9685_OK) { \ + DEBUG("[pca9685] %s: error code %d\n", __func__, _r); \ + return _r; \ + } \ + } while(0) + +#define EXEC_RET_CODE(f, c) \ + do { \ + int _r; \ + if ((_r = f) != PCA9685_OK) { \ + DEBUG("[pca9685] %s: error code %d\n", __func__, _r); \ + return c; \ + } \ + } while(0) + +#define EXEC(f) \ + do { \ + int _r; \ + if ((_r = f) != PCA9685_OK) { \ + DEBUG("[pca9685] %s: error code %d\n", __func__, _r); \ + return; \ + } \ + } while(0) + +/** Forward declaration of functions for internal use */ +static int _is_available(const pca9685_t *dev); +static int _init(pca9685_t *dev); + +static void _set_reg_bit(uint8_t *byte, uint8_t mask, uint8_t bit); + +static int _read(const pca9685_t *dev, uint8_t reg, uint8_t *data, uint32_t len); +static int _write(const pca9685_t *dev, uint8_t reg, const uint8_t *data, uint32_t len); +static int _update(const pca9685_t *dev, uint8_t reg, uint8_t mask, uint8_t data); + +inline static int _write_word(const pca9685_t *dev, uint8_t reg, uint16_t word); + +int pca9685_init(pca9685_t *dev, const pca9685_params_t *params) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(params != NULL); + ASSERT_PARAM(params->ext_freq <= 50000000); + + dev->powered_on = false; + dev->params = *params; + + DEBUG_DEV("params=%p", dev, params); + + if (dev->params.oe_pin != GPIO_UNDEF) { + /* init the pin an disable outputs first */ + gpio_init(dev->params.oe_pin, GPIO_OUT); + gpio_set(dev->params.oe_pin); + } + + /* test whether PWM device is available */ + EXEC_RET(_is_available(dev)); + + /* init the PWM device */ + EXEC_RET(_init(dev)); + + return PCA9685_OK; +} + +uint32_t pca9685_pwm_init(pca9685_t *dev, pwm_mode_t mode, uint32_t freq, + uint16_t res) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(freq >= 24 && freq <= 1526); + ASSERT_PARAM(res >= 2 && res <= 4096); + ASSERT_PARAM(mode == PWM_LEFT || mode == PWM_CENTER || mode == PWM_RIGHT); + + DEBUG_DEV("mode=%u freq=%"PRIu32" res=%u", dev, mode, freq, res); + + dev->params.mode = mode; + dev->params.freq = freq; + dev->params.res = res; + + /* prescale can only be set while in sleep mode (powered off) */ + if (dev->powered_on) { + pca9685_pwm_poweroff(dev); + } + + /* prescale = round(clk / (PCA9685_RESOLUTION * freq)) - 1; */ + uint32_t div = PCA9685_RESOLUTION * freq; + uint8_t byte = ((dev->params.ext_freq ? dev->params.ext_freq + : PCA9685_OSC_FREQ) + div/2) / div - 1; + + EXEC_RET(_write(dev, PCA9685_REG_PRE_SCALE, &byte, 1)); + + if (!dev->powered_on) { + pca9685_pwm_poweron(dev); + } + + return freq; +} + +void pca9685_pwm_set(pca9685_t *dev, uint8_t chn, uint16_t val) +{ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(chn <= PCA9685_CHANNEL_NUM); + + DEBUG_DEV("chn=%u val=%u", dev, chn, val); + + /* limit val to resolution */ + val = (val >= dev->params.res) ? dev->params.res : val; + + uint16_t on; + uint16_t off; + + if (val == 0) { + /* full off */ + on = 0; + off = PCA9685_LED_OFF; + } + else if (val == dev->params.res) { + /* full on */ + on = PCA9685_LED_ON; + off = 0; + } + else { + /* duty = scale(2^12) / resolution * value */ + uint32_t duty = PCA9685_RESOLUTION * val / dev->params.res; + switch (dev->params.mode) { + case PWM_LEFT: on = 0; + off = on + duty; + break; + case PWM_CENTER: on = (PCA9685_RESOLUTION - duty) >> 1; + off = on + duty; + break; + case PWM_RIGHT: off = PCA9685_RESOLUTION - 1; + on = off - duty; + break; + default: return; + } + } + + DEBUG_DEV("on=%u off=%u", dev, on, off); + + if (chn == PCA9685_CHANNEL_NUM) { + EXEC(_write_word(dev, PCA9685_REG_ALL_LED_ON, on)); + EXEC(_write_word(dev, PCA9685_REG_ALL_LED_OFF, off)); + } + else { + EXEC(_write_word(dev, PCA9685_REG_LED_ON(chn), on)); + EXEC(_write_word(dev, PCA9685_REG_LED_OFF(chn), off)); + } +} + +void pca9685_pwm_poweron(pca9685_t *dev) +{ + ASSERT_PARAM(dev != NULL); + DEBUG_DEV("", dev); + + uint8_t byte; + /* read MODE1 register */ + EXEC(_read(dev, PCA9685_REG_MODE1, &byte, 1)); + + /* check if RESTART bit is 1 */ + if (byte & PCA9685_MODE1_RESTART) { + /* clear the SLEEP bit */ + byte &= ~PCA9685_MODE1_SLEEP; + EXEC(_write(dev, PCA9685_REG_MODE1, &byte, 1)); + /* allow 500 us for oscilator to stabilize */ + xtimer_usleep(500); + /* clear the RESTART bit to start all PWM channels*/ + EXEC(_update(dev, PCA9685_REG_MODE1, PCA9685_MODE1_RESTART, 1)); + } + else { + EXEC(_update(dev, PCA9685_REG_MODE1, PCA9685_MODE1_SLEEP, 0)); + /* allow 500 us for oscilator to stabilize */ + xtimer_usleep(500); + /* clear the RESTART bit to start all PWM channels*/ + EXEC(_update(dev, PCA9685_REG_MODE1, PCA9685_MODE1_RESTART, 1)); + } + + if (dev->params.oe_pin != GPIO_UNDEF) { + gpio_clear(dev->params.oe_pin); + } + + dev->powered_on = true; +} + +void pca9685_pwm_poweroff(pca9685_t *dev) +{ + ASSERT_PARAM(dev != NULL); + DEBUG_DEV("", dev); + + if (dev->params.oe_pin != GPIO_UNDEF) { + gpio_set(dev->params.oe_pin); + } + + /* set sleep mode */ + EXEC(_update(dev, PCA9685_REG_MODE1, PCA9685_MODE1_SLEEP, 1)); + + dev->powered_on = false; +} + +/** Functions for internal use only */ + +static int _is_available(const pca9685_t *dev) +{ + uint8_t byte; + + /* simply tests to read */ + return _read(dev, PCA9685_REG_MODE1, &byte, 1); +} + +static int _init(pca9685_t *dev) +{ + /* set Auto-Increment flag */ + EXEC_RET(_update(dev, PCA9685_REG_MODE1, PCA9685_MODE1_AI, 1)); + + /* switch off all channels */ + EXEC_RET(_write_word(dev, PCA9685_REG_ALL_LED_OFF, PCA9685_ALL_LED_OFF)); + + /* set Auto-Increment flag */ + uint8_t byte = 0; + _set_reg_bit(&byte, PCA9685_MODE2_INVERT, dev->params.inv); + _set_reg_bit(&byte, PCA9685_MODE2_OUTDRV, dev->params.out_drv); + _set_reg_bit(&byte, PCA9685_MODE2_OUTNE, dev->params.out_ne); + EXEC_RET(_write(dev, PCA9685_REG_MODE2, &byte, 1)); + + /* set Sleep mode, Auto-Increment, Restart, All call and External Clock */ + byte = 0; + _set_reg_bit(&byte, PCA9685_MODE1_AI, 1); + _set_reg_bit(&byte, PCA9685_MODE1_SLEEP, 1); + _set_reg_bit(&byte, PCA9685_MODE1_RESTART, 1); + _set_reg_bit(&byte, PCA9685_MODE1_ALLCALL, 1); + EXEC_RET(_write(dev, PCA9685_REG_MODE1, &byte, 1)); + + /* must be done only in sleep mode */ + _set_reg_bit(&byte, PCA9685_MODE1_EXTCLK, dev->params.ext_freq ? 1 : 0); + EXEC_RET(_write(dev, PCA9685_REG_MODE1, &byte, 1)); + + return PCA9685_OK; +} + +static int _read(const pca9685_t *dev, uint8_t reg, uint8_t *data, uint32_t len) +{ + DEBUG_DEV("reg=%02x data=%p len=%"PRIu32"", dev, reg, data, len); + + /* acquire the I2C device */ + if (i2c_acquire(dev->params.i2c_dev)) { + DEBUG_DEV("could not aquire I2C bus", dev); + return -PCA9685_ERROR_I2C; + } + + if (i2c_read_regs(dev->params.i2c_dev, + dev->params.i2c_addr, reg, data, len, 0) != 0) { + i2c_release(dev->params.i2c_dev); + return -PCA9685_ERROR_I2C; + } + + /* release the I2C device */ + i2c_release(dev->params.i2c_dev); + + return PCA9685_OK; +} + +static int _write(const pca9685_t *dev, uint8_t reg, const uint8_t *data, uint32_t len) +{ + DEBUG_DEV("reg=%02x data=%p len=%"PRIu32"", dev, reg, data, len); + + if (i2c_acquire(dev->params.i2c_dev)) { + DEBUG_DEV("could not aquire I2C bus", dev); + return -PCA9685_ERROR_I2C; + } + + if (i2c_write_regs(dev->params.i2c_dev, + dev->params.i2c_addr, reg, data, len, 0) != 0) { + i2c_release(dev->params.i2c_dev); + return -PCA9685_ERROR_I2C; + } + + /* release the I2C device */ + i2c_release(dev->params.i2c_dev); + + return PCA9685_OK; +} + +inline static int _write_word(const pca9685_t *dev, uint8_t reg, uint16_t data) +{ + uint8_t bytes[2] = { data & 0xff, (data >> 8) & 0xff }; + return _write (dev, reg, bytes, 2); +} + +static int _update(const pca9685_t *dev, uint8_t reg, uint8_t mask, uint8_t data) +{ + uint8_t byte; + + /* read current register value */ + EXEC_RET(_read(dev, reg, &byte, 1)); + + /* set masked bits to the given value */ + _set_reg_bit(&byte, mask, data); + + /* write back new register value */ + EXEC_RET(_write(dev, reg, &byte, 1)); + + return PCA9685_OK; +} + +static void _set_reg_bit(uint8_t *byte, uint8_t mask, uint8_t bit) +{ + uint8_t shift = 0; + while (!((mask >> shift) & 0x01)) { + shift++; + } + *byte = ((*byte & ~mask) | ((bit << shift) & mask)); +} diff --git a/drivers/pca9685/pca9685_saul.c b/drivers/pca9685/pca9685_saul.c new file mode 100644 index 0000000000..18da35493b --- /dev/null +++ b/drivers/pca9685/pca9685_saul.c @@ -0,0 +1,36 @@ +/* + * 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_pca9685 + * @brief PCA9685 adaption to the RIOT actuator/sensor interface + * @author Gunar Schorcht + * @file + */ +#if MODULE_SAUL + +#include + +#include "saul.h" +#include "pca9685.h" + +extern pca9685_t pca9685_devs[]; + +static int set(const void *dev, phydat_t *data) +{ + const pca9685_saul_pwm_params_t *p = (const pca9685_saul_pwm_params_t *)dev; + pca9685_pwm_set(&pca9685_devs[p->dev], p->channel, (uint16_t)data->val[0]); + return 1; +} + +const saul_driver_t pca9685_pwm_saul_driver = { + .read = saul_notsup, + .write = set, + .type = SAUL_ACT_SERVO +}; +#endif /* MODULE_SAUL */