Carte de débogage STM32F4 dans le facteur de forme Raspberry Pi

image Bonjour, cher Khabrovites! Je veux présenter mon projet au public - une petite carte de débogage basée sur STM32, mais dans le facteur de forme Raspberry Pi. Il diffère des autres cartes de débogage en ce qu'il a une géométrie compatible avec les boîtiers Raspberry Pi et un module ESP8266 en tant que modem sans fil. Et aussi de jolis ajouts sous la forme d'un connecteur pour une carte micro-SD et un amplificateur stéréo. Pour profiter de toute cette richesse, j'ai développé une bibliothèque de haut niveau et un programme de démonstration (en C ++ 11). Dans l'article, je veux décrire en détail les parties matérielle et logicielle de ce projet.


Qui peut bénéficier de ce projet? Probablement, seulement à ceux qui veulent souder moi-même cette carte, car je n'envisage aucune option, même pour une production à petite échelle. C'est un pur hobby. À mon avis, la carte couvre un éventail assez large de tâches qui peuvent survenir dans le cadre de petits objets artisanaux utilisant le WiFi et le son.


Pour commencer, je vais essayer de répondre à la question de savoir pourquoi c'est tout. Les principaux facteurs de motivation de ce projet sont les suivants:


  • Le choix de la plate-forme STM32 est dû à des considérations purement esthétiques - j'aime le rapport prix / performances, plus une large gamme de périphériques, ainsi qu'un écosystème de développement large et pratique du fabricant du contrôleur (sw4stm, cubeMX, bibliothèque HAL).
  • Bien sûr, il existe de nombreuses cartes de débogage du fabricant du contrôleur lui-même (Discovery, Nucleo), ainsi que de fabricants tiers (par exemple, Olimex). Mais répéter beaucoup d'entre eux à la maison dans leur facteur de forme est problématique pour moi, au moins. Dans ma version, nous avons une topologie simple à deux couches et des composants pratiques pour le soudage manuel.
  • Pour leurs appareils, je veux avoir des boîtiers décents afin de masquer la faible qualité de l'électronique à l'intérieur. Il existe au moins deux plates-formes populaires pour lesquelles il existe un grand nombre de cas les plus divers: Arduino et Raspberry Pi. La seconde d'entre elles m'a paru plus pratique en termes de localisation des découpes pour les connecteurs. Par conséquent, en tant que donateur pour la géométrie de la planche, je l'ai choisi.
  • Le contrôleur que j'ai sélectionné à bord a un réseau USB, SDIO, I2S. D'autre part, ces mêmes interfaces sont également utiles pour une plate-forme de loisirs à domicile. C'est pourquoi, en plus du contrôleur avec un harnais standard, j'ai ajouté un connecteur USB, une carte SD, un chemin audio (convertisseur numérique-analogique et amplificateur), ainsi qu'un module sans fil basé sur l'ESP8266.

Circuit et composants


Il me semble qu'une assez belle planche avec les caractéristiques et composants suivants s'est avérée:


  • Contrôleur STM32F405RG : ARM Cortex-M4 32 bits avec un coprocesseur mathématique, fréquence jusqu'à 168 MHz, 1 Mo de mémoire flash, 196 Ko de RAM.
    Broches de contrôleur utilisées
    Liaison du contrôleur
  • Connecteur SWD pour la programmation du contrôleur (6 broches).
  • Bouton de réinitialisation pour redémarrer.
  • LED tricolore. D'une part, trois broches du contrôleur sont perdues. En revanche, ils seraient toujours perdus en raison des contacts limités sur les connecteurs GPIO, et pour le débogage d'une telle LED, la chose est très utile.
  • Quartz HSE haute fréquence (16 MHz pour l'horloge principale) et LSE basse fréquence (32,7680 kHz pour l'horloge en temps réel).
  • Les broches GPIO avec un pas de 2,54 mm sont compatibles avec les cartes de prototypage.
  • Au lieu de la prise audio 3,5 mm du Raspberry Pi, j'ai positionné le connecteur d'alimentation 5 volts. À première vue, la décision est controversée. Mais il y a des avantages. L'alimentation du connecteur USB est éventuellement présente (détails ci-dessous), mais pour le débogage du circuit, c'est une mauvaise option, car le temps avant de graver le port USB de l'ordinateur dans ce cas peut être assez court.

Circuit de puissance


  • Port mini-USB D'une part, il est connecté via la puce de protection STF203-22.TCT au port USB-OTG du contrôleur. D'un autre côté, la broche d'alimentation VBUS est connectée au connecteur GPIO. Si vous le connectez à la broche + 5V, la carte sera alimentée par le port USB.

Circuit USB


  • Connecteur pour carte mémoire micro-SD avec faisceau: résistances de traction de 47 kΩ, transistor de gestion de l'alimentation ( MOSFET BSH205 à canal P ) et une petite LED verte sur la ligne électrique.

Aperçu de la carte Micro SD


La grille du transistor est connectée à la broche PA15 du contrôleur. Il s'agit du contact système du contrôleur JTDI, ce qui est intéressant en ce que dans la position initiale, il est configuré comme une sortie avec un niveau élevé (pull-up) de tension. Étant donné que SWD est utilisé à la place de JTAG pour la programmation, ce contact reste libre et peut être utilisé à d'autres fins, par exemple pour contrôler un transistor. C'est pratique - lorsque l'alimentation est appliquée à la carte, la carte mémoire est hors tension; pour l'allumer, vous devez appliquer un niveau bas à la broche PA15.


  • Convertisseur numérique-analogique basé sur UDA1334 . Cette puce n'a pas besoin d'un signal d'horloge externe, ce qui facilite son utilisation. Les données sont transmises via le bus I2S. D'autre part, Datasheet recommande d'utiliser jusqu'à 5 condensateurs polaires à 47 μF. La taille est importante dans ce cas. Les plus petits qui se sont avérés être achetés sont du tantale d'une taille de 1411, qui n'est même pas bon marché. Cependant, je vais écrire sur le prix plus en détail ci-dessous. Pour la puissance analogique, son propre stabilisateur linéaire est utilisé, la puissance de la partie numérique est activée / désactivée par un double transistor.

Circuit DAC


  • Amplificateur à deux canaux basé sur deux puces 31AP2005 . Leur principal avantage est un petit nombre de composants de cerclage (uniquement des filtres de puissance et un filtre d'entrée). Sortie audio - 4 plates-formes avec un pas de 2,54 mm. Pour ma part, je n'ai pas encore décidé ce qui est le mieux - une telle option de fortune ou, comme sur une framboise, une prise de 3,5 mm. En règle générale, 3,5 mm est associé aux écouteurs, dans notre cas, nous parlons de connecter des haut-parleurs.

Circuit amplificateur


  • Le dernier module est un châle ESP11 avec un cerclage (alimentation, prise de programmation) comme modem WiFi. Les conclusions de la carte UART sont connectées au contrôleur et émises simultanément vers un connecteur externe (pour travailler avec la carte directement depuis le terminal et programmer). Il y a un interrupteur d'alimentation (externe permanent ou contrôle à partir d'un microcontrôleur). Il y a une LED supplémentaire pour indiquer la puissance et un connecteur "FLASH" pour mettre la carte en mode programmation.

Circuit ESP


Bien sûr, l'ESP8266 lui-même est un bon contrôleur, mais il est toujours inférieur au STM32F4 en termes de performances et de périphériques. Oui, et la taille avec le prix de ce module a laissé entendre qu'il s'agissait d'une unité de modem renversée pour son frère aîné. Le module est contrôlé par USRT à l'aide d'un protocole textuel AT .


Quelques photos:


Préparation du module ESP11


ESP8266 est une chose bien connue. Je suis sûr que beaucoup le connaissent déjà, donc un guide détaillé sera superflu ici. En raison des caractéristiques schématiques de la connexion du module ESP11 à la carte, je ne donnerai qu'un bref guide à ceux qui souhaitent changer son firmware:


  • J'utiliserai l'utilitaire esptool pour travailler avec ESP. Contrairement à l'utilitaire standard du fabricant, esptool est indépendant de la plate-forme.
  • Pour commencer, activez le mode d'alimentation externe avec le cavalier ESP-PWR (nous fermons les contacts 1 et 2) et connectez le module à l'ordinateur via n'importe quel adaptateur USART-USB. L'adaptateur se connecte aux broches GRD / RX / TD. Nous alimentons la carte:
  • Nous nous assurons que l'adaptateur est reconnu par le système d'exploitation. Dans mon exemple, j'utilise un adaptateur basé sur FT232, donc avec la liste des appareils, il devrait être visible en tant que CI série FT232 (UART):
    > lsusb ... Bus 001 Device 010: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 009: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC ... 
  • ESP8266 eux-mêmes diffèrent par la quantité de mémoire flash. En pratique, dans le même module ESP11, j'ai rencontré à la fois 512 Ko (4 Mbit) et 1 Mo (8 Mbit). La première chose à vérifier est donc la quantité de mémoire dans l'instance utilisée du module. Coupez l'alimentation de la carte, et mettez le module en mode programmation, en fermant le cavalier "FLASH":


  • Mettez sous tension, exécutez esptool avec les paramètres suivants

 > esptool.py --port /dev/ttyUSB0 flash_id Connecting.... Detecting chip type... ESP8266 Chip is ESP8266EX Uploading stub... Running stub... Stub running... Manufacturer: e0 Device: 4014 Detected flash size: 1MB Hard resetting... 

  • esptool rapporte que, dans ce cas, nous avons affaire à un module avec 1 Mo de mémoire.
  • Pour la version avec 1 Mo, vous pouvez utiliser le dernier firmware, par exemple, ESP8266 AT Bin V1.6.1 . Mais il ne convient pas à la version avec 4 Mbit, pour laquelle vous devez utiliser quelque chose de plus ancien, par exemple, celui-ci . Le firmware se compose de plusieurs fichiers, les adresses de départ de chaque fichier sont indiquées dans le document officiel ESP8266 AT Instruction Set . Ces adresses de début sont utilisées comme paramètres de l'utilitaire esptool. Par exemple, pour un module de 1 Mo, les paramètres d'esptool ressembleront à ceci (tous les fichiers nécessaires doivent d'abord être extraits de l'archive du firmware et collectés dans le répertoire de travail)
     > esptool.py --port /dev/ttyUSB0 write_flash 0x00000 boot.bin 0x01000 user1.1024.new.2.bin 0x7E000 blank.bin 0xFB000 blank.bin 0xFC000 esp_init_data_default.bin 0xFE000 blank.bin 
  • Nous alimentons la carte, exécutons esptool avec les paramètres spécifiés.
  • Après avoir terminé le script, coupez l'alimentation de la carte, ouvrez le cavalier "FLASH", allumez la commande d'alimentation du microcontrôleur. Le module est prêt à fonctionner.

Logiciels


Il existe un programme de test sur github . Elle fait ce qui suit:


  • affiche le contrôleur à la fréquence maximale (168 MHz)
  • active l'horloge en temps réel
  • active la carte SD et y lit la configuration réseau. La bibliothèque FatFS est utilisée pour travailler avec le système de fichiers
  • établit une connexion au WLAN spécifié
  • se connecte au serveur NTP spécifié et lui demande l'heure actuelle. Mène l'horloge.
  • surveille l'état de plusieurs ports spécifiés. Si leur état a changé, envoie un message texte au serveur TCP spécifié.
  • lorsque vous cliquez sur le bouton externe, il lit le fichier * .wav spécifié sur la carte SD et le lit en mode asynchrone (I2S utilisant le contrôleur DMA).
  • le travail avec ESP11 est également implémenté en mode asynchrone (jusqu'à présent sans DMA, juste sur les interruptions)
  • se connecte via USART1 (broches PB6 / PB7)
  • et, bien sûr, la LED clignote.

Sur Habré, il y avait de nombreux articles consacrés à la programmation de STM32 à un niveau plutôt bas (uniquement par la gestion des registres ou CMSIS). Par exemple, depuis le dernier: un , deux , trois . Les articles sont, bien sûr, de très haute qualité, mais mon opinion subjective est que pour le développement ponctuel d'un produit, cette approche se justifie peut-être. Mais pour un projet de loisir à long terme, lorsque vous voulez que tout soit beau et extensible, cette approche est trop basique. À mon avis, l'une des raisons de la popularité d'Arduino en tant que plate-forme logicielle est que les auteurs d'Arduino ont laissé un niveau si bas pour une architecture orientée objet. Par conséquent, j'ai décidé d'aller dans la même direction et d'ajouter une couche orientée objet de haut niveau sur la bibliothèque HAL.


Ainsi, trois niveaux du programme sont obtenus:


  • Les bibliothèques des fabricants (HAL, FatFS, dans le futur USB-OTG) forment la base
  • Ma bibliothèque StmPlusPlus est basée sur cette fondation. Il comprend un ensemble de classes de base (telles que System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), un ensemble de classes de pilotes de périphériques externes (tels que SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 et similaires), ainsi que des classes de service telles qu'un lecteur asynchrone WAV.
  • Basé sur la bibliothèque StmPlusPlus, l'application elle-même est en cours de construction.

Quant au dialecte de la langue. Bien que je sois un peu démodé, je reste en C ++ 11. Cette norme a plusieurs fonctionnalités qui sont particulièrement utiles pour développer un firmware: des classes enum, des constructeurs appelants avec des accolades pour contrôler les types de paramètres passés et des conteneurs statiques comme std :: array. Soit dit en passant, sur Habré, il y a un merveilleux article sur ce sujet.


Bibliothèque StmPlusPlus


Le code complet de la bibliothèque peut être consulté sur github . Ici, je ne donnerai que quelques petits exemples pour montrer la structure, l'idée et les problèmes générés par cette idée.


Le premier exemple est une classe pour interroger périodiquement un état de broche (par exemple, un bouton) et appeler un gestionnaire lorsque cet état change:


 class Button : IOPin { public: class EventHandler { public: virtual void onButtonPressed (const Button *, uint32_t numOccured) =0; }; Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay = 50, duration_ms _pressDuration = 300); inline void setHandler (EventHandler * _handler) { handler = _handler; } void periodic (); private: const RealTimeClock & rtc; duration_ms pressDelay, pressDuration; time_ms pressTime; bool currentState; uint32_t numOccured; EventHandler * handler; }; 

Le constructeur définit tous les paramètres du bouton:


 Button::Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay, duration_ms _pressDuration): IOPin{name, pin, GPIO_MODE_INPUT, pull, GPIO_SPEED_LOW}, rtc{_rtc}, pressDelay{_pressDelay}, pressDuration{_pressDuration}, pressTime{INFINITY_TIME}, currentState{false}, numOccured{0}, handler{NULL} { // empty } 

Si la gestion de tels événements n'est pas une priorité, alors l'utilisation d'interruptions est clairement superflue. Par conséquent, divers scénarios de pression (par exemple, une seule pression ou maintien) sont implémentés dans la procédure périodique, qui doit être appelée périodiquement à partir du code de programme principal. analyse périodiquement le changement d'état et appelle de manière synchrone le gestionnaire virtuel onButtonPressed, qui devrait être implémenté dans le programme principal:


 void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) { // state is not changed: check for periodical press event if (currentState && pressTime != INFINITY_TIME) { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d >= pressDuration) { handler->onButtonPressed(this, numOccured); pressTime = rtc.getUpTimeMillisec(); ++numOccured; } } } else if (!currentState && newState) { pressTime = rtc.getUpTimeMillisec(); numOccured = 0; } else { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d < pressDelay) { // nothing to do } else if (numOccured == 0) { handler->onButtonPressed(this, numOccured); } pressTime = INFINITY_TIME; } currentState = newState; } 

Le principal avantage de cette approche est la diversité de la logique et du code pour détecter un événement à partir de son traitement. Ce n'est pas HAL_GetTick qui est utilisé pour compter le temps, qui, en raison de son type (uint32_t), est réinitialisé par débordement toutes les 2 ^ 32 millisecondes (tous les 49 jours). J'ai implémenté ma propre classe RealTimeClock, qui compte des millisecondes depuis le début du programme, ou allumant le contrôleur comme uint64_t, ce qui donne environ 5 ^ 8 ans.


Le deuxième exemple fonctionne avec une interface matérielle, dont il existe plusieurs dans le contrôleur. Par exemple, SPI. Du point de vue du programme principal, il est très pratique de ne sélectionner que l'interface souhaitée (SPI1 / SPI2 / SPI3), et tous les autres paramètres qui dépendent de cette interface seront configurés par le constructeur de classe.


 class Spi { public: const uint32_t TIMEOUT = 5000; enum class DeviceName { SPI_1 = 0, SPI_2 = 1, SPI_3 = 2, }; Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull = GPIO_NOPULL); HAL_StatusTypeDef start (uint32_t direction, uint32_t prescaler, uint32_t dataSize = SPI_DATASIZE_8BIT, uint32_t CLKPhase = SPI_PHASE_1EDGE); HAL_StatusTypeDef stop (); inline HAL_StatusTypeDef writeBuffer (uint8_t *pData, uint16_t pSize) { return HAL_SPI_Transmit(hspi, pData, pSize, TIMEOUT); } private: DeviceName device; IOPin sck, miso, mosi; SPI_HandleTypeDef *hspi; SPI_HandleTypeDef spiParams; void enableClock(); void disableClock(); }; 

Les paramètres de broche et les paramètres d'interface sont stockés localement dans la classe. Malheureusement, j'ai choisi une option d'implémentation qui n'est pas entièrement réussie, lorsque les réglages de paramètres en fonction d'une interface spécifique sont implémentés directement:


 Spi::Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull): device(_device), sck(sckPort, sckPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), miso(misoPort, misoPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), mosi(mosiPort, mosiPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), hspi(NULL) { switch (device) { case DeviceName::SPI_1: #ifdef SPI1 sck.setAlternate(GPIO_AF5_SPI1); miso.setAlternate(GPIO_AF5_SPI1); mosi.setAlternate(GPIO_AF5_SPI1); spiParams.Instance = SPI1; #endif break; ... case DeviceName::SPI_3: #ifdef SPI3 sck.setAlternate(GPIO_AF6_SPI3); miso.setAlternate(GPIO_AF6_SPI3); mosi.setAlternate(GPIO_AF6_SPI3); spiParams.Instance = SPI3; #endif break; } spiParams.Init.Mode = SPI_MODE_MASTER; spiParams.Init.DataSize = SPI_DATASIZE_8BIT; spiParams.Init.CLKPolarity = SPI_POLARITY_HIGH; spiParams.Init.CLKPhase = SPI_PHASE_1EDGE; spiParams.Init.FirstBit = SPI_FIRSTBIT_MSB; spiParams.Init.TIMode = SPI_TIMODE_DISABLE; spiParams.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiParams.Init.CRCPolynomial = 7; spiParams.Init.NSS = SPI_NSS_SOFT; } 

Le même schéma implémente les procédures enableClock et disableClock, qui sont mal extensibles et mal portables pour les autres contrôleurs. Dans ce cas, il est préférable d'utiliser des modèles où le paramètre de modèle est le nom d'interface HAL (SPI1, SPI2, SPI3), les paramètres de broche (GPIO_AF5_SPI1) et quelque chose qui contrôle l'activation / la désactivation de l'horloge. Il y a un article intéressant sur ce sujet, bien qu'il passe en revue les contrôleurs AVR, ce qui, cependant, ne fait pas de différence fondamentale.


Le début et la fin du transfert sont contrôlés par deux méthodes de démarrage / arrêt:


 HAL_StatusTypeDef Spi::start (uint32_t direction, uint32_t prescaler, uint32_t dataSize, uint32_t CLKPhase) { hspi = &spiParams; enableClock(); spiParams.Init.Direction = direction; spiParams.Init.BaudRatePrescaler = prescaler; spiParams.Init.DataSize = dataSize; spiParams.Init.CLKPhase = CLKPhase; HAL_StatusTypeDef status = HAL_SPI_Init(hspi); if (status != HAL_OK) { USART_DEBUG("Can not initialize SPI " << (size_t)device << ": " << status); return status; } /* Configure communication direction : 1Line */ if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } /* Check if the SPI is already enabled */ if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { /* Enable SPI peripheral */ __HAL_SPI_ENABLE(hspi); } USART_DEBUG("Started SPI " << (size_t)device << ": BaudRatePrescaler = " << spiParams.Init.BaudRatePrescaler << ", DataSize = " << spiParams.Init.DataSize << ", CLKPhase = " << spiParams.Init.CLKPhase << ", Status = " << status); return status; } HAL_StatusTypeDef Spi::stop () { USART_DEBUG("Stopping SPI " << (size_t)device); HAL_StatusTypeDef retValue = HAL_SPI_DeInit(&spiParams); disableClock(); hspi = NULL; return retValue; } 

Travailler avec une interface matérielle utilisant des interruptions . La classe implémente une interface I2S à l'aide d'un contrôleur DMA. I2S (Inter-IC Sound) est un complément matériel et logiciel sur SPI, qui lui-même, par exemple, sélectionne la fréquence d'horloge et contrôle les canaux en fonction du protocole audio et du débit binaire.


Dans ce cas, la classe I2S est héritée de la classe «port», c'est-à-dire que I2S est un port avec des propriétés spéciales. Certaines données sont stockées dans des structures HAL (plus pour plus de commodité, moins pour la quantité de données). Certaines données sont transférées du code principal par des liens (par exemple, la structure irqPrio).


 class I2S : public IOPort { public: const IRQn_Type I2S_IRQ = SPI2_IRQn; const IRQn_Type DMA_TX_IRQ = DMA1_Stream4_IRQn; I2S (PortName name, uint32_t pin, const InterruptPriority & prio); HAL_StatusTypeDef start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat); void stop (); inline HAL_StatusTypeDef transmit (uint16_t * pData, uint16_t size) { return HAL_I2S_Transmit_DMA(&i2s, pData, size); } inline void processI2SInterrupt () { HAL_I2S_IRQHandler(&i2s); } inline void processDmaTxInterrupt () { HAL_DMA_IRQHandler(&i2sDmaTx); } private: I2S_HandleTypeDef i2s; DMA_HandleTypeDef i2sDmaTx; const InterruptPriority & irqPrio; }; 

Son constructeur définit tous les paramètres statiques:


 I2S::I2S (PortName name, uint32_t pin, const InterruptPriority & prio): IOPort{name, GPIO_MODE_INPUT, GPIO_NOPULL, GPIO_SPEED_FREQ_LOW, pin, false}, irqPrio{prio} { i2s.Instance = SPI2; i2s.Init.Mode = I2S_MODE_MASTER_TX; i2s.Init.Standard = I2S_STANDARD_PHILIPS; // will be re-defined at communication start i2s.Init.DataFormat = I2S_DATAFORMAT_16B; // will be re-defined at communication start i2s.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE; i2s.Init.AudioFreq = I2S_AUDIOFREQ_44K; // will be re-defined at communication start i2s.Init.CPOL = I2S_CPOL_LOW; i2s.Init.ClockSource = I2S_CLOCK_PLL; i2s.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE; i2sDmaTx.Instance = DMA1_Stream4; i2sDmaTx.Init.Channel = DMA_CHANNEL_0; i2sDmaTx.Init.Direction = DMA_MEMORY_TO_PERIPH; i2sDmaTx.Init.PeriphInc = DMA_PINC_DISABLE; i2sDmaTx.Init.MemInc = DMA_MINC_ENABLE; i2sDmaTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; i2sDmaTx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; i2sDmaTx.Init.Mode = DMA_NORMAL; i2sDmaTx.Init.Priority = DMA_PRIORITY_LOW; i2sDmaTx.Init.FIFOMode = DMA_FIFOMODE_ENABLE; i2sDmaTx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; i2sDmaTx.Init.MemBurst = DMA_PBURST_SINGLE; i2sDmaTx.Init.PeriphBurst = DMA_PBURST_SINGLE; } 

Le début du transfert de données est contrôlé par les méthodes de démarrage, qui sont responsables de la configuration des paramètres du port, de l'horloge de l'interface, de la définition des interruptions, du démarrage du DMA, du démarrage de l'interface elle-même avec les paramètres de transmission spécifiés.


 HAL_StatusTypeDef I2S::start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat) { i2s.Init.Standard = standard; i2s.Init.AudioFreq = audioFreq; i2s.Init.DataFormat = dataFormat; setMode(GPIO_MODE_AF_PP); setAlternate(GPIO_AF5_SPI2); __HAL_RCC_SPI2_CLK_ENABLE(); HAL_StatusTypeDef status = HAL_I2S_Init(&i2s); if (status != HAL_OK) { USART_DEBUG("Can not start I2S: " << status); return HAL_ERROR; } __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_LINKDMA(&i2s, hdmatx, i2sDmaTx); status = HAL_DMA_Init(&i2sDmaTx); if (status != HAL_OK) { USART_DEBUG("Can not initialize I2S DMA/TX channel: " << status); return HAL_ERROR; } HAL_NVIC_SetPriority(I2S_IRQ, irqPrio.first, irqPrio.second); HAL_NVIC_EnableIRQ(I2S_IRQ); HAL_NVIC_SetPriority(DMA_TX_IRQ, irqPrio.first + 1, irqPrio.second); HAL_NVIC_EnableIRQ(DMA_TX_IRQ); return HAL_OK; } 

La procédure d'arrêt fait le contraire:


 void I2S::stop () { HAL_NVIC_DisableIRQ(I2S_IRQ); HAL_NVIC_DisableIRQ(DMA_TX_IRQ); HAL_DMA_DeInit(&i2sDmaTx); __HAL_RCC_DMA1_CLK_DISABLE(); HAL_I2S_DeInit(&i2s); __HAL_RCC_SPI2_CLK_DISABLE(); setMode(GPIO_MODE_INPUT); } 

Il existe plusieurs fonctionnalités intéressantes ici:


  • Les interruptions utilisées dans ce cas sont définies comme des constantes statiques. C'est un inconvénient pour la portabilité vers d'autres contrôleurs.
  • Une telle organisation du code garantit que les broches du port sont toujours à l'état GPIO_MODE_INPUT lorsqu'il n'y a pas de transmission. C'est un plus.
  • La priorité des interruptions est transférée de l'extérieur, c'est-à-dire qu'il existe une bonne opportunité de définir une carte de priorité d'interruption à un endroit du code principal. C'est aussi un plus.
  • La procédure d'arrêt désactive la synchronisation DMA1. Dans ce cas, cette simplification peut avoir des conséquences très négatives si quelqu'un d'autre continue à utiliser DMA1. Le problème est résolu en créant un registre centralisé des consommateurs de ces appareils, qui seront responsables de la synchronisation.
  • Autre simplification - la procédure de démarrage ne ramène pas l'interface à son état d'origine en cas d'erreur (c'est un moins, mais facilement réparable). Dans le même temps, les erreurs sont enregistrées plus en détail, ce qui est un plus.
  • Lorsque vous utilisez cette classe, le code principal doit intercepter les interruptions SPI2_IRQn et DMA1_Stream4_IRQn et garantir que les gestionnaires processI2SInterrupt et processDmaTxInterrupt correspondants sont appelés.

Programme principal


Le programme principal est écrit en utilisant la bibliothèque décrite ci-dessus tout simplement:


 int main (void) { HAL_Init(); IOPort defaultPortA(IOPort::PortName::A, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortB(IOPort::PortName::B, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortC(IOPort::PortName::C, GPIO_MODE_INPUT, GPIO_PULLDOWN); // System frequency 168MHz System::ClockDiv clkDiv; clkDiv.PLLM = 16; clkDiv.PLLN = 336; clkDiv.PLLP = 2; clkDiv.PLLQ = 7; clkDiv.AHBCLKDivider = RCC_SYSCLK_DIV1; clkDiv.APB1CLKDivider = RCC_HCLK_DIV8; clkDiv.APB2CLKDivider = RCC_HCLK_DIV8; clkDiv.PLLI2SN = 192; clkDiv.PLLI2SR = 2; do { System::setClock(clkDiv, FLASH_LATENCY_3, System::RtcType::RTC_EXT); } while (System::getMcuFreq() != 168000000L); MyApplication app; appPtr = &app; app.run(); } 

Ici, nous initialisons la bibliothèque HAL, configurons toutes les broches du contrôleur par entrée (GPIO_MODE_INPUT / PULLDOWN) par défaut. Réglez la fréquence du contrôleur, démarrez l'horloge (y compris une horloge en temps réel à partir d'un quartz externe). Après cela, un peu dans le style de Java, nous créons une instance de notre application et appelons sa méthode d'exécution, qui implémente toute la logique de l'application.


Dans une section distincte, nous devons définir toutes les interruptions utilisées. Puisque nous écrivons en C ++ et que les interruptions sont des choses du monde du C, nous devons les masquer en conséquence:


 extern "C" { void SysTick_Handler (void) { HAL_IncTick(); if (appPtr != NULL) { appPtr->getRtc().onMilliSecondInterrupt(); } } void DMA2_Stream3_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaRxInterrupt(); } void DMA2_Stream6_IRQHandler (void) { Devices::SdCard::getInstance()->processDmaTxInterrupt(); } void SDIO_IRQHandler (void) { Devices::SdCard::getInstance()->processSdIOInterrupt(); } void SPI2_IRQHandler(void) { appPtr->getI2S().processI2SInterrupt(); } void DMA1_Stream4_IRQHandler(void) { appPtr->getI2S().processDmaTxInterrupt(); } void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *channel) { appPtr->processDmaTxCpltCallback(channel); } ... } 

La classe MyApplication déclare tous les périphériques utilisés, appelle des constructeurs pour tous ces périphériques et implémente également les gestionnaires d'événements nécessaires:


 class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler { public: static const size_t INPUT_PINS = 8; // Number of monitored input pins private: UsartLogger log; RealTimeClock rtc; IOPin ledGreen, ledBlue, ledRed; PeriodicalEvent heartbeatEvent; IOPin mco; // Interrupt priorities InterruptPriority irqPrioI2S; InterruptPriority irqPrioEsp; InterruptPriority irqPrioSd; InterruptPriority irqPrioRtc; // SD card IOPin pinSdPower, pinSdDetect; IOPort portSd1, portSd2; SdCard sdCard; bool sdCardInserted; // Configuration Config config; // ESP Esp11 esp; EspSender espSender; // Input pins std::array<IOPin, INPUT_PINS> pins; std::array<bool, INPUT_PINS> pinsState; // I2S2 Audio I2S i2s; AudioDac_UDA1334 audioDac; WavStreamer streamer; Devices::Button playButton; ... 

Autrement dit, tous les appareils utilisés sont déclarés statiquement, ce qui entraîne potentiellement une augmentation de la mémoire utilisée, mais simplifie considérablement l'accès aux données. Dans le constructeur de la classe MyApplication, il est nécessaire d'appeler les concepteurs de tous les périphériques, après quoi, au moment où la procédure d'exécution est lancée, tous les périphériques de microcontrôleur utilisés seront initialisés:


  MyApplication::MyApplication () : // logging log(Usart::USART_1, IOPort::B, GPIO_PIN_6, GPIO_PIN_7, 115200), // RTC rtc(), ledGreen(IOPort::C, GPIO_PIN_1, GPIO_MODE_OUTPUT_PP), ledBlue(IOPort::C, GPIO_PIN_2, GPIO_MODE_OUTPUT_PP), ledRed(IOPort::C, GPIO_PIN_3, GPIO_MODE_OUTPUT_PP), heartbeatEvent(rtc, 10, 2), mco(IOPort::A, GPIO_PIN_8, GPIO_MODE_AF_PP), // Interrupt priorities irqPrioI2S(6, 0), // I2S DMA interrupt priority: 7 will be also used irqPrioEsp(5, 0), irqPrioSd(3, 0), // SD DMA interrupt priority: 4 will be also used irqPrioRtc(2, 0), // SD card pinSdPower(IOPort::A, GPIO_PIN_15, GPIO_MODE_OUTPUT_PP, GPIO_PULLDOWN, GPIO_SPEED_HIGH, true, false), pinSdDetect(IOPort::B, GPIO_PIN_3, GPIO_MODE_INPUT, GPIO_PULLUP), portSd1(IOPort::C, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12, /* callInit = */false), portSd2(IOPort::D, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_2, /* callInit = */false), sdCard(pinSdDetect, portSd1, portSd2), sdCardInserted(false), // Configuration config(pinSdPower, sdCard, "conf.txt"), //ESP esp(rtc, Usart::USART_2, IOPort::A, GPIO_PIN_2, GPIO_PIN_3, irqPrioEsp, IOPort::A, GPIO_PIN_1), espSender(rtc, esp, ledRed), // Input pins pins { { IOPin(IOPort::A, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_6, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_7, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_0, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_1, GPIO_MODE_INPUT, GPIO_PULLUP) } }, // I2S2 Audio Configuration // PB10 --> I2S2_CK // PB12 --> I2S2_WS // PB15 --> I2S2_SD i2s(IOPort::B, GPIO_PIN_10 | GPIO_PIN_12 | GPIO_PIN_15, irqPrioI2S), audioDac(i2s, /* power = */ IOPort::B, GPIO_PIN_11, /* mute = */ IOPort::B, GPIO_PIN_13, /* smplFreq = */ IOPort::B, GPIO_PIN_14), streamer(sdCard, audioDac), playButton(IOPort::B, GPIO_PIN_2, GPIO_PULLUP, rtc) { mco.activateClockOutput(RCC_MCO1SOURCE_PLLCLK, RCC_MCODIV_5); } 

Par exemple, le gestionnaire d'événements pour cliquer sur un bouton qui démarre / arrête la lecture d'un fichier WAV:


  virtual void MyApplication::onButtonPressed (const Devices::Button * b, uint32_t numOccured) { if (b == &playButton) { USART_DEBUG("play button pressed: " << numOccured); if (streamer.isActive()) { USART_DEBUG(" Stopping WAV"); streamer.stop(); } else { USART_DEBUG(" Starting WAV"); streamer.start(AudioDac_UDA1334::SourceType:: STREAM, config.getWavFile()); } } } 

Enfin, la méthode d'exécution principale termine la configuration des périphériques (par exemple, définit MyApplication comme gestionnaire d'événements) et démarre une boucle sans fin, où elle se réfère périodiquement aux périphériques qui nécessitent une attention périodique:


 void MyApplication::run () { log.initInstance(); USART_DEBUG("Oscillator frequency: " << System::getExternalOscillatorFreq() << ", MCU frequency: " << System::getMcuFreq()); HAL_StatusTypeDef status = HAL_TIMEOUT; do { status = rtc.start(8 * 2047 + 7, RTC_WAKEUPCLOCK_RTCCLK_DIV2, irqPrioRtc, this); USART_DEBUG("RTC start status: " << status); } while (status != HAL_OK); sdCard.setIrqPrio(irqPrioSd); sdCard.initInstance(); if (sdCard.isCardInserted()) { updateSdCardState(); } USART_DEBUG("Input pins: " << pins.size()); pinsState.fill(true); USART_DEBUG("Pin state: " << fillMessage()); esp.assignSendLed(&ledGreen); streamer.stop(); streamer.setHandler(this); streamer.setVolume(1.0); playButton.setHandler(this); bool reportState = false; while (true) { updateSdCardState(); playButton.periodic(); streamer.periodic(); if (isInputPinsChanged()) { USART_DEBUG("Input pins change detected"); ledBlue.putBit(true); reportState = true; } espSender.periodic(); if (espSender.isOutputMessageSent()) { if (reportState) { espSender.sendMessage(config, "TCP", config.getServerIp(), config.getServerPort(), fillMessage()); reportState = false; } if (!reportState) { ledBlue.putBit(false); } } if (heartbeatEvent.isOccured()) { ledGreen.putBit(heartbeatEvent.occurance() == 1); } } } 

Un peu d'expérimentation


Un fait intéressant est que le microcontrôleur se prête à un overclocking partiel. — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.


Prix


. github . - , Mouser ( ). 37 . . , STM Olimex, .



. , :


  • ( ). , , . : 4 8 . PLL, .
  • , . 47 μF . , .
  • SWD . - , . .
  • . SMD , . 3 .

La documentation


github GPL v3:



Merci de votre attention!

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


All Articles