Ce document montre comment utiliser le convertisseur analogique/numérique (ADC) du microcontrôleur Nordic nRF52840, qui équipe l'arduino Arduino nano 33 BLE. Ce microcontrôleur comporte un processeur ARM Cortex M4, particulièrement adapté au traitement numérique du signal. Le Cortex M4 possède en effet une unité de calcul en virgule flottante et comporte des instructions optimisées pour le traitement numérique du signal (SMID : Single Instruction Multiple Data), en particulier pour le traitement du signal audio stéréo. Dans ce document, en verra un exemple de traitement du signal réalisé au moyen de la bibliothèque CMSIS-DSP (Cortex Microcontroller Software Interface Standard).
Le convertisseur A/N a une précision de 12 bits et peut numériser jusqu'à 200 000 échantillons par secondes. Le multiplexeur associé permet de numériser jusqu'à 8 voies en mode simple ou 4 voies en mode différentiel. Il comporte un amplificateur à gain réglable, qui permet d'effectuer une numérisation en haute résolution de signaux de très faible amplitude. Le convertisseur enregistre le signal numérisé directement en mémoire RAM, grace au mécanisme DMA (Direct Memory Access). Le microprocesseur est donc disponible pour des tâches de calcul lorsque le convertisseur travaille, ce qui est un avantage dans une application de traitement du signal.
La notice d'utilisation du microcontrôleur nRF52840 (nRF52840 Product Specification) détaille la programmation des périphériques associés au microprocesseur, en particulier le convertisseur SAADC (Successive Approximation Analog to Digital Converter). Nous aurons besoin aussi de programmer un TIMER (pour l'échantillonnage) et le PPI (Programmable peripheral interconnect), qui permet au Timer de déclencher la numérisation.
La configuration d'un périphérique se fait via des registres. Nous proposons une classe C++ NRF52_ADC qui permet d'utiliser facilement le convertisseur depuis un programme Arduino. L'intitulé NRF52 indique que cette classe fonctionne probablement sur tous les microcontrôleurs de la famille nRF52xxx, bien que nous l'ayons testé seulement sur le nRF52840. Voici le fichier entête :
#include "nrf.h" #ifndef _NRF52_ADC_ #define _NRF52_ADC_ #define NRF52_ADC_GAIN_1_6 0 #define NRF52_ADC_GAIN_1_5 1 #define NRF52_ADC_GAIN_1_4 2 #define NRF52_ADC_GAIN_1_3 3 #define NRF52_ADC_GAIN_1_2 4 #define NRF52_ADC_GAIN_1 5 #define NRF52_ADC_GAIN_2 6 #define NRF52_ADC_GAIN_4 7 #define NRF52_ADC_RESOLUTION_8 0 #define NRF52_ADC_RESOLUTION_10 1 #define NRF52_ADC_RESOLUTION_12 2 #define NRF52_ADC_RESOLUTION_14 3 #define NRF52_ADC_OVERSAMPLE_NONE 0 #define NRF52_ADC_OVERSAMPLE_2 1 #define NRF52_ADC_OVERSAMPLE_4 2 #define NRF52_ADC_OVERSAMPLE_8 3 #define NRF52_ADC_OVERSAMPLE_16 4 #define NRF52_ADC_OVERSAMPLE_32 5 #define NRF52_ADC_OVERSAMPLE_64 6 #define NRF52_ADC_OVERSAMPLE_128 7 #define NRF52_ADC_OVERSAMPLE_256 8 #define NRF52_COMP_REF_1_2 0 #define NRF52_COMP_REF_1_8 1 #define NRF52_COMP_REF_2_4 2 class NRF52_ADC { private: const float gains[8] = {1.0/6,1.0/5,1.0/4,1.0/3,1.0/2,1.0,2.0,4.0}; const uint16_t resolution[4] = {256,1024,4096,16384}; const uint8_t voies[8] = {3,4,7,6,8,1,5,2}; const float vref = 0.6; uint8_t prescaler; uint32_t compare; int8_t oversample; float fechant; float ampl_conv[8]; void reset_channels(); void config_timer(); public: NRF52_ADC(); void config_une_voie(uint8_t voie, uint8_t igain, uint8_t surechant, int16_t *data, int16_t nechant, uint8_t iresolution=2); void config_une_voie_differentielle(uint8_t voie_p, uint8_t voie_m, uint8_t igain, uint8_t surechant, int16_t *data, int16_t nechant, uint8_t iresolution=2); void config_multi_voies(uint8_t nvoies, uint8_t *ch, uint8_t *igain, int16_t *data, int16_t nechant, uint8_t iresolution=2); void config_multi_voies_differentielles(uint8_t nvoies, uint8_t *diff, uint8_t *ch_p, uint8_t *ch_n, uint8_t *igain, int16_t *data, int16_t nechant, uint8_t iresolution=2); void config_echant(uint8_t prescaler, uint32_t compare); void attendre(); float get_fechant(); float get_ampl_conv(uint8_t v=0); void declencher(uint8_t ch, uint8_t ref, uint8_t vdown, uint8_t vup, uint8_t sens); void declencher(); }; #endif
L'implémentation des différentes fonctions de cette classe est codée dans le fichier nrf52_adc.cpp, que nous allons décrire en détail.
L'activation des voies du multiplexeur se fait au moyen des registres CH[n].PSELP pour la borne positive et CH[n].PSELN pour la borne négative (utilisée en mode différentiel seulement). La fonction (privée) reset_channels désactive toutes les voies :
#include "Arduino.h" #include "nrf52_adc.h" #define PPI_SAMPLE_CH 10 #define PPI_TIMER_CH 11 NRF52_ADC::NRF52_ADC() { } void NRF52_ADC::reset_channels() { for (uint8_t i=0; i<8; i++) { NRF_SAADC->CH[i].PSELP = 0; NRF_SAADC->CH[i].PSELN = 0; } }
La fonction config_une_voie effectue la configuration pour l'utilisation d'une seule voie, en mode simple (non différentiel). Lorsqu'on utilise une seule voie, il est possible d'effectuer un sur-échantillonnage, qui consiste à stocker en mémoire la moyenne de plusieurs échantillons (par exemples 4 échantillons). La fréquence d'échantillonnage effective est alors 4 fois plus grande que la fréquence finale du signal mémorisé. Le filtrage passe-bas effectué avant réduction de la fréquence est un simple filtre moyenneur (moyenne arithmétique), qui n'est pas le filtre passe-bas optimal mais qui a l'avantage d'être effectué par le convertisseur lui-même. Voici l'implémentation de cette fonction :
void NRF52_ADC::config_une_voie(uint8_t voie, uint8_t igain, uint8_t surechant, int16_t *data, int16_t nechant, uint8_t iresolution) { NRF_SAADC->ENABLE = 1; reset_channels(); NRF_SAADC->CH[0].PSELP = voies[voie]; NRF_SAADC->CH[0].PSELN = 0; NRF_SAADC->CH[0].CONFIG = (igain << 8); NRF_SAADC->RESOLUTION = iresolution; NRF_SAADC->SAMPLERATE = 0; // échantillonnage externe (par Timer) NRF_SAADC->OVERSAMPLE = surechant; NRF_SAADC->RESULT.PTR = ((uint32_t)(data)); NRF_SAADC->RESULT.MAXCNT = nechant; oversample = surechant; ampl_conv[0] = vref/(gains[igain]*resolution[iresolution]); # facteur de conversion nombre->volts }
Les arguments de cette fonction sont :
Précisons la gamme de tensions converties. La tension appliquée sur une borne doit être comprise entre 0 et VDD=3,3 V. Si l'on a besoin d'appliquer une tension plus basse que la masse, il faut en principe ajouter un circuit d'adapation (voir plus loin). Le convertisseur fonctionne avec une référence interne vref=0.6 volts. Soient p le nombre de bits (en principe 12) et G le gain de l'amplificateur. Soient U la tension appliquée, soit entre la borne positive et la masse, soit entre la borne positive et la borne négative dans le cas du mode différentiel. Le nombre à p bits obtenu est donné par :
avec n=0 pour le mode simple et n=1 pour le mode différentiel. Dans ce second cas, l'entier fourni est signé. Par exemple pour p=12 bits, une conversion en mode simple donne un entier compris entre 0 et 4095 et une conversion en mode différentiel donne un entier compris entre -2048 et 2047 (codé par complément à deux, avec le bit p indiquant le signe). Voici la plage de tensions accessible (en mode simple) pour différentes valeurs du gain :
import numpy as np Vref = 0.6 gain = np.array([1.0/6,1.0/5,1.0/4,1.0/3,1.0/2,1.0,2.0,4.0]) Umax = Vref/gain
print(Umax) --> array([3.6 , 3. , 2.4 , 1.8 , 1.2 , 0.6 , 0.3 , 0.15])
Par exemple pour un gain de 1/4 (NRF52_ADC_GAIN_1_4), on peut mesurer une tension de 0 à 2,4 V (en mode simple) et de -1,2 V à 1,2 V en mode différentiel. Le gain le plus élevé (NRF52_ADC_GAIN_4) permet de mesurer des tensions entre 0 et 150 mV (en mode simple).
En mode simple, il se produit une saturation lorsque la tension appliquée excède la valeur maximale. En revanche, une valeur négative est tolérée et donne bien un nombre négatif. Par exemple pour un gain de 1, nous avons réussi à numériser en mode simple une tension alternative comprise entre -600 mV et 600 mV. Cette propriété n'est pas mentionnée dans la documentation du microcontrôleur. Il est même écrit : The AIN0-AIN7 inputs cannot exceed VDD, or be lower than VSS, or VSS=0 volts. La documentation indique toutefois que, en mode simple, le convertisseur fonctionne en fait en mode différentiel mais avec une entrée négative connectée à la masse, ce qui implique en effet qu'une tension négative puisse être numérisée. À défaut d'information sur la tension négative maximale acceptable, nous préférons éviter d'appliquer une tension négative inférieure à -300 mV, car c'est la valeur minimale indiquée dans le tableau de spécifications électriques du microcontrôleur.
La fonction config_une_voie_differentielle est similaire à la précédente, mais il faut bien sûr préciser en plus la voie utilisée pour la borne négative.
void NRF52_ADC::config_une_voie_differentielle(uint8_t voie_p, uint8_t voie_m, uint8_t igain, uint8_t surechant, int16_t *data, int16_t nechant, uint8_t iresolution) { NRF_SAADC->ENABLE = 1; reset_channels(); NRF_SAADC->CH[0].PSELP = voies[voie_p]; NRF_SAADC->CH[0].PSELN = voies[voie_m]; NRF_SAADC->CH[0].CONFIG = (igain << 8) | (1 << 20); // bit 20 = 1 pour le mode différentiel NRF_SAADC->RESOLUTION = iresolution; NRF_SAADC->SAMPLERATE = 0; NRF_SAADC->OVERSAMPLE = surechant; NRF_SAADC->RESULT.PTR = ((uint32_t)(data)); NRF_SAADC->RESULT.MAXCNT = nechant; oversample = surechant; ampl_conv[0] = vref/(gains[igain]*resolution[iresolution]/2); }
La fonction config_multi_voies permet de configurer plusieurs voies en mode simple.
void NRF52_ADC::config_multi_voies(uint8_t nvoies, uint8_t *ch, uint8_t *igain, int16_t *data, int16_t nechant, uint8_t iresolution) { NRF_SAADC->ENABLE = 1; reset_channels(); uint8_t i; for (i=0; i<nvoies; i++) { NRF_SAADC->CH[i].PSELP = voies[ch[i]]; NRF_SAADC->CH[i].PSELN = 0; NRF_SAADC->CH[i].CONFIG = (igain[i] << 8); ampl_conv[i] = vref/(gains[igain[i]]*resolution[iresolution]); } NRF_SAADC->RESOLUTION = iresolution; NRF_SAADC->SAMPLERATE = 0; NRF_SAADC->OVERSAMPLE = 0; NRF_SAADC->RESULT.PTR = ((uint32_t)(data)); NRF_SAADC->RESULT.MAXCNT = nechant*nvoies; oversample = 0; }
Les arguments de cette fonction sont :
La fonction config_multi_voies_differentielles permet de configurer plusieurs voies, chacune pouvant être en mode différentiel ou pas.
void NRF52_ADC::config_multi_voies_differentielles(uint8_t nvoies, uint8_t *diff, uint8_t *ch_p, uint8_t *ch_n, uint8_t *igain, int16_t *data, int16_t nechant, uint8_t iresolution) { NRF_SAADC->ENABLE = 1; reset_channels(); uint8_t i; for (i=0; i<nvoies; i++) { NRF_SAADC->CH[i].PSELP = voies[ch_p[i]]; if (diff[i]) { NRF_SAADC->CH[i].PSELN = voies[ch_n[i]] | (1 << 20); ampl_conv[i] = vref/(gains[igain[i]]*resolution[iresolution]/2); } else { NRF_SAADC->CH[i].PSELN = 0; ampl_conv[i] = vref/(gains[igain[i]]*resolution[iresolution]); } NRF_SAADC->CH[i].CONFIG = (igain[i] << 8); } NRF_SAADC->RESOLUTION = iresolution; NRF_SAADC->SAMPLERATE = 0; NRF_SAADC->OVERSAMPLE = 0; NRF_SAADC->RESULT.PTR = ((uint32_t)(data)); NRF_SAADC->RESULT.MAXCNT = nechant*nvoies; oversample = 0; }
Les arguments de cette fonction sont :
Voyons à présent la configuration de l'échantillonnage. Celui-ci est effectué au moyen d'un Timer (qu'on pourrait traduire par compteur ou chronomètre selon le contexte d'utilisation). Le microcontrôleur nRF52840 comporte 5 timers. Chacun de ces timers comporte un compteur (maximum 32 bits) qui est incrémenté à la fréquence suivante :
Le facteur de division (prescaler), est une entier codé sur 4 bits. La plus petite fréquence d'incrémentation du compteur est donc 1 MHz. Lorsque le compteur atteint sa valeur maximale (232 pour un compteur 32 bits), sa valeur revient à zéro. À chaque incrémentation du compteur, le timer compare sa valeur à 5 registres 32 bits appelés Capture/Compare (CC). Lorsque la valeur est égale à CC[0] (premier registre CC), un évènement EVENTS_COMPARE[0] est généré. Nous allons utiliser cet évènement pour déclencher la conversion A/N. La période d'échantillonnage sera ainsi :
Par exemple, une fréquence d'échantillonnage de 100 kHz pourra être obtenue avec prescaler=4 et CC=10. La période la plus grande est obtenue pour prescaler=4 et CC=232, soit environ 2495 s.
La fonction config_echant mémorise le prescaler et la valeur de CC puis calcule la fréquence d'échantillonnage. En cas de sur-échantillonnage (pour une seule voie seulement), la fréquence d'échantillonnage finale est réduite.
void NRF52_ADC::config_echant(uint8_t prescal, uint32_t cc) { compare = cc; prescaler = prescal; fechant = 16e6*1.0/pow(2,prescaler)*1.0/cc; fechant /= pow(2,oversample); }
La fréquence d'échantillonnage maximale est égale à 200 kHz divisé par le nombre de voies. Si on dépasse cette valeur, on doit s'attendre à des résultats incorrects. On peut ainsi numériser 5 voies en fréquence audio (40 kHz), ou 2 voies sur-échantillonnées à 96 kHz.
Les fonctions get_fechant et get_ampl_conv permettent d'obtenir les valeurs de la fréquence d'échantillonnage et du facteur de conversion (nombre vers tension) pour chaque voie configurée.
float NRF52_ADC::get_fechant() { return fechant; } float NRF52_ADC::get_ampl_conv(uint8_t v) { return ampl_conv[v]; }
La fonction config_timer (fonction privée) configure le Timer et le PPI puis démarre l'ADC (mais pas le Timer).
void NRF52_ADC::config_timer() { NRF_SAADC->EVENTS_END = 0; NRF_SAADC->EVENTS_STARTED = 0; NRF_SAADC->EVENTS_DONE = 0; NRF_SAADC->EVENTS_STOPPED = 0; NRF_SAADC->ENABLE = 1; // configuration du Timer3 NRF_TIMER3->TASKS_STOP = 1; NRF_TIMER3->MODE = 0; NRF_TIMER3->BITMODE = 3; // 32 bits NRF_TIMER3->TASKS_CLEAR = 1; NRF_TIMER3->PRESCALER = prescaler; NRF_TIMER3->CC[0] = compare; // configuration du Programmable Peripheral Interconnect NRF_PPI->CH[PPI_SAMPLE_CH].EEP = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[0]; NRF_PPI->CH[PPI_SAMPLE_CH].TEP = (uint32_t)&NRF_SAADC->TASKS_SAMPLE; NRF_PPI->FORK[PPI_SAMPLE_CH].TEP = (uint32_t)&NRF_TIMER3->TASKS_CLEAR; NRF_PPI->CHEN |= (1<< ((uint32_t)PPI_SAMPLE_CH)); NRF_PPI->CHENSET |= (1<< ((uint32_t)PPI_SAMPLE_CH)); NRF_SAADC->TASKS_START = 1; NRF_TIMER3->EVENTS_COMPARE[0] = 0; }
Le PPI (Programmable Peripheral Interconnect) est un périphérique qui permet de coordonner des actions entre différents périphérique. Il permet de relier un évènement émis par un périphérique à une tâche reçue par un autre périphérique. Dans notre cas, l'évènement EVENTS_COMPARE[0] généré par le Timer déclenche la tâche TASKS_SAMPLE de l'ADC, qui consiste à effectuer l'échantillonnage des voies configurées.
La fonction declencher déclenche une acquisition.
void NRF52_ADC::declencher() { config_timer(); NRF_TIMER3->TASKS_START = 1; }
La seconde version de la fonction declencher (qui comporte 4 arguments), permet de déclencher à partir d'un seuil sur une entrée analogique (autre qu'une entrée utilisée par l'ADC). On utilise pour cela le comparateur (COMP). Celui-ci procède à partir d'une tension de référence (VREF) égale à 1,2 V, 1,8 V ou 2,4 V. Le seuil de déclenchement est déterminé à partir de cette tension de référence au moyen d'une échelle à 64 niveaux (codés sur 6 bits). Plus précisément, on définit un seuil bas (THDOWN) et (THUP) afin d'obtenir un déclenchement avec hystérésis. Les deux tensions seuil sont :
La tension de seuil est égale soit à VUP soit à VDOWN. Lorsque la tension U appliquée sur l'entrée analogique passe par le seuil par valeur croissante, le seuil passe à VDOWN. Lorsque la tension U passe par le seuil par valeur décroissante, le seuil passe à VUP. En choisissant VDOWN < VUP, on obtient un effet d'hystérésis qui évite des basculements multiples en présence de bruit (mais ce n'est pas important pour le déclenchement de l'ADC).
Un canal de PPI permet de déclencher le démarrage du Timer (qui actionne l'échantilloneur de l'ADC) à partir d'un évènement EVENTS_UP du COMP (passage du seuil par valeur croissante) ou EVENTS_DOWN (par valeur décroissante).
void NRF52_ADC::declencher(uint8_t ch, uint8_t ref, uint8_t thdown, uint8_t thup, uint8_t sens) { config_timer(); // configuration du comparateur NRF_COMP->ENABLE = 1; NRF_COMP->MODE = 2; NRF_COMP->PSEL = voies[ch]-1; NRF_COMP->REFSEL = ref; NRF_COMP->TH = thdown | (thup << 8); // PPI pour déclencher le Timer à partir de l'évènement EVENTS_UP NRF_PPI->CH[PPI_TIMER_CH].EEP = (uint32_t)&NRF_COMP->EVENTS_UP; NRF_PPI->CH[PPI_TIMER_CH].TEP = (uint32_t)&NRF_TIMER3->TASKS_START; NRF_PPI->CHEN |= (1<< ((uint32_t)PPI_TIMER_CH)); NRF_PPI->CHENSET |= (1<< ((uint32_t)PPI_TIMER_CH)); // déclenchement du comparateur NRF_COMP->TASKS_START = 1; NRF_COMP->EVENTS_UP=0; }
Les arguments de cette fonction sont :
La fonction attendre permet d'attendre que l'ADC ait terminé la numérisation et le remplissage du tableau programmé. Elle stoppe le timer et la liaison PPI.
void NRF52_ADC::attendre() { while (NRF_SAADC->EVENTS_END==0); NRF_SAADC->EVENTS_END = 0; NRF_SAADC->ENABLE = 0; NRF_TIMER3->TASKS_STOP = 1; NRF_PPI->CHEN &= ~(1<< ((uint32_t)10)); }
Dans cette partie, nous présentons un exemple d'utilisation de la classe précédente, qui permet de tester ses différentes fonctions. Pour vérifier le bon fonctionnement de l'échantillonnage, une analyse spectrale est effectuée, grace à la fonction FFT de la bibliothèque CMSIS-DSP.
#include "nrf.h" #include <arm_math.h> // CMSIS DSP #include "nrf52_adc.h" //#define DEUX_VOIES // décommenter pour tester 2 voies //#define FFT // décommenter pour faire l'analyse spectrale #define NSAMPLES 512 // nombre d'échantillons pour chaque voie #ifdef DEUX_VOIES #define NSAMPLES_16BITS 1024 #else #define NSAMPLES_16BITS NSAMPLES #endif NRF52_ADC adc; int16_t data[NSAMPLES_16BITS]; uint8_t nvoies; float32_t data_f32[NSAMPLES]; arm_rfft_fast_instance_f32 rfft_instance; float32_t fft[NSAMPLES*2]; float32_t fft_amp[NSAMPLES];
La fonction setup effectue la configuration d'une voie ou de deux voies (si la macro DEUX_VOIES est définie), puis la configuration de l'échantillonnage.
void setup() { Serial.begin(115200); for (int i=0; i<NSAMPLES; i++) data[i] = 0; #ifdef DEUX_VOIES nvoies = 2; uint8_t voies[2] = {0,1}; uint8_t gains[2] = {NRF52_ADC_GAIN_1_4,NRF52_ADC_GAIN_1_4}; adc.config_multi_voies(nvoies,voies,gains,data,NSAMPLES,NRF52_ADC_RESOLUTION_12); #else nvoies = 1; adc.config_une_voie(0,NRF52_ADC_GAIN_1_4,NRF52_ADC_OVERSAMPLE_NONE,data,NSAMPLES,NRF52_ADC_RESOLUTION_12); #endif adc.config_echant(4,100); // échantillonnage à 10 kHz }
Dans la fonction loop, on déclenche la numérisation puis on attend qu'elle soit terminée. Les valeurs 12 bits mémorisées dans le tableau data sont converties en tensions et placées dans le tableau de flottants 32 bits data_f32. Si la macro FFT n'est pas définie, on affiche ces valeurs (ce qui permet de tracer la courbe avec le traceur série). Si la macro FFT est définie, on calcule la transformée de Fourier discrète avec la fonction arm_rfft_fast_f32 (fonction de CMSIS-DSP) puis on calcule le module avec la fonction arm_cmplx_mag_f32. Pour finir, le spectre est parcouru de manière à afficher les composantes dont l'amplitude est au-dessus d'un seuil. Si deux voies sont numérisées (A0 et A1), la voie précisée dans la variable voie est tracée ou analysée.
void loop() { float f; float a; uint16_t indice=0; uint8_t voie; float conv_a = adc.get_ampl_conv(); // facteur de conversion nombre->volt float deltaf = adc.get_fechant()/NSAMPLES; // résolution en fréquence //adc.declencher(); adc.declencher(7,NRF52_COMP_REF_2_4,32,32,1); // déclenchement par la borne A7 // on peut faire autre chose ici, pendant que l'ADC travaille adc.attendre(); for (indice=0; indice<NSAMPLES; indice++) data_f32[indice] = data[indice*nvoies+voie]*conv_a; delay(500); #ifndef FFT for (indice=0; indice<NSAMPLES; indice++) Serial.println(data_f32[indice]); #else arm_rfft_fast_init_f32(&rfft_instance,NSAMPLES); arm_rfft_fast_f32(&rfft_instance,data_f32,fft,0); arm_cmplx_mag_f32(fft,fft_amp,NSAMPLES); Serial.println("----------------"); for (int i=0; i<NSAMPLES; i++) { a = fft_amp[i]*2/NSAMPLES; f = i*deltaf; if (a > 0.3) { Serial.print(f); Serial.print(" Hz, "); Serial.println(a); } } #endif }
Lorsque la fréquence d'échantillonnage est faible (moins de 1 kHz), il peut être intéressant de traiter le signal pendant la numérisation, entre l'appel de adc.declencher et celui de adc.attendre. Le nombre de valeurs de 16 bits déjà écrites est donné par le registre NRF_SAADC->RESULT.AMOUNT. Voici pour exemple le code qu'il faudrait placer pour remplir le tableau data_f32 pendant que le remplissage du tableau data se fait :
adc.declencher(7,NRF52_COMP_REF_2_4,32,32,1); uint16_t ndata=0; uint8_t v=0; boolean traitement = true; while (traitement) { while (ndata < NRF_SAADC->RESULT.AMOUNT) { ndata += 1; if (v==voie) { data_f32[indice] = data[indice*nvoies+voie]*conv_a; } v += 1; if (v==nvoies) { v = 0; indice += 1; if (indice==NSAMPLES) traitement = false; } } } adc.attendre();