From bbc95d70dbca270534601c52ff65e9bcf370fdeb Mon Sep 17 00:00:00 2001 From: Matteo Golin Date: Mon, 21 Oct 2024 19:00:51 -0400 Subject: [PATCH] adc: Implement ADC driver interface for MCP3008 over SPI. Includes documentation page for the driver, and inclusion of driver registration code for RP2040-based boards. --- .../applications/examples/adc/index.rst | 2 + .../character/analog/adc/mcp3008/index.rst | 119 +++++ .../rp2040/common/src/rp2040_common_bringup.c | 28 ++ drivers/analog/CMakeLists.txt | 4 + drivers/analog/Kconfig | 28 ++ drivers/analog/Make.defs | 4 + drivers/analog/mcp3008.c | 406 ++++++++++++++++++ include/nuttx/analog/ioctl.h | 5 + include/nuttx/analog/mcp3008.h | 52 +++ 9 files changed, 648 insertions(+) create mode 100644 Documentation/components/drivers/character/analog/adc/mcp3008/index.rst create mode 100644 drivers/analog/mcp3008.c create mode 100644 include/nuttx/analog/mcp3008.h diff --git a/Documentation/applications/examples/adc/index.rst b/Documentation/applications/examples/adc/index.rst index 99272c0f63305..699ff68b038a5 100644 --- a/Documentation/applications/examples/adc/index.rst +++ b/Documentation/applications/examples/adc/index.rst @@ -1,3 +1,5 @@ +.. _adc-example: + ===================== ``adc`` Read from ADC ===================== diff --git a/Documentation/components/drivers/character/analog/adc/mcp3008/index.rst b/Documentation/components/drivers/character/analog/adc/mcp3008/index.rst new file mode 100644 index 0000000000000..06f4e47c719c2 --- /dev/null +++ b/Documentation/components/drivers/character/analog/adc/mcp3008/index.rst @@ -0,0 +1,119 @@ +======= +MCP3008 +======= + +Contributed by Matteo Golin + +The MCP3008 is a 10-bit, 8-channel ADC made by Microchip which operates over +SPI. + +There is the option to operate in single-ended mode, which measures the voltage +on each channel individually, or differential mode which measures the voltage +difference between pairs of channels. + +When operating in differential mode, the channel numbers below correspond to the +listed differential pairs: + +.. list-table:: Differential pair channel numbers + :widths: auto + + * - Channel number + - Sources + * - 0 + - CH0+, CH1- + * - 1 + - CH0-, CH1+ + * - 2 + - CH2+, CH3- + * - 3 + - CH2-, CH3+ + * - 4 + - CH4+, CH5- + * - 5 + - CH4-, CH5+ + * - 6 + - CH6+, CH7- + * - 7 + - CH6-, CH7+ + +Driver Interface +--------------------- + +To register the MCP3008 device driver as a standard NuttX analog device on your +board, you can use something similar to the below code for the RP2040. + +.. code-block:: c + + #include + #include + + /* Register MCP3008 ADC */ + + struct spi_dev_s *spi = rp2040_spibus_initialize(0); + if (spi == NULL) + { + syslog(LOG_ERR, "Failed to initialize SPI bus 0\n"); + } + + struct adc_dev_s *mcp3008 = mcp3008_initialize(spi); + if (mcp3008 == NULL) + { + syslog(LOG_ERR, "Failed to initialize MCP3008\n"); + } + + int ret = adc_register("/dev/adc1", mcp3008); + if (ret < 0) + { + syslog(LOG_ERR, "Failed to register MCP3008 device driver: %d\n", ret); + } + +Once registered, this driver can be interacted with using the ADC example +(:ref:`adc-example`). Be sure to enable the software trigger, since the MCP3008 +driver does not support hardware triggers (interrupts). You can also change the +number of samples per group up to 8 for all 8 channels of the ADC. + +You may need to increase the `CONFIG_ADC_FIFOSIZE` value to something larger +than 8 in order to be able to store all the ADC measurements after a measurement +trigger (i.e 9). + +You can configure the driver in differential mode by default using the +`CONFIG_ADC_MCP3008_DIFFERENTIAL` configuration option. + +You can also configure the speed of SPI communications to the MCP3008 using the +`CONFIG_ADC_MCP3008_SPI_FREQUENCY` configuration option. This speed should be +selected based on the supply voltage used to power the MCP3008: + +.. list-table:: SPI frequencies for supply voltage + :widths: auto + :header-rows: 1 + + * - Supply Voltage + - Frequency + * - VDD >= 4V + - 3.6MHz + * - VDD >= 3.3V + - 2.34MHz + * - VDD = 2.7V + - 1.35MHz + +If you have a measurement from the MCP3008, you can convert it into a voltage +like so: + +.. code-block:: c + + #define VREF (3.3) /* Whatever voltage is used on the VREF pin */ + + struct adc_msg_s msg; + + /* Some code here to read the ADC device, you can read the ADC driver docs */ + + double voltage = ((double)msg.am_data * VREF) / (1023.0); + +There is also an additional `ioctl()` command supported for the MCP3008 that +permits you to switch from differential to single ended mode at runtime: + +.. c:macro:: ANIOC_MCP3008_DIFF + +This command changes the mode of the MCP3008 driver. The argument passed should +be 0 to disable differential mode (and thus use single-ended mode), and 1 to +enable differential mode. No other values are allowed. diff --git a/boards/arm/rp2040/common/src/rp2040_common_bringup.c b/boards/arm/rp2040/common/src/rp2040_common_bringup.c index 136d4178d4810..07a63de24e51a 100644 --- a/boards/arm/rp2040/common/src/rp2040_common_bringup.c +++ b/boards/arm/rp2040/common/src/rp2040_common_bringup.c @@ -87,6 +87,12 @@ #include "rp2040_adc.h" #endif +#if defined(CONFIG_ADC) && defined(CONFIG_ADC_MCP3008) +#include +#include +#include "rp2040_spi.h" +#endif + #if defined(CONFIG_RP2040_BOARD_HAS_WS2812) && defined(CONFIG_WS2812) #include "rp2040_ws2812.h" #endif @@ -446,6 +452,28 @@ int rp2040_common_bringup(void) } #endif +#ifdef CONFIG_ADC_MCP3008 + /* Register MCP3008 ADC. */ + + struct spi_dev_s *spi = rp2040_spibus_initialize(0); + if (spi == NULL) + { + syslog(LOG_ERR, "Failed to initialize SPI bus 0\n"); + } + + struct adc_dev_s *mcp3008 = mcp3008_initialize(spi); + if (mcp3008 == NULL) + { + syslog(LOG_ERR, "Failed to initialize MCP3008\n"); + } + + ret = adc_register("/dev/adc1", mcp3008); + if (ret < 0) + { + syslog(LOG_ERR, "Failed to register MCP3008 device driver: %d\n", ret); + } +#endif + #ifdef CONFIG_FS_PROCFS /* Mount the procfs file system */ diff --git a/drivers/analog/CMakeLists.txt b/drivers/analog/CMakeLists.txt index 2a5e8ebb92909..57a836cb8d6f9 100644 --- a/drivers/analog/CMakeLists.txt +++ b/drivers/analog/CMakeLists.txt @@ -95,6 +95,10 @@ if(CONFIG_ADC) if(CONFIG_ADC_HX711) list(APPEND SRCS hx711.c) endif() + + if(CONFIG_ADC_MCP3008) + list(APPEND SRCS mcp3008.c) + endif() endif() if(CONFIG_LMP92001) diff --git a/drivers/analog/Kconfig b/drivers/analog/Kconfig index c7a5f0885b673..3d13c2ffebaa2 100644 --- a/drivers/analog/Kconfig +++ b/drivers/analog/Kconfig @@ -229,6 +229,34 @@ config ADC_HA711_ADD_DELAY endif # ADC_HX711 +config ADC_MCP3008 + bool "MCP3008 support" + default n + select SPI + ---help--- + Enable driver support for the Microchip MCP3008 8-channel, 10-bit ADC. + +if ADC_MCP3008 + +config ADC_MCP3008_SPI_FREQUENCY + int "Frequency in Hz" + default 2340000 + range 0 3600000 + ---help--- + The frequency of SPI communications to the MCP3008, which also has an + effect on sample frequency. 3.6MHz is recommended for VDD >= 4V, 2.34MHz + for VDD >= 3.3V and 1.35MHz for VDD = 2.7V. + +config ADC_MCP3008_DIFFERENTIAL + bool "Differential mode" + default n + ---help--- + If yes, MCP3008 will be used in differential mode, which uses channel pairs + to measure differential signals. Otherwise, single-ended mode is used which + measures the voltage on each channel individually. + +endif # ADC_MCP3008 + endif # ADC config COMP diff --git a/drivers/analog/Make.defs b/drivers/analog/Make.defs index 45ee37f371d4a..0bc3e001cdda8 100644 --- a/drivers/analog/Make.defs +++ b/drivers/analog/Make.defs @@ -107,6 +107,10 @@ endif ifeq ($(CONFIG_ADC_HX711),y) CSRCS += hx711.c endif + +ifeq ($(CONFIG_ADC_MCP3008),y) + CSRCS += mcp3008.c +endif endif ifeq ($(CONFIG_LMP92001),y) diff --git a/drivers/analog/mcp3008.c b/drivers/analog/mcp3008.c new file mode 100644 index 0000000000000..39e393f73c93c --- /dev/null +++ b/drivers/analog/mcp3008.c @@ -0,0 +1,406 @@ +/**************************************************************************** + * drivers/analog/mcp3008.c + * + * Contributed by Matteo Golin + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +/**************************************************************************** + * Preprocessor definitions + ****************************************************************************/ + +#if !defined(CONFIG_SPI) +#error "SPI Support Required." +#endif + +#define MCP3008_NUM_CHANNELS 8 + +/* 3.6MHz is recommended for VDD >= 4V + * 2.34MHz is recommended for VDD >= 3.3V + * 1.35MHz is recommended for VDD = 2.7V + */ + +#ifndef CONFIG_ADC_MCP3008_SPI_FREQUENCY +#define CONFIG_ADC_MCP3008_SPI_FREQUENCY 2340000 +#endif /* CONFIG_ADC_MCP3008_SPI_FREQUENCY */ + +/* Single-ended or differential modes */ + +#ifndef CONFIG_ADC_MCP3008_DIFFERENTIAL +#define CONFIG_ADC_MCP3008_DIFFERENTIAL 0 +#endif /* CONFIG_ADC_MCP3008_DIFFERENTIAL */ + +#define MCP3008_SPI_MODE (SPIDEV_MODE0) + +#if defined(CONFIG_ADC_MCP3008) + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct mcp3008_dev_s +{ + FAR struct spi_dev_s *spi; /* SPI interface */ + FAR const struct adc_callback_s *cb; + bool diff; /* True if the ADC is in differential mode, false otherwise. */ +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +/* ADC methods */ + +static int mcp3008_bind(FAR struct adc_dev_s *dev, + FAR const struct adc_callback_s *callback); +static void mcp3008_reset(FAR struct adc_dev_s *dev); +static int mcp3008_setup(FAR struct adc_dev_s *dev); +static void mcp3008_shutdown(FAR struct adc_dev_s *dev); +static void mcp3008_rxint(FAR struct adc_dev_s *dev, bool enable); +static int mcp3008_ioctl(FAR struct adc_dev_s *dev, int cmd, + unsigned long arg); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct adc_ops_s g_mcp3008ops = +{ + .ao_bind = mcp3008_bind, + .ao_reset = mcp3008_reset, + .ao_setup = mcp3008_setup, + .ao_shutdown = mcp3008_shutdown, + .ao_rxint = mcp3008_rxint, + .ao_ioctl = mcp3008_ioctl, +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: mcp3008_configspi + * + * Description: + * Configure the SPI interface for the MCP3008. + * + ****************************************************************************/ + +static inline void mcp3008_configspi(FAR struct spi_dev_s *spi) +{ + SPI_SETMODE(spi, MCP3008_SPI_MODE); + SPI_SETBITS(spi, 8); + SPI_HWFEATURES(spi, 0); + SPI_SETFREQUENCY(spi, CONFIG_ADC_MCP3008_SPI_FREQUENCY); +} + +/**************************************************************************** + * Name: mcp3008_readchannel + * + * Description: + * Read the corresponding channel of the MCP3008 ADC. + * + * Input Parameters: + * priv - An MCP3008 device structure. + * msg - An ADC message struct where the am_channel member contains the + * channel number to be read, and where the am_data member is where the + * reading is stored. + * + * NOTE: + * When single-ended mode is enabled, the channel number will correspond + * directly to the channel number (0-7) on the ADC. + * When differential mode is enabled, the channel numbers will correspond + * to the following differential pairs: + * + * msg->am_channel Source + * 0 CH0+, CH1- + * 1 CH0-, CH1+ + * 2 CH2+, CH3- + * 3 CH2-, CH3+ + * 4 CH4+, CH5- + * 5 CH4-, CH5+ + * 6 CH6+, CH7- + * 7 CH6-, CH7+ + * + ****************************************************************************/ + +static int mcp3008_readchannel(FAR struct mcp3008_dev_s *priv, + FAR struct adc_msg_s *msg) +{ + /* First byte is 0s followed by start byte. + * Second byte is control bits, set later. + * Third byte contents do not matter, but are 0 here. + * + * When data is received into this buffer: + * First byte is garbage. + * Second and third byte contain b9-b0. + */ + + uint8_t data[3] = + { + 1, 0, 0 + }; + + DEBUGASSERT(priv != NULL); + DEBUGASSERT(msg != NULL); + + if (priv->diff) + { + data[1] = 0x0; /* Differential control bits start with 0 in MSB. */ + } + else + { + data[1] = 0x8; /* Single-ended control bits start with 1 in MSB. */ + } + + /* Only get last three bits of the channel since there are only 8 channels. + */ + + data[1] |= (msg->am_channel & 0x7); + data[1] <<= 4; + + /* Send control message */ + + SPI_LOCK(priv->spi, true); + + mcp3008_configspi(priv->spi); + SPI_SELECT(priv->spi, SPIDEV_ADC(0), true); + + SPI_EXCHANGE(priv->spi, data, data, sizeof(data)); + + SPI_SELECT(priv->spi, SPIDEV_ADC(0), false); + SPI_LOCK(priv->spi, false); + + /* Mask out anything not part of the 10 measurement bits */ + + msg->am_data = ((data[1] & 3) << 8) + (data[2]); + return 0; +} + +/**************************************************************************** + * Name: mcp3008_bind + * + * Description: + * Bind the upper-half driver callbacks to the lower-half implementation. + * This must be called early in order to receive ADC event notifications. + * + ****************************************************************************/ + +static int mcp3008_bind(FAR struct adc_dev_s *dev, + FAR const struct adc_callback_s *callback) +{ + FAR struct mcp3008_dev_s *priv = (FAR struct mcp3008_dev_s *)dev->ad_priv; + + DEBUGASSERT(priv != NULL); + priv->cb = callback; + return 0; +} + +/**************************************************************************** + * Name: mcp3008_reset + * + * Description: + * Reset the ADC device. Called early to initialize the hardware. This + * is called, before ao_setup() and on error conditions. + * The MCP3008 can't be reset, nothing needs to be done here. + * + ****************************************************************************/ + +static void mcp3008_reset(FAR struct adc_dev_s *dev) +{ +} + +/**************************************************************************** + * Name: mcp3008_setup + * + * Description: + * Configure the ADC. This method is called the first time that the ADC + * device is opened. + * MCP3008 runs as soon as it is powered. There is no setup required. + * + ****************************************************************************/ + +static int mcp3008_setup(FAR struct adc_dev_s *dev) +{ + return OK; +} + +/**************************************************************************** + * Name: mcp3008_shutdown + * + * Description: + * Disable the ADC. This method is called when the ADC device is closed. + * This method should reverse the operation of the setup method. + * The MCP3008 cannot be shutdown unless powered off, so nothing is + * required. + * + ****************************************************************************/ + +static void mcp3008_shutdown(FAR struct adc_dev_s *dev) +{ +} + +/**************************************************************************** + * Name: mcp3008_rxint + * + * Description: + * Needed for ADC upper-half compatibility but conversion interrupts + * are not supported by the MCP3008. + * + ****************************************************************************/ + +static void mcp3008_rxint(FAR struct adc_dev_s *dev, bool enable) +{ +} + +/**************************************************************************** + * Name: mcp3008_ioctl + * + * Description: + * All ioctl calls will be routed through this method. + * + ****************************************************************************/ + +static int mcp3008_ioctl(FAR struct adc_dev_s *dev, int cmd, + unsigned long arg) +{ + FAR struct mcp3008_dev_s *priv = (FAR struct mcp3008_dev_s *)dev->ad_priv; + int ret = 0; + + switch (cmd) + { + /* Trigger a measurement */ + + case ANIOC_TRIGGER: + { + struct adc_msg_s msg; + + for (uint8_t i = 0; i < MCP3008_NUM_CHANNELS && (ret == 0); i++) + { + msg.am_channel = i; + ret = mcp3008_readchannel(priv, &msg); + if (ret == 0) + { + priv->cb->au_receive(dev, i, msg.am_data); + } + } + } break; + + /* Change differential mode */ + + case ANIOC_MCP3008_DIFF: + DEBUGASSERT(arg == 0 || arg == 1); + priv->diff = arg; + break; + + /* Get the number of channels */ + + case ANIOC_GET_NCHANNELS: + ret = 8; + break; + + /* Command was not recognized */ + + default: + ret = -EINVAL; + aerr("MCP3008 ioctl: Unrecognized cmd: %d\n", cmd); + break; + } + + return ret; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: mcp3008_initialize + * + * Description: + * Initialize ADC + * + * Input Parameters: + * spi - SPI driver instance + * spidev - SPI chip select number + * + * Returned Value: + * Valid MCP3008 ADC device structure reference on success; a NULL on + * failure + * + ****************************************************************************/ + +FAR struct adc_dev_s *mcp3008_initialize(FAR struct spi_dev_s *spi) +{ + FAR struct mcp3008_dev_s *priv; + FAR struct adc_dev_s *adcdev; + + DEBUGASSERT(spi != NULL); + + /* Initialize the ADC device structure */ + + priv = kmm_malloc(sizeof(struct mcp3008_dev_s)); + if (priv == NULL) + { + aerr("ERROR: Failed to allocate mcp3008_dev_s instance\n"); + free(priv); + return NULL; + } + + /* Initialize the MCP3008 device structure */ + + priv->cb = NULL; + priv->spi = spi; + priv->diff = CONFIG_ADC_MCP3008_DIFFERENTIAL; + + adcdev = kmm_malloc(sizeof(struct adc_dev_s)); + if (adcdev == NULL) + { + aerr("ERROR: Failed to allocate adc_dev_s instance\n"); + return NULL; + } + + memset(adcdev, 0, sizeof(struct adc_dev_s)); + adcdev->ad_ops = &g_mcp3008ops; + adcdev->ad_priv = priv; + + return adcdev; +} + +#endif /* CONFIG_ADC_MCP3008 */ diff --git a/include/nuttx/analog/ioctl.h b/include/nuttx/analog/ioctl.h index a18c4bf1904f9..e3c29f6fcad3a 100644 --- a/include/nuttx/analog/ioctl.h +++ b/include/nuttx/analog/ioctl.h @@ -111,6 +111,11 @@ #define AN_SAMV7_AFEC_FIRST (AN_MCP48XX_FIRST + AN_MCP48XX_NCMDS) #define AN_SAMV7_AFEC_NCMDS 1 +/* See include/nuttx/analog/mcp3008.h */ + +#define AN_MCP3008_FIRST (AN_SAMV7_AFEC_FIRST + AN_SAMV7_AFEC_NCMDS) +#define AN_MCP3008_NCMDS 1 + /**************************************************************************** * Public Function Prototypes ****************************************************************************/ diff --git a/include/nuttx/analog/mcp3008.h b/include/nuttx/analog/mcp3008.h new file mode 100644 index 0000000000000..e6341b765a2a1 --- /dev/null +++ b/include/nuttx/analog/mcp3008.h @@ -0,0 +1,52 @@ +/**************************************************************************** + * include/nuttx/analog/mcp3008.h + * + * Contributed by Matteo Golin + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_ANALOG_MCP3008_H +#define __INCLUDE_NUTTX_ANALOG_MCP3008_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include +#include + +/**************************************************************************** + * Preprocessor definitions + ****************************************************************************/ + +/* IOCTL Commands + * Cmd: ANIOC_MCP3008_DIFF Arg: 1 for differential, 0 for single-ended + */ + +#define ANIOC_MCP3008_DIFF _ANIOC(AN_MCP3008_FIRST + 0) + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +FAR struct adc_dev_s *mcp3008_initialize(FAR struct spi_dev_s *spi); + +#endif /* __INCLUDE_NUTTX_ANALOG_MCP3008_H */