Buenas tardes, queridos Khabrovites! Quiero presentar mi proyecto al público: una pequeña placa de depuración basada en STM32, pero con el factor de forma Raspberry Pi. Se diferencia de otras placas de depuración en que tiene una geometría compatible con las carcasas Raspberry Pi y un módulo ESP8266 como módem inalámbrico. Y también buenas adiciones en forma de un conector para una tarjeta micro-SD y un amplificador estéreo. Para aprovechar toda esta riqueza, desarrollé una biblioteca de alto nivel y un programa de demostración (en C ++ 11). En el artículo quiero describir en detalle las partes de hardware y software de este proyecto.
¿Quién puede beneficiarse de este proyecto? Probablemente, solo para aquellos que quieran soldar esta placa yo mismo, ya que no considero ninguna opción incluso para la producción a pequeña escala. Este es un pasatiempo puro. En mi opinión, el tablero cubre una gama bastante amplia de tareas que pueden surgir en el marco de pequeñas embarcaciones hogareñas que usan WiFi y sonido.
Para empezar, intentaré responder la pregunta de por qué esto es todo. Los principales motivadores de este proyecto son los siguientes:
- La elección de la plataforma STM32 se debe a consideraciones puramente estéticas: me gusta la relación precio / rendimiento, más una amplia gama de periféricos, más un ecosistema de desarrollo grande y conveniente del fabricante del controlador (sw4stm, cubeMX, biblioteca HAL).
- Por supuesto, hay muchas placas de depuración del propio fabricante del controlador (Discovery, Nucleo), así como de terceros fabricantes (por ejemplo, Olimex). Pero repetir muchas de ellas en casa en su factor de forma es problemático para mí, al menos. En mi versión, tenemos una topología simple de dos capas y componentes convenientes para la soldadura manual.
- Para sus dispositivos, quiero tener estuches decentes para enmascarar la baja calidad de la electrónica en su interior. Hay al menos dos plataformas populares para las cuales hay una gran cantidad de los casos más diversos: Arduino y Raspberry Pi. El segundo de ellos me pareció más conveniente en términos de la ubicación de los recortes para los conectores. Por lo tanto, como donante para la geometría del tablero, lo elegí.
- El controlador que seleccioné a bordo tiene USB, SDIO, I2S, red. Por otro lado, estas mismas interfaces también son útiles para una plataforma hobby doméstica. Es por eso que, además del controlador con un enlace estándar, agregué un conector USB, una tarjeta SD, una ruta de audio (convertidor y amplificador digital a analógico), así como un módulo inalámbrico basado en el ESP8266.
Circuito y componentes
Me parece que ha resultado una placa bastante bonita con las siguientes características y componentes:
- Controlador STM32F405RG : ARM Cortex-M4 de 32 bits con un coprocesador matemático, frecuencia de hasta 168 MHz, memoria flash de 1 Mb, 196 Kb de RAM.


- Conector SWD para programar el controlador (6 pines).
- Botón de reinicio para reiniciar.
- LED tricolor. Por un lado, se pierden tres pines del controlador. Por otro lado, todavía se perderían debido a los contactos limitados en los conectores GPIO, y para depurar un LED de este tipo, la cosa es muy útil.
- Cuarzo HSE de alta frecuencia (16 MHz para reloj central) y LSE de baja frecuencia (32.7680 kHz para reloj en tiempo real).
- Los pines GPIO con un paso de 2.54 mm son compatibles con las placas de prototipos.
- En lugar del conector de audio de 3,5 mm de la Raspberry Pi, coloqué el conector de alimentación de 5 voltios. A primera vista, la decisión es controvertida. Pero hay pros. La alimentación del conector USB está presente opcionalmente (detalles a continuación), pero esta es una mala opción para depurar el circuito, ya que el tiempo antes de quemar el puerto USB de la computadora en este caso puede ser bastante corto.

- Puerto mini-USB Por un lado, se conecta a través del chip de protección STF203-22.TCT al puerto USB-OTG del controlador. Por otro lado, el pin de alimentación VBUS está conectado al conector GPIO. Si lo conecta al pin + 5V, la placa se alimentará desde el puerto USB.

- Ranura para tarjeta de memoria Micro-SD con arnés: resistencias pull-up de 47 kΩ, transistor de administración de energía ( canal P MOSFET BSH205 ) y un pequeño LED verde en la línea de alimentación.

La puerta del transistor está conectada al pin PA15 del controlador. Este es el contacto del sistema del controlador JTDI, que es interesante porque en la posición inicial está configurado como una salida con un alto nivel (pull-up) de voltaje. Dado que se usa SWD en lugar de JTAG para la programación, este contacto permanece libre y se puede usar para otros fines, por ejemplo, controlar un transistor. Esto es conveniente: cuando se aplica alimentación a la placa, la tarjeta de memoria se desactiva; para habilitarla, debe aplicar un nivel bajo al pin PA15.
- Convertidor digital a analógico basado en UDA1334 . Este chip no necesita una señal de reloj externa, lo que facilita su uso. Los datos se transmiten a través del bus I2S. Por otro lado, Datasheet recomienda usar hasta 5 capacitores polares a 47 μF. El tamaño es importante en este caso. Los más pequeños que se compraron son el tantalio con un tamaño de 1411, que ni siquiera son baratos. Sin embargo, escribiré sobre el precio con más detalle a continuación. Para la alimentación analógica, se utiliza su propio estabilizador lineal, la potencia de la parte digital se enciende / apaga mediante un transistor doble.

- Amplificador de dos canales basado en dos chips 31AP2005 . Su principal ventaja es un pequeño número de componentes de flejado (solo filtros de potencia y un filtro de entrada). Salida de audio: 4 plataformas con un paso de 2,54 mm. Por mi parte, aún no he decidido qué es lo mejor: una opción improvisada o, como en una frambuesa, un enchufe de 3.5 mm. Como regla general, se asocian 3.5 mm con los auriculares, en nuestro caso estamos hablando de conectar altavoces.

- El último módulo es un chal ESP11 con un fleje (alimentación, toma de programación) como módem WiFi. Las conclusiones de la placa UART se conectan al controlador y se envían simultáneamente a un conector externo (para trabajar con la placa directamente desde el terminal y la programación). Hay un interruptor de encendido (permanente externo o control desde un microcontrolador). Hay un LED adicional para indicar la alimentación y un conector "FLASH" para poner la placa en modo de programación.

Por supuesto, el ESP8266 en sí mismo es un buen controlador, pero sigue siendo inferior al STM32F4 tanto en rendimiento como en periféricos. Sí, y el tamaño con el precio de este módulo insinuó que era una unidad de módem derramada para su hermano mayor. El módulo es controlado por USRT utilizando un protocolo de texto AT .
Un par de fotos:


Preparación del módulo ESP11
ESP8266 es algo muy conocido. Estoy seguro de que muchos ya lo conocen, por lo que una guía detallada será superflua aquí. Debido a las características esquemáticas de conectar el módulo ESP11 a la placa, solo daré una breve guía para aquellos que quieran cambiar su firmware:

- Encienda la alimentación, ejecute esptool con los siguientes parámetros
> 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...
Software
Hay un programa de prueba en github . Ella hace lo siguiente:
- muestra el controlador a la frecuencia máxima (168 MHz)
- activa el reloj en tiempo real
- activa la tarjeta SD y lee la configuración de red de ella. La biblioteca FatFS se usa para trabajar con el sistema de archivos.
- establece una conexión a la WLAN especificada
- se conecta al servidor NTP especificado y le solicita la hora actual. Lleva el reloj.
- supervisa el estado de varios puertos especificados. Si su estado ha cambiado, envía un mensaje de texto al servidor TCP especificado.
- Cuando hace clic en el botón externo, lee el archivo * .wav especificado de la tarjeta SD y lo reproduce en modo asíncrono (I2S usando el controlador DMA).
- el trabajo con ESP11 también se implementa en modo asíncrono (hasta ahora sin DMA, solo en interrupciones)
- inicia sesión a través de USART1 (pines PB6 / PB7)
- y, por supuesto, el LED parpadea.
En Habré había muchos artículos dedicados a programar STM32 en un nivel bastante bajo (solo por gestión de registros o CMSIS). Por ejemplo, desde el último: uno , dos , tres . Los artículos son, por supuesto, de muy alta calidad, pero mi opinión subjetiva es que para el desarrollo único de un producto, este enfoque, tal vez, se justifica. Pero para un proyecto de pasatiempo a largo plazo, cuando desea que todo sea hermoso y extensible, este enfoque es de nivel demasiado bajo. Una de las razones de la popularidad de Arduino como plataforma de software, en mi opinión, es que los autores de Arduino han dejado un nivel tan bajo para la arquitectura orientada a objetos. Por lo tanto, decidí ir en la misma dirección y agregar una capa orientada a objetos de bastante alto nivel sobre la biblioteca HAL.
Así, se obtienen tres niveles del programa:
- Las bibliotecas de fabricantes (HAL, FatFS, en el futuro USB-OTG) forman la base
- Mi biblioteca StmPlusPlus se basa en esta base. Incluye un conjunto de clases base (como System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), un conjunto de clases de controladores de dispositivos externos (como SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 y similares), así como clases de servicio como un reproductor asincrónico WAV.
- Basado en la biblioteca StmPlusPlus, la aplicación en sí está siendo construida.
En cuanto al dialecto de la lengua. Si bien estoy algo anticuado, sigo en C ++ 11. Este estándar tiene varias características que son especialmente útiles para desarrollar firmware: clases de enumeración, llamar a constructores con llaves para controlar los tipos de parámetros pasados y contenedores estáticos como std :: array. Por cierto, en Habré hay un artículo maravilloso sobre este tema.
Biblioteca StmPlusPlus
El código completo de la biblioteca se puede ver en github . Aquí daré solo algunos pequeños ejemplos para mostrar la estructura, idea y problemas generados por esta idea.
El primer ejemplo es una clase para sondear periódicamente el estado de un pin (por ejemplo, un botón) y llamar al controlador cuando este estado cambia:
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; };
El constructor define todos los parámetros del botón:
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 manejar tales eventos no es una prioridad, entonces usar interrupciones es claramente superfluo. Por lo tanto, se implementan varios escenarios de presión (por ejemplo, una sola presión o retención) en el procedimiento periódico, que debe llamarse periódicamente desde el código del programa principal. analiza periódicamente el cambio de estado y llama sincrónicamente al controlador virtual onButtonPressed, que debe implementarse en el programa principal:
void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) {
La principal ventaja de este enfoque es la diversidad de lógica y código para detectar un evento de su procesamiento. No se utiliza HAL_GetTick para contar el tiempo, que, debido a su tipo (uint32_t), se restablece por desbordamiento cada 2 ^ 32 milisegundos (cada 49 días). Implementé mi propia clase RealTimeClock, que cuenta milisegundos desde el inicio del programa, o encendí el controlador como uint64_t, que da unos 5 ^ 8 años.
El segundo ejemplo es trabajar con una interfaz de hardware, de las cuales hay varias en el controlador. Por ejemplo, SPI. Desde el punto de vista del programa principal, es muy conveniente seleccionar solo la interfaz deseada (SPI1 / SPI2 / SPI3), y el constructor de la clase configurará todos los demás parámetros que dependen de esta interfaz.
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(); };
Los parámetros de pin y los parámetros de interfaz se almacenan localmente en la clase. Desafortunadamente, elegí una opción de implementación no totalmente exitosa, cuando la configuración de parámetros que depende de una interfaz específica se implementa directamente:
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; }
El mismo esquema implementa los procedimientos enableClock y disableClock, que son poco extensibles y poco portátiles para otros controladores. En este caso, es mejor usar plantillas donde el parámetro de la plantilla es el nombre de la interfaz HAL (SPI1, SPI2, SPI3), los parámetros pin (GPIO_AF5_SPI1) y algo que controla el encendido / apagado del reloj. Hay un artículo interesante sobre este tema, aunque revisa los controladores AVR, que, sin embargo, no hace una diferencia fundamental.
El inicio y el final de la transferencia se controlan mediante dos métodos de inicio / detención:
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; }
Trabajar con interfaz de hardware mediante interrupciones . La clase implementa una interfaz I2S utilizando un controlador DMA. I2S (Inter-IC Sound) es un complemento de hardware y software sobre SPI que, por ejemplo, selecciona la frecuencia del reloj y controla los canales según el protocolo de audio y la velocidad de bits.
En este caso, la clase I2S se hereda de la clase "port", es decir, I2S es un puerto con propiedades especiales. Algunos datos se almacenan en estructuras HAL (más por conveniencia, menos por la cantidad de datos). Algunos datos se transfieren desde el código principal mediante enlaces (por ejemplo, la estructura 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; };
Su constructor establece todos los parámetros estáticos:
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;
El inicio de la transferencia de datos se controla mediante los métodos de inicio, que son responsables de configurar los parámetros del puerto, sincronizar la interfaz, configurar las interrupciones, iniciar el DMA, iniciar la propia interfaz con los parámetros de transmisión especificados.
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; }
El procedimiento de detención hace lo contrario:
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); }
Hay varias características interesantes aquí:
- Las interrupciones usadas en este caso se definen como constantes estáticas. Esto es un inconveniente para la portabilidad a otros controladores.
- Dicha organización del código garantiza que los pines del puerto estén siempre en el estado GPIO_MODE_INPUT cuando no haya transmisión. Esta es una ventaja.
- La prioridad de las interrupciones se transfiere desde el exterior, es decir, hay una buena oportunidad para establecer un mapa de prioridad de interrupciones en un lugar del código principal. Esto también es una ventaja.
- El procedimiento de parada deshabilita el reloj DMA1. En este caso, esta simplificación puede tener consecuencias muy negativas si alguien más continúa usando DMA1. El problema se resuelve mediante la creación de un registro centralizado de consumidores de dichos dispositivos, que será responsable del reloj.
- Otra simplificación: el procedimiento de inicio no lleva la interfaz a su estado original en caso de error (esto es un signo negativo, pero fácilmente reparable). Al mismo tiempo, los errores se registran con más detalle, lo cual es una ventaja.
- Cuando se utiliza esta clase, el código principal debe interceptar las interrupciones SPI2_IRQn y DMA1_Stream4_IRQn y garantizar que se invoquen los controladores correspondientes processI2SInterrupt y processDmaTxInterrupt.
Programa principal
El programa principal está escrito usando la biblioteca descrita anteriormente simplemente:
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);
Aquí inicializamos la biblioteca HAL, configuramos todos los pines del controlador por entrada (GPIO_MODE_INPUT / PULLDOWN) de forma predeterminada. Configuramos la frecuencia del controlador, iniciamos el reloj (incluido el reloj en tiempo real del cuarzo externo). Después de eso, un poco al estilo de Java, creamos una instancia de nuestra aplicación y llamamos a su método de ejecución, que implementa toda la lógica de la aplicación.
En una sección separada, debemos definir todas las interrupciones utilizadas. Como escribimos en C ++, y las interrupciones son cosas del mundo de C, debemos enmascararlas en consecuencia:
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 clase MyApplication declara todos los dispositivos usados, llama a los constructores para todos estos dispositivos y también implementa los controladores de eventos necesarios:
class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler { public: static const size_t INPUT_PINS = 8;
Es decir, de hecho, todos los dispositivos usados se declaran estáticamente, lo que potencialmente conduce a un aumento de la memoria usada, pero simplifica enormemente el acceso a los datos. En el constructor de la clase MyApplication, es necesario llamar a los diseñadores de todos los dispositivos, después de lo cual, para cuando se inicie el procedimiento de ejecución, se inicializarán todos los dispositivos de microcontrolador utilizados:
MyApplication::MyApplication () :
Como ejemplo, el controlador de eventos para hacer clic en un botón que inicia / detiene la reproducción de un archivo 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()); } } }
Y, por último, el método de ejecución principal completa la configuración de los dispositivos (por ejemplo, establece MyApplication como un controlador de eventos) e inicia un bucle sin fin, donde se refiere periódicamente a los dispositivos que requieren atención periódica:
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 poco de experimentación
— . — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.
. github . - , Mouser ( ). 37 . . , STM Olimex, .
. , :
- ( ). , , . : 4 8 . PLL, .
- , . 47 μF . , .
- SWD . - , . .
- . SMD , . 3 .
La documentación
github GPL v3:
Gracias por su atencion!