diff --git a/cpu/atmega_common/Makefile.features b/cpu/atmega_common/Makefile.features index 1987a53914cd..b2019902d69a 100644 --- a/cpu/atmega_common/Makefile.features +++ b/cpu/atmega_common/Makefile.features @@ -7,6 +7,7 @@ FEATURES_PROVIDED += cpu_core_atmega FEATURES_PROVIDED += atmega_pcint0 FEATURES_PROVIDED += dbgpin FEATURES_PROVIDED += periph_cpuid +FEATURES_PROVIDED += periph_adc_ng FEATURES_PROVIDED += periph_eeprom FEATURES_PROVIDED += periph_gpio periph_gpio_irq FEATURES_PROVIDED += periph_pm diff --git a/cpu/atmega_common/periph/adc_ng.c b/cpu/atmega_common/periph/adc_ng.c new file mode 100644 index 000000000000..797b534f7616 --- /dev/null +++ b/cpu/atmega_common/periph/adc_ng.c @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2016 Laurent Navet + * 2017 HAW Hamburg, Dimitri Nahm + * 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @ingroup drivers_periph + * @{ + * + * @file + * @brief Low-level ADC driver implementation for ATmega family + * + * @author Laurent Navet + * @author Dimitri Nahm + * @author Sebastian Meiling + * @author Marian Buschsieweke + * @} + */ + +#include +#include +#include +#include + +#include "adc_ng.h" +#include "bitarithm.h" +#include "cpu.h" +#include "irq.h" +#include "kernel_defines.h" +#include "periph_conf.h" +#include "xfa.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/* ADC clock device divides core clock down to: + * + * CLOCK_CORECLOCK / (1 << ADC_CLOCK_DIV) + * + * The ADC always generates 10 bits output, but with ADC clocks higher than + * 200 kHz the noise level rises reducing the number of meaningful bits. + * With a clock between 50 kHz and 200 kHz and low noise mode, the highest + * effective resolution is achieved. + * + * In this article the effective resolution of the ADC depending on the ADC + * clock is measured: http://www.openmusiclabs.com/learning/digital/atmega-adc/ + * + * This driver implements a 10 bit resolution with an ADC clock of (just below) + * 200 kHz and a 9 bit resolution with an ADC clock of (just below) 1MHz. + * Note: The article also tried ADC clocks as high as 4 MHz, but according to + * the data sheet the ADC clock must not exceed 1 MHz. So let's be better safe + * than sorry. + */ +#if CLOCK_CORECLOCK > 12800000U +#define ADC_10BIT_CLOCK_DIV (7U) +#elif CLOCK_CORELOCK > 6400000U +#define ADC_10BIT_CLOCK_DIV (6U) +#elif CLOCK_CORECLOCK > 3200000U +#define ADC_10BIT_CLOCK_DIV (5U) +#elif CLOCK_CORECLOCK > 1600000U +#define ADC_10BIT_CLOCK_DIV (4U) +#elif CLOCK_CORECLOCK > 800000U +#define ADC_10BIT_CLOCK_DIV (3U) +#elif CLOCK_CORECLOCK > 400000U +#define ADC_10BIT_CLOCK_DIV (2U) +#else +#define ADC_10BIT_CLOCK_DIV (1U) +#endif + +#if CLOCK_CORECLOCK > 16000000U +#define ADC_9BIT_CLOCK_DIV (5U) +#elif CLOCK_CORELOCK > 8000000U +#define ADC_9BIT_CLOCK_DIV (4U) +#elif CLOCK_CORECLOCK > 4000000U +#define ADC_9BIT_CLOCK_DIV (3U) +#elif CLOCK_CORECLOCK > 2000000U +#define ADC_9BIT_CLOCK_DIV (2U) +#else +#define ADC_9BIT_CLOCK_DIV (1U) +#endif + +#if defined(CPU_ATMEGA2560) +/* The ATmega2560 has 16 single-ended ADC channels */ +#define CHAN_NUMOF (16U) +#else +/* All other supported ATmegas have 8 single-ended ADC channels, except for + * the ATmega32U4. But the ADC channels of the ATmega32U4 are strangely + * mapped to pins, so we don't support channels 8-13 to keep the code readable + */ +#define CHAN_NUMOF (8U) +#endif + +#if defined(CPU_ATMEGA328P) +/* ATmega328P has 4 ADC multiplexer bits, all fit in the ADMUX register */ +#define MUX_WIDTH (4U) +#define ADMUX_MUX_MASK (0x0f) +#elif defined(CPU_ATMEGA1284P) +/* ATmega1284P has 5 ADC multiplexer bits, all fit in the ADMUX register */ +#define MUX_WIDTH (5U) +#define ADMUX_MUX_MASK (0x1f) +#else +/* ATmega32U4, 128RFA1, 256RFR2, 1281, and 2560 have 6 ADC multiplexer bits. + * The most significant bit (MUX5) is stored in ADCSRB rather than in ADMUX. + */ +#define MUX_WIDTH (6U) +#define ADMUX_MUX_MASK (0x1f) +#endif + +/* On ATmega128RFA1 and ATmega256RFR2 AVCC the internal 1.8V regulator is used + * as reference + */ +#if defined(CPU_ATMEGA128RFA1) || defined(CPU_ATMEGA256RFR2) +#define AVCC (1800U) +#elif !defined(AVCC) +/* AVCC can be specified in periph_conf.h - otherwise it will just be + * detected on boot. */ +#define AVCC (0U) +#endif /* AVCC */ + +#if defined(CPU_ATMEGA32U4) +#define NTC_CHANNEL (39U) +#elif defined(CPU_ATMEGA328P) +#define NTC_CHANNEL (8U) +#endif + +/* This value for the ADMUX selects the 1.1V internal reference as input for + * every support ATmega, after dropping additional bits. (E.g. the ATmega328P + * only supports 4 bits ADMUX and the value 0x0e (== 0x1e & 0x0f) selects the + * 1.1V reference.) + */ +#define REF_1V1_CHANNEL (0x1e) + +#define FLAG_RSHIFT (0x01) +#define FLAG_NOISE_CANCEL (0x02) +#define FLAG_ENTROPY (0x04) + +static int _init(void *handle, uint8_t chan, uint8_t res, uint8_t ref); +static void _off(void *handle); +static int _single(void *handle, int32_t *dest); + +static uint8_t flags = 0; + +/* Supported voltage references... */ +static int16_t refs[] = { +#if defined(CPU_ATMEGA32U4) + 2560, + AVCC, +#elif defined(CPU_ATMEGA128RFA1) || defined(CPU_ATMEGA256RFR2) + 1500, + 1600, + AVCC, +#elif defined(CPU_ATMEGA328P) + 1100, + AVCC, +#elif defined(CPU_ATMEGA1281) || defined(CPU_ATMEGA1284P) || defined(CPU_ATMEGA2560) + 1100, + 2560, + AVCC, +#else +#error "Missing reference voltage configuration" +#endif + 0 /* <-- terminating zero element */ +}; +/* ...and corresponding configuration bits */ +static const uint8_t ref_confs[] = { +#if defined(CPU_ATMEGA32U4) + /* ATmega32U4: See Table 24-3 in the data sheet */ + (1 << REFS1) | (1 << REFS0), + (1 << REFS0), +#elif defined(CPU_ATMEGA128RFA1) || defined(CPU_ATMEGA256RFR2) + /* ATmega128RFA1: See Table 27-10 in the data sheet */ + /* ATmega256RFR2: See Table 27-11 in the data sheet */ + (1 << REFS1), + (1 << REFS1) | (1 << REFS0), + (1 << REFS0), +#elif defined(CPU_ATMEGA328P) + /* ATmega328P: See Table 24-3 in the data sheet */ + (1 << REFS1) | (1 << REFS0), + (1 << REFS0), +#elif defined(CPU_ATMEGA1281) || defined(CPU_ATMEGA1284P) || defined(CPU_ATMEGA2560) + /* ATmega1281: See Table 26-3 in the data sheet */ + /* ATmega1284P: See Table 23-3 in the data sheet */ + /* ATmega2560: See Table 26-3 in the data sheet */ + (1 << REFS1), + (1 << REFS1) | (1 << REFS0), + (1 << REFS0), +#endif +}; + +#ifdef NTC_CHANNEL +static adc_ng_ntc_t int_ntc = { + .coefficient = 10533, + .offset = 361, +}; +#endif + +adc_ng_driver_t periph_adc_ng_driver = { + .init = _init, + .off = _off, + .single = _single, + .res_supported = BIT8 | BIT9, + .refs = refs, + /* For all ATmegas, the lowest reference voltage (idx 0) can be select + * as input */ + .ref_input_idx = 0, + /* And the last is always VCC */ + .ref_vcc_idx = ARRAY_SIZE(ref_confs) - 1, + .highest_single_ended_channel = CHAN_NUMOF - 1, +#ifdef NTC_CHANNEL + .entropy_bits = 1, + .ntc = &int_ntc +#endif +}; + +XFA_CONST(adc_ng_backends, 0) adc_ng_backend_t periph_adc_ng_backend = { + .driver = &periph_adc_ng_driver, + .handle = NULL, +}; +XFA(adc_ng_states, 0) adc_ng_state_t periph_adc_ng_state; + +ISR(ADC_vect) +{ + /* nothing to do: After ISR finishes, execution continues after the point + * ADC noise cancel mode was started + */ +} + +static void setup_admux(uint8_t chan, uint8_t ref) +{ + DEBUG("ref = %x, ref_conf = %x\n", ref, ref_confs[ref]); + if (chan == ADC_NG_CHAN_FIXED_REF) { + /* Magic channel that selects the smallest fixed voltage reference as + * input. (Useful to deduce AVCC, when AVCC is reference.) + * (For the ATmega328P the correct value would be 0x0e, but the + * leading bits are dropped later on anyway.) + */ + chan = REF_1V1_CHANNEL; + } +#ifdef NTC_CHANNEL + else if (chan == ADC_NG_CHAN_ENTROPY) { + chan = NTC_CHANNEL; + /* Always use the smallest reference voltage, as small noise on the + * input voltage will have bigger impact on the sample there */ + ref = 0; + } + else if (chan == ADC_NG_CHAN_NTC) { + chan = NTC_CHANNEL; + } +#endif /* NTC_CHANNEL */ + else if (chan < 8) { + /* Disable corresponding Digital input */ + DIDR0 |= (1 << chan); + /* Set ADC-pin as input */ +#if defined(CPU_ATMEGA328P) + DDRC &= ~(1 << chan); + PORTC &= ~(1 << chan); +#elif defined(CPU_ATMEGA1284P) + DDRA &= ~(1 << chan); + PORTA &= ~(1 << chan); +#elif defined(CPU_ATMEGA32U4) || defined(CPU_ATMEGA1281) || defined(CPU_ATMEGA2560) + DDRF &= ~(1 << chan); + PORTF &= ~(1 << chan); +#endif /* CPU_ATMEGA328P */ + } +#if defined(CPU_ATMEGA2560) + else if (chan < 16) { + /* Disable corresponding Digital input */ + DIDR2 |= (1 << (chan - 8)); + /* Set ADC-pin as input */ + DDRK &= ~(1 << (chan - 8)); + PORTK &= ~(1 << (chan - 8)); + /* single ended channels ADC8 - ADC15 have are mapped from 0x20 */ + chan &= 0x07; + chan |= 0x20; + } +#endif + + /* Set reference configuration and lower 5 bits of the channel mux */ + ADMUX = ref_confs[ref] | (chan & ADMUX_MUX_MASK); + +#if MUX_WIDTH == 6U + /* Set upper bits of the channel mux, if present */ + if (chan & 0x20) { + ADCSRB |= (1 << MUX5); + } + else { + ADCSRB &= ~(1 << MUX5); + } +#endif +} + +static int _init(void *handle, uint8_t chan, uint8_t res, uint8_t ref) +{ + (void)handle; + assert(ref < ARRAY_SIZE(ref_confs)); + + if ( + (chan >= CHAN_NUMOF) && (chan != ADC_NG_CHAN_FIXED_REF) +#ifdef NTC_CHANNEL + && (chan != ADC_NG_CHAN_ENTROPY) && (chan != ADC_NG_CHAN_NTC) +#endif + ) { + return -ENXIO; + } + +#ifdef CPU_ATMEGA32U4 + /* The ATmega32U4 is missing pins for PF2 and PF3, which would have been + * ADC channel 2 and 3 if present */ + if ((chan == 2) || (chan == 3)) { + return -ENXIO; + } +#endif + + unsigned state = irq_disable(); + if (ADCSRA & (1 << ADEN)) { + irq_restore(state); + return -EALREADY; + } + + /* enable ADC and set ADC clock corresponding to the effective resolution + * requested */ +#ifdef NTC_CHANNEL + if (chan == ADC_NG_CHAN_ENTROPY) { + /* Ignore requested resolution according to doc and freely choose + * setting providing highest entropy per conversion: Go with fastest + * ADC clock (most noisy), do not use noise cancel mode, do not + * discard noisy last bit. + */ + ADCSRA = (1 << ADEN) | ADC_10BIT_CLOCK_DIV; + flags = FLAG_ENTROPY | FLAG_NOISE_CANCEL; + } + else +#endif /* NTC_CHANNEL */ + if (res == 10) { + ADCSRA = (1 << ADEN) | ADC_10BIT_CLOCK_DIV; + /* Reduce noise by turning CPU during ADC conversion off */ + flags = FLAG_NOISE_CANCEL; + } + else { + ADCSRA = (1 << ADEN) | ADC_9BIT_CLOCK_DIV; + /* Result has to be shifted to the right by one to discard the noisy + * last bit. Perform ADC conversion with CPU active for better + * conversion speed + */ + flags = FLAG_RSHIFT | FLAG_NOISE_CANCEL; + } + + irq_restore(state); + + setup_admux(chan, ref); + + if (ref < ARRAY_SIZE(ref_confs) - 1) { + /* All but the last voltage reference are constant references. + * According to the data sheet, the first measurement with a constant + * reference is inaccurate as the reference needs some time to + * stabilize. As workaround we'll just take one sample to through + * away. + */ + int32_t trash; + /* through away first measurement to let reference voltage settle */ + _single(NULL, &trash); + } + else if (chan == ADC_NG_CHAN_FIXED_REF) { + /* There seems to be no warning in the data sheet, but using the + * constant reference as input has shown to be widely unreliable + * until it finally stabilizes after a couple of measurements. No idea + * why it takes much longer to stabilize when used as input rather than + * as reference... + */ + int32_t trash; + for (uint8_t i = 0; i < 8; i++) { + _single(NULL, &trash); + } + } + + return 0; +} + +static void _off(void *handle) +{ + (void)handle; + ADCSRA &= ~(1 << ADEN); +} + +static int _single(void *handle, int32_t *dest) +{ + (void)handle; + assert(dest != NULL); + assert(ADCSRA & (1 << ADEN)); + + DEBUG("ADMUX = 0x%x, ADCSRA = 0x%x, ADCSRB = 0x%x\n", ADMUX, ADCSRA, ADCSRB); + + if (flags & FLAG_NOISE_CANCEL) { + /* Wait for TX to complete */ + while (avr8_is_uart_tx_pending()) {} + /* Turn on ADC interrupt to wake CPU up after conversion is done */ + ADCSRA |= (1 << ADIF) | (1 << ADIE); + set_sleep_mode(SLEEP_MODE_ADC); + sleep_mode(); + } + else { + /* Perform single conversion with CPU active */ + ADCSRA |= (1 << ADSC); + + /* Wait until the conversion is complete */ + while (ADCSRA & (1 << ADSC)) {} + } + + /* Get conversion result */ + *dest = (ADC) >> (flags & FLAG_RSHIFT); + + /* Clear the ADIF flag */ + ADCSRA |= (1 << ADIF); + + return 0; +} + +#if !AVCC +void periph_adc_ng_init(void) +{ + /* Detect AVCC. Rely on peripheral ADC getting priority over external ones, + * so that we are ADC 0 */ + const uint8_t idx = ARRAY_SIZE(ref_confs) - 1; + adc_ng_measure_ref(0, idx, &refs[idx]); +} +#endif diff --git a/cpu/avr8_common/avr8_cpu.c b/cpu/avr8_common/avr8_cpu.c index 2323236e9736..b865ff6a5d7e 100644 --- a/cpu/avr8_common/avr8_cpu.c +++ b/cpu/avr8_common/avr8_cpu.c @@ -105,12 +105,6 @@ void cpu_init(void) #ifdef MODULE_AVR_LIBC_EXTRA avr8_stdio_init(); #endif - /* Initialize peripherals for which modules are included in the makefile.*/ - /* spi_init */ - /* rtc_init */ - /* hwrng_init */ - periph_init(); - #ifdef CPU_ATXMEGA /* Enable Multilevel Interrupt Controller */ PMIC.CTRL |= PMIC_HILVLEN_bm @@ -119,6 +113,12 @@ void cpu_init(void) #endif irq_enable(); + + /* Initialize peripherals for which modules are included in the makefile.*/ + /* spi_init */ + /* rtc_init */ + /* hwrng_init */ + periph_init(); } struct __freelist { diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index 84dc17307d20..df1b5c484cb2 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -4,6 +4,10 @@ ifneq (,$(filter adc%1c,$(USEMODULE))) USEMODULE += adcxx1c endif +ifneq (,$(filter adc_ng_default,$(USEMODULE))) + USEMODULE += adc_ng +endif + ifneq (,$(filter ads101%,$(USEMODULE))) USEMODULE += ads101x endif diff --git a/drivers/adc_ng/Makefile b/drivers/adc_ng/Makefile new file mode 100644 index 000000000000..43883d23f698 --- /dev/null +++ b/drivers/adc_ng/Makefile @@ -0,0 +1,6 @@ +SRC := adc_ng.c xfa.c +# Some adc_ng_ pseudo module should be result in corresponding .c +# to be included for compilation, but not every adc_ng_ is a submodule +SUBMODULES := 1 +SUBMODULES_NOFORCE := 1 +include $(RIOTBASE)/Makefile.base diff --git a/drivers/adc_ng/Makefile.dep b/drivers/adc_ng/Makefile.dep new file mode 100644 index 000000000000..d285cd096c95 --- /dev/null +++ b/drivers/adc_ng/Makefile.dep @@ -0,0 +1,2 @@ +USEMODULE += auto_init +FEATURES_OPTIONAL += periph_adc_ng diff --git a/drivers/adc_ng/Makefile.include b/drivers/adc_ng/Makefile.include new file mode 100644 index 000000000000..4374f891dea0 --- /dev/null +++ b/drivers/adc_ng/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_adc_ng := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_adc_ng) diff --git a/drivers/adc_ng/adc_ng.c b/drivers/adc_ng/adc_ng.c new file mode 100644 index 000000000000..88355a133405 --- /dev/null +++ b/drivers/adc_ng/adc_ng.c @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_adc_ng + * @{ + * + * @file + * @brief Implementation of common functions of the ADC API + * + * @author Marian Buschsieweke + * @} + */ + +#include +#include + +#include "adc_ng.h" +#include "kernel_defines.h" +#include "xfa.h" + +int adc_ng_init(uint8_t adc, uint8_t chan, uint8_t res, int16_t *ref) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(ref); + assert((res > 0) && (res <= 32)); + const adc_ng_backend_t *be = &adc_ng_backends[adc]; + adc_ng_state_t *state = &adc_ng_states[adc]; + + if (!(be->driver->res_supported & (1 << (res - 1)))) { + return -ENOTSUP; + } + + uint8_t idx = 0; + + if (*ref == ADC_NG_MAX_REF) { + while (be->driver->refs[++idx]) { } + idx--; + } + else { + if ((*ref < 0) && (*ref < be->driver->refs[0])) { + /* No reference small enough to cover the range [*ref; 0] */ + return -ERANGE; + } + /* Positive reference voltage: Select the lowest reference that + * is equal to or greater than the requested reference */ + while (be->driver->refs[idx] < *ref) { + if (!be->driver->refs[++idx]) { + if (*ref < 0) { + idx--; + break; + } + /* No reference big enough to cover the range [0; ref] */ + return -ERANGE; + } + } + } + + state->reference_mv = *ref = be->driver->refs[idx]; + state->resolution = res; + + return be->driver->init(be->handle, chan, res, (uint8_t)idx); +} + +int16_t adc_ng_convert(uint8_t adc, int32_t sample) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + adc_ng_state_t *state = &adc_ng_states[adc]; + /* V_in = (sample * V_ref) / (2^resolution - 1) */ + int64_t vin = sample; + vin *= state->reference_mv; + vin /= (1 << state->resolution) - 1; + return (int16_t)vin; +} + +int adc_ng_measure_ref(uint8_t adc, uint8_t ref_idx, int16_t *dest) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(dest); + const adc_ng_backend_t *be = &adc_ng_backends[adc]; + /* It makes no sense to use the same voltage source as reference and input */ + assert(be->driver->ref_input_idx != ref_idx); + + int retval; + int32_t sample; + uint8_t res_max = adc_ng_max_res(adc); + retval = be->driver->init(be->handle, ADC_NG_CHAN_FIXED_REF, res_max, + ref_idx); + if (retval) { + return retval; + } + retval = be->driver->single(be->handle, &sample); + be->driver->off(be->handle); + if (retval) { + return retval; + } + /* + * The sample s with the resolution r has the value: + * + * s = (V_in * 2^r) / V_ref + * + * In this case we're interested in V_ref and know V_in, so: + * + * V_ref = (V_in * 2^r) / s + */ + int64_t vref = (1ULL << res_max); + vref *= be->driver->refs[be->driver->ref_input_idx]; + vref += sample >> 1; /* <- Scientific rounding */ + vref /= sample; + *dest = (int16_t)vref; + return 0; +} diff --git a/drivers/adc_ng/include/adc_ng_internal.h b/drivers/adc_ng/include/adc_ng_internal.h new file mode 100644 index 000000000000..963040b515b1 --- /dev/null +++ b/drivers/adc_ng/include/adc_ng_internal.h @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @ingroup drivers_adc + * @{ + * + * @file + * @brief Internal types used in the common ADC API + * + * @author Marian Buschsieweke + * + * Writing an ADC NG Backend + * ========================= + * + * Terminology + * ----------- + * + * - **Single Ended Input:** The ADC converts a single input voltage into + * an unsigned value in which zero refers to 0V and the highest value + * indicates that the input is equal to (or higher than) the reference + * voltage. + * - **Differential Input:** The ADC use a positive and a negative input + * and converts the voltage difference between the two into a signed value: + * A value of zero indicates no voltage difference, a positive value that the + * positive inputs is greater than the negative, and a negative value that + * the negative inputs is greater than the positive. If the difference is + * equal to the reference voltage, the highest (or lowest) value is returned. + * + * Channel Mapping + * --------------- + * + * Every ADC driver must at least one single ended input. All other channel + * types are optional. The initialization code in @ref adc_ng_driver_t::init + * must however be prepared to be called for every possible channel number and + * must return `-ENXIO` for unsupported channels. The channel number of the + * highest single ended channel must be given in the driver in + * @ref adc_ng_driver_t::highest_single_ended_channel. All channel numbers + * below this *must* also refer to single ended channels. + * + * Channels above the value given in + * @ref adc_ng_driver_t::highest_single_ended_channel but below 224 are driver + * specific and can be used to provide access to differential inputs, select + * different gains, etc. + * + * Channels starting from 224 are reserved for driver independent special + * channels. Those special channels currently are @ref ADC_NG_CHAN_FIXED_REF + * (to select a fixed reference voltage as input to allow measuring the value + * of other reference voltages), @ref ADC_NG_CHAN_NTC (that refers to an + * internally connected integrated thermistor), and @ref ADC_NG_CHAN_ENTROPY + * (to collect entropy). + */ + +#ifndef ADC_NG_INTERNAL_H +#define ADC_NG_INTERNAL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief This special channel must refer to an internal fixed reference + * voltage used as input + * + * ADCs not supporting this will return `-ERANGE` when this channel is + * selected. If this is supported, it can be used to measure the correct value + * of voltage references depending on supply voltages (including the MCUs + * supply voltage, if selectable as reference voltage). This allows to + * compensate for differences between nominal and actual voltage reference + * during conversion to physical units. + * + * In case of boards running directly of a battery, measuring the supply + * voltage is particularly useful to estimate the remaining battery charge. + */ +#define ADC_NG_CHAN_FIXED_REF (UINT8_MAX) + +/** + * @brief This special channel must refer to an internally connected + * thermistor + */ +#define ADC_NG_CHAN_NTC (UINT8_MAX - 1) + +/** + * @brief This special channel must refer to a channel collecting entropy + * + * @note When this channel is selected, a driver can (and likely should) + * ignore the requested resolution and reference voltage. + * + * When this channel is used, the @ref adc_ng_driver_t::entropy_bits least + * significant of every sample obtained will contain a some (possibly weak) + * entropy. The contents of the remaining bits are undefined. + */ +#define ADC_NG_CHAN_ENTROPY (UINT8_MAX - 2) + +/** + * @brief Use this value in @ref adc_ng_driver_t::ref_input_idx or + * @ref adc_ng_driver_t::ref_vcc_idx to indicate that this feature + * is not supported + */ +#define ADC_NG_NO_SUCH_REF (UINT8_MAX) + +/** + * @brief Description of an thermistor to use for temperature measurements + */ +typedef struct { + /** + * @brief Contains the temperature coefficient of the NTC, or zero + * + * The coefficient is is given in 1/1024 mV per 0.1 °C. The resulting + * temperature in 0.1 °C is calculated from the measured voltage using: + * + * T[d°C] = (coefficient * (mV - offset)) / 1024 + */ + uint16_t coefficient; + /** + * @brief The offset in mV to use for obtaining the temperature + * + * See @ref adc_ng_ntc_t::coefficient + */ + uint16_t offset; +} adc_ng_ntc_t; + +/** + * @brief Structure to capture the state of an ADC-NG backedn + */ +typedef struct { + int16_t reference_mv; /**< Currently selected reference voltage (in mV) */ + uint8_t resolution; /**< Currently selected resolution (in bits) */ + +} adc_ng_state_t; + +/** + * @brief Signature of @ref adc_ng_driver_t::init + * + * @param handle Handle of the ADC + * @param[in] chan The ADC channel to initialize + * @param[in] res The resolution to select + * @param[in ref Index of the reference to use + * (refers to @ref adc_ng_driver_t::refs) + * + * @retval 0 Success + * @retval -ENXIO Invalid channel given in @p channel + * @retval -EALREADY The ADC is already powered and configured + * @retval <0 Other error (see device driver doc) + * + * @post When 0 is returned, the channel @p channel is ready take samples + * @post If @p res contains an unsupported resolution, an assert blows up + * @post If `-EALREADY` is returned, the ADC is state remains unchanged + * @post On other error codes the ADC is powered down + * + * @note A call to @ref adc_ng_driver_t::off is needed to disable the + * channel again and preserve power + */ +typedef int (*adc_ng_init_t)(void *handle, uint8_t chan, uint8_t res, + uint8_t ref); + +/** + * @brief Signature of @ref adc_ng_driver_t::off + * + * @param handle Handle of the ADC + */ +typedef void (*adc_ng_off_t)(void *handle); + +/** + * @brief Signature of @ref adc_ng_driver_t::single + * + * @param handle Handle of the ADC + * @param[out] dest The sample will be stored here + * + * @return The result of the conversion + * + * @pre The ADC has been initialized, see @ref adc_ng_driver_t::init + * + * @post If the ADC is not initialized, an assertion blows up + * + * @retval 0 Success + * @retval <0 Error (check device driver's doc for error codes) + */ +typedef int (*adc_ng_single_t)(void *handle, int32_t *dest); + +/** + * @brief Internal driver interface + */ +typedef struct { + /** + * @brief Initialize the given ADC channel and prepare it for sampling + * + * @note A call to @ref adc_ng_driver_t::off is needed to disable the + * channel again and preserve power + */ + adc_ng_init_t init; + /** + * @brief Disable the given ADC channel again and bring the ADC into a low + * power state + * + * @details If no power saving is implemented (or possible), this should be + * a `NULL` pointer. + */ + adc_ng_off_t off; + /** + * @brief Runs a single conversion and returns the sample + */ + adc_ng_single_t single; + /** + * @brief Bitmap containing the supported ADC resolutions + * + * If e.g. the resolutions 4bit, 6bit and 8bit are supported it should have + * the value `BIT3 | BIT5 | BIT7`. Thus, currently the highest resolution + * supported is 32 bit. + */ + uint32_t res_supported; + /** + * @brief The reference voltages supported in ascending order + * + * This list should be sorted in ascending order and terminated with a + * value of zero. The reference voltage are a bitmask with the 14 least + * significant bits containing the voltage value in mV, and the two + * most significant bits indicate whether the reference voltage is + * calibrated (@ref ADC_NG_REF_CALIBRATED) and if the MCUs supply voltage + * is used as reference (@ref ADC_NG_REF_MCU_VCC). + */ + const int16_t *refs; + /** + * @brief Parameters of the internally connected thermistor or `NULL` + */ + const adc_ng_ntc_t *ntc; + /** + * @brief The index of the reference voltage that can be used as input + * using channel @ref ADC_NG_CHAN_FIXED_REF + * + * In case the fixed reference voltage can only be used as input, and not + * set up as reference during conversion, add this reference to @ref + * adc_ng_driver_t::refs after the last item (identified using value zero) + * and use that index here. This will prevent that reference to be used + * except as input. + * + * Use the special value @ref ADC_NG_NO_SUCH_REF if no fixed reference + * voltage can be used as input. + */ + uint8_t ref_input_idx; + /** + * @brief The index of the VCC reference voltage + * + * If VCC can be chosen as reference, this should give its index in + * @ref adc_ng_driver_t::refs. Otherwise it should have the value + * @ref ADC_NG_NO_SUCH_REF. + */ + uint8_t ref_vcc_idx; + /** + * @brief The number of least significant bits containing entropy + * + * @note This only refers to channel @ref ADC_NG_CHAN_ENTROPY + * + * A value of zero must be used when the ADC does not support harvesting + * entropy. Note that even a source of weak entropy can be useful, when + * fed into a entropy extractor. Often only taking only the least + * significant bit when measuring a noisy source is a good choice + * (so a value of `1`). + */ + uint8_t entropy_bits; + /** + * @brief The number of the highest single ended channel + * + * All channel with a number small than or equal to this value must be + * regular single ended channels. All channel higher than this number but + * lower than 224 are driver defined special channels (if any), e.g. + * differential inputs. All channels equal to 224 and above are reserved + * for driver-independent special channels like @ref ADC_NG_CHAN_FIXED_REF, + * @ref ADC_NG_CHAN_NTC, or @ref ADC_NG_CHAN_ENTROPY. + */ + uint8_t highest_single_ended_channel; +} adc_ng_driver_t; + +/** + * @brief Type of the ADC-NG backend + */ +typedef struct { + const adc_ng_driver_t *driver; /**< Driver interface of the backend */ + void *handle; /**< Handle of the driver (e.g. device descriptor) */ +} adc_ng_backend_t; + +/** + * @brief Convert an ADC sample to a voltage level in mV + * + * @param[in] adc ADC that was used to take the sample + * @param[in] sample The ADC sample to convert + * + * @return The voltage level in mV + * + * @pre The ADC identified by @p adc has not been re-initialized since + * taking the sample. (But it can be offline.) + */ +int16_t adc_ng_convert(uint8_t adc, int32_t sample); + +/** + * @brief Measure the actual value of a reference voltage by selecting + * the constant voltage reference as input + * + * @param[in] adc ADC of which the reference voltage should be + * measured + * @param[in] ref_idx Index of the reference to measure + * @param[out] dest_mv Measured value of the voltage reference in mV + */ +int adc_ng_measure_ref(uint8_t adc, uint8_t ref_idx, int16_t *dest_mv); + +#ifdef __cplusplus +} +#endif + +#endif /* ADC_NG_INTERNAL_H */ +/** @} */ diff --git a/drivers/adc_ng/util.c b/drivers/adc_ng/util.c new file mode 100644 index 000000000000..e9a992aa45fb --- /dev/null +++ b/drivers/adc_ng/util.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_adc_ng + * @{ + * + * @file + * @brief Implementation of common functions of the ADC API + * + * @author Marian Buschsieweke + * @} + */ + +#include + +#include "adc_ng_util.h" + +int adc_ng_vcc(uint8_t adc, int16_t *dest_mv) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(dest_mv); + const adc_ng_backend_t be = adc_ng_backends[adc]; + if (be.driver->ref_vcc_idx == ADC_NG_NO_SUCH_REF) { + return -ENOTSUP; + } + + return adc_ng_measure_ref(adc, be.driver->ref_vcc_idx, dest_mv); +} + +int adc_ng_ntc(uint8_t adc, uint8_t chan, const adc_ng_ntc_t *ntc, int16_t *temp) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(ntc && temp); + + uint8_t res = adc_ng_max_res(adc); + int16_t ref_mv = ntc->offset << 1; + int retval = adc_ng_init(adc, chan, res, &ref_mv); + if (retval) { + return retval; + } + + int16_t vin; + retval = adc_ng_voltage(adc, &vin); + adc_ng_off(adc); + if (retval) { + return retval; + } + + int32_t tmp = vin; + tmp -= ntc->offset; + tmp *= (uint32_t)ntc->coefficient; + tmp >>= 10; + *temp = (int16_t)tmp; + + return 0; +} + +int adc_ng_entropy(uint8_t adc, void *_dest, size_t size) +{ + uint8_t *dest = _dest; + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(dest && size); + const adc_ng_backend_t be = adc_ng_backends[adc]; + + if (!be.driver->entropy_bits) { + return -ENOTSUP; + } + + uint8_t *end = dest + size; + + uint8_t res = adc_ng_max_res(adc); + int16_t ref_mv = 0; + int retval = adc_ng_init(adc, ADC_NG_CHAN_ENTROPY, res, &ref_mv); + if (retval) { + return retval; + } + + uint16_t entropy = 0; + unsigned entropy_bits = 0; + const unsigned bytes_per_iteration = be.driver->entropy_bits / 8; + const unsigned bits_per_iteration = be.driver->entropy_bits % 8; + /* Bitmask to select only the bits_per_iteration least significant bits */ + const uint8_t entropy_mask = (1 << bits_per_iteration) - 1; + while (dest < end) { + union { + int32_t s32; + uint8_t u8[4]; + } tmp; + retval = be.driver->single(be.handle, &tmp.s32); + if (retval) { + be.driver->off(be.handle); + return retval; + } + + for (unsigned i = 0; i < bytes_per_iteration; i++) { + *dest++ = tmp.u8[i]; + if (dest >= end) { + be.driver->off(be.handle); + return 0; + } + } + entropy <<= bits_per_iteration; + entropy |= tmp.u8[0] & entropy_mask; + entropy_bits += bits_per_iteration; + if (entropy_bits >= 8) { + *dest++ = (uint8_t)entropy; + entropy >>= 8; + entropy_bits -= 8; + } + } + + if (be.driver->off) { + be.driver->off(be.handle); + } + return 0; +} diff --git a/drivers/adc_ng/xfa.c b/drivers/adc_ng/xfa.c new file mode 100644 index 000000000000..125f00e334d3 --- /dev/null +++ b/drivers/adc_ng/xfa.c @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_adc_ng + * @{ + * + * @file + * @brief XFA initializations + * + * @author Marian Buschsieweke + * @} + */ + +#include + +#include "adc_ng_internal.h" +#include "xfa.h" + +/* Compilation fails when XFA_USE_CONST() and XFA_INIT_CONST() are part of the + * same compilation unit. This is worked around by splitting out the XFA + * initializations into this file, where we don't need to include adc_ng.h */ +XFA_INIT_CONST(adc_ng_backend_t, adc_ng_backends); +XFA_INIT(adc_ng_state_t, adc_ng_states); diff --git a/drivers/include/adc_ng.h b/drivers/include/adc_ng.h new file mode 100644 index 000000000000..886c21ff1412 --- /dev/null +++ b/drivers/include/adc_ng.h @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @defgroup drivers_adc_ng Common ADC API + * @ingroup drivers_periph + * + * This module contains a platform and hardware independent ADC API. + * It is intended to address both advanced and simple use cases and allow + * using both external and internal ADCs transparently. + * + * @{ + * + * @file + * @brief Interface definition of the common ADC API + * + * @author Marian Buschsieweke + */ + +#ifndef ADC_NG_H +#define ADC_NG_H + +#include +#include +#include +#include + +#include "adc_ng_internal.h" +#include "bitarithm.h" +#include "xfa.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Pass this special value as parameter `ref` in @ref adc_ng_init + * to select the highest supported reference voltage + */ +#define ADC_NG_MAX_REF (0U) + +/** + * @name Helper functions query supported ADC resolutions + * + * @{ + */ + +/** + * @brief Maximum resolution supported by the API + * + * @note The maximum resolution supported by drivers can be and often is + * lower than this value + */ +#define ADC_NG_MAX_RES (32U) + +XFA_USE_CONST(adc_ng_backend_t, adc_ng_backends); +XFA_USE(adc_ng_state_t, adc_ng_states); + +/** + * @brief Check if the given ADC supports the given resolution + * + * @param[in] adc ADC device to check + * @param[in] res Resolution to check + * + * @retval 1 Resolution is supported + * @retval 0 Resolution is not supported + */ +static inline int adc_ng_supports_res(uint8_t adc, uint8_t res) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert((res <= ADC_NG_MAX_RES) && (res > 0)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + return (be.driver->res_supported & (1 << (res - 1))); +} + +/** + * @brief Get the highest supported resolution of an ADC + * + * @param[in] adc ADC to get the highest supported resolution of + */ +static inline uint8_t adc_ng_max_res(uint8_t adc) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + return (uint8_t)(bitarithm_msb(be.driver->res_supported) + 1); +} + +/** + * @brief Get the highest supported resolution of an ADC + * + * @param[in] adc ADC to get the highest supported resolution of + */ +static inline uint8_t adc_ng_min_res(uint8_t adc) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + return (uint8_t)(bitarithm_lsb(be.driver->res_supported) + 1); +} + +/** + * @brief Select a resolution supported by the given ADC that is equal to + * or greater than the requested resolution. + * + * @param[in] adc ADC to select an appropriate resolution of + * @param[in,out] res In: The resolution to select. Out: The resolution + * actually selected + * @param[out] shift The number of bits to shift samples to the right + * to achieve the requested resolution + * + * @retval 0 Success + * @retval -ENOTSUP No resolution equal to or greater than the requested + * found. + * + * Usage: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c} + * uint8_t res = 10, shift; + * int32_t sample; + * + * if (adc_ng_select_res(adc, &res, &shift) { + * return -1; + * } + * + * if (adc_ng_init(adc, res, channel, &reference_voltage) { + * return -1; + * } + * + * if (adc_ng_single(adc, &sample)) { + * return -1; + * } + * + * adc_ng_off(adc); + * + * sample >>= shift; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * @note In case of differential channels, the sample can be negative. In + * that case an arithmetic right shift is required. Most (all?) C + * compilers do so on signed types, but the C standard leaves it up + * to the implementation to choose arithmetic or logical right shift. + * For single ended ("normal") ADC channels samples are always positive + * (or zero), and both flavours of right shift are fine. + */ +static inline int adc_ng_select_res(uint8_t adc, uint8_t *res, uint8_t *shift) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(res != NULL); + uint8_t res_selected = *res; + while (!adc_ng_supports_res(adc, res_selected)) { + if (++res_selected > ADC_NG_MAX_RES) { + return -ENOTSUP; + } + } + + if (shift) { + *shift = res_selected - *res; + } + *res = res_selected; + return 0; +} + +/** @} */ + +/** + * @brief Initialize and power up the ADC channel @p chan of device @p adc + * + * @param[in] adc ADC device to initialize a channel of + * @param[in] chan Channel to initialize + * @param[in] res Resolution to sample at + * @param[in,out] ref Reference voltage in mV to use (***see details!***) + * + * @retval 0 Success + * @retval -ENOTSUP Requested resolution not supported + * @retval -ENXIO No such channel + * @retval -ERANGE Requested reference voltage is higher than all + * available references + * @retval -EALREADY The ADC is already powered and configured + * @retval <0 A driver specific error occurred + * + * @note On success, the ADC is powered up and consumes additional power + * until @ref adc_ng_off is called. + * + * The reference voltage to use is given with @p ref in millivolt. The driver + * will pick a reference voltage that is as close to @p ref as possible, but + * doesn't reduce the range. (The absolute value of the chosen reference is not + * smaller than the absolute value of the requested reference and it has the + * same sign as the requested reference.) The actually chosen reference voltage + * is stored in @p ref. + * + * E.g. if a device supports reference voltages -2.56V, -1.1V, 1.1V, and 2.56V + * and the user requests a reference voltage of 1.2V (`*ref == 1200`), the + * reference 2.56V is chosen even though 1.1V is a closer match. Otherwise + * input voltages above 1.1V would no longer be distinguishable. Similar if + * -1.3V is requested -2.56V is chosen over -1.1V. + */ +int adc_ng_init(uint8_t adc, uint8_t chan, uint8_t res, int16_t *ref); + +/** + * @brief Turn of the given ADC device + * + * @param[in] adc ADC device to turn off + */ +static inline void adc_ng_off(uint8_t adc) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + if (be.driver->off) { + be.driver->off(be.handle); + } +} + +/** + * @brief Return the number of the highest single ended channel of the + * given ADC + * + * @param[in] adc ADC to get the number of the highest single + * ended channel of + * + * @details All channel from zero to the returned value are single ended + * channels + * @note Some ADCs might have "holes" in the channel map, e.g. channels + * 0, 1 and 3 might be available, but channel 2 is not supported. + * Checking for the return value `-ENXIO` in @ref adc_ng_init can be + * used to detect these "holes". + * + * A single ended input is the "normal" ADC channel where a single input + * voltage is converted into an unsigned integer with the value 0 referring to + * an input voltage of 0 V and the highest value referring to an input voltage + * equal to the voltage reference (or higher). + */ +static inline uint8_t adc_ng_highest_single_ended_channel(uint8_t adc) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + return be.driver->highest_single_ended_channel; +} + +/** + * @name Functions access to access the raw ADC output + * + * These functions return the binary value returned by the ADC, rather than + * meaningful physical units. + * + * @{ + */ + +/** + * @brief Perform a single conversion using the specified ADC channel + * + * @param[in] adc ADC device to use + * @param[out] dest The result is stored here + * + * @pre The ADC @p adc is initialized using @ref adc_ng_init + * + * @retval 0 Success + * @retval <0 A device specific error occurred + * + * @note The sample is returned as signed integer to allow for differential + * two ended channels: If the input voltage on the positive input is + * lower than the voltage at the negative input the result should be + * negative. For "normal" single ended inputs the result must always + * be positive or zero. + */ +static inline int adc_ng_single(uint8_t adc, int32_t *dest) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + assert(dest); + const adc_ng_backend_t be = adc_ng_backends[adc]; + return be.driver->single(be.handle, dest); +} + +/** + * @brief Initialize an ADC channel, perform a single conversion with + * maximum resolution and range, and power it off again + * + * @param[in] adc ADC device to use + * @param[in] chan Channel to use + * @param[out] dest The result is stored here + * + * @retval 0 Success + * @retval <0 A driver specific error occurred + * + * Refer to the documentation of @ref adc_init for details on @p ref + */ +static inline int adc_ng_quick(uint8_t adc, uint8_t chan, + int32_t *dest) +{ + int16_t ref = ADC_NG_MAX_REF; + int retval = adc_ng_init(adc, chan, adc_ng_max_res(adc), &ref); + if (retval) { + return retval; + } + retval = adc_ng_single(adc, dest); + adc_ng_off(adc); + return retval; +} + +/** @} */ + +/** + * @name Functions to get measurements in voltage levels + * + * @{ + */ + +/** + * @brief Run a single measurements and get the result in mV + * + * @param[in] adc ADC to use + * @param[out] dest_mv Write the measurement result in mV here + * + * @pre The state given in @p state has been initialized (see + * @ref adc_ng_init) and the ADC currently is powered + * + * @retval 0 Success + * @retval <0 A driver specific error occurred + * + * @note Please note that a while a precision roughly in the order of µV + * is with the right setup and ADC technically feasible, the accuracy + * of the result is more likely in the order of mV. + */ +static inline int adc_ng_voltage(uint8_t adc, int16_t *dest_mv) +{ + int32_t sample; + int retval = adc_ng_single(adc, &sample); + if (retval) { + return retval; + } + + *dest_mv = adc_ng_convert(adc, sample); + return 0; +} +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* ADC_NG_H */ +/** @} */ diff --git a/drivers/include/adc_ng_util.h b/drivers/include/adc_ng_util.h new file mode 100644 index 000000000000..86d5126613fa --- /dev/null +++ b/drivers/include/adc_ng_util.h @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @defgroup drivers_adc_ng_util Utility functions for ADC NG + * @ingroup drivers_adc_ng + * + * This module contains contains utility functions for the ADC NG API. + * + * @{ + * + * @file + * @brief Interface definition of the ADC utility API + * + * @author Marian Buschsieweke + */ + +#ifndef ADC_NG_UTIL_H +#define ADC_NG_UTIL_H + +#include +#include +#include + +#include "adc_ng.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Measure the MCU's supply voltage using the given ADC + * + * @param[in] adc ADC to use + * @param[out] dest_mv Measured supply voltage in mV will be stored here + * + * @retval 0 Success + * @retval -ENOTSUP Measuring the supply voltage is not supported using + * the ADC @p adc + * @retval <0 Error + * + * @pre The ADC is currently offline, it will internally be initialized + */ +int adc_ng_vcc(uint8_t adc, int16_t *dest_mv); + +/** + * @brief Measure the temperature using a thermistor + * + * @param[in] adc ADC device to use + * @param[in] chan Channel the NTC is connected to + * @param[in] ntc Parameters of the NTC used + * @param[out] temp The temperature in 0.1°C will be stored here + * + * @retval 0 Success + * @retval <0 Error + * + * @pre The ADC identified by @p adc is currently offline + */ +int adc_ng_ntc(uint8_t adc, uint8_t chan, const adc_ng_ntc_t *ntc, int16_t *temp); + +/** + * @brief Measure the temperature using a thermistor internally connected to + * the ADC + * + * @param[in] adc ADC device to use + * @param[out] temp The temperature in 0.1°C will be stored here + * + * @retval 0 Success + * @retval -ENOTSUP No internal NTC connected to the given ADC + * @retval <0 Other device specific error + * + * @pre The ADC identified by @p adc is currently offline + */ +static inline int adc_ng_internal_ntc(uint8_t adc, int16_t *temp) +{ + assert(adc < XFA_LEN(adc_ng_backend_t, adc_ng_backends)); + const adc_ng_backend_t be = adc_ng_backends[adc]; + if (!be.driver->ntc) { + return -ENOTSUP; + } + + return adc_ng_ntc(adc, ADC_NG_CHAN_NTC, be.driver->ntc, temp); +} + +/** + * @brief Use the ADC to obtain entropy + * + * @param[in] adc ADC device to use + * @param[out] dest Buffer to store the collected entropy into + * @param[in] size Size of @p dest + * + * @retval 0 Success + * @retval -ENOTSUP The given ADC does not support collecting entropy + * @retval <0 Other device specific error + * + * @pre The ADC identified by @p adc is currently offline + * + * @warning The quality of the entropy generated from an ADC can depend on + * many factors and even under ideal circumstances is usually not + * well suited for direct use in cryptographic contexts. However, as + * an additionally source of weak entropy used by an entropy extractor, + * this should work well. + */ +int adc_ng_entropy(uint8_t adc, void *dest, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* ADC_NG_UTIL_H */ +/** @} */ diff --git a/drivers/include/periph/adc.h b/drivers/include/periph/adc.h index b6eb4de7133b..357583200715 100644 --- a/drivers/include/periph/adc.h +++ b/drivers/include/periph/adc.h @@ -86,6 +86,8 @@ typedef uint_fast8_t adc_t; #define ADC_LINE(x) (x) #endif +#if !defined(MODULE_ADC_NG_COMPAT) || defined(DOXYGEN) + /** * @brief Possible ADC resolution settings */ @@ -128,6 +130,74 @@ int adc_init(adc_t line); */ int32_t adc_sample(adc_t line, adc_res_t res); +#else /* MODULE_ADC_NG_COMPAT */ + +#include "adc_ng.h" + +/** + * @brief Possible ADC resolution settings + */ +typedef enum { + ADC_RES_1BIT = 1, /**< ADC resolution: 1 bit */ + ADC_RES_2BIT, /**< ADC resolution: 2 bit */ + ADC_RES_3BIT, /**< ADC resolution: 3 bit */ + ADC_RES_4BIT, /**< ADC resolution: 4 bit */ + ADC_RES_5BIT, /**< ADC resolution: 5 bit */ + ADC_RES_6BIT, /**< ADC resolution: 6 bit */ + ADC_RES_7BIT, /**< ADC resolution: 7 bit */ + ADC_RES_8BIT, /**< ADC resolution: 8 bit */ + ADC_RES_9BIT, /**< ADC resolution: 9 bit */ + ADC_RES_10BIT, /**< ADC resolution: 10 bit */ + ADC_RES_11BIT, /**< ADC resolution: 11 bit */ + ADC_RES_12BIT, /**< ADC resolution: 12 bit */ + ADC_RES_13BIT, /**< ADC resolution: 13 bit */ + ADC_RES_14BIT, /**< ADC resolution: 14 bit */ + ADC_RES_15BIT, /**< ADC resolution: 15 bit */ + ADC_RES_16BIT, /**< ADC resolution: 16 bit */ + ADC_RES_17BIT, /**< ADC resolution: 17 bit */ + ADC_RES_18BIT, /**< ADC resolution: 18 bit */ + ADC_RES_19BIT, /**< ADC resolution: 19 bit */ + ADC_RES_20BIT, /**< ADC resolution: 20 bit */ + ADC_RES_21BIT, /**< ADC resolution: 21 bit */ + ADC_RES_22BIT, /**< ADC resolution: 22 bit */ + ADC_RES_23BIT, /**< ADC resolution: 23 bit */ + ADC_RES_24BIT, /**< ADC resolution: 24 bit */ + ADC_RES_25BIT, /**< ADC resolution: 25 bit */ + ADC_RES_26BIT, /**< ADC resolution: 26 bit */ + ADC_RES_27BIT, /**< ADC resolution: 27 bit */ + ADC_RES_28BIT, /**< ADC resolution: 28 bit */ + ADC_RES_29BIT, /**< ADC resolution: 29 bit */ + ADC_RES_30BIT, /**< ADC resolution: 30 bit */ + ADC_RES_31BIT, /**< ADC resolution: 31 bit */ + ADC_RES_32BIT, /**< ADC resolution: 32 bit */ +} adc_res_t; + +static inline int adc_init(adc_t line) +{ + (void)line; + return 0; +} + +static inline int32_t adc_sample(adc_t line, adc_res_t res) +{ + int16_t ref = ADC_NG_MAX_REF; + int32_t result; + + if (adc_ng_init(0, (uint8_t)line, (uint8_t)res, &ref)) { + return -1; + } + if (adc_ng_single(0, &result)) { + adc_ng_off(0); + return -1; + } + + adc_ng_off(0); + + return result; +} + +#endif /* MODULE_ADC_NG_COMPAT */ + #ifdef __cplusplus } #endif diff --git a/drivers/periph_common/init.c b/drivers/periph_common/init.c index f547852cf563..3709bfeb57fb 100644 --- a/drivers/periph_common/init.c +++ b/drivers/periph_common/init.c @@ -55,6 +55,14 @@ #endif #endif /* MODULE_PERIPH_INIT */ +#ifdef MODULE_PERIPH_INIT_ADC_NG +__attribute__((weak)) +void periph_adc_ng_init(void) +{ + /* empty fallback */ +} +#endif + void periph_init(void) { #ifdef MODULE_PERIPH_INIT @@ -117,5 +125,9 @@ void periph_init(void) vbat_init(); #endif +#ifdef MODULE_PERIPH_INIT_ADC_NG + periph_adc_ng_init(); +#endif + #endif /* MODULE_PERIPH_INIT */ } diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index cc64d28a9ac6..25c304090c44 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -20,6 +20,7 @@ ## ## @{ +PSEUDOMODULES += adc_ng_% PSEUDOMODULES += atomic_utils PSEUDOMODULES += base64url diff --git a/tests/adc_ng/Makefile b/tests/adc_ng/Makefile new file mode 100644 index 000000000000..692af508d98c --- /dev/null +++ b/tests/adc_ng/Makefile @@ -0,0 +1,9 @@ +BOARD ?= arduino-mega2560 +include ../Makefile.tests_common + +USEMODULE += adc_ng_default +USEMODULE += adc_ng_util +USEMODULE += fmt +USEMODULE += shell + +include $(RIOTBASE)/Makefile.include diff --git a/tests/adc_ng/main.c b/tests/adc_ng/main.c new file mode 100644 index 000000000000..51ee7b9443b6 --- /dev/null +++ b/tests/adc_ng/main.c @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for ADC NG drivers + * + * @author Marian Buschsieweke + * + * @} + */ + +#include + +#include "adc_ng.h" +#include "adc_ng_util.h" +#include "fmt.h" +#include "periph/adc.h" +#include "shell.h" + +static int cmd_init(int argc, char **argv); +static int cmd_off(int argc, char **argv); +static int cmd_single(int argc, char **argv); +static int cmd_quick(int argc, char **argv); +static int cmd_volt(int argc, char **argv); +static int cmd_vcc(int argc, char **argv); +static int cmd_ntc(int argc, char **argv); +static int cmd_entropy(int argc, char **argv); + +static const shell_command_t cmds[] = { + { "init", "adc_ng_init(adc, chan, res, ref)", cmd_init }, + { "off", "adc_ng_off(adc)", cmd_off }, + { "single", "adc_ng_single(adc)", cmd_single }, + { "quick", "adc_ng_quick(adc, chan)", cmd_quick }, + { "volt", "adc_ng_voltage(adc)", cmd_volt }, + { "vcc", "adc_ng_vcc(adc)", cmd_vcc }, + { "ntc", "adc_ng_internal_ntc(adc)", cmd_ntc }, + { "entropy", "adc_ng_entropy(adc, num_bytes)", cmd_entropy }, + { NULL, NULL, NULL } +}; + +static uint8_t entropy_buf[32]; + +static int is_adc_invalid(uint8_t adc) +{ + if (adc >= XFA_LEN(adc_ng_backend_t, adc_ng_backends)) { + print_str("ADC #"); + print_u32_dec(adc); + print_str(" is out of range\n"); + return 1; + } + + return 0; +} + +static int cmd_init(int argc, char **argv) +{ + if (argc != 5) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + uint8_t chan = atoi(argv[2]); + uint8_t res = atoi(argv[3]); + int16_t ref = atoi(argv[4]); + + print_str("adc_ng_init("); + print_u32_dec(adc); + print_str(", "); + print_u32_dec(chan); + print_str(", "); + print_u32_dec(res); + print_str(", &"); + print_u32_dec(ref); + print_str(")\n"); + + int retval = adc_ng_init(adc, chan, res, &ref); + if (!retval) { + print_str("Chosen reference voltage: "); + char buf[16]; + print(buf, fmt_s32_dfp(buf, ref, -3)); + print_str(" V \n"); + } + else { + print_str("adc_ng_init() failed with "); + print_s32_dec(retval); + print_str("\n"); + } + + return 0; +} + +static int cmd_off(int argc, char **argv) +{ + if (argc != 2) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + adc_ng_off(adc); + print_str("adc_ng_off("); + print_u32_dec(adc); + print_str(")\n"); + + return 0; +} + +static int cmd_single(int argc, char **argv) +{ + if (argc != 2) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + int32_t sample; + print_str("adc_ng_single("); + print_u32_dec(adc); + print_str("): "); + int retval = adc_ng_single(adc, &sample); + if (!retval) { + print_str("Success\nSample: "); + print_s32_dec(sample); + print_str("\n"); + } + else { + print_str("Failed ("); + print_s32_dec(retval); + print_str(")\n"); + } + + return 0; +} + +static int cmd_quick(int argc, char **argv) +{ + if (argc != 3) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + uint8_t chan = atoi(argv[2]); + + print_str("adc_ng_quick("); + print_u32_dec(adc); + print_str(", "); + print_u32_dec(chan); + print_str(", &sample)\n"); + + int32_t sample; + int retval = adc_ng_quick(adc, chan, &sample); + if (retval) { + print_str("adc_ng_quick() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + print_str("sample = "); + print_s32_dec(sample); + print_str("\n"); + + return 0; +} + +static int cmd_volt(int argc, char **argv) +{ + if (argc != 2) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + + print_str("adc_ng_voltage("); + print_u32_dec(adc); + print_str(", &value)\n"); + + int16_t value; + int retval = adc_ng_voltage(adc, &value); + if (retval) { + print_str("adc_ng_voltage() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + char buf[16]; + print_str("value = "); + print(buf, fmt_s32_dfp(buf, value, -3)); + print_str(" V \n"); + + return 0; +} + +static int cmd_vcc(int argc, char **argv) +{ + if (argc != 2) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + + print_str("adc_ng_vcc("); + print_u32_dec(adc); + print_str(", &value)\n"); + + int16_t value; + int retval = adc_ng_vcc(adc, &value); + if (retval) { + print_str("adc_ng_vcc() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + char buf[16]; + print_str("value = "); + print(buf, fmt_s32_dfp(buf, value, -3)); + print_str(" V \n"); + + return 0; +} + +static int cmd_ntc(int argc, char **argv) +{ + if (argc != 2) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + + print_str("adc_ng_internal_ntc("); + print_u32_dec(adc); + print_str(", &value)\n"); + + int16_t value; + int retval = adc_ng_internal_ntc(adc, &value); + if (retval == -ENOTSUP) { + print_str("No NTC connected to this ADC (or configured)\n"); + return -1; + } + else if (retval != 0) { + print_str("adc_ng_internal_ntc() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + char buf[16]; + print_str("value = "); + print(buf, fmt_s32_dfp(buf, value, -1)); + print_str("°C\n"); + + return 0; +} + +static int cmd_entropy(int argc, char **argv) +{ + if (argc != 3) { + print_str("Usage: "); + print_str(argv[0]); + print_str(" \n"); + return -1; + } + + uint8_t adc = atoi(argv[1]); + if (is_adc_invalid(adc)) return -1; + + size_t size = atoi(argv[2]); + + if (size > sizeof(entropy_buf)) { + print_str("Given size too big, must be at most "); + print_u32_dec(sizeof(entropy_buf)); + print_str("\n"); + return -1; + } + + print_str("adc_ng_entropy("); + print_u32_dec(adc); + print_str(", buf, "); + print_u32_dec(size); + print_str(")\n"); + + int retval = adc_ng_entropy(adc, entropy_buf, size); + if (retval == -ENOTSUP) { + print_str("The given ADC doesn't support entropy collection.\n"); + return -1; + } + else if (retval) { + print_str("adc_ng_entropy() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + + uint8_t *buf = (uint8_t *)entropy_buf; + print_str("Output:\n"); + char printbuf[2]; + fmt_byte_hex(printbuf, buf[0]); + print(printbuf, 2); + for (size_t i = 1; i < size; i++) { + print((i % 8) ? " " : "\n", 1); + fmt_byte_hex(printbuf, buf[i]); + print(printbuf, 2); + } + print_str("\n"); + + return 0; +} + +static void print_driver(uint8_t adc) +{ + const adc_ng_backend_t be = adc_ng_backends[adc]; + const adc_ng_driver_t *drv = be.driver; + print_str("Resolutions supported: "); + uint8_t lowest = adc_ng_min_res(adc); + print_u32_dec(lowest); + print_str(" bit"); + for (uint8_t i = lowest + 1; i < 32; i++) { + if (adc_ng_supports_res(adc, i)) { + print_str(", "); + print_u32_dec(i); + print_str(" bit"); + } + } + + print_str("\nChannels referring to single ended inputs: 0 - "); + print_u32_dec(adc_ng_highest_single_ended_channel(adc)); + + print_str("\nSupported references: "); + char buf[16]; + print(buf, fmt_s32_dfp(buf, drv->refs[0], -3)); + print_str(" V"); + for (unsigned i = 1; drv->refs[i] != 0; i++) { + print_str(", "); + print(buf, fmt_s32_dfp(buf, drv->refs[i], -3)); + print_str(" V"); + } + + print_str("\nReference selectable as input: "); + if (drv->ref_input_idx == ADC_NG_NO_SUCH_REF) { + print_str("None"); + } + else { + int32_t ref = drv->refs[drv->ref_input_idx]; + print(buf, fmt_s32_dfp(buf, ref, -3)); + print_str(" V"); + } +} + +int main(void) +{ + print_str( + "RIOT ADC NG driver test\n" + "=======================\n" + "\n" + ); + + /* initialize all available ADC lines */ + for (uint8_t i = 0; i < XFA_LEN(adc_ng_backend_t, adc_ng_backends); i++) { + print_str("Driver #"); + print_u32_dec(i); + print_str(":\n"); + print_driver(i); + } + + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(cmds, line_buf, SHELL_DEFAULT_BUFSIZE); + + return 0; +} diff --git a/tests/adc_ng_entropy/Makefile b/tests/adc_ng_entropy/Makefile new file mode 100644 index 000000000000..a541ca89b538 --- /dev/null +++ b/tests/adc_ng_entropy/Makefile @@ -0,0 +1,8 @@ +BOARD ?= arduino-nano +include ../Makefile.tests_common + +USEMODULE += adc_ng_default +USEMODULE += adc_ng_util +USEMODULE += fmt + +include $(RIOTBASE)/Makefile.include diff --git a/tests/adc_ng_entropy/main.c b/tests/adc_ng_entropy/main.c new file mode 100644 index 000000000000..7954be028bd8 --- /dev/null +++ b/tests/adc_ng_entropy/main.c @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for ADC NG drivers + * + * @author Marian Buschsieweke + * + * @} + */ + +#include + +#include "adc_ng.h" +#include "adc_ng_util.h" +#include "fmt.h" + +int main(void) +{ + print_str( + "RIOT ADC NG NTC entropy driver test\n" + "===================================\n" + "\n" + ); + + while (1) { + int8_t entropy[16]; + int retval = adc_ng_entropy(0, entropy, sizeof(entropy)); + if (retval) { + print_str("adc_ng_entropy() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + char buf[2]; + fmt_byte_hex(buf, entropy[0]); + print(buf, 2); + for (unsigned i = 1; i < sizeof(entropy); i++) { + print(" ", 1); + fmt_byte_hex(buf, entropy[i]); + print(buf, 2); + } + print("\n", 1); + } + + return 0; +} diff --git a/tests/adc_ng_ntc/Makefile b/tests/adc_ng_ntc/Makefile new file mode 100644 index 000000000000..a541ca89b538 --- /dev/null +++ b/tests/adc_ng_ntc/Makefile @@ -0,0 +1,8 @@ +BOARD ?= arduino-nano +include ../Makefile.tests_common + +USEMODULE += adc_ng_default +USEMODULE += adc_ng_util +USEMODULE += fmt + +include $(RIOTBASE)/Makefile.include diff --git a/tests/adc_ng_ntc/main.c b/tests/adc_ng_ntc/main.c new file mode 100644 index 000000000000..98842b38a191 --- /dev/null +++ b/tests/adc_ng_ntc/main.c @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for ADC NG drivers + * + * @author Marian Buschsieweke + * + * @} + */ + +#include + +#include "adc_ng.h" +#include "adc_ng_util.h" +#include "fmt.h" + +int main(void) +{ + print_str( + "RIOT ADC NG NTC temperature driver test\n" + "=======================================\n" + "\n" + ); + + while (1) { + int16_t value; + int retval = adc_ng_internal_ntc(0, &value); + if (retval) { + print_str("adc_ng_internal_ntc() failed with "); + print_s32_dec(retval); + print_str("\n"); + return -1; + } + char buf[16]; + print(buf, fmt_s32_dfp(buf, value, -1)); + print_str("°C\n"); + } + + return 0; +}