Un petit sujet de discussion pour partager mon expérimentation de ce matin.
Rien de fracassant, mais quelques infos quand même.
La question de départ était :
Depuis le programme arduino, comment vérifier la présence de message reçu dans le MCP2515, de la manière la plus rapide possible ?En effet, quel que soit le programme, l'arduino va devoir se poser cette question très régulièrement, disons plusieurs milliers de fois par seconde.
Et dans la plupart des cas, la réponse sera négative.
Il serait donc dommage que la question coûte cher en CPU, surtout sur les cas de réponse négative.
Le MCP2515 dispose d'une sortie INT. Celle-ci donne l'état de ses buffers de réception internes.
Elle est à GND tant qu'il y a un message disponible à récupérer.
Par ailleurs, l'arduino peut dialoguer avec le MCP2515 via la liaison SPI, au travers des méthodes CAN.checkReceive() et CAN.readMsgBuf().
Il est donc possible de ne pas utiliser la sortie INT et d'utiliser seulement ces méthodes d'accès.
De plus, comme la méthode CAN.readMsgBuf() retourne la valeur CAN_NOMSG lorsqu'il n'y a rien à lire, on peut s'interroger sur l'utilité d'appeler systématiquement CAN.checkReceive() avant CAN.readMsgBuf().
Cet appel systématique pourrait être justifié si le checkReceive() répondait significativement plus vite en cas d'absence de message à récupérer.
J'ai donc testé plusieurs cas :
- sortie INT utilisée via une interruption
- sortie INT lue par un digitalRead
- sortie INT lue par accès direct aux registres de l'arduino
- utilisation de la méthode checkReceive()
- utilisation directe de la méthode readMsgBuf()
Le test a été effectué à vide, c'est-à-dire avec un bus CAN sur lequel aucun message ne circule, car l'objectif était de vérifier l'absence de message disponible dans le MCP2515, le plus vite possible
Le programme de test
#include <mcp_can.h>
const char* pgm =
"Programme d'evaluation des diverses façons de verifier la presence d'un message CAN a lire";
/* Cablage Arduino <-> MCP2515
* D3 <-> INT
* D13 <-> SCK
* D11 <-> SI
* D12 <-> SO
* D10 <-> CS
* GND <-> GND
* 5V <-> VCC
*/
const uint8_t pin_INT = 3;
const uint8_t pin_CS = 10;
MCP_CAN CAN(pin_CS);
const uint32_t nb_tests = 100000UL;
uint32_t bidon;//variable globale bidon pour tromper le compilo
char* reference()
{
for (uint32_t i=0; i<nb_tests; i++)
{
//Si on ne met rien de consistant dans la boucle for,
//le compilo va optimiser tout ça et virer le code inutile
if (bidon < 100) bidon++;
//A cet endroit on va mettre les differentes versions à tester
}
return __func__;
}
volatile bool message_a_lire = false; //mise à jour par interruption, non implémentée pour le test
char* version_interruption()
{
for (uint32_t i=0; i<nb_tests; i++)
{
if (bidon < 100) bidon++;
if (message_a_lire)
{
bidon++;//on n'arrive jamais ici car aucun message echangé lors du test
}
}
return __func__;
}
char* version_lecture_pin_INT_digitalRead()
{
for (uint32_t i=0; i<nb_tests; i++)
{
if (bidon < 100) bidon++;
if (!digitalRead(pin_INT))
{
bidon++;
}
}
return __func__;
}
char* version_lecture_pin_INT_directe()
{
for (uint32_t i=0; i<nb_tests; i++)
{
if (bidon < 100) bidon++;
if (!(PIND & (1 << pin_INT)))
{
bidon++;
}
}
return __func__;
}
char* version_CAN_checkReceive()
{
for (uint32_t i=0; i<nb_tests; i++)
{
if (bidon < 100) bidon++;
if (CAN.checkReceive() == CAN_MSGAVAIL)
{
bidon++;
}
}
return __func__;
}
char* version_CAN_readMsgBuf()
{
uint32_t rxId;
uint8_t rxTaille;
uint8_t rxData[8];
for (uint32_t i=0; i<nb_tests; i++)
{
if (bidon < 100) bidon++;
if (CAN.readMsgBuf(&rxId, &rxTaille, rxData) == CAN_OK)
{
bidon++;
}
}
return __func__;
}
void evaluer(char* fonction())
{
static uint32_t chrono_reference;
uint32_t chrono = micros();
char* nom = fonction();
chrono = micros() - chrono;
if (fonction == reference)
{
chrono_reference = chrono;
}
Serial.print("\nEvaluation ");
Serial.print(nom);
Serial.println(" :");
Serial.print(" chrono_global_brut=");
Serial.print(chrono);
Serial.println(" µs");
Serial.print(" chrono_unitaire_brut=");
Serial.print(chrono / (nb_tests / 1000));
Serial.println(" ns");
chrono -= chrono_reference;
Serial.print(" chrono_global_net=");
Serial.print(chrono);
Serial.println(" µs");
Serial.print(" chrono_unitaire_net=");
Serial.print(chrono / (nb_tests / 1000));
Serial.println(" ns");
}
void setup()
{
pinMode(pin_INT, INPUT);
Serial.begin(115200);
Serial.println(pgm);
Serial.print("\nNombre de tests de lecture par tir : ");
Serial.println(nb_tests);
evaluer(reference);
evaluer(version_interruption);
evaluer(version_lecture_pin_INT_digitalRead);
evaluer(version_lecture_pin_INT_directe);
evaluer(version_CAN_checkReceive);
evaluer(version_CAN_readMsgBuf);
}
void loop()
{
}
Résultat sur le terminal :
Programme d'evaluation des diverses façons de verifier la presence d'un message CAN a lire
Nombre de tests de lecture par tir : 100000
Evaluation reference :
chrono_global_brut=75968 µs
chrono_unitaire_brut=759 ns
chrono_global_net=0 µs
chrono_unitaire_net=0 ns
Evaluation version_interruption :
chrono_global_brut=107392 µs
chrono_unitaire_brut=1073 ns
chrono_global_net=31424 µs
chrono_unitaire_net=314 ns
Evaluation version_lecture_pin_INT_digitalRead :
chrono_global_brut=478392 µs
chrono_unitaire_brut=4783 ns
chrono_global_net=402424 µs
chrono_unitaire_net=4024 ns
Evaluation version_lecture_pin_INT_directe :
chrono_global_brut=145120 µs
chrono_unitaire_brut=1451 ns
chrono_global_net=69152 µs
chrono_unitaire_net=691 ns
Evaluation version_CAN_checkReceive :
chrono_global_brut=1974784 µs
chrono_unitaire_brut=19747 ns
chrono_global_net=1898816 µs
chrono_unitaire_net=18988 ns
Evaluation version_CAN_readMsgBuf :
chrono_global_brut=2006252 µs
chrono_unitaire_brut=20062 ns
chrono_global_net=1930284 µs
chrono_unitaire_net=19302 ns
Il y a une fonction de référence dont on mesure le temps d'exécution, puis les diverses versions testées.
Afin d'évaluer le temps d'exécution spécifique à chacune d'elles, je prends son temps d'exécution brut, duquel je déduis le temps d'exécution de la fonction de référence, pour avoir un résultat net qui correspond exactement aux quelques lignes de différence dans le code.
Evidemment, sans surprise, le test sur un booléen d'état mis à jour par une interruption est le plus rapide. Il ne coûte que 314 nanosecondes.
Encore que, cela me semble vraiment très peu. Je me demande si le compilo n'a pas optimisé ce petit bout de programme trop simple, pour laisser cette variable dans un registre de l'atmega.
On voit bien la différence entre un bon gros digitalRead et une lecture directe du registre PIND. En termes de rapidité, celui-ci n'est d'ailleurs pas loin du cas avec interruption.
Enfin, le checkReceive et le readMsgBuf ont des temps d'exécution voisins à 19 microsecondes (toujours dans le cas où il n'y a rien à lire), avec un petit avantage pour le checkReceive.
Au vu de ces résultats, je pars sur le modèle de réception CAN ci-dessous
#include <mcp_can.h>
/* Cablage Arduino <-> MCP2515
* D3 <-> INT
* D13 <-> SCK
* D11 <-> SI
* D12 <-> SO
* D10 <-> CS
* GND <-> GND
* 5V <-> VCC
*/
const uint8_t pin_INT = 3; //valeur 2 ou 3
const uint8_t pin_CS = 10;
MCP_CAN CAN(pin_CS);
volatile bool message_CAN_disponible = false;
void ISR_reception_CAN()
{
message_CAN_disponible = true;
}
void initialiser_reception_CAN()
{
pinMode(pin_INT, INPUT);
int8_t numero_interruption = digitalPinToInterrupt(pin_INT);
if (numero_interruption != -1)
{
attachInterrupt(numero_interruption, ISR_reception_CAN, FALLING);
}
else
{
//erreur : pas d'interruption disponible pour la valeur de pin_INT
}
}
void CAN_recup()
{
if (message_CAN_disponible)
{
message_CAN_disponible = false;
while (!(PIND & (1 << pin_INT)))// ou bien !digitalRead(pin_INT)
{
uint32_t rxId;
uint8_t rxTaille;
uint8_t rxBuffer[8];
if (CAN.readMsgBuf(&rxId, &rxTaille, rxBuffer) == CAN_OK)
{
traiter_message_CAN_recu(rxId, rxTaille, rxBuffer);
}
}
}
}
void traiter_message_CAN_recu(uint32_t id, uint8_t taille, uint8_t buffer[])
{
//...ou bien chargement dans un buffer en RAM pour traitement ultérieur
}
void setup()
{
initialiser_reception_CAN();
//...
}
void loop()
{
CAN_recup();
//...
}