Nous avons l'intention dans ce chapitre d’aborder divers types de communication : série asynchrone, série synchrone, I2C et USB.
Communication série asynchrone ou RS232
La partie du circuit spécialisée gèrant la liaison série (ou RS232) s’appelle une UART.
On rappelle que ce type de liaison série permet d'envoyer des informations sans horloge de référence. Elle est à un logique au repos. Une trame est composée des 8 bits à transmettre, précédé d'un bit de start ("0" logique), et suivi par au moins un bit de stop ("1" logique). Le bit de stop peut être doublé et éventuellement précédé par un bit de parité. Pour une parité paire, le bit de parité est mis à "0" quand le nombre de "1" est pair tandis qu'une parité impaire met à "0" pour un nombre impair de "1".
La vue de l'image ci-contre semble indiquer le contraire de ce que je suis en train d'expliquer. Mais n'oubliez pas que du point de vue électrique, un niveau logique "0" est représenté par une tension de +3 V à +25 V et un niveau logique "1" par une tension de −3 V à −25 V (codage NRZ). Ordinairement, des niveaux de +12 V et −12 V sont utilisés.
Comme nous allons utiliser pour nos essais des platines Arduino pour lesquelles la liaison série est réalisée à l'aide de l’USB, les problèmes de tension n'ont pas lieu.
La liaison série est très populaire à cause de son évolution justement avec l'USB et surtout que tous les systèmes d'exploitation proposent un logiciel pour communiquer par la liaison série. Il est appelé hyperterminal sous Windows. Même si nous utilisons essentiellement Linux pour nos essais, nous continuerons à l'appeler hyperTerminal.
Voici quelques brochages utiles qu’il est facile de retrouver sur Internet.
rs232 basse tension | RXD | TXD |
---|---|---|
UNO | PD0(Arduino:0) | PD1 (Arduino:1) |
LEONARDO | PD2 (Arduino:0) | PD3 (Arduino:1) |
Pro Micro (Sparkfun) | PD2 (Arduino:0) | PD3 (Arduino:1) |
Les registres associés
Le « n » apparaissant dans ces dessins peut prendre la valeur 0 ou 1 dans l'ATMega328.
Les bits du registre UCSRnA sont :
- RXCn : réception complète
- TXCn : transmission complète
- UDREn : USART Data Register Empty (vide)
- FEn : Frame Error
- DORn : Data OverRun
- UPEn : USART Parity Error
- U2Xn : est expliqué dans le dessin
- MPCMn : Multi Processor Communication Mode
Les bits du registre UCSRnB sont :
- RXCIEn : interruption quand réception complète autorisée
- TXCIEn : interruption quand transmission complète autorisée
- UDRIEn : interruption quand UDRn est vide autorisée
- RXENn : autorisation de la réception
- TXENn : autorisation de la transmission
- UCSZn2 : taille de caractère quand combiné avec UCSZn1:0
- RXB8n : 9° bit de réception au cas où ce mode est choisi
- TXB8n : 9° bit de transmission au cas où ce mode est choisi
Une librairie d'utilisation
Nous avons trouvé sur Internet la librairie suivante qui a été écrite pour un ATMega168. Nous l'avons fait fonctionner telle quelle sur un ATMega2560 et la platine Arduino correspondante. Nous la publions avec quelques modifications.
/*
Title: SerialCom.c
Date Created: 6/9/2009
Last Modified: 6/9/2009
Target: Atmel ATmega168, ATmega368
Environment: AVR-GCC
Note: the makefile is expecting a '168 with a 16 MHz crystal.
Adapted from the Arduino sketch "Serial Call and Response," by Tom Igoe.
// This program sends an ASCII A (byte of value 65) on startup
// and repeats that until it gets some data in.
// Then it waits for a byte in the serial port, and
// sends three (faked) sensor values whenever it gets a byte in.
Written by Windell Oskay, http://www.evilmadscientist.com/
Copyright 2009 Windell H. Oskay
Distributed under the terms of the GNU General Public License, please see below.
Additional license terms may be available; please contact us for more information.
More information about this project is at
http://www.evilmadscientist.com/article.php/peggy
-------------------------------------------------
USAGE: How to compile and install
A makefile is provided to compile and install this program using AVR-GCC and avrdude.
To use it, follow these steps:
1. Update the header of the makefile as needed to reflect the type of AVR programmer that you use.
2. Open a terminal window and move into the directory with this file and the makefile.
3. At the terminal enter
make clean <return>
make all <return>
make program <return>
4. Make sure that avrdude does not report any errors. If all goes well, the last few lines output by avrdude
should look something like this:
avrdude: verifying ...
avrdude: XXXX bytes of flash verified
avrdude: safemode: lfuse reads as E2
avrdude: safemode: hfuse reads as D9
avrdude: safemode: efuse reads as FF
avrdude: safemode: Fuses OK
avrdude done. Thank you.
If you a different programming environment, make sure that you copy over
the fuse settings from the makefile.
-------------------------------------------------
This code should be relatively straightforward, so not much documentation is provided. If you'd like to ask
questions, suggest improvements, or report success, please use the evilmadscientist forum:
http://www.evilmadscientist.com/forum/
-------------------------------------------------
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <avr/io.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
unsigned char serialCheckRxComplete(void)
{
return( UCSR0A & _BV(RXC0)) ; // nonzero if serial data is available to read.
}
unsigned char serialCheckTxReady(void)
{
return( UCSR0A & _BV(UDRE0) ) ; // nonzero if transmit register is ready to receive new data.
}
unsigned char serialRead(void)
{
while (serialCheckRxComplete() == 0) // While data is NOT available to read
{;;}
return UDR0;
}
void serialWrite(unsigned char DataOut)
{
while (serialCheckTxReady() == 0) // while NOT ready to transmit
{;;}
UDR0 = DataOut;
}
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
Cette librairie a été utilisée avec succès avec une platine Arduino UNO et MEGA2560. L'horloge est aussi à 16 MHz comme ce qui est supposé par cette librairie. L'exercice 8 ici fait une lecture d'une valeur supposée être fournie par deux caractères, valeur variant entre 00 et FF, pour régler le rapport cyclique.
Exercices
Un exercice a déjà été proposé dans ce livre : PWM rapide dans le chapitre sur le timer 0.
En voici un autre assez proche.
Travail à réaliser en C
Pour cette partie, on utilise l'IDE Arduino mais le langage C pur (avec donc un main()).
Changer la vitesse de transmission
La librairie présentée fonctionne pour une vitesse de transmission de 9600 bauds. Cette vitesse est choisie avec les registres UBRR0H et UBRR0L comme indiqué dans les commentaires.
Modifier la librairie pour la faire fonctionner à 19200 bauds. Écrire un programme complet qui permet de réaliser un test avec cette nouvelle vitesse dans l'hyperterminal Arduino.
#include <avr/io.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 19200 //!!!!!!!! seul ce commentaire à changé !!!!!!
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
unsigned char serialCheckRxComplete(void) {
return( UCSR0A & _BV(RXC0)) ; // nonzero if serial data is available to read.
}
unsigned char serialCheckTxReady(void) {
return( UCSR0A & _BV(UDRE0) ) ; // nonzero if transmit register is ready to receive new data.
}
unsigned char serialRead(void) {
while (serialCheckRxComplete() == 0) // While data is NOT available to read
{;;}
return UDR0;
}
void serialWrite(unsigned char DataOut) {
while (serialCheckTxReady() == 0) // while NOT ready to transmit
{;;}
UDR0 = DataOut;
}
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
int main() {
unsigned char val;
serialInit();
while(1) {
val=serialRead();
serialWrite(val);
}
return 0;
}
SerialRead() est une fonction bloquante ce qui permet de ne pas mettre de temporisation dans ce programme. Ce programme ne fait que recopier ce qui arrive par la liaison série dans la liaison série. Dans l'hyperterminal Arduino ce n’est pas spectaculaire, mais c’est bien l'Arduino qui fait ce travail de recopie.
Après cette mise en jambes, venons-en aux problèmes sérieux...
Changer la fréquence sur un buzzer
On désire changer la fréquence sonore d'un buzzer à l'aide du mode CTC du timer 2. La valeur envoyée pour déterminer la fréquence sera systématiquement envoyée par la liaison série sous forme de deux caractères 0,...,9,A,...,F. Dans ce genre de problème, la difficulté est de comprendre la différence entre des caractères et des valeurs.
- Écrire un sous-programme "void usart_puts_hexa(unsigned char val)" destiné à transformer la valeur val (hexadécimale) en deux caractères affichables (deux parcequ'on est sur 8 bits). Tester avec un programme principal.
La difficulté pour un étudiant, dans ce genre de sous-programme, est de bien faire la distinction entre valeur numérique et son affichage sous forme de chaîne de caractères.
void usart_puts_hexa(unsigned char val) {
char tab[2];
tab[0] = val >> 4; //poids fort
tab[1] = val & 0x0F; //poids faible
if (tab[0] < 10) tab[0] += '0'; else tab[0] += '7';
if (tab[1] < 10) tab[1] += '0'; else tab[1] += '7';
serialWrite(tab[0]); // poids fort en premier
serialWrite(tab[1]); // puis poids faible
}
Faites un effort pour comprendre l'apparition de '0' et de '7' !
Nous allons maintenant réaliser l'opération inverse : lire deux caractères (en représentation hexadécimale) qui arrivent par la liaison série et les transformer en une valeur numérique correspondante sur un octet.
- Un sous-programme "unsigned char usart_gets_hexa()" sera donc chargé de lire ces deux caractères d’en vérifier la syntaxe et d’en retourner la valeur. Écrire ce sous-programme et le tester avec le sous-programme de la question précédente. On affichera "--" en cas d'erreur de syntaxe. TOUT SE PASSE PAR LA LIAISON SERIE. Pouvez-vous modifier la fonction un peu naïve ci-dessous pour qu'elle gère correctement les erreurs (car elle ne le fait pas !!!!) même si elle fonctionne parfaitement :
// Pour comprendre la faiblesse de cette fonction donnez-lui "GG" à convertir
// elle vous retourne quelque chose alors que "GG" n'est manifestement pas un
// nombre hexadécimal
unsigned char usart_gets_hexa() {
unsigned char val;
char tab[3];
tab[0] = serialRead(); //poids fort
tab[1] = serialRead(); //poids faible
while (serialCheckRxComplete()) serialRead(); // on vide buffer
tab[0] -= '0';
tab[1] -= '0';
if (tab[0] > 9) tab[0] -= 7;
if (tab[1] > 9) tab[1] -= 7;
val = (tab[0] << 4) + tab[1];
return val;
}
Il y a plusieurs manière de gérer les erreurs :
- retourner une structure avec un champ réservé aux erreurs
- retourner un entier sur 16 bits. Les 8 bits de poids fort seront réservés aux erreurs
// non testé pour le moment
unsigned int usart_gets_hexa() {
unsigned int val;
char tab[3];
tab[0] = serialRead(); //poids fort
tab[1] = serialRead(); //poids faible
while (serialCheckRxComplete()) serialRead(); // on vide buffer
tab[0] -= '0';
tab[1] -= '0';
if (tab[0] > 9) tab[0] -= 7;
if (tab[1] > 9) tab[1] -= 7;
if ((tab[0] < 16) && (tab[0] >= 0) && (tab[1] < 16) && (tab[1] >= 0))
val = (tab[0] << 4) + tab[1];
else
val |= 0x0100; // flag d'erreur positionné
return val;
}
int main() {
unsigned int val;
serialInit();
while(1) {
val = usart_gets_hexa();
if (val & 0x0100) { // erreur dans ce cas
serialWrite('-');serialWrite('-');
} else
usart_puts_hexa(val);
}
return 0;
}
Le "GG" de tout à l’heure ne passera plus mais le fait de tester tab[0] et tab[1] après calcul est une erreur. En effet quelques caractères pourront passer les tests alors qu’ils ne sont pas corrects. Ce doit être le cas de tous les caractères entre '9' et 'A'. Pour le moment nous considérons ce sous-programme comme assez fiable pour nos besoins. Nous le modifierons plus tard.
Et l’apothéose finale ....
- Changer votre programme en utilisant les sous-programmes développés pour la question précédente pour qu’il réalise une fréquence audible sur le bit OC0A du PORTB variant en fonction de la valeur reçue (par la liaison série).
Le bit OC0A correspond au bit B6 pour l'ATMega328 de l'Arduino UNO.
Exercice 2 : Commande d'un servomoteur
Un potentiomètre est branché sur un Arduino sur l'entrée A0. Un servomoteur est branché sur un deuxième Arduino (sortie (9). Les deux Arduinos sont reliés entre eux par une liaison série. Écrire les deux programmes en C pour les deux Arduinos pour que le potentiomètre commande le servomoteur par l'intermédiaire de la liaison série.
Indications :
- la résolution de ce problème nécessite quelques connaissances sur la conversion analogique numérique. La broche A0 dont il est question ici est l'entrée AN0. Ceci peut sembler être une information redondante mais elle est seulement vraie pour les Arduino UNO (pas LEONARDO par exemple).
- La commande d'un servomoteur par un potentiomètre est étudiée dans le chapitre sur la conversion analogique numérique.
- La broche (9) de l'Arduino est PD1. Mais elle est aussi OC1A. Cela signifie clairement que le timer que l'on utilisera pour commander le servomoteur est le timer1
- La commande d'un servomoteur par le timer1 a déjà été traité comme exercice 3 dans le chapitre sur le TIMER1
Voici le code de l'Arduino sur lequel est branché le potentiomètre :
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
// Frequence 16MHz
#define F_CPU 16000000
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void uart_putchar(char c)
{
loop_until_bit_is_set(UCSR0A, UDRE0); /* Wait until data register empty. */
UDR0 = c;
}
void uart_init() {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
//******************* Pour platine UNO **************
int main(){
//********* Configuration du CAN **************
uint16_t result;
uint8_t toSend;
// Choose AVCC pin for the comparison voltage
// Choose channel 0 in the multiplexer
ADMUX = (1 << REFS0) ;
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2);
//********** Serial **************
//Serial Initialization
uart_init();
while(1){
// start conversion
ADCSRA |= (1 << ADSC);
// Wait for the measuring process to finish
//while (ADCSRA & (1 << ADSC));
loop_until_bit_is_clear(ADCSRA , ADSC);
// Read the 16-bit value
result = ADC;
toSend = (uint8_t)(ADC * 0.1212 +32);
uart_putchar(toSend);
_delay_ms(100);
}
}
Voici maintenant la partie code de l'Arduino sur lequel est branché le servomoteur :
#include <avr/io.h>
#include <avr/sfr_defs.h>
#include <util/delay.h>
// Frequence 16MHz
#define F_CPU 16000000
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
void uart_init() {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
uint8_t uart_Read(void) {
//loop_until_bit_is_set(UCSR0A, RXC0);// While data is NOT available to read
while (bit_is_clear(UCSR0A, RXC0)) _delay_ms(1);
return UDR0;
}
void uart_putchar(char c)
{
loop_until_bit_is_set(UCSR0A, UDRE0); /* Wait until data register empty. */
UDR0 = c;
}
//******************* Pour platine UNO **************
int main(){
uint8_t PWM_servo=100;
//********** Serial **************
//Serial Initialization
uart_init();
//*********** partie Timer 1 **************
// initialisation pour comparaison
DDRB |= (1<<DDB1); // 1 = sortie=OC1A
ICR1 = 1250; // MLI à 50 Hz
// Prescaler 256 (Clock/256)
TCCR1B = (1<<CS12);
// Timer 1 en mode 14
TCCR1B |= (1<<WGM12);
TCCR1A |= (1<<WGM11)|(1<<WGM10);
TCCR1A |= (1<<COM1A1); //bascule sortie a chaque comparaison
OCR1A = 32; // 32<=OCR1A<=156
while(1){
PWM_servo = uart_Read();
OCR1A = PWM_servo;
_delay_ms(1);
}
}
Cas particulier de l'Arduino Leonardo
La particularité de l'Arduino Leonardo est la gestion de la liaison série par l'USB.
Nous allons utiliser du code du site pjrc.com : USB serial
Voir aussi WIKI IUT Troyes
Communication série synchrone ou SPI
Un article de wikipédia Serial Peripheral Interface présente les communications SPI qui nous intéressent dans cette section.
Interface SPI décrit des programmes que nous reprendrons.
Description du protocole
Le protocole SPI est synchrone : une horloge synchronise l'échange de données. Dans tout échange on définit un Maître qui réalise l'horloge et un Esclave. L'esclave (il peut y en avoir plusieurs) est choisi avec une entrée spéciale appelée SS (Slave Select). A ce point, nous connaissons deux broches du SPI : l'horloge appelée SCK et SS. Il en manque deux pour être complet :
- MISO : Master in Slave Out
- MOSI : Master Out Slave In
Ces deux broches laissent entendre un échange full duplex (complet dans les deux sens). Voici donc comment les choses se passent avec un Maître et un Esclave :
Si vous avez plusieurs esclaves une configuration de ce genre est à choisir :
Comme nous allons utiliser des cartes Arduino pour nos expérimentations, nous allons commencer par donner leurs brochages.
Quelques brochages pour les cartes Arduino UNO, Leonardo et Mega2560
Voici résumé dans un tableau le brochage des cartes qui nous intéressent :
SPI | MISO | MOSI | SCK | SS |
---|---|---|---|---|
UNO / NANO | PB4 (Arduino:12) | PB3 (Arduino:11) | PB5 (Arduino:13) | PB2 (Arduino:10) |
LEONARDO | PB3 (ICSP:1) | PB2 (ICSP:4) | PB1 (ICSP:3) | --aucune-- |
MEGA2560 | PB3 (Arduino:50) | PB2 (Arduino:51) | PB1 (Arduino:52) | PB0 (Arduino:53) |
Pro Micro (Sparkfun) | PB3 (Arduino:14) | PB2 (Arduino:16) | PB1 (Arduino:15) | --aucune-- |
Quel est l'impact sur le Leonardo du manque de broche dédiée à SS ? C'est une question à laquelle nous n'avons pas d'explication définitive, mais il nous semble que cela empêche de fonctionner en SPI esclave. A vérifier !
Particularité du Leonardo
Comme on peut le voir dans ce tableau, les broches utilisées pour SPI sur la Leonardo ne sont pas reliées aux connecteurs mais directement sur le programmateur ICSP. Pour éviter des recherches sur internet, nous rappelons la connectique associée des six broches avec les deux photos ci-contre.
Voici par exemple ci-contre la connectique ICSP présente sur une carte Arduino.
Mise en œuvre matérielle
Le problème de la polarité d'horloge est important : un maître et un esclave peuvent être incapabes de se comprendre s'ils n'ont pas la même polarité. Le dessin de wikipédia est redonné ici comme référence.
Les registres correspondants à la description matérielle sont maintenant présentés et seront suivi par un schéma de l'architecture correspondante. En ce qui concerne le registre SPCR :
- SPIE: pour autoriser les interruptions SPI
- SPE: SPI Enable, doit être positionné à 1 pour toute opération SPI
- DORD: Data Order, 1 pour une transmissin du poids faible en premier
- MSTR: Master/Slave Select, 1 pour le positionnement en maître. Ce bit nécessite d’être en accord avec SS (Slave Select)
- CPOL: Clock Polarity, 1 pour SCK haut quand idle, 0 pour SCK bas quand idle.
- CPHA: Clock Phase, détermine si les données sont échantillonnées sur le premier front ou le deuxième front de SCK.
Pour le registre SPSR :
- SPIF: est le drapeau d'interruption
- WCOL: Write COLlision Flag
Le reste est donné dans le dessin.
Travail en C sur le SPI
Les programmes C donnés dans cette section n'ont pas encore été testés. Nous enlèverons cette remarque dès que ce ne sera plus le cas.
De toute façon il vous faudra adapter le brochage en fonction de la platine Arduino utilisée.
Il est facile de trouver un programme d'exemple en C sur internet. En voici un exemple serial peripheral interface in avr microcontrollers :
//SPI master
#include <avr/io.h>
#include <util/delay.h>
//SPI initvoid
void SPIMasterInit(void) {
//set MOSI, SCK and SS as output
DDRB |= (1<<PB3)|(1<<PB5)|(1<<PB2);
//set SS to high
PORTB |= (1<<PB2);
//enable master SPI at clock rate Fck/16
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0);
}
//master send function
void SPIMasterSend(uint8_t data){
//select slave
PORTB &= ~(1<<PB2);
//send data
SPDR=data;
//wait for transmition complete
while (!(SPSR &(1<<SPIF)));
//SS to high
PORTB |= (1<<PB2);
}
int main(void) {
//initialize master SPI
SPIMasterInit();
//initial PWM value
uint8_t pwmval = 0;
while (1) {
SPIMasterSend(pwmval++);
_delay_ms(1000);
}
}
Trouvez un esclave SPI et essayez ce programme. Si aucun esclave SPI n'est disponible, réalisez-en un avec une autre platine Arduino.
//SPI slave
#include <avr/io.h>
#include <avr/interrupt.h>
//SPI init
void SPISlaveInit(void) {
//set MISO as output
DDRB |= (1<<PB4);
//enable SPI and enable SPI interrupt
SPCR = (1<<SPE)|(1<<SPIE);
}
void InitPort(void) {
//set PD6 (OC0A) as output
DDRD|=(1<<PD6);
}
//Initialize Timer0
void InitTimer0(void) {
//Set Initial Timer value
TCNT0=0;
//Place compare value to Output compare register
OCR0A=0;
//Set fast PWM mode
//and make clear OC0A on compare match
TCCR0A|=(1<<COM0A1)|(1<<WGM01)|(1<<WGM00);
}
void StartTimer0(void) {
//Set prescaller 64 and start timer
TCCR0B|=(1<<CS01)|(1<<CS00);
}
ISR(SPI_STC_vect) {
OCR0A=SPDR;
}
int main(void) {
//initialize slave SPI
SPISlaveInit();
InitPort();
InitTimer0();
StartTimer0();
sei();
while (1) {
//loop
}
return 0;
}
On ne vous demande pas de comprendre la partie interruption SPI de ce programme. Seule la partie Timer0 est à comprendre puisqu’il faut correctement brancher une LED dont la luminosité doit changer en fonction de ce qui est envoyé par le SPI.
Exercice
En vous aidant de la documentation du brochage des quelques Arduino plus haut, on vous demande de :
1°) modifier le programme ci-dessus (uniquement void SPISlaveInit(void)) pour qu’il fonctionne correctement avec un MEGA2560 avec une horloge SPI égale à la fréquence système divisée par 8.
2°) trouver le mode de fonctionnement correspondant ?
3°) Étant pointilleux sur les principes, on vous demande de modifier aussi le sous-programme de réception, sachant que SPIF est un drapeau comme son nom l'indique.
Voir aussi
- Processeur ATMega avec Arduino et SPI dans un autre projet où un esclave SPI est construit en VHDL et testé.
Communication I2C
Le protocole I²C est un protocole d'échange pour les composants électroniques différents d'un même appareil. Son intérêt par rapport aux protocoles examinés auparavant est qu’il y a une notion d'adressage des esclaves. En clair un maître peut adresser plusieurs esclaves. Quand l'esclave se reconnaît par son adresse, il répond au maître.
Voici quelques brochages utiles qu’il est facile de retrouver sur Internet.
I2C | SCL | SDA |
---|---|---|
UNO | PC5 (Arduino:A5) | PC4 (Arduino:A4) |
LEONARDO | PD0 (Arduino:SCL) | PD1 (Arduino:SDA) |
Pro Micro (Sparkfun) | PD0 (Arduino:3) | PD1 (Arduino:2) |
Adaptation de tensions
Les cartes Arduino que nous utilisons sont en général en 5V. Il existe cependant un certain nombre de périphériques I2C qui sont en 3,3 V. La manette Nunchuk en est un exemple. Il faut donc réaliser une adaptation bidirectionnelle. Ce genre d'adaptation est décrit par exemple dans ce lien. La manette Nunchuk est quant à elle décrite ICI.
Interface I2C dans l'AVR
Programmation de l'interface I2C décrit en anglais ce que nous allons présenter dans ce chapitre.
Le premier registre important est TWBR qui est utilisé pour régler la fréquence d'horloge de la broche SCL. Il faut ajouter au contenu de ce registre les deux bits TWPS1 et TWPS2 du registre TWSR qui sont destiné à réaliser un préscaler avec les valeurs 1, 4, 16 et 64.
Les documentations officielles donnent la formule de calcul de la fréquence :
Comme toujours, il existe un registre de contrôle TWCR qui a un ensemble de bits utilisés pour :
- autoriser l'interruption TWIE
- autoriser le module i2C TWEN : le module prend alors le contrôle physique sur les broches dédiées à l'i2c
- envoi d'un départ (start) TWSTA : doit être remis à 0 par le logiciel quand le start est effectif
- envoi d'un stop TWSTO
- Drapeau d'interruption TWINT qui fonctionne comme un drapeau : mis à 0 par écriture d'un 1. Il peut être utilisé pour détecter une fin de transmission. Il faut un "sei()" et TWIE = 1 en supplément pour déclencher une interruption. Cette possibilité de l'utiliser sans interruption fait qu'il n'est pas mis automatiquement à zéro par l'exécution de l'interruption comme certains autres drapeaux.
- autorise l'envoi d'un ack avec TWEA quand une donnée de l'esclave est reçue
Le registre d'état déjà mentionné TWSR contient les bits de réglage du présscaler. Mais son utilisation principale est de donner l'état du bus I2C avec les bits TWS[7:3]. TWDR est le registre de données qui est utilisé pour mémoriser le prochain octet à transmettre ou l'octet reçu. Les registres TWAR et TWARM sont utilisés quand l'AVR travaille en mode esclave.
La documentation du bit TWSTA insiste sur la nécessité d'une mise à 0 logicielle de ce bit. La bibliothèque présentée plus bas ne le fait pas. Nous réglerons tous ces détails plus tard. Pour le moment nous n'avons fait que le rajouter en commentaire puisque nous n'avons pas de périphérique i2c sous la main.
Une bibliothèque simple pour utiliser l'i2c
Programmation de l'interface I2C donne un ensemble de sous-programmes suffisamment intéressants pour être repris ici.
Commençons à une initialisation à 400 kHz :
void TWIInit(void)
{
//set SCL to 400kHz
TWSR = 0x00;
TWBR = 0x0C;
//enable TWI
TWCR = (1<<TWEN);
}
Nous n'avons pas besoin de préscaler. Pour ce qui est du départ et de l'arrêt, ces deux sous-programmes sont proposés :
//send start signal
void TWIStart(void)
{
TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
while ((TWCR & (1<<TWINT)) == 0);
//TWCR &= ~(1<<TWSTA); //RAZ logiciel du bit TWSTA en commentaire avant des tests
}
//send stop signal
void TWIStop(void)
{
TWCR = (1<<TWINT)|(1<<TWSTO)|(1<<TWEN);
}
Pour "start" nous devons positionner TWSTA et pour "stop" TWSTO ainsi que les bits TWINT et TWEN. Après qu'un "start" est demandé, il nous faut attendre jusqu'à ce que le bit TWINT passe à un.
Un sous-programme d'écriture peut être :
void TWIWrite(uint8_t u8data)
{
TWDR = u8data;
TWCR = (1<<TWINT)|(1<<TWEN);
while ((TWCR & (1<<TWINT)) == 0);
}
La lecture est plus compliquée car elle nécessite deux formes :
- une avec acquittement
- une sans acquittement
Voici les deux sous-programmes correspondants :
//read byte with ACK
uint8_t TWIReadACK(void)
{
TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWEA);
while ((TWCR & (1<<TWINT)) == 0);
return TWDR;
}
//read byte with NACK
uint8_t TWIReadNACK(void)
{
TWCR = (1<<TWINT)|(1<<TWEN);
while ((TWCR & (1<<TWINT)) == 0);
return TWDR;
}
Pour compléter tout ceci, une lecture de l'état est nécessaire :
uint8_t TWIGetStatus(void)
{
uint8_t status;
//mask status
status = TWSR & 0xF8;
return status;
}
Nous allons utiliser ces sous-programmes pour lire la manette Nunchuk.
Application à la manette de jeux Nunchuk en mode Arduino
Cette section a été réalisée avec un Arduino 3,3 V. En effet la manette Nunchuk fonctionne avec cette tension basse. La brancher directement sur un Arduino UNO peut endommager la manette ! |
Un chapitre de ce livre (parmi les chapitres suivants) est consacré à l'Arduino. Si vous n'avez aucune connaissance sur l'Arduino, lisez-le au moins en partie. Si vous avez de bonnes connaissances en C, il vous suffit cependant de savoir qu'un programme Arduino est composé par un setup() et un loop() en lieu et place du célèbre main(). Le setup() est réalisé une seule fois au démarrage et le loop() est réalisé en boucle (loop en anglais se traduit par boucle en français).
La manette Nunchuk est décrite dans un autre projet : Les nouvelles interfaces : de la nunchuk de Nintendo à android où seule la partie concernant la Nunchuk est à lire.
Voici quelques brochages utiles qu’il est facile de retrouver sur Internet.
I2C | SCL | SDA |
---|---|---|
UNO | PC5 (Arduino:A5) | PC4 (Arduino:A4) |
LEONARDO | PD0 (Arduino:SCL) | PD1 (Arduino:SDA) |
Pro Micro (Sparkfun) | PD0 (Arduino:3) | PD1 (Arduino:2) |
Wii Nunchuk help donne aussi des informations importantes sur la manette Nunchuk. En particulier, il vous faut garder à l'esprit que cette manette est faite pour fonctionner sous 3,3 V alors que le monde Arduino fonctionne en général plutôt à 5 V.
Deuxième piqûre de rappel : le risque de détruire une manette Nunchuk est donc réel si vous la connectez à une platine Arduino. Faites une adaptation comme décrit plus haut en tout début de la section sur l'i2c. |
Nous avons choisi la Platine "Pro Micro - 3,3 V/8MHz" de chez Sparkfun pour faire les tests de cette section. Comme elle est alimentée en 3,3 V, on n'a aucun problème de compatibilité.
Le point essentiel est que la manette Nunchuk retourne 6 octets dont la signification est la suivante :
Bit | ||||||||
Byte | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0 | SX<7:0> | |||||||
1 | SY<7:0> | |||||||
2 | AX<9:2> | |||||||
3 | AY<9:2> | |||||||
4 | AZ<9:2> | |||||||
5 | AZ<1:0> | AY<1:0> | AX<1:0> | BC | BZ |
(tableau tiré de wiibrew)
SX,SY sont les positions du Joystick analogique en X et Y, tandis que AX, AY, et AZ sont les donnée de l'accéléromètre sur 10 bits suivant les trois axes.
Voici un exemple de lecture des accéléromètres en langage Arduino :
#include <Wire.h>;
// adresse I2C du nunchuck
#define WII_NUNCHUK_I2C_ADDRESS 0x52
// définition d'une variable counter
uint8_t counter;
// définition d'un tableau de données
uint8_t data[6];
void setup() {
Serial.begin(9600);
// initialisation du nunchuck
Wire.begin();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xF0);
Wire.write(0x55);
Wire.endTransmission();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xFB);
Wire.write(0x00);
Wire.endTransmission();
}
void loop() {
// on demande 6 octets au nunchuck
Wire.requestFrom(WII_NUNCHUK_I2C_ADDRESS, 6);
counter = 0; // tant qu’il y a des données
while(Wire.available()) {
// on récupère les données
data[counter++] = Wire.read();
}
// on réinitialise le nunchuck pour la prochaine demande
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0x00);
Wire.endTransmission();
if(counter >= 5){
// on extrait les données
// dans mon exemple j'utilise uniquement les données d'accélération sur l'axe Y
int16_t accelX = ((data[2] << 2) + ((data[5] >> 2) & 0x03));
int16_t accelY = ((data[3] << 2) + ((data[5] >> 4) & 0x03));
int16_t accelZ = ((data[4] << 2) + ((data[5] >> 6) & 0x03));
Serial.print("ax = ");
Serial.print(accelX);
Serial.print(" : ay = ");
Serial.print(accelY);
Serial.print(" : az = ");
Serial.println(accelZ);
}
// un petit delai pour pas saturer la liaison série.
delay(1000);
}
Travail à faire
Si l'accéléromètre donne une valeur quand ses positions changent c’est tout simplement parce qu’il est sensible à l'accélération de pesanteur.
1°) On vous demande les valeurs envoyées par l’accéléromètre pour chacun des axes en positif et négatif. Profitez de cet exercice pour bien repérer les trois axes x, y et z de l'accélération. Pour cela chercher à rendre maximum l'accélération sur un des axes : la verticale vous donnera la direction de l'axe correspondant.
Pour la Nunchuk que nous possédons, on trouve :
- axmin = 311, axmax=703 qui nous donne un point milieu à 507 et un Delta de 392
- axmin = 303, axmax=712 qui nous donne un point milieu à 507 et un Delta de 409
- azmin = 308, axmax=702 qui nous donne un point milieu à 505 et un Delta de 394
2°) On vous demande de repérer le point 0 d'accélération (entre les deux extrêmes, puis de convertir les valeurs données par l'accéléromètre en unité standard sachant que l'accélération de pesanteur vaut 9,81 S.I. ()
#include <Wire.h>;
// adresse I2C du nunchuck
#define WII_NUNCHUK_I2C_ADDRESS 0x52
// définition d'une variable counter
int counter;
// définition d'un tableau de données
uint8_t data[6];
void setuPour la Nunchuk que nous possédons, on trouve :
*axmin = 311, axmax=703 qui nous donne un point milieu à 507 et un Delta de 392
*axmin = 303, axmax=712 qui nous donne un point milieu à 507 et un Delta de 409
*azmin = 308, axmax=702 qui nous donne un point milieu à 505 et un Delta de 394 p() {
Serial.begin(9600);
// initialisation du nunchuck
Wire.begin();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xF0);
Wire.write(0x55);
Wire.endTransmission();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xFB);
Wire.write(0x00);
Wire.endTransmission();
}
void loop() {
float accelxSI,accelySI,accelzSI;
int16_t accelX,accelY,accelZ;
// on demande 6 octets au nunchuck
Wire.requestFrom(WII_NUNCHUK_I2C_ADDRESS, 6);
counter = 0; // tant qu’il y a des données
while(Wire.available()) {
// on récupère les données
data[counter++] = Wire.read();
}
// on réinitialise le nunchuck pour la prochaine demande
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0x00);
Wire.endTransmission();
if(counter >= 5){
// on extrait les données
// dans mon exemple j'utilise uniquement les données d'accélération sur l'axe Y
accelX = ((data[2] << 2) + ((data[5] >> 2) & 0x03));
accelY = ((data[3] << 2) + ((data[5] >> 4) & 0x03));
accelZ = ((data[4] << 2) + ((data[5] >> 6) & 0x03));
Serial.print("ax = ");
Serial.print(accelX);
Serial.print(" : ay = ");
Serial.print(accelY);
Serial.print(" : az = ");
Serial.println(accelZ);
accelxSI=(accelX-507)*19.62/392;
accelzSI=(accelZ-505)*19.62/394;
accelySI=(accelY-507)*19.62/409;
Serial.print("ax = ");
Serial.print(accelxSI);
Serial.print(" SI : ay = ");
Serial.print(accelySI);
Serial.print(" SI : az = ");
Serial.print(accelzSI);
Serial.println(" SI");
}
// un petit delai pour pas saturer le servomoteur
delay(2000);
}
Ce programme montre quelques erreurs de calibrations : l'accélération de pesanteur allant jusqu'à 10,31 SI.
Cela est du essentiellement à la manière simpliste avec laquelle nous avons calibré : repère max et min et on hasarde l'hypothèse que le zéro est au milieu !
3°) Choisissez un axe et réalisez la plus grande accélération possible. Combien de ms⁻2 faites-vous (sans vous démonter l'épaule) ?
Nous avons réussi un petit 22 mais par chance car nous n'avons pas accéléré le "delay(2000);". Un moyen de faire des tests plus concluants serait certainement d'enregistrer les accéléromètres sans arrêt en calculant au fur et à mesure les maxima et minima puis en lançant le tout sur la liaison série quand tout est terminé... par appui sur le bouton Z de la Nunchuk par exemple. Un calcul simple nous donne pour un max sur 10 bits, soit 1023 une accélération de 25,8 soit environ 2,5 g. Tout ceci est fait en question suivante.
4°) Faire un programme qui calcule sans arrêt les minima et maxima des accélérations tant que vous n'appuyez pas sur le bouton Z et affiche les résultats si vous appuyez dessus.
Voici un programme qui fait le travail :
#include <Wire.h>;
// adresse I2C du nunchuck
#define WII_NUNCHUK_I2C_ADDRESS 0x52
// définition d'une variable counter
int counter;
// définition d'un tableau de données
uint8_t data[6];
void setup() {
Serial.begin(9600);
// initialisation du nunchuck
Wire.begin();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xF0);
Wire.write(0x55);
Wire.endTransmission();
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0xFB);
Wire.write(0x00);
Wire.endTransmission();
}
void loop() {
float accelxSI,accelySI,accelzSI;
int16_t accelX,accelY,accelZ;
static int16_t maxAccelX=0,maxAccelY=0,maxAccelZ=0,minAccelX=1023,minAccelY=1023,minAccelZ=1023;
// on demande 6 octets au nunchuck
Wire.requestFrom(WII_NUNCHUK_I2C_ADDRESS, 6);
counter = 0; // tant qu’il y a des données
while(Wire.available()) {
// on récupère les données
data[counter++] = Wire.read();
}
// on réinitialise le nunchuck pour la prochaine demande
Wire.beginTransmission(WII_NUNCHUK_I2C_ADDRESS);
Wire.write(0x00);
Wire.endTransmission();
if (data[5] & 0x01) { // si Z relaché et oui il fonctionne à l’envers
if(counter >= 5) {
// on extrait les données
// dans mon exemple j'utilise uniquement les données d'accélération sur l'axe Y
accelX = ((data[2] << 2) + ((data[5] >> 2) & 0x03));
accelY = ((data[3] << 2) + ((data[5] >> 4) & 0x03));
accelZ = ((data[4] << 2) + ((data[5] >> 6) & 0x03));
if (accelX > maxAccelX) maxAccelX = accelX;
if (accelX < minAccelX) minAccelX = accelX;
if (accelY > maxAccelY) maxAccelY = accelY;
if (accelY < minAccelY) minAccelY = accelY;
if (accelZ > maxAccelZ) maxAccelZ = accelZ;
if (accelZ < minAccelZ) minAccelZ = accelZ;
}
} else {
accelxSI=(maxAccelX-507)*19.62/392;
accelzSI=(maxAccelZ-505)*19.62/394;
accelySI=(maxAccelY-507)*19.62/409;
Serial.print("max ax = ");
Serial.print(accelxSI);
Serial.print(" SI : max ay = ");
Serial.print(accelySI);
Serial.print(" SI : max az = ");
Serial.print(accelzSI);
Serial.println(" SI");
// un petit delai pour pas saturer le servomoteur
delay(2000);
}
delay(100); // semble obligatoire !!!
}
Nous avons pu saturer tous les axes à 25 SI en le faisant bouger dans tous les sens !
Application à la manette Nunchuk en C pur
Un exemple peut être trouvé ICI sur Internet.
Communication USB
Il y a deux librairies avancées pour l'USB :
- VUSB
- LUFA
L'utilisation de la librairie LUFA de manière transparente dans le monde Arduino est développée dans un autre chapitre avec la réalisation d'un clavier USB.
Nous allons commencer par la librairie VUSB.
Un clavier USB qui utilise la librairie VUSB
Voir aussi
Une liaison série qui utilise la librairie VUSB
Voir aussi
- Serial USB for any AVR Microcontroller
- Vidéo Youtube sur le sujet en anglais
Utilisation de la platine Arduino Leonardo
La platine Arduino Leonardo est équipée d'un AVR32U4 qui permet de faire de l'USB de manière matérielle.
Dans le chapitre sur Arduino de ce livre nous parlons de l'USB sur cette platine. Vous pouvez trouver cela dans la section Clavier USB avec la platine Leonardo. Cela nous montre la simplicité que procure l'environnement Arduino pour résoudre des problèmes assez complexes. Pourtant, si l’on veut aller plus loin il va bien falloir comprendre l’utilisation des registres pour gérer l'USB. C'est ce que nous nous proposons d'examiner maintenant.