Hydroponique sur un rebord de fenêtre ou C ++ 11 dans les microcontrôleurs AVR

Le projet ne contient pas Arduino


Ce projet devait à l'origine être différent - une structure monumentale composée d'un piédestal avec des boîtes et des pompes, un aquarium monté dessus et une oasis de tomates au-dessus. Une cascade était prévue dans le paradis d'une oasis de tomates et des formes de vie de poissons dans l'aquarium, dont la principale exigence était la capacité de manger les habitants imprévus de l'aquarium et de garder le verre propre; les principaux candidats sont le somiki et le gourami. Comme vous l’avez peut-être deviné, ma devise est «la paresse est le moteur du progrès» (et que pouvez-vous faire pour ne pas nettoyer l’aquarium et ne pas arroser les tomates).

Un monument à cette devise aurait probablement été érigé s'il ne s'était pas déjà effondré au stade de la coordination des ébauches avec sa femme. Elle n'a pas été inspirée par l'idée de faire de cette bandura la décoration principale du salon, et même la cascade ne l'a pas convaincue. Mais l'idée d'un système autonome, une symbiose de la biologie et de l'électronique, ne voulait pas sortir de ma tête, et le projet s'est réduit à la taille d'un pot de fleur - l'aquaponie s'est transformée en hydroponie, la vie des poissons a été sauvée.

L'idée principale de la culture hydroponique est l'utilisation d'une solution aqueuse de nutriments au lieu du sol. Cela permet à un ordre de grandeur d'accélérer la croissance des plantes. Cependant, on ne peut pas simplement abaisser les racines dans l'eau - elles ont besoin d'oxygène, sans quoi elles commenceront à mourir. À cet égard, il existe des options - souffler constamment de l'eau avec un compresseur, comme dans un aquarium, ou inonder périodiquement les racines avec une solution nutritive, et l'égoutter après un certain temps. La première option a un inconvénient - le bourdonnement constant du compresseur. La deuxième option a un avantage - la plupart des racines sont dans l'air, respirent activement et l'effet d'accélération de la croissance devrait être encore plus important. De plus, ils sont immergés dans un substrat de granules poreux spéciaux qui retiennent l'humidité. Le choix était évident, j'ai pris la deuxième option comme base.Dans le cas des poissons, le système pourrait se révéler presque complètement fermé - les sécrétions de poissons sont traitées par des bactéries spéciales dans un biofiltre, le produit transformé est introduit dans les plantes, une couche de sable filtre l'eau, de l'eau propre est renvoyée dans l'aquarium. Dans un cas idéal, les aliments sont parfois saupoudrés dans un chargeur automatique et les tomates se rassemblent dans les buissons. Mais cela n'a pas grandi ensemble, c'est peut-être pour le mieux - qui sait comment se terminerait la commande par courrier d'une souche de bactéries nécessaires.

En conséquence, l'appareil de la tomate a pris des contours. Deux vaisseaux - le bas avec de l'eau, le supérieur avec un substrat et une plante. Pour les inondations, nous utiliserons une petite pompe chinoise avec un moteur à courant continu, pour le drainage, nous utiliserons un siphon automatique. Le principe de fonctionnement du siphon dans la vidéo:

Hydroponie avec un siphon similaire:


Le cerveau de l'appareil est le microcontrôleur ATMEGA328P (simplement parce que le placer était à portée de main). Ses tâches comprennent la gestion des inondations et des rejets selon un calendrier, la surveillance du niveau d'eau dans le réservoir et la signalisation de son manque, le contrôle de l'éclairage de la plante (nous voulons avoir une certaine longueur minimale de lumière du jour; à la fin de la lumière naturelle, la lumière artificielle s'allume progressivement), une interface utilisateur pour la visualisation l'état, la gestion et la configuration de toute cette économie. De toute évidence, cela nécessite une sorte de solution pour le capteur de niveau d'eau, les capteurs de lumière, une horloge en temps réel et une sorte de terminal utilisateur.

Avant de décrire les détails, une liste des ressources du projet:

Ici vous pouvez voir des photos du résultat et du processus de fabrication.

Petite vidéo:



Le projet est disponible sur GitHub . Là, dans les versions, un fichier avec le projet de pièce électronique dans KiCAD et les projets de conception de cloches et de sifflets dans SolidWorks est disposé (des fichiers STL pour l'impression sont joints).

Caractéristiques de l'assemblage du firmware
« ». , , , USB AVR (, , , , ), . - , , 'ADK_ROOT' , 'scons'.


Schéma de la partie électronique:



Plus de détails, une description des pièges et un peu de code. Description des problèmes logiciels à la toute fin . Peut-être que quelqu'un sera intéressé de voir un nouvel exemple de travail avec I2C, un valcoder, un module RTC et un affichage graphique. Tout le code du projet a été écrit «à partir de zéro» sans utiliser de solutions tierces (car je le peux).

Capteur de niveau d'eau


La question la plus sensible a été décidée en premier. Il y avait, bien sûr, une variante d'une sorte de flotteur de sorte que, par exemple, il déplacerait le rail sur lequel le code Gray est appliqué, et les capteurs optiques seraient lus. Mais cela semblait vraiment peu fiable. La recherche sur eBay n'a pas donné de résultat - il y avait soit des interrupteurs à flotteur (atteint ou non le niveau souhaité), soit des électrodes immergées et des lectures basées sur la conductivité du milieu, mais cela a été immédiatement noté, car la composition de l'eau changerait constamment avec la conductivité des engrais ajoutés et la dissolution impuretés du substrat. En conséquence, l'idée est venue d'utiliser un télémètre à ultrasons, l'un de ceux qui sont généralement placés sur différents robots. Comme prévu, le capteur est placé dans le couvercle du réservoir et le signal est réfléchi directement depuis la surface de l'eau. A été acheté HC-SR04 (le choix de la plus petite valeur de la distance de travail minimale - il a 2 cm),et le concept a été vérifié sur un seau d'eau. Il s'est avéré que cela fonctionnait pour lui-même (on craignait qu'il n'y ait pas de réflexion normale à la surface de l'eau, ou qu'il n'y ait pas assez de directivité du faisceau et qu'il y ait des réflexions indésirables des parois du réservoir). Soit dit en passant, le télémètre était également une option de sauvegarde, mais infrarouge. À la surface de l'eau était censé lancer un flotteur avec un réflecteur. Le seul problème est leur distance de travail minimale de 10 cm (de celles que j'ai trouvées), ce qui est déjà un peu trop pour les dimensions données.À la surface de l'eau était censé lancer un flotteur avec un réflecteur. Le seul problème est leur distance de travail minimale de 10 cm (de celles que j'ai trouvées), ce qui est déjà un peu trop pour les dimensions données.À la surface de l'eau était censé lancer un flotteur avec un réflecteur. Le seul problème est leur distance de travail minimale de 10 cm (de celles que j'ai trouvées), ce qui est déjà un peu trop pour les dimensions données.




Selon les résultats du projet, cette approche fonctionne et peut être utilisée dans la pratique, aucun problème n'a été constaté. Il vaut la peine de prendre des mesures pour isoler la planche de l'humidité (scellement dans le boîtier). C'est juste que les capteurs eux-mêmes restent ouverts, peut-être que ça revient toujours.

L'interface du capteur est simple - une impulsion est envoyée à l'entrée de déclenchement, ce qui déclenche un signal d'écho. Une impulsion est générée à la sortie d'écho, dont la longueur est égale au temps écoulé entre le début du rayonnement et l'acceptation du signal d'écho réfléchi. En mesurant la longueur d'impulsion, en connaissant la vitesse du son et le fait que le signal va à l'objet et vice-versa, vous pouvez calculer la distance. Dans le projet, cela est implémenté dans la classe LevelGauge. Pour mesurer la longueur d'impulsion, la capacité matérielle de la «capture d'entrée» MK AVR est utilisée. Dans ce cas, le temporisateur matériel est réinitialisé sur le front montant de l'impulsion et sur la valeur descendante du temporisateur, le matériel est stocké dans le registre ICR1 et une interruption est générée. Ainsi, il est possible de mesurer la durée d'impulsion avec une précision suffisante et une consommation minimale de temps processeur.

Même avec ce modèle de capteur, un problème a été remarqué - lorsque l'alimentation a été appliquée, la ligne d'écho est restée constamment active. Il a contourné en appliquant une impulsion au déclencheur et en attendant que le premier cycle de localisation d'écho soit passé.

Rétroéclairage


Le rétro-éclairage est composé de trois LED de préhension. J'ai plié le cadre triangulaire du profil en aluminium, collé les LED dessus avec de l'époxy. J'ai commandé un stabilisateur de courant chinois à 700mA pour l'alimentation. Environ trois volts tombent sur chaque diode, le stabilisateur nécessite une différence entre les tensions d'entrée et de sortie d'au moins deux volts, et j'allais alimenter la wunderwafer entière à partir d'une alimentation de 12 volts. De là, il est facile de calculer pourquoi exactement trois LED.

Les diodes sont d'un blanc chaud. Cela me semblait naturel, le spectre solaire et tout ça. Mais comme je l'ai découvert plus tard, après les avoir commandées, les plantes utilisent généralement une combinaison de rouge et de bleu. Pour autant que je sache, toute la question n'est que de l'efficacité. Si vous avez une grande ferme avec un éclairage 24h / 24, alors vous êtes intéressé par le fait que toute l'énergie dépensée est dépensée pour de bon. Sous un éclairage blanc, les feuilles vertes refléteront la composante verte, une partie importante de l'énergie dépensée pour l'éclairage sera gaspillée.





Une caractéristique importante du stabilisateur est la présence d'une entrée pour la régulation PWM, que j'utilise pour régler la luminosité. Voici un autre râteau chinois. Tout d'abord, il s'est avéré être simplement une fonction d'activation / désactivation actuelle. Autrement dit, je m'attendais à ce que le courant de sortie ne soit pas modulé, et sa valeur dépendrait du rapport cyclique du signal PWM, mais le courant répétait simplement les impulsions à l'entrée de commande. Mais ce n'est pas si mal, une autre embuscade était que le régulateur réagit de manière inadéquate au PWM avec une fréquence assez élevée. J'ai dû le baisser à 300 Hz, auquel il fonctionnait plus ou moins normalement. Le signal PWM est généré par le microcontrôleur dans le matériel en utilisant l'un des temporisateurs.



Les capteurs de lumière constituent une autre partie importante de l'ensemble de rétroéclairage. Des phototransistors ont été sélectionnés dans ce rôle. Et oui, il y en a deux - un au-dessus des LED pour mesurer la lumière naturelle, le second sous les LED pour fournir un retour. Certes, la fonctionnalité d'extension automatique de la lumière du jour n'a pas encore été implémentée, comme c'était le cas en été, et elle n'était pas nécessaire (et la motivation est une affaire sérieuse). Il a été supposé que dès que le premier capteur détecte une diminution du niveau d'éclairage (et que le temps alloué pour les heures de clarté n'a pas encore expiré), la lumière est réglée de sorte que le deuxième capteur produise un niveau correspondant à l'éclairage souhaité. Pour ce faire, vous devez implémenter un contrôleur PID simple dans le code. Mais dans l'interface, vous ne pouvez voir que les lectures actuelles du capteur et enrouler manuellement la luminosité de rétro-éclairage souhaitée.

Faites attention à la connexion des capteurs. Chacun d'eux a deux plages fixes, qui sont sélectionnées en connectant à zéro la résistance correspondante. Le pied du microcontrôleur, connecté à la deuxième résistance, est alors transféré dans un état de haute résistance. Vous pouvez activer les deux résistances simultanément, puis il y aura trois plages de mesure fixes. Le signal des résistances d'émetteur passe à travers un circuit RC pour filtrer les impulsions modulantes - la lumière des LEDs pulsent avec le signal PWM sur le régulateur de courant.

Pompe




L'équipement chinois le moins cher, avec un moteur à courant continu. Des embuscades, bien sûr, sont disponibles. Malgré le fait qu'il indique 12V, il ne fonctionne pas longtemps à cette tension. Un a brûlé avant l'assemblage de la structure. Le schéma prévoit pour PWM pour cela, la puissance maximale est configurée dans l'interface, dans la pratique, il n'a pas réglé au-dessus de 70%. Déjà à ce niveau, il hurle sauvagement au travail, mais la plupart du temps, il travaille à une puissance beaucoup plus faible - environ 30% et gronde assez silencieusement. A propos de ses modes de fonctionnement ci-dessous, dans la description de la logique de l'inondation. Le plus grand condensateur (C8 dans le diagramme) doit être positionné plus près du circuit d'alimentation de la pompe, sinon il y aura de grandes interférences sur l'ensemble du circuit (en pratique, il s'est avéré que le régulateur de courant pour les LED y est le plus sensible, la musique légère commence).

Horloge temps réel


Il y avait une idée folle d'utiliser les ressources du microcontrôleur à ces fins. Le générateur d'horloge à quartz a une assez bonne précision, dans un autre projet, cette approche a bien fonctionné. Mais le problème est qu'absolument tous les temporisateurs matériels ont déjà été utilisés à d'autres fins. Il n'y avait pas d'autre choix que de trouver le module RTC externe. Louez les Chinois, ils sont là et ils sont bon marché.



Le module basé sur le DS3231 possède une interface I2C, sa propre alimentation redondante - le temps ne se passera pas mal avec une panne de courant. Il y a une sortie en méandre à plusieurs fréquences fixes - 1 kHz, 4 kHz et 8 kHz. C'était très utile pour les signaux audio - encore une fois, vous n'avez pas besoin de charger le MCU, et il n'y avait pas de minuteries gratuites pour cela. L'EEPROM 32Kbit est un bonus, mais il n'est pas utilisé dans ce projet.

Étonnamment, il est très précis - en quelques mois, il a perdu sa force pendant plusieurs secondes. Il a déclaré qu'il tenait compte de l'influence de la température sur la fréquence du générateur, et apparemment cela fonctionne. Si, néanmoins, le temps passe, il existe la possibilité d'une correction de fréquence logicielle. Les lectures du capteur de température sont disponibles et dans ce projet sont affichées dans l'interface.

La classe Rtc est responsable de travailler avec ce module dans le code.

Afficher


J'ai longtemps voulu faire quelque chose avec un écran graphique. La recherche du moins cher avec l'interface I2C a donné cette option.



Affichage OLED monochrome 128x64 pixels basé sur le contrôleur SSD1306 plutôt populaire. Lors du choix, vous devez examiner attentivement la description - la même puce prend en charge d'autres interfaces, à l'exception d'I2C, et il existe des options sans elle. Ou ils écrivent que c'est universel, il prend également en charge I2C, mais en réalité, il sera nécessaire de modifier légèrement la carte en réorganisant les valeurs nulles sur d'autres sites. Par conséquent, si vous prévoyez d'utiliser I2C, il est préférable d'en choisir un où seul I2C est affiché sur la carte, il y aura moins de problèmes avec une carte qui n'a presque pas de documentation (documentation uniquement pour la puce). Cette version fonctionne à partir de 5V, la carte a un régulateur 3,3V requis pour le contrôleur. J'ai rencontré des critiques que dans certaines versions, il se peut que ce ne soit pas le cas.

L'affichage est généralement satisfait. Je n'ai remarqué qu'une seule caractéristique désagréable - la luminosité d'une rangée de pixels dépend du nombre de pixels qui y sont allumés. Plus il est éclairé, plus la luminosité est faible. Le contraste entre les lignes peut être visible si des zones complètement remplies avec des éléments étroits alternent sur l'écran. Mais en pratique, cela n'est pas visible sur mes photos et n'est pas frappant.

Le contrôleur peut être configuré pour fonctionner dans différents modes d'affichage du contenu de la mémoire d'écran sur une matrice de pixels. C'était plus pratique pour moi lorsque chaque octet est mappé sur une colonne verticale de huit pixels de haut, et que les colonnes vont horizontalement de gauche à droite, remplissant l'écran de lignes de huit pixels de haut. Dans ce mode, il est plus pratique de dessiner du texte.

Souvent, une approche est pratiquée dans laquelle la mémoire d'affichage est dupliquée dans la RAM MCU - tout d'abord, toutes les actions avec l'image sont effectuées dans la RAM, puis tous les pixels modifiés sont copiés dans la mémoire d'affichage. Dans ce projet, cette approche n'est pas utilisée pour économiser des ressources. Tous les emplacements modifiés sont immédiatement redessinés dans la mémoire d'affichage.

Comme suggéré dans les commentaires, les écrans OLED s'estompent avec le temps. J'ai également soupçonné cela (en me souvenant de ce qu'est l'économiseur d'écran) et j'ai prévu que l'affichage s'éteigne quelques minutes après la dernière activité sur les commandes. Il s'allume lorsque vous tournez ou appuyez sur l'encodeur.

Dans le code, le travail avec l'affichage est implémenté dans la classe Display.

Valkoder:



À mon avis, le valcoder est la meilleure option de contrôle pour ces appareils qui ont au moins une certaine interface utilisateur. Il est compact et très confortable. Il leur est pratique de parcourir et de sélectionner des éléments de menu, de modifier les valeurs de tout paramètre, de changer de mode, etc.

Pour se connecter, trois pattes d'entrée du microcontrôleur sont nécessaires. Un pour le bouton (vous pouvez appuyer sur la poignée), deux pour le valcoder lui-même. Il y a un signal de code gris de l'encodeur . A chaque tour, un bit change sur deux lignes. La séquence détermine le sens de rotation.

Tout semble simple, mais apparemment, les développeurs ne sont pas toujours en mesure de fournir un support de haute qualité pour un tel appareil. Par exemple, sur mon imprimante 3D, il y a une carte RAMPS et une carte avec un écran et le même encodeur est connecté. Le firmware Marlin fonctionne avec, mais l'expérience de son utilisation est très mauvaise - il n'y a aucun sentiment de fiabilité - lorsque vous cliquez sur le bouton lorsque vous le tournez, l'interface s'arrête souvent au mauvais élément de menu ou à la valeur de paramètre sur laquelle il était attendu. Avec une rotation rapide, on a l'impression que les clics sautent. À un moment donné, la commutation ne commence pas lors des clics, mais quelque part entre les deux, est très désagréable. Oui, qu'est-ce que Marlin, j'ai parfois le même sentiment sur le système multimédia intégré dans la voiture. À cet égard, quelques conseils (et, bien sûr, regardez le code à proximité de la classe RotEnc).

Tout d'abord, un point assez évident pour quiconque connecte des boutons au microcontrôleur - vous devez gérer le rebond. Cet encodeur mécanique et ses lignes de signaux sont, en fait, les mêmes boutons, et ils ont également un bavardage. D'abord, nous filtrons le bavardage, puis nous traitons la séquence d'états des lignes de signal. Il peut y avoir des valcoders avec des capteurs optiques, cela dépend déjà des schémas de traitement du signal d'eux. Si les jambes d'un phototransistor sont directement mises en évidence, il peut y vibrer lentement, mais s'il existe un schéma de traitement introduisant une hystérésis, une suppression logicielle n'est pas nécessaire. Mais ces appareils sont plus chers et sont rarement utilisés dans les appareils amateurs, les plus courants sont mécaniques, quelques dollars par groupe.

Deuxièmement, un point un peu moins évident, probablement l'un de ceux sur lesquels Marlin s'est brûlé - la poignée a des positions stables pendant la rotation - clics (clics). Ce modèle comporte quatre étapes d'une séquence de codes pour chaque clic. Vous devez donc répondre aux clics et non aux étapes de la séquence. Et le plus important est de se synchroniser avec des positions stables. Beaucoup entrent simplement la constante STEPS_PER_CLICK et, par exemple, répondent à chaque quatrième étape. Mais le problème est que le signal n'est pas parfait, les séquences peuvent ne pas être entièrement correctes. Avec une certaine orthographe, le code peut «s'égarer», par conséquent, chaque quatrième étape sera obtenue quelque part au milieu du clic, ce qui sera inconfortable pour l'utilisateur. Dans le même temps, la position fixe de la poignée pour un modèle particulier correspond à une valeur de code fixe,il doit y être attaché.

Troisièmement, encore une fois, un point assez évident pour les développeurs plus ou moins expérimentés de systèmes de microcontrôleurs - utiliser des interruptions matérielles pour changer l'état des lignes d'entrée. Au minimum, il y aura moins de risque de «perdre» les étapes de la séquence. Mais en général, comme vous le savez, les interruptions sont notre tout. Le MCU doit être mis en veille dans la mesure du possible, ne se réveillant que sur des interruptions - soit depuis la périphérie externe, soit à partir d'une minuterie pour effectuer une tâche retardée. Ce sont les principes d'une bonne conception de l'architecture du système.

Le design dans son ensemble


Il est fait de matériaux improvisés et de différentes pièces imprimés sur une imprimante 3D en ABS.

Le principe de fonctionnement du siphon est illustré dans la vidéo ci-dessus. Pour moi, c'est un tube externe en PVC et un tube interne avec un entonnoir à l'extrémité. Pour le siphon classique, un autre genou est nécessaire, mais il était déjà difficile de le fabriquer de manière constructive pour moi. Lorsque des problèmes avec le drain ont été trouvés, un petit bain a été collé sur la paroi du réservoir inférieur, dans lequel l'extrémité du tube intérieur a été immergé, et créant une résistance au drain, afin que le siphon puisse fonctionner.

Il s'est avéré que l'ABS est un matériau très hydrophobe. L'eau ne déborde littéralement pas; j'ai même dû refaire l'entonnoir à siphon. Cela vaut la peine de considérer cette propriété, il est impossible de créer des systèmes hydrauliques miniatures (par exemple, je voulais faire des surfaces de guidage sur l'entonnoir à siphon, pour tordre l'eau, pour améliorer la réponse du siphon. Mais avec de telles dimensions et l'hydrophobicité de l'ABS, cela n'a pas de sens) .

J'ai aussi d'abord essayé de tout coller avec un pistolet à colle chaude. Cela ne fonctionne pas - au début, tout semblait tenir bien, mais après quelques jours, il est tombé de lui-même. La meilleure option est l'Asie centrale. Les détails, même sous l'eau, tiennent bien.

La plus grande erreur de calcul dans la conception est les conteneurs transparents. J'ai complètement oublié le fait que l'eau fleurit dans la lumière. J'ai dû l'envelopper avec un matériau opaque. Eh bien, vous pouvez ajouter périodiquement du permanganate de potassium pour la désinfection, cela ne semble pas nuire aux plantes.

L'algorithme d'inondation est le suivant - d'abord la pompe est allumée à faible puissance et remplit tranquillement le réservoir supérieur entier. Le processus est surveillé par un capteur de niveau. Lorsque l'eau commence à déborder à travers l'entonnoir à siphon, la baisse de niveau dans le réservoir inférieur s'arrête, ce qui est détecté par le capteur. Un petit débit créé à faible puissance n'est pas suffisant pour déclencher un siphon. La pompe s'arrête, le volume pompé dans le réservoir supérieur est mémorisé. Les racines sont maintenues dans la solution pendant plusieurs minutes, après quoi la pompe se rallume. Tout d'abord, à faible puissance, jusqu'à ce que l'eau atteigne à nouveau l'entonnoir (pendant les temps d'arrêt, elle baisse plus bas en raison de l'effet du siphon), et lorsque le niveau de l'entonnoir est atteint, la pompe se met en marche pour augmenter la puissance, fournissant un débit suffisant pour que le siphon fonctionne. Le débit à travers le siphon est garanti supérieur au débit de la pompe,en conséquence, le niveau dans le réservoir inférieur commence à augmenter, cela est détecté par le capteur et la pompe s'arrête.

Les cycles d'inondation commencent périodiquement, à des intervalles de temps fixes et configurables, de l'aube au crépuscule. Selon le plan, l'aube était censée être fixée par le capteur de lumière et la longueur de la lumière du jour, si nécessaire, était étendue à la valeur définie, mais jusque-là les mains ne l'avaient pas atteinte. L'heure de l'aube est simplement définie dans les paramètres.


Et où est C ++ 11?



Peut-être que quelqu'un doutera que C ++ 11 puisse être utile dans la programmation de microcontrôleurs (parmi ceux qui sont généralement conscients que les microcontrôleurs peuvent être programmés en C ++). Je vais essayer de donner des exemples spécifiques des avantages du C ++ 11 dans ce domaine (en plus de petites choses évidentes et agréables comme constexpr, override, default, etc.).

Placement des ressources de chaîne

Beaucoup de gens savent que la RAM dans les microcontrôleurs est une ressource très limitée. Cela peut être un problème si votre application, par exemple, possède une interface utilisateur et que votre programme utilise un nombre assez important de lignes. Si dans le code pour écrire quelque chose comme
PromptUser("Are you sure you want to format SD-card?");

puis la ligne passée dans les arguments sera placée dans la section des données initialisées (ci-après, le comportement du compilateur GCC pour la plate-forme AVR) - c'est-à-dire, dans la zone RAM, qui au démarrage (avant l'appel de la fonction principale) est initialisée à partir de la mémoire flash du programme. La fonction PromptUser () recevra un pointeur vers l'emplacement souhaité dans la RAM. Si vous utilisez une approche similaire tout au long du programme, la RAM se terminera assez rapidement (dans l'ATMEGA328P utilisé dans ce projet, il ne fait que 2 kilo-octets, et cela vaut également pour BSS, tas et pile). Pour contourner cette limitation, des fonctions comme PromptUser () apprennent à fonctionner non pas avec des pointeurs vers la RAM, mais avec des pointeurs vers une région dans la mémoire flash du programme. Vous ne pouvez y lire qu'à l'aide d'instructions spéciales, qui, par exemple, dans avr-libc sont enveloppées dans des fonctions de la famille eeprom_read_ [byte | word | dword | ...].
Dans ce cas, la chaîne doit d'abord être placée dans une variable équipée de l'attribut PROGMEM, qui indique au compilateur qu'elle doit être placée dans la mémoire du programme .
char prompt[] PROGMEM = "Are you sure you want to format SD-card?";
PromptUser(prompt);

Cela n'est pas pratique si vous souhaitez déclarer toutes les lignes de manière centrale. Ensuite, vous devez d'abord déclarer leur déclaration dans le fichier d'en-tête:
extern char prompt[] PROGMEM;

Et dans un fichier .cpp distinct, définissez:
char prompt[] PROGMEM = "Are you sure you want to format SD-card?";

Duplication de code, ce qui n'est pas bon, et très gênant quand il y a beaucoup de telles lignes. Oui, cela peut être contourné en créant une macro délicate et en incluant le fichier d'en-tête dans un fichier .cpp distinct, dans lequel la macro sera développée dans la définition, tandis que dans d'autres contextes, elle sera développée dans la déclaration. Mais avec C ++ 11, il existe une option plus propre si vous utilisez l'initialisation des membres de classe lors de la déclaration. Dans le fichier d'en-tête, déclarez la classe avec les lignes:
#define DEF_STR(__name, __text) \
    const char __name[sizeof(__text)] = __text;

class Strings {
public:
    DEF_STR(Prompt, "Are you sure you want to format SD-card?")
    DEF_STR(OtherString, "...")
} __attribute__((packed));

extern const Strings strings PROGMEM;

Dans le fichier .cpp:
const Strings strings PROGMEM;

Maintenant, toutes les lignes sont déclarées en un seul endroit, placées dans la mémoire du programme, et vous pouvez y accéder comme ceci:
PromptUser(strings.prompt);

Dans ce projet, une approche basée sur le même principe est utilisée pour déterminer les bitmaps - différentes images affichées sur un écran graphique.
/** Bitmap descriptor. */
struct Bitmap {
    /** Pointer to data array if in data memory. Offset of data array relatively
     * to Bitmaps class instance start address if in program memory.
     */
    const u8 *data;
    /** Number of pages in the bitmap. */
    u8 numPages,
    /** Number of columns in the bitmap. */
       numColumns;
} __PACKED;

template<u8... data>
constexpr static u8
Bitmap_NumDataBytes()
{
    return sizeof...(data);
}

/** Define bitmap.
 * @param __name Name for accessing.
 * @param __numPages Number of pages in the bitmap. Number of columns defined as
 *      total number of data bytes divided by number of pages.
 * @param __VA_ARGS__ Data bytes.
 */
#define DEF_BITMAP(__name, __numPages, ...) \
    const u8 __CONCAT(__name, __data__) \
        [Bitmap_NumDataBytes<__VA_ARGS__>()] = { __VA_ARGS__ }; \
    const Bitmap __name { \
        reinterpret_cast<const u8 *>(OFFSETOF(Bitmaps, __CONCAT(__name, __data__))), \
        __numPages, \
        sizeof(__CONCAT(__name, __data__)) / __numPages};

/** Global bitmaps repository. Stored in program memory. */
class Bitmaps {
public:

    /** Thermometer icon. */
    DEF_BITMAP(Thermometer, 1,
        0b01101010,
        0b10011110,
        0b10000001,
        0b10011110,
        0b01101010
    )

    /** Sun icon. */
    DEF_BITMAP(Sun, 1,
        0b00100100,
        0b00011000,
        0b10100101,
        0b01000010,
        0b01000010,
        0b10100101,
        0b00011000,
        0b00100100
    )
    ...
};
extern const Bitmaps bitmaps PROGMEM;

La différence est qu'en plus des données d'image elles-mêmes, il est également nécessaire de placer des attributs (tailles d'image). Chaque octet définit une colonne de huit pixels. Les colonnes peuvent remplir une ou plusieurs lignes; leur nombre est indiqué par le deuxième paramètre après le nom. Il s'avère que la hauteur des bitmaps doit être un multiple de huit pour une largeur arbitraire, ce qui est tout à fait acceptable pour ce projet.

Littéraux binaires

Vous avez peut-être déjà remarqué que les bitmaps de l'exemple précédent utilisent des littéraux binaires pour déterminer. C'est vraiment très pratique - vous pouvez éditer des bitmaps simples directement dans le code, surtout si l'éditeur permet de les mettre en évidence. Par exemple, les définitions des caractères de police dans le fichier font.h:



Modèles variadic

Où alors sans eux alors. Eh bien, par exemple, les commandes du contrôleur d'affichage peuvent avoir une longueur d'un à plusieurs octets. Envoyé avec le code suivant:

SendCommand(Command::DISPLAY_ON);
SendCommand(Command::SET_COM_PINS, COM_PINS | COM_PINS_ALTERNATIVE);
SendCommand(Command::SET_COLUMN_ADDRESS, curVp.minCol, curVp.maxCol);

Pratique, non?
    /** Queue command sending.
     * @param bytes Up to MAX_CMD_SIZE bytes of command data.
     */
    template <typename... TByte>
    void
    SendCommand(TByte... bytes)
    {
        cmdSize = sizeof...(bytes);
        controlSent = false;
        cmdInProgress = true;
        SetCmdByte(sizeof...(bytes) - 1, bytes...);
        i2cBus.RequestTransfer(DISPLAY_ADDRESS, true,
                               CommandTransferHandler);
    }

    template <typename... TByte>
    inline void
    SetCmdByte(int idx, u8 byte, TByte... bytes)
    {
        cmdBuf[idx] = byte;
        SetCmdByte(idx - 1, bytes...);
    }

    inline void
    SetCmdByte(int, u8 byte)
    {
        cmdBuf[0] = byte;
    }


Le fichier variant.h décrit une classe qui ressemble vaguement à boost :: variant à l'aide de modèles variadic. Il est utilisé pour organiser les pages de l'interface utilisateur. Il s'agit encore une fois d'économiser de la mémoire - où la gestion dynamique de la mémoire est un luxe inacceptable, vous devez esquiver (bien que 2K soit encore beaucoup, vous ne pouvez pas esquiver, mais dans la même ligne ATMEGA, sa taille atteint 512 octets, et chaque octet par Compte). Dans mon interface, une page est affichée à l'écran à tout moment. Par conséquent, pour toutes les pages, vous pouvez utiliser le même morceau de mémoire, ce qu'on appelle l'union en C. Pour les classes en C ++, cela est généralement appelé variante. Contrairement à l'union, nous devons nous rappeler d'appeler le destructeur du contenu précédent avant d'appeler le constructeur du nouveau.

    Variant<MainPage,
            Menu,
            LinearValueSelector,
            TimeSelector> curPage;
    ...
    /** Get type code for the specified page class. */
    template <class TPage>
    static constexpr u8
    GetPageTypeCode()
    {
        return decltype(curPage)::GetTypeCode<TPage>();
    }
...
curPage.Engage(nextPageTypeCode, page);


Pour la compilation, les binutils GCC et GNU sont utilisés pour la plate-forme AVR (dans Ubuntu il y a un paquet prêt à l'emploi gcc-avr). Les détails du processus d'assemblage ont été donnés ci-dessus. Les paramètres du compilateur ressemblent à ceci (les financements et les inclusions spécifiques au projet sont omis): Lien: Convertissez la section de code au format hexadécimal: Créez une image EEPROM: Micrologiciel pour le microcontrôleur: PS Les premières tomates étaient déjà mûres et n'avaient pas très bon goût. Apparemment, ils n'aimaient pas quelque chose dans l'alimentation. Il faudra probablement changer de culture.
avr-g++ -o build/native-debug/src/firmware/cpu/lighting.cpp.o -c -fno-exceptions -fno-rtti -std=c++1y -Wall -Werror -Wextra -ggdb3 -Os -mcall-prologues -mmcu=atmega328p -fshort-wchar -fshort-enums src/firmware/cpu/lighting.cpp


avr-g++ -o build/native-debug/src/firmware/cpu/cpu -mmcu=atmega328p build/native-debug/src/firmware/cpu/adc.cpp.o build/native-debug/src/firmware/cpu/application.cpp.o …


avr-objcopy -j .text -j .data -O ihex build/native-debug/src/firmware/cpu/cpu build/native-debug/src/firmware/cpu/cpu_rom.hex


avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex build/native-debug/src/firmware/cpu/cpu build/native-debug/src/firmware/cpu/cpu_eeprom.hex


avrdude -p atmega328p -c avrisp2 -P /dev/avrisp -U flash:w:build/native-debug/src/firmware/cpu/cpu_rom.hex:i


Source: https://habr.com/ru/post/fr385135/


All Articles