Un client nous a signalé que l'API Yoctopuce ne permet pas d'utiliser un Yocto-I2C pour scanner un bus I2C et détecter les périphériques qui y sont rattachés. Il se trouve que ce n'est pas tout à fait exact...
Pour être honnête, il ne nous est jamais venu à l'idée de proposer une fonction permettant de balayer l'espace d'adresses d'un bus I2C pour y détecter les périphériques présents parce que l'adresse d'un périphérique I2C est presque toujours déterminée au niveau matériel et ne peut pas changer dynamiquement. A priori les adresses occupées sont donc connues à l'avance. Par conséquent, à moins que vous ne comptiez faire un système plug-and-play avec des modules I2C, ce pourquoi I2C n'est clairement pas fait pour, scanner le bus n'a que peu d'intérêt.
Ceci dit, l'API Yoctopuce contient toutes les primitives pour écrire une fonction permettant de réaliser le balayage d'un bus I2C.
Principe
L'idée, c'est d'essayer de contacter chaque adresse tour à tour en envoyant:
- Une start condition {S} qui indique que l'on débute une transaction I2C
- L'adresse à tester (décalée d'un bit vers la gauche)
- Une stop condition {P} qui signale qu'en fin de compte on abandonne la transaction.
S'il y a quelqu'un à l'adresse testée, il va répondre par un Acknowledge {A} sinon un timeout va se produire. On peut facilement tester le principe avec le VirtualHub. Par exemple si on veut tester la présence d'un chip RTC DS3231 de Maxim dont l'adresse I2C est 0x68, on va essayer d'envoyer {S}D0{P} et on devrait obtenir une réponse se terminant par "{A}". Si personne ne répond, on obtiendra un message se terminant par un point d'exclamation.
Test avec le VirtualHub, il y a quelqu'un à l'adresse 0x68, personne n'habite à l'adresse 0x69
Pour faire la même chose par programmation, il suffit d'utiliser la fonction Yi2cPort.queryLine() avec un timeout minimal mais suffisant pour laisser à un éventuel périphérique le temps de répondre. Ensuite il faut vérifier si le résultat se termine par "{A}":
if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
Console.writeline("I2C device present at address 0x68")
Subtilité
Par contre il y a une petite subtilité pour que cela fonctionne bien. Par défaut, le Yocto-I2C va attendre deux secondes après une erreur I2C. L'idée c'est que beaucoup de périphériques I2C réinitialisent automatiquement leurs communications si une transaction I2C s'interrompt pendant un certain temps. Ainsi, après une erreur I2C, on a plus de chance de repartir d'un bon pied si on attend un peu avant de recommencer une transaction. Il se trouve qu'à chaque fois qu'on va essayer contacter une adresse qui n'existe pas, cela va causer un timeout qui est considéré comme une erreur par le Yocto-I2C. C'est pourquoi il est important de prendre la précaution de réduire ce délai d'attente au minimum pendant qu'on effectue le balayage, quitte à le remettre à une valeur plus grande après.
i2cPort.set_i2cMode("100kbps,5ms");
La fonction scanI2CBus()
Voici donc la fonction qui permet de scanner un bus I2C et renvoie une liste d'adresses où quelqu'un a répondu. C'est du C# mais c'est facile à transcrire dans n'importe quel autre langage.
{ string errmsg="";
List<byte> res = new List<byte>();
// save I2C speed config
string prevmode = i2cPort.get_i2cMode();
// set standard speed, 5ms error recovery
i2cPort.set_i2cMode("100kbps,5ms");
// loop over addresses
for (byte i = FirstAddr; i <= LastAddr; i++)
{ // command to contact address device #i
string data = "{S}" + (i << 1).ToString("X2") + "{P}";
// send command, 10ms timeout
string result = i2cPort.queryLine(data, 10);
if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
{ //received an Aknowledge, there is someone living at address #i
res.Add(i);
}
else
{ // no answer, wait for error recovery delay
YAPI.Sleep(5, ref errmsg);
}
}
// restore previous speed configuration
i2cPort.set_i2cMode(prevmode);
return res;
}
On s'est demandé si on allait ajouter cette fonction directement dans l'API Yoctopuce. A priori c'est non, parce que c'est une fonction assez lente et bloquante, comptez environ 4 secondes pour balayer l'ensemble des 127 adresses I2C possibles. On a un peu peur que les utilisateurs l'utilisent à tord et à travers sans se rendre compte qu'elle va dégrader les performances de leur application.
Limitations
Il faut noter que cette méthode ne permettra pas forcément de détecter tous les périphériques I2C de la création dans la mesure où certains ne sont pas forcément capables de répondre rapidement n'importe quand. Il existe des capteurs dont l'implémentation I2C un peu simpliste les empêche d'honorer une transaction I2C pendant qu'ils sont occupés à faire une mesure. C'est par exemple le cas des capteurs de CO2 SenseAir Kxx.
Extrait du document "I2C communication guide K20/K22/K30/K33/K45/K50 plattforms"
Conclusion
En guise de conclusion, voici le code complet d'un petit programme C# qui scanne toutes les adresses possibles d'un bus I2C.
using System.Collections.Generic;
using System.Diagnostics;
namespace ConsoleApplication1
{
class Program
{
static List<byte> scanI2CBus(YI2cPort i2cPort, byte FirstAddr, byte LastAddr)
{ string errmsg="";
List<byte> res = new List<byte>();
// save I2C speed config
string prevmode = i2cPort.get_i2cMode();
// set standard speed, 5ms error recovery
i2cPort.set_i2cMode("100kbps,5ms");
// loop over addresses
for (byte i = FirstAddr; i <= LastAddr; i++)
{ // command to contact address device #i
string data = "{S}" + (i << 1).ToString("X2") + "{P}";
// send command, 10ms timeout
string result = i2cPort.queryLine(data, 10);
if (((result.Length) > 3) && result.Substring(result.Length - 3) == "{A}")
{ //received an Aknowledge, there is someone living at address #i
res.Add(i);
}
else
{ // no answer, wait for error recovery delay
YAPI.Sleep(5, ref errmsg);
}
}
// restore previous speed configuration
i2cPort.set_i2cMode(prevmode);
return res;
}
static void Main(string[] args)
{
string errmsg = "";
string target;
YI2cPort i2cPort;
if (YAPI.RegisterHub("usb", ref errmsg) != YAPI.SUCCESS)
{ Console.WriteLine("RegisterHub error: " + errmsg);
Environment.Exit(0);
}
i2cPort = YI2cPort.FirstI2cPort();
if (i2cPort == null)
{ Console.WriteLine("No Yocto-I2C Module connected");
Environment.Exit(0);
}
i2cPort.set_i2cMode("100kbps");
i2cPort.set_i2cVoltageLevel(YI2cPort.I2CVOLTAGELEVEL_3V3);
i2cPort.reset();
Console.WriteLine("Scanning... please wait");
Stopwatch timer = new Stopwatch();
timer.Start();
List<byte> addr = scanI2CBus(i2cPort, 1, 127);
timer.Stop();
Console.Write("found " + addr.Count + " I2C slave(s) in "
+ timer.Elapsed.ToString()+"sec ");
for (int i = 0; i < addr.Count; i++)
{ Console.Write(" 0x" + addr[i].ToString("X2"));
}
Console.WriteLine("");
YAPI.FreeAPI();
}
}
}
Voilà, voilà, c'est tout pour cette semaine.