Micro contrôleurs AVR/Les communications en tout genre
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
modifierLa 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
modifierLe « 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
modifierNous 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
modifierUn 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
modifierPour cette partie, on utilise l'IDE Arduino mais le langage C pur (avec donc un main()).
Changer la vitesse de transmission
modifierLa 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
modifierOn 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
modifierUn 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
modifierLa 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
modifierUn 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
modifierLe 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 : un des esclaves envoie des données au maître
- MOSI : Master Out Slave In : le maître envoie des données à un des esclaves qu'il a choisi avec le Slave Select)
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 :
Ce schéma ne représente que le protocole électrique. En général il y a un protocole logique au-dessus, c'est-à-dire un moyen pour le maître de demander certaines données et pas d'autres à l'esclave. Cela se fait avec une série d'octets prévue par le constructeur de l'esclave. Il vous faudra donc lire la documentation de l'esclave (en général un capteur) pour connaître ce protocole.
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
modifierVoici 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 ? En principe aucun parce que cette broche SS n'est utile que si l'on désire utiliser le Leonardo en esclave. Vous pouvez utiliser n'importe quel autre bit de port pour sélectionner des esclaves. Mais il faudra les gérer par vous-même.
Particularité du Leonardo
modifierComme 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 pour laquelle MISO et MOSI sont inversés par rapport à la figure ci-dessus.
Mise en œuvre matérielle
modifierLe 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
modifierLes 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 2
modifierEn 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.
Lecture des données provenant d'une caméra Pixy2
modifierLa caméra Pixy2 est très utile si vous voulez faire des challenges de robotique mobile. Elle est capable d'apprendre à reconnaître des objets, de les traquer et surtout de fournir un certain nombre de données sur le position et la taille des objets reconnus. Par exemple, dans la coupe de France des IUT GEII, le problème est de reconnaître des balles de tennis et de les mettre du côté adverse. C'est typiquement ce qui peut être fait à l'aide d'une caméra Pixy. Mais l'utilisation d'autres caméras est naturellement possible. Une vidéo de cette coupe de France est disponible maintenant ainsi que son Règlement 2020.
Nous allons étudier dans cette section la communication SPI avec la caméra. Il faut savoir qu'il existe une librairie C++ fonctionnant directement sur Arduino et qui s'installe directement dans l'environnement Arduino. La caméra est fournie avec un câble qu'il est possible de brancher directement sur le programmateur ICSP d'une carte Arduino. Le protocole utilisé avec ce câble est évidemment le protocole SPI mais il a une particularité, c'est qu'il n'a pas besoin d'une broche supplémentaire pour faire le "Slave Select". Il est possible d'utiliser d'autres protocoles série et en particulier le SPI avec Slave Select mais aussi la communication série RS232 ou l'i2c.
En toute franchise, nous en sommes arrivés à court-circuiter la librairie Arduino parce que nous avons commencé à résoudre ce problème dans un FPGA et que le processeur que l'on utilise dans le FPGA n'est pas complètement compatible avec un Arduino. Nous avons donc voulu le réaliser avec une carte Arduino parce que des essais directs à partir du FPGA n'ont pas été concluants. Pour tout vous dire, les essais sur Arduino nous ont pris un certain temps aussi... et c'est pour cela que nous publions ici nos résultats.
Pourquoi avons-nous eu autant de difficultés ?
- première raison, nous ne possédons pas d'outils pour déboguer le SPI (oscilloscope ou autre)
- deuxième raison, après avoir résolu partiellement la première raison en affichant des résultats par la liaison série, nous ne nous sommes pas aperçu tout de suite que les deux octets d'entête de la réponse arrivaient mais avec du retard. Une fois que nous avons remarqué ce détail, nous avons écrit un sous-programme qui laisse tomber ce qu'il reçoit par le SPI tant qu'il n'a pas reçu ces deux octets.
- troisième raison, nous n'avons pas détecté dans le code source de la librairie Arduino ce type d'attente/synchronisation.
Il est maintenant grand temps de passer à la pratique.
Protocole d'échange
modifierLa connaissance du protocole d'échange entre la caméra et votre processeur est importante. Ce protocole ne dépend pas de la façon matérielle choisie : i2c, SPI sans SS, SPI avec SS, rs232. Voici comment il est documenté dans le document officiel (très partiellement traduit ici). Nous allons examiner par exemple le cas de la recherche de la résolution de la caméra.
- La demande réalisée par votre processeur doit être conforme à la requête standard pour ce type de données :
- Requête
Byte | Description | Value(s) |
0 - 1 | 16-bit sync | 174, 193 (0xc1ae) |
2 | Type de paquet | 12 |
3 | Longueur de données | 1 |
4 | Type (non utilisé réservé pour les versions futures) | 0 - 255 |
Cette suite d'octets est obligatoirement à envoyer si vous voulez obtenir la réponse correspondant à la résolution de l'image en pixels de la part de la caméra Pixy2. Cette résolution pouvant être réglée à l'aide de PixyMon, il vous faut être sûr de la connaître si vous voulez positionner correctement un objet (par rapport à vous) dont les coordonnées vous sont retournées par la caméra.
- La réponse de la caméra doit être conforme à :
- Réponse
Byte | Description | Value(s) |
0 - 1 | 16-bit sync | 175, 193 (0xc1af) |
2 | Type de paquet | 13 |
3 | Longueur des données | 2 |
4 - 5 | 16-bit checksum | sum of payload bytes |
6 - 7 | 16-bit Largeur d'image (en pixels) | 0 - 511 |
8 - 9 | 16-bit Hauteur d'image(en pixels) | 0 - 511 |
Nous allons maintenant nous intéresser à la façon de gérer ce type de données en langage C. Deux façons nous viennent à l'esprit :
- Utilisation d'un type structure en C
/**** déclaration du type avec une structure ****/
/**** correspond au premier tableau ci-dessus ****/
struct getRes {
uint16_t _16bitsSync;
uint8_t TofPacket;
uint8_t LenOfPayload;
uint8_t unUsed;
};
/**** déclaration avec initialisation d'une variable ****/
struct getRes requestRes = {0xC1AE,12,1,0};
- Utilisation d'un tableau
/**** déclaration avec initialisation du tableau ****/
uint8_t ResData[5]={0xAE,0xC1,12,1,0};
Les deux méthodes ont leurs avantages et inconvénients. L'intérêt d'une structure est qu'elle permet d'accéder aux champs directement par leurs noms. Son inconvénient est qu'en général les sous-programmes utilisés pour envoyer ou recevoir des données utilisent des tableaux. Il faudra donc transtyper et ceci vous vaudra un warning de la part du compilateur.
L'inconvénient du tableau est qu'il ne faut pas vous tromper pour mettre les données : regardez comment est mis la valeur 0xC1AE dans le tableau. Vous aurez évidemment le même problème pour retrouver les données.
Nous vous laissons choisir.
Exercice 3
modifierNous nous proposons d'écrire un programme C capable de gérer le SPI sans "slave select" (puis avec) pour récupérer les données de position de de largeur et grosseur de l'objet détecté. Les données seront envoyées par la liaison série pour les tests.
- Chercher dans cette section et dans la précédente du code pour utiliser la liaison série et la liaison SPI. Éventuellement vous pouvez simplifier le code sachant que la liaison série ne sera utilisée que dans un sens.
- C'est votre processeur qui sera le maître de la liaison SPI. La caméra Pixy2 sera donc votre esclave.
- Dans un premier temps on utilisera la liaison SPI sans "Slave Select". Votre maître SPI est sensé avoir une horloge à 2 MHz (horloge processeur / 8) au maximum et être en mode 3 (bits CPOL=1 et CPHA=1 du registre SPCR. Modifier votre programme d'initialisation trouvé plus haut pour répondre à ces contraintes.
- Écrire maintenant des sous-programmes génériques pour écrire un ou plusieurs octets ainsi pour demander une lecture d'octets réponses de la Pixy2.
- Examinez dans la documentation appropriée le protocole d'échange pour getResolution() et pour getBlocks(sigmap, maxBlocks) et construisez des types de données pour réaliser correctement ces protocoles ainsi que les sous-programmes correspondants. Testez.
- Arrivés ici, vous remarquez que la communication ne se fait pas correctement. Pour un fonctionnement correct, on vous demande d'écrire un sous-programme qui commence à lire les données de la Pixy que quand il vient de recevoir l'entête de la réponse 0xC1AF sachant que la réception se fait d'abord par 0xAF puis par 0xC1. Il serait très bien de mettre un compteur qui limite à 50 les données lues en attente de 0xC1AF. En cas de limite dépassée, on sort du sous-programme avec la valeur 0 pour dire que l'on a rien lu. Dans le cas contraire, c'est le nombre d'octets lus qui est retourné.
- Modifier le programme précédent pour utiliser le Slave Select. On rappelle que n'importe quelle broche de l'Arduino peut servir à cela. L'utilisation de la broche notée SS ("Slave Select") dans la documentation n'est obligatoire que si votre processeur est un esclave SPI.
Nous donnons en vrac du code fonctionnel utilisant le protocole SPI sans Slave Select dans un Arduino Uno.
//SPI master
#include <avr/io.h>
#define F_CPU 16000000 // 16 MHz oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
#include <avr/sfr_defs.h>
#include <util/delay.h>
// struct pour gérer la demande de résolution
struct getRes {
uint16_t _16bitsSync;
uint8_t TofPacket;
uint8_t LenOfPayload;
uint8_t unUsed;
};
// struct pour gérer le résultat retourné par la Pixy2
struct Result {
uint16_t _16bitsSync;
uint8_t TofPacket;
uint8_t LenOfPayload;
uint16_t checkSum;
uint16_t width;
uint16_t height;
};
struct Result resolution;
// declaration de la variable utilisée pour la requête de résolution
// avec son initialisation
struct getRes requestRes = {0xC1AE,12,1,0};
// déclaration de la variable utilisée pour la requête de blocs
// et son initialisation
uint8_t getCCC[6]={0xAE,0xC1,32,2,1,1};
// déclaration de la structure utilisée pour la réception des données des blocs
struct CCCBlock_t {
uint16_t _16bitsSync;
uint8_t TofPacket;
uint8_t LenOfPayload;
uint16_t checkSum;
uint16_t colorCodeNumber;
uint16_t XCenter;
uint16_t YCenter;
uint16_t Width;
uint16_t Height;
uint16_t angleOfColor;
uint8_t TrackingIndex;
uint8_t Age;
} oneCCCBlock; // variable ici
//SPI initvoid
void SPIMasterInit(void) {
//set MOSI, SCK and SS as output
DDRB |= (1<<PB3)|(1<<PB5)|(1<<PB2);
SPCR = 0;
//enable master SPI at clock rate Fck/16 mode 3
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0)|(1<<CPHA)|(1<<CPOL);
//SPSR = (1<<SPI2X); // now clock rate Fck/8
}
//master send function
uint8_t SPI_transfer(uint8_t data){
//send data
SPDR=data;
//wait for transmition complete
while (!(SPSR &(1<<SPIF)));
return SPDR;
}
void SPI_sendData8XX(uint8_t *data, uint8_t howmany)
{
uint8_t data1 = 0;
uint8_t count=0;
for(count=0;count<howmany;count++)
{
data1 = data[count];
SPI_transfer(data1);
}
}
void SPI_recieveData8XX(uint8_t *data, uint8_t howmany){
uint8_t data1 = 0;
uint8_t count=0;
for(count=0;count<howmany;count++)
{
data1= SPI_transfer(0x00);
data[count] = data1;
}
}
uint8_t SPI_recieveExpectedData8XX(uint8_t *data, uint8_t howmany, uint16_t expectedHeader){
uint8_t data1 = 0, data2, waitingTries=0;
//PORTC &= 0xFE; // SSEL=CS low
uint8_t count=0;
// wait for answer
data2 = expectedHeader;
do {
data1= SPI_transfer(0x00);
if (data1 == data2) {
data[count] = data1;
count++;
} else {
waitingTries++;
}
if (waitingTries > 50) return 0;
} while(data1 != data2);
data2 = expectedHeader>>8;
do {
data1= SPI_transfer(0x00);
if (data1 == data2) {
data[count] = data1;
count++;
} else {
waitingTries++;
}
if (waitingTries > 50) return 0;
} while(data1 != data2);
for(count=2;count<howmany;count++)
{
data1= SPI_transfer(0x00);
data[count] = data1;
}
//PORTC |= 0x01; // SSEL=CS high
return count;
}
void serialWrite(unsigned char DataOut)
{
loop_until_bit_is_set(UCSR0A, UDRE0); /* Wait until data register empty. */
UDR0 = DataOut;
}
void serialPuts(char str[]){
uint8_t i=0;
do {
serialWrite(str[i]);
i++;
} while(str[i]!=0);
}
void serialputsData(uint8_t data[],uint8_t howMany){
uint8_t i,digit;
for (i=0;i<howMany;i++) {
digit = data[i] >> 4; if (digit <10) serialWrite(digit + '0'); else serialWrite(digit - 10 + 'A');
digit = data[i] & 0x0F; if (digit <10) serialWrite(digit + '0'); else serialWrite(digit - 10 + 'A');
serialWrite('-');
}
serialWrite(0x0D);serialWrite(0x0A);
}
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(void) {
uint16_t resolutionX=0;
//initialize master SPI
SPIMasterInit();
serialInit();
SPI_sendData8XX((char *) &requestRes, 5);
SPI_recieveExpectedData8XX((char *) &resolution,10, 0xC1AF);
serialputsData((char *) &resolution,10);
serialPuts("Largeur en pixels : ");
resolutionX=resolution.width;
serialWrite(resolutionX/100+'0');serialWrite((resolutionX%100)/10+'0');serialWrite(resolutionX%10+'0');
serialWrite(0x0D);serialWrite(0x0A);
while (1) {
SPI_sendData8XX(getCCC, 6);
SPI_recieveExpectedData8XX((char *) &oneCCCBlock,20, 0xC1AF);
serialputsData((char *) &oneCCCBlock,20);
// enfait position X :
resolutionX = oneCCCBlock.XCenter;
serialWrite(resolutionX/100+'0');serialWrite((resolutionX%100)/10+'0');serialWrite(resolutionX%10+'0');
serialWrite(0x0D);serialWrite(0x0A);
_delay_ms(1000);
}
}
Étant donné que l'entête retournée est toujours la même 0xC1AF il est possible de supprimer le paramètre du sous-programme correspondant :
uint8_t SPI_recieveExpectedData8XX(uint8_t *data, uint8_t howmany){
uint8_t data1 = 0, waitingTries=0;
uint8_t count=0;
// wait for answer OxAF
do {
data1= SPI_transfer(0x00);
if (data1 == 0xAF) {
data[count] = data1;
count++;
} else {
waitingTries++;
}
if (waitingTries > 50) return 0;
} while(data1 != 0xAF);
// wait for answer OxC1
do {
data1= SPI_transfer(0x00);
if (data1 == 0xC1) {
data[count] = data1;
count++;
} else {
waitingTries++;
}
if (waitingTries > 50) return 0;
} while(data1 != 0xC1);
for(count=2;count<howmany;count++)
{
data1= SPI_transfer(0x00);
data[count] = data1;
}
return count;
}
Il vous faut alors modifier tous les appels à ce sous-programme.
La structure de ce sous-programme est discutable : en principe il faut attendre les deux octets 0xAF et 0xC1 d'affilés alors que là ils peuvent être séparés par des octets indésirables ! La résolution et le test de ce problème sont laissés au lecteur.
Voir aussi
modifier- Processeur ATMega avec Arduino et SPI dans un autre projet où un esclave SPI est construit en VHDL et testé.
- (en) PixyCAM UART Interface (document PDF)
- (en) Comment apprendre à reconnaître des objets à la caméra Pixy2
- (en) Guide de communication avec la Pixy2
- Vidéo de la coupe de France de robotique GEII à Cachan
Communication I2C
modifierLe 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
modifierLes 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
modifierProgrammation 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
modifierProgrammation 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
modifier
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
modifierSi 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
modifierUn exemple peut être trouvé ICI sur Internet.
Communication USB
modifierIl 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
modifierVoir aussi
modifier- VUSB et particulièrement le projet sur le clavier
Une liaison série qui utilise la librairie VUSB
modifierVoir aussi
modifier- Serial USB for any AVR Microcontroller
- Page d'accueil de V-USB pour AVR
- GitHub pour la V-librairie pour Arduino
- Vidéo Youtube sur le sujet en anglais
Utilisation de la platine Arduino Leonardo pour réaliser une liaison série USB
modifierLa platine Arduino Leonardo est équipée d'un AVR32U4 qui permet de faire de l'USB de manière matérielle. L'environnement de développement Arduino détourne automatiquement les "Serial.print()" pour utiliser la liaison série à travers l'USB. Mais qu'en est-il si vous voulez travailler en C pur ? C'est à cette question que nous allons tenter de répondre dans cette section.
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.
Premier essai
modifierIl est possible de partir des fichiers Liaison série avec Leonardo trouvés dans le Wiki de l'IUT de Troyes.
Le fichier d'exemple du lien précédent comporte deux main() mais si vous le regardez attentivement le premier est entouré par un #if 0 suivi du code et suivi d'un else. Par défaut ce ne sera pas ce main qui sera compilé.
#if 0
// Very simple character echo test
int main(void)
{
CPU_PRESCALE(0);
usb_init();
while (1) {
int n = usb_serial_getchar();
if (n >= 0) usb_serial_putchar(n);
}
}
#else
L'autre main() réalise un mini terminal série avec comme écran d'accueil :
Teensy USB Serial Example, Simple Pin Control Shell Example Commands B0? Read Port B, pin 0 C2=0 Write Port C, pin 1 LOW D6=1 Write Port D, pin 6 HIGH (D6 is LED pin)
Sur la carte Arduino Leonardo la célèbre LED 13 est câblée sur le bit 7 du PORTC. Ainsi l'envoi de la commande C7=0 vous éteindra cette LED 13 tandis que C7=1 l'allumera....
Détails de la compilation du premier essai
modifierLe problème est que ces fichiers sources ne se compilent pas facilement avec l'environnement Arduino.
- En effet celui-ci essaie de compiler avec avr-g++ mais ce dernier refuse de compiler des structures contenant un tableau non dimensionné. Il faut le forcer à utiliser avr-gcc pour résoudre le problème. Un autre essai nous a montré qu'il utilisait en fait les deux compilateurs. Mais l'erreur de double définition des vecteurs d'interruptions 10 et 11 (spécifiques à l'USB) montre que dans ce cas il initialise déjà la liaison USB. Étrange, nous avions toujours pensé que la présence d'un "main()" dans un programme Arduino mettait de côté toutes les initialisations nécessaires au bon fonctionnement de l'environnement Arduino !
- L'utilisation de avr-gcc peut être directement réalisée avec un autre environnement que l'environnement Arduino comme par exemple Eclipse avec son plugin avr. Avec cet environnement tout ceci se compile directement.
- L'utilisation d'avr-gcc peut s'utiliser manuellement. Si vous avez l'environnement Arduino installé vous avez automatiquement la chaîne de compilation des AVR installée et vous disposez alors de deux possibilités pour compiler :
- réaliser un Makefile
- réaliser un script
Avec makefile
modifierVoici le makefile que l'on utilise pour compiler :
# All Target
all:example.o usb_serial.o example.elf example.hex
example.o:
avr-gcc -g -mmcu=atmega32u4 -Wall -Os -c example.c
usb_serial.o:
avr-gcc -g -mmcu=atmega32u4 -Wall -Os -c usb_serial.c
example.elf:
avr-gcc -g -mmcu=atmega32u4 -o example.elf -Wl,-Map,example.map example.o usb_serial.o
example.hex:
avr-objcopy -R .eeprom -R .fuse -R .lock -R .signature -O ihex example.elf "example.hex"
clean:
rm *.o
rm *.elf
rm *.hex
rm *.map
Et maintenant voici le script que l'on utilise pour utiliser avrdude :
#!/bin/bash
/home/serge/arduino-1.8.9/hardware/tools/avr/bin/avrdude -C/home/serge/arduino-1.8.9/hardware/tools/avr/etc/avrdude.conf -v -patmega32u4 -cavr109 -P/dev/ttyACM0 -b57600 -D -Uflash:w:example.hex:i
- le script fonctionne sous Linux et est très dépendant de votre installation de l'environnement Arduino.
- Notons que la carte Arduino Leonardo pose parfois des problèmes pour le téléchargement des programmes. Le problème ici est que le programme utilise un moniteur série (nous on prend celui de l'environnement Arduino) et qu'il ne faut pas oublier de le fermer pour un téléchargement de programme.
- Le port série est écrit dans le script (/dev/ttyACM0) et parfois il n'arrête pas de changer ! Aujourd'hui, par exemple, nous sommes montés jusqu'à /dev/ttyACM7. Ce changement inopportun, nous l'avons constaté sous Linux et sous Windows quand la liaison série USB sert à plusieurs choses (ici : téléchargement et liaison série). L'environnement Arduino est pratique pour connaître le nom de votre port série qui sera plutôt "COM7" ou autre sous Windows.