Atmel SAM4S

Atmel SAM4S sous Linux - TP n°2 : les E/S analogiques

L’objectif est de mettre en oeuvre les E/S analogiques de la carte Atmel SAM4S.

Remarque : Pour ce TP, vous aurez besoin de relier un adaptateur USB/RS232 sur le port série UART J7 et d’utiliser un logiciel d’émulation de terminal ($ picocom -b 115200 /dev/ttyUSB0 ou cutecom sous GNU/Linux ou TeraTerm sous Windows) sur le port série virtuel pour visualiser les messages de débuggage. On quitte picocom en utilisant la combinaison de touches Ctrl-A puis Ctrl-X.

Ressources

Introduction

Les E/S analogiques sont très utilisées dans les systèmes de contrôle industriel et d’acquisition de grandeurs physiques.

Pour acquérir une grandeur physique comme une température, un éclairement lumineux, une masse, etc … il faut un capteur. Un capteur est construit autour d’un transducteur. Un transducteur est un dispositif convertissant un signal physique en un autre, dans notre cas, un signal électrique. Ce signal électrique, image de la grandeur physique à mesurer, est un signal analogique qu’il faudra convertir. En effet, pour traiter ce signal à partir d’un système informatique, il faudra le convertir en signal numérique. C’est le rôle du CAN (ADC). Le principe de la conversion analogique-numérique est étudiée en cours de Sciences Physiques.

Remarque : il est possible d’utiliser des capteurs qui intègre déjà un convertisseur analogique-numérique et même une logique de contrôle permettant d’obtenir un “capteur numérique”. Les sorties de ce type de capteur sont le plus souvent : série TTL, 1-Wire ou I2C (TWI).

Dans l’autre sens, c’est le système informatique qui veut produire un signal analogique (un son par exemple) à partir d’un signal numérique. Il faut dans ce cas un CNA (DAC). Cela peut permettre aussi de commander une charge en gradateur (en faisant varier sa puissance). Voir la notion de triac.

Les E/S analogiques

La carte Atmel SAM4S dispose :

  • 16 canaux CAN (ADC) 12 bits/1 MHz (avec entrée différentielle, gain programmable et calibration automatique) fournissant 16 entrées analogiques (dont une est utilisée en interne pour le capteur de température)
  • 1 double canal CNA (DAC) 12 bits fournissant 2 sorties analogiques
  • un comparateur analogique

Remarque : les E/S ne sont pas toutes laissées disponibles.

On va tout d’abord mettre en oeuvre :

  • la capteur de température intégré à la carte (sur le canal 15) et le potentiomètre intégré en PB1 (sur le canal 5)

  • la sortie analogique 0 en PB13

Les fonctionnalités essentielles des E/S fournies par Atmel sont :

  • la sélection du mode d’entrée (simple ou différentielle)
  • la programmation du gain
  • le multiplexage des 16 entrées analogiques via les PIOx
  • le choix du mode de déclenchement de conversion (trigger hardware ou software, interne ou externe)

Le contrôle des E/S peut être réalisé :

  • directement en accèdant aux registres associés
  • par les fonctions de la bibliothèque logicielle ASF

Le fichier /usr/local/asf/sam/utils/cmsis/sam4s/include/sam4s16c.h fournit les adresses de base des circuit ADC et DAC :

#define ADC        ((Adc    *)0x40038000U) /**< \brief (ADC       ) Base Address */
#define DACC       ((Dacc   *)0x4003C000U) /**< \brief (DACC      ) Base Address */

Pour manipuler le convertisseur analogique-numérique, le fichier /usr/local/asf/sam/utils/cmsis/sam4s/include/component/component_adc.h déclare une structure Adc permettant d’accèder à ses registres :

typedef struct {
  __O  uint32_t ADC_CR;        /**< \brief (Adc Offset: 0x00) Control Register */
  __IO uint32_t ADC_MR;        /**< \brief (Adc Offset: 0x04) Mode Register */
  __IO uint32_t ADC_SEQR1;     /**< \brief (Adc Offset: 0x08) Channel Sequence Register 1 */
  __IO uint32_t ADC_SEQR2;     /**< \brief (Adc Offset: 0x0C) Channel Sequence Register 2 */
  __O  uint32_t ADC_CHER;      /**< \brief (Adc Offset: 0x10) Channel Enable Register */
  __O  uint32_t ADC_CHDR;      /**< \brief (Adc Offset: 0x14) Channel Disable Register */
  __I  uint32_t ADC_CHSR;      /**< \brief (Adc Offset: 0x18) Channel Status Register */
  __I  uint32_t Reserved1[1];
  __I  uint32_t ADC_LCDR;      /**< \brief (Adc Offset: 0x20) Last Converted Data Register */
  __O  uint32_t ADC_IER;       /**< \brief (Adc Offset: 0x24) Interrupt Enable Register */
  __O  uint32_t ADC_IDR;       /**< \brief (Adc Offset: 0x28) Interrupt Disable Register */
  __I  uint32_t ADC_IMR;       /**< \brief (Adc Offset: 0x2C) Interrupt Mask Register */
  __I  uint32_t ADC_ISR;       /**< \brief (Adc Offset: 0x30) Interrupt Status Register */
  __I  uint32_t Reserved2[2];
  __I  uint32_t ADC_OVER;      /**< \brief (Adc Offset: 0x3C) Overrun Status Register */
  __IO uint32_t ADC_EMR;       /**< \brief (Adc Offset: 0x40) Extended Mode Register */
  __IO uint32_t ADC_CWR;       /**< \brief (Adc Offset: 0x44) Compare Window Register */
  __IO uint32_t ADC_CGR;       /**< \brief (Adc Offset: 0x48) Channel Gain Register */
  __IO uint32_t ADC_COR;       /**< \brief (Adc Offset: 0x4C) Channel Offset Register */
  __I  uint32_t ADC_CDR[16];   /**< \brief (Adc Offset: 0x50) Channel Data Register */
  __I  uint32_t Reserved3[1];
  __IO uint32_t ADC_ACR;       /**< \brief (Adc Offset: 0x94) Analog Control Register */
  __I  uint32_t Reserved4[19];
  __IO uint32_t ADC_WPMR;      /**< \brief (Adc Offset: 0xE4) Write Protect Mode Register */
  __I  uint32_t ADC_WPSR;      /**< \brief (Adc Offset: 0xE8) Write Protect Status Register */
  __I  uint32_t Reserved5[5];
  __IO uint32_t ADC_RPR;       /**< \brief (Adc Offset: 0x100) Receive Pointer Register */
  __IO uint32_t ADC_RCR;       /**< \brief (Adc Offset: 0x104) Receive Counter Register */
  __I  uint32_t Reserved6[2];
  __IO uint32_t ADC_RNPR;      /**< \brief (Adc Offset: 0x110) Receive Next Pointer Register */
  __IO uint32_t ADC_RNCR;      /**< \brief (Adc Offset: 0x114) Receive Next Counter Register */
  __I  uint32_t Reserved7[2];
  __O  uint32_t ADC_PTCR;      /**< \brief (Adc Offset: 0x120) Transfer Control Register */
  __I  uint32_t ADC_PTSR;      /**< \brief (Adc Offset: 0x124) Transfer Status Register */
} Adc;

L’affectation des E/S analogiques pour la carte Atmel SAM4S sont définies dans le fichier header /usr/local/asf/sam/utils/cmsis/sam4s/include/pio/pio_sam4s16c.h :

/* ========== Pio definition for ADC peripheral ========== */
#define PIO_PA17X1_AD0     (1u << 17) /**< \brief Adc signal: AD0 */
#define PIO_PA18X1_AD1     (1u << 18) /**< \brief Adc signal: AD1 */
#define PIO_PC13X1_AD10    (1u << 13) /**< \brief Adc signal: AD10 */
#define PIO_PC15X1_AD11    (1u << 15) /**< \brief Adc signal: AD11 */
#define PIO_PC12X1_AD12    (1u << 12) /**< \brief Adc signal: AD12 */
#define PIO_PC29X1_AD13    (1u << 29) /**< \brief Adc signal: AD13 */
#define PIO_PC30X1_AD14    (1u << 30) /**< \brief Adc signal: AD14 */
#define PIO_PA19X1_AD2     (1u << 19) /**< \brief Adc signal: AD2/WKUP9 */
#define PIO_PA19X1_WKUP9   (1u << 19) /**< \brief Adc signal: AD2/WKUP9 */
#define PIO_PA20X1_AD3     (1u << 20) /**< \brief Adc signal: AD3/WKUP10 */
#define PIO_PA20X1_WKUP10  (1u << 20) /**< \brief Adc signal: AD3/WKUP10 */
#define PIO_PB0X1_AD4      (1u << 0)  /**< \brief Adc signal: AD4/RTCOUT0 */
#define PIO_PB0X1_RTCOUT0  (1u << 0)  /**< \brief Adc signal: AD4/RTCOUT0 */
#define PIO_PB1X1_AD5      (1u << 1)  /**< \brief Adc signal: AD5/RTCOUT1 */
#define PIO_PB1X1_RTCOUT1  (1u << 1)  /**< \brief Adc signal: AD5/RTCOUT1 */
#define PIO_PB2X1_AD6      (1u << 2)  /**< \brief Adc signal: AD6/WKUP12 */
#define PIO_PB2X1_WKUP12   (1u << 2)  /**< \brief Adc signal: AD6/WKUP12 */
#define PIO_PB3X1_AD7      (1u << 3)  /**< \brief Adc signal: AD7 */
#define PIO_PA21X1_AD8     (1u << 21) /**< \brief Adc signal: AD8 */
#define PIO_PA22X1_AD9     (1u << 22) /**< \brief Adc signal: AD9 */
#define PIO_PA8B_ADTRG     (1u << 8)  /**< \brief Adc signal: ADTRG */

/* ========== Pio definition for DACC peripheral ========== */
#define PIO_PB13X1_DAC0    (1u << 13) /**< \brief Dacc signal: DAC0 */
#define PIO_PB14X1_DAC1    (1u << 14) /**< \brief Dacc signal: DAC1 */
#define PIO_PA2C_DATRG     (1u << 2)  /**< \brief Dacc signal: DATRG */

Et celles des registres dans le fichier header /usr/local/asf/sam/utils/cmsis/sam4s/include/instance/instance_adc.h.

Remarque : il vous faudra inclure les fichiers d’en-têtes (header) asf.h, adc.h et dacc.h.

Fonctionnement

La conversion d’une entrée analogique peut être déclenchée logiciellement (software trigger) ou matériellement (hardware trigger) :

  • logiciel : il suffit d’écrire un 1 dans le bit START du registre de contrôle (ADC_CR) pour démarrer une conversion (ou en appelant la fonction adc_start()). En écrivant un 1 dans le bit SWRST, on arrête la conversion en provoquant un reset du circuit ADC.
  • matériel : on peut utiliser une des 3 sorties du circuit TC (Timer Counter), un des 4 canaux PWM ou un signal externe sur l’entrée ADTRG. On configurera alors les champs TRGSEL et TRGEN du registe ADC_MR (Mode Register).

Les 16 canaux analogiques sont multiplexés par des lignes du PIO. Les entrées sont activées en écrivant dans le registre CER (Channel Enable Register). Elles peuvent être configurées en entrée simple (bit DIFF à 0 dans le registre ADC_COR) ou en mode différentielle (bit DIFF à 1 dans ADC_COR).

Il est possible de configurer un gain d’amplification de 1/2, 1, 2 ou 4 par les bits GAINx du registre ADC_CGR (Channel Gain register) pour chaque canal. Avec le bit ANACH du registre MR, on peut choisir d’appliquer la configuration à l’ensemble des canaux ou pas.

Il reste à configurer les 2 temps de fonctionnement de l’ADC :

  • tracking time : c’est le temps entre la sélection d’un canal et le démarrage d’une conversion. On le configure par le champ TRACKTIM du registre MR.
  • conversion time : c’est le temps de conversion d’un signal analogique. Après chaque démarrage d’une conversion, l’ADC attend un certain nombre de cycles d’horloge (hold time) avant de changer la sélection d’un canal. On le configure par le champ STARTUP (Startup Time) du registre MR.

La configuration des temps est basée sur la fréquence d’horloge du circuit. L’ADC utilise l’horloge ADCCLK pour cadencer ces conversions. La fréquence d’horloge est sélectionnée par le champ PRESCAL du registre MR. Elle est comprise entre fréquence d’horloge/2 (avec PRESCAL = 0) et fréquence d’horloge/512 (avec PRESCAL = 255 (0xFF)).

Le mode de fonctionnement de l’ADC se configure donc à partir du registre MR :

Quand une conversion est terminée, la valeur convertie est stockée dans le registre ADC_CDRx (Channel Data register) où x correspond au canal et dans le registre ADC_LCDR (Last Converted Data register) pour toujours conserver la dernière valeur convertie.

Les bits EOCx (End Of Conversion) et DRDY (Data Ready) du registre ADC_ISR (Interrupt Status register) sont alors mis à 1. Ils peuvent servir à mettre en oeuvre un gestion des interruptions. D’autres interruptions sont disponibles (buffer plein, …).

Lire le registre ADC_CDRx (Channel Data register) provoque la remise à zéro du bit EOC du canal correspondant. Et lire le registre ADC_LCDR effacera alors le bit DRDY.

Les registres du CAN/CNA sont

  • ADC Control Register : 0x40038000

  • DACC Control Register : 0x4003C000

Pour arrêter/démarrer une conversion CAN, il faut écrire dans le registre Control Register :

// Démarre la conversion
ADC->ADC_CR = ADC_CR_START; // bit 1 START soit (1 << 1)

// ...

// Stoppe la conversion
ADC->ADC_CR = ADC_CR_SWRST; // bit 0 SWRST soit (1 << 0)

Remarque : le bit 3 AUTOCAL de ce registre permet l’autocalibration.

Pour lire une valeur convertie sur 12 bits, il suffit de lire le registre Channel Data Register CDRx où x représente le numéro de canal :

uint32_t ul_data = 0;

// lit la valeur du canal 15 (capteur de température interne)
ul_data = *(ADC->ADC_CDR + 15); 

// ou la dernière valeur convertie
//ul_data = ADC->ADC_LCDR;

Attention : avant de démarrer une conversion et de lire une valeur, il faut INITIALISER le CAN (ADC) !

La partie la plus complexe est l’initialisation et la gestion des différents modes du CAN (ADC). La lecture et l’étude de la documentation est indispensable.

Documentation : Chapitre 42 Analog-to-Digital Converter (ADC) page 1083 et Chapitre 43 Digital-to-Analog Converter Controller (DACC) page 1121 du atmel-11100-32-bit-cortex-m4-microcontroller-sam4s_datasheet.pdf

Pour initialiser le CAN (ADC) pour le capteur de température, on peut réaliser la séquence suivante :

// Active l'horloge associé à l'ADC
pmc_enable_periph_clk(ID_ADC);

// Réinitialise le contrôleur
ADC->ADC_CR = ADC_CR_SWRST;

// Réinitialise le registe de configuration du mode 
ADC->ADC_MR = 0;

// Configure la fréquence du convertisseur (valeur PRESCAL) à partir de l'horloge
/*
 * Formula: ADCClock = MCK / ( (PRESCAL+1) * 2 )
 * For example, MCK = 64MHZ, PRESCAL = 4, then:
 * ADCClock = 64 / ((4+1) * 2) = 6.4MHz;
 *
 * Formula:
 *     Startup  Time = startup value / ADCClock
 *     Startup time = 64 / 6.4MHz = 10 us
 */
uint32_t ul_prescal = sysclk_get_cpu_hz() / (2 * 6400000) - 1;
ADC->ADC_MR |= ADC_MR_PRESCAL(ul_prescal) | ADC_STARTUP_TIME_4;

// Active le canal 15 du capteur de température
//ADC->ADC_CHER = ADC_CHER_CH15;
ADC->ADC_CHER = 1 << 15;

// Active le capteur de température
ADC->ADC_ACR |= ADC_ACR_TSON;

// Démarre la conversion
ADC->ADC_CR = ADC_CR_START; //(0x1u << 1)

Les fonctions de la bibliotèque logicielle ASF

Pour simplifier la programmation directe des ADC/DAC, la bibliothèque logicielle ASF fournit des fonctions de contrôle : http://asf.atmel.com/docs/latest/sam4s/html/group__sam__drivers__adc__group.html et http://asf.atmel.com/docs/latest/sam4s/html/group__sam__drivers__dacc__group.html.

Pour initialiser le CAN (ADC) pour le capteur de température, on peut réaliser la séquence suivante :

// Active l'horloge associé à l'ADC
pmc_enable_periph_clk(ID_ADC);

// Initialise l'ADC
adc_init(ADC, sysclk_get_cpu_hz(), 6400000, ADC_STARTUP_TIME_4);

// Vérifie la configuration
//adc_check(ADC, sysclk_get_cpu_hz());

// Active le canal 15 du capteur de température
adc_enable_channel(ADC, ADC_TEMPERATURE_SENSOR);

// Active le capteur de température
adc_enable_ts(ADC);

// Démarre la conversion
adc_start(ADC);

Pour lire une valeur convertie sur 12 bits, il suffit de lire le canal correspondant :

uint32_t ul_data = 0;

ul_data = adc_get_channel_value(ADC, ADC_TEMPERATURE_SENSOR); 

Manipulations

Lire une entrée analogique

On désire acquérir la température à partir du capteur analogique intégré à la carte Atmel SAM4S et relié sur l’entrée 15.

On crée une première version du programme principal qui utilise les directement registres de l’ADC :

$ vim main.c
// Acquérir une tension analogique sur la carte Atmel SAM4S 
// version 1 : à partir des registres de l'ADC

#include <asf.h>
#include <adc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STRING_EOL    "\r"
#define STRING_HEADER \
        "-- Exemple ADC - "BOARD_NAME" --\r\n" \
        "-- "__DATE__" "__TIME__" --"STRING_EOL

/** ADC */
/** Tension de référence en mv. */
#define MVOLT_REF       (3300)
/** Tension de référence en v. */
#define VOLT_REF        (3.3)
/** Résolution 12-bits */
#define MAX_DIGITAL     (4095)

/**
 *  Mes variables globales
 */
volatile uint32_t g_ul_ms_ticks     = 0;
uint32_t data                       = 0;

/**
 *  Mes fonctions
 */
void print(uint32_t ul_data, bool temperature);
static void configure_uart(void);
static void my_init(void);
static void mdelay(uint32_t ul_dly_ticks);

/**
 * \brief Programme principal
 *
 */
int main (void)
{
    uint32_t ul_data = 0;    
    
    my_init();
   
    while (1) 
    {
        // Active l'horloge associé à l'ADC
        pmc_enable_periph_clk(ID_ADC);
    
        // Réinitialise le contrôleur
        ADC->ADC_CR = ADC_CR_SWRST;

        // Réinitialise Reset Mode
        ADC->ADC_MR = 0;

        // Initialise l'ADC
        /*
         * Formula: ADCClock = MCK / ( (PRESCAL+1) * 2 )
         * For example, MCK = 64MHZ, PRESCAL = 4, then:
         * ADCClock = 64 / ((4+1) * 2) = 6.4MHz;
         *
         * Startup  Time = startup value / ADCClock
         * Startup time = 64 / 6.4MHz = 10 us
         */
        uint32_t ul_prescal = sysclk_get_cpu_hz() / (2 * 6400000) - 1;
        ADC->ADC_MR |= ADC_MR_PRESCAL(ul_prescal) | ADC_STARTUP_TIME_4;

        // Active le canal 15 du capteur de température
        ADC->ADC_CHER = ADC_CHER_CH15;
        //ADC->ADC_CHER = 1 << 15;

        // Active le capteur de température
        ADC->ADC_ACR |= ADC_ACR_TSON;

        // Démarre la conversion
        ADC->ADC_CR = ADC_CR_START; //(0x1u << 1)

        // Lit la valeur de la conversion d'un canal (ici le 15)
        ul_data = *(ADC->ADC_CDR + 15);
        print(ul_data, 1);
        
        printf("\r\n");
        mdelay(500); /* Attend 500ms */
    }

    return 0;
}

/**
 * \brief Affichage
 * 
 */
void print(uint32_t ul_data, bool temperature)
{
    float f_tension = 0;
    uint32_t ul_tension;
    float f_temp = 0;
    char tampon[128]; // pour l'affichage
    
    // La donnée brute sur 12 bits 
    printf("Data : %ld (0x%04X)\r\n", ul_data, (unsigned int)(ul_data & 0x00000FFF));
        
    // Conversion en tension
    f_tension = ((float)ul_data * VOLT_REF) / (float)MAX_DIGITAL;

    // Conversion en chaîne de caractères pour l'affichage
    // Affichage en V
    sprintf(tampon, "%.2f", f_tension);
    printf("Tension : %s V\r\n", tampon);
    // Affichage en mV
    ul_tension = (uint32_t)(f_tension*1000.);
    printf("Tension : %ld mV\r\n", ul_tension);
    
    if(temperature == 1)
    {
        // Conversion en température
        /* Utilise une multiplication (*0.21276) au lieu d'une division (/4.7). */
        f_temp = (float)(ul_tension - 1440) * 0.21276 + 27.0;
        sprintf(tampon, "%.1f", f_temp);
        printf("Temperature : %s Celcius\r\n", tampon);
    }
}

/* ... */

Code source : tp2-temperature.zip

Test :

$ make
$ ./run.sh xxx_flash.elf 

On crée une deuxième version qui utilise l’API ADC de la bibliothèque ASF.

On améliore le programme en intégrant une gestion d’interruption basée sur la fin de conversion. Il faut alors définir le gestionnaire d’interruption ADC_Handler().

$ vim main.c
// Acquérir une tension analogique sur la carte Atmel SAM4S 
// version 2 : à partir des fonctions ASF

#include <asf.h>
#include <adc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STRING_EOL    "\r"
#define STRING_HEADER \
        "-- Exemple ADC - "BOARD_NAME" --\r\n" \
        "-- "__DATE__" "__TIME__" --"STRING_EOL

/** ADC */
/** Tension de référence en mv. */
#define MVOLT_REF       (3300)
/** Tension de référence en v. */
#define VOLT_REF        (3.3)
/** Résolution 12-bits */
#define MAX_DIGITAL     (4095)

/**
 *  Mes variables globales
 */
volatile uint32_t g_ul_ms_ticks     = 0;
uint32_t data                       = 0;

/**
 *  Mes fonctions
 */
void print(uint32_t ul_data, bool temperature);
static void configure_uart(void);
static void my_init(void);
static void mdelay(uint32_t ul_dly_ticks);

/**
 * \brief Programme principal
 *
 */
int main (void)
{
    uint32_t ul_data = 0;    
    
    my_init();
   
    while (1) 
    {
        // Version 2 :
        // Active l'horloge associé à l'ADC
        pmc_enable_periph_clk(ID_ADC);
        
        // Initialise l'ADC
        // Formula: ADCClock = MCK / ( (PRESCAL+1) * 2 )
        // For example, MCK = 64MHZ, PRESCAL = 4, then:
        // ADCClock = 64 / ((4+1) * 2) = 6.4MHz;
        // Startup  Time = startup value / ADCClock
        // Startup time = 64 / 6.4MHz = 10 us
        adc_init(ADC, sysclk_get_cpu_hz(), 6400000, ADC_STARTUP_TIME_4);
        
        // Active le canal 15 du capteur de température
        adc_enable_channel(ADC, ADC_TEMPERATURE_SENSOR);

        // Active le capteur de température
        adc_enable_ts(ADC);

        // Active les interruption de l'ADC (cf. ADC_Handler)
        NVIC_EnableIRQ(ADC_IRQn);

        // Démarre la conversion
        adc_start(ADC);

        // Active l'interruption
        //adc_enable_interrupt(ADC, ADC_ISR_DRDY); // (0x1u << 24)
        adc_enable_interrupt(ADC, ADC_ISR_EOC15);
        
        printf("\r\n");
        mdelay(500); /* Attend 500ms */
    }

    return 0;
}

/**
 * \brief Le gestionnaire d'interruption de l'ADC
 */
void ADC_Handler(void)
{
    // Vérifie l'état de la conversion canal 15
    if ((adc_get_status(ADC) & ADC_ISR_EOC15) == ADC_ISR_EOC15)
    {
        // Vérifie que la donnée est prête à être lue
        if (((adc_get_status(ADC) & ADC_ISR_DRDY) == ADC_ISR_DRDY))
        {
            // Lit la valeur de la conversion d'un canal (ici le 15)
            data = adc_get_channel_value(ADC, ADC_TEMPERATURE_SENSOR);         
            // Lit la dernière valeur
            //data = adc_get_latest_value(ADC);
            print(data, 1);
        }
    }
}

Attention : Il faut ajouter sam/drivers/adc/adc.c dans la liste des fichiers à compiler (variable CSRCS du fichier config.mk) et sam/drivers/adc dans la liste des chemins des fichiers headers (variable INC_PATH du fichier config.mk).

Remarque : la fonction print() permet d’afficher dans la console de débugage. Si l’argument temperature est à 1, elle convertit ul_data et affiche la température correspondante. La formule de conversion de la température est décrite à la page 1183 du datasheet. Le code source est fourni en Annexe.

À vous de jouer :

  1. On désire acquérir la tension analogique aux bornes du potentiomètre intégré à la carte Atmel SAM4S :

Le potentiomètre est relié sur l’entrée PB1 (canal 5).

Écrire les deux versions du programme de test.

Commande d’une sortie analogique

On désire piloter la sortie analogique n°0 (PB13) de la carte Atmel SAM4S.

Attention : la tension de sortie est comprise entre (1/6) × VADVREF et (5/6) × VADVREF. Pour VADVREF = 3.3V, cela donne une plage de 0.55V et 2.75V.

On crée une version qui utilise l’API DACC de la bibliothèque ASF :

$ vim main.c
// Produire un signal analogique sur la carte Atmel SAM4S 
// version : à partir des fonctions ASF

#include <asf.h>
#include <dacc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STRING_EOL    "\r"
#define STRING_HEADER \
        "-- Exemple DAC - "BOARD_NAME" --\r\n" \
        "-- "__DATE__" "__TIME__" --"STRING_EOL

/** DAC */
#define DACC_CHANNEL        0 // (PB13)
#define DACC_ANALOG_CONTROL (DACC_ACR_IBCTLCH0(0x02) | DACC_ACR_IBCTLCH1(0x02) | DACC_ACR_IBCTLDACCORE(0x01))
/** Tension de référence en v. */
#define VOLT_REF        (3.3)
/** Résolution 12-bits */
#define MAX_DIGITAL     (4095)

/**
 * \brief Programme principal
 *
 */
int main (void)
{
    uint32_t status;
    float f_tension;
    uint32_t ul_data = 0;
    
    my_init();
   
    while (1) 
    {        
        // Version 2 :
        // Active l'horloge associé au DACC
        sysclk_enable_peripheral_clock(ID_DACC);

        // Réinitialise les registres
        dacc_reset(DACC);

        // Configure le mode de transfert 
        // (0 = Half-word transfer [16bits -> une donnée] ou 1 = Word transfer [32bits -> 2 données])
        dacc_set_transfer_mode(DACC, 0);

        // Initialise le DAC
        /* Timing:
         * refresh        - 0x08 (1024*8 dacc clocks)
         * max speed mode -    0 (disabled)
         * startup time   - 0x10 (1024 dacc clocks)
         */
        dacc_set_timing(DACC, 0x08, 0, 0x10);

        // Désactive le TAG et sélectionne la sortie analogique
        // la sélection du canal se fait par le registre MR (USER_SEL) 
        // et non par les bits 12 et 13 de la donnée (mode TAG)
        dacc_set_channel_selection(DACC, DACC_CHANNEL);

        // Active la sortie analogique
        dacc_enable_channel(DACC, DACC_CHANNEL);

        // Controle les performances par rapport à la consommation
        dacc_set_analog_control(DACC, DACC_ANALOG_CONTROL);
        
        f_tension = 1.4;
        // Conversion en data
        ul_data = (uint32_t)(((f_tension * (float)MAX_DIGITAL) / VOLT_REF) - (0.55 * (float)MAX_DIGITAL / VOLT_REF));
        // La donnée brute sur 12 bits 
        printf("Data : %ld (0x%04X)\r\n", ul_data, (unsigned int)(ul_data & 0x00000FFF));
       
        status = dacc_get_interrupt_status(DACC);

        // Écrit une valeur sur la sortie analogique
        if ((status & DACC_ISR_TXRDY) == DACC_ISR_TXRDY) 
        {
            dacc_write_conversion_data(DACC, ul_data);
        }
        
        printf("\r\n");
        mdelay(500); /* Attend 500ms */
    }

    return 0;
}

Attention : Il faut ajouter sam/drivers/dacc/dacc.c dans la liste des fichiers à compiler (variable CSRCS du fichier config.mk) et sam/drivers/dacc dans la liste des chemins des fichiers headers (variable INC_PATH du fichier config.mk).

Test :

$ make
$ ./run.sh xxx_flash.elf 

À vous de jouer :

  1. Écrire un programme permettant de commander la sortie AUDIO_OUTL (PB14).

Travail demandé

  1. Cabler un montage exploitant une entrée et une sortie analogiques.

  2. Écrire un programme de démonstration du montage réalisé.

Bilan

Vous devez être capable de répondre aux questions suivantes :

  1. Définir une E/S analogique, le rôle d’un CAN/CNA.

  2. Quel est le rôle d’un CAN ? d’un CNA ?

  3. Définir le terme de transducteur.

  4. Décrire la méthode de programmation d’une E/S analogique.

  5. Quel est le nom du registre qui permet de sélectionner un canal ?

  6. Décrire le principe de gestion d’une interruption matérielle ?

  7. Qu’est-ce que le quantum dans une conversion analogique-numérique ?

  8. Combien de tensions analogiques différentes peut-on obtenir avec un CAN 12 bits ?

  9. En électronique numérique, qu’est-ce qu’une horloge ?

  10. Qu’est-ce qu’un trigger ?

Glossaire

Information analogique
Elle se caractérise par les grandeurs physiques évoluant de manière continue en fonction du temps. La grandeur physique peut prendre une infinité de valeur. Cette information peut être périodique ou non périodique. Lire : cours_capteurs.pdf
CAN
CAN est un convertisseur analogique-numérique (ou en anglais ADC pour Analog to Digital Converter) dont la fonction est de traduire une grandeur analogique en une valeur numérique codée sur plusieurs bits.
CNA
CNA est un convertisseur numérique-analogique (ou en anglais, DAC pour Digital to Analogic) dont la fonction est de transformer une valeur numérique (codée sur plusieurs bits) en une valeur analogique proportionnelle à la valeur numérique codée.
Registre
Un registre est un emplacement de mémoire interne à un circuit intégré (un processeur, microcontrôleur, …). Leur taille est limitée généralement 8, 16, 32 ou 64 bits. Sur certains circuits complexes, leur capacité peut atteindre quelques dizaines d’octets. On y accède comme une case mémoire en les manipulant très souvent à partir de masque. Historiquement, les registres étaient réalisés à partir de bascules D.
Interruption
Une interruption matérielle (Interrupt ReQuest ou IRQ) est une interruption déclenchée par un périphérique d’entrée-sortie d’un microprocesseur ou d’un microcontrôleur. Les interruptions matérielles sont utilisées en informatique lorsqu’il est nécessaire de réagir en temps réel à un événement asynchrone, ou bien, de manière plus générale, afin d’économiser le temps d’exécution lié à une boucle de consultation (polling loop). L’autre sorte d’interruption est l’interruption logicielle (software interrupt), généralement déclenchée par une instruction spéciale du processeur. Les deux sortes d’interruptions déclenchent un basculement de contexte vers le gestionnaire d’interruption (handler) associé.

Annexe : les fonctions print(), my_init() et mdelay()

La fonction my_init() fournie permet d’initialiser la carte (board_init()), le système d’horloge (sysclk_init()) et le port série UART (configure_uart()). Une fonction mdelay() fournit un moyen de réaliser des temporisations d’attente en millisecondes.

#define STRING_EOL    "\r"
#define STRING_HEADER \
        "-- Exemple LED Bleue - "BOARD_NAME" --\r\n" \
        "-- "__DATE__" "__TIME__" --"STRING_EOL

/** UART Interface */
#define CONF_UART            CONSOLE_UART
#define CONF_UART_BAUDRATE   115200
#define CONF_UART_PARITY     UART_MR_PAR_NO
#define CONF_UART_CHAR_LENGTH  US_MR_CHRL_8_BIT
#define CONF_UART_STOP_BITS    US_MR_NBSTOP_1_BIT

volatile uint32_t g_ul_ms_ticks = 0;

static void my_init(void);
static void configure_uart(void);
static void mdelay(uint32_t ul_dly_ticks);

/**
 * \brief Affichage et conversion
 * 
 */
void print(uint32_t ul_data, bool temperature)
{
    float f_tension = 0;
    uint32_t ul_tension;
    float f_temp = 0;
    char tampon[128]; // pour l'affichage
    
    // La donnée brute sur 12 bits 
    printf("Data : %ld (0x%04X)\r\n", ul_data, (unsigned int)(ul_data & 0x00000FFF));
        
    // Conversion en tension
    f_tension = ((float)ul_data * VOLT_REF) / (float)MAX_DIGITAL;

    // Conversion d'un float en chaîne de caractères pour l'affichage
    // Affichage en V
    sprintf(tampon, "%.2f", f_tension);
    printf("Tension : %s V\r\n", tampon);
    // Affichage en mV
    ul_tension = (uint32_t)(f_tension*1000.);
    printf("Tension : %ld mV\r\n", ul_tension);
    
    if(temperature == 1)
    {
        // Conversion en température
        /* Utilise une multiplication (*0.21276) au lieu d'une division (/4.7). */
        f_temp = (float)(ul_tension - 1440) * 0.21276 + 27.0;
        sprintf(tampon, "%.1f", f_temp);
        printf("Temperature : %s Celcius\r\n", tampon);
    }
}

/**
 *  Initialise
 */
static void my_init(void)
{
    board_init();
    sysclk_init();

    /* Initialise le port uart */
    configure_uart();

    /* Envoie une chaîne de caractères */
    puts(STRING_HEADER);
    
    /* Configure les tics à 1 ms */
    if (SysTick_Config(sysclk_get_cpu_hz() / 1000)) 
    {
        puts("Erreur systick configuration\r");
        while (1);
    }
}

/**
 *  \brief Gestionnaire d'évènement pour les tics d'interruptions système
 *
 *  Incremente le compteur g_ul_ms_ticks
 *  
 */
void SysTick_Handler(void)
{
    g_ul_ms_ticks++;
}

/**
 * \brief Temporisation d'attente en millisecondes (basée sur les tics systèmes du microcontrôleur)
 *
 * \param ul_dly_ticks  temps d'attente en millisecondes
 */
static void mdelay(uint32_t ul_dly_ticks)
{
    uint32_t ul_cur_ticks;

    ul_cur_ticks = g_ul_ms_ticks;
    while ((g_ul_ms_ticks - ul_cur_ticks) < ul_dly_ticks);
}

/**
 *  Configure UART.
 */
static void configure_uart(void)
{
    const usart_serial_options_t uart_serial_options = 
    {
        .baudrate = CONF_UART_BAUDRATE,
        .charlength = CONF_UART_CHAR_LENGTH,
        .paritytype = CONF_UART_PARITY,
        .stopbits = CONF_UART_STOP_BITS,
    };

    sysclk_enable_peripheral_clock(CONSOLE_UART_ID);
    stdio_serial_init(CONF_UART, &uart_serial_options);
}

Retour au sommaire