Mon implémentation d'un tampon en anneau dans NOR flash

Préhistoire

Il existe des distributeurs automatiques de notre propre conception. À l'intérieur du Raspberry Pi et du câblage sur une carte séparée. Un monnayeur, un accepteur de billets, un terminal bancaire sont connectés... Tout est contrôlé par un programme auto-écrit. L'intégralité de l'historique de travail est enregistrée dans un journal sur une clé USB (MicroSD), qui est ensuite transmis via Internet (à l'aide d'un modem USB) au serveur, où il est stocké dans une base de données. Les informations de vente sont chargées dans 1c, il existe également une interface Web simple pour le suivi, etc.

C'est-à-dire que le journal est vital - pour la comptabilité (revenus, ventes, etc.), le suivi (toutes sortes de pannes et autres circonstances de force majeure) ; C'est, pourrait-on dire, toutes les informations dont nous disposons sur cette machine.

problème

Les lecteurs Flash se révèlent être des appareils très peu fiables. Ils échouent avec une régularité enviable. Cela entraîne à la fois un temps d'arrêt de la machine et (si pour une raison quelconque le journal n'a pas pu être transféré en ligne) une perte de données.

Ce n'est pas la première expérience d'utilisation de clés USB, avant cela, il y avait un autre projet avec plus d'une centaine d'appareils, où le magazine était stocké sur des clés USB, il y avait aussi des problèmes de fiabilité, parfois le nombre de ceux qui tombaient en panne un mois se comptait par dizaines. Nous avons essayé différents lecteurs flash, y compris des lecteurs de marque avec mémoire SLC, et certains modèles sont plus fiables que d'autres, mais le remplacement des lecteurs flash n'a pas radicalement résolu le problème.

Attention! Longue lecture ! Si le « pourquoi » ne vous intéresse pas, mais seulement le « comment », vous pouvez aller directement À la fin articles.

décision

La première chose qui me vient à l'esprit est : abandonnez la MicroSD, installez, par exemple, un SSD et démarrez à partir de celui-ci. Théoriquement possible, probablement, mais relativement cher et moins fiable (un adaptateur USB-SATA est ajouté ; les statistiques de pannes pour les SSD économiques ne sont pas non plus encourageantes).

Le disque dur USB ne semble pas non plus être une solution particulièrement attrayante.

Par conséquent, nous sommes arrivés à cette option : quitter le démarrage à partir de MicroSD, mais les utiliser en mode lecture seule et stocker le journal des opérations (et d'autres informations propres à un élément matériel particulier - numéro de série, étalonnages des capteurs, etc.) ailleurs. .

Le sujet du FS en lecture seule pour les framboises a déjà été étudié de fond en comble, je ne m'attarderai pas sur les détails d'implémentation dans cet article (mais s'il y a de l'intérêt, j'écrirai peut-être un mini-article sur ce sujet). Le seul point que je voudrais souligner est que tant d'après l'expérience personnelle que d'après les avis de ceux qui l'ont déjà mis en œuvre, il y a un gain de fiabilité. Oui, il est impossible de se débarrasser complètement des pannes, mais réduire considérablement leur fréquence est tout à fait possible. Et les cartes sont de plus en plus unifiées, ce qui facilite grandement le remplacement pour le personnel de service.

Matériel

Il n'y avait aucun doute particulier sur le choix du type de mémoire - NOR Flash.
Arguments:

  • connexion simple (le plus souvent le bus SPI, que vous avez déjà une expérience d'utilisation, donc aucun problème matériel n'est à prévoir) ;
  • prix ridicule ;
  • protocole d'exploitation standard (l'implémentation est déjà dans le noyau Linux, si vous le souhaitez, vous pouvez en prendre un tiers, qui est également présent, ou même écrire le vôtre, heureusement tout est simple) ;
  • fiabilité et ressource :
    à partir d'une fiche technique type : les données sont stockées pendant 20 ans, 100000 XNUMX cycles d'effacement pour chaque bloc ;
    provenant de sources tierces : BER extrêmement faible, postule qu'il n'est pas nécessaire de recourir à des codes de correction d'erreurs (Certains travaux considèrent ECC pour NOR, mais généralement ils signifient toujours MLC NOR ; cela arrive également).

Estimons les besoins en volume et en ressources.

Je souhaite que les données soient garanties pendant plusieurs jours. Ceci est nécessaire pour qu'en cas de problèmes de communication, l'historique des ventes ne soit pas perdu. Nous nous concentrerons sur 5 jours, pendant cette période (même en tenant compte des week-ends et jours fériés) le problème peut être résolu.

Nous collectons actuellement environ 100 Ko de journaux par jour (3 à 4 10 entrées), mais ce chiffre augmente progressivement - les détails augmentent, de nouveaux événements sont ajoutés. De plus, il y a parfois des rafales (certains capteurs commencent à envoyer du spam avec des faux positifs, par exemple). Nous calculerons pour 100 XNUMX enregistrements de XNUMX octets chacun - mégaoctets par jour.

Au total, 5 Mo de données propres (bien compressées) sortent. Plus pour eux (estimation approximative) 1 Mo de données de service.

Autrement dit, nous avons besoin d’une puce de 8 Mo si nous n’utilisons pas de compression, ou de 4 Mo si nous l’utilisons. Des chiffres assez réalistes pour ce type de mémoire.

Quant à la ressource : si nous prévoyons que la mémoire entière ne sera pas réécrite plus d'une fois tous les 5 jours, alors sur 10 ans de service, nous obtiendrons moins de mille cycles de réécriture.
Permettez-moi de vous rappeler que le constructeur en promet cent mille.

Un peu sur NOR vs NAND

Aujourd'hui, bien sûr, la mémoire NAND est beaucoup plus populaire, mais je ne l'utiliserais pas pour ce projet : NAND, contrairement à NOR, nécessite forcément l'utilisation de codes correcteurs d'erreurs, d'un tableau des blocs défectueux, etc., et aussi des pattes de Les puces NAND sont généralement beaucoup plus nombreuses.

Les inconvénients du NOR incluent :

  • petit volume (et, par conséquent, prix élevé par mégaoctet) ;
  • faible vitesse de communication (en grande partie due au fait qu'une interface série est utilisée, généralement SPI ou I2C) ;
  • effacement lent (selon la taille du bloc, cela prend d'une fraction de seconde à plusieurs secondes).

Il semble qu'il n'y ait rien de critique pour nous, alors nous continuons.

Si les détails sont intéressants, le microcircuit a été sélectionné at25df321a (Cependant, cela n'a pas d'importance, il existe de nombreux analogues sur le marché qui sont compatibles en termes de brochage et de système de commande ; même si nous voulons installer un microcircuit d'un autre fabricant et/ou d'une taille différente, tout fonctionnera sans changer le code).

J'utilise le pilote intégré au noyau Linux ; sur Raspberry, grâce au support de la superposition de l'arborescence des périphériques, tout est très simple : il faut mettre la superposition compilée dans /boot/overlays et modifier légèrement /boot/config.txt.

Exemple de fichier dts

Pour être honnête, je ne suis pas sûr que ce soit écrit sans erreurs, mais ça marche.

/*
 * Device tree overlay for at25 at spi0.1
 */

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835", "brcm,bcm2836", "brcm,bcm2708", "brcm,bcm2709"; 

    /* disable spi-dev for spi0.1 */
    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";
            spidev@1{
                status = "disabled";
            };
        };
    };

    /* the spi config of the at25 */
    fragment@1 {
        target = <&spi0>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            flash: m25p80@1 {
                    compatible = "atmel,at25df321a";
                    reg = <1>;
                    spi-max-frequency = <50000000>;

                    /* default to false:
                    m25p,fast-read ;
                    */
            };
        };
    };

    __overrides__ {
        spimaxfrequency = <&flash>,"spi-max-frequency:0";
        fastread = <&flash>,"m25p,fast-read?";
    };
};

Et une autre ligne dans config.txt

dtoverlay=at25:spimaxfrequency=50000000

J'omettrai la description de la connexion de la puce au Raspberry Pi. D'une part, je ne suis pas un expert en électronique, d'autre part, tout ici est banal même pour moi : le microcircuit n'a que 8 pattes, dont on a besoin de masse, d'alimentation, de SPI (CS, SI, SO, SCK ); les niveaux sont les mêmes que ceux du Raspberry Pi, aucun câblage supplémentaire n'est requis - il suffit de connecter les 6 broches indiquées.

Formulation du problème

Comme d’habitude, l’énoncé du problème passe par plusieurs itérations, et il me semble qu’il est temps de passer à la suivante. Alors arrêtons-nous, rassemblons ce qui a déjà été écrit et clarifions les détails qui restent dans l'ombre.

Nous avons donc décidé que le journal sera stocké dans SPI NOR Flash.

Qu’est-ce que NOR Flash pour ceux qui ne le savent pas ?

Il s'agit d'une mémoire non volatile avec laquelle vous pouvez effectuer trois opérations :

  1. Lecture:
    La lecture la plus courante : nous transmettons l'adresse et lisons autant d'octets que nécessaire ;
  2. Запись:
    L'écriture sur NOR Flash ressemble à une écriture classique, mais elle a une particularité : vous ne pouvez changer que 1 en 0, mais pas l'inverse. Par exemple, si nous avions 0x55 dans une cellule mémoire, alors après y avoir écrit 0x0f, 0x05 y sera déjà stocké (voir tableau juste en dessous);
  3. Effacer:
    Bien sûr, nous devons être capables de faire l'opération inverse - changer 0 en 1, c'est exactement à cela que sert l'opération d'effacement. Contrairement aux deux premiers, il ne fonctionne pas avec des octets, mais avec des blocs (le bloc d'effacement minimum dans la puce sélectionnée est de 4 Ko). L'effacement détruit le bloc entier et constitue le seul moyen de changer 0 en 1. Par conséquent, lorsque vous travaillez avec de la mémoire flash, vous devez souvent aligner les structures de données sur la limite du bloc d'effacement.
    Enregistrement en NOR Flash :

Données binaires

C'était
01010101

Enregistré
00001111

Devenu
00000101

Le journal lui-même représente une séquence d'enregistrements de longueur variable. La longueur typique d'un enregistrement est d'environ 30 octets (bien que des enregistrements de plusieurs kilo-octets se produisent parfois). Dans ce cas, nous travaillons avec eux simplement comme un ensemble d'octets, mais, si cela vous intéresse, CBOR est utilisé à l'intérieur des enregistrements.

En plus du journal, nous devons stocker certaines informations de « paramètres », à la fois mises à jour et non : un certain identifiant d'appareil, les étalonnages des capteurs, un indicateur « l'appareil est temporairement désactivé », etc.
Ces informations sont un ensemble d'enregistrements clé-valeur, également stockés dans CBOR. Nous n'avons pas beaucoup de ces informations (quelques kilo-octets au maximum) et elles sont rarement mises à jour.
Dans ce qui suit, nous l'appellerons contexte.

Si l’on se souvient du début de cet article, il est très important de garantir un stockage fiable des données et, si possible, un fonctionnement continu même en cas de panne matérielle/corruption des données.

Quelles sources de problèmes peuvent être envisagées ?

  • Éteignez pendant les opérations d’écriture/effacement. Cela fait partie de la catégorie « il n’y a pas de truc contre le pied-de-biche ».
    Information provenant de discussions sur stackexchange : lorsque l'alimentation est coupée pendant que vous travaillez avec le flash, l'effacement (réglé sur 1) et l'écriture (réglé sur 0) conduisent à un comportement indéfini : les données peuvent être écrites, partiellement écrites (disons, nous avons transféré 10 octets/80 bits , mais on ne peut pas encore écrire que 45 bits), il est également possible que certains bits soient dans un état « intermédiaire » (la lecture peut produire à la fois 0 et 1) ;
  • Erreurs dans la mémoire flash elle-même.
    Le BER, bien que très faible, ne peut être égal à zéro ;
  • Erreurs de bus
    Les données transmises via SPI ne sont en aucun cas protégées : des erreurs sur un seul bit et des erreurs de synchronisation peuvent très bien se produire - perte ou insertion de bits (ce qui entraîne une corruption massive des données) ;
  • Autres erreurs/problèmes
    Erreurs dans le code, problèmes de Raspberry, interférences extraterrestres...

J'ai formulé les exigences dont le respect, à mon avis, est nécessaire pour assurer la fiabilité :

  • les enregistrements doivent être immédiatement placés dans la mémoire flash, les écritures différées ne sont pas prises en compte ; - si une erreur survient, elle doit être détectée et traitée le plus tôt possible ; - le système doit, si possible, se remettre des erreurs.
    (un exemple tiré de la vie « comment ça ne devrait pas être », que je pense que tout le monde a rencontré : après un redémarrage d'urgence, le système de fichiers est « cassé » et le système d'exploitation ne démarre pas)

Idées, approches, réflexions

Lorsque j'ai commencé à réfléchir à ce problème, de nombreuses idées me sont venues à l'esprit, par exemple :

  • utiliser la compression des données ;
  • utilisez des structures de données intelligentes, par exemple en stockant les en-têtes d'enregistrement séparément des enregistrements eux-mêmes, de sorte que s'il y a une erreur dans un enregistrement, vous puissiez lire le reste sans aucun problème ;
  • utilisez des champs de bits pour contrôler l'achèvement de l'enregistrement lorsque l'alimentation est coupée ;
  • stocker les sommes de contrôle pour tout ;
  • utilisez un certain type de codage résistant au bruit.

Certaines de ces idées ont été utilisées, tandis que d’autres ont été abandonnées. Allons-y dans l'ordre.

Compression de données

Les événements eux-mêmes que nous enregistrons dans le journal sont assez similaires et reproductibles (« a lancé une pièce de 5 roubles », « a appuyé sur le bouton pour rendre la monnaie », ...). La compression devrait donc être assez efficace.

Le surcoût de compression est insignifiant (notre processeur est assez puissant, même le premier Pi avait un cœur avec une fréquence de 700 MHz, les modèles actuels ont plusieurs cœurs avec une fréquence supérieure au gigahertz), le taux d'échange avec le stockage est faible (plusieurs mégaoctets par seconde), la taille des enregistrements est petite. De manière générale, si la compression a un impact sur les performances, celui-ci ne sera que positif. (absolument non critique, je dis juste). De plus, nous n'avons pas de véritable Linux intégré, mais un Linux classique - donc la mise en œuvre ne devrait pas nécessiter beaucoup d'efforts (il suffit de lier la bibliothèque et d'en utiliser plusieurs fonctions).

Un morceau du journal a été extrait d'un appareil fonctionnel (1.7 Mo, 70 4 entrées) et sa compressibilité a d'abord été vérifiée à l'aide de gzip, lz2, lzop, bzipXNUMX, xz, zstd disponibles sur l'ordinateur.

  • gzip, xz, zstd ont montré des résultats similaires (40 Ko).
    J'ai été surpris que le xz à la mode se montre ici au niveau de gzip ou zstd ;
  • lzip avec les paramètres par défaut a donné des résultats légèrement moins bons ;
  • lz4 et lzop n'ont pas montré de très bons résultats (150 Ko) ;
  • bzip2 a montré un résultat étonnamment bon (18 Ko).

Ainsi, les données sont très bien compressées.
Donc (si on ne trouve pas de défauts fatals) il y aura compression ! Tout simplement parce que davantage de données peuvent tenir sur la même clé USB.

Pensons aux inconvénients.

Premier problème : nous avons déjà convenu que chaque enregistrement devait immédiatement passer au flash. En règle générale, l'archiveur collecte les données du flux d'entrée jusqu'à ce qu'il décide qu'il est temps d'écrire le week-end. Nous devons immédiatement recevoir un bloc de données compressé et le stocker dans une mémoire non volatile.

Je vois trois manières :

  1. Compressez chaque enregistrement en utilisant la compression par dictionnaire au lieu des algorithmes décrits ci-dessus.
    C'est une option tout à fait efficace, mais je ne l'aime pas. Pour assurer un niveau de compression plus ou moins correct, le dictionnaire doit être « adapté » à des données spécifiques ; tout changement entraînera une baisse catastrophique du niveau de compression. Oui, le problème peut être résolu en créant une nouvelle version du dictionnaire, mais c'est un casse-tête - nous devrons stocker toutes les versions du dictionnaire ; dans chaque entrée, nous devrons indiquer avec quelle version du dictionnaire il a été compressé...
  2. Compressez chaque enregistrement en utilisant des algorithmes « classiques », mais indépendamment des autres.
    Les algorithmes de compression considérés ne sont pas conçus pour fonctionner avec des enregistrements de cette taille (dizaines d'octets), le taux de compression sera clairement inférieur à 1 (c'est-à-dire augmenter le volume des données au lieu de compresser) ;
  3. Faites FLUSH après chaque enregistrement.
    De nombreuses bibliothèques de compression prennent en charge FLUSH. Il s'agit d'une commande (ou d'un paramètre de la procédure de compression), à la réception de laquelle l'archiveur forme un flux compressé afin qu'il puisse être utilisé pour restaurer tous données non compressées déjà reçues. Un tel analogue sync dans les systèmes de fichiers ou commit en SQL.
    Ce qui est important est que les opérations de compression ultérieures pourront utiliser le dictionnaire accumulé et le taux de compression ne souffrira pas autant que dans la version précédente.

Je pense qu’il est évident que j’ai choisi la troisième option, regardons-la plus en détail.

Trouvé excellent article à propos de FLUSH dans zlib.

J'ai fait un test de genou basé sur l'article, j'ai pris 70 60 entrées de journal à partir d'un appareil réel, avec une taille de page de XNUMX Ko (nous reviendrons sur la taille des pages plus tard) reçu:

Données initiales
Compression gzip -9 (pas de FLUSH)
zlib avec Z_PARTIAL_FLUSH
zlib avec Z_SYNC_FLUSH

Volume, Ko
1692
40
352
604

À première vue, le prix apporté par FLUSH est excessivement élevé, mais en réalité nous n'avons guère de choix - soit ne pas compresser du tout, soit compresser (et très efficacement) avec FLUSH. Il ne faut pas oublier que nous avons 70 4 enregistrements, la redondance introduite par Z_PARTIAL_FLUSH n'est que de 5 à 5 octets par enregistrement. Et le taux de compression s'est avéré proche de 1:XNUMX, ce qui est plus qu'un excellent résultat.

Cela peut surprendre, mais Z_SYNC_FLUSH est en fait un moyen plus efficace de faire FLUSH

Lors de l'utilisation de Z_SYNC_FLUSH, les 4 derniers octets de chaque entrée seront toujours 0x00, 0x00, 0xff, 0xff. Et si nous les connaissons, nous n’avons pas besoin de les stocker, donc la taille finale n’est que de 324 Ko.

L'article auquel j'ai lié a une explication :

Un nouveau bloc de type 0 avec un contenu vide est ajouté.

Un bloc de type 0 avec un contenu vide se compose de :

  • l'en-tête du bloc de trois bits ;
  • 0 à 7 bits égaux à zéro, pour réaliser l'alignement des octets ;
  • la séquence de quatre octets 00 00 FF FF.

Comme vous pouvez facilement le constater, dans le dernier bloc avant ces 4 octets, il y a de 3 à 10 bits zéro. Cependant, la pratique a montré qu'il existe en réalité au moins 10 bits nuls.

Il s'avère que ces blocs de données courts sont généralement (toujours ?) codés à l'aide d'un bloc de type 1 (bloc fixe), qui se termine nécessairement par 7 bits zéro, ce qui donne un total de 10 à 17 bits zéro garantis (et le reste sera être nul avec une probabilité d'environ 50 %).

Ainsi, sur les données de test, dans 100 % des cas il y a un octet zéro avant 0x00, 0x00, 0xff, 0xff, et dans plus d'un tiers des cas il y a deux octets zéro (Le fait est peut-être que j'utilise CBOR binaire, et lors de l'utilisation de texte JSON, des blocs de type 2 - un bloc dynamique seraient plus courants, respectivement, des blocs sans zéro octet supplémentaire avant 0x00, 0x00, 0xff, 0xff seraient rencontrés).

Au total, en utilisant les données de test disponibles, il est possible d'intégrer moins de 250 Ko de données compressées.

Vous pouvez économiser un peu plus en jonglant avec les bits : pour l'instant on ignore la présence de quelques bits zéro en fin de bloc, quelques bits en début de bloc ne changent pas non plus...
Mais j’ai ensuite pris la décision ferme d’arrêter, sinon, à ce rythme, je pourrais finir par développer mon propre archiveur.

Au total, à partir de mes données de test, j'ai reçu 3 à 4 octets par écriture, le taux de compression s'est avéré supérieur à 6:1. Je vais être honnête : je ne m'attendais pas à un tel résultat ; à mon avis, tout ce qui est meilleur que 2:1 est déjà un résultat qui justifie l'utilisation de la compression.

Tout va bien, mais zlib (deflate) est toujours un algorithme de compression archaïque, bien mérité et légèrement démodé. Le simple fait que les 32 derniers Ko du flux de données non compressé soient utilisés comme dictionnaire semble étrange aujourd'hui (c'est-à-dire que si un bloc de données est très similaire à ce qui se trouvait dans le flux d'entrée il y a 40 Ko, alors il recommencera à être archivé, et ne fera pas référence à un événement antérieur). Dans les archiveurs modernes à la mode, la taille du dictionnaire est souvent mesurée en mégaoctets plutôt qu'en kilo-octets.

Nous continuons donc notre mini-étude sur les archiveurs.

Nous avons ensuite testé bzip2 (rappelez-vous, sans FLUSH, il affichait un fantastique taux de compression de près de 100:1). Malheureusement, les résultats ont été très médiocres avec FLUSH ; la taille des données compressées s'est avérée plus grande que celle des données non compressées.

Mes hypothèses sur les raisons de l'échec

Libbz2 n'offre qu'une seule option de vidage, qui semble effacer le dictionnaire (analogue à Z_FULL_FLUSH dans zlib) ; il n'est plus question de compression efficace après cela.

Et le dernier à tester était zstd. Selon les paramètres, il compresse soit au niveau de gzip, mais beaucoup plus rapidement, soit mieux que gzip.

Hélas, avec FLUSH, cela ne fonctionnait pas très bien : la taille des données compressées était d'environ 700 Ko.

Я a posé une question sur la page github du projet, j'ai reçu une réponse selon laquelle il faut compter jusqu'à 10 octets de données de service pour chaque bloc de données compressées, ce qui est proche des résultats obtenus ; il n'y a aucun moyen de rattraper le dégonflage.

J'ai décidé de m'arrêter à ce stade de mes expérimentations avec les archiveurs (je vous rappelle que xz, lzip, lzo, lz4 ne se sont pas montrés même au stade des tests sans FLUSH, et je n'ai pas envisagé d'algorithmes de compression plus exotiques).

Revenons aux problèmes d'archivage.

Le deuxième problème (comme on dit dans l'ordre et non dans la valeur) est que les données compressées sont un flux unique dans lequel il y a constamment des références aux sections précédentes. Ainsi, si une section de données compressées est endommagée, nous perdons non seulement le bloc de données non compressées associé, mais également tous les suivants.

Il existe une approche pour résoudre ce problème :

  1. Empêchez le problème de se produire - ajoutez de la redondance aux données compressées, ce qui vous permettra d'identifier et de corriger les erreurs ; nous en reparlerons plus tard ;
  2. Minimiser les conséquences en cas de problème
    Nous avons déjà dit plus tôt que vous pouvez compresser chaque bloc de données indépendamment et que le problème disparaîtra de lui-même (les dommages causés aux données d'un bloc entraîneront la perte de données uniquement pour ce bloc). Cependant, il s’agit d’un cas extrême dans lequel la compression des données sera inefficace. L'extrême opposé : utiliser les 4 Mo de notre puce comme une seule archive, ce qui nous donnera une excellente compression, mais des conséquences catastrophiques en cas de corruption des données.
    Oui, un compromis est nécessaire en termes de fiabilité. Mais nous devons nous rappeler que nous développons un format de stockage de données pour mémoire non volatile avec un BER extrêmement faible et une période de stockage des données déclarée de 20 ans.

Au cours des expérimentations, j'ai découvert que des pertes plus ou moins notables du niveau de compression commencent sur des blocs de données compressées de moins de 10 Ko.
Il a été mentionné précédemment que la mémoire utilisée est paginée ; je ne vois aucune raison pour laquelle la correspondance « une page - un bloc de données compressées » ne devrait pas être utilisée.

Autrement dit, la taille de page minimale raisonnable est de 16 Ko (avec une réserve pour les informations de service). Cependant, une taille de page aussi petite impose des restrictions importantes sur la taille maximale de l'enregistrement.

Même si je ne m'attends pas encore à des enregistrements de plus de quelques kilo-octets sous forme compressée, j'ai décidé d'utiliser des pages de 32 Ko (pour un total de 128 pages par puce).

Résumé:

  • Nous stockons les données compressées à l'aide de zlib (deflate) ;
  • Pour chaque entrée, nous définissons Z_SYNC_FLUSH ;
  • Pour chaque enregistrement compressé, nous coupons les octets de fin (par exemple 0x00, 0x00, 0xff, 0xff); dans l'en-tête, nous indiquons combien d'octets nous avons coupés ;
  • Nous stockons les données dans des pages de 32 Ko ; il y a un seul flux de données compressées à l’intérieur de la page ; Sur chaque page nous recommençons la compression.

Et, avant d'en finir avec la compression, je voudrais attirer votre attention sur le fait que nous n'avons que quelques octets de données compressées par enregistrement, il est donc extrêmement important de ne pas gonfler les informations de service, chaque octet compte ici.

Stockage des en-têtes de données

Puisque nous avons des enregistrements de longueur variable, nous devons déterminer d’une manière ou d’une autre l’emplacement/les limites des enregistrements.

Je connais trois approches:

  1. Tous les enregistrements sont stockés dans un flux continu, il y a d'abord un en-tête d'enregistrement contenant la longueur, puis l'enregistrement lui-même.
    Dans ce mode de réalisation, les en-têtes et les données peuvent être de longueur variable.
    Essentiellement, nous obtenons une liste à chaînage unique qui est utilisée tout le temps ;
  2. Les en-têtes et les enregistrements eux-mêmes sont stockés dans des flux séparés.
    En utilisant des collecteurs de longueur constante, nous garantissons que les dommages causés à un collecteur n'affectent pas les autres.
    Une approche similaire est utilisée, par exemple, dans de nombreux systèmes de fichiers ;
  3. Les enregistrements sont stockés dans un flux continu, la limite de l'enregistrement est déterminée par un certain marqueur (un caractère/une séquence de caractères interdit dans les blocs de données). S'il y a un marqueur à l'intérieur de l'enregistrement, nous le remplaçons par une séquence (l'évitons).
    Une approche similaire est utilisée, par exemple, dans le protocole PPP.

Je vais illustrer.

Option 1:
Mon implémentation d'un tampon en anneau dans NOR flash
Tout est ici très simple : connaissant la longueur de l'enregistrement, on peut calculer l'adresse du prochain en-tête. On se déplace donc dans les rubriques jusqu'à rencontrer une zone remplie de 0xff (zone libre) ou la fin de la page.

Option 2:
Mon implémentation d'un tampon en anneau dans NOR flash
En raison de la longueur variable des enregistrements, nous ne pouvons pas dire à l'avance de combien d'enregistrements (et donc d'en-têtes) nous aurons besoin par page. Vous pouvez répartir les en-têtes et les données elles-mêmes sur différentes pages, mais je préfère une approche différente : nous plaçons à la fois les en-têtes et les données sur une seule page, mais les en-têtes (de taille constante) viennent du début de la page, et le les données (de longueur variable) proviennent de la fin. Dès qu'ils se « rencontrent » (il n'y a pas assez d'espace libre pour une nouvelle entrée), nous considérons cette page comme terminée.

Option 3:
Mon implémentation d'un tampon en anneau dans NOR flash
Il n'est pas nécessaire de stocker la longueur ou d'autres informations sur l'emplacement des données dans l'en-tête : des marqueurs indiquant les limites des enregistrements suffisent. Cependant, les données doivent être traitées lors de l'écriture/lecture.
J'utiliserais 0xff comme marqueur (qui remplit la page après effacement), donc la zone libre ne sera certainement pas traitée comme des données.

Tableau de comparaison:

Option 1
Option 2
Option 3

Tolérance aux erreurs
-
+
+

Compacité
+
-
+

Complexité de mise en œuvre
*
**
**

L'option 1 présente un défaut fatal : si l'un des collecteurs est endommagé, toute la chaîne suivante est détruite. Les options restantes vous permettent de récupérer certaines données même en cas de dommages massifs.
Mais ici, il convient de rappeler que nous avons décidé de stocker les données sous forme compressée, et ainsi nous perdons toutes les données de la page après un enregistrement « cassé », donc même s'il y a un moins dans le tableau, nous ne le faisons pas. Prenez le en compte.

Compacité :

  • dans la première option, nous devons stocker uniquement la longueur dans l'en-tête ; si nous utilisons des entiers de longueur variable, alors dans la plupart des cas, nous pouvons nous contenter d'un octet ;
  • dans la deuxième option, nous devons stocker l'adresse de départ et la longueur ; l'enregistrement doit être d'une taille constante, j'estime 4 octets par enregistrement (deux octets pour le décalage, et deux octets pour la longueur) ;
  • la troisième option n'a besoin que d'un seul caractère pour indiquer le début de l'enregistrement, et l'enregistrement lui-même augmentera de 1 à 2 % en raison du blindage. En général, à peu près à parité avec la première option.

Au départ, j'ai considéré la deuxième option comme la principale (et j'ai même écrit l'implémentation). Je ne l'ai abandonné que lorsque j'ai finalement décidé d'utiliser la compression.

Peut-être qu'un jour j'utiliserai encore une option similaire. Par exemple, si je dois m'occuper du stockage de données pour un navire voyageant entre la Terre et Mars, il y aura des exigences complètement différentes en matière de fiabilité, de rayonnement cosmique, ...

Quant à la troisième option : je lui ai donné deux étoiles pour la difficulté de mise en œuvre tout simplement parce que je n'aime pas jouer avec les blindages, changer la longueur au passage, etc. Oui, je suis peut-être partial, mais je vais devoir écrire le code - pourquoi vous forcer à faire quelque chose que vous n'aimez pas.

Résumé: Nous choisissons l'option de stockage sous forme de chaînes « en-tête de longueur - données de longueur variable » en raison de son efficacité et de sa facilité de mise en œuvre.

Utilisation de champs de bits pour surveiller le succès des opérations d'écriture

Je ne me souviens plus d'où m'est venue l'idée, mais cela ressemble à ceci :
Pour chaque entrée, nous allouons plusieurs bits pour stocker les drapeaux.
Comme nous l'avons dit plus tôt, après l'effacement, tous les bits sont remplis de 1 et nous pouvons changer 1 en 0, mais pas l'inverse. Donc pour « le drapeau n’est pas défini », nous utilisons 1, pour « le drapeau est défini », nous utilisons 0.

Voici à quoi pourrait ressembler la mise en mémoire flash d'un enregistrement de longueur variable :

  1. Activez le drapeau « l'enregistrement de la durée a commencé » ;
  2. Enregistrez la longueur ;
  3. Activez le drapeau « l'enregistrement des données a commencé » ;
  4. Nous enregistrons des données ;
  5. Définissez le drapeau « enregistrement terminé ».

De plus, nous aurons un indicateur « erreur survenue », pour un total de 4 indicateurs bits.

Dans ce cas, nous avons deux états stables « 1111 » - l'enregistrement n'a pas démarré et « 1000 » - l'enregistrement a réussi ; en cas d'interruption inattendue du processus d'enregistrement, nous recevrons des états intermédiaires, que nous pourrons alors détecter et traiter.

L'approche est intéressante, mais elle ne protège que contre les coupures de courant soudaines et les pannes similaires, ce qui, bien sûr, est important, mais c'est loin d'être la seule (ni même la principale) raison d'éventuelles pannes.

Résumé: Passons à la recherche d'une bonne solution.

Sommes de contrôle

Les sommes de contrôle permettent également de s'assurer (avec une probabilité raisonnable) que l'on lit exactement ce qui aurait dû être écrit. Et contrairement aux champs de bits évoqués ci-dessus, ils fonctionnent toujours.

Si nous considérons la liste des sources potentielles de problèmes dont nous avons discuté ci-dessus, alors la somme de contrôle est capable de reconnaître une erreur quelle que soit son origine. (sauf peut-être pour les extraterrestres malveillants - ils peuvent également falsifier la somme de contrôle).

Donc, si notre objectif est de vérifier que les données sont intactes, les sommes de contrôle sont une excellente idée.

Le choix de l'algorithme de calcul de la somme de contrôle n'a posé aucune question - CRC. D'une part, les propriétés mathématiques permettent de détecter à 100 % certains types d'erreurs ; d'autre part, sur des données aléatoires, cet algorithme montre généralement la probabilité de collisions qui n'est pas très supérieure à la limite théorique. Mon implémentation d'un tampon en anneau dans NOR flash. Ce n'est peut-être pas l'algorithme le plus rapide, ni toujours le minimum en termes de nombre de collisions, mais il a une qualité très importante : dans les tests que j'ai rencontrés, il n'y a eu aucun modèle dans lequel il a clairement échoué. La stabilité est la principale qualité dans ce cas.

Exemple d'étude volumétrique : Partie 1, Partie 2 (liens vers narod.ru, désolé).

Cependant, la tâche de sélection d'une somme de contrôle n'est pas terminée : le CRC est toute une famille de sommes de contrôle. Vous devez décider de la longueur, puis choisir un polynôme.

Choisir la longueur de la somme de contrôle n’est pas une question aussi simple qu’il y paraît à première vue.

Permettez-moi d'illustrer :
Ayons la probabilité d'une erreur dans chaque octet Mon implémentation d'un tampon en anneau dans NOR flash et une somme de contrôle idéale, calculons le nombre moyen d'erreurs par million d'enregistrements :

Données, octet
Somme de contrôle, octet
Erreurs non détectées
Détections de fausses erreurs
Total des faux positifs

1
0
1000
0
1000

1
1
4
999
1003

1
2
≈0
1997
1997

1
4
≈0
3990
3990

10
0
9955
0
9955

10
1
39
990
1029

10
2
≈0
1979
1979

10
4
≈0
3954
3954

1000
0
632305
0
632305

1000
1
2470
368
2838

1000
2
10
735
745

1000
4
≈0
1469
1469

Il semblerait que tout soit simple - en fonction de la longueur des données à protéger, choisissez la longueur de la somme de contrôle avec un minimum de positifs incorrects - et l'astuce est dans le sac.

Cependant, un problème se pose avec les sommes de contrôle courtes : bien qu'elles soient efficaces pour détecter les erreurs sur un seul bit, elles peuvent, avec une probabilité assez élevée, accepter des données complètement aléatoires comme correctes. Il y avait déjà un article sur Habré décrivant problème dans la vraie vie.

Par conséquent, pour rendre presque impossible une correspondance de somme de contrôle aléatoire, vous devez utiliser des sommes de contrôle d’une longueur de 32 bits ou plus. (pour les longueurs supérieures à 64 bits, des fonctions de hachage cryptographique sont généralement utilisées).

Malgré le fait que j'ai écrit plus tôt que nous devons par tous les moyens économiser de l'espace, nous utiliserons toujours une somme de contrôle de 32 bits (16 bits ne suffisent pas, la probabilité d'une collision est supérieure à 0.01 % ; et 24 bits, car ils disons, ne sont ni ici ni là) .

Une objection peut surgir ici : a-t-on sauvegardé chaque octet lors du choix de la compression pour donner désormais 4 octets d'un coup ? Ne vaudrait-il pas mieux ne pas compresser ou ajouter de somme de contrôle ? Bien sûr que non, pas de compression ne veut pas dire, que nous n’avons pas besoin de vérification d’intégrité.

Lors du choix d'un polynôme, nous ne réinventerons pas la roue, mais prendrons le désormais populaire CRC-32C.
Ce code détecte des erreurs de 6 bits sur des paquets jusqu'à 22 octets (peut-être le cas le plus courant pour nous), des erreurs de 4 bits sur des paquets jusqu'à 655 octets (un cas également courant pour nous), 2 ou tout nombre impair d'erreurs de bits sur les paquets. de toute longueur raisonnable.

Si quelqu'un est intéressé par les détails

Article Wikipédia à propos du CRC.

Paramètres du code CRC-32c sur Site Internet de Koopman – peut-être le principal spécialiste du CRC de la planète.

В son article il est un autre code intéressant, qui fournit des paramètres légèrement meilleurs pour les longueurs de paquets qui nous concernent, mais je n'ai pas considéré la différence comme significative et j'étais suffisamment compétent pour choisir un code personnalisé au lieu du code standard et bien documenté.

Aussi, puisque nos données sont compressées, la question se pose : faut-il calculer la somme de contrôle des données compressées ou non compressées ?

Arguments en faveur du calcul de la somme de contrôle des données non compressées :

  • En fin de compte, nous devons vérifier la sécurité du stockage des données - nous le vérifions donc directement (en même temps, les erreurs possibles dans la mise en œuvre de la compression/décompression, les dommages causés par une mémoire cassée, etc. seront vérifiés) ;
  • L'algorithme deflate dans zlib a une implémentation assez mature et ne devrait pas tomber avec des données d'entrée « tordues » ; de plus, il est souvent capable de détecter indépendamment les erreurs dans le flux d'entrée, réduisant ainsi la probabilité globale de ne pas détecter une erreur (effectué un test en inversant un seul bit dans un enregistrement court, zlib a détecté une erreur dans environ un tiers des cas).

Arguments contre le calcul de la somme de contrôle des données non compressées :

  • CRC est « adapté » spécifiquement pour les quelques erreurs binaires caractéristiques de la mémoire flash (une erreur binaire dans un flux compressé peut provoquer un changement massif dans le flux de sortie, sur lequel, en théorie pure, nous pouvons « attraper » une collision) ;
  • Je n'aime pas trop l'idée de transmettre des données potentiellement cassées au décompresseur, qui saitcomment il va réagir.

Dans ce projet, j'ai décidé de m'écarter de la pratique généralement acceptée consistant à stocker une somme de contrôle de données non compressées.

Résumé: Nous utilisons CRC-32C, nous calculons la somme de contrôle à partir des données sous la forme dans laquelle elles sont écrites en flash (après compression).

Redondance

Bien entendu, l’utilisation d’un codage redondant n’élimine pas la perte de données, mais elle peut réduire considérablement (souvent de plusieurs ordres de grandeur) la probabilité d’une perte de données irrécupérable.

Nous pouvons utiliser différents types de redondance pour corriger les erreurs.
Les codes de Hamming peuvent corriger des erreurs sur un seul bit, les codes de caractères Reed-Solomon, plusieurs copies de données combinées avec des sommes de contrôle ou des codages comme RAID-6 peuvent aider à récupérer des données même en cas de corruption massive.
Au départ, j'étais attaché à l'utilisation généralisée du codage résistant aux erreurs, mais j'ai ensuite réalisé que nous devons d'abord avoir une idée des erreurs contre lesquelles nous voulons nous protéger, puis choisir le codage.

Nous avons dit plus tôt que les erreurs doivent être détectées le plus rapidement possible. À quels moments pouvons-nous rencontrer des erreurs ?

  1. Enregistrement inachevé (pour une raison quelconque au moment de l'enregistrement, l'alimentation a été coupée, le Raspberry s'est figé, ...)
    Hélas, dans le cas d'une telle erreur, il ne reste plus qu'à ignorer les enregistrements invalides et à considérer les données comme perdues ;
  2. Erreurs d'écriture (pour une raison quelconque, ce qui a été écrit dans la mémoire flash n'est pas ce qui a été écrit)
    Nous pouvons détecter immédiatement de telles erreurs si nous effectuons un test de lecture immédiatement après l'enregistrement ;
  3. Distorsion des données en mémoire lors du stockage ;
  4. Erreurs de lecture
    Pour le corriger, si la somme de contrôle ne correspond pas, il suffit de répéter la lecture plusieurs fois.

Autrement dit, seules les erreurs du troisième type (corruption spontanée des données lors du stockage) ne peuvent pas être corrigées sans un codage résistant aux erreurs. Il semble que de telles erreurs soient encore extrêmement improbables.

Résumé: il a été décidé d'abandonner le codage redondant, mais si l'opération montre l'erreur de cette décision, alors revenez à l'examen du problème (avec les statistiques d'échec déjà accumulées, qui permettront de choisir le type de codage optimal).

autre

Bien entendu, le format de l'article ne nous permet pas de justifier chaque élément du format (et mes forces sont déjà épuisées), je vais donc revenir brièvement sur certains points non abordés précédemment.

  • Il a été décidé de rendre toutes les pages « égales »
    Autrement dit, il n'y aura pas de pages spéciales avec des métadonnées, des threads séparés, etc., mais plutôt un seul thread qui réécrit toutes les pages à tour de rôle.
    Cela garantit une usure uniforme des pages, sans aucun point de défaillance, et j'aime ça ;
  • Il est impératif de prévoir un versionnage du format.
    Un format sans numéro de version dans l’en-tête, c’est mal !
    Il suffit d'ajouter un champ avec un certain Numéro Magique (signature) à l'en-tête de la page, qui indiquera la version du format utilisé (Je ne pense pas qu'en pratique il y en aura ne serait-ce qu'une douzaine);
  • Utilisez un en-tête de longueur variable pour les enregistrements (qui sont nombreux), en essayant de lui donner une longueur de 1 octet dans la plupart des cas ;
  • Pour coder la longueur de l'en-tête et la longueur de la partie tronquée de l'enregistrement compressé, utilisez des codes binaires de longueur variable.

A beaucoup aidé générateur en ligne Codes de Huffman. En quelques minutes seulement, nous avons pu sélectionner les codes de longueur variable requis.

Description du format de stockage des données

Ordre des octets

Les champs de plus d'un octet sont stockés au format big-endian (ordre des octets du réseau), c'est-à-dire que 0x1234 est écrit sous la forme 0x12, 0x34.

Pagination

Toute la mémoire flash est divisée en pages de taille égale.

La taille de page par défaut est de 32 Ko, mais pas plus de 1/4 de la taille totale de la puce mémoire (pour une puce de 4 Mo, 128 pages sont obtenues).

Chaque page stocke les données indépendamment des autres (c'est-à-dire que les données d'une page ne font pas référence aux données d'une autre page).

Toutes les pages sont numérotées dans l'ordre naturel (par ordre croissant d'adresses), en commençant par le numéro 0 (la page zéro commence à l'adresse 0, la première page commence à 32 Ko, la deuxième page commence à 64 Ko, etc.)

La puce mémoire est utilisée comme tampon cyclique (ring buffer), c'est-à-dire que la première écriture va à la page numéro 0, puis au numéro 1, ..., lorsque nous remplissons la dernière page, un nouveau cycle commence et l'enregistrement continue à partir de la page zéro. .

À l'intérieur de la page

Mon implémentation d'un tampon en anneau dans NOR flash
Au début de la page, un en-tête de page de 4 octets est stocké, puis une somme de contrôle d'en-tête (CRC-32C), puis les enregistrements sont stockés au format « en-tête, données, somme de contrôle ».

Le titre de la page (vert sale dans le schéma) se compose de :

  • champ Numéro Magique de deux octets (également signe de la version du format)
    pour la version actuelle du format, il est calculé comme suit 0xed00 ⊕ номер страницы;
  • compteur sur deux octets « Version de la page » (numéro de cycle de réécriture en mémoire).

Les entrées sur la page sont stockées sous forme compressée (l'algorithme deflate est utilisé). Tous les enregistrements d'une page sont compressés dans un seul thread (un dictionnaire commun est utilisé) et à chaque nouvelle page, la compression recommence. Autrement dit, pour décompresser n'importe quel enregistrement, tous les enregistrements précédents de cette page (et seulement celui-ci) sont requis.

Chaque enregistrement sera compressé avec le drapeau Z_SYNC_FLUSH, et à la fin du flux compressé il y aura 4 octets 0x00, 0x00, 0xff, 0xff, éventuellement précédés d'un ou deux octets zéro supplémentaires.
Nous supprimons cette séquence (d'une longueur de 4, 5 ou 6 octets) lors de l'écriture dans la mémoire flash.

L'en-tête d'enregistrement est de 1, 2 ou 3 octets stockant :

  • un bit (T) indiquant le type d'enregistrement : 0 - contexte, 1 - journal ;
  • un champ de longueur variable (S) de 1 à 7 bits, définissant la longueur de l'en-tête et de la « queue » qu'il faut ajouter à l'enregistrement pour la décompression ;
  • longueur d'enregistrement (L).

Tableau des valeurs S :

S
Longueur de l'en-tête, octets
Rejeté lors de l'écriture, octet

0
1
5 (00 00 00 ff ff)

10
1
6 (00 00 00 00 ff ff)

110
2
4 (00 00 ff ff)

1110
2
5 (00 00 00 ff ff)

11110
2
6 (00 00 00 00 ff ff)

1111100
3
4 (00 00 ff ff)

1111101
3
5 (00 00 00 ff ff)

1111110
3
6 (00 00 00 00 ff ff)

J'ai essayé d'illustrer, je ne sais pas avec quelle clarté cela s'est avéré :
Mon implémentation d'un tampon en anneau dans NOR flash
Le jaune indique ici le champ T, le blanc le champ S, le vert L (la longueur des données compressées en octets), le bleu les données compressées, le rouge les derniers octets des données compressées qui ne sont pas écrits dans la mémoire flash.

Ainsi, nous pouvons écrire des en-têtes d'enregistrement de la longueur la plus courante (jusqu'à 63+5 octets sous forme compressée) dans un octet.

Après chaque enregistrement, une somme de contrôle CRC-32C est stockée, dans laquelle la valeur inversée de la somme de contrôle précédente est utilisée comme valeur initiale (init).

CRC a la propriété de « durée », la formule suivante fonctionne (inversion de bits plus ou moins dans le processus) : Mon implémentation d'un tampon en anneau dans NOR flash.
Autrement dit, nous calculons le CRC de tous les octets précédents d'en-têtes et de données sur cette page.

Directement après la somme de contrôle se trouve l'en-tête de l'enregistrement suivant.

L'en-tête est conçu de telle sorte que son premier octet soit toujours différent de 0x00 et 0xff (si au lieu du premier octet de l'en-tête nous rencontrons 0xff, cela signifie qu'il s'agit d'une zone inutilisée ; 0x00 signale une erreur).

Exemples d'algorithmes

Lecture à partir de la mémoire Flash

Toute lecture s'accompagne d'une vérification de la somme de contrôle.
Si la somme de contrôle ne correspond pas, la lecture est répétée plusieurs fois dans l'espoir de lire les données correctes.

(cela a du sens, Linux ne met pas en cache les lectures de NOR Flash, testé)

Écrire dans la mémoire flash

Nous enregistrons les données.
Lisons-les.

Si les données lues ne correspondent pas aux données écrites, nous remplissons la zone avec des zéros et signalons une erreur.

Préparer un nouveau microcircuit pour le fonctionnement

Pour l'initialisation, un en-tête avec la version 1 est écrit sur la première page (ou plutôt zéro).
Après cela, le contexte initial est écrit sur cette page (contient l'UUID de la machine et les paramètres par défaut).

Ça y est, la mémoire flash est prête à l'emploi.

Chargement de la machine

Lors du chargement, les 8 premiers octets de chaque page (en-tête + CRC) sont lus, les pages avec un Numéro Magique inconnu ou un CRC incorrect sont ignorées.
Parmi les pages « correctes », les pages avec la version maximale sont sélectionnées et la page avec le numéro le plus élevé en est extraite.
Le premier enregistrement est lu, l'exactitude du CRC et la présence du flag « contexte » sont vérifiées. Si tout va bien, cette page est considérée comme actuelle. Sinon, nous revenons à la précédente jusqu'à ce que nous trouvions une page « en direct ».
et sur la page trouvée nous lisons tous les enregistrements, ceux que nous utilisons avec le drapeau « contexte ».
Enregistrez le dictionnaire zlib (il sera nécessaire pour l'ajouter à cette page).

Ça y est, le téléchargement est terminé, le contexte est restauré, vous pouvez travailler.

Ajouter une entrée de journal

Nous compressons l'enregistrement avec le bon dictionnaire, en spécifiant Z_SYNC_FLUSH. Nous voyons si l'enregistrement compressé tient sur la page actuelle.
Si cela ne convient pas (ou s'il y a eu des erreurs CRC sur la page), démarrez une nouvelle page (voir ci-dessous).
Nous notons le dossier et le CRC. Si une erreur se produit, démarrez une nouvelle page.

Nouvelle page

Nous sélectionnons une page gratuite avec le nombre minimum (nous considérons qu'une page gratuite est une page avec une somme de contrôle incorrecte dans l'en-tête ou avec une version inférieure à l'actuelle). S'il n'y a pas de telles pages, sélectionnez la page avec le nombre minimum parmi celles qui ont une version égale à la version actuelle.
Nous effaçons la page sélectionnée. Nous vérifions le contenu avec 0xff. Si quelque chose ne va pas, prenez la page gratuite suivante, etc.
Nous écrivons un en-tête sur la page effacée, la première entrée est l'état actuel du contexte, la suivante est l'entrée de journal non écrite (s'il y en a une).

Applicabilité du format

À mon avis, il s'est avéré être un bon format pour stocker des flux d'informations plus ou moins compressibles (texte brut, JSON, MessagePack, CBOR, éventuellement protobuf) dans NOR Flash.

Bien entendu, le format est « adapté » pour SLC NOR Flash.

Il ne doit pas être utilisé avec des supports à BER élevé tels que NAND ou MLC NOR (une telle mémoire est-elle même disponible à la vente ? Je ne l'ai vue mentionnée que dans des ouvrages sur les codes de correction).

De plus, il ne doit pas être utilisé avec des appareils dotés de leur propre FTL : clé USB, SD, MicroSD, etc. (pour une telle mémoire, j'ai créé un format avec une taille de page de 512 octets, une signature au début de chaque page et des numéros d'enregistrement uniques - il était parfois possible de récupérer toutes les données d'un lecteur flash « défectueux » par simple lecture séquentielle).

Selon les tâches, le format peut être utilisé sans modification sur des clés USB de 128 Ko (16 Ko) à 1 Gbit (128 Mo). Si vous le souhaitez, vous pouvez l'utiliser sur des puces plus grandes, mais vous devrez probablement ajuster la taille de la page. (Mais ici la question de la faisabilité économique se pose déjà ; le prix du NOR Flash à gros volume n'est pas encourageant).

Si quelqu'un trouve le format intéressant et souhaite l'utiliser dans un projet ouvert, écrivez, j'essaierai de trouver le temps, de peaufiner le code et de le publier sur github.

Conclusion

Comme vous pouvez le constater, le format s'est finalement avéré simple. et même ennuyeux.

Il est difficile de refléter l’évolution de mon point de vue dans un article, mais croyez-moi : au départ, je voulais créer quelque chose de sophistiqué, d’indestructible, capable de survivre même à une explosion nucléaire à proximité. Cependant, la raison (je l'espère) a quand même gagné et progressivement les priorités se sont déplacées vers la simplicité et la compacité.

Se pourrait-il que j'aie eu tort ? Oui bien sûr. Il se pourrait bien, par exemple, que nous ayons acheté un lot de microcircuits de mauvaise qualité. Ou pour une autre raison, l'équipement ne répondra pas aux attentes en matière de fiabilité.

Est-ce que j'ai un plan pour ça ? Je pense qu’après avoir lu l’article, vous n’avez aucun doute sur l’existence d’un plan. Et même pas seul.

Sur une note un peu plus sérieuse, le format a été développé à la fois comme une option de travail et comme un « ballon d'essai ».

Pour le moment, tout sur la table fonctionne bien, littéralement l'autre jour, la solution sera déployée (environ) sur des centaines d'appareils, voyons ce qui se passe en opération « combat » (heureusement, j'espère que le format vous permettra de détecter les pannes de manière fiable ; vous pourrez ainsi collecter des statistiques complètes). Dans quelques mois, il sera possible de tirer des conclusions (et si vous n'avez pas de chance, même plus tôt).

Si, sur la base des résultats d'utilisation, de graves problèmes sont découverts et que des améliorations sont nécessaires, j'écrirai certainement à ce sujet.

littérature

Je ne voulais pas faire une longue liste fastidieuse d’ouvrages d’occasion ; après tout, tout le monde possède Google.

Ici, j'ai décidé de laisser une liste de découvertes qui m'ont semblé particulièrement intéressantes, mais progressivement elles ont migré directement dans le texte de l'article, et un élément est resté sur la liste :

  1. Utilitaire infgen de l'auteur zlib. Peut afficher clairement le contenu des archives deflate/zlib/gzip. Si vous devez gérer la structure interne du format deflate (ou gzip), je vous le recommande vivement.

Source: habr.com

Ajouter un commentaire