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.

This commit is contained in:
Matteo Golin 2024-10-21 19:00:51 -04:00 committed by Xiang Xiao
parent 9b0fc1277b
commit bbc95d70db
9 changed files with 648 additions and 0 deletions

View File

@ -1,3 +1,5 @@
.. _adc-example:
=====================
``adc`` Read from ADC
=====================

View File

@ -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 <nuttx/analog/mcp3008.h>
#include <nuttx/analog/adc.h>
/* 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.

View File

@ -87,6 +87,12 @@
#include "rp2040_adc.h"
#endif
#if defined(CONFIG_ADC) && defined(CONFIG_ADC_MCP3008)
#include <nuttx/analog/mcp3008.h>
#include <nuttx/analog/adc.h>
#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 */

View File

@ -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)

View File

@ -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

View File

@ -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)

406
drivers/analog/mcp3008.c Normal file
View File

@ -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 <nuttx/config.h>
#include <assert.h>
#include <debug.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
#include <nuttx/analog/adc.h>
#include <nuttx/arch.h>
#include <nuttx/kmalloc.h>
#include <nuttx/spi/spi.h>
#include <nuttx/analog/mcp3008.h>
/****************************************************************************
* 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 */

View File

@ -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
****************************************************************************/

View File

@ -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 <nuttx/analog/ioctl.h>
#include <nuttx/config.h>
#include <nuttx/spi/spi.h>
/****************************************************************************
* 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 */