PIC32 vs dsPIC vs ARM vs AVR, l'architecture importe-t-elle quand nous programmons en langage C de toute façon? [fermé]


10

Nous utilisons actuellement un microcontrôleur PIC32 32 bits. Cela fonctionne bien pour nos besoins, mais nous explorons également d'autres microcontrôleurs qui peuvent mieux nous adapter + nous avons d'autres projets pour lesquels nous sélectionnons MCU. À cet effet, nous avons sélectionné le microcontrôleur SAM DA basé sur ARM, qui est le même 32 bits mais est basé sur ARM (plus populaire que PIC32 - en termes d'industrie).

Maintenant, pour PIC32, nous utilisons MPLAB mais pour ARM cortex-M0, nous utiliserons Atmel Studio. Nous utiliserons le langage C sur les deux plates-formes. Ce qui me préoccupe, c'est que nous utiliserons deux microcontrôleurs 32 bits (de la même entreprise) mais avec des architectures différentes. Cela nous obligera à apprendre deux appareils différents et augmentera notre "courbe d'apprentissage" + le délai de livraison. Mais d'un autre côté, je pense également que puisque nous utiliserons le langage C dans les deux cas, la courbe d'apprentissage pour ARM ne devrait pas être aussi entendue et cela vaut la peine d'explorer ce processeur également.

Ma question principale est de savoir quelle différence l'architecture fait lorsque nous programmons en langage C car elle fournit une abstraction des internes du microcontrôleur. Et quelles sont les principales différences entre MPLAP et Atmel Studio , compte tenu de la programmation en langage C.


2
Si les choses fonctionnent avec le PIC32, alors à quoi bon changer? Même si le code est complètement porté (ce ne sera pas le cas), il y a toujours la nouvelle chaîne d'outils et l'IDE à utiliser. À quoi ça sert? Changer pour des raisons religieuses ou pour être "basé sur ARM" (ou quoi que ce soit d'autre) est stupide. Vous devez avoir une bonne raison, mais vous ne nous en avez montré aucune.
Olin Lathrop

Je n'ai pas demandé de changement. J'ai parlé de choisir une architecture différente pour d'autres projets car nous travaillons sur plusieurs projets + il y a place à amélioration dans notre conception existante. Le point principal portait sur la courbe d'apprentissage et les défis liés au travail avec deux architectures différentes en même temps.
ingénieur

Une chose que j'ai trouvée est que Atmel Studio offre un timing supérieur à la vidéo YouTube
ingénieur

Réponses:


20

C'est tout à fait un sujet d'opinion. Je peux parler pour moi (AVR, ARM, MSP430).

La différence 1 (la plus significative) concerne les périphériques. Chacun des MCU a des UART, SPI, temporisateurs, etc. similaires - les noms de registre et les bits sont différents. La plupart du temps, c'était le principal problème avec lequel je devais faire face lors du déplacement de code entre puces. Solution: écrivez vos pilotes avec une API commune, afin que votre application puisse être portable.

La différence 2 est l'architecture de la mémoire. Si vous souhaitez placer des constantes en flash sur un AVR, vous devez utiliser des attributs spéciaux et des fonctions spéciales pour les lire. Dans le monde ARM, vous déréférencez simplement un pointeur car il n'y a qu'un seul espace d'adressage (je ne sais pas comment les petits PIC le gèrent, mais supposeraient qu'ils sont plus proches d'AVR).

La différence 3 est la déclaration et la gestion des interruptions. avr-gcca la ISR()macro. ARM a juste un nom de fonction (comme someUART_Handler () - si vous utilisez des en-têtes CMSIS et du code de démarrage). Les vecteurs d'interruption ARM peuvent être placés n'importe où (y compris la RAM) et modifiés au moment de l'exécution (très pratique si vous avez par exemple deux protocoles UART différents qui peuvent être commutés). AVR n'a que la possibilité d'utiliser des vecteurs soit dans "flash principal" soit dans la "section du chargeur de démarrage" (donc si vous voulez gérer les interruptions différemment, vous devez utiliser une ifinstruction).

Différence 4 - modes de veille et contrôle de l'alimentation. Si vous avez besoin de la plus faible consommation d'énergie, vous devez niveler toutes les fonctionnalités du MCU. Cela peut différer considérablement entre les MCU - certains ont des modes d'économie d'énergie plus grossiers, certains peuvent activer / désactiver des périphériques individuels. Certains MCU ont des régulateurs réglables afin que vous puissiez les faire fonctionner avec une tension inférieure à une vitesse plus lente, etc. Je ne vois pas de moyen facile d'obtenir la même efficacité sur un MCU (disons) avec 3 modes de puissance globale et un autre avec 7 modes de puissance contrôle individuel de l'horloge périphérique.

La chose la plus importante lorsque vous vous souciez de la portabilité est de diviser clairement votre code en parties dépendantes du matériel (pilotes) et indépendantes du matériel (application). Vous pouvez développer et tester ce dernier sur un PC ordinaire avec un faux pilote (par exemple une console au lieu d'un UART). Cela m'a sauvé plusieurs fois car 90% du code d'application était terminé avant que le matériel prototype ne sorte du four de refusion :)

À mon avis, la bonne chose à propos d'ARM est la "monoculture" - la disponibilité de nombreux compilateurs (gcc, Keil, IAR ... pour n'en nommer que quelques-uns), de nombreux IDE gratuits et officiellement pris en charge (au moins pour NXP, STM32, Silicon Labs, Nordic), de nombreux outils de débogage (SEGGER - en particulier Ozone, ULINK, OpenOCD ...) et de nombreux fournisseurs de puces (je ne vais même pas commencer à les nommer). Le PIC32 est principalement limité à Microchip (mais cela n'a d'importance que si vous n'aimez pas leurs outils.

En ce qui concerne le code C. C'est 99% pareil, une ifdéclaration est la même, une boucle fonctionne de la même manière. Cependant, vous devez vous soucier de la taille du mot natif. Par exemple, une forboucle sur un AVR est la plus rapide si vous utilisez uint8_tpour le compteur, tandis que sur ARM uint32_test le type le plus rapide (ou int32_t). ARM devra vérifier chaque fois le débordement de 8 bits si vous utilisez un type plus petit.

La sélection d'un MCU et / ou d'un fournisseur en général concerne principalement la politique et la logistique (sauf si vous avez des contraintes d'ingénierie très claires, par exemple: haute température - utilisez MSP430 ou Vorago). Même si l'application peut fonctionner sur n'importe quoi et que seulement 5% du code (pilotes) doit être développé et pris en charge pendant la durée de vie du produit - c'est toujours un coût supplémentaire pour l'entreprise. Tous les endroits où j'ai travaillé avaient un fournisseur préféré et une gamme de microcontrôleurs (comme "choisissez les Kinetis que vous voulez, sauf s'il y a une très bonne raison de choisir quelque chose de différent"). Cela aide également si vous avez d'autres personnes pour demander de l'aide, donc en tant que responsable, j'éviterais d'avoir un service de développement de 5 personnes où tout le monde utilise une puce totalement différente.


3
«L'AVR est le plus rapide si vous utilisez uint8_t pour le compteur, tandis que sur ARM uint32_t est le type le plus rapide (ou int32_t). ARM devrait vérifier chaque fois un débordement de 8 bits si vous utilisez un type plus petit. » vous pouvez utiliser uint_fast8_t si vous n'avez besoin que d'au moins 8 bits.
Michael

@Michael - bien sûr, vous pouvez utiliser les types _fast, mais vous ne pouvez pas compter sur le comportement de débordement. Dans stdint.h de mon gcc, j'ai "typedef unsigned int uint_fast8_t", qui est fondamentalement un uint32_t :)
filo

Essayer d'écrire une API qui est efficace, universelle et complète est difficile étant donné que les différentes plates-formes ont des capacités différentes. Le CPU importe probablement moins que les périphériques et les décisions de conception prises avec eux. Par exemple, certains appareils ont permis à divers périphériques d'être reconfigurés à tout moment en quelques microsecondes au maximum, tandis que d'autres peuvent nécessiter plusieurs étapes réparties sur des centaines de microsecondes, voire des millisecondes. Une fonction API qui est destinée à l'ancien modèle peut être utilisable dans une routine de service d'interruption qui s'exécute à 10 000 Hz, mais ...
supercat

... ne pouvait pas prendre en charge une telle utilisation sur des plates-formes qui nécessiteraient d'étaler les opérations sur des centaines de microsecondes. Je ne sais pas pourquoi les concepteurs de matériel ne semblent pas s’efforcer de prendre en charge la sémantique de l’API «opération rapide à tout moment», mais beaucoup utilisent un modèle qui synchronise les opérations individuelles plutôt que d’indiquer de sorte que si, par exemple, une demande a été donnée allumez un appareil et le code se rend compte qu'il n'a pas besoin d'être allumé, le code doit attendre que l'appareil s'allume avant de pouvoir émettre la demande de désactivation. La manipulation sans heurts dans une API ajoute des complications majeures.
supercat

11

J'ai utilisé plusieurs MCU de quatre fabricants différents. Le travail principal à chaque fois est de se familiariser avec les périphériques.

Par exemple, un UART lui-même n'est pas trop complexe et je trouve facilement le port de mes pilotes. Mais la dernière fois, il m'a fallu presque un jour pour régler les horloges, les broches d'E / S, les activer, etc.

Le GPIO peut être très complexe. Bit-set, bit-clear, bit-toggle, Activation / désactivation des fonctions spéciales, trois états. Ensuite, vous obtenez des interruptions: n'importe quel bord, montée, chute, niveau bas, niveau haut, auto-compensation ou non.

Ensuite, il y a I2C, SPI, PWM, Timers et deux douzaines de types de périphériques supplémentaires chacun avec leur propre horloge active et chaque fois que les registres sont différents avec de nouveaux bits. Pour tous ceux-ci, il faut de nombreuses heures à lire la fiche technique pour définir quel bit dans quelles circonstances.

Le dernier fabricant avait beaucoup d'exemples de code que j'ai trouvé inutilisables. Tout était abstrait. Mais quand je l'ai retrouvé, le code est passé par six! niveaux d'appels de fonction pour définir un bit GPIO. Bien si vous avez un processeur 3GHz mais pas sur un MCU de 48MHz. Mon code à la fin était une seule ligne:

GPIO->set_output = bit.

J'ai essayé d'utiliser des pilotes plus génériques mais j'ai abandonné. Sur un MCU, vous êtes toujours aux prises avec des cycles d'espace et d'horloge. J'ai trouvé que la couche d'abstraction est la première à sortir de la fenêtre si vous générez une forme d'onde spécifique dans une routine d'interruption appelée à 10 KHz.

Alors maintenant, tout fonctionne et je prévois de ne PAS changer à nouveau, sauf pour une très, très bonne raison.

Tout ce qui précède doit être amorti sur le nombre de produits que vous vendez et sur ce que vous économisez. Vendre un million: économiser 0,10 pour passer à un autre type signifie que vous pouvez dépenser 100 000 heures logicielles. En vendant 1000, vous n'en avez que 100 à dépenser.


1
Personnellement c'est pourquoi je reste fidèle à l'assembleur. Beau binaire, pas d'abstraction.
Ian Bland

Le préprocesseur de C peut très bien faire avec les choses, surtout lorsqu'il est combiné avec les intrinsèques __builtin_constant. Si l'on définit des constantes pour chaque bit d'E / S du formulaire (numéro de port * 32 + numéro de bit), il est possible d'écrire une macro pour OUTPUT_HI(n)laquelle donnera un code équivalent à GPIOD->bssr |= 0x400;if nest une constante comme 0x6A, mais appelez un sous-programme simple if nis pas constant. Cela dit, la plupart des API des fournisseurs que j'ai vues varient entre médiocres et horribles.
supercat

8

Il s'agit plus d'une opinion / d'un commentaire que d'une réponse.

Vous ne voulez pas et ne devriez pas programmer en C. C ++, lorsqu'il est utilisé de la bonne façon , est de loin supérieur. (OK, je dois admettre que lorsqu'il est utilisé de manière incorrecte, il est bien pire que C.) Cela vous limite aux puces qui ont un compilateur C ++ (moderne), qui est à peu près tout ce qui est pris en charge par GCC, y compris AVR (avec certaines limitations, filo mentionne les problèmes d'un espace d'adressage non uniforme), mais en excluant presque tous les PIC (PIC32 pourrait être pris en charge, mais je n'ai pas encore vu de port décent).

Lorsque vous programmez des algorithmes en C / C ++, la différence entre les choix que vous mentionnez est faible (sauf qu'une puce de 8 ou 16 bits sera fortement désavantagée lorsque vous effectuez beaucoup d'arithmétique de 16, 32 bits ou plus). Lorsque vous avez besoin de la dernière once de performances, vous devrez probablement utiliser l'assembleur (le vôtre ou le code fourni par le fournisseur ou un tiers). Dans ce cas, vous voudrez peut-être reconsidérer la puce que vous avez sélectionnée.

Lorsque vous codez pour le matériel, vous pouvez utiliser une couche d'abstraction (souvent fournie par le fabricant) ou écrire la vôtre (sur la base de la fiche technique et / ou d'un exemple de code). Les abstractions C existantes d'IME (mbed, cmsis, ...) sont souvent fonctionnellement (presque) correctes, mais échouent horriblement en termes de performances (vérifiez les oldfarts rant environ 6 couches d'indirection pour une opération de jeu de broches), l'utilisabilité et la portabilité. Ils veulent vous exposer toutes les fonctionnalités de la puce particulière, dont dans presque tous les cas vous n'aurez pas besoin et ne vous souciez pas, et cela verrouille votre code à ce fournisseur particulier (et probablement à cette puce particulière).

C'est là que le C ++ peut faire beaucoup mieux: lorsqu'il est fait correctement, un ensemble de broches peut traverser 6 couches d'abstraction ou plus (car cela rend une meilleure interface (portable!) Et un code plus court possible), tout en fournissant une interface indépendante de la cible pour les cas simples , et donnent toujours le même code machine que vous écririez dans l'assembleur .

Un extrait du style de codage que j'utilise, qui peut vous rendre enthousiaste ou vous détourner d'horreur:

// GPIO part of a HAL for atsam3xa
enum class _port { a = 0x400E0E00U, . . . };

template< _port P, uint32_t pin >
struct _pin_in_out_base : _pin_in_out_root {

   static void direction_set_direct( pin_direction d ){
      ( ( d == pin_direction::input )
         ? ((Pio*)P)->PIO_ODR : ((Pio*)P)->PIO_OER )  = ( 0x1U << pin );
   }

   static void set_direct( bool v ){
      ( v ? ((Pio*)P)->PIO_SODR : ((Pio*)P)->PIO_CODR )  = ( 0x1U << pin );    
   }
};

// a general GPIO needs some boilerplate functionality
template< _port P, uint32_t pin >
using _pin_in_out = _box_creator< _pin_in_out_base< P, pin > >;

// an Arduino Due has an on-board led, and (suppose) it is active low
using _led = _pin_in_out< _port::b, 27 >;
using led  = invert< pin_out< _led > >;

En réalité, il existe d'autres couches d'abstraction. Pourtant, l'utilisation finale de la led, disons pour l'allumer, ne montre pas la complexité ou les détails de la cible (pour une arduin uno ou une pilule bleue ST32 le code serait identique).

target::led::init();
target::led::set( 1 );

Le compilateur n'est pas intimidé par toutes ces couches, et comme aucune fonction virtuelle n'est impliquée, l'optimiseur voit tout (certains détails, omis, comme l'activation de l'horloge périphérique):

 mov.w  r2, #134217728  ; 0x8000000
 ldr    r3, [pc, #24]   
 str    r2, [r3, #16]
 str    r2, [r3, #48]   

C'est ainsi que je l'aurais écrit en assembleur - SI j'avais réalisé que les registres PIO peuvent être utilisés avec des décalages à partir d'une base commune. Dans ce cas, je le ferais probablement, mais le compilateur est bien meilleur pour optimiser de telles choses que moi.

Donc, pour autant que j'ai une réponse, c'est: écrivez une couche d'abstraction pour votre matériel, mais faites-le en C ++ moderne (concepts, modèles) afin que cela ne nuise pas à vos performances. Avec cela en place, vous pouvez facilement passer à une autre puce. Vous pouvez même commencer à développer sur une puce aléatoire que vous avez en place, que vous connaissez, avez de bons outils de débogage, etc. et reporter le choix final à plus tard (lorsque vous aurez plus d'informations sur la mémoire requise, la vitesse du processeur, etc.).

IMO, l'une des failles du développement intégré est de choisir d'abord la puce (c'est une question souvent posée sur ce forum: pour quelle puce dois-je choisir ... La meilleure réponse est généralement: cela n'a pas d'importance.)

(edit - réponse à "Donc, en termes de performances, C ou C ++ serait au même niveau?")

Pour les mêmes constructions, C et C ++ sont identiques. C ++ a beaucoup plus de constructions pour l'abstraction (juste quelques-unes: classes, modèles, constexpr) qui peuvent, comme tout outil, être utilisées pour le bien ou pour le mal. Pour rendre les discussions plus intéressantes: tout le monde n'est pas d'accord sur ce qui est bon ou mauvais ...


Donc, en termes de performances, C ou C ++ serait au même niveau? Je pense que C ++ aura plus de surcharge. Certainement vous m'avez pointé dans la bonne direction, C ++ est la voie à suivre pas C.
ingénieur

Les modèles C ++ forcent le polymorphisme au moment de la compilation qui peut être nul (ou même négatif) en termes de performances, car le code est compilé pour chaque cas d'utilisation spécifique. Cependant, cela tend à se prêter mieux à la vitesse de ciblage (O3 pour GCC). Le polymorphisme au moment de l'exécution, comme les fonctions virtuelles, peut subir une pénalité beaucoup plus importante, bien qu'il soit sans doute plus facile à entretenir et dans certains cas suffisamment bon.
Hans

1
Vous prétendez que C ++ est meilleur, mais vous allez ensuite utiliser des transtypages de style C. Par honte.
JAB

@JAB Je n'ai jamais ressenti grand-chose pour les modèles de style nouveau, mais je vais les essayer. Mais ma priorité actuelle est sur d'autres parties de cette bibliothèque. Le vrai problème est bien sûr que je ne pouvais pas passer les pointeurs comme paramètres de modèle.
Wouter van Ooijen

@Hans mon style cto (Compile Time Objects) a un cas d'utilisation plutôt étroit (proche du matériel, situation connue à la compilation), il s'agit plus d'un C-killer que d'un remplacement pour les utilisations traditionnelles des OO basées sur le virtuel. Une prise accessoire utile est que l'absence d'indirection permet de calculer la taille de la pile.
Wouter van Ooijen

4

Si je comprends bien, vous voulez savoir quelles fonctionnalités spécifiques à l'architecture de la plate-forme "apparaissent" dans votre environnement de langage C, ce qui rend plus difficile l'écriture de code portable maintenable sur les deux plates-formes.

C est déjà assez flexible dans la mesure où il s'agit d'un "assembleur portable". Toutes les plateformes que vous avez sélectionnées ont des compilateurs GCC / commerciaux disponibles qui prennent en charge les normes de langage C89 et C99, ce qui signifie que vous pouvez exécuter un code similaire sur toutes les plateformes.

Il y a quelques considérations:

  • Certaines architectures sont Von Neumann (ARM, MIPS), d'autres sont Harvard. Les principales limitations surviennent lorsque votre programme C doit lire des données à partir de la ROM, par exemple pour imprimer des chaînes, avoir des données définies comme "const" ou similaire.

Certaines plates-formes / compilateurs peuvent mieux masquer cette "limitation" que d'autres. Par exemple, sur AVR, vous devez utiliser des macros spécifiques pour lire les données ROM. Sur PIC24 / dsPIC, il existe également des instuctions tblrd dédiées. Cependant, certaines parties disposent également de la fonctionnalité "visibilité de l'espace programme" (PSVPAG) qui permet de mapper une page du FLASH dans la RAM, rendant l'adressage immédiat des données disponible sans tblrd. Le compilateur peut le faire assez efficacement.

ARM et MIPS sont Von Neumann, ont ainsi des régions de mémoire pour ROM, RAM et périphériques regroupés sur 1 bus. Vous ne remarquerez aucune différence entre la lecture des données de la RAM ou de la "ROM".

  • Si vous plongez sous C et regardez les instructions générées pour certaines opérations, vous trouverez de grandes différences autour des E / S. ARM et MIPS sont une architecture de registre de magasin de charge RISC . Cela signifie que l'accès aux données sur le bus mémoire doit passer par des instructions MOV. Cela signifie également que toute modification d'une valeur périphérique entraînera une opération de lecture-modification-écriture (RMW). Certaines parties ARM prennent en charge la bande de bits, qui mappent les registres set / clr-bit dans l'espace périphérique d'E / S. Cependant, vous devez coder cet accès vous-même.

D'un autre côté, un PIC24 permet aux opérations ALU de lire et d'écrire des données directement via l'adressage indirect (même avec des modifications de pointeur ..). Cela a certaines caractéristiques d'une architecture de type CISC, donc 1 instruction peut faire plus de travail. Cette conception peut entraîner des cœurs de processeur plus complexes, des horloges plus faibles, une consommation d'énergie plus élevée, etc. Heureusement pour vous, la pièce est déjà conçue. ;-)

Ces différences peuvent signifier qu'un PIC24 peut être "plus percutant" par rapport aux opérations d'E / S qu'une puce ARM ou MIPS à horloge similaire. Cependant, vous pouvez obtenir une pièce ARM / MIPS d'horloge beaucoup plus élevée pour les mêmes contraintes de prix / emballage / conception. Je suppose que pour des termes pratiques, je pense que beaucoup d '"apprentissage de la plate-forme" consiste à comprendre ce que l'architecture peut et ne peut pas faire, la rapidité de quelques opérations, etc.

  • Les périphériques, la gestion de l'horloge, etc. diffèrent par famille de pièces. À strictement parler, cela changera également au sein de l'écosystème ARM entre les fournisseurs, à l'exception de quelques périphériques liés à Cortex m comme NVIC et SysTick.

Ces différences peuvent être quelque peu encapsulées par les pilotes de périphérique, mais au final, le micrologiciel intégré a un niveau élevé de couplage avec le matériel, de sorte que le travail personnalisé ne peut parfois pas être évité.

De plus, si vous quittez les écosystèmes de Microchip / ancien Atmel, vous constaterez peut-être que les pièces ARM nécessitent plus de configuration pour les faire fonctionner. Je veux dire en termes de; activer les horloges sur les périphériques, puis configurer les périphériques et les "activer", configurer NVIC séparément, etc. Ceci n'est qu'une partie de la courbe d'apprentissage. Une fois que vous vous souvenez de faire toutes ces choses, dans le bon ordre, l'écriture des pilotes de périphérique pour tous ces microcontrôleurs se sentira assez similaire à un moment donné.

  • Essayez également d'utiliser des bibliothèques comme stdint.h, stdbool.h, etc. si vous ne l'êtes pas déjà. Ces types entiers rendent les largeurs explicites, ce qui rend le comportement du code le plus prévisible entre les plates-formes. Cela peut signifier l'utilisation d'entiers 32 bits sur un AVR 8 bits; mais si votre code en a besoin, qu'il en soit ainsi.

3

Oui et non. Du point de vue des programmeurs, vous cachez idéalement les détails du jeu d'instructions. Mais cela n'est pas dans une certaine mesure déjà pertinent les périphériques qui est tout l'intérêt d'écrire le programme ne font pas partie du jeu d'instructions. Maintenant, en même temps, vous ne pouvez pas comparer les pièces flash de 4096 octets à travers ces jeux d'instructions, en particulier si vous utilisez C, la quantité de consommation du flash / de la mémoire est fortement déterminée par le jeu d'instructions et le compilateur, certains ne devraient jamais voir un compilateur (toux PIC toux) en raison de la quantité de gaspillage de ces ressources consommée par la compilation. Pour d'autres, la consommation de flash est une surcharge. Les performances sont également un problème lors de l'utilisation d'un langage de haut niveau et les questions de performances dans les applications MCU, elles peuvent donc faire la différence entre dépenser 3 $ par carte pour le mcu ou 1 $.

S'il s'agit de faciliter la programmation (au coût global du produit), vous devriez pouvoir télécharger un package de développement pour le mcu de telle sorte que l'architecture du jeu d'instructions soit quelque chose que vous ne voyez jamais, donc si c'est votre principale préoccupation, il n'est pas une préoccupation. L'utilisation de ces bibliothèques vous coûte toujours de l'argent en ce qui concerne le coût du produit, mais le temps de mise sur le marché peut être plus court, je trouve que les bibliothèques prennent plus de temps / travail à utiliser que de parler directement aux périphériques.

En conclusion, les ensembles d'instructions sont le moindre de vos soucis, passez à de vrais problèmes.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.