30 septembre 2012
... on intéressera à ceux de l’I2C
Sur micro-contrôleurs, la configuration des périphériques se fait en accédant aux registres et en leur affectant des valeurs conformément aux indications de la fiche technique du composant. Sur le Raspberry, nous sommes pour l’instant à la merci de la communauté Linux pour tout ce qui concerne l’accès au matériel. Pour reprendre notre exemple précédent sur l’I2C, nous n’avons trouvé aucun moyen pour régler la vitesse du bus à partir de Linux. Nous ne connaissons même pas la fréquence de l’horloge I2C. Or, en lisant la fiche technique, nous savons comment configurer un périphérique, sauf qu’il nous manquait quelques connaissance Linux...
Pourtant, la fiche technique du processeur nous donne toutes les informations nécessaires. Chapitre 3 : BSC, p28 de la fiche technique du BCM2835, nous obtenons les 3 adresses de base des registres :
BSC0 : 0x7E20_5000
BSC1 : 0x7E80_4000
BSC2 : 0x7E80_5000
Le tableau qui suit donne les adresses des registres par rapport à cette adresse de base. Le registre qui nous intéresse est DIV, il se trouve à l’adresse 0x14. Nous supposons que nous utilisions le bus BSC0, le registre que nous voulons lire (dans un premier temps) se trouve à l’adresse 0x7E80_5014.
Il s’agit ici des adresses du BUS. La page 5 nous donne une idée des équivalences entre le BUS, l’adresse physique et l’adresse virtuel. Nous comptons utiliser /dev/mem qui représente la mémoire physique. Il faut donc remplace le préfixe 0x7E par 0x20. Nous trouvons donc l’adresse 0x20205014.
En ouvrant le fichier /proc/iomem, nous avons la bonne surprise de découvrir que cette adresse semble correspondre au bus I2C. Mais en tenant le même raisonnement pour le BCS1, on tombe sur le bus spi. Ceci n’est pas forcément aberrent, puisque dans la fiche technique, les deux périphériques partagent les mêmes registres.
POIVRON@raspberrypi:/proc$ cat iomem
00000000-07ffffff : System RAM
00008000-0045b40f : Kernel text
0047c000-00525f3b : Kernel data
20000000-20000fff : bcm2708_vcio
20003000-20003fff : bcm2708_systemtimer
20007000-20007fff : bcm2708_dma.0
20007000-20007fff : bcm2708_dma
20100000-201000ff : bcm2708_powerman.0
20200000-20200fff : bcm2708_gpio
20201000-20201fff : dev:f1
20201000-20201fff : uart-pl011
20204000-202040ff : bcm2708_spi.0
20205000-202050ff : bcm2708_i2c.0
20215040-2021505f : serial
20300000-203000ff : bcm2708_sdhci.0
20300000-203000ff : mmc0
20804000-208040ff : bcm2708_i2c.1
20980000-2099ffff : bcm2708_usb
20980000-2099ffff : dwc_otg
Nous n’allons pas directement travailler avec /dev/mem, surtout si nous voulons par la suite y écrire, ce serait risquer de planter pas mal de choses. Nous allons apprendre à utiliser la fonction mmap.
mmap permet d’adresser une partie d’un fichier, au lieu d’accéder à l’ensemble du fichier. Il faut lui spécifier un accès vers le fichier, l’adresse dans le fichier à partir de laquelle nous travailler et la taille de la partie qui nous intéresse. La page man de mmap contient de précieuses informations.
Nous commençons par ouvrir le fichier en question :
int fd;
fd = open("/dev/mem", O_RDONLY);
On note deux choses : que l’accès à /dev/mem est réservé à root, il faudra donc lancer le programme avec les droits nécessaires et qu’on utilise le drapeau O_RDONLY pour ne rien casser.
Ensuite nous ouvrons la portion désirée avec mmap. L’adresse doit être un multiple de la taille des pages mémoires. On obtient la taille d’une page mémoire avec :
sysconf(_SC_PAGE_SIZE)
Le code suivant permet de trouver le multiple de la taille des pages, inférieur à notre valeur, le plus proche de notre valeur avec très peu de calcul (pour la machine). Il n’est valable que si les pages mémoire ont une taille égale à une puissance de 2.
int offset_mmap;
int offset = 0x20205000;
offset_mmap = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
La longueur exacte qui nous intéresse est plus difficile à déterminer. Dans la fiche technique, les registres de 32 bits sont aux adresses suivantes : 0x00, 0x04, 0x08, 0x0C (12), 0x10 (16), 0x14 (20), 0x18 (24), 0x1C (28). On pourrait supposer que la zone d’intérêt s’étale sur 29 emplacement mémoire (de 0 à 28). Il n’en n’est rien ! Les adresses de la fiche technique sont données comme si le processeur adressait des registre de 8 bits. Or la puce dispose de registres de 32 bits. C’est pour cela qu’un registre de 32 bits tient sur 4 emplacements. Nous voulons donc que mmap nous ramène le contenu de 8 emplacements mémoire de 32 bits.
Nous indiquons ensuite si la zone mémoire récupérée sera synchronisée avec son emplacement d’origine ou pas. Nous utilisons MAP_PRIVATE pour travailler sur une copie locale, et éviter que nos modifications se répercutent sur les registres ou la RAM.
Nous devons aussi indiquer si notre zone de travail pourra être lu, écrite et exécutée. Avec PROT_READ | PROT_WRITE, nous autorisons la lecture et l’écriture.
Le premier argument permet de spécifier où mmap doit installer notre espace de travail. En mettant 0, nous indiquons que nous n’avons pas de préférence. Enfin, nous transformons le pointeur retourné par mmap en un pointeur sur entier (int). Ceci nous permettra de visiter la mémoire sous forme de tableau.
int * acces_I2C_0;
int length = 8;
acces_I2C_0 = (int *) mmap(0,length + offset - offset_mmap,
PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, offset_mmap);
Il ne nous reste plus qu’a afficher le contenu du tableau acces_I2C_0. Nous utilisons le coté obscure du printf pour un affichage en hexadécimal.
for (i =0; i<length; i++){
printf( "0x%02.2X - 0x%08.8X\n",i*4,registres[i]);
}
Nous utilisons toujours gcc pour la compilation, sans makefile
gcc fichier.c -o nom_du_programme
Notre code de test est un peu plus épais puisqu’il teste que chaque étape se soit bien déroulée avant de passer à la suivante. Ceci est vital et vous permettra de comprendre rapidement que si votre code ne marche pas, c’est simplement que vous avez oublié de le lancer avec les droits requis (un exemple parmi d’autres).
Voici nos relevés
0x00 - 0x00000000
0x04 - 0x00000050
0x08 - 0x00000000
0x0C - 0x00000000
0x10 - 0x00000000
0x14 - 0x000005DC
0x18 - 0x00300030
0x1C - 0x00000040
0x00 - 0x00000000
0x04 - 0x00000050
0x08 - 0x00000006
0x0C - 0x00000052
0x10 - 0x00000052
0x14 - 0x000009C4
0x18 - 0x00300030
0x1C - 0x00000040
On remarque aisément le 52 à l’adresse 0x0C censé contenir l’adresse de l’esclave. On retrouve cette valeur en 0x10 qui contient l’octet à envoyer.
Ce qui nous intéresse est au registre 0x14. La fréquence de l’horloge de l’I2C est cette de la puce divisée par 0x9C4. On voit bien qu’avant l’utilisation cette valeur est à la valeur par défaut donnée dans la fiche technique. Le BCM tournerai à 250 Mhz, ce qui nous donne une fréquence pour l’I2C de 100 kHz (0x9C4 = 2500).
Le registre C (pour control) nous semble inaccessible. Ou alors il n’est lu qu’après que les opérations I2C se soient terminée. Bref, il reste obstinément à 0.
Le registre DLEN (data length) contient la taille des donnés à transmettre. Il vaut généralement 6 (la réceptions des 6 octets de la manette) mais on le voit aussi à 1 (pour l’envoi du registre à lire). Ces valeurs dépendent, bien entendu, des transactions I2C en cours.
Le registre DEL qui permet un réglage précis temps entre un changement d’état de l’horloge I2C (SCL) et un changement d’état du bus de données I2C (SDA). Celui-ci est laissé à sa valeur par défaut.
Enfin le registre CLKT, qui permet de définir le temps maximal dont dispose un esclave pour répondre ACK. Ce registre est aussi laissé à sa valeur par défaut.
Voici le code que nous avons utilisé. Nous avons fait tourner ce programme en même temps que celui pour lire l’état de la manette de Wii.
Il est bien possible de modifier les registres. Il faut ouvrir /dev/mem avec le drapeau O_RDWR et utiliser mmap avec les drapeaux PROT_READ | PROT_WRITE et MAP_SHARED. MAP_SHARED nous garantit que la mémoire sera synchronisée après un appel de munmap ou de msync. Bref, notre programme de lecture nous permettra de vérifier que l’opération s’est bien déroulée. Nous créons donc deux programmes, l’un pour écrire la valeur, l’autre pour lire les registres.
On s’apercevra avec déception que la valeur de l’horloge I2C est réécrite par le pilote à chaque opération de lecture/écriture sur le bus. Il ne nous est donc pas possible d’utiliser le pilote Linux et de définir notre horloge. Un petit tour dans le code du noyau nous montrera que cette valeur est codée en dure. Faudra-t-il recompiler un noyau pour changer la vitesse de l’I2C ?