Boa tarde, queridos Khabrovites! Quero apresentar meu projeto ao público - uma pequena placa de depuração baseada no STM32, mas com o fator de forma do Raspberry Pi. Difere de outras placas de depuração, pois possui uma geometria compatível com os gabinetes Raspberry Pi e um módulo ESP8266 como modem sem fio. E também adições agradáveis na forma de um conector para um cartão micro-SD e um amplificador estéreo. Para tirar proveito de toda essa riqueza, desenvolvi uma biblioteca de alto nível e um programa de demonstração (em C ++ 11). No artigo, quero descrever em detalhes as partes de hardware e software deste projeto.
Quem pode se beneficiar desse projeto? Provavelmente, apenas para aqueles que desejam soldar esta placa pessoalmente, pois não considero nenhuma opção, mesmo para produção em pequena escala. Este é um hobby puro. Na minha opinião, o conselho abrange uma gama bastante ampla de tarefas que podem surgir no âmbito de pequenos trabalhos domésticos usando Wi-Fi e som.
Para começar, tentarei responder à pergunta por que isso é tudo. Os principais motivadores deste projeto são os seguintes:
- A escolha da plataforma STM32 deve-se a considerações puramente estéticas - eu gosto da relação preço / desempenho, além de uma ampla gama de periféricos, além de um amplo e conveniente ecossistema de desenvolvimento do fabricante do controlador (biblioteca sw4stm, cubeMX, HAL).
- Obviamente, existem muitas placas de depuração do próprio fabricante do controlador (Discovery, Nucleo), bem como de fabricantes de terceiros (por exemplo, Olimex). Mas repetir muitos deles em casa em seu formato é problemático para mim, pelo menos. Na minha versão, temos uma topologia simples de duas camadas e componentes convenientes para a soldagem manual.
- Para os dispositivos deles, quero ter caixas decentes para mascarar a baixa qualidade dos eletrônicos internos. Existem pelo menos duas plataformas populares para as quais há um grande número de casos mais diversos: Arduino e Raspberry Pi. O segundo deles me pareceu mais conveniente em termos da localização dos recortes dos conectores. Portanto, como doador da geometria do painel, eu o escolhi.
- O controlador que eu selecionei a bordo tem USB, SDIO, I2S, rede. Por outro lado, essas mesmas interfaces também são úteis para uma plataforma de hobby em casa. É por isso que, além do controlador com um chicote padrão, adicionei um conector USB, um cartão SD, um caminho de áudio (conversor e amplificador digital-analógico) e um módulo sem fio baseado no ESP8266.
Circuito e componentes
Parece-me que uma placa muito bonita com as seguintes características e componentes acabou:
- Controlador STM32F405RG : ARM Cortex-M4 de 32 bits com um coprocessador matemático, frequência de até 168 MHz, memória flash de 1 Mb, 196 Kb de RAM.


- Conector SWD para programar o controlador (6 pinos).
- Redefinir para reiniciar.
- LED de três cores. Por um lado, três pinos do controlador são perdidos. Por outro lado, eles ainda se perderiam devido aos contatos limitados nos conectores GPIO e, para depurar esse LED, a coisa é muito útil.
- HSE de alta frequência (16 MHz para clock principal) e LSE de baixa frequência (32,7680 kHz para clock em tempo real).
- Os pinos GPIO com passo de 2,54 mm são compatíveis com placas de prototipagem.
- No lugar da tomada de áudio de 3,5 mm do Raspberry Pi, posicionei o conector de 5 volts. À primeira vista, a decisão é controversa. Mas existem profissionais. A energia do conector USB está opcionalmente presente (detalhes abaixo), mas essa é uma opção ruim para depuração do circuito, pois o tempo antes de gravar a porta USB do computador nesse caso pode ser bastante curto.

- Porta Mini-USB Por um lado, é conectado através do chip de proteção STF203-22.TCT à porta USB-OTG do controlador. Por outro lado, o pino de força do VBUS está conectado ao conector GPIO. Se você conectá-lo ao pino + 5V, a placa será alimentada a partir da porta USB.

- Conector para cartão de memória micro-SD com chicote: resistores pull-up de 47 kΩ, transistor de gerenciamento de energia ( MOSFET BSH205 de canal P ) e um pequeno LED verde na linha de energia.

A porta do transistor está conectada ao pino PA15 do controlador. Este é o contato do sistema do controlador JTDI, o que é interessante, pois na posição inicial ele é configurado como uma saída com um alto nível (tração) de tensão. Como o SWD é usado em vez do JTAG para programação, esse contato permanece livre e pode ser usado para outros fins, por exemplo, controlar um transistor. Isso é conveniente - quando a energia é aplicada à placa, o cartão de memória é desenergizado; para habilitá-lo, é necessário aplicar um nível baixo ao pino PA15.
- Conversor digital-analógico baseado em UDA1334 . Este chip não precisa de um sinal de relógio externo, o que facilita seu uso. Os dados são transmitidos pelo barramento I2S. Por outro lado, a Folha de dados recomenda o uso de até 5 capacitores polares a 47 μF. Tamanho é importante neste caso. Os menores que acabaram sendo comprados são o tântalo, com um tamanho de 1411, que nem é barato. No entanto, escreverei sobre o preço com mais detalhes abaixo. Para energia analógica, é usado seu próprio estabilizador linear, a energia da parte digital é ligada / desligada por um transistor duplo.

- Amplificador de dois canais com base em dois chips 31AP2005 . Sua principal vantagem é um pequeno número de componentes de cintas (apenas filtros de energia e um filtro de entrada). Saída de áudio - 4 plataformas com um passo de 2,54 mm. Para mim, ainda não decidi o que é melhor - uma opção improvisada ou, como em uma framboesa, um plugue de 3,5 mm. Como regra, 3,5 mm estão associados aos fones de ouvido. No nosso caso, estamos falando sobre conectar alto-falantes.

- O último módulo é um xale ESP11 com uma cinta (alimentação, soquete de programação) como um modem WiFi. As conclusões da placa UART são conectadas ao controlador e emitidas simultaneamente para um conector externo (para trabalhar com a placa diretamente do terminal e da programação). Há um interruptor de energia (externo permanente ou controle de um microcontrolador). Há um LED adicional para indicar energia e um conector "FLASH" para colocar a placa no modo de programação.

Obviamente, o próprio ESP8266 é um bom controlador, mas ainda é inferior ao STM32F4, tanto em desempenho quanto em periféricos. Sim, e o tamanho do preço deste módulo indicava que era uma unidade de modem derramada para seu irmão mais velho. O módulo é controlado pela USRT usando um protocolo AT de texto.
Algumas fotos:


Preparando o módulo ESP11
ESP8266 é uma coisa bem conhecida. Tenho certeza de que muitos já estão familiarizados com isso, portanto um guia detalhado será supérfluo aqui. Devido aos recursos esquemáticos de conectar o módulo ESP11 à placa, darei apenas um breve guia para quem deseja alterar seu firmware:

- Ligue a energia, execute esptool com os seguintes 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...
De software
Existe um programa de teste no github . Ela faz o seguinte:
- exibe o controlador na frequência máxima (168 MHz)
- ativa o relógio em tempo real
- ativa o cartão SD e lê a configuração de rede a partir dele. A biblioteca FatFS é usada para trabalhar com o sistema de arquivos.
- estabelece uma conexão com a WLAN especificada
- conecta ao servidor NTP especificado e solicita a hora atual dele. Leva o relógio.
- monitora o status de várias portas especificadas. Se o status deles foi alterado, envia uma mensagem de texto para o servidor TCP especificado.
- quando você clica no botão externo, ele lê o arquivo * .wav especificado do cartão SD e o reproduz no modo assíncrono (I2S usando o controlador DMA).
- o trabalho com o ESP11 também é implementado no modo assíncrono (até agora sem DMA, apenas com interrupções)
- efetua login via USART1 (pinos PB6 / PB7)
- e, claro, o LED pisca.
Em Habré, havia muitos artigos dedicados à programação do STM32 em um nível bastante baixo (apenas pelo gerenciamento de registros ou CMSIS). Por exemplo, do relativamente último: um , dois , três . Os artigos são, obviamente, de alta qualidade, mas minha opinião subjetiva é de que, para o desenvolvimento único de um produto, essa abordagem talvez se justifique. Porém, para um projeto de hobby de longo prazo, quando você deseja que tudo seja bonito e extensível, essa abordagem é de nível muito baixo. Uma das razões para a popularidade do Arduino como plataforma de software, na minha opinião, é que os autores do Arduino deixaram um nível tão baixo para a arquitetura orientada a objetos. Portanto, decidi seguir na mesma direção e adicionar uma camada orientada a objetos de nível bastante alto sobre a biblioteca HAL.
Assim, são obtidos três níveis do programa:
- As bibliotecas de fabricantes (HAL, FatFS, no futuro USB-OTG) formam a base
- Minha biblioteca StmPlusPlus é baseada nessa base. Inclui um conjunto de classes base (como System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), um conjunto de classes de drivers de dispositivos externos (como SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 e similares), além de classes de serviço, como um player assíncrono WAV.
- Com base na biblioteca StmPlusPlus, o próprio aplicativo está sendo construído.
Quanto ao dialeto do idioma. Enquanto eu sou um pouco antiquado, permaneço em C ++ 11. Esse padrão possui vários recursos que são especialmente úteis para o desenvolvimento de firmware: classes enum, chamando construtores com chaves para controlar os tipos de parâmetros passados e recipientes estáticos como std :: array. A propósito, em Habré, há um artigo maravilhoso sobre esse assunto.
Biblioteca StmPlusPlus
O código completo da biblioteca pode ser visualizado no github . Aqui darei apenas alguns pequenos exemplos para mostrar a estrutura, a idéia e os problemas gerados por essa idéia.
O primeiro exemplo é uma classe para pesquisar periodicamente o estado de um pino (por exemplo, um botão) e chamar o manipulador quando esse estado muda:
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; };
O construtor define todos os parâmetros do botão:
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} {
Se o tratamento desses eventos não for uma prioridade, o uso de interrupções será claramente supérfluo. Portanto, vários cenários de prensagem (por exemplo, uma única pressão ou retenção) são implementados no procedimento periódico, que deve ser chamado periodicamente a partir do código principal do programa. periodic analisa a mudança de estado e chama de forma síncrona o manipulador virtual onButtonPressed, que deve ser implementado no programa principal:
void Button::periodic () { if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) {
A principal vantagem dessa abordagem é a diversidade de lógica e código para detectar um evento de seu processamento. Não é o HAL_GetTick usado para o tempo de contagem, que, devido ao seu tipo (uint32_t), é redefinido por estouro a cada 2 ^ 32 milissegundos (a cada 49 dias). Eu implementei minha própria classe RealTimeClock, que conta milissegundos desde o início do programa, ou liguei o controlador como uint64_t, que fornece cerca de 5 ^ 8 anos.
O segundo exemplo está trabalhando com uma interface de hardware, da qual existem várias no controlador. Por exemplo, SPI. Do ponto de vista do programa principal, é muito conveniente selecionar apenas a interface desejada (SPI1 / SPI2 / SPI3), e todos os outros parâmetros que dependem dessa interface serão configurados pelo construtor da 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(); };
Os parâmetros de pinos e os parâmetros de interface são armazenados localmente na classe. Infelizmente, escolhi uma opção de implementação não totalmente bem-sucedida, quando as configurações de parâmetros, dependendo de uma interface específica, são implementadas diretamente:
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; }
O mesmo esquema implementa os procedimentos enableClock e disableClock, que são pouco extensíveis e pouco portáveis para outros controladores. Nesse caso, é melhor usar modelos nos quais o parâmetro do modelo é o nome da interface HAL (SPI1, SPI2, SPI3), parâmetros de pinos (GPIO_AF5_SPI1) e algo que controla o relógio on / off. Há um artigo interessante sobre esse tópico, embora ele revise os controladores AVR, que, no entanto, não fazem uma diferença fundamental.
O início e o fim da transferência são controlados por dois métodos de início / parada:
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; }
Trabalhe com a interface de hardware usando interrupções . A classe implementa uma interface I2S usando um controlador DMA. O I2S (Inter-IC Sound) é um complemento de software de hardware para SPI, que, por exemplo, realiza a seleção do relógio e o controle do canal, dependendo do protocolo de áudio e da taxa de bits.
Nesse caso, a classe I2S é herdada da classe “port”, ou seja, I2S é uma porta com propriedades especiais. Alguns dados são armazenados em estruturas HAL (mais por conveniência, menos a quantidade de dados). Alguns dados são transferidos do código principal por links (por exemplo, a estrutura 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; };
Seu construtor define todos os 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;
O início da transferência de dados é controlado pelos métodos de início, responsáveis pela configuração dos parâmetros da porta, cronometrando a interface, definindo interrupções, iniciando o DMA, iniciando a própria interface com os parâmetros de transmissão 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; }
O procedimento de parada faz o oposto:
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); }
Existem vários recursos interessantes aqui:
- As interrupções usadas neste caso são definidas como constantes estáticas. Isso é menos a portabilidade para outros controladores.
- Essa organização do código garante que os pinos da porta estejam sempre no estado GPIO_MODE_INPUT quando não houver transmissão. Isto é uma vantagem.
- A prioridade de interrupções é transferida de fora, ou seja, existe uma boa oportunidade para definir um mapa de prioridades de interrupção em um local do código principal. Isso também é uma vantagem.
- O procedimento de parada desativa o relógio do DMA1. Nesse caso, essa simplificação pode ter consequências muito negativas se outra pessoa continuar usando o DMA1. O problema é resolvido com a criação de um registro centralizado dos consumidores desses dispositivos, que será responsável pelo relógio.
- Outra simplificação - o procedimento de inicialização não leva a interface ao seu estado original em caso de erro (este é um sinal de menos, mas facilmente corrigível). Ao mesmo tempo, os erros são registrados com mais detalhes, o que é uma vantagem.
- Ao usar essa classe, o código principal deve interceptar as interrupções SPI2_IRQn e DMA1_Stream4_IRQn e garantir que os manipuladores processI2SInterrupt e processDmaTxInterrupt correspondentes sejam chamados.
Programa principal
O programa principal é escrito usando a biblioteca descrita acima de maneira bastante simples:
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);
Aqui, inicializamos a biblioteca HAL, configuramos todos os pinos do controlador por entrada (GPIO_MODE_INPUT / PULLDOWN) por padrão. Defina a frequência do controlador, inicie o relógio (incluindo um relógio em tempo real de um quartzo externo). Depois disso, um pouco no estilo de Java, criamos uma instância de nosso aplicativo e chamamos seu método run, que implementa toda a lógica do aplicativo.
Em uma seção separada, devemos definir todas as interrupções usadas. Como escrevemos em C ++ e as interrupções são coisas do mundo de C, precisamos mascará-las de acordo:
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); } ... }
A classe MyApplication declara todos os dispositivos usados, chama construtores para todos esses dispositivos e também implementa os manipuladores de eventos necessários:
class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler { public: static const size_t INPUT_PINS = 8;
Na verdade, todos os dispositivos usados são declarados estaticamente, o que potencialmente leva a um aumento na memória usada, mas simplifica bastante o acesso aos dados. No construtor da classe MyApplication, é necessário chamar os projetistas de todos os dispositivos, após os quais, quando o procedimento de execução for iniciado, todos os dispositivos de microcontroladores usados serão inicializados:
MyApplication::MyApplication () :
Como exemplo, o manipulador de eventos para clicar em um botão que inicia / para a reprodução de um arquivo 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()); } } }
E, finalmente, o método principal de execução conclui a configuração dos dispositivos (por exemplo, define MyApplication como manipulador de eventos) e inicia um loop infinito, onde se refere periodicamente aos dispositivos que requerem atenção 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); } } }
Um pouco de experimentação
— . — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.
. github . - , Mouser ( ). 37 . . , STM Olimex, .
. , :
- ( ). , , . : 4 8 . PLL, .
- , . 47 μF . , .
- SWD . - , . .
- . SMD , . 3 .
A documentação
github GPL v3:
Obrigado pela atenção!