mirror of
https://github.com/zephyrproject-rtos/zephyr
synced 2025-08-09 01:16:06 +00:00
Move all adc driver api structs into an iterable section, this allows us to verify if an api pointer is located in compatible linker section. Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
1252 lines
36 KiB
C
1252 lines
36 KiB
C
/*
|
|
* Copyright (c) 2024 STMicroelectronics
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Terminology used in this file:
|
|
* - sampling: a single analog-to-digital conversion performed by the ADC
|
|
* - sequence: one or more sampling(s) performed one after the other by the
|
|
* ADC after a single programmation. This is the meaning used in the
|
|
* STM32WB0 ADC documentation.
|
|
* - round: all ADC operations needed to read all channels in the adc_sequence passed
|
|
* to adc_read. Zephyr calls this a "sampling", but we use the term "round" to
|
|
* prevent confusion with STM32 terminology. A single round may require multiple
|
|
* sequences to be performed by the ADC to be completed, due to hardware limitations.
|
|
*
|
|
* When Zephyr's "sequence" feature is used, the same round is repeated multiple times.
|
|
*
|
|
* - idle mode: clock & ADC configuration that minimizes power consumption
|
|
* - Only the ADC digital domain clock is turned on:
|
|
* - ADC is powered off (CTRL.ADC_CTRL_ADC_ON_OFF = 0)
|
|
* - ADC analog domain clock is turned off
|
|
* - If applicable:
|
|
* - ADC LDO is disabled
|
|
* - ADC I/O Booster clock is turned off
|
|
* - ADC I/O Booster is disabled
|
|
* - ADC-SMPS clock synchronization is disabled
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT st_stm32wb0_adc
|
|
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
|
|
#include <zephyr/drivers/adc.h>
|
|
#include <zephyr/drivers/pinctrl.h>
|
|
#include <zephyr/drivers/clock_control/stm32_clock_control.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/init.h>
|
|
#include <zephyr/pm/device.h>
|
|
#include <zephyr/pm/policy.h>
|
|
#include <zephyr/sys/check.h>
|
|
#include <zephyr/sys/util_macro.h>
|
|
#include <zephyr/sys/math_extras.h>
|
|
|
|
#include <soc.h>
|
|
#include <stm32_ll_adc.h>
|
|
#include <stm32_ll_utils.h>
|
|
|
|
#ifdef CONFIG_ADC_STM32_DMA
|
|
#include <zephyr/drivers/dma/dma_stm32.h>
|
|
#include <zephyr/drivers/dma.h>
|
|
#include <zephyr/toolchain.h>
|
|
#include <stm32_ll_dma.h>
|
|
#endif
|
|
|
|
#define ADC_CONTEXT_USES_KERNEL_TIMER
|
|
#define ADC_CONTEXT_ENABLE_ON_COMPLETE
|
|
#include "adc_context.h"
|
|
|
|
#include <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(adc_stm32wb0, CONFIG_ADC_LOG_LEVEL);
|
|
|
|
/**
|
|
* Driver private definitions & assertions
|
|
*/
|
|
#define ADC_INSTANCE 0
|
|
#define ADC_NODE DT_DRV_INST(ADC_INSTANCE)
|
|
#define ADC_USE_IO_BOOSTER DT_PROP_OR(ADC_NODE, io_booster, 0)
|
|
|
|
#define LL_ADC_EXTERNAL_CHANNEL_NUM 12 /* must be a plain constant for LISTIFY */
|
|
#define LL_ADC_EXTERNAL_CHANNEL_MAX (LL_ADC_CHANNEL_VINP3_VINM3 + 1U)
|
|
#define LL_ADC_CHANNEL_MAX (LL_ADC_CHANNEL_TEMPSENSOR + 1U)
|
|
#define LL_ADC_VIN_RANGE_INVALID ((uint8_t)0xFFU)
|
|
|
|
#define NUM_CALIBRATION_POINTS 4 /* 4 calibration point registers (COMP_[0-3]) */
|
|
|
|
#if !defined(ADC_CONF_SAMPLE_RATE_MSB)
|
|
# define NUM_ADC_SAMPLE_RATES 4 /* SAMPLE_RATE on 2 bits */
|
|
#else
|
|
# define NUM_ADC_SAMPLE_RATES 32 /* SAMPLE_RATE on 5 bits */
|
|
#endif
|
|
|
|
/* The STM32WB0 has a 12-bit ADC, but the resolution can be
|
|
* enhanced to 16-bit by oversampling (using the downsampler)
|
|
*/
|
|
#define ADC_MIN_RESOLUTION 12
|
|
#define ADC_MAX_RESOLUTION 16
|
|
|
|
/* ADC channel type definitions are not provided by LL as
|
|
* it uses per-type functions instead. Bring our own.
|
|
*/
|
|
#define ADC_CHANNEL_TYPE_SINGLE_NEG (0x00U) /* Single-ended, positive */
|
|
#define ADC_CHANNEL_TYPE_SINGLE_POS (0x01U) /* Single-ended, negative */
|
|
#define ADC_CHANNEL_TYPE_DIFF (0x02U) /* Differential */
|
|
#define ADC_CHANNEL_TYPE_INVALID (0xFFU) /* Invalid */
|
|
|
|
/** See RM0505 §6.2.1 "System clock details" */
|
|
BUILD_ASSERT(CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC >= (8 * 1000 * 1000),
|
|
"STM32WB0: system clock frequency must be at least 8MHz to use ADC");
|
|
|
|
/**
|
|
* Driver private structures
|
|
*/
|
|
struct adc_stm32wb0_data {
|
|
struct adc_context ctx;
|
|
const struct device *const dev;
|
|
|
|
/**
|
|
* Bitmask of all channels requested as part of this round
|
|
* but not sampled yet.
|
|
*/
|
|
uint32_t unsampled_channels;
|
|
|
|
/**
|
|
* Pointer in output buffer where the first data sample of the round
|
|
* is stored. This is used to reload next_sample_ptr when the user
|
|
* callback asks to repeat a round.
|
|
*/
|
|
uint16_t *round_buf_pointer;
|
|
|
|
/**
|
|
* Pointer in output buffer where the next data sample from ADC should
|
|
* be stored.
|
|
*/
|
|
uint16_t *next_sample_ptr;
|
|
|
|
#if defined(CONFIG_ADC_STM32_DMA)
|
|
/** Size of the sequence currently scheduled and executing */
|
|
size_t sequence_length;
|
|
struct dma_config dmac_config;
|
|
struct dma_block_config dma_block_config;
|
|
#endif
|
|
|
|
/** Channels configuration */
|
|
struct adc_stm32wb0_channel_config {
|
|
/** Vinput range selection */
|
|
uint8_t vinput_range;
|
|
} channel_config[LL_ADC_CHANNEL_MAX];
|
|
};
|
|
|
|
struct adc_stm32wb0_config {
|
|
ADC_TypeDef *reg;
|
|
const struct pinctrl_dev_config *pinctrl_cfg;
|
|
/** ADC digital domain clock */
|
|
struct stm32_pclken dig_clk;
|
|
/** ADC analog domain clock */
|
|
struct stm32_pclken ana_clk;
|
|
#if defined(CONFIG_ADC_STM32_DMA)
|
|
const struct device *dmac;
|
|
uint32_t dma_channel;
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
* Driver private utility functions
|
|
*/
|
|
|
|
/**
|
|
* In STM32CubeWB0 v1.0.0, the LL_GetPackageType is buggy and returns wrong values.
|
|
* This bug is reported in the ST internal bugtracker under reference 185295.
|
|
* For now, implement the function ourselves.
|
|
*/
|
|
static inline uint32_t ll_get_package_type(void)
|
|
{
|
|
return sys_read32(PACKAGE_BASE);
|
|
}
|
|
|
|
static inline struct adc_stm32wb0_data *drv_data_from_adc_ctx(struct adc_context *adc_ctx)
|
|
{
|
|
return CONTAINER_OF(adc_ctx, struct adc_stm32wb0_data, ctx);
|
|
}
|
|
|
|
static inline uint8_t vinput_range_from_adc_ref(uint32_t reference)
|
|
{
|
|
switch (reference) {
|
|
case ADC_REF_INTERNAL:
|
|
case ADC_REF_VDD_1:
|
|
return LL_ADC_VIN_RANGE_3V6;
|
|
case ADC_REF_VDD_1_2:
|
|
return LL_ADC_VIN_RANGE_2V4;
|
|
case ADC_REF_VDD_1_3:
|
|
return LL_ADC_VIN_RANGE_1V2;
|
|
default:
|
|
return LL_ADC_VIN_RANGE_INVALID;
|
|
}
|
|
}
|
|
|
|
static inline uint32_t ds_width_from_adc_res(uint32_t resolution)
|
|
{
|
|
/*
|
|
* 12 -> 0 (LL_ADC_DS_DATA_WIDTH_12_BIT)
|
|
* 13 -> 1 (LL_ADC_DS_DATA_WIDTH_13_BIT)
|
|
* 14 -> 2 (LL_ADC_DS_DATA_WIDTH_14_BIT)
|
|
* 15 -> 3 (LL_ADC_DS_DATA_WIDTH_15_BIT)
|
|
* 16 -> 4 (LL_ADC_DS_DATA_WIDTH_16_BIT)
|
|
*/
|
|
return resolution - 12;
|
|
}
|
|
|
|
static inline uint8_t get_channel_type(uint32_t channel)
|
|
{
|
|
switch (channel) {
|
|
case LL_ADC_CHANNEL_VINM0:
|
|
case LL_ADC_CHANNEL_VINM1:
|
|
case LL_ADC_CHANNEL_VINM2:
|
|
case LL_ADC_CHANNEL_VINM3:
|
|
case LL_ADC_CHANNEL_VBAT:
|
|
return ADC_CHANNEL_TYPE_SINGLE_NEG;
|
|
case LL_ADC_CHANNEL_VINP0:
|
|
case LL_ADC_CHANNEL_VINP1:
|
|
case LL_ADC_CHANNEL_VINP2:
|
|
case LL_ADC_CHANNEL_VINP3:
|
|
case LL_ADC_CHANNEL_TEMPSENSOR:
|
|
return ADC_CHANNEL_TYPE_SINGLE_POS;
|
|
case LL_ADC_CHANNEL_VINP0_VINM0:
|
|
case LL_ADC_CHANNEL_VINP1_VINM1:
|
|
case LL_ADC_CHANNEL_VINP2_VINM2:
|
|
case LL_ADC_CHANNEL_VINP3_VINM3:
|
|
return ADC_CHANNEL_TYPE_DIFF;
|
|
default:
|
|
__ASSERT_NO_MSG(0);
|
|
return ADC_CHANNEL_TYPE_INVALID;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Checks all fields of the adc_sequence and asserts they are
|
|
* valid and all configuration options are supported by the driver.
|
|
*
|
|
* @param sequence adc_sequence to validate
|
|
* @return 0 if the adc_sequence is valid, negative value otherwise
|
|
*/
|
|
static int validate_adc_sequence(const struct adc_sequence *sequence)
|
|
{
|
|
const size_t round_size = sizeof(uint16_t) * POPCOUNT(sequence->channels);
|
|
size_t needed_buf_size;
|
|
|
|
if (sequence->channels == 0 ||
|
|
(sequence->channels & ~BIT_MASK(LL_ADC_CHANNEL_MAX)) != 0) {
|
|
LOG_ERR("invalid channels selection");
|
|
return -EINVAL;
|
|
}
|
|
|
|
CHECKIF(!sequence->buffer) {
|
|
LOG_ERR("storage buffer pointer is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!IN_RANGE(sequence->resolution, ADC_MIN_RESOLUTION, ADC_MAX_RESOLUTION)) {
|
|
LOG_ERR("invalid resolution %u (must be between %u and %u)",
|
|
sequence->resolution, ADC_MIN_RESOLUTION, ADC_MAX_RESOLUTION);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* N.B.: LL define is in the same log2(x) format as the Zephyr variable */
|
|
if (sequence->oversampling > LL_ADC_DS_RATIO_128) {
|
|
LOG_ERR("oversampling unsupported by hardware (max: %lu)", LL_ADC_DS_RATIO_128);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if (sequence->options) {
|
|
const size_t samplings = (size_t)sequence->options->extra_samplings + 1;
|
|
|
|
if (size_mul_overflow(round_size, samplings, &needed_buf_size)) {
|
|
return -ENOMEM;
|
|
}
|
|
} else {
|
|
needed_buf_size = round_size;
|
|
}
|
|
|
|
if (needed_buf_size > sequence->buffer_size) {
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Set which channel is sampled during a given conversion of the sequence.
|
|
*
|
|
* @param ADCx ADC registers pointer
|
|
* @param Conversion Target conversion index (0~15)
|
|
* @param Channel Channel to sample during specified conversion
|
|
*
|
|
* @note This function is a more convenient implementation of LL_ADC_SetSequencerRanks
|
|
*/
|
|
static inline void ll_adc_set_conversion_channel(ADC_TypeDef *ADCx,
|
|
uint32_t Conversion, uint32_t Channel)
|
|
{
|
|
/**
|
|
* There are two registers to control the sequencer:
|
|
* - SEQ_1 holds channel selection for conversions 0~7
|
|
* - SEQ_2 holds channel selection for conversions 8~15
|
|
*
|
|
* Notice that all conversions in SEQ_2 have 3rd bit set,
|
|
* whereas all conversions in SEQ_1 have 3rd bit clear.
|
|
*
|
|
* In a SEQ_x register, each channel occupies 4 bits, so the
|
|
* field for conversion N is at bit offset (4 * (N % 7)).
|
|
*/
|
|
const uint32_t reg = (Conversion & 8) ? 1 : 0;
|
|
const uint32_t shift = 4 * (Conversion & 7);
|
|
|
|
MODIFY_REG((&ADCx->SEQ_1)[reg], ADC_SEQ_1_SEQ0 << shift, Channel << shift);
|
|
}
|
|
|
|
/**
|
|
* @brief Set the calibration point to use for a chosen channel type and Vinput range.
|
|
*
|
|
* @param ADCx ADC registers pointer
|
|
* @param Type Channel type
|
|
* @param Range Channel Vinput range
|
|
* @param Point Calibration point to use
|
|
*
|
|
* @note This is a generic version of the LL_ADC_SetCalibPointFor* functions.
|
|
*/
|
|
static inline void ll_adc_set_calib_point_for_any(ADC_TypeDef *ADCx, uint32_t Type,
|
|
uint32_t Range, uint32_t Point)
|
|
{
|
|
__ASSERT(Range == LL_ADC_VIN_RANGE_1V2
|
|
|| Range == LL_ADC_VIN_RANGE_2V4
|
|
|| Range == LL_ADC_VIN_RANGE_3V6, "Range is not valid");
|
|
|
|
__ASSERT(Type == ADC_CHANNEL_TYPE_SINGLE_NEG
|
|
|| Type == ADC_CHANNEL_TYPE_SINGLE_POS
|
|
|| Type == ADC_CHANNEL_TYPE_DIFF, "Type is not valid");
|
|
|
|
__ASSERT(Point == LL_ADC_CALIB_POINT_1
|
|
|| Point == LL_ADC_CALIB_POINT_2
|
|
|| Point == LL_ADC_CALIB_POINT_3
|
|
|| Point == LL_ADC_CALIB_POINT_4, "Point is not valid");
|
|
|
|
/* Register organization is as follows:
|
|
*
|
|
* - Group for 1.2V Vinput range
|
|
* - Group for 2.4V Vinput range
|
|
* - Group for 3.6V Vinput range
|
|
*
|
|
* where Group is organized as:
|
|
* - Select for Single Negative mode
|
|
* - Select for Single Positive mode
|
|
* - Select for Differential mode
|
|
*
|
|
* Each select is 2 bits, and each group is thus 6 bits.
|
|
*/
|
|
|
|
uint32_t type_shift, group_shift;
|
|
|
|
switch (Type) {
|
|
case ADC_CHANNEL_TYPE_SINGLE_NEG:
|
|
type_shift = 0 * 2;
|
|
break;
|
|
case ADC_CHANNEL_TYPE_SINGLE_POS:
|
|
type_shift = 1 * 2;
|
|
break;
|
|
case ADC_CHANNEL_TYPE_DIFF:
|
|
type_shift = 2 * 2;
|
|
break;
|
|
default:
|
|
CODE_UNREACHABLE;
|
|
}
|
|
|
|
switch (Range) {
|
|
case LL_ADC_VIN_RANGE_1V2:
|
|
group_shift = 0 * 6;
|
|
break;
|
|
case LL_ADC_VIN_RANGE_2V4:
|
|
group_shift = 1 * 6;
|
|
break;
|
|
case LL_ADC_VIN_RANGE_3V6:
|
|
group_shift = 2 * 6;
|
|
break;
|
|
default:
|
|
CODE_UNREACHABLE;
|
|
}
|
|
|
|
const uint32_t shift = (group_shift + type_shift);
|
|
|
|
MODIFY_REG(ADCx->COMP_SEL, (ADC_COMP_SEL_OFFSET_GAIN0 << shift), (Point << shift));
|
|
}
|
|
|
|
static void adc_acquire_pm_locks(void)
|
|
{
|
|
pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
|
|
if (IS_ENABLED(CONFIG_PM_S2RAM)) {
|
|
pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_RAM, PM_ALL_SUBSTATES);
|
|
}
|
|
}
|
|
|
|
static void adc_release_pm_locks(void)
|
|
{
|
|
pm_policy_state_lock_put(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
|
|
if (IS_ENABLED(CONFIG_PM_S2RAM)) {
|
|
pm_policy_state_lock_put(PM_STATE_SUSPEND_TO_RAM, PM_ALL_SUBSTATES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Driver private functions
|
|
*/
|
|
|
|
static void configure_tempsensor_calib_point(ADC_TypeDef *adc, uint32_t calib_point)
|
|
{
|
|
uint16_t gain;
|
|
#if defined(CONFIG_SOC_STM32WB09XX) || defined(CONFIG_SOC_STM32WB05XX)
|
|
/** RM0505/RM0529 §12.2.1 "Temperature sensor subsystem" */
|
|
gain = 0xFFF;
|
|
#else
|
|
/** RM0530 §12.2.2 "Temperature sensor subsystem" */
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINPX_1V2();
|
|
#endif /* CONFIG_SOC_STM32WB09XX | CONFIG_SOC_STM32WB05XX */
|
|
|
|
LL_ADC_ConfigureCalibPoint(adc, calib_point, gain, 0x0);
|
|
}
|
|
|
|
/**
|
|
* @brief Obtain calibration data for specified channel type and Vinput range
|
|
* from engineering flash, and write it to specified calibration point
|
|
*
|
|
* @param ADCx ADC registers pointer
|
|
* @param Point Calibration point to configure
|
|
* @param Type Target channel type
|
|
* @param Range Target channel Vinput range
|
|
*/
|
|
static void configure_calib_point_from_flash(ADC_TypeDef *ADCx, uint32_t Point,
|
|
uint32_t Type, uint32_t Range)
|
|
{
|
|
int8_t offset = 0;
|
|
uint16_t gain = 0;
|
|
|
|
switch (Range) {
|
|
case LL_ADC_VIN_RANGE_1V2:
|
|
switch (Type) {
|
|
case ADC_CHANNEL_TYPE_SINGLE_POS:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINPX_1V2();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINPX_1V2();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_SINGLE_NEG:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINMX_1V2();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINMX_1V2();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_DIFF:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINDIFF_1V2();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINDIFF_1V2();
|
|
break;
|
|
}
|
|
break;
|
|
case LL_ADC_VIN_RANGE_2V4:
|
|
switch (Type) {
|
|
case ADC_CHANNEL_TYPE_SINGLE_POS:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINPX_2V4();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINPX_2V4();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_SINGLE_NEG:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINMX_2V4();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINMX_2V4();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_DIFF:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINDIFF_2V4();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINDIFF_2V4();
|
|
break;
|
|
}
|
|
break;
|
|
case LL_ADC_VIN_RANGE_3V6:
|
|
switch (Type) {
|
|
case ADC_CHANNEL_TYPE_SINGLE_POS:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINPX_3V6();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINPX_3V6();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_SINGLE_NEG:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINMX_3V6();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINMX_3V6();
|
|
break;
|
|
case ADC_CHANNEL_TYPE_DIFF:
|
|
gain = LL_ADC_GET_CALIB_GAIN_FOR_VINDIFF_3V6();
|
|
offset = LL_ADC_GET_CALIB_OFFSET_FOR_VINDIFF_3V6();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
LL_ADC_ConfigureCalibPoint(ADCx, Point, gain, offset);
|
|
}
|
|
|
|
static void adc_enter_idle_mode(ADC_TypeDef *adc, const struct stm32_pclken *ana_clk)
|
|
{
|
|
const struct device *clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
|
|
int err;
|
|
|
|
/* Power down the ADC */
|
|
LL_ADC_Disable(adc);
|
|
|
|
#if SMPS_MODE != STM32WB0_SMPS_MODE_OFF
|
|
/* Disable SMPS synchronization */
|
|
LL_ADC_SMPSSyncDisable(adc);
|
|
#endif /* SMPS_MODE != STM32WB0_SMPS_MODE_OFF */
|
|
|
|
#if ADC_USE_IO_BOOSTER
|
|
/* Disable ADC I/O booster */
|
|
LL_RCC_IOBOOST_Disable();
|
|
|
|
# if defined(RCC_CFGR_IOBOOSTCLKEN)
|
|
/* Disable ADC I/O Booster clock if present */
|
|
LL_RCC_IOBOOSTCLK_Disable();
|
|
# endif
|
|
#endif /* ADC_USE_IO_BOOSTER */
|
|
|
|
#if defined(ADC_CTRL_ADC_LDO_ENA)
|
|
/* Disable ADC voltage regulator */
|
|
LL_ADC_DisableInternalRegulator(adc);
|
|
#endif /* ADC_CTRL_ADC_LDO_ENA */
|
|
|
|
/* Turn off ADC analog domain clock */
|
|
err = clock_control_off(clk, (clock_control_subsys_t)ana_clk);
|
|
if (err < 0) {
|
|
LOG_WRN("failed to turn off ADC analog clock (%d)", err);
|
|
}
|
|
|
|
/* Release power management locks */
|
|
adc_release_pm_locks();
|
|
}
|
|
|
|
static int adc_exit_idle_mode(ADC_TypeDef *adc, const struct stm32_pclken *ana_clk)
|
|
{
|
|
const struct device *clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
|
|
int err;
|
|
|
|
/* Acquire power management locks */
|
|
adc_acquire_pm_locks();
|
|
|
|
/* Turn on ADC analog domain clock */
|
|
err = clock_control_on(clk,
|
|
(clock_control_subsys_t)ana_clk);
|
|
if (err < 0) {
|
|
LOG_ERR("failed to turn on ADC analog clock: %d", err);
|
|
adc_release_pm_locks();
|
|
return err;
|
|
}
|
|
|
|
#if defined(ADC_CTRL_ADC_LDO_ENA)
|
|
/* RM0479 §12.6.3: bit ADC_LDO_ENA must not be set on QFN32 packages.
|
|
* Using an equality check with supported package types ensures that
|
|
* we never accidentally set the bit on an unsupported MCU.
|
|
*/
|
|
const uint32_t package_type = ll_get_package_type();
|
|
|
|
if (package_type == LL_UTILS_PACKAGETYPE_QFN48
|
|
|| package_type == LL_UTILS_PACKAGETYPE_CSP49) {
|
|
LL_ADC_EnableInternalRegulator(adc);
|
|
}
|
|
#endif /* ADC_CTRL_ADC_LDO_ENA */
|
|
|
|
#if ADC_USE_IO_BOOSTER
|
|
# if defined(RCC_CFGR_IOBOOSTCLKEN)
|
|
/* Enable ADC I/O Booster clock if needed by hardware */
|
|
LL_RCC_IOBOOSTCLK_Enable();
|
|
# endif
|
|
|
|
/* Enable ADC I/O Booster */
|
|
LL_RCC_IOBOOST_Enable();
|
|
#endif /* ADC_USE_IO_BOOSTER*/
|
|
|
|
#if SMPS_MODE != STM32WB0_SMPS_MODE_OFF
|
|
/* RM0505 §6.2.2 "Peripherals clock details":
|
|
* To avoid SNR degradation of the ADC,
|
|
* SMPS and ADC clocks must be synchronous.
|
|
*/
|
|
LL_ADC_SMPSSyncEnable(adc);
|
|
#endif /* SMPS_MODE != STM32WB0_SMPS_MODE_OFF */
|
|
|
|
/* Power up the ADC */
|
|
LL_ADC_Enable(adc);
|
|
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* @brief Schedule as many samplings as possible in a sequence
|
|
* then start the ADC conversion.
|
|
*/
|
|
static void schedule_and_start_adc_sequence(ADC_TypeDef *adc, struct adc_stm32wb0_data *data)
|
|
{
|
|
uint32_t remaining_unsampled = data->unsampled_channels;
|
|
uint32_t allocated_calib_points = 0;
|
|
uint32_t sequence_length = 0;
|
|
bool temp_sensor_scheduled = false;
|
|
|
|
/**
|
|
* These tables are used to keep track of which calibration
|
|
* point registers are used for what type of acquisition, in
|
|
* order to share the same calibration point for different
|
|
* channels if they use compatible configurations.
|
|
*
|
|
* Initialize only the first table with invalid values; since
|
|
* both tables are updated at the same time, this is sufficient
|
|
* to know when to stop programming calibration points.
|
|
*/
|
|
uint8_t calib_pt_ch_type[NUM_CALIBRATION_POINTS] = {
|
|
ADC_CHANNEL_TYPE_INVALID, ADC_CHANNEL_TYPE_INVALID,
|
|
ADC_CHANNEL_TYPE_INVALID, ADC_CHANNEL_TYPE_INVALID
|
|
};
|
|
uint8_t calib_pt_vin_range[NUM_CALIBRATION_POINTS];
|
|
|
|
/* Schedule as many channels as possible for sampling */
|
|
for (uint32_t channel = 0;
|
|
channel < LL_ADC_CHANNEL_MAX && remaining_unsampled != 0U;
|
|
channel++) {
|
|
const uint32_t ch_bit = BIT(channel);
|
|
|
|
if ((remaining_unsampled & ch_bit) == 0) {
|
|
continue;
|
|
}
|
|
|
|
/* Get channel information */
|
|
const uint8_t ch_type = get_channel_type(channel);
|
|
const uint8_t ch_vin_range = data->channel_config[channel].vinput_range;
|
|
|
|
/* Attempt to find a compatible calibration point */
|
|
uint32_t calib_pt = 0;
|
|
|
|
for (; calib_pt < allocated_calib_points; calib_pt++) {
|
|
if (calib_pt_ch_type[calib_pt] == ch_type
|
|
&& calib_pt_vin_range[calib_pt] == ch_vin_range) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (calib_pt == allocated_calib_points) {
|
|
/* No compatible calibration point found.
|
|
* If an unallocated calibration point remains, use it.
|
|
* Otherwise, this channel cannot be scheduled; since we must
|
|
* perform samplings in order, exit the scheduling loop.
|
|
*/
|
|
if (allocated_calib_points < NUM_CALIBRATION_POINTS) {
|
|
allocated_calib_points++;
|
|
} else {
|
|
/* Exit scheduling loop */
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (channel == LL_ADC_CHANNEL_TEMPSENSOR) {
|
|
if (calib_pt_ch_type[calib_pt] == ADC_CHANNEL_TYPE_INVALID) {
|
|
/**
|
|
* Temperature sensor is a special channel: it must be sampled
|
|
* with special gain/offset instead of the calibration values found
|
|
* in engineering flash. For this reason, it must NOT be scheduled
|
|
* with any other 1.2V Vinput range, single-ended positive channel.
|
|
*
|
|
* If this check succeeds, then no such channel is scheduled, and we
|
|
* can add the temperature sensor to this sequence. We're sure there
|
|
* won't be any conflict because the temperature sensor is the last
|
|
* channel. Otherwise, a channel with 1.2V Vinput range has been
|
|
* scheduled and we must delay the temperature sensor measurement to
|
|
* another sequence.
|
|
*/
|
|
temp_sensor_scheduled = true;
|
|
} else {
|
|
/* Exit scheduling loop before scheduling temperature sensor */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Ensure calibration point tables are updated.
|
|
* This is unneeded if the entry was already filled up,
|
|
* but cheaper than checking for duplicate work.
|
|
*/
|
|
calib_pt_ch_type[calib_pt] = ch_type;
|
|
calib_pt_vin_range[calib_pt] = ch_vin_range;
|
|
|
|
/* Remove channel from unscheduled list */
|
|
remaining_unsampled &= ~ch_bit;
|
|
|
|
/* Add channel to sequence */
|
|
ll_adc_set_conversion_channel(adc, sequence_length, channel);
|
|
sequence_length++;
|
|
|
|
/* Select the calibration point to use for channel */
|
|
ll_adc_set_calib_point_for_any(adc, ch_type, ch_vin_range, calib_pt);
|
|
|
|
/* Configure the channel Vinput range selection.
|
|
* This must not be done for internal channels, which
|
|
* use a hardwired Vinput range selection instead.
|
|
*/
|
|
if (channel < LL_ADC_EXTERNAL_CHANNEL_MAX) {
|
|
LL_ADC_SetChannelVoltageRange(adc, channel, ch_vin_range);
|
|
}
|
|
#if !defined(CONFIG_ADC_STM32_DMA)
|
|
/* If DMA is not enabled, only schedule one channel at a time.
|
|
* Otherwise, the ADC will overflow and everything will break.
|
|
*/
|
|
__ASSERT_NO_MSG(sequence_length == 1);
|
|
break;
|
|
#endif
|
|
}
|
|
|
|
/* Configure all (used) calibration points */
|
|
for (int i = 0; i < NUM_CALIBRATION_POINTS; i++) {
|
|
uint8_t type = calib_pt_ch_type[i];
|
|
uint8_t range = calib_pt_vin_range[i];
|
|
|
|
if (type == ADC_CHANNEL_TYPE_INVALID) {
|
|
break;
|
|
} else if ((type == ADC_CHANNEL_TYPE_SINGLE_POS)
|
|
&& (range == LL_ADC_VIN_RANGE_1V2)
|
|
&& temp_sensor_scheduled) {
|
|
/* Configure special calibration point for temperature sensor */
|
|
configure_tempsensor_calib_point(adc, i);
|
|
} else {
|
|
configure_calib_point_from_flash(adc, i, type, range);
|
|
}
|
|
}
|
|
|
|
__ASSERT_NO_MSG(sequence_length > 0);
|
|
|
|
/* Now that scheduling is done, we can set the sequence length */
|
|
LL_ADC_SetSequenceLength(adc, sequence_length);
|
|
|
|
/* Save unsampled channels (if any) for next sequence */
|
|
data->unsampled_channels = remaining_unsampled;
|
|
|
|
#if defined(CONFIG_ADC_STM32_DMA)
|
|
const struct adc_stm32wb0_config *config = data->dev->config;
|
|
int err;
|
|
|
|
/* Save sequence length in driver data for later usage */
|
|
data->sequence_length = sequence_length;
|
|
|
|
/* Prepare the DMA controller for ADC->memory transfers */
|
|
data->dma_block_config.source_address = (uint32_t)&adc->DS_DATAOUT;
|
|
data->dma_block_config.dest_address = (uint32_t)data->next_sample_ptr;
|
|
data->dma_block_config.block_size = data->sequence_length * sizeof(uint16_t);
|
|
|
|
err = dma_config(config->dmac, config->dma_channel, &data->dmac_config);
|
|
if (err < 0) {
|
|
LOG_ERR("%s: FAIL - dma_config returns %d", __func__, err);
|
|
adc_context_complete(&data->ctx, err);
|
|
return;
|
|
}
|
|
|
|
err = dma_start(config->dmac, config->dma_channel);
|
|
if (err < 0) {
|
|
LOG_ERR("%s: FAIL - dma_start returns %d", __func__, err);
|
|
adc_context_complete(&data->ctx, err);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
/* Start conversion sequence */
|
|
LL_ADC_StartConversion(adc);
|
|
}
|
|
|
|
static inline void handle_end_of_sequence(ADC_TypeDef *adc, struct adc_stm32wb0_data *data)
|
|
{
|
|
if (data->unsampled_channels != 0) {
|
|
/* Some channels requested for this round have
|
|
* not been sampled yet. Schedule and start another
|
|
* acquisition sequence.
|
|
*/
|
|
schedule_and_start_adc_sequence(adc, data);
|
|
} else {
|
|
/* All channels sampled: round is complete. */
|
|
adc_context_on_sampling_done(&data->ctx, data->dev);
|
|
}
|
|
}
|
|
|
|
static int initiate_read_operation(const struct device *dev,
|
|
const struct adc_sequence *sequence)
|
|
{
|
|
const struct adc_stm32wb0_config *config = dev->config;
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
ADC_TypeDef *adc = (ADC_TypeDef *)config->reg;
|
|
int err;
|
|
|
|
err = validate_adc_sequence(sequence);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
/* Take ADC out of idle mode before getting to work */
|
|
err = adc_exit_idle_mode(adc, &config->ana_clk);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
/* Initialize output pointers to first byte of user buffer */
|
|
data->next_sample_ptr = data->round_buf_pointer = sequence->buffer;
|
|
|
|
/* Configure resolution */
|
|
LL_ADC_SetDSDataOutputWidth(adc, ds_width_from_adc_res(sequence->resolution));
|
|
|
|
/* Configure oversampling */
|
|
LL_ADC_SetDSDataOutputRatio(adc, sequence->oversampling);
|
|
|
|
/* Start reading using the ADC */
|
|
adc_context_start_read(&data->ctx, sequence);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if !defined(CONFIG_ADC_STM32_DMA)
|
|
void adc_stm32wb0_isr(const struct device *dev)
|
|
{
|
|
const struct adc_stm32wb0_config *config = dev->config;
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
ADC_TypeDef *adc = config->reg;
|
|
|
|
/* Down sampler output data available */
|
|
if (LL_ADC_IsActiveFlag_EODS(adc)) {
|
|
/* Clear pending interrupt flag */
|
|
LL_ADC_ClearFlag_EODS(adc);
|
|
|
|
/* Write ADC data to output buffer and update pointer */
|
|
*data->next_sample_ptr++ = LL_ADC_DSGetOutputData(adc);
|
|
}
|
|
|
|
/* Down sampler overflow detected - return error */
|
|
if (LL_ADC_IsActiveFlag_OVRDS(adc)) {
|
|
LL_ADC_ClearFlag_OVRDS(adc);
|
|
|
|
LOG_ERR("ADC overflow\n");
|
|
|
|
adc_context_complete(&data->ctx, -EIO);
|
|
return;
|
|
}
|
|
|
|
if (!LL_ADC_IsActiveFlag_EOS(adc)) {
|
|
/* ADC sequence not finished yet */
|
|
return;
|
|
}
|
|
|
|
/* Clear pending interrupt flag */
|
|
LL_ADC_ClearFlag_EOS(adc);
|
|
|
|
/* Execute end-of-sequence logic */
|
|
handle_end_of_sequence(adc, data);
|
|
}
|
|
#else /* CONFIG_ADC_STM32_DMA */
|
|
static void adc_stm32wb0_dma_callback(const struct device *dma, void *user_data,
|
|
uint32_t dma_channel, int dma_status)
|
|
{
|
|
struct adc_stm32wb0_data *data = user_data;
|
|
const struct device *dev = data->dev;
|
|
const struct adc_stm32wb0_config *config = dev->config;
|
|
ADC_TypeDef *adc = config->reg;
|
|
int err;
|
|
|
|
/* N.B.: some of this code is borrowed from existing ADC driver,
|
|
* but may be not applicable to STM32WB0 series' ADC.
|
|
*/
|
|
if (dma_channel == config->dma_channel) {
|
|
if (LL_ADC_IsActiveFlag_OVRDS(adc) || (dma_status >= 0)) {
|
|
/* Sequence finished - update driver data accordingly */
|
|
data->next_sample_ptr += data->sequence_length;
|
|
|
|
/* Stop the DMA controller */
|
|
err = dma_stop(config->dmac, config->dma_channel);
|
|
LOG_DBG("%s: dma_stop returns %d", __func__, err);
|
|
|
|
LL_ADC_ClearFlag_OVRDS(adc);
|
|
|
|
/* Execute the common end-of-sequence logic */
|
|
handle_end_of_sequence(adc, data);
|
|
} else { /* dma_status < 0 */
|
|
LOG_ERR("%s: dma error %d", __func__, dma_status);
|
|
LL_ADC_StopConversion(adc);
|
|
|
|
err = dma_stop(config->dmac, config->dma_channel);
|
|
|
|
LOG_DBG("dma_stop returns %d", err);
|
|
|
|
adc_context_complete(&data->ctx, dma_status);
|
|
}
|
|
} else {
|
|
LOG_DBG("dma_channel 0x%08X != config->dma_channel 0x%08X",
|
|
dma_channel, config->dma_channel);
|
|
}
|
|
}
|
|
#endif /* !CONFIG_ADC_STM32_DMA */
|
|
|
|
/**
|
|
* adc_context API implementation
|
|
*/
|
|
static void adc_context_start_sampling(struct adc_context *ctx)
|
|
{
|
|
struct adc_stm32wb0_data *data = drv_data_from_adc_ctx(ctx);
|
|
const struct adc_stm32wb0_config *config = data->dev->config;
|
|
|
|
/* Mark all channels of this round as unsampled */
|
|
data->unsampled_channels = data->ctx.sequence.channels;
|
|
|
|
/* Schedule and start first sequence of this round */
|
|
schedule_and_start_adc_sequence(config->reg, data);
|
|
}
|
|
|
|
static void adc_context_update_buffer_pointer(
|
|
struct adc_context *ctx, bool repeat_sampling)
|
|
{
|
|
struct adc_stm32wb0_data *data = drv_data_from_adc_ctx(ctx);
|
|
|
|
if (repeat_sampling) {
|
|
/* Roll back output pointer to address of first sample in round */
|
|
data->next_sample_ptr = data->round_buf_pointer;
|
|
} else /* a new round is starting: */ {
|
|
/* Save address of first sample in round in case we have to repeat it */
|
|
data->round_buf_pointer = data->next_sample_ptr;
|
|
}
|
|
}
|
|
|
|
static void adc_context_on_complete(struct adc_context *ctx, int status)
|
|
{
|
|
struct adc_stm32wb0_data *data = drv_data_from_adc_ctx(ctx);
|
|
const struct adc_stm32wb0_config *config = data->dev->config;
|
|
|
|
ARG_UNUSED(status);
|
|
|
|
/**
|
|
* All ADC operations are complete.
|
|
* Save power by placing ADC in idle mode.
|
|
*/
|
|
adc_enter_idle_mode(config->reg, &config->ana_clk);
|
|
|
|
/* Prevent data corruption if something goes wrong. */
|
|
data->next_sample_ptr = NULL;
|
|
}
|
|
|
|
/**
|
|
* Driver subsystem API implementation
|
|
*/
|
|
int adc_stm32wb0_channel_setup(const struct device *dev,
|
|
const struct adc_channel_cfg *channel_cfg)
|
|
{
|
|
CHECKIF(dev == NULL) { return -ENODEV; }
|
|
CHECKIF(channel_cfg == NULL) { return -EINVAL; }
|
|
const bool is_diff_channel =
|
|
(channel_cfg->channel_id == LL_ADC_CHANNEL_VINP0_VINM0
|
|
|| channel_cfg->channel_id == LL_ADC_CHANNEL_VINP1_VINM1
|
|
|| channel_cfg->channel_id == LL_ADC_CHANNEL_VINP2_VINM2
|
|
|| channel_cfg->channel_id == LL_ADC_CHANNEL_VINP3_VINM3);
|
|
const uint8_t vin_range = vinput_range_from_adc_ref(channel_cfg->reference);
|
|
const uint32_t channel_id = channel_cfg->channel_id;
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
int res;
|
|
|
|
/* Forbid reconfiguration while operation in progress */
|
|
res = k_sem_take(&data->ctx.lock, K_NO_WAIT);
|
|
if (res < 0) {
|
|
return res;
|
|
}
|
|
|
|
/* Validate channel configuration parameters */
|
|
if (channel_cfg->gain != ADC_GAIN_1) {
|
|
LOG_ERR("gain unsupported by hardware");
|
|
res = -ENOTSUP;
|
|
goto unlock_and_return;
|
|
}
|
|
|
|
if (vin_range == LL_ADC_VIN_RANGE_INVALID) {
|
|
LOG_ERR("invalid channel voltage reference");
|
|
res = -EINVAL;
|
|
goto unlock_and_return;
|
|
}
|
|
|
|
if (channel_id >= LL_ADC_CHANNEL_MAX) {
|
|
LOG_ERR("invalid channel id %d", channel_cfg->channel_id);
|
|
res = -EINVAL;
|
|
goto unlock_and_return;
|
|
} else if (is_diff_channel != channel_cfg->differential) {
|
|
/* channel_cfg->differential flag does not match
|
|
* with the selected channel's type
|
|
*/
|
|
LOG_ERR("differential flag does not match channel type");
|
|
res = -EINVAL;
|
|
goto unlock_and_return;
|
|
}
|
|
|
|
if (channel_cfg->acquisition_time != ADC_ACQ_TIME_DEFAULT) {
|
|
LOG_ERR("acquisition time unsupported by hardware");
|
|
res = -ENOTSUP;
|
|
goto unlock_and_return;
|
|
}
|
|
|
|
/* Verify that the correct reference is selected for special channels */
|
|
if (channel_id == LL_ADC_CHANNEL_VBAT && vin_range != LL_ADC_VIN_RANGE_3V6) {
|
|
LOG_ERR("invalid reference for Vbat channel");
|
|
res = -EINVAL;
|
|
goto unlock_and_return;
|
|
} else if (channel_id == LL_ADC_CHANNEL_TEMPSENSOR && vin_range != LL_ADC_VIN_RANGE_1V2) {
|
|
LOG_ERR("invalid reference for temperature sensor channel");
|
|
res = -EINVAL;
|
|
goto unlock_and_return;
|
|
}
|
|
|
|
/* Save the channel configuration in driver data.
|
|
* Note that the only configuration option available
|
|
* is the ADC channel reference (= Vinput range).
|
|
*/
|
|
data->channel_config[channel_id].vinput_range = vin_range;
|
|
|
|
unlock_and_return:
|
|
/* Unlock the instance after updating configuration */
|
|
k_sem_give(&data->ctx.lock);
|
|
|
|
return res;
|
|
}
|
|
|
|
int adc_stm32wb0_read(const struct device *dev,
|
|
const struct adc_sequence *sequence)
|
|
{
|
|
CHECKIF(dev == NULL) { return -ENODEV; }
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
int err;
|
|
|
|
adc_context_lock(&data->ctx, false, NULL);
|
|
|
|
/* When context is locked in synchronous mode, this
|
|
* function blocks until the whole operation is complete.
|
|
*/
|
|
err = initiate_read_operation(dev, sequence);
|
|
|
|
if (err >= 0) {
|
|
err = adc_context_wait_for_completion(&data->ctx);
|
|
} else {
|
|
adc_release_pm_locks();
|
|
}
|
|
|
|
adc_context_release(&data->ctx, err);
|
|
|
|
return err;
|
|
}
|
|
|
|
#if defined(CONFIG_ADC_ASYNC)
|
|
int adc_stm32wb0_read_async(const struct device *dev,
|
|
const struct adc_sequence *sequence, struct k_poll_signal *async)
|
|
{
|
|
CHECKIF(dev == NULL) { return -ENODEV; }
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
int err;
|
|
|
|
adc_context_lock(&data->ctx, true, async);
|
|
|
|
/* When context is locked in synchronous mode, this
|
|
* function blocks until the whole operation is complete.
|
|
*/
|
|
err = initiate_read_operation(dev, sequence);
|
|
if (err < 0) {
|
|
adc_release_pm_locks();
|
|
}
|
|
|
|
adc_context_release(&data->ctx, err);
|
|
|
|
return err;
|
|
}
|
|
#endif /* CONFIG_ADC_ASYNC */
|
|
|
|
static DEVICE_API(adc, api_stm32wb0_driver_api) = {
|
|
.channel_setup = adc_stm32wb0_channel_setup,
|
|
.read = adc_stm32wb0_read,
|
|
#if defined(CONFIG_ADC_ASYNC)
|
|
.read_async = adc_stm32wb0_read_async,
|
|
#endif /* CONFIG_ADC_ASYNC */
|
|
.ref_internal = 3600U /* ADC_REF_INTERNAL is mapped to Vinput 3.6V range */
|
|
};
|
|
|
|
int adc_stm32wb0_init(const struct device *dev)
|
|
{
|
|
const struct device *clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
|
|
const struct adc_stm32wb0_config *config = dev->config;
|
|
struct adc_stm32wb0_data *data = dev->data;
|
|
ADC_TypeDef *adc = config->reg;
|
|
int err;
|
|
|
|
if (!device_is_ready(clk)) {
|
|
LOG_ERR("clock control device not ready");
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* Turn on ADC digital clock (always on) */
|
|
err = clock_control_on(clk,
|
|
(clock_control_subsys_t)&config->dig_clk);
|
|
if (err < 0) {
|
|
LOG_ERR("failed to turn on ADC digital clock (%d)", err);
|
|
return err;
|
|
}
|
|
|
|
/* Configure DT-provided signals when available */
|
|
err = pinctrl_apply_state(config->pinctrl_cfg, PINCTRL_STATE_DEFAULT);
|
|
if (err < 0 && err != -ENOENT) {
|
|
/* ENOENT indicates no entry - should not be treated as failure */
|
|
LOG_ERR("fail to apply ADC pinctrl state (%d)", err);
|
|
return err;
|
|
}
|
|
|
|
#if defined(ADC_SUPPORT_AUDIO_FEATURES)
|
|
/* Configure ADC for analog sampling */
|
|
LL_ADC_SetADCMode(adc, LL_ADC_OP_MODE_ADC);
|
|
#endif
|
|
|
|
#if defined(PWR_CR2_ENTS)
|
|
/* Enable on-die temperature sensor */
|
|
LL_PWR_EnableTempSens();
|
|
#endif
|
|
|
|
/* Set ADC sample rate to 1 Msps (maximum speed) */
|
|
LL_ADC_SetSampleRate(adc, LL_ADC_SAMPLE_RATE_16);
|
|
|
|
/* Keep new data on overrun, if it ever happens */
|
|
LL_ADC_SetOverrunDS(adc, LL_ADC_NEW_DATA_IS_KEPT);
|
|
|
|
#if !defined(CONFIG_ADC_STM32_DMA)
|
|
/* Attach ISR and enable ADC interrupt in NVIC */
|
|
IRQ_CONNECT(DT_IRQN(ADC_NODE), DT_IRQ(ADC_NODE, priority),
|
|
adc_stm32wb0_isr, DEVICE_DT_GET(ADC_NODE), 0);
|
|
irq_enable(DT_IRQN(ADC_NODE));
|
|
|
|
/* Enable ADC interrupt after each sampling.
|
|
* NOTE: enabling EOS interrupt is not necessary because
|
|
* the EODS interrupt flag is also set to high when the
|
|
* EOS flag is being set to high.
|
|
*/
|
|
LL_ADC_EnableIT_EODS(adc);
|
|
#else
|
|
/* Check that DMA controller exists and is ready to be used */
|
|
if (!config->dmac) {
|
|
LOG_ERR("no DMA assigned to ADC in DMA driver mode!");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (!device_is_ready(config->dmac)) {
|
|
LOG_ERR("DMA controller '%s' for ADC not ready", config->dmac->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
/* Finalize DMA configuration structure in driver data */
|
|
data->dmac_config.head_block = &data->dma_block_config;
|
|
data->dmac_config.user_data = data;
|
|
|
|
/* Enable DMA datapath in ADC */
|
|
LL_ADC_DMAModeDSEnable(adc);
|
|
#endif /* !CONFIG_ADC_STM32_DMA */
|
|
|
|
/* Unlock the ADC context to mark ADC as ready to use */
|
|
adc_context_unlock_unconditionally(&data->ctx);
|
|
|
|
/* Keep ADC powered down ("idle mode").
|
|
* It will be awakened on-demand when a call to the ADC API
|
|
* is performed by the application.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Driver power management implementation
|
|
*/
|
|
#ifdef CONFIG_PM_DEVICE
|
|
static int adc_stm32wb0_pm_action(const struct device *dev,
|
|
enum pm_device_action action)
|
|
{
|
|
const struct adc_stm32wb0_config *config = dev->config;
|
|
ADC_TypeDef *adc = config->reg;
|
|
int res;
|
|
|
|
switch (action) {
|
|
case PM_DEVICE_ACTION_RESUME:
|
|
return adc_stm32wb0_init(dev);
|
|
case PM_DEVICE_ACTION_SUSPEND:
|
|
adc_enter_idle_mode(adc, &config->ana_clk);
|
|
|
|
/* Move pins to sleep state */
|
|
res = pinctrl_apply_state(config->pinctrl_cfg, PINCTRL_STATE_SLEEP);
|
|
|
|
/**
|
|
* -ENOENT is returned if there are no pins defined in DTS for sleep mode.
|
|
* This is fine and should not block PM from suspending the device.
|
|
* Silently ignore the error by returning 0 instead.
|
|
*/
|
|
if (res >= 0 || res == -ENOENT) {
|
|
return 0;
|
|
} else {
|
|
return res;
|
|
}
|
|
default:
|
|
return -ENOTSUP;
|
|
}
|
|
}
|
|
#endif /* CONFIG_PM_DEVICE */
|
|
|
|
/**
|
|
* Driver device instantiation
|
|
*/
|
|
PINCTRL_DT_DEFINE(ADC_NODE);
|
|
|
|
static const struct adc_stm32wb0_config adc_config = {
|
|
.reg = (ADC_TypeDef *)DT_REG_ADDR(ADC_NODE),
|
|
.pinctrl_cfg = PINCTRL_DT_DEV_CONFIG_GET(ADC_NODE),
|
|
.dig_clk = STM32_CLOCK_INFO(0, ADC_NODE),
|
|
.ana_clk = STM32_CLOCK_INFO(1, ADC_NODE),
|
|
#if defined(CONFIG_ADC_STM32_DMA)
|
|
.dmac = DEVICE_DT_GET(DT_DMAS_CTLR_BY_IDX(ADC_NODE, 0)),
|
|
.dma_channel = DT_DMAS_CELL_BY_IDX(ADC_NODE, 0, channel),
|
|
#endif
|
|
};
|
|
|
|
static struct adc_stm32wb0_data adc_data = {
|
|
ADC_CONTEXT_INIT_TIMER(adc_data, ctx),
|
|
ADC_CONTEXT_INIT_LOCK(adc_data, ctx),
|
|
ADC_CONTEXT_INIT_SYNC(adc_data, ctx),
|
|
.dev = DEVICE_DT_GET(ADC_NODE),
|
|
.channel_config = {
|
|
/* Internal channels selection is hardwired */
|
|
[LL_ADC_CHANNEL_VBAT] = {
|
|
.vinput_range = LL_ADC_VIN_RANGE_3V6
|
|
},
|
|
[LL_ADC_CHANNEL_TEMPSENSOR] = {
|
|
.vinput_range = LL_ADC_VIN_RANGE_1V2
|
|
}
|
|
},
|
|
#if defined(CONFIG_ADC_STM32_DMA)
|
|
.dmac_config = {
|
|
.dma_slot = DT_INST_DMAS_CELL_BY_IDX(ADC_INSTANCE, 0, slot),
|
|
.channel_direction = STM32_DMA_CONFIG_DIRECTION(
|
|
STM32_DMA_CHANNEL_CONFIG_BY_IDX(ADC_INSTANCE, 0)),
|
|
.channel_priority = STM32_DMA_CONFIG_PRIORITY(
|
|
STM32_DMA_CHANNEL_CONFIG_BY_IDX(ADC_INSTANCE, 0)),
|
|
.source_data_size = STM32_DMA_CONFIG_PERIPHERAL_DATA_SIZE(
|
|
STM32_DMA_CHANNEL_CONFIG_BY_IDX(ADC_INSTANCE, 0)),
|
|
.dest_data_size = STM32_DMA_CONFIG_MEMORY_DATA_SIZE(
|
|
STM32_DMA_CHANNEL_CONFIG_BY_IDX(ADC_INSTANCE, 0)),
|
|
.source_burst_length = 1, /* SINGLE transfer */
|
|
.dest_burst_length = 1, /* SINGLE transfer */
|
|
.block_count = 1,
|
|
.dma_callback = adc_stm32wb0_dma_callback,
|
|
/* head_block and user_data are initialized at runtime */
|
|
},
|
|
.dma_block_config = {
|
|
.source_addr_adj = DMA_ADDR_ADJ_NO_CHANGE,
|
|
.source_reload_en = 0,
|
|
.dest_addr_adj = DMA_ADDR_ADJ_INCREMENT,
|
|
.dest_reload_en = 0,
|
|
}
|
|
#endif
|
|
};
|
|
|
|
PM_DEVICE_DT_DEFINE(ADC_NODE, adc_stm32wb0_pm_action);
|
|
|
|
DEVICE_DT_DEFINE(ADC_NODE, &adc_stm32wb0_init, PM_DEVICE_DT_GET(ADC_NODE),
|
|
&adc_data, &adc_config, POST_KERNEL, CONFIG_ADC_INIT_PRIORITY,
|
|
&api_stm32wb0_driver_api);
|