Périphériques I2C¶
Côté programme, machine.I2C fait du microcontrôleur le maître d'un vrai bus I2C électrique : il génère l'horloge sur SCL et échange les données sur SDA (API). Peripherals fournit trois périphériques esclaves à brancher sur ce bus. Leur comportement est toujours décrit par un script Python. Un autre MCU peut aussi être l'esclave, avec machine.I2CTarget (API, exemples MultiMcu.I2c et MultiMcu.I2cIrq).
Lignes SCL et SDA de l'exemple I2c.Echo : START, octet d'adresse, acquittement de l'esclave, puis premier octet de données.
Câblage¶
Deux fils partagés par tous les acteurs du bus, plus la masse :
| Microcontrôleur | Chaque périphérique |
|---|---|
broche SCL choisie par le programme (ex. GP4) |
SCL |
broche SDA choisie par le programme (ex. GP5) |
SDA |
GND |
GND |
from machine import Pin, I2C
i2c = I2C(0, scl=Pin(4), sda=Pin(5), freq=100000)
print(i2c.scan()) # adresses présentes, ex. [66]
i2c.writeto(0x42, b'Hello')
print(i2c.readfrom(0x42, 5))
Le bus est en drain ouvert : chaque acteur ne sait que tirer une ligne à la masse ou la relâcher, et ce sont des résistances de tirage qui remontent les lignes. Au moins un périphérique du bus doit porter ces résistances (usePullUp = true). I2C() active bien les tirages internes du microcontrôleur sur SCL et SDA, comme sur le Pico, mais 50 kΩ face aux capacités d'entrée des périphériques, c'est beaucoup trop lent : une ligne relâchée n'a pas le temps de remonter, et chaque transaction lève OSError(ETIMEDOUT), comme sur un montage où on a oublié les résistances (exemple I2c.NoPullUp). L'écran Grove les porte par défaut, comme le module réel.
Autant de périphériques qu'on veut se partagent les deux fils ; chacun ne répond qu'à ses adresses (exemple I2c.MultiDevice).
Les périphériques fournis¶
| Composant | Adresse(s) | Script par défaut | Rôle |
|---|---|---|---|
I2cGroveLcdRgb |
0x3E, 0x62 |
grove_lcd_rgb.py |
Écran Grove - LCD RGB Backlight, 16 × 2 caractères, rétroéclairage coloré. Tirages activés (usePullUp = true) |
I2cEchoDevice |
0x42 |
i2c_echo.py |
Composant de test : relit au maître ce qu'il vient de lui écrire |
I2cGenericDevice |
0x42, à régler |
i2c_generic.py |
Gabarit : un banc de 16 registres, point de départ d'un nouveau périphérique |
Les scripts par défaut sont dans Resources/Scripts/Device/.
L'écran Grove LCD RGB¶
Le module réel porte deux circuits, d'où deux adresses pour un seul composant ; le script les émule d'après leurs fiches techniques :
- JHD1313 (
0x3E, contrôleur d'écran compatible HD44780) : effacement, retour au début, position d'écriture, écran allumé ou éteint, décalage. Écran éteint à la mise sous tension, comme le vrai : le driver doit l'allumer. - PCA9633 (
0x62, driver du rétroéclairage) : intensités rouge, vert, bleu.
L'icône affiche les deux lignes et prend la couleur du rétroéclairage pendant la relecture du résultat. valueOut rend (rouge, vert, bleu, écran allumé), intensités de 0 à 255.
L'exemple I2c.GroveLcd pilote cet écran avec un driver MicroPython du commerce, exécuté sans modification : driver_grove_lcd_rgb.py, posé à côté du programme qui l'importe.
Connecteurs¶
| Connecteur | Rôle |
|---|---|
SDA |
Données du bus |
SCL |
Horloge du bus |
GND |
Masse, à relier à celle du microcontrôleur |
valueIn[nIn] |
Grandeurs du modèle transmises au script (argument v) |
valueOut[nOut] |
Grandeurs rendues par le script (outputs()) : le périphérique devient un actionneur. Peut rester non connecté |
Paramètres¶
Bus et comportement (onglet General)¶
| Paramètre | Défaut | Groupe | Rôle |
|---|---|---|---|
addresses |
selon le composant | I2C bus | Adresse(s) sur 7 bits, sous forme de texte : "0x42" ou "0x3E, 0x62" (4 au plus) |
usePullUp |
false (true pour le Grove) |
I2C bus | Porter les résistances de tirage de SDA et SCL vers VOH. Plusieurs périphériques peuvent les porter : elles se mettent en parallèle |
RPullUp |
4,7 kΩ | I2C bus | Valeur de chaque résistance de tirage |
scriptPath |
script du composant | Behaviour | Fichier .py décrivant le périphérique |
Entrées / sorties (onglet Inputs / outputs)¶
| Paramètre | Défaut | Rôle |
|---|---|---|
useValueInput |
false |
Prendre les grandeurs sur le connecteur valueIn ; sinon, fixedValue |
nIn |
1 | Nombre de grandeurs reçues du modèle (4 au plus) |
fixedValue |
0 | Valeur utilisée quand valueIn n'est pas utilisé |
nOut |
1 (3 pour l'écho, 4 pour le Grove) | Nombre de grandeurs rendues par outputs() (4 au plus) |
Électrique (onglet Electrical)¶
| Paramètre | Défaut | Rôle |
|---|---|---|
VOH |
3,3 V | Tension d'alimentation des tirages |
VIH, VIL |
2,0 V, 0,8 V | Seuils de lecture de SDA et SCL |
ROut |
100 Ω | Résistance du transistor qui tire SDA à la masse |
GOff |
1 nS | Fuite du transistor bloqué (ligne relâchée) |
CIn |
10 pF | Capacité d'entrée de chaque broche. Avec RPullUp, elle fixe le temps de montée des fronts (4,7 kΩ × 10 pF = 47 ns) : l'augmenter montre les fronts dégradés d'un bus trop chargé |
Écrire le script d'un périphérique¶
Le script ne voit que des transactions : ni bits, ni START/STOP, ni acquittements. Quatre fonctions, toutes facultatives :
registres = bytearray(16)
pointeur = 0
def on_write(addr, data, t, v): # le maître vient d'écrire data (bytes, jamais vide)
global pointeur
pointeur = data[0] % 16
for octet in data[1:]:
registres[pointeur] = octet
pointeur = (pointeur + 1) % 16
def on_read(addr, t, v): # le maître commence à lire
return bytes(registres[pointeur:]) # bytes, str, liste d'entiers ou entier
def outputs(): # relue après chaque appel -> valueOut
return registres[0]
def lines(): # relue après chaque appel -> deux lignes de texte (écrans)
return ('ligne 1', 'ligne 2')
| Fonction | Appelée | Reçoit | Retourne |
|---|---|---|---|
on_write(addr, data, t, v) |
à la fin d'une écriture adressée (STOP ou START répété) | addr : l'adresse utilisée ; data : les octets ; t : temps simulé ; v : tuple de valueIn |
rien |
on_read(addr, t, v) |
quand le maître commence une lecture | idem, sans data |
les octets à envoyer ; on_read est rappelée si le maître en veut plus, 0xFF si rien |
outputs() |
après chaque gestionnaire | — | un nombre ou une séquence, recopié sur valueOut |
lines() |
après chaque gestionnaire | — | deux chaînes, pour un composant d'affichage |
addr permet à un composant à plusieurs adresses de savoir à quel circuit on s'adresse. Mêmes règles que pour les appareils série : un script chargé une fois par composant, des variables qui persistent d'un appel à l'autre, pas d'attente ni d'accès à machine, print() dans le journal, arrêt de la simulation sur exception.
Pour créer un nouveau périphérique : copier Resources/Scripts/Device/i2c_generic.py et le désigner dans un I2cGenericDevice.
Grandeurs à tracer¶
| Variable | Contenu |
|---|---|
dev.SDA.v, dev.SCL.v |
Tensions des lignes (pour un périphérique nommé dev) |
dev.busy |
Une transaction adressée à ce périphérique est en cours |
dev.sdaDriveLow |
Le périphérique tire SDA à la masse (acquittement, bit à 0) |
dev.eventSeq |
Nombre de transactions closes |
Message « Chattering detected » dans le journal
OpenModelica signale ainsi une rafale d'événements dans un seul pas de sortie. C'est bénin : la séquence I2C est pilotée par événements. Le message apparaît quand l'intervalle de sortie (Interval) est grand devant la période d'horloge du bus ; le réduire le fait disparaître.
Exemples¶
| Exemple | Ce qu'il montre |
|---|---|
I2c.Echo |
Écriture puis relecture d'une trame ; lecture d'un registre derrière un START répété |
I2c.MultiDevice |
Trois périphériques sur un bus à 400 kHz, trouvés par scan() |
I2c.NoPullUp |
Le même bus sans résistances de tirage externes : les tirages internes seuls sont trop lents, OSError(ETIMEDOUT) |
I2c.GroveLcd |
Écran Grove LCD RGB piloté par un driver du commerce |
Weighing.KitchenScale |
Balance de cuisine avec écran Grove |