Beaglebone Black en tant que boussole

 druide

But du projet

Dans le cadre de mes cours d'atelier, j'ai trouvé intéressant de mettre en place différents éléments pour transformer la carte Beaglebone Black en boussole. Ce n'est pas une application "très réaliste" mais plus un travail de laboratoire permettant l'acquisition et la révision de notions de base.

Découverte de l'i2c

Pour réaliser ce travail, j'aurai besoin de l'interface i2c. Cette interface, ce bus ou cette norme (toutes ces dénominations désignent l'i2c), a été développée par Philips en 1982. De par sa robustesse et le nombre de périphériques compatibles i2c, cette interface a su s'imposer sur la durée si bien qu'on trouve encore actuellement beaucoup de périphériques i2c.


Le principe de l'i2c c'est de faire dialoguer un maître et un esclave via deux lignes nommées SDA (Serial DAta) et SCL (Serial CLock).
Plusieurs maîtres et plusieurs esclaves peuvent cohabiter sur les mêmes lignes SDA, SCL grâce à un adressage réalisé sur 7 bits.

source:wikipedia.org


Dans ce projet, la carte BBB jouera le rôle du maître. Pour permettre au maître de dialoguer avec un périphérique, on devra être en mesure de trouver l'adresse dudit périphérique.

HMC5883L

Le périphérique en question est un magnétomètre de Honeywell. Plusieurs fournisseurs ont créés des « breakout » pour ce capteur. On en trouve un par exemple chez Sparkfun. Ce capteur HCM5883L est de type magnéto-résistif. Il comporte 3 axes nommés X, Y et Z. Ci-dessous une représentation du breakout Sparkfun:


source: sparkfun.com


Le capteur dispose de plusieurs mode de fonctionnement, configurable par des registres, dans lesquels on peut choisir le nombre de mesures par seconde, la précision de la mesure, nombre d’échantillons à prendre pour faire une moyenne, un mode « single/continuous » et le gain.


Pour « le gain », on peut comprendre que plus le gain est élevé, moins le capteur est précis mais plus il peut mesurer des champs forts. La valeur par défaut est choisie pour des mesures de faibles champs, tel que le champ magnétique terrestre mais il est tout à fait possible de mesurer des aimants permanents si on change la valeur du gain pour ne pas saturer le capteur à pleine échelle si on approche à peine l'aimant.

Comme je ne maîtrise pas tous les aspects d'une mesure de magnétisme, j'ai fait confiance aux valeurs par défaut du fabriquant. Bien entendu, rien ne vous empêche de faire des tests avec d'autres valeurs, le code (plus bas dans le document) le permet...

Mesurer le Nord

Dans un monde parfait, les lignes de champs magnétiques seraient toutes perpendiculaires aux pôles. Elles seraient toutes parallèles entre elles et elles ne changeraient pas d'un jour à l'autre. Malheureusement, nous ne vivons pas dans ce monde.

Dans le nôtre c'est tout le contraire. Les lignes de champs magnétiques varient dans le temps:

source: geomag
Cette variation s'appelle la déclinaison. On peut connaître la déclinaison en un point donnée de la terre grâce à ce site web magnetic-declination



On peut voir que la déclinaison pour Neuchâtel est de +1° 33'. Comme les calculs que j'effectuerais par la suite se feront en radian, je dois faire la conversion de degrés minutes en radian. Pour rappel, il y a π radian dans 180° donc:



Comme c'est une déclinaison positive, il faudra ajouter cette valeur à mon calcul.

Les vecteurs

Comme on peut s'en rendre compte sur l'image ci-dessous, les lignes de champs magnétiques autour de la terre au niveau de Neuchâtel sont presque perpendiculaires à l'horizon. Elles « entres » dans la terre (ou en sortent) avec un angle > 60°.


source: planet-voda


Quel est l'impact sur le capteur ?

Ce dernier est composé de 3 capteurs, un par axe. Il est donc en mesure de « voir » le champ magnétique terrestre de 3 points de vue différents, ou de 3 « fenêtres » (analogie)...


En positionnant l'axe Z parallèle aux lignes de champ magnétique terrestre, ou dit d'une autre manière, en alignant l'axe Z avec la ligne de champ magnétique terrestre qui le traverse, on force la valeur de mesure du capteur par la « fenêtre » Z à son maximum. Si on retourne de 180° le capteur, on force cette même valeur à son minimum. Ça peut être un moyen de calibrage du capteur pour l'aligner sur le champ magnétique terrestre à l'endroit où on se trouve.

Le champ magnétique qui traverse alors le capteur est également vu par les deux autres « fenêtres ». Il a alors, sur ces deux fenêtres, une action qui est proportionnelle à l'angle qu'il forme avec les deux axes. C'est cette propriété que je vais utiliser pour en déduire la position du Nord magnétique.

Pour la mesure, je vais partir du principe que le capteur est posé à plat et que la mesure sur l'axe Z ne change pas si on tourne le capteur sur lui-même (rotation autour de l'axe Z). Il ne reste alors que des variations sur les deux autres axes X, Y. Ces variations sont mesurées par le capteur qui me fournit des valeurs dont les unités ne sont finalement pas importantes.


On peut voir la flèche « r » de l'image ci-dessus comme l'aiguille de la boussole et les flèches x, y comme les valeurs fournies par le capteur.

Il ne reste qu'à faire un peu de trigonométrie pour trouver l'angle α.


Et lui ajouter la déclinaison.

// Read all registers
if (read(file, read_buffer, 13) != 13)
{

short x = (read_buffer[3] << 8) | read_buffer[4]; short y = (read_buffer[7] << 8) | read_buffer[8];


float heading = atan2(y, x);
// Add declinaison for Neuchâtel
heading += 0.02705f;
float headingDegrees = heading * 180.0f / M_PI;
}




Branchement et test avec le shell

La carte BBB comporte deux interfaces i2c


J'ai choisi d'utiliser:

  • PIN 1 pour GND (NOIR)
  • PIN 3 pour DC 3.3V (ROUGE)
  • PIN 19 pour SCL (Serial CLock) (ORANGE)
  • PIN 20 pour SDA (Serial DAta) (BLANC)


Maintenant que tout est câblé, il faut utiliser les outils i2c pour obtenir un peu plus d'informations.

Détection des interfaces i2c


root@beaglebone:~# i2cdetect -l
i2c-0	i2c       	OMAP I2C adapter                	I2C adapter
i2c-1	i2c       	OMAP I2C adapter                	I2C adapter

 

Détection des périphériques sur l'interface i2c-1


root@beaglebone:~# i2cdetect -r 1
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1 using read byte commands.
I will probe address range 0x03-0x77.
Continue? [Y/n] 
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- UU UU UU UU -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --  

Notre capteur est bien présent avec son adresse 0x1e

Dump des registres du capteur


root@beaglebone:~# i2cdump 1 0x1e
No size specified (using byte-data access)
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1, address 0x1e, mode byte
Continue? [Y/n] 
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: 70 a0 00 00 a6 00 f4 ff c2 03 48 34 33 00 00 3c    p?..?.?.??H43..<
10: 00 00 00 00 00 00 00 00 00 00 00 06 18 14 e8 10    ...........?????
20: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00    ........?.......
30: 00 00 00 14 05 66 76 00 90 00 07 00 00 00 00 00    ...??fv.?.?.....
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
80: 70 a0 00 00 a7 00 f6 ff c2 03 48 34 33 00 00 3c    p?..?.?.??H43..<
90: 00 00 00 00 00 00 00 00 00 00 00 06 18 14 e8 10    ...........?????
a0: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00    ........?.......
b0: 00 00 00 14 05 66 76 00 90 00 07 00 00 00 00 00    ...??fv.?.?.....
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................


Dans ce dump on peut voir les valeurs actuelles des registres du capteur:


Par exemple, le registre de configuration A contient actuellement la valeur 0x70. La valeur de l'axe X est actuellement de X (MSB) + X (LSB) soit 0x00a6 etc...

Code C

Je peux maintenant faire un programme C qui permet la lecture et l'écriture des registres A, B, Mode, X, Y et Z.

Pointeur interne

Dans le but de réduire les échanges entre le maître et l'esclave, le capteur intègre en interne un pointeur d'adresse qu'il incrémente à chaque lecture/écriture. Ainsi, si on souhaite écrire une valeur dans le registre A, il faut envoyer deux octets. Le premier pour initialiser le pointeur interne sur l'adresse du registre A, le second avec la valeur à écrire dans le registre.

Exemple:

/*
* Configuration Register A
* Register: 0x00

* Value : 0x70 -> 0111'0000

* Select number of samples averaged to 8
* Data Output Rate Bits to 15 (default)
*/
rd_wr_buffer[0] = 0x00;
rd_wr_buffer[1] = 0x70;
write_register(file, rd_wr_buffer, 2);

Ça fait deux octets au lieu de 1!!

Dans le cas d'une lecture, on commence par placer le pointeur sur l'adresse de début et on lit en une seule fois plusieurs valeurs car le capteur incrémente automatiquement la valeur du pointeur d'adresse interne après chaque lecture.
Exemple:

unsigned char reg_buffer[0] = 0x00;
unsigned char read_buffer[13];

if (write(file, reg_buffer, 1) != 1)
{
fprintf(stderr, "Failed to initialize internal pointer to address 0x00\n");
}

if (read(file, read_buffer, 13) != 13)
{
// read_buffer contains 13 bytes from address 0x00 to address 0x12
}

Pour réaliser le programme complet, je me suis basé sur celui fournit par Robert Edwards. Je l'ai ensuite adapté à mes besoins, notamment sur le fait d'écrire la valeur résultante du calcul dans un fichier texte (/var/www/heading.txt) au lieu de l'afficher.

[collapse collapsed title=Source code]

/*
* Author: Robert Edwards
*
* BeagleBone Black driver for HMC5883L magnetometer.
* Communication to magnetometer is I2C protocal using
* the i2c-1 port on the BeagleBone Black.
*
* I2C2_SCL - pin 19 P9
* I2C2_SDA - pin 20 P9
*
* Linux port is i2c-1 but pinout is I2C2 (19,20)
*

* Modified: Pierre Ferrari

*
* Addapting the original program for CPLN-ET lesson.
*/

#include #include #include #include #include #include #include #include #include #include #include


// DELAY = 500ms because usleep work with micro seconds
#define DELAY 5*1000*100
#define ADDR 0x1e
// #define DEBUG 1

/* We can find this address with ic2detect
*
* ubuntu@bb1:~$ sudo i2cdetect -r 1
* WARNING! This program can confuse your I2C bus, cause data loss and worse!
* I will probe file /dev/i2c-1 using read byte commands.
* I will probe address range 0x03-0x77.
* Continue? [Y/n] y
* 0 1 2 3 4 5 6 7 8 9 a b c d e f
* 00: -- -- -- -- -- -- -- -- -- -- -- -- --
* 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e --
*/

int write_register(int f, char* buffer, int size)
{
int ret = write(f, buffer, size);
#ifdef DEBUG
if (ret != size)
{
fprintf(stderr, "Failed to write to I2C bus.\n");
}
else
{
fprintf(stdout, "0x%d to 0x%d\n", *buffer, *(buffer+1));
}
#endif
return ret;
}

int main(int argc, char** argv)
{
int file;
char filename[] = "/dev/i2c-1";

/*
* Stores values read from registers (13 registers)
*
*/
char read_buffer[13] = {0};

// Data to be written to device. 2 bytes (port, value)
char rd_wr_buffer[2] = {0};

// Value of first register
char reg_buffer[1] = {0};

// Value of first register
unsigned char register_value = 0x00;

if ((file = open(filename, O_RDWR)) < 0)

{
perror("Failed to open the i2c bus");
exit(EXIT_FAILURE);
}

if (ioctl(file, I2C_SLAVE, ADDR) < 0)

{
printf("Failed to acquire buss access and/or talk to slave. \n");
exit(EXIT_FAILURE);
}

#ifdef DEBUG
fprintf(stdout, "[Setting Registers A, B, Mode]\n");
#endif

/*
* Configuration Register A
* Register: 0x00

* Value : 0x70 -> 0111'0000

* Select number of samples averaged to 8
* Data Output Rate Bits to 15 (default)
*/
rd_wr_buffer[0] = 0x00;
rd_wr_buffer[1] = 0x70;
write_register(file, rd_wr_buffer, 2);

/*
* Configuration Register B
* Register: 0x01

* Value : 0x20 -> 0010'0000

* Gain Configuration Bits:
* Recommended Sensor Field Range: ±1.3 Ga
* Gain (LSb/Gauss): 1090
* Digital Resolution (mG/LSb): 0.92
* Output Range: 0xF800–0x07FF (-2048–2047)
*/
rd_wr_buffer[0] = 0x01;
rd_wr_buffer[1] = 0xA0;
write_register(file, rd_wr_buffer, 2);

/*
* Mode Register
* Register: 0x02
* Value : 0x00

* Mode Select Bits -> Continuous-Measurement Mode

*/
rd_wr_buffer[0] = 0x02;
rd_wr_buffer[1] = 0x00;
write_register(file, rd_wr_buffer, 2);

/*
* In continuous-measurement mode, the device continuously performs
* measurements and places the result in the data register.
* RDY goes high when new data is placed in all three registers.
* After a power-on or a write to the mode or configuration
* register, the first measurement set is available from all
* three data output registers after a period of 2/fₒ and
* subsequent measurements are available at a frequency of fₒ,
* where fₒ is the frequency of data output.
*/
usleep(6*1000);

for(;;)
{
/*
* The devices uses an address pointer to indicate which
* register location is to be read from or written to.
* These pointer locations are sent from the master to this
* slave device and succeed the 7-bit address (0x1E) plus 1 bit
* read/write identifier, i.e. 0x3D for read and 0x3C for write.
* To move the address pointer to a random register location,
* first issue a "write" to that register location with no data
* byte following the commend. For example, to move the address
* pointer to register 10, send 0x3C 0x0A.
*
* Reset Register Ptr to first register
*/
reg_buffer[0] = register_value;
if (write(file, reg_buffer, 1) != 1)
{
fprintf(stderr, "Failed to write to I2C bus.\n");
}

//Using I2C read
if (read(file, read_buffer, 13) != 13)
{
/* Read 13 bytes into "read_buffer" then check 13 bytes
* were read Internal HMC5883L Addr Ptr automatically
* incremented after every read
*/
fprintf(stderr, "Failed to read from the I2C bus: %s.\n",
strerror(errno));
}
else
{

short x = (read_buffer[3] << 8) | read_buffer[4];

#ifdef DEBUG

short z = (read_buffer[5] << 8) | read_buffer[6];

#endif

short y = (read_buffer[7] << 8) | read_buffer[8];


float declinationAngle = 27.05f / 1000.0f; // Valeur pour Neuchâtel
float heading = (atan2(y, x) + declinationAngle) + M_PI;
float headingDegrees = heading * 180.0f / M_PI;

FILE *f = fopen("/var/www/heading.txt", "w");
if (f)
{
fprintf(f, "%.2f", headingDegrees);
fclose(f);
}
#ifdef DEBUG
fprintf(stdout, "Heading = %.2f Degrees = %.2f (%i:%i:%i)\n", heading, headingDegrees, x, y, z);
#endif
}
usleep(DELAY);
}
return 0;
}

[/collapse]

Il ne reste plus qu'à compiler le programme et le démarrer (^C indique que pour quitter le programme j'ai pressé les touches clavier CTRL+C) :

root@beaglebone:~# g++ -Wall -std=c++0x compass.c -o compass
root@beaglebone:~# ./compass
^C


Une nouvelle valeur (l'angle) est écrite toutes les 500ms dans le fichier texte:


root@beaglebone:~# cat /var/www/heading.txt 
67.67

La boussole

Pour réaliser une boussole à l'aide de mon capteur, je suis parti du tutoriel suivant : geeksretreat Dans ce tutoriel, l’auteur explique comment utiliser un canvas pour dessiner deux images, le cadran de la boussole et son aiguille. L'image de l'aiguille subira une rotation en fonction d'un angle (ctx.rotate(degrees)) après lui avoir fait faire une translation à l'origine (ctx.translate(x, y)). Le résultat peut-être vu à cette adresse. Le code source se trouve quand à lui à cette adresse. Il ne reste plus qu'à adapter le code source pour l'angle de rotation de l'aiguille provienne du fichier texte écrit par mon programme. Cette adaptation se fait en modifiant la méthode draw pour qu'elle reçoive la valeur en paramètre:

function draw(degrees) {
    ...
    ̶d̶e̶g̶r̶e̶e̶s̶ ̶+̶=̶ ̶5̶;̶
}

 ̶f̶u̶n̶c̶t̶i̶o̶n̶ ̶i̶m̶g̶L̶o̶a̶d̶e̶d̶(̶)̶ ̶{̶
 ̶ ̶/̶/̶ ̶I̶m̶a̶g̶e̶ ̶l̶o̶a̶d̶e̶d̶ ̶e̶v̶e̶n̶t̶ ̶c̶o̶m̶p̶l̶e̶t̶e̶.̶
 ̶ ̶S̶t̶a̶r̶t̶ ̶t̶h̶e̶ ̶t̶i̶m̶e̶r̶ ̶ ̶/̶/̶s̶e̶t̶I̶n̶t̶e̶r̶v̶a̶l̶(̶d̶r̶a̶w̶,̶ ̶1̶0̶0̶)̶;̶
 ̶}̶

function init() {
  ...
 ̶ ̶i̶m̶g̶.̶o̶n̶l̶o̶a̶d̶ ̶=̶ ̶i̶m̶g̶L̶o̶a̶d̶e̶d̶;̶
}


La page web devient alors:

[collapse title=Page web]





  
  
  
  
  
  


	

$(document).ready(function() {
var myVar = setInterval(function(){ myTimer() }, 1000);

function myTimer() {
   $.ajax({
       url : "heading.txt",
       dataType: "text",
       success : function (data) {
           draw(data);
       }
   });
  }
}); 



[/collapse]


Il faut bien entendu, placer les deux images à la racine de votre site web ou alors, adapter les chemins d'accès dans le script.




Lienothèque

devinter slug.blog.aeminium.org jarzebski meccanismocomplesso wolframalpha geeksretreat Robert Edwards

Tags: BBB HMC5883L boussole c

  • 4 years 1 month before
  • |