Site icon AranaCorp

Coder un Arduino en C embarqué

Pour aller plus loin avec les cartes Arduino, il est possible de les programmer comme n’importe quel système embarqué. Les microcontrôleurs Arduino sont des outils puissants pour apprendre la programmation et l’électronique. Pour aller plus loin et améliorer ses compétences en programmation et en compréhension des systèmes embarqués, il faut se détacher des API proposées par Arduino IDE et utiliser tous le potentiel du microcontrôleur.

Les systèmes embarqués

Pour pouvoir programmer l’Arduino au plus proche de ses composants, il faut se renseigner sur son architecture et les fiches technique de chacun de ses composants.

source : researchgate

Dans un premier temps, voici les données du microcontrôleur qui vont nous intéresser

AVR 8-bit microcontroller

Schéma de brochage

Pour contrôler les entrées sorties d’un microcontrôleur, on utilise les registres qui enregistre en mémoire l’état des broches.

Sur le schéma de brochage de l’Arduino, on retrouve les liens entre les broches de l’Arduino et de l’ATmega

Physical PinPort PinPin InterruptsPin functionSerial comIDE Id
1PC6PCINT14RESET
2PD0PCINT16RXDD0
3PD1PCINT17TXDD1
4PD2PCINT18INT0D2
5PD3PCINT19INT1/OC2BD3
6PD4PCINT20XCK/T0D4
11PD5PCINT21T1/OC0BD5
12PD6PCINT22AIN0/OC0AD6
13PD7PCINT23AIN1D7
14PB0PCINT0CLK0/ICP1D8
15PB1PCINT1OC1AD9
16PB2PCINT2OC1BSSD10
17PB3PCINT3OC2AMOSID11
18PB4PCINT4MISOD12
19PB5PCINT5built-in LEDSCKD13
23PC0PCINT8ADC014/A0
24PC1PCINT9ADC115/A1
25PC2PCINT10ADC216/A2
26PC3PCINT11ADC317/A3
27PC4PCINT12ADC4SDA18/A4
28PC5PCINT13ADC5SCL19/A5

Par exemple, on peut identifier que la broche D13 de l’Arduino correspond à la broche 19 de l’ATmega, dont l’état est liés au bit 5 du registre PORTB, elle est relié à la LED embarquée sur l’Arduino et est utilisée pour la communication SPI. On peut détecter son changement d’état avec l’interruption PCINT5.

Opérations binaires

Les opérations binaires sont souvent utilisées dans la programmation bas niveau de système embarqué.

La combinaison de ces opérations permet de travailler simplement avec les registres en sélectionnant les bits à mettre à 1 ou à 0.

int REG = B00000000;
REG |= ( 1 << 6 ); # set bit 6 at 1 without changing the others
# REG = B01000000

REG &= ~( 1 << 4 ); #set bit 4 at 0 without changing the others

REG ^= (1 << 7 ); #toggle bit 7 without changing the others

bool bit = (REG >> 3) & 1 ; #get value of bit 3

Changer l’état d’une broche (ch13)

Pour changer l’état d’un entrée sortie, il faut déjà spécifier la direction entrée ou sortie dans le registre DDRxn(13.4.3-6-9)

DDRD |= (0 << DDRD2) ||  (0 << DDRD4) #set pin D2 and D4 as inputs
DDRB |= ( 1 << DDRB5 ) # set pin D13 as output

Puis définir l’état de la broche dans le registre correspondant PORTxn

PORTB |= ( 1 << PORTB5 ) # set pin D13 at HIGH

Utiliser les Timer (ch14)

Le registre TCNTx contient la valeur du timer/counter, le registre TCCRxA et TCCRxB sont les registres de contrôles. OCRxA rt OCRxB contiennent les valeurs de registre à comparer. TIMSKx contient le masque d’interruption et TIFRx, quant à lui, contient les flags d’activation.

Une interruption est généré à chaque fois que le compteur TCNTx atteint la valeur OCRxA. Nous allons calculer, les valeurs prescale et autoreload nécessaires pour faire un timer avec le timer0.

Si nous souhaitons un timer à 300ms, il est possible de définir une période de 2ms(500Hz) à laquelle nous incrémentons un compteur. On peut ensuite calculer la valeur seuil du compteur TCNT0 (OCR0A), en divisant la fréquence du CPU (16MHz) par le facteur 256 (CS02=1) et par la fréquence (500Hz)

OCR0A = 16MHz/( 256 * 500Hz ) -1 = 124

La valeur 124 peut être codé sur les 8bits du registre OCR0A sans perte de précision. Pour obtenir 300ms avec ce timer à 2ms, il faut incrémenter le compteur 150 fois

300ms = 150 * 2ms

Il peut exister plusieurs solutions pour une même période. Soit on choisit un facteur de division de l’horloge CPU et on calcule la valeur cible du compteur, soit on choisit la valeur max du compteur et on calcul le facteur de division de l’horloge.

OCRxA = freq_cpu / ( freq_timer * prescale) -1 = period_timer / (period_cpu * prescale) -1

// Fcpu = 16MHz
// Td = 300 ms
// inter_at = 2ms -> 500Hz
// 300/2 = 150 tic

unsigned long elapsedTime, previousTime;

void onTimer(){
  elapsedTime=millis()-previousTime;
  Serial.print(F(" - "));Serial.print(elapsedTime);Serial.println(F("ms"));
  previousTime=millis();
}

void setup(){
  Serial.begin(9600);
  pinMode(13, OUTPUT);

  // clkio = 16MHz
  cli();           // disable all interrupts
  //TCCR0B = (0<<WGM02)|(0<<CS02)|(0<<CS01)|(1<<CS00); // select clock src clkti =16MHz
  TCCR0B = (0<<WGM02)|(1<<CS02)|(0<<CS01)|(0<<CS00); // select clock src clkti= clkio/256
  TIMSK0 = (1<<OCIE0A); // Local interruption compare match A
  OCR0A = 124;       //124 = 16Mhz/(256 * 500Hz) -1
  sei();             // enable all interrupts

ISR(TIMER0_COMPA_vect){         // timer compare interrupt service routine
  static int counter=0;
  if (++counter >= 150) { // 150 * 2 ms = 300 ms
    counter = 0;
    onTimer();
  }
}

void loop(){
}

Vous pouvez voir d’autres exemples d’utilisation de timer dans cet article

N.B.: l’idée est de simplifier les calculs. On prend un facteur de division assez grand et on calcul la valeur seuil du compteur tout en s’assurant qu’elle puisse être stocké sur 8 ou 16 bits

Lire une entrée analogique (ch22)

La tension analogique de référence est donnée par Vref (5V par défaut), un Arduino peut donc lire une tension jusqu’à 5V.

     ADMUX = (1<<REFS0) | (chan & 0x0f);  //select ref (REFS0) and channel 0 - 8
     ADCSRA |= (1<<ADSC);                 //start the conversion
     while (ADCSRA & (1<<ADSC));          //wait for end of conversion
     int value = ADCL | (ADCH << 8);          //Return 16 Bit Reading Register
     float voltage = value * (5 / 1024); // conversion to volt

La valeur de tension en sortie de l’ADC est codée sur 10bits on obtient donc la formule ADC = Vin * 2^10 / Vref.

Activer une interruption (ch12)

Contrairement aux interruption hardware, il est possible d’activer une interruption sur toutes broches du microcontrôleur qui pourront détecter un changement d’état (front montant et front descendant). La description de la gestion des interruptions se trouve dans le chapitre 12 de la datasheet.

Activer les interruptions sur les registres (12.2.4)

PCICR | = B00000001; // activate the interrupts on PORTB
PCICR | = B00000010; // activate the interrupts on PORTC
PCICR | = B00000100; // activate the interrupts on PORTD

Activer les interruptions sur les broches (12.2.6-8)

PCMSK0 |= B00000001; // activate interrupt on pin D8
PCMSK1 |= B00001000; // activate interrupt on pin A3
PCMSK2 |= B00010100; // activate interrupt on pin D2 and D4

Il existe donc trois groupes d’interruption, pour détecter l’interruption, nous testons les vecteurs d’interruption PCINT0_vect, PCINT1_vect et PCINT2_vect. Ces vecteurs donnent le groupe qui a généré l’interruption. Il faut ensuite tester chacune des broches pour savoir sur laquelle a eu lieu le changement d’état.

ISR (PCINT2_vect) {
  if (digitalRead(2)) {
    //pin2 state changed
  }
  else if (digitalRead(4)) {
    //pin4 state changed
  }
}

N.B.: la fonction ISR (Interrupt Service Routine) doit être aussi courte que possible

Écrire dans l’EEPROM (ch7)

Voici les fonctions pour lire et écrire à l’aide des registres de l’EEPROM

void EEPROM_write(unsigned int uiAddress, unsigned char ucData)
{
/* Wait for completion of previous write */
while(EECR & (1<<EEPE))
;
/* Set up address and Data Registers */
EEAR = uiAddress;
EEDR = ucData;
/* Write logical one to EEMPE */
EECR |= (1<<EEMPE);
/* Start eeprom write by setting EEPE */
EECR |= (1<<EEPE);
}
unsigned char EEPROM_read(unsigned int uiAddress)
{
/* Wait for completion of previous write */
while(EECR & (1<<EEPE))
;
/* Set up address register */
EEAR = uiAddress;
/* Start eeprom read by writing EERE */
EECR |= (1<<EERE);
/* Return data from Data Register */
return EEDR;
}

Communication SPI (ch18)

Registres d’intéret

Initialisation des broches utilisées par la communication SPI

void SPI_init()
{
    // set CS, MOSI and SCK to output
    SPI_DDR |= (1 << CS) | (1 << MOSI) | (1 << SCK);

    // enable SPI, set as master, and clock to fosc/128
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0);
}

Fonction pour la transmission SPI

void SPI_masterTransmit(uint8_t data)
{
    // load data into register
    SPDR = data;

    // Wait for transmission complete
    while(!(SPSR & (1 << SPIF)));
}

Avant de transmettre à l’élément esclave la ligne doit être définie à LOW

    // drive slave select low
    SPI_DDR &= ~(1 << SS);

    // transmit byte to slave
    SPI_masterTransmit(0x55);

    // return slave select to high
    SPI_DDR |= (1 << SS);

Fonction pour la réception SPI

uint8_t SPI_masterReceive()
{
    // transmit dummy byte
    SPDR = 0xFF;

    // Wait for reception complete
    while(!(SPSR & (1 << SPIF)));

    // return Data Register
    return SPDR;
}

Communication UART (ch19)

void USART_Transmit(unsigned char data)
{
    /* Wait for empty transmit buffer */
    while (!(UCSRnA & (1<<UDREn)));
    /* Put data into buffer, sends the data */
    UDRn = data;
}

unsigned char USART_Receive(void)
{
    /* Wait for data to be received */
    while (!(UCSRnA & (1<<RXCn)));
    /* Get and return received data from buffer */
    return UDRn;
}

CommunicationI2C (ch21)

Registres d’intérêt

Initialisation du port I2C et de la fréquence de communication

#define F_CPU 16000000UL   // Frequence CPU
#define SCL_CLOCK 100000L  // Frequence I2C (100kHz)

// Initialisation I2C master
void I2C_Init(void) {
    // SCL freq = F_CPU / (16 + 2*TWBR*Prescaler)
    TWSR = 0x00; // prescaler = 1
    TWBR = ((F_CPU / SCL_CLOCK) - 16) / 2;
}

Commencer et finir la communication

void I2C_Start(void)
{
    TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
    while ((TWCR & (1<<TWINT)) == 0);
}

void I2C_Stop(void)
{
    TWCR = (1<<TWINT)|(1<<TWSTO)|(1<<TWEN);
}

Envoyer et réceptionner des données

// Write byte
void I2C_Write(uint8_t data) {
    TWDR = data;
    TWCR = (1<<TWINT)|(1<<TWEN);            // Clear flag, Enable
    while ((TWCR & (1<<TWINT))  == 0);           // Attente fin
}

// Read byte with ACK
uint8_t I2C_ReadAck(void) {
    TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);  // ACK after reception
    while (!(TWCR & (1<<TWINT)));
    return TWDR;
}

// Write byte without ACK
uint8_t I2C_ReadNack(void) {
    TWCR = (1<<TWINT)|(1<<TWEN);            // No ACK
    while (!(TWCR & (1<<TWINT)));
    return TWDR;
}

N.B. Pour améliorer vos compétences en programmation bas niveau, vous pouvez vous inspirer des librairies disponibles pour Arduino ou autres microcontrôleurs. Vous pouvez aussi combiner API Arduino et code bas niveau

Sources

Quitter la version mobile