Un signal binaire comporte seulement deux états, l'état bas et l'état haut (0 et 3,3 volts dans le cas de l'Arduino GIGA R1). L'analyse d'un signal binaire (appelée aussi analyse logique) consiste à déterminer les intervalles de temps entre les fronts montants et descendants du signal. Si l'état à l'instant initial de l'analyse est connu, on peut reconstituer entièrement le signal. Les applications sont très nombreuses. On peut citer :
Ce document montre comment utiliser un timer pour effectuer l'analyse d'un signal binaire.
Il s'agit de programmer les timers du microcontrôleur en accédant directement aux registres. Trois documents sont utiles :
Le doc 1 indique à quel port GPIO du microcontrôleur chaque borne de la carte Arduino est reliée. Par exemple, il nous informe que la borne D2 est reliée au port PA3. Chaque port est identifié par un groupe défini par une lettre et par un numéro dans ce groupe. Un timer peut générer des signaux PWM sur des ports bien précis. Pour qu'un port soit utilisé comme sortie d'un timer, il doit être configuré en mode AF (Alternate Function). Le tableau 8 du doc 2 indique, pour chaque port du groupe A, les fonctions alternatives disponibles. Celles correspondant à des sorties de timers ont notées TIMn-CHx où n désigne le numéro du timer et x le numéro de la sortie du timer. Les tableaux suivants font de même pour les autres groupes de port.
Le tableau suivant regroupe les informations importantes : pour chaque borne de la carte, le port correspondant et les sorties de timers disponibles. Chaque timer est identifié par une couleur.
On se limite à l'analyse d'un signal sur les bornes D2 à D13. Le timer utilisé doit pouvoir être synchronisé de l'extérieur, ce qui exclut les timers 16 et 17. Par ailleurs, on doit se limiter aux voies CH1 et CH2. Il reste donc les timers suivants :
On pourra donc faire l'analyse simultanée de 4 signaux, par exemple sur les bornes D10, D7, D4 et D2. Un timer ne peut faire l'analyse que d'un seul signal donc il n'est pas possible d'analyser, par exemple, deux signaux sur D2 et D3.
Les timers sont souvent utilisés pour générer des signaux MLI (voir Arduino GIGA R1 : modulation de largeur d'impulsion). Pour l'analyse de signaux, le timer fonctionne en mode Input Capture.
Un timer utilise un compteur (16 bits dans le cas des timers 1,3,8 et 15). Le compteur est un registre 16 bits (CNT) incrémenté d'une unité à la période de l'horloge divisée par PSC+1 (prescaler). Nous utilisons le mode de fonctionnement edge aligned : après que CNT a atteint la valeur de ARR (auto reload register), il revient à zéro. Le timer et configuré en mode input capture. La voie CH1 est reliée à l'entrée TI1 (Timer Input 1). La voie CH2 est reliée à l'entrée TI2. Supposons que l'on analyse le signal délivré sur CH1 (par exemple la borne D10 pour le timer 1). Lorsqu'un front est détecté sur TI1, le timer enregistre la valeur de CNT dans son registre CCR1 (capture/compare register). Il peut réagir à un front montant, un front descendant, ou les deux. Pour l'exemple suivant, on choisit de détecter les fronts montants et descendants du signal analysé mais on laissera la possibilité de détecter seulement un des deux sens de front. Les valeurs de ARR et de PSC doivent être telles que la durée qu'il faut au compteur pour incrémenter CNT de 0 à ARR soit supérieure à l'intervalle de temps entre deux fronts consécutifs du signal analysé (c'est-à-dire à sa période si ce signal est périodique). La figure suivante montre l'état du compteur et le signal sur TI1 (reliée à CH1).
Figure pleine pageLes flèches bleues avec CCR1 indiquent les valeurs de CNT copiées dans le registre CCR1. Il faudra récupérer cette valeur dans une fonction ISR (Interrupt Service Routine) d'une interruption déclenchée par cette copie dans CCR1 (capture/compare interrupt). L'intervalle de temps entre deux fronts consécutifs détectés est égal à cette valeur retranchée de la valeur précédente de CCR1 si cette dernière est inférieure. Si la valeur précédente de CRR1 est supérieure (comme c'est le cas pour le quatrième front sur la figure ci-dessus), il faut ajouter ARR à la différence. Si la période du timer est supérieure à l'intervalle entre deux fronts, il se produit au plus une annulation de CNT entre deux fronts.
La période d'incrémentation du compteur est définie par :
où MAINCLOCK est la fréquene de l'horloge utilisée par le timer (240 MHz).
L'intervalle de temps entre deux fronts détectés est .
Le choix de PSC et de ARR se fait comme pour la génération de signaux PWM (Arduino GIGA R1 : modulation de largeur d'impulsion) : on choisit une période qui doit être supérieure au plus grand intervalle de temps entre les fronts détectés puis on calcule PSC et ARR par :
La période correspond à l'intervalle de temps entre deux remise à zéro du compteur si on le laissait atteindre la valeur ARR. Si le signal analysé est périodique, cette période doit être supérieure à la période du signal. La meilleure précision sera obtenue si elle est peu supérieure car alors la valeur de CCR1 sera plus grande.
Pour configurer une entrée d'un timer, il faut tout d'abord activer l'horloge pour cette entrée en mettant à 1 le bit correspondant dans le registre RCC->AHB4ENR (doc 3, page 492). Il faut ensuite configurer cette entrée en mode AF (Alternate Function) dans le registre GPIOx->MODER, où x est la lettre indiquant le groupe de la sortie (doc3 page 578). Le code 4 bits indiquant la fonction alternative doit être défini dans le registre GPIOx->AR[0] pour les sorties de numéro 0 à 7, dans le registre GPIOx->AR[1] pour les sorties de numéro 8 à 15 (doc 3 page 582). Ce code est accessible par une macro de la forme GPIO_AFm_TIMn où m est le numéro de l'AF (voir tableau ci-dessus) et n le numéro du timer. Par exemple, pour configurer la sortie CH1 du timer 1 sur le port PK1 (pin D10), il faut utiliser la macro GPIO_AF1_TIM1. Pour finir, l'entrée doit être configurée en vitesse très rapide avec le registre GPIOx->OSPEEDR (doc 3 page 579).
La classe GPIOConfig exposée dans Arduino GIGA R1 : modulation de largeur d'impulsion permet de faire la configuration d'une entrée.
L'horloge pour le timer doit être activée en mettant à 1 le bit correspondant dans le registre RCC->APB1LENR (pour le timer 3) ou bien RCC->APB2ENR (pour les timers 1,8 et 15), (doc 3 pages 496 et 502). Les sorties du timer que l'on souhaite utiliser doivent être configurées comme expliqué plus haut. En fonction de la période souhaitée (durée maximale entre deux fronts consécutifs), on calcule les valeurs de PSC et ARR, que l'on place dans les registres TIMn->PSC et TIMn->ARR (doc 3 pages 1866).
La relation entre la voie utilisée (CH1 ou CH2) et l'entrée du timer (TI1 ou TI2) est faite dans le registre TIMn->TISEL (timer input selection register, doc 3 page 1796). On associe CH1 à T1 ou bien CH2 à T2. La fonction input capture est configurée dans le registre TIMn->CCMR1 (capture compare mode register, doc 3 page 1769). Pour les bits CC1S, on choisit : CC1 channel is configured as input, IC1 is mapped on TI1. L'entrée est soumise à un filtrage numérique qui permet d'éviter les transitions multiples lors d'un front. Ce filtre est configuré par un nombre de 4 bits (bits IC1F, voir doc 3 page 1770). La capture est faite sans prescaler. La polarité de la capture (front montant, front descendant ou les deux) se configure dans les bits CC1NP et CC1P (ou bien CCN2P et CC2P) du registre TIMn->CCER (captue/compare enable register, doc 3 page 1776). C'est aussi dans ce registre que l'on active le mode capture (bit CC1E ou CC2E). L'interruption déclenchée par l'évènement capture se configure dans le registre TIMn->DIER (DMA/Interrupt enable register, donc 3 page 1764). On active CC1IE ou bien CC2IE (capture/compare interrupt).
Il faut également activer le traitement de l'interruption par le processeur dans le NVIC (nested vector interrupt controller) avec la fonction NVIC_EnableIRQ. Les noms des interruptions sont consultables doc 3 table 147 (page 798). Par exemple, la fonction de traitement pour le timer 15 est nommée TIM15 et il s'active par : NVIC_EnableIRQ(TIM15_IRQn). La fonction de traitement de l'interruption (ou ISR, interrupt service routine) doit être définie de la manière suivante :
extern "C" void TIM15_IRQHandler() { TIM15->SR &= ~TIM_SR_CC1IF; // CC2IF si CCR2 est utilisé // action à exécuter lors de l'interruption }
Pour le compilateur, TIM15_IRQHandler désigne l'emplacement précis de la mémoire où il faut placer le code de la fonction. Lorsque l'interruption se déclenche, le processeur exécute les instructions débutant à cette adresse. La mise à zéro du bit TIM_SR_CC1IF dans le registre TIMn->SR indique que l'interruption a bien été traitée. Sans cette mise à zéro, la fonction d'interruption est appelée en boucle.
Pour les timers 1 et 8, l'interruption est spécifique pour l'évènement capture/compare et se nomme TIM1_CC ou TIM8_CC.
Pour déclencher le timer, il faut mettre à 1 le bit CEN du registre TIMn->CR1 (doc 3 page 1848). C'est aussi dans ce registre (2 bits CMS) que l'on configure le mode center aligned ou edge aligned. La raison de la présence de cette configuration dans ce registre est l'impossibilité de changer de mode lorsque le timer est en fonctionnement. Nous choisissons le mode edge aligned bien que l'autre mode conduirait au même résultat puisque la remise à zéro du compteur est en fait déclenchée par l'évèvement capture et non pas par l'atteinte de ARR.
Si l'on veut utiliser plusieurs timers simultanément, il peut être nécessaire de les synchroniser. Une action sur un timer peut être déclenché par un autre timer via un canal ITR (Internal Trigger). La table 351 (doc 3 page 1853) nous informe que TIM3 peut être déclenché par TIM1 via ITR0. La table 346 (doc 3 page 1764) nous informe que TIM8 peut être déclenché par TIM1 via ITR0. La table 359 (doc 3 page 1974) nous informe que TIM15 peut être déclenché par TIM1 via ITR0. Nous choisissons donc un mode synchrone (optionnel) dans lequel le timer 1 est maître et les timers 3,8 et 15 sont esclaves. Pour le timer 1, il faut choisir le mode maître dans les bits MMS du registre TIM1->CR2 (doc 3, page 1849). Nous choisissons le mode Enable car nous voulons que le timer déclenche un signal au moment de son propre déclenchement. Ce signal est utilisé pour déclencher les autres timers. Il faut aussi activer le bit MMS dans TIM1->SMCR). Pour configure un timer en esclave, il faut préciser la source du déclenchement dans les bits TS de TIMn->SMCR (doc 3, page 1852) : on choisit ITR0 puisque c'est par ce canal que le signal en provenance de TIM1 est envoyé. Les bits SMS du même registre (Salve Mode Selection) permettent de choisir l'action à déclencher sur le timer esclave : nous choissons Trigger Mode, qui signifie que le timer sera déclenché lorsque un front montant sera reçu sur ITR0.
Il peut être nécessaire de connaître le temps écoulé depuis le démarrage du timer. Pour cela, il faut mémoriser le nombre de cycles du compteur. On prévoit donc la possibilité d'activer l'interruption update, qui se déclenche lorsque CNT atteint ARR. Notons que l'interruption correspondante est nommée TIM1_UP pour le timer 1 et TIM8_UP_TIM13 pour le timer 8. Pour les autres timers, les interruptions sont nommées TIM3 et TIM15; dans ce cas, l'ISR est la même que pour les interruptions CC (capture/compare). Il faut donc, dans cette fonction, consulter TIMn->SR pour connaître la cause de l'interruption.
Afin de programmer facilement les timers, nous écrivons une classe TimerIC. Voici le fichier entête :
#ifndef TIMERIC #define TIMERIC #include <Arduino.h> #define MAIN_CLOCK 240 // MHz #define POLARITY_RISING 0 #define POLARITY_FALLING 1 #define POLARITY_BOTH 3 class TimerIC { private: TIM_TypeDef * tim; void CC1config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity); void CC2config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity); void CC3config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity); void CC4config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity); public: uint8_t timnum; // numéro du timer uint8_t nchannels; // nombre d'entrée (1 ou 2) uint16_t psc;// clock prescaler uint16_t arr; // auto-reload register uint16_t cr1; double clockPeriod; TimerIC(uint8_t timnum, uint8_t nchan,double period, uint8_t captureFilter,uint8_t *polarity, bool synchro, bool upinter); void setPeriod(double period); void start(); void stop(); }; #endif
Les arguments du constructeur sont :
Voici le code source :
#include "timeric.h" #include "Arduino.h" #include "gpioconfig.h" void TimerIC::CC1config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity, bool upinter) { tim->TISEL |= (tim->TISEL & ~TIM_TISEL_TI1SEL); //TIMx_CH1 input tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_CC1S) | TIM_CCMR1_CC1S_0; //CC1 channel is configured as input, IC1 is mapped on TI1 tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_IC1F)| (captureFilter << TIM_CCMR1_IC1F_Pos); // Input capture 1 filter tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_IC1PSC) | (capturePrescaler << TIM_CCMR1_IC1PSC_Pos); uint8_t pol = 0; if (polarity==POLARITY_FALLING) pol = TIM_CCER_CC1P; else if (polarity==POLARITY_BOTH) pol = TIM_CCER_CC1P | TIM_CCER_CC1NP; tim->CCER |= (tim->CCER & ~(TIM_CCER_CC1P | TIM_CCER_CC1NP)) | pol; tim->CCER |= TIM_CCER_CC1E; //Capture mode enabled tim->DIER |= TIM_DIER_CC1IE; // Capture/Compare 1 interrupt enable } void TimerIC::CC2config(uint8_t captureFilter, uint8_t capturePrescaler, uint8_t polarity) { tim->TISEL |= (tim->TISEL & ~TIM_TISEL_TI2SEL); //TIMx_CH2 input tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_CC2S) | TIM_CCMR1_CC2S_0; //CC2 channel is configured as input, IC2 is mapped on TI2 tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_IC2F)| (captureFilter << TIM_CCMR1_IC2F_Pos); // Input capture 2 filter tim->CCMR1 |= (tim->CCMR1 & ~TIM_CCMR1_IC2PSC) | (capturePrescaler << TIM_CCMR1_IC2PSC_Pos); uint8_t pol = 0; if (polarity==POLARITY_FALLING) pol = TIM_CCER_CC2P; else if (polarity==POLARITY_BOTH) pol = TIM_CCER_CC2P | TIM_CCER_CC2NP; tim->CCER |= (tim->CCER & ~(TIM_CCER_CC2P | TIM_CCER_CC2NP)) |pol; tim->CCER |= TIM_CCER_CC2E; //Capture mode enabled tim->DIER |= TIM_DIER_CC2IE; // Capture/Compare 2 interrupt enable } TimerIC::TimerIC(uint8_t num, uint8_t nchan, double period, uint8_t captureFilter, uint8_t *polarity, bool synchro) { timnum = num; nchannels = nchan; switch (timnum) { case 1: tim = TIM1; break; case 3: tim = TIM3; break; case 8: tim = TIM8; break; case 15: tim = TIM15; break; default: return; } GPIOConfig gpio; psc = period*MAIN_CLOCK/0xFFFF; arr = period*MAIN_CLOCK/((psc+1))-1; clockPeriod = 1.0*(psc+1)/MAIN_CLOCK; switch(timnum) { case 1: RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // Enable TIM1 peripheral clock source if (nchan>=1) { ENABLE_CLOCK_PORTK gpio.setAFportK(1,GPIO_AF1_TIM1); // pin D10, PK1, TIM1_CH1 } if (nchan==2) { ENABLE_CLOCK_PORTJ gpio.setAFportJ(11,GPIO_AF1_TIM1); // pin D12, PJ11, TIM1_CH2 } break; case 3: RCC->APB1LENR |= RCC_APB1LENR_TIM3EN; // Enable TIM3 peripheral clock source if (nchan>=1) { ENABLE_CLOCK_PORTB gpio.setAFportB(4,GPIO_AF2_TIM3); // pin D7, PB4, TIM3_CH1 } if (nchan==2) { ENABLE_CLOCK_PORTA gpio.setAFportA(7,GPIO_AF2_TIM3); // pin D5, PA7, TIM3_CH2 } break; case 8: RCC->APB2ENR |= RCC_APB2ENR_TIM8EN; // Enable TIM8 peripheral clock source if (nchan>=1) { ENABLE_CLOCK_PORTJ gpio.setAFportJ(8,GPIO_AF3_TIM8); // pin D4, port J8, TIM8_CH1 } if (nchan==2) { ENABLE_CLOCK_PORTJ gpio.setAFportJ(10,GPIO_AF3_TIM8); // pin D11, port J10, TIM8_CH2 } break; case 15: RCC->APB2ENR |= RCC_APB2ENR_TIM15EN; // Enable TIM15 peripheral clock source ENABLE_CLOCK_PORTA if (nchan>=1) { gpio.setAFportA(2,GPIO_AF4_TIM15); // pin D2, port A2, TIM15_CH1 } if (nchan==2) { gpio.setAFportA(3,GPIO_AF4_TIM15); // pin D3, port A3, TIM15_CH2 } break; } __disable_irq(); tim->CR1 = 0; tim->PSC = psc; tim->ARR = arr; uint8_t capturePrescaler = 0; if (nchan>=1) CC1config(captureFilter,capturePrescaler,polarity[0]); if (nchan==2) CC2config(captureFilter,capturePrescaler,polarity[1]); switch(timnum) { case 1: NVIC_EnableIRQ(TIM1_CC_IRQn); break; case 3: NVIC_EnableIRQ(TIM3_IRQn); break; case 8: NVIC_EnableIRQ(TIM8_CC_IRQn); break; case 15: NVIC_EnableIRQ(TIM15_IRQn); break; } if (synchro) { if (timnum==1) {// master tim->CR2 = (tim->CR2 & ~TIM_CR2_MMS) | (1 << TIM_CR2_MMS_Pos); // Master mode selection : Enable (signal CNT_EN is used as trigger output (TRGO)) tim->SMCR = TIM_SMCR_MSM; // master slave mode } else { //slave tim->SMCR = (tim->SMCR & ~TIM_SMCR_TS) | (0 << TIM_SMCR_TS_Pos); // internal trigger ITR0 tim->SMCR = (tim->SMCR & ~TIM_SMCR_SMS) | (6 << TIM_SMCR_SMS_Pos);// slave mode selection : trigger mode tim->SMCR |= TIM_SMCR_MSM; } } if (upinter) { tim->DIER |= TIM_DIER_UIE; if (timnum==1) NVIC_EnableIRQ(TIM1_UP_IRQn); else if (timnum==8) NVIC_EnableIRQ(TIM8_UP_TIM13_IRQn); } __enable_irq(); } void TimerIC::setPeriod(double period) { psc = period*MAIN_CLOCK/0xFFFF; arr = period*MAIN_CLOCK/((psc+1))-1; clockPeriod = 1.0*(psc+1)/MAIN_CLOCK; tim->PSC = psc; tim->ARR = arr; } void TimerIC::start() { tim->CNT = 0; tim->CR1 |= TIM_CR1_CEN; } void TimerIC::stop() { tim->CR1 &= ~TIM_CR1_CEN; }
Le programme suivant effectue l'analyse d'un signal sur D10 au moyen du timer 1 (CH1).
#include "gpioconfig.h" #include "timeric.h" #define NCOUNT 100 uint16_t count_buffer1[2][NCOUNT]; uint16_t count_buffer2[2][NCOUNT]; uint16_t count_index1, count_index2; uint8_t nbuf1,nbuf2; TimerIC *timeric; uint16_t ccr1,lastCCR1; uint16_t ccr2,lastCCR2; void setup() { pinMode(13,OUTPUT); pinMode(12,OUTPUT); Serial.begin(115200); while (!Serial); lastCCR1 = lastCCR2 = 0; nbuf1 = nbuf2 = 0; count_index1 = count_index2 = 0; uint8_t polarity[2] = {POLARITY_BOTH,POLARITY_BOTH}; uint32_t period = 10000; timeric = new TimerIC(8,2,period,0,polarity,false,false); timeric->start(); } extern "C" void TIM8_CC_IRQHandler() { if (TIM8->SR & TIM_SR_CC1IF) { TIM8->SR &= ~ TIM_SR_CC1IF ; // Capture/compare 1 interrupt flag GPIOH->BSRR |= GPIO_BSRR_BS6; //digitalWrite(13,HIGH); ccr1 = TIM8->CCR1; if (ccr1>lastCCR1) count_buffer1[nbuf1][count_index1] =ccr1-lastCCR1; else count_buffer1[nbuf1][count_index1] =timeric->arr+1+(ccr1-lastCCR1); lastCCR1 = ccr1; count_index1++; if (count_index1==NCOUNT) { count_index1 = 0; if (nbuf1==1) nbuf1=0; else nbuf1=1; } GPIOH->BSRR |= GPIO_BSRR_BR6; //digitalWrite(13,LOW); } else if (TIM8->SR & TIM_SR_CC2IF) { TIM8->SR &= ~ TIM_SR_CC2IF ; // Capture/compare 2 interrupt flag GPIOJ->BSRR |= GPIO_BSRR_BS11; //digitalWrite(12,HIGH); ccr2 = TIM8->CCR2; if (ccr2>lastCCR2) count_buffer2[nbuf2][count_index2] =ccr2-lastCCR2; else count_buffer1[nbuf2][count_index2] =timeric->arr+1+(ccr2-lastCCR2); lastCCR2 = ccr2; count_index2++; if (count_index2==NCOUNT) { count_index2 = 0; if (nbuf2==1) nbuf2=0; else nbuf2=1; } GPIOJ->BSRR |= GPIO_BSRR_BR11; //digitalWrite(12,LOW); } } void loop() { delay(1000); Serial.println("------------------------------"); double mean1 = 0.0; double mean2 = 0.0; double deltaT; uint8_t nb1,nb2; if (nbuf1==1) nb1=0; else nb1=1; if (nbuf2==1) nb2=0; else nb2=1; for (int i=0; i<NCOUNT; i++) { deltaT = count_buffer1[nb1][i]*timeric->clockPeriod; Serial.print(deltaT,5); mean1 += deltaT; Serial.print(", "); deltaT = count_buffer2[nb2][i]*timeric->clockPeriod; Serial.println(deltaT,5); mean2 += deltaT; } mean1 /= NCOUNT; mean2 /= NCOUNT; Serial.println("------------------------------"); Serial.println(timeric->clockPeriod,5); Serial.print(mean1,5); Serial.print(", "); Serial.println(mean2,5); }
Les valeurs de CCR1 sont stockées dans un double tampon de taille NCOUNT. Lorsqu'un tampon est en cours de remplissage (dans l'ISR), l'autre tampon est disponible pour les calculs ou pour une transmission par port série. On procède de même pour CCR2 dans un autre double tampon. Dans la boucle loop, on calcule les durées correspondantes et leur moyenne.
La fonction de traitement de l'interruption (ISR) est la même pour les deux causes d'interruption, CC1 ou CC2. Il faut donc tester l'indicateur dans TIM8->SR pour connaître la cause et remplir le bon tampon. Afin de mesurer le temps d'exécution de l'ISR, nous avons ajouté une mise au niveau haut de la sortie D13 au début et une mise au niveau bas à la fin pour l'interruption déclenchée par CC1, de même avec D12 pour l'interruption déclenchée par CC2.
Le test est fait avec un signal carré de fréquence 500 kHz (rapport cyclique 1) sur D4 et un signal carré de fréquence 500 KHz (rapport cyclique 0,1) sur D11. Ces signaux sont générés par l'Analog Discovery 2. Voici les dernières valeurs affichées (en microsecondes) :
999.61667, 200.10833 1000.23333, 1800.35833 1000.23333, 199.64583 1000.07917, 1800.66667 1000.23333, 199.80000 1000.23333, 1800.66667 1000.07917, 199.64583 1000.23333, 1800.66667 1000.23333, 199.64583 1000.23333, 1800.82083 1000.07917, 199.64583 1000.23333, 1800.35833 999.77083, 200.10833 1000.07917, 1800.20417 1000.23333, 199.64583 ------------------------------ 0.15417 1000.14392, 1000.14854
La période d'horloge (avec prescaler) est 0,15417 microsecondes et elle définit la précision des mesures.
La durée d'exécution de l'ISR est d'environ 170 ns (le processeur M7 est cadencé à 480 MHz).
Si le même signal est envoyé sur D4 (CH1) et sur D11 (CH2), nous pouvons visualiser le déroulement des deux interruptions, celle causée par CC1 et celle causée par CC2. Celle de CC2 se fait en second, avec un délai de 600 ns.
Pour analyser des signaux de plus haute fréquence, on choisit une période de timer de 100 microsecondes. Par ailleurs, on peut analyser un seul signal en détectant les fronts montants sur CH1 et les fronts descendants sur CH2 :
uint8_t polarity[2] = {POLARITY_RISING,POLARITY_FALLING}; uint32_t period = 100; timeric = new TimerIC(8,2,period,0,polarity,false);
Voici le résultat pour un signal carré de fréquence 20 kHz et rapport cyclique 1 envoyé à la fois sur CH1 et sur CH2 :
50.00417, 50.00833 50.00833, 50.00833 50.00833, 50.00417 50.00833, 50.00833 50.00417, 50.00833 50.00417, 50.00833 50.00833, 50.00417 50.00833, 50.00833 50.00417, 50.00833 50.00417, 50.00833 50.00833, 50.00417 50.00833, 50.00417 ------------------------------ 0.00417 50.00704, 50.00717
Pour ce choix de période, PSC=0 donc la précision est la meilleures (1/240 microsecondes). Voici les signaux sur D12 et D13 :
import numpy as np from matplotlib.pyplot import * [t,u1,u2] = np.loadtxt("interrupt-1.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t*1e6,u1,label="CC1") plot(t*1e6,u2+4,label="CC2") grid() legend(loc="upper right") ylim(-1,8) ylabel("Volts",fontsize=16) xlabel(r"$t\ (\rm \mu s)$",fontsize=16)fig1.pdf
Compte tenu de la durée d'exécution de l'ISR, nous pouvons envisager d'analyser des signaux jusqu'à une fréquence de 1 MHz. Voici le résultat pour une fréquence de 100 kHz :
10.00000, 10.00000 10.00000, 10.00417 10.00417, 10.00000 10.00417, 10.00417 10.00000, 10.00000 10.00000, 10.00000 10.00000, 20.00417 10.00417, 10.00000 10.00000, 10.00417 10.00000, 10.00417 10.00417, 10.00000 10.00417, 10.00000 10.00000, 10.00000 10.00417, 10.00417 10.00000, 10.00000 10.00000, 10.00000 ------------------------------ 0.00417 10.00133, 10.10138
La durée entre deux fronts consécutifs détectés (soit montant soit descendant) est bien de 10 microsecondes mais il y a des erreurs sur CH2 : il arrive que la durée entre deux évènements CC2 soit 20 microsecondes.
[t,u1,u2] = np.loadtxt("interrupt-2.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t*1e6,u1,label="CC1") plot(t*1e6,u2+4,label="CC2") grid() legend(loc="upper right") ylim(-1,8) ylabel("Volts",fontsize=16) xlabel(r"$t\ (\rm \mu s)$",fontsize=16)fig2.pdf
Voici les résultats pour une fréquence de 200 kHz :
5.00000, 5.00000 5.00000, 5.00000 10.00417, 5.00000 5.00000, 5.00000 5.00000, 5.00417 5.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 5.00417, 5.00000 10.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 5.00000, 5.00000 ------------------------------ 0.00417 5.10075, 5.25071
Les erreurs (10 us au lieu de 5 us) sont plus fréquentes et affectent les deux voies. L'effet de ces erreurs sur la valeur moyenne est grand. Il faut observer les signaux sur D12 et D13 au moment où ces erreurs se produisent. Pour cela, nous utilisons un oscilloscope à grande mémoire interne (PicoScope 3204B, mémoire 8MS). Cette analyse révèle que les erreurs se produisent toutes les 1000 ms, qui est la durée du délai défini dans la boucle loop. Voici les signaux sur D12 et D13 au moment de la transmission des données par Serial :
[t,u1,u2] = np.loadtxt("interrupt-3.txt",skiprows=3,unpack=True) figure(figsize=(16,6)) plot(t,u1,label="CC1") plot(t,u2+4,label="CC2") grid() legend(loc="upper right") xlim(0,200) ylim(-1,8) ylabel("Volts",fontsize=16) xlabel(r"$t\ (\rm \mu s)$",fontsize=16)fig3.pdf
L'erreur se manifeste par le saut d'une interruption ou de deux interruptions, c'est-à-dire qu'une interruption ou deux consécutives ne sont pas exécutée, ce qui explique la valeur double de l'intervalle de temps ontenu de temps à autre. Sur CC2, il y des sauts de 2 interruptions.
Si nous enlevons les appels aux fonctions de Serial dans cette boucle, les erreurs disparaissent. La communication par port série utilise en effet des interruptions et il semble que certaines interruptions déclenchées par CC1 et CC2 ne soient pas exécutées.
Afin d'analyser d'un signal binaire, on utilise un timer avec CH1 et CH2. Le signal à analyser est envoyé sur les deux bornes correspondantes, par exemple D4 et D11 pour le timer 8. Les fronts montant du signal sont détectés sur CH1, les fronts descendants sur CH2. Alors que dans l'exemple précédent on calculait l'intervalle de temps entre deux front montants ou entre deux fronts descendants, il faut à présent calculer l'intervalle de temps entre un front montant (descendant) et le front descendant (montant) qui le précède. Considérons par exemple qu'un front montant soit détecté (évènement CC1). L'intervalle de temps avec le front descendant précédent se calcule en considérant la différence de CCR1 avec le CCR2 précédent. Si cette différence est positive, il s'agit de l'intervalle de temps recherché; si elle est négative, il faut lui ajouter ARR pour obtenir l'intervalle de temps. Comme précédemment, cette méthode ne fonctionne que si la période du timer est supérieure à celle du signal analysé. Les résultats sont transmis au PC en utilisant le protocole défini dans Échanges de données avec un Arduino. Les deux tampons transmis au PC contiennent :
Ainsi, l'élément d'indice i du tampon 0 donne l'intervalle de temps avec le front précédent et l'élément d'indice i du tampon 1 donne l'état juste après ce front.
Chacun des deux tampons est en fait un double tampon : l'un est en cours de remplissage par la fonction ISR alors que l'autre peut être transmis au PC. La taille d'un tampon est définie par la macro BUFSIZE. Les données transmises au PC sont :
La période du timer est transmise depuis le PC sous la forme d'un double (8 octets).
#include "gpioconfig.h" #include "timeric.h" TimerIC *timer8; // Timer 8 : D4 et D11 uint16_t tim8_ccr1,tim8_last_ccr1; uint16_t tim8_ccr2,tim8_last_ccr2; #define BUFSIZE 100 uint16_t buffer0[2][BUFSIZE]; // durée depuis le front précédent uint8_t buffer1[2][BUFSIZE]; // état (0 ou 1) juste après le front uint8_t indice, nbuf; double period = 1000; // transmission des données #define GET_DATA 10 #define SET_DATA 11 #define DATA_0_SIZE 200 // 2*BUFSIZE #define DATA_1_SIZE 100 // BUFSIZE #define DATA_2_SIZE 8 // period, double uint8_t data_2[DATA_2_SIZE]; bool data_0_ready = false; bool data_1_ready = false; bool data_0_request = false; bool data_1_request = false; extern "C" void TIM8_CC_IRQHandler() { if (TIM8->SR & TIM_SR_CC1IF) { // front montant TIM8->SR &= ~ TIM_SR_CC1IF ; // Capture/compare 1 interrupt flag GPIOH->BSRR |= GPIO_BSRR_BS6; //digitalWrite(13,HIGH); tim8_ccr1 = TIM8->CCR1; if (tim8_ccr1 > tim8_last_ccr2) buffer0[nbuf][indice] =tim8_ccr1-tim8_last_ccr2; else buffer0[nbuf][indice] =timer8->arr+1+(tim8_ccr1-tim8_last_ccr2); tim8_last_ccr1 = tim8_ccr1; buffer1[nbuf][indice] = 1; indice++; if (indice==BUFSIZE) { indice = 0; if (nbuf==1) nbuf=0; else nbuf=1; data_0_ready = true; data_1_ready = true; } GPIOH->BSRR |= GPIO_BSRR_BR6; //digitalWrite(13,LOW); } else if (TIM8->SR & TIM_SR_CC2IF) { // front descendant TIM8->SR &= ~ TIM_SR_CC2IF ; // Capture/compare 2 interrupt flag GPIOJ->BSRR |= GPIO_BSRR_BS11; //digitalWrite(12,HIGH); tim8_ccr2 = TIM8->CCR2; if (tim8_ccr2 > tim8_last_ccr1) buffer0[nbuf][indice] =tim8_ccr2-tim8_last_ccr1; else buffer0[nbuf][indice] =timer8->arr+1+(tim8_ccr2-tim8_last_ccr1); tim8_last_ccr2 = tim8_ccr2; buffer1[nbuf][indice] = 0; indice++; if (indice==BUFSIZE) { indice = 0; if (nbuf==1) nbuf=0; else nbuf=1; data_0_ready = true; data_1_ready = true; } GPIOJ->BSRR |= GPIO_BSRR_BR11; //digitalWrite(12,LOW); } } void setup() { pinMode(13,OUTPUT); pinMode(12,OUTPUT); Serial.begin(500000); while (!Serial); indice = 0; nbuf = 0; tim8_last_ccr1 = tim8_last_ccr2 = 0; uint8_t polarity[2] = {POLARITY_RISING,POLARITY_FALLING}; timer8 = new TimerIC(8,2,period,0,polarity,false,false); timer8->start(); } void get_data() { char n; while (Serial.available()<1) {}; n = Serial.read(); if (n==0) data_0_request = true; else if (n==1) data_1_request = true; } void send_data() { if ((data_0_ready)&&(data_0_request)) { data_0_ready = false; data_0_request = false; Serial.write((uint8_t *)(buffer0[abs(nbuf-1)]),DATA_0_SIZE); } if ((data_1_ready)&&(data_1_request)) { data_1_ready = false; data_1_request = false; Serial.write((uint8_t *)(buffer1[abs(nbuf-1)]),DATA_1_SIZE); } } void set_data() { char n; while (Serial.available()<1) {}; n = Serial.read(); if (n==2) { while (Serial.available()<DATA_2_SIZE) {}; Serial.readBytes(data_2,DATA_2_SIZE); memcpy(&period,data_2,DATA_2_SIZE); timer8->setPeriod(period); } } void read_serial() { char com; if (Serial.available()>0) { com = Serial.read(); if (com==GET_DATA) get_data(); else if (com==SET_DATA) set_data(); } } void loop() { read_serial(); send_data(); }
Le traitement des résultats se fait avec un script Python. La classe Arduino, permet d'échanger des données selon le protocole défini dans Échanges de données avec un Arduino. Elle est définie dans le fichier Arduino.py.
Le script suivant effectue l'acquisition de nblock contenus de tampons consécutifs, pour une période de timer choisie. La fonction setSignal permet d'obtenir, à partir du tableau des intervalles de temps et de celui des états, une série de lignes à tracer avec matplotlib.pyplot.plot.
from Arduino import * import matplotlib.pyplot as plt def getSignal(durees,etats, Tc): N = len(durees) x = [etats[0]] t = 0 temps = [0] for i in range(1,N): x.append(etats[i-1]) temps.append(t+durees[i]) x.append(etats[i]) temps.append(t+durees[i]) t += durees[i] return np.array(temps)*Tc,np.array(x) BUFSIZE = 100 period = 100 MAINCLOCK = 240 psc = int(period*MAINCLOCK/65535) Tc = (psc+1)/MAINCLOCK nblock = 2 N = nblock*BUFSIZE data0 = np.zeros(N) data1 = np.zeros(N) ard = Arduino('COM5',[BUFSIZE*2,BUFSIZE,8],baudrate=500000) ard.write_double(2,period) i = 0 for k in range(nblock): data0[i:i+BUFSIZE] = ard.read_int16_array(0,signed=False) data1[i:i+BUFSIZE] = ard.read_int8_array(1,signed=False) i += BUFSIZE ard.close() print("Période = %f us"%((data0[0]+data0[1])*Tc)) plt.figure() t,x = getSignal(data0,data1,Tc) plt.plot(t,x,"b-") plt.grid() np.savetxt("signal-1.txt",np.array([t,x]).T,header="t(us)\t etat") plt.show()
Voici le résultat pour un signal de 20 kHz avec une rapport cyclique de 0,2 :
[t,x] = np.loadtxt("signal-1.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t,x) grid() xlabel(r"$t\ (\rm \mu s)$",fontsize=16) ylim(-0.5,1.5) xlim(0,1000)fig4.pdf
Voici le résultat pour un signal de 50 kHz avec une rapport cyclique de 0,2 :
[t,x] = np.loadtxt("signal-2.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t,x) grid() xlabel(r"$t\ (\rm \mu s)$",fontsize=16) ylim(-0.5,1.5) xlim(0,1000)fig5.pdf
Voici le résultat pour un signal de 100 kHz avec une rapport cyclique de 0,2 :
[t,x] = np.loadtxt("signal-3.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t,x) grid() xlabel(r"$t\ (\rm \mu s)$",fontsize=16) ylim(-0.5,1.5) xlim(0,500)fig6.pdf
Voici le résultat pour un signal de 200 kHz avec une rapport cyclique de 0,2 :
[t,x] = np.loadtxt("signal-4.txt",skiprows=1,unpack=True) figure(figsize=(16,6)) plot(t,x) grid() xlabel(r"$t\ (\rm \mu s)$",fontsize=16) ylim(-0.5,1.5) xlim(0,200)fig7.pdf
À cette fréquence, les interruptions générées par les timers ne fonctionnent plus correctement, à cause de l'utilisation de Serial.