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.

Dans un premier temps, voici les données du microcontrôleur qui vont nous intéresser
AVR 8-bit microcontroller
- CPU frequency 16MHz
- 1Kbytes EEPROM
- 2Kbytes internal SRAM
- Two 8-bit Timer/Counter
- One 16-bit Timer/Counter
- Six PWM channels
- Programmable serial USART
- Master/slave SPI serial interface
- I2C serial interface
- 23 programmable I/O lines
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 Pin | Port Pin | Pin Interrupts | Pin function | Serial com | IDE Id |
1 | PC6 | PCINT14 | RESET | ||
2 | PD0 | PCINT16 | RXD | D0 | |
3 | PD1 | PCINT17 | TXD | D1 | |
4 | PD2 | PCINT18 | INT0 | D2 | |
5 | PD3 | PCINT19 | INT1/OC2B | D3 | |
6 | PD4 | PCINT20 | XCK/T0 | D4 | |
11 | PD5 | PCINT21 | T1/OC0B | D5 | |
12 | PD6 | PCINT22 | AIN0/OC0A | D6 | |
13 | PD7 | PCINT23 | AIN1 | D7 | |
14 | PB0 | PCINT0 | CLK0/ICP1 | D8 | |
15 | PB1 | PCINT1 | OC1A | D9 | |
16 | PB2 | PCINT2 | OC1B | SS | D10 |
17 | PB3 | PCINT3 | OC2A | MOSI | D11 |
18 | PB4 | PCINT4 | MISO | D12 | |
19 | PB5 | PCINT5 | built-in LED | SCK | D13 |
23 | PC0 | PCINT8 | ADC0 | 14/A0 | |
24 | PC1 | PCINT9 | ADC1 | 15/A1 | |
25 | PC2 | PCINT10 | ADC2 | 16/A2 | |
26 | PC3 | PCINT11 | ADC3 | 17/A3 | |
27 | PC4 | PCINT12 | ADC4 | SDA | 18/A4 |
28 | PC5 | PCINT13 | ADC5 | SCL | 19/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é.
- AND : ET logique &&
- OR : ou logique ||
- NOT : opposé ~
- XOR : ou exclusif ^
- shift left or right : décalage à gauche ou à droite
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)
- registre PCMSK0 lié au PORTB -> pin D8 à D13
- registre PCMSK1 lié au PORTC -> pin A0 à A5
- registre PCMSK2 lié au PORTD -> pin D0 à D7
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
- SPCR : SPI Control Register (activation, mode maître/esclave, vitesse, mode CPOL/CPHA).
- SPSR : SPI Status Register (flags, double vitesse).
- SPDR : SPI Data Register (données à envoyer/recevoir).
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
- TWSR : TWI Status Register (bits de statut + prescaler).
- TWDR : TWI Data Register (données à envoyer/recevoir).
- TWCR : TWI Control Register (contrôle des opérations).
- TWAR : TWI Address Register (adresse esclave si on configure en esclave)
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