A propos des bus I2C ou SPI
Les bus I2C et SPI sont deux bus série dédiés à l'interfaçage de composants électroniques avec des microcontroleurs ou des microprocesseurs. Chacun de ces bus présente ses propres avantages et inconvénients. Je dois bien avouer, que je ne suis pas fan du bus SPI. Les raisons sont multiples : Il nécessite plus de connexions. Il impose une gestion et un arbitrage du bus et des périphériques nettement plus complexe. Il n'est pas réellement adapter pour l'interfaçage d'un nombre de composants important. Bref, je n'utilise le bus SPI que :
- Quand je ne peux pas faire autrement. A titre d'exemple : Si le composant n'existe pas en I2C. Un LCD piloté par un composant ST7789 est un bon exemple.
- Si je n'ai pas plus d'un (ou grand maximum deux périphériques sur le bus SPI).
- Si j'ai plus d'un périphérique, mais que tous ces périphériques sont les mêmes. Par exemple, utiliser 3 cartes MAX31865 pour 3 mesures de températures faites à des endroits différents.
La gestion de périphériques différents sur le bus SPI peut vite se révéler être un casse tête si vous avez, à titre d'exemple, deux périphériques qui ne fonctionnent pas dans le même mode, ou qui supportent des fréquences différentes, des polarités de lignes CS différentes. A chaque fois, que vous passez d'un périphérique à l'autre, vous devez reconfigurer le bus.
Dans tous les cas, je tente toujours de privilégier le bus I2C, voire même les bus USB ou série UART, avant le bus SPI. Ainsi, dans une des dernières applications que j'ai développé (Un système de contrôle d'arrosage avec centrale météo intégrée) je n'utilise en SPI qu'un seul LCD sur ST7789 et 7 périphériques en I2C. C'est beaucoup plus simple.
Enfin, pour être tout à fait honnète, que ce soit en SPI ou I2C, la véritable complexité tient plus du périphérique et de sa documentation. Il existe des périphériques SPI parfaitement bien documentés, et des péripéhriques I2C dont la documentation est illisible ou incompréhensible au commun des mortels.
Tous les exemples donnés ici sont essentiellement issus du code source de la librairie. Pour étudier le code dans son intégralité, vous pouvez vous y reporter.
Options d'utilisation
Le support d'un nouveau périphérique sur les bus I2C ou SPI peut se faire de diverses manières avec la librairie cpp2835. En effet, 3 classes vous donnent accès à ces bus, les classes C2835Driver, CI2CBus et CI2CDevice pour le bus I2C et les classe CSPIBus et CSPIDevice pour le bus SPI.
Il est pourtant conseillé de ne s'appuyer que sur les classes CI2CDevice et CSPIDevice. En effet, ces classes utilisent les fonctions des classes CI2CBus et CSPIBus, (qui elles mêmes utliisent les fonctions de C2835Driver). il est très rare d'avoir à utiliser les fonctions I2C et SPI autres que celles de CI2CDevice ou CSPIDevice. En extrême limite, vous pouvez même remonter aux fonctions de la librairie bcm2835.
La meilleure méthode consiste donc à créer une nouvelle classe héritant de CI2CDevice ou CSPIDevice en lui passant l'adresse d'un objet CI2CBus ou CSPIBus.
Bus I2C
Les fichiers "CBME280.h" et "CBME280.cpp" du code source de la librairie et déclarant et implémentant le classe "CBME280" pour un composant de type BME280 de Bosch sont un bon exemple de la création d'une classe pour un nouveau périphérique.
Il suffit de faire hériter cette classe de la classe CI2CDevice et de passer à son constructeur un pointeur vers un objet CI2CBus. Dans la nouvelle classe, tous les accès au bus se feront via les méthodes héritées de CI2CDevice, qui elle même accède aux méthodes de CI2CBus via l'instance vers ce type d'objet passée en paramètre au constructeur. Voir ci-dessous les extraits de code.
Fichier "CBME280.h" : Extrait de déclaration de la classe :
{
public:
CBME280(CI2CBus* pI2CBus, const BME280_ADR& Address);
.../...
Fichier "CBME280.cpp" : Implémentation du constructeur :
: CI2CDevice(pI2CBus, (uint8_t)Address)
{
}
Exemple de code utilisateur de la librairie et de la classe
pDriver->Initialize();
CI2CBus* pBus = new CI2CBus(pDriver);
pBus->Open(I2C_CLOCK_DIVIDER_65536);
.../...
CBME280* pBME = new CBME280(pBus, BME280_ADR_0X76);
.../...
// Utilisation de pBME
.../...
delete pBME;
delete pBus;
pDriver->Terminate();
delete pDriver;
Dans la très grande majorité des cas, l'échange de données en I2C avec un périphérique se fait par lecture et/ou écriture dans ou depuis les registes du périphérique. Tout ce qui est nécessaire à ce type d'échange est supporté par la classe CI2CDevice dont hérite la nouvelle classe. Il suffit donc à cette dernière d'utiliser les fonctions de la classe CI2CDevice dont elle hérite. Il suffit de passer l'adresse du registre, la nouvelle valeur à y écrire, ou un paramètre recevant en sortie de la fonction la valeur lue sur le registre. Les registres peuvent être de 8 ou 16 bit.
Il existe des exceptions, certains périphériques n'utilisent pas de registres (tout du moins des registres accessibles via le bus I2C). Un bon exemple est le composant MCP3424 supporté par la librairie cpp2835. Ce composant ne peut recevoir qu'un seul octet de commande. Il n'est donc pas nécessaire, dans ce cas précis, d'adresser un registre quelconque. En lecture, c'est la même histoire. Dès que vous venez le lire, il vous envoie 3 ou 4 octets selon sa configuration courante. Ce composant est un convertisseur ADC (CAN en français). S'il est configuré en mesures de 12 à 16 bits de résolution, quand vous le lisez, il retourne 3 octets : Son octet de configuration et 2 octets pour la mesure. S'il est confguré en 18 bits de résolution, il renvoie 4 octets : Son octet de configuration suivi de 3 octets pour la mesure. Vous pouvez vous reporter au code source de ce composant dans la librairie pour voir comment on le configure puis qu'on lit les résultats. Dans le cas de tels composants, vous pouvez utiliser les fonctions "Read et Write" de la classe CI2CDevice.
Fichier "CBME280.cpp" : Extrait de code. Lecture d'un registre :
{
uint8_t nVal;
if(!ReadByteFromRegister(BME280_ID_REG, &nVal)) return false;
*pCheck = (nVal == 0x60);
return true;
}
Fichier "CBME280.cpp" : Extrait de code. Ecriture d'un registre :
{
return WriteByteToRegister((uint8_t)BME280_RESET_REG, 0xB6);
}
Fichier "CMCP3424.cpp" : Extrait de code. Ecriture et lecture sans registres :
const MCP3424_RESOLUTION& eResolution,
const MCP3424_GAIN& eGain,
int32_t* pRaw)
{
char buf[4];
uint32_t nlen = 0;
// Trig a conversion
char uCmd = (0b10000000 | (char)eChannel | (char)eResolution | (char)eGain) & 0b11101111;
if(!Write(&uCmd, 1)) return false;
// Read it
CSleeper::msleep(300);
if(!Read(buf, 4, &nlen)) return false;
*pRaw = (int32_t)buf[2] + ((int32_t)buf[1] * 256) + ((int32_t)buf[0] * 65536);
return true;
}
Bus SPI
L'idée reste la même pour le bus SPI que celle évoquée pour le bus I2C. En plus complexe du fait même de la nature du bus SPI. il faut se rappeler que la configuration à un instant du bus ne dépend ni du bus, ni de vos choix, mais uniquement du ou des périphériques auxquels on s'adresse. Les constructeurs auront donc plus de paramètres, et les lectures ou écritures depuis ou vers des registres des composants imposeront à chaque fois une reconfiguraiton du bus. Ou alors, tous les périphériues utilisent la même configuraiotn de bus. Mais même dans ce cas précis, vous devez quand même reconfigurer le bus à chaque écriture ou lecture, car au moment ou vous développez votre nouvelle classe pour un nouveau composant, vous ne pouvez pas préjuger qu'il n'aura pas à fonctionner dans le futur avec d'autres classes pour d'autres composants qui n'utiliseront pas nécessairement la même configuration de bus.
Les extraits de code donnés ci-dessous sont expurgés de tout ce qui ne concerne pas le bus lui même afin de ne pas surcharger et générer des incompréhensions. Pour le code exhaustif, reportez vous au code source.
Fichier "CMAX31865.h" : Extrait de déclaration de la classe :
{
public:
CMAX31865(CSPIBus* pSPIBus,
const GPIO_PIN& eSpiCSPin,
const SPI_CS_POLARITY& eSpiCSPolarity,
const SPI_BIT_ORDER& eSpiBitOrder,
const SPI_CLOCK_DIVIDER& eSpiClockDivider,
const SPI_DATA_MODE& eSpiDataMode);
.../...
Fichier "CMAX31865.cpp" : Implémentation du constructeur :
const GPIO_PIN& eSpiCSPin,
const SPI_CS_POLARITY& eSpiCSPolarity,
const SPI_BIT_ORDER& eSpiBitOrder,
const SPI_CLOCK_DIVIDER& eSpiClockDivider,
const SPI_DATA_MODE& eSpiDataMode)
: CSPIDevice(pSPIBus, eSpiCSPin, eSpiCSPolarity, eSpiBitOrder, eSpiClockDivider, eSpiDataMode)
{
}
Exemple de code utilisateur de la librairie et de la classe
pDriver->Initialize();
CSPIBus* pBus = new CSPIBus(pDriver);
m_pBus->Open(SPI_BIT_ORDER_MSBFIRST, SPI_CLOCK_DIVIDER_64, SPI_DATA_MODE_1);
.../...
CMAX31865* pMAX = new CMAX31865(pBus, GPIO_22, SPI_CS_POLARITY_NEGATIVE, SPI_BIT_ORDER_MSBFIRST, SPI_CLOCK_DIVIDER_1024, SPI_DATA_MODE_3);
.../...
// Utilisation de CMAX31865
.../...
delete pMAX;
delete pBus;
pDriver->Terminate();
delete pDriver;
Pour lire ou éccrire des données depuis ou vers un registre u périphérique. Il sera nécessaire de reconfigurer le bus pour ce périphérique, asserter la ligne CS attachée au périphérique, faire l'opération de lecture et/ou écriture, puis désasserter la ligne CS. Bien sur si vous faites plusieurs opérations de lecture et/ou écriture pour une seule et unique fonction de votre classe, toutes les opérations de lecture et/ou écriture peuvent se faire sans avoir à reconfigurer le bus et asserter la ligne CS entre chaque.
Exemple de lecture et écriture
.../...
// Reconfiguration du bus
if(!SetBusParameters()) return MAX31865_ERROR_CODE_2835DRIVER_ERROR;
// Assertion de CS
if(!ChipSelect()) return MAX31865_ERROR_CODE_2835DRIVER_ERROR;
// Opération c'écriture puis lecture
if(!WriteReadBuffer(m_cTrBuf, m_cRcvBuf, 9)) return MAX31865_ERROR_CODE_2835DRIVER_ERROR;
// Déassertion de CS
if(!ChipUnselect()) return MAX31865_ERROR_CODE_2835DRIVER_ERROR;
.../...