walnux/arch/arm/src/rp2040/rp2040_adc.c
Niccolò Maggioni 7ea3f8e333 arch/arm/rp2040: Support non-sequential ADC channels and standardize internal function names
Enabling a higher channel of the internal ADC had the effect of
initializing the lower ones as well. Now that happens only if
actively requested.

Also, the functions for handling the internal ADC did not follow
the typical naming used by comparable modules for the same arch
and were renamed for coherence. Informational logging calls were
also made slightly more useful and discernible in case of having
multiple ADCs.

Signed-off-by: Niccolò Maggioni <nicco.maggioni+nuttx@gmail.com>
2025-08-13 23:23:17 +08:00

666 lines
19 KiB
C

/****************************************************************************
* arch/arm/src/rp2040/rp2040_adc.c
*
* 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.
*
****************************************************************************/
/****************************************************************************
* Notes:
* The ADC driver upper-half returns signed values of up to 32-bits to
* the user and expects the high-order bits in any result to be
* significant. So, while the RP2040 hardware returns an unsigned value
* in the low-order 12 bits of the result register, we shift this
* value to the high-order bits.
*
* The result is that to convert a 32-bit value returned from the ADC
* driver, you should use V = ADC_AVDD * value / (2^31) where ADC_AVDD
* is the analogue reference voltage supplied to the RP2040 chip. If
* 8 or 16 bit values are returned the divisor would be (2^15) or (2^7)
* respectively.
*
* Also, if the conversion error bit was set for a particular sample,
* the return value will be negated. Any negative return value should
* be treated as erroneous.
*
* -------------
*
* This lower-half supports multiple drivers (/dev/adc0, /dev/adc1, etc.)
* that each may read data from any of the ADC ports. The driver reads
* whichever ADC ports are needed by ANY of the drivers in strict
* round-robin fashion, passing the converted values to the drivers that
* needed it. Data is only passed if the driver is open.
*
* --------------
*
* This code reads the ADC ports at full speed. At the time this comment
* was written, the upper-half will throw away any converted values it
* receives when the buffer is full; therefore, if the data is not read
* for a while, the returned values may be stale when finally read. You
* can use the ANIOC_RESET_FIFO ioctl call to flush this stale data.
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <rp2040_adc.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "arm_internal.h"
#include <rp2040_gpio.h>
#include <hardware/rp2040_adc.h>
#include <nuttx/analog/adc.h>
#include <nuttx/kmalloc.h>
#ifdef CONFIG_RP2040_ADC
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define ADC_CHANNEL_COUNT 5
#define ADC_TEMP_CHANNEL 4
/* Get the private data pointer from a device pointer */
#define PRIV(x) ((struct private_s *)(x)->ad_priv)
/****************************************************************************
* Private Types
****************************************************************************/
struct private_s
{
const struct adc_callback_s *callback; /* ADC Callback Structure */
struct adc_dev_s *next_device;
struct adc_dev_s *prior_device;
bool has_channel[ADC_CHANNEL_COUNT];
};
/****************************************************************************
* Private Functions
****************************************************************************/
static int interrupt_handler(int irq,
void *context,
void *arg);
static void adc_get_next_channel(void);
static void adc_add_device(struct adc_dev_s *dev);
static void adc_remove_device(struct adc_dev_s *dev);
/* ADC methods */
static int adc_bind(struct adc_dev_s *dev,
const struct adc_callback_s *callback);
static void adc_reset(struct adc_dev_s *dev);
static int adc_setup(struct adc_dev_s *dev);
static void adc_shutdown(struct adc_dev_s *dev);
static void adc_rxint(struct adc_dev_s *dev, bool enable);
static int adc_ioctl(struct adc_dev_s *dev, int cmd, unsigned long arg);
/****************************************************************************
* Private Data
****************************************************************************/
static const struct adc_ops_s g_adcops =
{
.ao_bind = adc_bind, /* Called first during initialization. */
.ao_reset = adc_reset, /* Called second during initialization. */
.ao_setup = adc_setup, /* Called during first open. */
.ao_shutdown = adc_shutdown, /* Called during last close. */
.ao_rxint = adc_rxint, /* Called to enable/disable interrupts. */
.ao_ioctl = adc_ioctl, /* Called for custom ioctls. */
};
static const int8_t g_gpio_map[ADC_CHANNEL_COUNT] =
{
26, 27, 28, 29, -1
};
static struct adc_dev_s *g_first_device = NULL;
static uint8_t g_current_channel = 0xf0; /* too big */
static uint8_t g_active_count = 0;
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: interrupt_handler
*
* Description:
* ADC interrupt handler. Note that this one handler is shared between
* all ADC devices.
*
* Note: This is called from inside an interrupt service routine.
*
****************************************************************************/
static int interrupt_handler(int irq, void *context, void *arg)
{
struct adc_dev_s *a_device;
const struct adc_callback_s *a_callback;
int32_t value;
bool error_bit_set;
if (g_active_count == 0)
{
/* Last device has been removed -- turn off ADC */
putreg32(0, RP2040_ADC_CS);
putreg32(0, RP2040_ADC_INTE);
up_disable_irq(RP2040_ADC_IRQ_FIFO);
irq_detach(RP2040_ADC_IRQ_FIFO);
/* Flush the FIFO */
while (getreg32(RP2040_ADC_FCS) & RP2040_ADC_FCS_LEVEL_MASK)
{
getreg32(RP2040_ADC_FIFO);
}
return OK;
}
/* Fetch the data from the FIFO register */
value = getreg32(RP2040_ADC_FIFO);
error_bit_set = (value & RP2040_ADC_FIFO_ERR) != 0;
/* Shift value to top of signed 32-bit word for upper-halfs benefit. */
value <<= 19;
if (error_bit_set)
{
value = -value;
}
for (a_device = g_first_device;
a_device != NULL;
a_device = PRIV(a_device)->next_device)
{
a_callback = PRIV(a_device)->callback;
if (a_callback != NULL && a_callback->au_receive != NULL)
{
if (PRIV(a_device)->has_channel[g_current_channel])
{
if (a_callback->au_receive(a_device,
g_current_channel,
value) != OK)
{
/* ### TODO ### Upper half buffer overflow */
}
}
}
}
/* Start next channel read */
adc_get_next_channel();
return OK;
}
/****************************************************************************
* Name: adc_get_next_channel
*
* Description:
* Update g_current_channel to point to next channel in use and start
* the conversion.
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
static void adc_get_next_channel(void)
{
struct adc_dev_s *a_device;
uint8_t next = g_current_channel + 1;
uint32_t value;
while (true)
{
if (next >= ADC_CHANNEL_COUNT)
{
next = 0;
}
for (a_device = g_first_device;
a_device != NULL;
a_device = PRIV(a_device)->next_device)
{
if (PRIV(a_device)->has_channel[next])
{
g_current_channel = next;
while (getreg32(RP2040_ADC_FCS)
& RP2040_ADC_FCS_LEVEL_MASK)
{
getreg32(RP2040_ADC_FIFO);
}
/* Enable Interrupt on ADC Completion */
putreg32(RP2040_ADC_INTE_FIFO, RP2040_ADC_INTE);
/* Configure CS to read one value from current channel */
value = (g_current_channel << RP2040_ADC_CS_AINSEL_SHIFT)
| RP2040_ADC_CS_EN;
if (g_current_channel == ADC_TEMP_CHANNEL)
{
value |= RP2040_ADC_CS_TS_ENA;
}
putreg32(value, RP2040_ADC_CS);
while ((getreg32(RP2040_ADC_CS)
& RP2040_ADC_CS_READY) == 0)
{
/* Wait for ready to go high. The rp2040 docs
* say this is only a few clock cycles so we'll
* just busy wait.
*/
}
/* Start the conversion */
value += RP2040_ADC_CS_START_ONCE;
putreg32(value, RP2040_ADC_CS);
return;
}
}
/* if we've looped all the way we're in trouble */
ASSERT(next != g_current_channel);
/* try another */
next += 1;
}
}
/****************************************************************************
* Name: adc_add_device
*
* Description:
* This function is called to link the device int the device list.
* It also makes sure ADC reads are taking place
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
static void adc_add_device(struct adc_dev_s *dev)
{
uint32_t value;
g_active_count += 1;
if (g_first_device != NULL)
{
PRIV(g_first_device)->prior_device = dev;
}
PRIV(dev)->next_device = g_first_device;
PRIV(dev)->prior_device = NULL;
g_first_device = dev;
if (PRIV(g_first_device)->next_device == NULL)
{
/* We just added first device */
/* Make sure ADC interrupts are disabled */
putreg32(0, RP2040_ADC_INTE);
/* Configure FCS to use FIFO and interrupt on first value */
value = (1 << RP2040_ADC_FCS_THRESH_SHIFT)
| RP2040_ADC_FCS_OVER
| RP2040_ADC_FCS_UNDER
| RP2040_ADC_FCS_ERR
| RP2040_ADC_FCS_EN;
putreg32(value, RP2040_ADC_FCS);
/* Set up for interrupts */
irq_attach(RP2040_ADC_IRQ_FIFO, interrupt_handler, NULL);
up_enable_irq(RP2040_ADC_IRQ_FIFO);
/* Start conversions on first required channel. */
adc_get_next_channel();
ainfo("new cur %d\n", g_current_channel);
}
}
/****************************************************************************
* Name: adc_remove_device
*
* Description:
* This function is called to unlink the device from the device list.
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
void adc_remove_device(struct adc_dev_s *dev)
{
struct adc_dev_s *a_device;
if (dev == g_first_device)
{
/* Special handling for first device */
g_first_device = PRIV(g_first_device)->next_device;
if (g_first_device != NULL)
{
PRIV(g_first_device)->prior_device = NULL;
}
g_active_count -= 1;
}
else
{
/* Make sure dev is on the change, and unlink if it is. */
for (a_device = g_first_device;
a_device != NULL;
a_device = PRIV(a_device)->next_device)
{
if (a_device == dev)
{
PRIV(PRIV(dev)->prior_device)->next_device =
PRIV(dev)->next_device;
PRIV(PRIV(dev)->next_device)->prior_device =
PRIV(dev)->prior_device;
g_active_count -= 1;
break;
}
}
}
}
/****************************************************************************
* Name: adc_bind
*
* Description:
* This function is called when a driver is registered. It give us a
* chance to bind the upper-half callbacks to our private data structure so
* they can be accessed later.
*
****************************************************************************/
static int adc_bind(struct adc_dev_s *dev,
const struct adc_callback_s *callback)
{
DEBUGASSERT(PRIV(dev) != NULL);
ainfo("Binding: 0x%08lX\n", dev);
PRIV(dev)->callback = callback;
return OK;
}
/****************************************************************************
* Name: adc_reset
*
* Description:
* This is called by the upper-half as part of the driver registration
* process. The upper half documentation also claims that it may
* be called as part of an error condition.
*
* Set the pin for the ADC's to standard GPIO input with no pulls.
*
****************************************************************************/
static void adc_reset(struct adc_dev_s *dev)
{
int a_gpio;
ainfo("Resetting: 0x%08lX\n", dev);
for (int i = 0; i < ADC_CHANNEL_COUNT; ++i)
{
a_gpio = g_gpio_map[i];
if (PRIV(dev)->has_channel[i])
{
if (a_gpio >= 0)
{
ainfo("Attaching GPIO %d to ADC channel %d\n", a_gpio, i + 1);
rp2040_gpio_setdir(a_gpio, false);
rp2040_gpio_set_function(a_gpio, RP2040_GPIO_FUNC_NULL);
rp2040_gpio_set_pulls(a_gpio, false, false);
}
else
{
ainfo("ADC channel %d is internally connected\n", i + 1);
}
}
}
}
/****************************************************************************
* Name: adc_setup
*
* Description:
* This is called when a particular ADC driver is first opened.
*
* We don't do anything here.
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
static int adc_setup(struct adc_dev_s *dev)
{
int ret;
ainfo("Setup: 0x%08lX\n", dev);
/* Note: We check g_active_count here so we can return an error
* in the, probably impossible, case we have too many.
*/
if (g_active_count >= 200)
{
aerr("Too many active devices.");
ret = -EBUSY;
}
else
{
ret = OK;
}
return ret;
}
/****************************************************************************
* Name: adc_shutdown
*
* Description:
* This is called to shutdown an ADC device. It unlinks the
* device from out local chain and turns off ADC interrupts if no
* more devices are active.
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
static void adc_shutdown(struct adc_dev_s *dev)
{
ainfo("Shutdown: 0x%08lX\n", dev);
/* Remove adc_dev_s structure from the list */
adc_remove_device(dev);
}
/****************************************************************************
* Name: adc_rxint
*
* Description:
* Call to enable or disable ADC RX interrupts
*
* Note: This is called from inside a critical section.
*
****************************************************************************/
static void adc_rxint(struct adc_dev_s *dev, bool enable)
{
if (enable)
{
ainfo("Enabling: 0x%08lX\n", dev);
adc_add_device(dev);
}
else
{
ainfo("Disabling: 0x%08lX\n", dev);
adc_remove_device(dev);
}
}
/****************************************************************************
* Name: adc_ioctl
*
* Description:
* All ioctl calls will be routed through this method
*
****************************************************************************/
static int adc_ioctl(struct adc_dev_s *dev,
int cmd,
unsigned long arg)
{
/* No ioctl commands supported */
ainfo("ioctl: 0x%08lX\n", dev);
return -ENOTTY;
}
/****************************************************************************
* Public Function
****************************************************************************/
#ifdef CONFIG_ADC
/****************************************************************************
* Name: rp2040_adc_initialize
*
* Description:
* Initialize and register the ADC driver.
*
* Input Parameters:
* path - Path to the adc device (e.g. "/dev/adc0")
* read_adc0 - This device reads ADC0
* read_adc1 - This device reads ADC1
* read_adc2 - This device reads ADC3
* read_adc3 - This device reads ADC4
* read_temp - This device reads the chip temperature.
*
* Returned Value:
* An opaque pointer that can be passed to rp2040_adc_release on
* success or NULL (with errno set) on failure
****************************************************************************/
int rp2040_adc_initialize(const char *path,
bool read_adc0,
bool read_adc1,
bool read_adc2,
bool read_adc3,
bool read_temp)
{
struct adc_dev_s *dev;
struct private_s *priv;
int ret;
ainfo("Initializing: 0x%08lX\n", dev);
if (!read_adc0 && !read_adc1 && !read_adc2 && !read_adc3 && !read_temp)
{
aerr("No ADC inputs selected.\n");
return -EINVAL;
}
dev = kmm_zalloc(sizeof(struct adc_dev_s));
if (dev == NULL)
{
aerr("Failed to allocate adc_dev_s.\n");
return -ENOMEM;
}
priv = kmm_zalloc(sizeof(struct private_s));
if (priv == NULL)
{
aerr("Failed to allocate private_s.\n");
kmm_free(dev);
return -ENOMEM;
}
priv->has_channel[0] = read_adc0;
priv->has_channel[1] = read_adc1;
priv->has_channel[2] = read_adc2;
priv->has_channel[3] = read_adc3;
priv->has_channel[4] = read_temp;
dev->ad_ops = &g_adcops;
dev->ad_priv = priv;
ret = adc_register(path, dev);
return ret;
}
#endif /* if CONFIG_ADC */
#endif /* if CONFIG_RP2040_ADC */