Discussions Générales > Bus CAN

Réception CAN optimale

(1/4) > >>

bricoleau:
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

--- Code: ---#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()
{
}
--- Fin du code ---

Résultat sur le terminal :

--- Code: ---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
--- Fin du code ---

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

--- Code: ---#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();
  //...
}
--- Fin du code ---

Dominique:
Très belle démonstration avec un programme de test qui peut servir à plein de choses  :D

Si le but est de réduire le temps de traitement hors message CAN (c’est à dire quand il n’y a aucune chance d’en perdre), quand un message arrive, il faut ajouter le traitement qui peut être plus ou moins long.

Sur MEGA j’utilise le buffer circulaire, mais pas sur le DUE qui est infiniment plus rapide.

Lors de mes tests il y a maintenant 2 ans, j’avais développé un générateur de messages avec des données qui étaient incrémentées à l’emission pour détecter les pertes de messages à la réception.

Mais je pense que le plus utile est de définir tous les types de messages nécessaires et les circonstances de transmission. En DCC, il n’y a pas de contraintes de temps donc on peut toujours s’arranger pour ne pas en perdre.

bricoleau:

--- Citation de: Dominique le décembre 22, 2017, 10:07:07 pm ---Lors de mes tests il y a maintenant 2 ans, j’avais développé un générateur de messages avec des données qui étaient incrémentées à l’emission pour détecter les pertes de messages à la réception.

--- Fin de citation ---

Oui oui t'inquiète j'ai commencé par ça aussi  :D

Un arduino émetteur et un arduino récepteur.
L'arduino émetteur procède à une série de tirs.
Chaque tir est annoncé par un message, qui contient les caractéristiques des messages qui vont suivre : identifiant, longueur et nombre d'envois. Les donnée sont incrémentées de manière circulaire.
L'arduino récepteur vérifie qu'il reçoit tout bien avec les bonnes valeurs, et retourne un message d'acquittement du tir. Côté récepteur, j'affiche aussi la part de cpu dévolue à la récupération des messages.
J'ai fait varier la longueur des messages, la quantité de la rafale, ainsi que la durée entre deux émissions, en mesurant tout sans jamais rien perdre.

Comme on ne peut pas avoir deux terminaux ouverts sur l'IDE arduino, j'ai utilisé putty.

Je mets en pièce jointe les deux programmes et les deux résultats.

Par la suite, je pense faire des tests de tirs croisés avec deux émetteurs et deux récepteurs, histoire d'observer la saturation du bus. Il me faudra un cinquième arduino pour rendre compte des annonces et acquittements sur un terminal unique.

CATPLUS:
Bonjour

Tout ceci est légèrement confus dans mon esprit.
Ma question ?
Si j'ai bien tout compris, on envoie des informations de l'Arduino1 vers l'Arduino2, en retour le l'Arduino2 revoie l'information au 1 et ainsi de suite.
Pourrais-tu STP faire un schéma

Cordialement
Bonnes Fêtes de Fin d'Année

Marcel

bricoleau:
Par exemple si je prends le tir 150 que l'on peut voir dans le fichier joint ci-dessus CAN_00_E.txt


--- Code: ---Declenchement tir 150 delai=00500 nb_envois=1000 taille=5 ... OK duree=00499832 microsecondes.  Accuse reception OK
--- Fin du code ---

L'arduino 1 a d'abord émis un tout premier message CAN d'annonce de tir, dans lequel il indique qu'il va envoyer 1000 messages de 5 octets. L'identifiant CAN de chacun de ces 1000 messages est 150 (=le numéro de tir)

L'arduino 2 lit ce tout premier message d'annonce, et se met en attente de la salve

L'arduino 1 envoie les 1000 messages, à raison d'un message toutes les 500 microsecondes (valeur du délai pour ce tir)

L'arduino 2 recoit les 1000 messages, contrôle tout (identifiant, longueur, nombre de messages, contenu des données), puis émet un seul message, qui sert d'accusé de réception pour rendre compte de ses contrôles.

L'arduino 1 reçoit l'accusé de réception en provenance de l'arduino 2. Si un problème a été détecté par l'arduino2, le programme s'arrête, sinon il passe au tir suivant, et on recommence

Navigation

[0] Index des messages

[#] Page suivante

Utiliser la version classique