Datalogging rapide sur SD (30Hz), Stratégie?

Bonjour à tous,

Voici mon point dur du moment.

J'ai donc un arduino uno Rev 3 sur laquelle est enfichée une SD shield type :

Mon programme est simple, j'enregistre la valeur du timer sur la SD le plus vite possible (pas de délais dans la Loop) :

#include <SD.h>

const int chipSelect = 10;

void setup()
{
  // réglage timer 1
  TCCR1A = 0b00000000;// initialize timer1 
  TCCR1B = 0b00000000; // initialize timer1 
  TCCR1B |= (1 << CS11);// no prescaler, clock source on T1
  TCCR1B |= (1 << CS12);// no prescaler, clock source on T1
  TCCR1B |= (1 << WGM12);// set CTC mode  
  OCR1A = 32768-1;// set compare match
  
  Serial.begin(9600);
  
  // vérification rpésence carte
  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);
  if (!SD.begin(chipSelect))
  {
    Serial.println("Card failed, or not present");
    return;
  }
  Serial.println("card initialized.");
}

void loop()
{
  File dataFile = SD.open("datalog.txt", FILE_WRITE);
  dataFile.println(TCNT1);
  dataFile.close();
  Serial.println(TCNT1);
}

Ce qu'il en résulte sur la SD :

On voit ici l'apparition de sauts (points manquants). Par contre quand tout va bien, je suis aux alentours des 50Hz.
EDIT : Je parle bien des petits sauts, les grands sauts interviennent quand le timer passe de 32768-1 (valeur max) à 0.

Après quelques recherches en tous genres,je pense que ce mode d'acquisition rapide n'est pas approprié aux cartes SD. à certains moments, le controleur de la carte ne suit pas, mais je ne suis pas assez calé vous dire pourquoi. Et je ne suis pas sûr que la dernière carte à la mode, aussi bonne soit la latence me permettre 0 défaut.

Par contre j'ai entendu parler dans plusieurs topics de l'utilisation des buffers :

  • On utilise un buffer 1 qui enregistre les données
  • Quand le buffer 1 est rempli, on bascule l'acquisition des points sur un buffer 2 pendant que le buffer 1 se décharge dans la SD
  • Quand le buffer2 est rempli, on bascule l'acquisition des points sur un buffer 1 pendant que le buffer2 se décharge dans la SD
  • et ainsi de suite...

Ce que j'ai du mal à capter, c'est la simultanéïté entre l'acquisition dans un buffer et le déchargement d'un autre buffer dans la sd. Peut on faire ça vraiment simultanément? et par quel moyen?

sinon avez vous d'autres stratégies?

Merci

A première vue, les ouvertures et fermetures de fichiers prennent du temps à chaque fois.
Essaye de remplir un fichier (TXT) avec un gros paquet de mesures avant de l'envoyer d'un coup dans la carte SD.

Oui, mais pendant l'écriture, qui prendra surement un temps non négligeable, l'arduino est entièrement voué à cette tâche. Si je commence à mettre une interruption pendant l'écriture, que se passe t'il? Je vais déjà essayer sur mon exemple pour voir ce que cela donne.

Et je viens de m'en apercevoir, il y a exactement 60 mesures entre chaques petits sauts? une raison particulière?

Merci

L'accès à la carte se fait sous forme de blocs de 512 octets. Cela peut être lié.

J'ai bien une vague idée de ce qui se passe mais cela fait des lustres que je n'ai plus utilisé de carte SD avec un microcontrôleur, et donc avant d'écrire un long roman d'explication qui pourrait se révéler inexact, est-ce-que tu pourrais tester le code ci-dessous et nous redonner le résultat stp (je n'ai pas les moyens de tester moi-même là) :

#include <SD.h>

const int chipSelect = 10;

void setup()
{
  // réglage timer 1
  TCCR1A = 0b00000000;// initialize timer1 
  TCCR1B = 0b00000000; // initialize timer1 
  TCCR1B |= (1 << CS11);// no prescaler, clock source on T1
  TCCR1B |= (1 << CS12);// no prescaler, clock source on T1
  TCCR1B |= (1 << WGM12);// set CTC mode  
  OCR1A = 32768-1;// set compare match
  
  Serial.begin(9600);
  
  // vérification rpésence carte
  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);
  if (!SD.begin(chipSelect))
  {
    Serial.println("Card failed, or not present");
    return;
  }
  Serial.println("card initialized.");
}

void loop()
{
  File dataFile = SD.open("datalog.txt", O_WRITE | O_CREAT);
  while (true) {
    dataFile.println(TCNT1);
    Serial.println(TCNT1);
    if (TIFR1 & (1 << OCF1A)) {
      dataFile.flush();
      TIFR1 = (1 << OCF1A);
    }
  }
}

haifger:
J'ai bien une vague idée de ce qui se passe mais cela fait des lustres que je n'ai plus utilisé de carte SD avec un microcontrôleur, et donc avant d'écrire un long roman d'explication qui pourrait se révéler inexact, est-ce-que tu pourrais tester le code ci-dessous et nous redonner le résultat stp (je n'ai pas les moyens de tester moi-même là) :

bonjour
meme intuition
à l'oeil les open/close systematiques ce n'est surement pas le meilleur moyen pour gagner du temps et/ou ne pas perturper le flux d'enregistrement

Ouah :astonished:

Résultat très concluant !!
Voici un premier résultat du code proposé par haifger :


145 échantillons par seconde environs, les sauts réguliers n'existent plus.

Maintenant petit test en augmentant la vitesse de la liaison Serial de 9600 à 115200 :


plus de 1300ech/seconde !

Et sans l'echo sur la liaison série : 2300ech/s , sans points manquants !

Je suis bluffé, c'est largement au-delà des 30ech/seconde que j'attendais !

Je vais tenter de continuer mon projet sur cette voie, mais je veux bien des explications sur le problème et la résolution du problème.

Merci

Bonne nouvelle :slight_smile: Si cela ne te dérange pas, j'aimerai avant de me lancer dans les explications que tu fasse un dernier test histoire de valider (ou non) mon pressentiment initial :

il faudrait faire tourner le code que j'ai donné mais sans les envois vers la liaison série (comme ton dernier test donc), cette fois en mettant FILE_WRITE comme paramètre de SD.open (comme ton code initial) en lieu et place du O_WRITE | O_CREAT que j'avais modifié.

Avec le FILE_WRITE, le problème est revenu :grin:

Bon, donc c’est parti pour les explications sur le pourquoi du comment :slight_smile: [attention ça va être trèèèès long]

Si je résume tes tests avec leurs résultats en termes de vitesse d’écriture, ça donne ça :

  • 1 : SD.open(FILE_WRITE) + Serial(9600) ? 30 ech/s
  • 2 : SD.open(O_WRITE|O_CREAT) + Serial(9600) ? 145 ech/s
  • 3 : SD.open(O_WRITE|O_CREAT) + Serial(115200) ? 1300 ech/s
  • 4 : SD.open(O_WRITE|O_CREAT) + pas de Serial ? 2300 ech/s
  • 5 : SD.open(FILE_WRITE) + pas de Serial ? 30 ech/s

Comme tu l’as deviné toi-même, envoyer des données via la liaison série est un moyen de debug bien pratique, mais qui peut introduire des délais dont il faut savoir tenir compte. Cependant, la comparaison des tests 1 et 5 montre que ce n’est pas le principal facteur limitant de ton code initial.

Je vais quand même commencer par là parce que c’est peut-être le plus facile à comprendre. Avec une configuration «standard» (que tu utilises parce que tu n’as rien paramétré de spécial), la liaison est en 8N1, ce qui signifie que pour chaque octet envoyé on a :

  • 1 bit de start
  • 8 bits de données
  • aucun bit de parité (le N)
  • 1 bit de stop

Soit 10 bits qui circulent sur la ligne pour chaque octet transmis. À 9600 bit/s il est donc possible d’envoyer 9600/10=960 octet/s.

Maintenant, puisque tu utilises Serial.println pour transmettre un entier de 16 bits, c’est sa représentation ASCII qui va transiter, pas la représentation binaire : la majeure partie de tes données étant constitué de nombre à 5 chiffres, chaque appel à println va donc envoyer 5 octets, plus le caractère retour chariot et le caractère saut de ligne, soit 7 octets en tout.

En reprenant le calcul précédent, on en déduit que tu peux envoyer un maximum de 960/7=137 échantillons par seconde. Si on tient compte des approximations que j’ai faites (ie que les nombre sont toujours composés de 5 chiffres, ce qui n’est pas tout à fait vrai), on retrouve bien là la butée que tu as atteinte dans le test numéro 2. On pourrait refaire le même calcul et aboutir à la même conclusion pour le test 3 (toujours aux approximations près, c’est l’ordre de grandeur qui est utile ici, pas la valeur exacte).


Venons en désormais au problème principal, qui est la vitesse d’écriture sur la carte SD. La première chose à savoir est que la communication via le bus SPI est en quelque sorte un mode de fonctionnement dégradé : les lecteurs de cartes/PC/APN/… utilisent en général un bus parallèle à 4 bits beaucoup plus rapide, mais qui nécessite un contrôleur spécifique qu’à ma connaissance aucun microcontrôleur 8bit ne possède. Tout ça pour dire qu’il est illusoire d’espérer atteindre les vitesses de transferts «nominales» des cartes avec un petit atmega. Il n’en reste pas moins que 30 ech/s c’est plutôt très lent, et il y a donc forcément moyen de faire mieux…

La communication avec les cartes SD est assez complexe à mettre en oeuvre, et si on ne veut pas s’embêter avec les détails, il est nécessaire d’utiliser des librairies toutes faites. sdfatlib est une librairie gérant les fichiers et le système de fichiers présents sur la carte. SD est une surcouche à sdfatlib permettant de simplifier encore plus le travail du développeur, mais bien sûr cette simplification a un coût.

Par défaut, la librairie SD est configurée pour écrire les données qu’on lui transmet immédiatement sur la carte. D’un côté c’est pratique parce que cela évite notamment à l’utilisateur/développeur d’avoir à gérer le vidage du tampon, mais de l’autre l’impact sur la vitesse d’écriture est important.

Comme l’a indiqué @fdunews un petit peu plus haut, l’accès aux données d’une carte SD se fait par blocs de 512 octets. Ce qui signifie qu’à chaque fois que l’on souhaite écrire 1 octet, l’ensemble du bloc doit être lu, modifié, puis retransmis en entier. Mais la modification des données d’un fichier modifie aussi le système de fichier : le bloc de données correspondant doit donc lui aussi être lu puis retransmis dans sa totalité. Donc pour résumer, à chaque fois que l’on modifie 1 seul octet sur la carte, il y a 2048 octets qui circulent sur le bus ! Ouch. Et ceci sans même compter les octets de commande qui doivent également être envoyé à la carte pour lui expliquer ce que l’on attend d’elle - mais qui au final comptent pour quantité négligeable dans ce flot de données.

Pire encore : tes échantillons sont des entiers de 16 bits qui sont transmis via println, c’est donc leur représentation ASCII qui circule, et qui comme indiqué dans la partie communication série est en majorité composée de chaînes 7 caractères. Or comme je l’ai dit plus tôt, la librairie SD écrit sur la carte chaque donnée qu’elle reçoit immédiatement : les 7 caractères ne vont donc pas être envoyés «en bloc» mais écrit les uns après les autres, soit 7x2048=14336 octets transmis sur le bus SPI pour chaque échantillons !

Pour résumer, chaque fois que tu envoie vers la carte SD un de tes échantillons qui dans l’AVR tiennent sur seulement 2 octets, il y a au bas mot 14400 octets qui sont transférés… Je te laisse calculer le rendement du bouzin, mais c’est à peu près aussi bon que les panneaux solaires chers à @Batto :slight_smile:

Faisons un petit calcul : par défaut la librairie SD configure l’horloge du périphérique SPI à 4MHz :

  • 4 Mbit/s / 8 = 500000 octet/s
  • 500000 / (7*2048) = 35 ech/s

Encore une fois, on retrouve (comme par magie) la butée qui a été atteinte dans les test 1 et 5 (aux approximations près, comme toujours).

La modification que j’ai proposée, et qui consiste à utiliser O_WRITE|O_CREATE à la place de FILE_WRITE revient à dire à la librairie de n’écrire physiquement sur la carte que lorsque c’est strictement nécessaire, par exemple lorsque le tampon est plein ou que l’on change de bloc, etc. Ce qui signifie que dans le code que je t’ai mis, la section qui contient dataFile.flush() n’est normalement pas nécessaire, mais comme je le disais je n’ai pas les moyens de tester actuellement.

Une dernière note avant de finir : tel qu’il est mon code n’est bon que pour faire des tests, en conditions réelles il faudrait prévoir un moyen de sortir de la boucle while (bouton+interruption par exemple) et qui ensuite ferme le fichier, sinon les derniers échantillons acquis mais non encore physiquement écrit sur la carte SD seraient perdus.

Voilà, j’arrête là parce que ça commence à être beaucoup trop long. J’avais plus ou moins prévu de répondre également au sujets des buffers (tampons) puisque tu a posé la question, mais je ne vais pas non plus écrire un roman :slight_smile: Pour faire court : les 2 librairies SD et Serial utilisent déjà des tampons mais ils sont globalement inopérants ici parce ce que ça ne correspond pas vraiment à ton problème/besoin. Et si tu veux chercher par toi-même : dans le graph du test 2 que tu as publié, les petits «plateaux» que tu vois au tout début et après chaque remise à zéro du compteur, c’est l’effet du tampon de Serial.

haifger:
Bon, donc c’est parti pour les explications sur le pourquoi du comment :slight_smile: [attention ça va être trèèèès long]

Bonjour haifger
N'hesite pas à completer ton analyse lorsque tu aura du temps
C'est tres interessant (pour la recherche de limite)

Je te remercie grandement pour ces infos très instructives.

Mais je voudrai m'excuser parce qu'en passant au peigne fin tes explications, je me suis rendu compte que je ne t'ai pas donné les bonnes infos :blush:

J'espère que ça ne remet pas en cause tes explications car je m'en voudrais de tout ce temps que tu as passé si je vais tout casser avec ce qui suit :blush:

Tu as le droit de m'envoyer chi**r si tu veux...

Alors si on en revient à ce que j'ai mesuré, entre file_write et o_write, rien ne change en fait.... un coup je n'ai pas fait l'acquisition assez longtemps pour me rendre compte que le problème était toujours là surtest 3 115200 en o_write, et pensant que tout aller bien, je n'ai pas tirer la courbe sans le sur test 4 serial o_write... mais les sauts étaient bien là.

Je vais essayer de reprendre proprement :

J'ai refait 6 test

A : SD.open(FILE_WRITE) + Serial(9600)
B : SD.open(O_WRITE|O_CREAT) + Serial(9600)
C : SD.open(FILE_WRITE) + Serial(115200)
D : SD.open(O_WRITE|O_CREAT) + Serial(115200)
E : SD.open(FILE_WRITE) + pas de Serial
F : SD.open(O_WRITE|O_CREAT) + pas de Serial

A : SD.open(FILE_WRITE) + Serial(9600)

B : SD.open(O_WRITE|O_CREAT) + Serial(9600)

C : SD.open(FILE_WRITE) + Serial(115200)

et le zoom (carré noir)

D : SD.open(O_WRITE|O_CREAT) + Serial(115200)

E : SD.open(FILE_WRITE) + pas de Serial

F : SD.open(O_WRITE|O_CREAT) + pas de Serial

  • Le comportement est le même entre SD.open(O_WRITE|O_CREAT) et SD.open(FILE_WRITE)

  • Les sauts (flèches noires) sont présents sur les tests C D E F, mais si je fais une acquisition plus longue, il se pourrait peut être que je vois le phénomène sur les tests A et B.

  • Sur le test C, je vous fais voir un zoom pour apercevoir les petits segments (micro-découpage). Le micro-découpage est de même nature sur les tests C D E F, à savoir :
    des segments de 84 points en 4 DIGITS et de 72 points en 5 DIGITS
    si on fait 512/84 = 6.09 et 512/72 = 7.11, on est proche des 6 et 7 octets comme expliqué ici

Maintenant, puisque tu utilises Serial.println pour transmettre un entier de 16 bits, c’est sa représentation ASCII qui va transiter, pas la représentation binaire : la majeure partie de tes données étant constitué de nombre à 5 chiffres, chaque appel à println va donc envoyer 5 octets, plus le caractère retour chariot et le caractère saut de ligne, soit 7 octets en tout.

et j'ai refais les calculs sur tous les tests comme tu as fait ici

En reprenant le calcul précédent, on en déduit que tu peux envoyer un maximum de 960/7=137 échantillons par seconde. Si on tient compte des approximations que j’ai faites (ie que les nombre sont toujours composés de 5 chiffres, ce qui n’est pas tout à fait vrai), on retrouve bien là la butée que tu as atteinte dans le test numéro 2. On pourrait refaire le même calcul et aboutir à la même conclusion pour le test 3 (toujours aux approximations près, c’est l’ordre de grandeur qui est utile ici, pas la valeur exacte).

Les échantillonages sont retrouvés (ordre de grandeurs). Je repère la variation d'échantillonnage entre l'écriture en 4 digits et 5 digits. On le voit sur les tests A et B, les points sont plus rapprochés en dessous de 10000 que au-dessus.

  • sur toutes les courbes, tous les points en dessous de 500 environ sont absents. Si j'ai bien compris le code, le dataflush est lancer une fois le timer1 en butée. Je pense que le dataflush est un peu long, du coup les points sous 500 (15ms à 32768hz) ne passent pas dans le dataprintln

  • dernière remarque, si j'enlève le dataflush, rien de ressort sur la SD. De ce que j'en conclue, c'est un substitut de Sd.close mais qui permet de ne pas faire SD.open derrière (gain de temps).

Mon but final, est de synchroniser de la data acquise sur le SD sur une video à 30images/s, d'où les 30Hz cible. donc finalement les micro-coupures ne sont pas génantes (déjà non visible à 137Hz sur le test A et B). Mais les sauts (flèches noires m'embêtent un peu. peut être qu'un dataflush plus régulier ferait l'affaire. entre 2 acquisition à 30hz, j'ai donc 33ms à disposition. Je pense que j'ai le temps de gérer tout ce petit monde. Je creuse de mon côté.

Encore pardon et merci

Loic