From 499cbb90627d6e4c486d0a3b90381dfb411f285e Mon Sep 17 00:00:00 2001 From: Gunar Schorcht Date: Mon, 6 Dec 2021 23:49:18 +0100 Subject: [PATCH 1/4] drivers/mcp47xx: add driver for MCP47xx DAC devices --- drivers/include/mcp47xx.h | 349 +++++++++++++++++++++++ drivers/mcp47xx/Makefile | 1 + drivers/mcp47xx/Makefile.dep | 1 + drivers/mcp47xx/Makefile.include | 2 + drivers/mcp47xx/include/mcp47xx_params.h | 109 +++++++ drivers/mcp47xx/mcp47xx.c | 298 +++++++++++++++++++ 6 files changed, 760 insertions(+) create mode 100644 drivers/include/mcp47xx.h create mode 100644 drivers/mcp47xx/Makefile create mode 100644 drivers/mcp47xx/Makefile.dep create mode 100644 drivers/mcp47xx/Makefile.include create mode 100644 drivers/mcp47xx/include/mcp47xx_params.h create mode 100644 drivers/mcp47xx/mcp47xx.c diff --git a/drivers/include/mcp47xx.h b/drivers/include/mcp47xx.h new file mode 100644 index 0000000000..2c0165944f --- /dev/null +++ b/drivers/include/mcp47xx.h @@ -0,0 +1,349 @@ +/* + * 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. + */ + +/** + * @defgroup drivers_mcp47xx MCP47xx DAC with I2C interface + * @ingroup drivers_saul + * @ingroup drivers_actuators + * @brief Device driver for Microchip MCP47xx DAC with I2C interface + * + * ## Overview + * + * The driver supports the different Microchip MCP47xx DAC variants. + * + * Expander | Channels | Resolution + * :--------|:--------:|:----------: + * MCP4706 | 1 | 8-bit + * MCP4716 | 1 | 10-bit + * MCP4725 | 1 | 12-bit + * MCP4726 | 1 | 12-bit + * MCP4728 | 4 | 12-bit + * + * @note The following features of MCP47xx DAC devices are not supported at the + * moment: + * - configuring and reading address bits to/from EEPROM + * - writing DAC channel output values to the EEPROM + * - setting the UDAC bit and using the LDAC pin for MCP4728 + * + * ## Usage + * + * Multiple MCP47xx DAC devices and different variants can be used at the + * same time. + * + * The driver interface is kept as compatible as possible with the peripheral + * DAC interface. The only differences are that + * + * - functions have the prefix `mcp47xx_` and + * - functions require an additional parameter, the pointer to the MCP47xx + * device of type #mcp47xx_t. + * + * Please refer the test application in `tests/driver_mcp47xx` for an example + * on how to use the driver. + * + * ## SAUL Capabilities + * + * The driver provides SAUL capabilities that are compatible with SAUL + * actuators of type #SAUL_ACT_DIMMER. + * Each MCP47xx channel can be mapped directly to SAUL by defining an + * according entry in \c MCP47XX_SAUL_DAC_PARAMS. Please refer file + * `$RIOTBASE/drivers/mcp47xx/include/mcp47xx_params.h` for an example. + * + * mcp47xx_saul_dac_params_t mcp47xx_saul_dac_params[] = { + * { + * .name = "DAC00", + * .dev = 0, + * .channel = 0, + * .initial = 32768, + * }, + * }; + * + * For each DAC 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. + * + * ## Using Multiple Devices + * + * It is possible to used multiple devices and different variants of MCP47xx + * DAC devices at the same time. The application has to configure all + * devices by defining the configuration parameter set `mcp47xx_params` + * of type #mcp47xx_params_t. As an example, the default configuration for + * one MCP4725 device is defined in `drivers/mcp47xx/mcp47xx_params.h`. + * + * The application can override it by placing a file `mcp47xx_params.h` in + * the application directory `$(APPDIR)`. For example, the definition of + * the configuration parameter set for the two devices (one MCP4725 and one + * MCP4728) could looks like: + * + * static const mcp47xx_params_t mcp47xx_params[] = { + * { + * .variant = MCP4725, + * .dev = I2C_DEV(0), + * .addr = MCP47XX_BASE_ADDR + 2, + * .gain = MCP47XX_GAIN_1X, + * .vref = MCP47XX_VDD, + * .pd_mode = MCP47XX_PD_LARGE, + * }, + * { + * .variant = MCP4728, + * .dev = I2C_DEV(0), + * .addr = MCP47XX_BASE_ADDR + 3, + * .gain = MCP47XX_GAIN_2X, + * .vref = MCP47XX_VREF_INT, + * .pd_mode = MCP47XX_PD_LARGE, + * }, + * }; + * + * @{ + * + * @author Gunar Schorcht + * @file + */ + +#ifndef MCP47XX_H +#define MCP47XX_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + +#include "kernel_defines.h" +#include "periph/dac.h" +#include "periph/gpio.h" +#include "periph/i2c.h" + +/** + * @name MCP47xx I2C slave addresses + * + * MCP47xx 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 MCP47XX_BASE_ADDR (0x60) /**< MCP47xx I2C slave base address. + Addresses are then in the range from + 0x60 to 0x67 */ +/** @} */ + +/** + * @name MCP47xx Channel number + * @{ + */ +#define MCP4706_CHN_NUM (1) /**< MCP4706 has 1 channel */ +#define MCP4716_CHN_NUM (1) /**< MCP4716 has 1 channel */ +#define MCP4725_CHN_NUM (1) /**< MCP4725 has 1 channel */ +#define MCP4726_CHN_NUM (1) /**< MCP4726 has 1 channel */ +#define MCP4728_CHN_NUM (4) /**< MCP4728 has 4 channels */ +#define MCP47XX_CHN_NUM_MAX (4) /**< maximum number of channels */ +/** @} */ + +/** + * @brief MCP47xx driver error codes */ +typedef enum { + MCP47XX_OK, /**< success */ + MCP47XX_ERROR_I2C, /**< I2C communication error */ + MCP47XX_ERROR_NOT_AVAIL, /**< device not available */ +} mcp47xx_error_codes_t; + +/** + * @brief Supported MCP47xx variants + * + * It is used in configuration parameters to specify the MCP47xx expander + * used by device. + */ +typedef enum { + MCP4706, /**< 1 channel 8-bit DAC */ + MCP4716, /**< 1 channel 10-bit DAC */ + MCP4725, /**< 1 channel 12-bit DAC */ + MCP4726, /**< 1 channel 12-bit DAC */ + MCP4728, /**< 4 channel 12-bit DAC */ +} mcp47xx_variant_t; + +/** + * @brief MCP47xx gain configuration type + * + * @note Gains are not supported by MCP4725. + */ +typedef enum { + MCP47XX_GAIN_1X = 0, /**< Gain is 1.0, supported by all MCP47xx variants */ + MCP47XX_GAIN_2X = 1, /**< Gain is 2.0, not supported by MCP4725 */ +} mcp47xx_gain_t; + +/** + * @brief MCP47xx V_REF configuration type + * + * @note Different MCP47xx variants allow different V_REF configurations + */ +typedef enum { + MCP47XX_VREF_VDD = 0, /**< V_REF = V_DD, supported by all MCP47xx */ + MCP47XX_VREF_INT = 1, /**< V_REF = internal (2.048 V), MCP4728 only */ + MCP47XX_VREF_PIN = 2, /**< V_REF = VREF pin not buffered, MCP47x6 only */ + MCP47XX_VREF_BUF = 3, /**< V_REF = VREF pin buffered, MCP47x6 only */ +} mcp47xx_vref_t; + +/** + * @brief MCP47xx Power-down mode selection type + * + * Defines the possible power-down modes used for MCP47xx device configuration. + * The mode is used by function #mcp47xx_dac_poweroff to set the DAC into the + * configured power-down mode. + * + * @note #MCP47XX_NORMAL cannot be configured as power-down mode. + */ +typedef enum { + MCP47XX_NORMAL = 0, /**< Normal mode */ + MCP47XX_PD_SMALL = 1, /**< Power down, small resistor 1 kOhm */ + MCP47XX_PD_MEDIUM = 2, /**< Power down, medium resistor, + 125 kOhm for MCP47x6, 100 kOhm otherwise */ + MCP47XX_PD_LARGE = 3, /**< Power down, large resistor, + 640 kOhm for MCP47x6, 125 kOhm otherwise */ +} mcp47xx_pd_mode_t; + +/** + * @brief MCP47xx device configuration parameters + */ +typedef struct { + + i2c_t dev; /**< I2C device */ + uint16_t addr; /**< I2C slave address MCP47XX_BASE_ADDR + [0...7] */ + + mcp47xx_variant_t variant; /**< used variant of MCP47xx */ + mcp47xx_gain_t gain; /**< Gain selection */ + mcp47xx_vref_t vref; /**< Voltage reference selection */ + mcp47xx_pd_mode_t pd_mode; /**< Power-down mode selection */ + +} mcp47xx_params_t; + +/** + * @brief MCP47xx device data structure type + */ +typedef struct { + mcp47xx_params_t params; /**< device configuration parameters */ + uint16_t values[MCP47XX_CHN_NUM_MAX]; /**< contains the last values set + for persistence when device is + powered off */ +} mcp47xx_t; + +#if IS_USED(MODULE_SAUL) || DOXYGEN +/** + * @brief MCP47xx configuration structure for mapping DAC channels to SAUL + */ +typedef struct { + const char *name; /**< name of the MCP47xx device */ + unsigned int dev; /**< index of the MCP47xx device */ + uint8_t channel; /**< channel of the MCP47xx device */ + uint16_t initial; /**< initial value */ +} mcp47xx_saul_dac_params_t; +#endif + +/** + * @brief Initialize the MCP47xx DAC + * + * All expander pins are set to be input and are pulled up. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] params configuration parameters, see #mcp47xx_params_t + * + * @retval MCP47XX_OK on success + * @retval MCP47XX_ERROR_* a negative error code on error, + * see #mcp47xx_error_codes_t + */ +int mcp47xx_init(mcp47xx_t *dev, const mcp47xx_params_t *params); + +/** + * @brief Initialize a MCP47xx DAC channel + * + * After the initialization, the DAC channel is active and its output is set + * to 0. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] chn channel to initialize + * + * @retval MCP47XX_OK on success + * @retval MCP47XX_ERROR_* a negative error code on error, + * see #mcp47xx_error_codes_t + */ +int mcp47xx_dac_init(mcp47xx_t *dev, uint8_t chn); + +/** + * @brief Write a value to a MCP47xx DAC channel + * + * The value is always given as 16-bit value and is internally scaled to the + * actual resolution that the DAC unit provides, e.g., 12-bit. So to get the + * maximum output voltage, this function has to be called with value set + * to 65535 (UINT16_MAX). + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] chn channel to set + * @param[in] value value to set line to + * + * @retval none + */ +void mcp47xx_dac_set(mcp47xx_t *dev, uint8_t chn, uint16_t value); + +/** + * @brief Get the current value of a MCP47xx DAC channel + * + * The value is always given as 16-bit value and is internally scaled to the + * actual resolution that the DAC unit provides, e.g., 12-bit. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] chn channel to set + * @param[out] value value to set line to + * + * @retval none + */ +void mcp47xx_dac_get(mcp47xx_t *dev, uint8_t chn, uint16_t *value); + +/** + * @brief Enables the MCP47xx DAC device + * + * MCP47xx is enabled and the output is set to the last set value. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] chn channel to power on + * @retval none + */ +void mcp47xx_dac_poweron(mcp47xx_t *dev, uint8_t chn); + +/** + * @brief Disables the MCP47xx DAC device + * + * MCP47xx is switched to the power-down mode configured by the configuration + * parameter mcp47xx_params_t::pd_mode. V_OUT is loaded with the configured + * resistor to ground. Most of the channel circuits are powered off. + * + * @note If #MCP47XX_NORMAL is used as power-down mode, the DAC can't be + * powerd off. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @param[in] chn channel to power on + * @retval none + */ +void mcp47xx_dac_poweroff(mcp47xx_t *dev, uint8_t chn); + +/** + * @brief Returns the number of channels of MCP47xx DAC device + * + * This function returns the number of channels of the device MCP47xx DAC + * device. + * + * @param[in] dev descriptor of the MCP47xx DAC device + * @retval number of channels on success or 0 on error + */ +uint8_t mcp47xx_dac_channels(mcp47xx_t *dev); + +#ifdef __cplusplus +} +#endif + +#endif /* MCP47XX_H */ +/** @} */ diff --git a/drivers/mcp47xx/Makefile b/drivers/mcp47xx/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/mcp47xx/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/mcp47xx/Makefile.dep b/drivers/mcp47xx/Makefile.dep new file mode 100644 index 0000000000..e67057d463 --- /dev/null +++ b/drivers/mcp47xx/Makefile.dep @@ -0,0 +1 @@ +FEATURES_REQUIRED += periph_i2c diff --git a/drivers/mcp47xx/Makefile.include b/drivers/mcp47xx/Makefile.include new file mode 100644 index 0000000000..fb0f98e52a --- /dev/null +++ b/drivers/mcp47xx/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_mcp47xx := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_mcp47xx) diff --git a/drivers/mcp47xx/include/mcp47xx_params.h b/drivers/mcp47xx/include/mcp47xx_params.h new file mode 100644 index 0000000000..4ea008aca5 --- /dev/null +++ b/drivers/mcp47xx/include/mcp47xx_params.h @@ -0,0 +1,109 @@ +/* + * 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. + */ + +/** + * @ingroup drivers_mcp47xx + * @brief Default configuration for Microchip MCP47xx DAC with I2C interface + * @author Gunar Schorcht + * @file + * @{ + */ + +#ifndef MCP47XX_PARAMS_H +#define MCP47XX_PARAMS_H + +#include "board.h" +#include "mcp47xx.h" +#include "saul_reg.h" +#include "saul/periph.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters + * @{ + */ +#ifndef MCP47XX_PARAM_VARIANT +/** Default MCP47xx variant */ +#define MCP47XX_PARAM_VARIANT (MCP4725) +#endif + +#ifndef MCP47XX_PARAM_DEV +/** Default I2C device */ +#define MCP47XX_PARAM_DEV I2C_DEV(0) +#endif + +#ifndef MCP47XX_PARAM_ADDR +/** Default I2C slave address as offset to MCP47XX_BASE_ADDR */ +#define MCP47XX_PARAM_ADDR (MCP47XX_BASE_ADDR + 2) +#endif + +#ifndef MCP47XX_PARAM_GAIN +/** Default MCP47xx gain selection */ +#define MCP47XX_PARAM_GAIN (MCP47XX_GAIN_1X) +#endif + +#ifndef MCP47XX_PARAM_VREF +/** Default MCP47xx V_REF selection */ +#define MCP47XX_PARAM_VREF (MCP47XX_VREF_VDD) +#endif + +#ifndef MCP47XX_PARAM_PD_MODE +/** Default MCP47xx Power-Down mode selection */ +#define MCP47XX_PARAM_PD_MODE (MCP47XX_PD_LARGE) +#endif + +#ifndef MCP47XX_PARAMS +/** Default MCP47xx configuration parameters */ +#define MCP47XX_PARAMS { \ + .dev = MCP47XX_PARAM_DEV, \ + .addr = MCP47XX_PARAM_ADDR, \ + .variant = MCP47XX_PARAM_VARIANT, \ + .gain = MCP47XX_PARAM_GAIN, \ + .vref = MCP47XX_PARAM_VREF, \ + .pd_mode = MCP47XX_PARAM_PD_MODE, \ + }, +#endif /* MCP47XX_PARAMS */ + +#ifndef MCP47XX_SAUL_DAC_PARAMS +/** Example for mapping DAC channels to SAUL */ +#define MCP47XX_SAUL_DAC_PARAMS { \ + .name = "DAC00", \ + .dev = 0, \ + .channel = 0, \ + .initial = 32768, \ + }, +#endif +/**@}*/ + +/** + * @brief Allocate some memory to store the actual configuration + */ +static const mcp47xx_params_t mcp47xx_params[] = +{ + MCP47XX_PARAMS +}; + +#if IS_USED(MODULE_SAUL) || DOXYGEN +/** + * @brief Additional meta information to keep in the SAUL registry + */ +static const mcp47xx_saul_dac_params_t mcp47xx_saul_dac_params[] = +{ + MCP47XX_SAUL_DAC_PARAMS +}; +#endif /* IS_USED(MODULE_SAUL) || DOXYGEN */ + +#ifdef __cplusplus +} +#endif + +#endif /* MCP47XX_PARAMS_H */ +/** @} */ diff --git a/drivers/mcp47xx/mcp47xx.c b/drivers/mcp47xx/mcp47xx.c new file mode 100644 index 0000000000..66bbc0f188 --- /dev/null +++ b/drivers/mcp47xx/mcp47xx.c @@ -0,0 +1,298 @@ +/* + * 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. + */ + +/** + * @ingroup drivers_mcp47xx + * @brief Device driver for the Microchip MCP47xx DAC with I2C interface + * @author Gunar Schorcht + * @file + * @{ + */ + +#include +#include + +#include "mcp47xx.h" + +#include "irq.h" +#include "log.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +#if ENABLE_DEBUG + +#define ASSERT_PARAM(cond) \ + do { \ + if (!(cond)) { \ + DEBUG("[mcp47xx] %s: %s\n", \ + __func__, "parameter condition (" #cond ") not fulfilled"); \ + assert(cond); \ + } \ + } while (0) + +#define DEBUG_DEV(f, d, ...) \ + DEBUG("[mcp47xx] %s i2c dev=%d addr=%02x: " f "\n", \ + __func__, d->params.dev, d->params.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("[mcp47xx] %s i2c dev=%d addr=%02x: " f "\n", \ + __func__, d->params.dev, d->params.addr, ## __VA_ARGS__) + +/** Forward declaration of functions for internal use */ + +static int _get(mcp47xx_t *dev, uint8_t chn, uint16_t* value); +static int _set(mcp47xx_t *dev, uint8_t chn, uint16_t value, bool pd); +static int _read(const mcp47xx_t *dev, uint8_t *data, size_t len); +static int _write(const mcp47xx_t *dev, uint8_t* data, size_t len); + +static const uint8_t _mcp47xx_chn_nums[] = { + MCP4706_CHN_NUM, + MCP4716_CHN_NUM, + MCP4725_CHN_NUM, + MCP4726_CHN_NUM, + MCP4728_CHN_NUM +}; + +int mcp47xx_init(mcp47xx_t *dev, const mcp47xx_params_t *params) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(params != NULL); + + DEBUG_DEV("params=%p", dev, params); + + ASSERT_PARAM(params->gain <= MCP47XX_GAIN_2X); + ASSERT_PARAM(params->vref <= MCP47XX_VREF_BUF); + ASSERT_PARAM(params->pd_mode <= MCP47XX_PD_LARGE); + ASSERT_PARAM(params->pd_mode != MCP47XX_NORMAL); + + /* MCP4725 supports only MCP47XX_GAIN_1X */ + ASSERT_PARAM(params->variant != MCP4725 || params->gain == MCP47XX_GAIN_1X); + /* MCP4725 supports only MCP47XX_VDD */ + ASSERT_PARAM(params->variant != MCP4725 || params->vref == MCP47XX_VREF_VDD); + + /* MCP47XX_INT is only supported by MCP4728 */ + ASSERT_PARAM(params->vref != MCP47XX_VREF_INT || params->variant == MCP4728); + /* MCP47XX_PIN is only supported by MCP47x6 */ + ASSERT_PARAM((params->vref != MCP47XX_VREF_PIN && + params->vref != MCP47XX_VREF_BUF) || params->variant == MCP4706 + || params->variant == MCP4716 + || params->variant == MCP4726); + /* init sensor data structure */ + dev->params = *params; + + /* check for availability */ + uint8_t bytes[3]; + if (_read(dev, bytes, 3) != MCP47XX_OK) { + DEBUG_DEV("device not available", dev); + return MCP47XX_ERROR_NOT_AVAIL; + } + + /* power_off all channels */ + for (unsigned i = 0; i < _mcp47xx_chn_nums[dev->params.variant]; i++) { + mcp47xx_dac_poweroff(dev, i); + } + + return MCP47XX_OK; +} + +int mcp47xx_dac_init(mcp47xx_t *dev, uint8_t chn) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(chn < _mcp47xx_chn_nums[dev->params.variant]); + + DEBUG_DEV("chn=%u", dev, chn); + + /* get the current value */ + uint16_t value; + int res; + if ((res = _get(dev, chn, &value))) { + return res; + } + + /* set the channel value */ + if ((res = _set(dev, chn, 0, false))) { + return res; + } + + return MCP47XX_OK; +} + +void mcp47xx_dac_set(mcp47xx_t *dev, uint8_t chn, uint16_t value) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(chn < _mcp47xx_chn_nums[dev->params.variant]); + + DEBUG_DEV("chn=%u val=%u", dev, chn, value); + + dev->values[chn] = value; + + _set(dev, chn, value, false); +} + +void mcp47xx_dac_get(mcp47xx_t *dev, uint8_t chn, uint16_t *value) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(value != NULL); + ASSERT_PARAM(chn < _mcp47xx_chn_nums[dev->params.variant]); + + DEBUG_DEV("chn=%u val=%p", dev, chn, value); + + _get(dev, chn, value); + + dev->values[chn] = *value; +} + +void mcp47xx_dac_poweroff(mcp47xx_t *dev, uint8_t chn) +{ + _set(dev, chn, dev->values[chn], true); +} + +void mcp47xx_dac_poweron(mcp47xx_t *dev, uint8_t chn) +{ + _set(dev, chn, dev->values[chn], false); +} + +uint8_t mcp47xx_dac_channels(mcp47xx_t *dev) +{ + /* some parameter sanity checks */ + ASSERT_PARAM(dev != NULL); + + return _mcp47xx_chn_nums[dev->params.variant]; +} + +/** Functions for internal use only */ + +/* Write DAC Register/Volatile Memory command */ +#define MCP47XX_CMD_WRITE_DAC (0x40) + +static int _get(mcp47xx_t *dev, uint8_t chn, uint16_t* value) +{ + /* + * read formats: + * + * MCP4706 BR0VVPPG DDDDDDDD -------- V - Voltage selection VREF1,VREF0 + * MCP4716 BR0VVPPG DDDDDDDD DD------ P - Power Mode seclection PD1,PD0 + * MCP4726 BR0VVPPG DDDDDDDD DDDD---- G - Gain selection + * MCP4725 BR---PP- DDDDDDDD DDDD---- D - Data from MSB to LSB + * MCP4728 BRCC0AAA VPPGDDDD DDDDDDDD C - Channel selection CH1,CH0 + * B - BSY/RDY EEPROM Write status + * R - Power on Reset + */ + int res; + + if (dev->params.variant == MCP4728) { + uint8_t bytes[24]; + res = _read(dev, bytes, 24); + if (res) { + return res; + } + *value = ((uint16_t)bytes[chn * 6 + 1] & 0x000f) << 12; + *value |= bytes[chn * 6 + 2] << 4; + } + else { + uint8_t bytes[3]; + res = _read(dev, bytes, 3); + if (res) { + return res; + } + DEBUG_DEV("read %02x %02x %02x", dev, bytes[0], bytes[1], bytes[2]); + *value = ((uint16_t)bytes[1] << 8) | bytes[2]; + } + + return MCP47XX_OK; +} + +static int _set(mcp47xx_t *dev, uint8_t chn, uint16_t value, bool pd) +{ + /* + * write command formats: + * + * MCP4706 010VVPPG DDDDDDDD -------- V - Voltage selection VREF1,VREF0 + * MCP4716 010VVPPG DDDDDDDD DD------ P - Power Mode seclection PD1,PD0 + * MCP4726 010VVPPG DDDDDDDD DDDD---- G - Gain selection + * MCP4725 010--PP- DDDDDDDD DDDD---- D - Data from MSB to LSB + * MCP4728 01000CCU VPPGDDDD DDDDDDDD C - Channel selection CH1,CH0 + * U - UDAC bit + */ + uint8_t bytes[3] = { }; + + if (dev->params.variant == MCP4728) { + /* U=0 update V_OUT directly */ + bytes[0] = MCP47XX_CMD_WRITE_DAC | (chn << 1); + bytes[1] = (value >> 12) | (dev->params.vref << 7) + | (dev->params.gain << 4) + | (pd ? dev->params.pd_mode << 5 : 0); + bytes[2] = (value >> 4) & 0xff; + } + else { + bytes[0] = MCP47XX_CMD_WRITE_DAC | (dev->params.vref << 3) + | dev->params.gain + | (pd ? dev->params.pd_mode << 1 : 0); + /* + * resolution handling is not required since only the n most + * significant bits are used + */ + bytes[1] = value >> 8; + bytes[2] = value & 0xff; + } + DEBUG_DEV("write %02x %02x %02x", dev, bytes[0], bytes[1], bytes[2]); + + return _write(dev, bytes, 3); +} + +static int _read(const mcp47xx_t *dev, uint8_t *data, size_t len) +{ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(data != NULL); + + DEBUG_DEV("", dev); + + i2c_acquire(dev->params.dev); + int res = i2c_read_bytes(dev->params.dev, dev->params.addr, data, len, 0); + i2c_release(dev->params.dev); + + if (res != 0) { + DEBUG_DEV("could not read data, reason %d (%s)", + dev, res, strerror(res * -1)); + return -MCP47XX_ERROR_I2C; + } + + return res; +} + +static int _write(const mcp47xx_t *dev, uint8_t* data, size_t len) +{ + ASSERT_PARAM(dev != NULL); + ASSERT_PARAM(data != NULL); + + DEBUG_DEV("", dev); + + i2c_acquire(dev->params.dev); + int res = i2c_write_bytes(dev->params.dev, dev->params.addr, data, len, 0); + i2c_release(dev->params.dev); + + if (res != 0) { + DEBUG_DEV("could not write data, reason %d (%s)", + dev, res, strerror(res * -1)); + return -MCP47XX_ERROR_I2C; + } + + return res; +} From 033fa00b9f039a68788042c7c3363a338b357b0f Mon Sep 17 00:00:00 2001 From: Gunar Schorcht Date: Mon, 6 Dec 2021 23:50:31 +0100 Subject: [PATCH 2/4] drivers/mcp47xx: SAUL integration --- drivers/mcp47xx/mcp47xx_saul.c | 43 +++++++++++ drivers/saul/init_devs/auto_init_mcp47xx.c | 90 ++++++++++++++++++++++ drivers/saul/init_devs/init.c | 4 + 3 files changed, 137 insertions(+) create mode 100644 drivers/mcp47xx/mcp47xx_saul.c create mode 100644 drivers/saul/init_devs/auto_init_mcp47xx.c diff --git a/drivers/mcp47xx/mcp47xx_saul.c b/drivers/mcp47xx/mcp47xx_saul.c new file mode 100644 index 0000000000..dd9965be26 --- /dev/null +++ b/drivers/mcp47xx/mcp47xx_saul.c @@ -0,0 +1,43 @@ +/* + * 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. + */ + +/** + * @ingroup drivers_mcp47xx + * @brief MCP47xx adaption to the RIOT actuator/sensor interface + * @author Gunar Schorcht + * @file + */ +#if MODULE_SAUL + +#include + +#include "saul.h" +#include "mcp47xx.h" + +extern mcp47xx_t mcp47xx_devs[]; + +static int set(const void *dev, phydat_t *data) +{ + const mcp47xx_saul_dac_params_t *p = (const mcp47xx_saul_dac_params_t *)dev; + mcp47xx_dac_set(&mcp47xx_devs[p->dev], p->channel, (uint16_t)data->val[0]); + return 1; +} + +static int get(const void *dev, phydat_t *data) +{ + const mcp47xx_saul_dac_params_t *p = (const mcp47xx_saul_dac_params_t *)dev; + mcp47xx_dac_get(&mcp47xx_devs[p->dev], p->channel, (uint16_t*)&data->val[0]); + return 1; +} + +const saul_driver_t mcp47xx_dac_saul_driver = { + .read = get, + .write = set, + .type = SAUL_ACT_DIMMER +}; +#endif /* MODULE_SAUL */ diff --git a/drivers/saul/init_devs/auto_init_mcp47xx.c b/drivers/saul/init_devs/auto_init_mcp47xx.c new file mode 100644 index 0000000000..cb506318de --- /dev/null +++ b/drivers/saul/init_devs/auto_init_mcp47xx.c @@ -0,0 +1,90 @@ +/* + * 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. + */ + +/** + * @ingroup drivers_mcp47xx + * @ingroup sys_auto_init_saul + * @brief Auto initialization of Microchip MCP47xx DAC with I2C interface + * @author Gunar Schorcht + * @file + * @{ + */ + +#include "assert.h" +#include "log.h" + +#include "saul_reg.h" +#include "saul/periph.h" + +#include "mcp47xx.h" +#include "mcp47xx_params.h" + +/** + * @brief Number of configured MCP47xx DAC devices + */ +#define MCP47XX_NUM (sizeof(mcp47xx_params) / sizeof(mcp47xx_params[0])) + +/** + * @brief Number of configured SAUL MCP47xx DAC channels + */ +#define MCP47XX_SAUL_DAC_NUMOF (sizeof(mcp47xx_saul_dac_params) / \ + sizeof(mcp47xx_saul_dac_params[0])) + +/** + * @brief Number of saul info + */ +#define MCP47XX_INFO_NUM (sizeof(mcp47xx_saul_info) / \ + sizeof(mcp47xx_saul_info[0])) + +/** + * @brief Allocate the memory for the MCP47xx DAC device descriptors + */ +mcp47xx_t mcp47xx_devs[MCP47XX_NUM]; + +/** + * @brief Allocate the memory for MCP47xx DAC SAUL registry entries + */ +static saul_reg_t mcp47xx_saul_reg_entries[MCP47XX_SAUL_DAC_NUMOF]; + +/** + * @brief Reference the MCP47xx DAC input mode driver struct + */ +extern saul_driver_t mcp47xx_dac_in_saul_driver; + +/** + * @brief Reference to the MCP47xx DAC output mode driver struct + */ +extern saul_driver_t mcp47xx_dac_saul_driver; + +void auto_init_mcp47xx(void) +{ + for (unsigned int i = 0; i < MCP47XX_NUM; i++) { + LOG_DEBUG("[auto_init_saul] initializing MCP47xx DAC dev #%u\n", i); + mcp47xx_init(&mcp47xx_devs[i], &mcp47xx_params[i]); + } + + for (unsigned int i = 0; i < MCP47XX_SAUL_DAC_NUMOF; i++) { + const mcp47xx_saul_dac_params_t *p = &mcp47xx_saul_dac_params[i]; + + LOG_DEBUG("[auto_init_saul] initializing MCP47xx DAC channel #%u\n", i); + + /* check the MCP47xx device index */ + assert(p->dev < MCP47XX_NUM); + + mcp47xx_saul_reg_entries[i].dev = (void *)p; + mcp47xx_saul_reg_entries[i].name = p->name; + mcp47xx_saul_reg_entries[i].driver = &mcp47xx_dac_saul_driver; + + /* initialize the MCP47xx pin */ + mcp47xx_dac_init(&mcp47xx_devs[p->dev], p->channel); + mcp47xx_dac_set(&mcp47xx_devs[p->dev], p->channel, p->initial); + + /* add to registry */ + saul_reg_add(&(mcp47xx_saul_reg_entries[i])); + } +} diff --git a/drivers/saul/init_devs/init.c b/drivers/saul/init_devs/init.c index e052c8410c..9d0d810c89 100644 --- a/drivers/saul/init_devs/init.c +++ b/drivers/saul/init_devs/init.c @@ -195,6 +195,10 @@ void saul_init_devs(void) extern void auto_init_mag3110(void); auto_init_mag3110(); } + if (IS_USED(MODULE_MCP47XX)) { + extern void auto_init_mcp47xx(void); + auto_init_mcp47xx(); + } if (IS_USED(MODULE_MHZ19)) { extern void auto_init_mhz19(void); auto_init_mhz19(); From 1aa35847869fdbd43d6de699b280945202c9ffa6 Mon Sep 17 00:00:00 2001 From: Gunar Schorcht Date: Tue, 7 Dec 2021 02:35:12 +0100 Subject: [PATCH 3/4] tests/driver_mcp47xx: add test app for MCP47xx driver --- tests/driver_mcp47xx/Makefile | 7 ++ tests/driver_mcp47xx/Makefile.ci | 8 ++ tests/driver_mcp47xx/main.c | 208 +++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 tests/driver_mcp47xx/Makefile create mode 100644 tests/driver_mcp47xx/Makefile.ci create mode 100644 tests/driver_mcp47xx/main.c diff --git a/tests/driver_mcp47xx/Makefile b/tests/driver_mcp47xx/Makefile new file mode 100644 index 0000000000..c903f78b7d --- /dev/null +++ b/tests/driver_mcp47xx/Makefile @@ -0,0 +1,7 @@ +include ../Makefile.tests_common + +USEMODULE += mcp47xx +USEMODULE += shell +USEMODULE += benchmark + +include $(RIOTBASE)/Makefile.include diff --git a/tests/driver_mcp47xx/Makefile.ci b/tests/driver_mcp47xx/Makefile.ci new file mode 100644 index 0000000000..1152ca53bc --- /dev/null +++ b/tests/driver_mcp47xx/Makefile.ci @@ -0,0 +1,8 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-nano \ + arduino-uno \ + atmega328p \ + atmega328p-xplained-mini \ + # diff --git a/tests/driver_mcp47xx/main.c b/tests/driver_mcp47xx/main.c new file mode 100644 index 0000000000..8983648b04 --- /dev/null +++ b/tests/driver_mcp47xx/main.c @@ -0,0 +1,208 @@ +/* + * 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. + */ + +/** + * @ingroup tests + * @brief Test application for Microchip MCP47xx DAC with I2C interface + * @author Gunar Schorcht + * @file + * + * This test appliation demonstrates the usage of the MCP47xx driver. + * It can be used to test each MCP47xx DAC channel with shell commands. + * + * Functions `init`, `set`, `poweron`, `poweroff` demonstrate the usage of + * the driver API for one channel of one device using the driver API. + * The `setall` function shows the iteration over all channels of all + * devices for setting a value. + */ + +#include +#include + +#include "mcp47xx.h" +#include "mcp47xx_params.h" + +#include "irq.h" +#include "shell.h" +#include "benchmark.h" + +#define BENCH_RUNS_DEFAULT (100UL * 100) + +/* Number of configured MCP47xx I/O expander devices */ +#define MCP47XX_NUM (sizeof(mcp47xx_params) / sizeof(mcp47xx_params[0])) + +/* MCP47xx devices allocation */ +mcp47xx_t mcp47xx_dev[MCP47XX_NUM]; + +static int init(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + int dev = atoi(argv[1]); + int chn = atoi(argv[2]); + + if (mcp47xx_dac_init(&mcp47xx_dev[dev], chn) < 0) { + printf("error: init MCP47xx pin (dev %i, chn %i) failed\n", dev, chn); + return 1; + } + + return 0; +} + +static int set(int argc, char **argv) +{ + if (argc < 4) { + printf("usage: %s \n", argv[0]); + return 1; + } + + mcp47xx_dac_set(&mcp47xx_dev[atoi(argv[1])], atoi(argv[2]), + atoi(argv[3])); + return 0; +} + +static int setall(int argc, char **argv) +{ + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + uint16_t value = atoi(argv[1]); + + for (unsigned dev = 0; dev < MCP47XX_NUM; dev++) { + uint8_t num = mcp47xx_dac_channels(&mcp47xx_dev[dev]); + + for (uint8_t chn = 0; chn < num; chn++) { + mcp47xx_dac_set(&mcp47xx_dev[dev], chn, value); + } + } + + return 0; +} + +static int poweron(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + mcp47xx_dac_poweron(&mcp47xx_dev[atoi(argv[1])], atoi(argv[2])); + + return 0; +} + +static int poweroff(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + mcp47xx_dac_poweroff(&mcp47xx_dev[atoi(argv[1])], atoi(argv[2])); + + return 0; +} + +static int channels(int argc, char **argv) +{ + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + uint8_t chn = mcp47xx_dac_channels(&mcp47xx_dev[atoi(argv[1])]); + + printf("channel_num: %u\n", chn); + return 0; +} + +static int saw(int argc, char **argv) +{ + if (argc < 4) { + printf("usage: %s \n", argv[0]); + return 1; + } + + int dev = atoi(argv[1]); + int chn = atoi(argv[2]); + unsigned num = atoi(argv[3]); + + for (unsigned i = 0; i < num; i++) { + for (uint16_t j = 0; j < UINT16_MAX; j ++) { + mcp47xx_dac_set(&mcp47xx_dev[dev], chn, j); + } + } + + return 0; +} + +static int bench(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s [# of runs]\n", argv[0]); + return 1; + } + + unsigned long runs = BENCH_RUNS_DEFAULT; + if (argc > 3) { + runs = (unsigned long)atol(argv[3]); + } + + puts("\nDAC driver run-time performance benchmark\n"); + + int dev = atoi(argv[1]); + int chn = atoi(argv[2]); + BENCHMARK_FUNC("nop loop", runs, __asm__ volatile("nop")); + mcp47xx_dac_init(&mcp47xx_dev[dev], chn); + BENCHMARK_FUNC("dac_set", runs, mcp47xx_dac_set(&mcp47xx_dev[dev], chn, 0)); + + puts("\n --- DONE ---"); + return 0; +} + +static const shell_command_t shell_commands[] = { + { "init", "init one channel of a device", init }, + { "set", "set the output of one channel of a device", set }, + { "setall", "set the outputs of all channels of all devices", setall }, + { "poweron", "power on the output of one channel of a device", poweron }, + { "poweroff", "power off the output of one channel of a device", poweroff }, + { "channels", "number of channels of a device", channels }, + { "saw", "generate a saw signal on one channel of a device", saw }, + { "bench", "run a set of predefined benchmarks", bench }, + { NULL, NULL, NULL } +}; + +int main(void) +{ + puts("MCP47xx DAC peripheral driver test\n"); + puts("Initializing MCP47xx"); + + /* initialize configured MCP47xx devices */ + for (unsigned i = 0; i < MCP47XX_NUM; i++) { + if (mcp47xx_init(&mcp47xx_dev[i], &mcp47xx_params[i]) != MCP47XX_OK) { + puts("[Failed]"); + return 1; + } + } + puts("[OK]\n"); + + puts("In this test, DAC lines are specified by device and channel numbers.\n" + "NOTE: make sure the values exist! The\n" + " behavior for not existing devices/channels is not defined!"); + + /* start the shell */ + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + + return 0; +} From 739fbf41fb883fb009b79aab86172ae95d714062 Mon Sep 17 00:00:00 2001 From: Gunar Schorcht Date: Tue, 7 Dec 2021 00:53:00 +0100 Subject: [PATCH 4/4] drivers/mcp47xx: add Kconfig --- drivers/Kconfig | 1 + drivers/mcp47xx/Kconfig | 14 ++++++++++++++ tests/driver_mcp47xx/app.config.test | 5 +++++ 3 files changed, 20 insertions(+) create mode 100644 drivers/mcp47xx/Kconfig create mode 100644 tests/driver_mcp47xx/app.config.test diff --git a/drivers/Kconfig b/drivers/Kconfig index eead0d8c2e..7d2ced81a9 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -19,6 +19,7 @@ rsource "dynamixel/Kconfig" rsource "feetech/Kconfig" rsource "grove_ledbar/Kconfig" rsource "motor_driver/Kconfig" +rsource "mcp47xx/Kconfig" rsource "my9221/Kconfig" rsource "rgbled/Kconfig" rsource "servo/Kconfig" diff --git a/drivers/mcp47xx/Kconfig b/drivers/mcp47xx/Kconfig new file mode 100644 index 0000000000..cce4ad076e --- /dev/null +++ b/drivers/mcp47xx/Kconfig @@ -0,0 +1,14 @@ +# 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_MCP47XX + bool "MCP47xx DAC with I2C interface" + depends on HAS_PERIPH_I2C + select MODULE_PERIPH_I2C + help + Driver for Microchip MCP47xx DAC devices with I2C interfaces. The + driver supports MCP4706, MCP4716, MCP4725, MCP4726 and MCP4728. diff --git a/tests/driver_mcp47xx/app.config.test b/tests/driver_mcp47xx/app.config.test new file mode 100644 index 0000000000..3e9c052a82 --- /dev/null +++ b/tests/driver_mcp47xx/app.config.test @@ -0,0 +1,5 @@ +# this file enables modules defined in Kconfig. Do not use this file for +# application configuration. This is only needed during migration. +CONFIG_MODULE_MCP47XX=y +CONFIG_MODULE_SHELL=y +CONFIG_MODULE_BENCHMARK=y