STM32F4 Debug Board im Raspberry Pi Formfaktor

Bild Guten Tag, liebe Chabrowiten! Ich möchte mein Projekt der Öffentlichkeit vorstellen - ein kleines Debugboard, das auf STM32 basiert, aber im Raspberry Pi-Formfaktor. Es unterscheidet sich von anderen Debug-Boards dadurch, dass es eine mit Raspberry Pi-Gehäusen kompatible Geometrie und ein ESP8266-Modul als drahtloses Modem aufweist. Und auch schöne Ergänzungen in Form eines Anschlusses für eine Micro-SD-Karte und einen Stereoverstärker. Um all diesen Reichtum zu nutzen, habe ich eine Bibliothek auf hoher Ebene und ein Demo-Programm (in C ++ 11) entwickelt. In dem Artikel möchte ich sowohl die Hardware- als auch die Softwareteile dieses Projekts detailliert beschreiben.


Wer kann von diesem Projekt profitieren? Wahrscheinlich nur für diejenigen, die diese Platine selbst löten möchten, da ich selbst für die Produktion in kleinem Maßstab keine Optionen in Betracht ziehe. Das ist ein reines Hobby. Meiner Meinung nach deckt das Board ein ziemlich breites Spektrum von Aufgaben ab, die im Rahmen kleiner Heimwerkerarbeiten mit WLAN und Sound entstehen können.


Zunächst werde ich versuchen, die Frage zu beantworten, warum dies alles ist. Die Hauptmotivatoren dieses Projekts sind:


  • Die Wahl der STM32-Plattform beruht auf rein ästhetischen Überlegungen - ich mag das Preis-Leistungs-Verhältnis sowie eine breite Palette an Peripheriegeräten und ein großes und praktisches Entwicklungs-Ökosystem des Controller-Herstellers (sw4stm, cubeMX, HAL-Bibliothek).
  • Natürlich gibt es viele Debug-Boards des Controller-Herstellers selbst (Discovery, Nucleo) sowie von Drittherstellern (z. B. Olimex). Aber viele von ihnen zu Hause in ihrem Formfaktor zu wiederholen, ist zumindest für mich problematisch. In meiner Version haben wir eine einfache zweischichtige Topologie und Komponenten, die zum manuellen Löten geeignet sind.
  • Für ihre Geräte möchte ich anständige Gehäuse haben, um die schlechte Qualität der Elektronik im Inneren zu maskieren. Es gibt mindestens zwei beliebte Plattformen, für die es eine Vielzahl unterschiedlichster Fälle gibt: Arduino und Raspberry Pi. Der zweite von ihnen schien mir in Bezug auf die Position der Ausschnitte für die Verbinder bequemer zu sein. Deshalb habe ich mich als Spender für die Geometrie des Boards dafür entschieden.
  • Der an Bord ausgewählte Controller verfügt über ein USB-, SDIO-, I2S- und Netzwerk. Andererseits sind dieselben Schnittstellen auch für eine Heim-Hobby-Plattform nützlich. Aus diesem Grund habe ich zusätzlich zum Controller mit einem Standardkabelbaum einen USB-Anschluss, eine SD-Karte, einen Audiopfad (Digital-Analog-Wandler und Verstärker) sowie ein auf dem ESP8266 basierendes Funkmodul hinzugefügt.

Schaltung und Komponenten


Es scheint mir, dass sich ein ziemlich schönes Board mit den folgenden Eigenschaften und Komponenten herausgestellt hat:


  • STM32F405RG- Controller: ARM 32-Bit-Cortex-M4 mit einem mathematischen Coprozessor, Frequenz bis 168 MHz, 1 MB Flash-Speicher, 196 KB RAM.
    Verwendete Controller-Pins
    Controller-Bindung
  • SWD-Anschluss zur Programmierung der Steuerung (6 Pins).
  • Reset-Taste zum Neustart.
  • Dreifarbige LED. Einerseits gehen drei Controller-Pins verloren. Andererseits würden sie aufgrund der begrenzten Kontakte an den GPIO-Anschlüssen immer noch verloren gehen, und zum Debuggen einer solchen LED ist das Ding sehr nützlich.
  • Hochfrequenz-HSE-Quarz (16 MHz für Kerntakt) und Niederfrequenz-LSE-Quarz (32,7680 kHz für Echtzeituhr).
  • GPIO-Pins mit einem Abstand von 2,54 mm sind mit Prototyping-Boards kompatibel.
  • Anstelle der 3,5-mm-Audiobuchse des Raspberry Pi habe ich den 5-Volt-Stromanschluss positioniert. Die Entscheidung ist auf den ersten Blick umstritten. Aber es gibt Profis. Die Stromversorgung über den USB-Anschluss ist optional vorhanden (Details siehe unten). Für das Debuggen der Schaltung ist dies jedoch eine schlechte Option, da die Zeit vor dem Brennen des USB-Anschlusses des Computers in diesem Fall recht kurz sein kann.

Stromkreis


  • Mini-USB-Anschluss Zum einen wird es über den Schutzchip STF203-22.TCT an den USB-OTG-Port des Controllers angeschlossen. Andererseits ist der VBUS-Stromanschluss mit dem GPIO-Anschluss verbunden. Wenn Sie es an den + 5V-Pin anschließen, wird die Karte über den USB-Anschluss mit Strom versorgt.

USB-Schaltung


  • Anschluss für Micro-SD-Speicherkarte mit Kabelbaum: 47-kΩ-Pull-up-Widerstände, Power-Management-Transistor ( P-Kanal-MOSFET BSH205 ) und eine kleine grüne LED an der Stromleitung.

Umriss der Micro-SD-Karte


Das Transistor-Gate ist mit dem PA15-Pin der Steuerung verbunden. Dies ist der Systemkontakt des JTDI-Controllers, was insofern interessant ist, als er in der Ausgangsposition als Ausgang mit einem hohen Spannungspegel (Pull-up) konfiguriert ist. Da SWD anstelle von JTAG für die Programmierung verwendet wird, bleibt dieser Kontakt frei und kann für andere Zwecke verwendet werden, beispielsweise zum Steuern eines Transistors. Dies ist praktisch: Wenn die Karte mit Strom versorgt wird, ist die Speicherkarte stromlos. Zum Einschalten müssen Sie einen niedrigen Pegel an Pin PA15 anlegen.


  • Digital-Analog-Wandler basierend auf UDA1334 . Dieser Chip benötigt kein externes Taktsignal, was seine Verwendung erleichtert. Daten werden über den I2S-Bus übertragen. Auf der anderen Seite empfiehlt Datasheet die Verwendung von bis zu 5 Polarkondensatoren bei 47 μF. Größe ist in diesem Fall wichtig. Die kleinsten, die sich als gekauft herausstellten, sind Tantal mit einer Größe von 1411, die nicht einmal billig sind. Ich werde jedoch weiter unten ausführlicher über den Preis schreiben. Für die analoge Leistung wird ein eigener linearer Stabilisator verwendet, die Leistung des digitalen Teils wird durch einen Doppeltransistor ein- und ausgeschaltet.

DAC-Schaltung


  • Zweikanalverstärker basierend auf zwei 31AP2005- Chips. Ihr Hauptvorteil ist eine geringe Anzahl von Umreifungskomponenten (nur Leistungsfilter und ein Eingangsfilter). Audioausgang - 4 Plattformen mit einem Abstand von 2,54 mm. Für mich selbst habe ich noch nicht entschieden, was am besten ist - eine solche provisorische Option oder, wie bei einer Himbeere, einen 3,5-mm-Stecker. In der Regel sind 3,5 mm mit Kopfhörern verbunden. In unserem Fall handelt es sich um das Anschließen von Lautsprechern.

Verstärkerschaltung


  • Das letzte Modul ist ein ESP11-Schal mit einer Umreifung (Stromversorgung, Programmierbuchse) als WLAN-Modem. Die Schlussfolgerungen der UART-Karte werden an die Steuerung angeschlossen und gleichzeitig an einen externen Anschluss ausgegeben (zum Arbeiten mit der Karte direkt vom Terminal und zur Programmierung). Es gibt einen Netzschalter (permanent extern oder Steuerung über einen Mikrocontroller). Es gibt eine zusätzliche LED zur Anzeige der Stromversorgung und einen „FLASH“ -Anschluss, um die Karte in den Programmiermodus zu versetzen.

ESP-Schaltung


Natürlich ist der ESP8266 selbst ein guter Controller, aber er ist dem STM32F4 in Leistung und Peripherie immer noch unterlegen. Ja, und die Größe mit dem Preis dieses Moduls deutete darauf hin, dass es sich um eine verschüttete Modemeinheit für seinen älteren Bruder handelte. Das Modul wird von USRT unter Verwendung eines Text- AT- Protokolls gesteuert.


Ein paar Fotos:


Vorbereiten des ESP11-Moduls


ESP8266 ist eine bekannte Sache. Ich bin mir sicher, dass viele bereits damit vertraut sind, daher ist eine ausführliche Anleitung hier überflüssig. Aufgrund der schematischen Merkmale beim Anschließen des ESP11-Moduls an die Karte werde ich nur eine kurze Anleitung für diejenigen geben, die die Firmware ändern möchten:


  • Ich werde das Dienstprogramm esptool verwenden , um mit ESP zu arbeiten. Im Gegensatz zum Standarddienstprogramm des Herstellers ist esptool plattformunabhängig.
  • Schalten Sie zunächst den externen Stromversorgungsmodus mit dem ESP-PWR-Jumper ein (wir schließen die Kontakte 1 und 2) und verbinden Sie das Modul über einen beliebigen USART-USB-Adapter mit dem Computer. Der Adapter wird an die GRD / RX / TD-Pins angeschlossen. Wir versorgen das Board mit Strom:
  • Wir stellen sicher, dass der Adapter vom Betriebssystem erkannt wird. In meinem Beispiel verwende ich einen Adapter, der auf FT232 basiert. In der Liste der Geräte sollte er daher als FT232 Serial (UART) IC angezeigt werden:
    > 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 selbst unterscheiden sich in der Größe des Flash-Speichers. In der Praxis stieß ich im selben ESP11-Modul sowohl auf 512 KB (4 Mbit) als auch auf 1 MB (8 Mbit). Als erstes muss überprüft werden, wie viel Speicher sich in der verwendeten Modulinstanz befindet. Schalten Sie die Platine aus, versetzen Sie das Modul in den Programmiermodus und schließen Sie den Jumper "FLASH":


  • Schalten Sie das Gerät ein und führen Sie esptool mit den folgenden Parametern aus

 > 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 berichtet, dass es sich in diesem Fall um ein Modul mit 1 MB Speicher handelt.
  • Für die Version mit 1 MB können Sie die neueste Firmware verwenden, z. B. ESP8266 AT Bin V1.6.1 . Es ist jedoch nicht für die Version mit 4 Mbit geeignet, für die Sie etwas Älteres verwenden müssen, zum Beispiel dieses . Die Firmware besteht aus mehreren Dateien. Die Startadressen jeder Datei sind im offiziellen Dokument ESP8266 AT Instruction Set angegeben . Diese Startadressen werden als Parameter des Dienstprogramms esptool verwendet. Bei einem Modul mit 1 MB sehen die Parameter von esptool beispielsweise folgendermaßen aus (alle erforderlichen Dateien müssen zuerst aus dem Firmware-Archiv extrahiert und im Arbeitsverzeichnis gesammelt werden).
     > 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 
  • Wir versorgen die Platine mit Strom und führen esptool mit den angegebenen Parametern aus.
  • Schalten Sie nach Abschluss des Skripts die Stromversorgung von der Karte aus, öffnen Sie den "FLASH" -Jumper und schalten Sie die Stromversorgung über den Mikrocontroller ein. Das Modul ist betriebsbereit.

Software


Es gibt ein Testprogramm auf Github . Sie macht folgendes:


  • zeigt den Controller mit der maximalen Frequenz (168 MHz) an
  • Aktiviert die Echtzeituhr
  • Aktiviert die SD-Karte und liest die Netzwerkkonfiguration daraus. Die FatFS-Bibliothek wird verwendet, um mit dem Dateisystem zu arbeiten.
  • stellt eine Verbindung zum angegebenen WLAN her
  • stellt eine Verbindung zum angegebenen NTP-Server her und fordert die aktuelle Uhrzeit von diesem an. Führt die Uhr.
  • überwacht den Status mehrerer angegebener Ports. Wenn sich ihr Status geändert hat, wird eine Textnachricht an den angegebenen TCP-Server gesendet.
  • Wenn Sie auf die externe Schaltfläche klicken, wird die angegebene * .wav-Datei von der SD-Karte gelesen und im asynchronen Modus (I2S mit dem DMA-Controller) abgespielt.
  • Die Arbeit mit ESP11 wird auch im asynchronen Modus implementiert (bisher ohne DMA, nur bei Interrupts).
  • meldet sich über USART1 an (Pins PB6 / PB7)
  • und natürlich blinkt die LED.

Auf Habré gab es viele Artikel, die sich mit der Programmierung von STM32 auf einer relativ niedrigen Ebene befassten (nur durch Registerverwaltung oder CMSIS). Zum Beispiel vom relativ letzten: eins , zwei , drei . Die Artikel sind natürlich von sehr hoher Qualität, aber meine subjektive Meinung ist, dass sich dieser Ansatz für die einmalige Entwicklung eines Produkts vielleicht rechtfertigt. Aber für ein langfristiges Hobbyprojekt, bei dem alles schön und erweiterbar sein soll, ist dieser Ansatz zu niedrig. Einer der Gründe für die Popularität von Arduino als Softwareplattform ist meiner Meinung nach, dass die Autoren von Arduino ein so niedriges Niveau für objektorientierte Architektur hinterlassen haben. Aus diesem Grund habe ich mich entschlossen, in die gleiche Richtung zu gehen und der HAL-Bibliothek eine objektorientierte Ebene auf ziemlich hoher Ebene hinzuzufügen.


Somit werden drei Ebenen des Programms erhalten:


  • Herstellerbibliotheken (HAL, FatFS, zukünftig USB-OTG) bilden die Grundlage
  • Meine StmPlusPlus-Bibliothek basiert auf dieser Grundlage. Es enthält eine Reihe von Basisklassen (wie System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), eine Reihe von Treiberklassen für externe Geräte (wie SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 und dergleichen) Serviceklassen wie ein asynchroner WAV-Player.
  • Basierend auf der StmPlusPlus-Bibliothek wird die Anwendung selbst erstellt.

Wie für den Dialekt der Sprache. Während ich etwas altmodisch bin, bleibe ich in C ++ 11. Dieser Standard verfügt über mehrere Funktionen, die besonders für die Entwicklung von Firmware nützlich sind: Enum-Klassen, Aufruf von Konstruktoren mit geschweiften Klammern zur Steuerung der übergebenen Parametertypen und statische Container wie std :: array. Auf Habré gibt es übrigens einen wunderbaren Artikel zu diesem Thema.


StmPlusPlus-Bibliothek


Der vollständige Bibliothekscode kann auf github eingesehen werden . Hier werde ich nur einige kleine Beispiele geben, um die Struktur, Idee und Probleme zu zeigen, die durch diese Idee erzeugt werden.


Das erste Beispiel ist eine Klasse zum periodischen Abrufen eines Pin-Status (z. B. einer Schaltfläche) und zum Aufrufen eines Handlers, wenn sich dieser Status ändert:


 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; }; 

Der Konstruktor definiert alle Schaltflächenparameter:


 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 } 

Wenn die Behandlung solcher Ereignisse keine Priorität hat, ist die Verwendung von Interrupts eindeutig überflüssig. Daher werden in der periodischen Prozedur verschiedene Pressenszenarien (z. B. ein einzelnes Drücken oder Halten) implementiert, die regelmäßig aus dem Hauptprogrammcode aufgerufen werden sollten. analysiert regelmäßig die Statusänderung und ruft synchron den virtuellen Handler onButtonPressed auf, der im Hauptprogramm implementiert werden sollte:


 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; } 

Der Hauptvorteil dieses Ansatzes ist die Vielfalt von Logik und Code zum Erkennen eines Ereignisses aus seiner Verarbeitung. Es ist nicht HAL_GetTick, das zum Zählen der Zeit verwendet wird, die aufgrund ihres Typs (uint32_t) alle 2 ^ 32 Millisekunden (alle 49 Tage) durch Überlauf zurückgesetzt wird. Ich habe meine eigene Klasse RealTimeClock implementiert, die Millisekunden ab dem Start des Programms zählt, oder den Controller wie uint64_t eingeschaltet, was ungefähr 5 ^ 8 Jahre ergibt.


Das zweite Beispiel ist die Arbeit mit einer Hardwareschnittstelle, von der sich mehrere in der Steuerung befinden. Zum Beispiel SPI. Aus Sicht des Hauptprogramms ist es sehr praktisch, nur die gewünschte Schnittstelle (SPI1 / SPI2 / SPI3) auszuwählen, und alle anderen Parameter, die von dieser Schnittstelle abhängen, werden vom Klassenkonstruktor konfiguriert.


 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(); }; 

Pin-Parameter und Schnittstellenparameter werden lokal in der Klasse gespeichert. Leider habe ich mich für eine nicht ganz erfolgreiche Implementierungsoption entschieden, wenn Parametereinstellungen abhängig von einer bestimmten Schnittstelle direkt implementiert werden:


 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; } 

Das gleiche Schema implementiert die Prozeduren enableClock und disableClock, die schlecht erweiterbar und für andere Controller schlecht portierbar sind. In diesem Fall ist es besser, Vorlagen zu verwenden, bei denen der Vorlagenparameter der Name der HAL-Schnittstelle (SPI1, SPI2, SPI3), Pin-Parameter (GPIO_AF5_SPI1) und etwas ist, das die Uhr ein / aus steuert. Es gibt einen interessanten Artikel zu diesem Thema, der sich jedoch mit AVR-Controllern befasst, der jedoch keinen grundlegenden Unterschied macht.


Der Start und das Ende der Übertragung werden durch zwei Start / Stopp-Methoden gesteuert:


 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; } 

Arbeiten Sie mit der Hardware-Schnittstelle mithilfe von Interrupts . Die Klasse implementiert eine I2S-Schnittstelle mithilfe eines DMA-Controllers. I2S (Inter-IC Sound) ist ein Hardware- und Software-Add-On über SPI, das beispielsweise selbst die Taktfrequenz auswählt und die Kanäle abhängig vom Audioprotokoll und der Bitrate steuert.


In diesem Fall wird die I2S-Klasse von der "Port" -Klasse geerbt, dh I2S ist ein Port mit speziellen Eigenschaften. Einige Daten werden in HAL-Strukturen gespeichert (plus der Einfachheit halber minus der Datenmenge). Einige Daten werden vom Hauptcode über Links übertragen (z. B. die irqPrio-Struktur).


 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; }; 

Sein Konstruktor legt alle statischen Parameter fest:


 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; } 

Der Start der Datenübertragung wird durch die Startmethoden gesteuert, die für die Konfiguration der Portparameter, die Taktung der Schnittstelle, das Setzen von Interrupts, das Starten des DMA und das Starten der Schnittstelle selbst mit den angegebenen Übertragungsparametern verantwortlich sind.


 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; } 

Das Stoppverfahren macht das Gegenteil:


 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); } 

Hier gibt es einige interessante Funktionen:


  • Verwendete Interrupts werden in diesem Fall als statische Konstanten definiert. Dies ist ein Minus für die Portabilität auf andere Controller.
  • Eine solche Organisation des Codes stellt sicher, dass sich die Port-Pins immer im Status GPIO_MODE_INPUT befinden, wenn keine Übertragung erfolgt. Das ist ein Plus.
  • Die Priorität von Interrupts wird von außen übertragen, dh es besteht eine gute Möglichkeit, eine Interrupt-Prioritätszuordnung an einer Stelle des Hauptcodes festzulegen. Dies ist auch ein Plus.
  • Die Stoppprozedur deaktiviert die DMA1-Taktung. In diesem Fall kann diese Vereinfachung sehr negative Folgen haben, wenn jemand anderes weiterhin DMA1 verwendet. Das Problem wird gelöst, indem ein zentrales Register der Verbraucher solcher Geräte erstellt wird, das für das Timing verantwortlich ist.
  • Eine weitere Vereinfachung: Die Startprozedur bringt die Schnittstelle im Fehlerfall nicht in den ursprünglichen Zustand (dies ist ein Minus, aber leicht zu beheben). Gleichzeitig werden Fehler detaillierter protokolliert, was ein Plus ist.
  • Bei Verwendung dieser Klasse sollte der Hauptcode die Interrupts SPI2_IRQn und DMA1_Stream4_IRQn abfangen und sicherstellen, dass die entsprechenden Handler processI2SInterrupt und processDmaTxInterrupt aufgerufen werden.

Hauptprogramm


Das Hauptprogramm wird mit der oben beschriebenen Bibliothek ganz einfach geschrieben:


 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(); } 

Hier initialisieren wir die HAL-Bibliothek und konfigurieren standardmäßig alle Controller-Pins per Eingabe (GPIO_MODE_INPUT / PULLDOWN). Wir stellen die Frequenz des Controllers ein, starten die Uhr (einschließlich der Echtzeituhr vom externen Quarz). Danach erstellen wir, etwas im Stil von Java, eine Instanz unserer Anwendung und rufen deren Ausführungsmethode auf, die die gesamte Anwendungslogik implementiert.


In einem separaten Abschnitt müssen wir alle verwendeten Interrupts definieren. Da wir in C ++ schreiben und Interrupts Dinge aus der Welt von C sind, müssen wir sie entsprechend maskieren:


 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); } ... } 

Die MyApplication-Klasse deklariert alle verwendeten Geräte, ruft Konstruktoren für alle diese Geräte auf und implementiert auch die erforderlichen Ereignishandler:


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

Das heißt, tatsächlich werden alle verwendeten Geräte statisch deklariert, was möglicherweise zu einer Erhöhung des verwendeten Speichers führt, aber den Zugriff auf Daten erheblich vereinfacht. Im Konstruktor der MyApplication-Klasse müssen die Designer aller Geräte aufgerufen werden. Danach werden zum Start der Ausführungsprozedur alle verwendeten Mikrocontroller-Geräte initialisiert:


  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); } 

Beispiel: Der Ereignishandler zum Klicken auf eine Schaltfläche zum Starten / Stoppen der Wiedergabe einer WAV-Datei:


  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()); } } } 

Schließlich schließt die Hauptausführungsmethode die Konfiguration der Geräte ab (z. B. legt MyApplication als Ereignishandler fest) und startet eine Endlosschleife, in der regelmäßig auf die Geräte verwiesen wird, die regelmäßige Aufmerksamkeit erfordern:


 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); } } } 

Ein bisschen experimentieren


Interessant ist, dass sich der Mikrocontroller zum teilweisen Übertakten eignet. — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.


Preis


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



. , :


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

Die Dokumentation


github GPL v3:



Vielen Dank für Ihre Aufmerksamkeit!

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


All Articles