Placa de depuração STM32F4 no fator de forma do Raspberry Pi

imagem 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.
    Pinos controladores usados
    Ligação do controlador
  • 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.

Circuito de potência


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

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

Contorno do cartão Micro SD


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.

Circuito DAC


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

Circuito amplificador


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

Circuito ESP


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:


  • Vou usar o utilitário esptool para trabalhar com o ESP. Diferentemente do utilitário padrão do fabricante, o esptool é independente da plataforma.
  • Para começar, ative o modo de energia externa com o jumper ESP-PWR (fechamos os contatos 1 e 2) e conecte o módulo ao computador através de qualquer adaptador USART-USB. O adaptador se conecta aos pinos GRD / RX / TD. Fornecemos energia à placa:
  • Garantimos que o adaptador seja reconhecido pelo sistema operacional. No meu exemplo, eu uso um adaptador baseado no FT232, portanto, com a lista de dispositivos, ele deve estar visível como IC serial do FT232 (UART):
    > lsusb ... Bus 001 Device 010: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 009: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC ... 
  • Os próprios ESP8266 diferem na quantidade de memória flash. Na prática, no mesmo módulo ESP11, me deparei com 512 KB (4 Mbit) e 1 MB (8 Mbit). Portanto, a primeira coisa a verificar é quanta memória existe na instância usada do módulo. Desligue a alimentação da placa e coloque o módulo no modo de programação, fechando o jumper "FLASH":


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

  • A esptool relata que, neste caso, estamos lidando com um módulo com 1 MB de memória.
  • Para a versão com 1 MB, você pode usar o firmware mais recente, por exemplo, ESP8266 AT Bin V1.6.1 . Mas não é adequado para a versão de 4 Mbit, para a qual você precisa usar algo mais antigo, por exemplo, este . O firmware consiste em vários arquivos, os endereços iniciais de cada arquivo são indicados no documento oficial ESP8266 AT Conjunto de instruções . Esses endereços de início são usados ​​como parâmetros do utilitário esptool. Por exemplo, para um módulo com 1 MB, os parâmetros do esptool terão a seguinte aparência (todos os arquivos necessários devem ser extraídos primeiro do arquivo de firmware e coletados no diretório de trabalho)
     > 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 
  • Nós fornecemos energia para a placa, execute esptool com os parâmetros especificados.
  • Após concluir o script, desligue a energia da placa, abra o jumper "FLASH", ligue o controle de energia do microcontrolador. O módulo está pronto para o trabalho.

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} { // empty } 

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

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. 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; } /* 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; } 

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

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

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

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

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!

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


All Articles