Accéléromètre 3 axes MMA7660FC

Ce tutoriel explique comment mettre en oeuvre l’accéléromètre 3 axes Grove en MicroPython. Le capteur utilisé est basé sur le composant MMA7660FC de NXP Semiconductorsque l’on retrouve sur les cartes Pyboard v1.1.

Matériel requis

  1. Une carte d’extension de base Grove
  2. La carte NUCLEO-WB55
  3. Un module accéléromètre 3 axes Grove

Le module Grove accéléromètre numérique 3 axes - 1.5g :

Grove - Accéléromètre 3 axes MMA7660FC version 1.3

Crédit images : Seeed Studio

On sera attentif à la sérigraphie sur la platine du module, qui indique l’orientation des axes de l’accéléromètre (y vers le bas, x vers la gauche et z vers nous, “sortant de l’écran” dans la photo ci-dessus). Cette information est très importante lorsqu’on souhaite exploiter ce module avec d’autres apportant des fonctions inertielles complémentaires (magnétomètres et/ou gyroscopes MEMS) dans le cadre de la problématique de la fusion de données, ou tout simplement lorsqu’on souhaite exploiter des fonctions avancées de positionnement dans l’espace (voir la section Le code MicroPython en utilisant une classe “pilote” plus bas).

Le code MicroPython sans utiliser une classe

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Dans un premier temps, nous allons mettre en oeuvre l’accéléromètre dans le script main.py sans utiliser (importer) une classe contenant son pilote. L’objectif est d’expliquer pas à pas comment on commande l’accéléromètre. Une lecture attentive du script qui suit vous permettra de comprendre comment sont programmés tous les composants électroniques dotés d’un microcontrôleur : en lisant et/ou écrivant les valeurs adéquates dans les registres adéquats.

Vous pouvez imaginer le circuit électronique qui commande le capteur comme un ensemble de “blocs” qui ont chacun une fonction précise : démarrer le capteur, arrêter le capteur, lire les mesures du capteur, définir la fréquence de celles-ci, etc. Le comportement de chacun de ces blocs est commandé par une mémoire composée d’un certain nombre d’octets qui l’on appelle un registre.

Pour écrire des valeurs dans un registre donné, on doit préciser son adresse, un nombre entier qui peut être codé en base binaire, décimale ou hexadécimale (peu importe). Ensuite, on écrit à l’adresse du registre, donc dans celui-ci, les valeurs qui conviennent pour activer la fonction qui nous intéresse, ce seront ici encore des entiers encodés dans la base qui nous arrange.
Par exemple, pour démarrer l’accéléromètre, nous avons besoin des informations suivantes :

  • Identifier à quelle adresse MMA7660FC_ADR il se trouve sur le bus I2C. Cette information est donnée par la fonction i2c.scan() ou encore par la fiche technique du capteur. On a MMA7660FC_ADR = 0x4c (soit 76 en décimal).
  • Identifier l’adresse MMA7660FC_SMR du registre du capteur qui permet de sélectionner son mode de fonctionnement. Cette information est disponible dans la fiche technique du MMA7660FC. On a MMA7660FC_SMR = 7.
  • Connaître la valeur MMA7660FC_ACTIVE que l’on doit écrire dans le précédent registre pour activer le capteur. De nouveau, en se référant à la fiche technique du capteur, on trouve MMA7660FC_ACTIVE = 1.

Finalement, la fonction qui permet d’écrire la valeur MMA7660FC_ACTIVE dans le registre MMA7660FC_SMR du capteur situé à l’adresse MMA7660FC_ADR du bus i2c est : i2c.writeto_mem(MMA7660FC_ADR, MMA7660FC_SMR, MMA7660FC_ACTIVE)

Nous n’entrerons pas dans les détails de la syntaxe MicroPython (qui peuvent nécessiter quelques heures d’essais et erreurs pour un débutant !), celle-ci est déduite des exemples de la documentation officielle, et d’autres trouvés sur Internet.

Editez maintenant le script main.py :

# Objet du script : Mise en oeuvre de l'accéléromètre 3 axes MMA7660FC (+/- 1.5g)
# Datasheet : https://www.nxp.com/docs/en/data-sheet/MMA7660FC.pdf
# Cet exemple est adapté de :
# https://github.com/ControlEverythingCommunity/MMA7660FC/blob/master/Python/MMA7660FC.py

from machine import I2C # Pour gérer le bus I2C
import time # Pour gérer les temporisations

# On utilise l'I2C n°1 de la carte NUCLEO-WB55 pour communiquer avec le capteur
i2c = I2C(1) 

# Pause d'une seconde pour laisser à l'I2C le temps de s'initialiser
time.sleep_ms(1000)

# Liste des adresses I2C des périphériques présents
print("Adresses I2C utilisées : " + str(i2c.scan()))

# Adresse du capteur sur le bus I2C : 0x4C (76) 
MMA7660FC_ADR = const(76)

# Adresse du registre de sélection du mode : 0x07 (7)
MMA7660FC_SMR = const(7)

# Mode "standby" : 0x00 (0)
MMA7660FC_STANDBY = b'\x00'

# Mode "Actif" : 0x01 (1)
MMA7660FC_ACTIVE = b'\x01'

# Adresse du registre permettant de fixer la fréquence d'échantillonnage : 0x08(8)
MMA7660FC_SRR = const(8)

# Adresse du registre de sortie exposant les 3 octets contenant les accélérations : 0x00 (0)
MMA7660FC_ODR = const(0)

# La fréquence d'échantillonnage sera deux mesures par seconde : 0x06(6)
# On peut changer cette valeur pour des mesures plus fréquentes, jusqu'à 120 par seconde
# (voir https://www.nxp.com/docs/en/data-sheet/MMA7660FC.pdf)
MMA7660FC_SRATE1 = b'\x07'
MMA7660FC_SRATE2 = b'\x06'
MMA7660FC_SRATE4 = b'\x05'
MMA7660FC_SRATE8 = b'\x04'
MMA7660FC_SRATE16 = b'\x03'
MMA7660FC_SRATE32 = b'\x02'
MMA7660FC_SRATE64 = b'\x01'
MMA7660FC_SRATE120 = b'\x00'

# Passe en mode "standby"
# - écrit dans la mémoire du périphérique I2C situé à l'adresse MMA7660FC_ADR
# - écrit à partir de l'adresse MMA7660FC_SMR
# - écrit les octets contenus dans MMA7660FC_STANDBY, qui doivent être placés dans un tableau
i2c.writeto_mem(MMA7660FC_ADR, MMA7660FC_SMR, MMA7660FC_STANDBY)

# Programme la fréquence d'échantillonnage (seize mesures par seconde)
i2c.writeto_mem(MMA7660FC_ADR, MMA7660FC_SRR, MMA7660FC_SRATE16)

# Passe en mode "actif" 
i2c.writeto_mem(MMA7660FC_ADR, MMA7660FC_SMR, MMA7660FC_ACTIVE)

# Pause de 500 millisecondes pour s'assurer que l'écriture est bien terminée
time.sleep_ms(500)

# Facteur de conversion entre les valeurs lues dans les registres et l'accélération physique
# exprimée en g.
RAW_TO_G = 0.047

while True: # Boucle sans clause de sortie

	# Lecture du vecteur d'accélération : trois octets à partir de l'adresse du registre de sortie 
	# MMA7660FC_ODR
	data = i2c.readfrom_mem(MMA7660FC_ADR, MMA7660FC_ODR, 3)

	time.sleep_ms(500)

	# Les valeurs de l'accélération sont codées sur les six premiers bits (de droite à gauche) de 
	# chaque octet.
	# On doit donc appliquer un masque binaire sur les octets lus, avec l'opération logique "&" afin 
	# de mettre à zéro les deux bits les plus à gauche.
	# Le masque qui convient est donc  00111111 (en binaire) = 0x3F (en hexadécimal).

	xAccl = data[0] & 0x3F

	# On recentre le résultat non signé codé sur 6 bits de l'intervalle [0, 64] dans l'intervalle
	# [-32, 31] afin de restituer le signe de l'accélération suivant chaque axe (complément à deux).
	
	if xAccl > 31 :
		xAccl -= 64

	yAccl = data[1] & 0x3F
	if yAccl > 31 :
		yAccl -= 64

	zAccl = data[2] & 0x3F
	if zAccl > 31 :
		zAccl -= 64

	# Affichage des accélérations en g, en appliquant le facteur de conversion RAW_TO_G
	# Les données sont affichées comme des nombres décimaux avec 1 chiffre après la virgule (%.1f)

	print("Acceleration axe X : %.1f g" %(xAccl * RAW_TO_G))
	print("Acceleration axe Y : %.1f g" %(yAccl * RAW_TO_G))
	print("Acceleration axe Z : %.1f g" %(zAccl * RAW_TO_G))

	# Temporisation d'un quart de seconde
	time.sleep_ms(250)

Affichage sur le terminal série de l’USB User :

Une fois le script lancé avec CTRL-D, faites varier l’orientation de l’accéléromètre et observez les valeurs affichées. Notez que la valeur de l’accélération mesurée selon un axe sera d’autant plus proche de +/-1g que celui-ci sera aligné avec la verticale ; situation dans laquelle il mesure l’accélération de la pesanteur terrestre.

One Wire output

Le code MicroPython en utilisant une classe “pilote”

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Le code donné ci-avant est fonctionnel, mais il présente un énorme défaut : il mélange la problématique du pilotage de l’accéléromètre avec celle de l’application utilisateur. Il serait plus judicieux de séparer les deux en créant une classe MicroPython qui contiendrait toutes les fonctions (on parle alors de méthodes) permettant de piloter l’accéléromètre et qui serait ensuite importée par le script main.py.

De cette façon :

  • main.py deviendrait plus lisible puisqu’on n’aurait plus besoin d’y inscrire toutes les instructions relatives à la manipulation des registres du MMA7660FC ;
  • L’utilisation de l’accéléromètre serait facilitée car n’importe quel programmeur disposant de sa classe pilote pourrait le commander en appelant les méthodes de cette classe ; en leur passant si nécessaire les bons arguments.

L’écriture d’un pilote sous forme de classe est ce que l’on appelle une abstraction du matériel. Si on programme directement les registres du MMA7660FC, il faut connaitre les adresses de ses registres, leurs fonctions et les valeurs que l’on doit y écrire. En utilisant une classe, on passe par des méthodes qui réalisent ces opérations à la place du programmeur ce qui rend le programme à la fois plus lisible et plus facile à modifier et à corriger.

Voci le contenu du script mma7660.py, implémentation de la classe pilote :

# Classe qui implémente le pilote de l'accélèromètre 3 axes MMA7660FC (+/- 1.5g)
# Fonctions du registre TILT ajoutées à partir des explications :
# - De l'ouvrage "MicroPython et Pyboard - Python sur microcontrôleur : 
#   de la prise en main à l'utilisation avancée".
#   Auteur : Dominique Meurisse, ISBN-10 : 2409022901, ISBN-13‏ :‎ 978-2409022906
# - Des exemples fournis par Frédéric Boulanger, CentraleSupélec - Département Informatique
#   https://github.com/Frederic-soft/pyboard/blob/master/MMA7660/MMA7660.py

_DEVICE_ADDRESS = const(0x4C) # Adresse de l'accéléromètre sur le bus I2C

# Adresse du registre de sortie exposant les 3 octets contenant les accélérations
#  Accélération selon x : adresse = 0
#  Accélération selon y : adresse = 1
#  Accélération selon z : adresse = 2
_OUTPUT_REG = const(0)

# Adresse du registre de détection de conditions particulières
_TILT_REG = const(3)

# Adresse du registre de paramétrage des interruptions de l'accéléromètre
_INTSU_REG = const(6)

# Paramètres pour le registre INTSU
# Active toutes les interruptions
_INT_SET = b'\xFF'

# Adresse du registre permettant de fixer la fréquence d'échantillonnage
_SR_REG = const(8)

# Paramètres pour le registre SR
# Fréquence de mesure / échantillonnage (en nb par seconde)
#_SRATE1 = b'\x07'
#_SRATE2 = b'\x06'
#_SRATE4 = b'\x05'
#_SRATE8 = b'\x04'
#_SRATE16 = b'\x03'
#_SRATE32 = b'\x02'
#_SRATE64 = b'\x01'
_SRATE120 = b'\x00'

# Adresse du registre de sélection du mode
_SM_REG = const(7)

# Paramètres pour le registre SM
_STANDBY = b'\x00' # Mode "standby"
_ACTIVE = b'\x01' # Mode "actif"

# Adresse du registre des paramètres des taps
_PDET_REG = const(9)

# Paramètres le registre PDET
# Détection des taps sur les 3 axes (bits [7-5] à 0)
# Anti-rebond  : on filtre 20 oscillations, soit 0x14 (bits [4-0]) (valeur possible de 1 à 31)
_TAPS_BEBOUNCE = b'\x14'

# Adresse du registre de filtrage de la détection des taps
_PD_REG = const(10)

# Paramètre pour le registre PD
# Délai de détection des taps, agrège jusqu'à 31 taps consécutifs
_TAPS_FUSE = b'\x1F'

# Facteur de conversion entre les valeurs lues dans les registres et l'accélération physique
# exprimée en g.
_RAW_TO_G = 0.047

class MMA7660():

	def __init__(self, i2c, addr = _DEVICE_ADDRESS, srate = _SRATE120):
		self.i2c = i2c
		self.i2c.scan()
		self.address = addr 
		# Tableau "tampon" d'octets pour récupérer les valeurs du registre de sortie de 
		# l'accéléromètre
		self.databuf = bytearray(3)
		# Tableau contenant les valeurs dimensionnées des accélérations suivant les trois axes
		self.data = [0,0,0]
		# On place l'accéléromètre est en mode "standby"
		self.stop()

		# On active les interruptions
		self.i2c.writeto_mem(self.address, _INTSU_REG, _INT_SET)
		# On fixe sa fréquence de mesures
		self.i2c.writeto_mem(self.address, _SR_REG, srate)
		# On fixe les paramètres de la détection des "taps"
		self.i2c.writeto_mem(self.address, _PDET_REG, _TAPS_BEBOUNCE)
		self.i2c.writeto_mem(self.address, _PD_REG, _TAPS_FUSE)

	# Méthode pour démarrer l'accéléromètre
	def start(self):
		self.i2c.writeto_mem(self.address, _SM_REG, _ACTIVE)

	# Méthode pour arrêter l'accéléromètre
	def stop(self):
		self.i2c.writeto_mem(self.address, _SM_REG, _STANDBY)

	# Méthode pour changer la fréquence de mesures
	def setSamplingRate(self, rate):
		# Passe en mode "standby"
		self.stop()
		# Programme la fréquence d'échantillonnage (seize mesures par seconde)
		self.i2c.writeto_mem(self.address, _SR_REG, rate)
		# Passe en mode "actif" 
		self.start()

	# Méthode pour obtenir les mesures d'accélération
	def get(self):
		self.databuf = self.i2c.readfrom_mem(self.address, _OUTPUT_REG, 3) # Lecture des données

		# Pour l'accélération selon l'axe x
		ax = self.databuf[0] & 0x3F # Complément à deux
		if ax > 31:
			ax = ax - 64
		self.data[0] = ax * _RAW_TO_G # Conversion en g

		# Pour l'accélération selon l'axe y
		ay = self.databuf[1] & 0x3F
		if ay > 31:
			ay = ay - 64
		self.data[1] = ay * _RAW_TO_G

		# Pour l'accélération selon l'axe z
		az = self.databuf[2] & 0x3F
		if az > 31:
			az = az - 64
		self.data[2] = az * _RAW_TO_G

		return tuple(self.data)

	# Méthode pour temporiser jusqu'à la mise à jour du registre TILT
	def _read_tilt_reg(self):
		# Lecture du registre (un octet)
		reg_content = self.i2c.readfrom_mem(self.address, _TILT_REG, 1)

		# Si le registre n'était pas en cours de mise à jour (bit numéro 6 égal à "1")
		if not (reg_content[0] & (1<<6)):
			return reg_content[0]
		else:
			return 0

	# Méthode pour déterminer si l'accéléromètre est secoué
	# Retourne :
	#  1 si l'accéléromètre est secoué 
	#  0 sinon
	def shake(self):
		val = self._read_tilt_reg() & (1<<7)
		if val:
			return 1
		else:
			return 0

	# Méthode pour déterminer si l'accéléromètre est tapoté
	# Retourne :
	#  1 si l'accéléromètre est tapoté 
	#  0 sinon
	def tap(self):
		val = self._read_tilt_reg() & (1<<5)
		if val:
			return 1
		else:
			return 0

	# Méthode pour déterminer si l'accélèromètre et posé côté pile ou côté face
	# Retourne :
	#  0 si l'accélèromètre est tourné côté "FACE"
	#  1 si l'accélèromètre est tourné côté "PILE"
	#  -1 si état indéterminé
	def facing(self):
		val = self._read_tilt_reg() & 0b11 
		if val == 1:
			return 0
		elif val == 2:
			return 1
		else:
			return -1 

	# Méthode pour détemrminer le mode portrait/paysage
	# Retourne :
	#  1 si mode paysage, vers la gauche
	#  2 si mode paysage, vers la droite
	#  5 si position verticale inversée
	#  6 si position verticale normale
	def portrait_landscape(self):
		return ( self._read_tilt_reg() & 0b11100 ) >> 2

On remarque que :

  • les noms des constantes ont été modifiés pour commencer par un caractère _. Ceci permet de s’assurer que le programme principal qui importera mma7660.py ne “verra” pas ces constantes.
  • On a rajouté des fonctions à notre classe (lecture du registre TILT). Ces informations sont tirées de la fiche technique de l’accéléromètre et des explications fournies par l’excellent ouvrage : “MicroPython et Pyboard - Python sur microcontrôleur : de la prise en main à utilisation avancée”, Auteur : Dominique Meurisse, ISBN-10 : 2409022901 et ISBN-13‏ :‎ 978-2409022906.

Et, pour finir, le script main.py qui importe la classe MMA7660 et l’utilise :

# Objet du script : Mise en oeuvre de l'accéléromètre 3 axes MMA7660FC (+/- 1.5g)
# Datasheet : https://www.nxp.com/docs/en/data-sheet/MMA7660FC.pdf

from machine import I2C
import mma7660 # Pour gérer l'accéléromètre
import pyb # Pour gérer les entrées-sorties (LED)
from time import sleep_ms # Pour les temporisations

# On utilise l'I2C n°1 de la carte NUCLEO-WB55 pour communiquer avec le capteur
i2c = I2C(1) 

# Pause d'une seconde pour laisser à l'I2C le temps de s'initialiser
sleep_ms(1000)

# Liste des adresses I2C des périphériques présents
print("Adresses I2C utilisées : " + str(i2c.scan()))

# Instanciation de l'accéléromètre
accelerometre = mma7660.MMA7660(i2c)

# On appelle la méthode "start()" pour démarrer l'accéléromètre
accelerometre.start()

# Instantiation des LED
led_bleu = pyb.LED(1)
led_vert = pyb.LED(2)
led_rouge = pyb.LED(3)

SEUIL = 0.75 # Seuil d'accélération pour allumer ou éteindre les LED

last_face = -1
last_portrait_landscape = -1

while True:

	# On appelle la méthode "get()" pour récupérer les mesures de l'accéléromètre
	ax, ay, az = accelerometre.get()
	
	# Si la valeur absolue de l'accélération sur l'axe X est supérieure à SEUIL mg alors
	if abs(ax) > SEUIL :
		led_vert.on()
	else:
		led_vert.off()
		
	# Si la valeur absolue de l'accélération sur l'axe Y est supérieure à SEUIL mg alors
	if abs(ay) > SEUIL : 
		led_bleu.on()
	else:
		led_bleu.off()

	# Si la valeur absolue de l'accélération sur l'axe Z est supérieure à SEUIL mg alors
	if abs(az) > SEUIL : 
		led_rouge.on()
	else:
		led_rouge.off()

	# Rapporte les taps
	if accelerometre.tap():
		print("Tap !")

	# Rapporte les secousses
	if accelerometre.shake():
		print("Secousse !")

	# Rapporte l'orientation (type "pile ou face") du module. Pour que la réponse de cette
	# fonction soit cohérente, vous devez positionner le module Grove de sorte que
	# sont axe Z soit proche de la verticale.

	if abs(az) > 0.7: # Si le module est tenu presque horizontalement
		face = accelerometre.facing()
		if face != last_face:
			last_face = face
			if face == 0:
				print("Plan du module (côté connecteur Grove) orienté vers le haut")
			elif face == 1 :
				print("Plan du module (côté connecteur Grove) orienté vers le bas")
	else:
		last_face = -1

	# Test de l'orientation en mode portrait - paysage. Pour que la réponse de cette
	# fonction soit cohérente, vous devez positionner le module Grove de sorte que
	# sont axe Z pointe vers vous et soit proche de l'horizontale.
	
	if abs(az) < 0.3: # Si le module est tenu presque verticalement
		portrait_landscape = accelerometre.portrait_landscape()
		if portrait_landscape != last_portrait_landscape:
			last_portrait_landscape = portrait_landscape
			if portrait_landscape == 6:
				print("Portrait - paysage : axe Y vers la droite, axe X vers le bas")
			elif portrait_landscape == 5:
				print("Portrait - paysage : axe Y vers la gauche, axe X vers le haut")
			elif portrait_landscape == 2:
				print("Portrait - paysage : axe Y vers le bas, axe X vers la gauche")
			elif portrait_landscape == 1:
				print("Portrait - paysage : axe Y vers le haut, axe X vers la droite")
	else:
		last_portrait_landscape = -1

	sleep_ms(250) # Temporisation d'un quart de seconde

Mise en oeuvre

Copiez mma7660.py et main.py sur le disque PYBFLASH. Dans la console de PuTTY, appuyez sur CTRL+D. Vous pouvez observer les messages dans le terminal et les LED qui s’éteignent ou s’allument et les messages renvoyés selon les mouvements imposés à l’accéléromètre.