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.


- 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.

- 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.

- 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.

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.

- 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.

- 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.

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:

- 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...
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} {
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) {
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; } if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { __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;
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);
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;
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 () :
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!