Raspberry Pi外形尺寸的STM32F4调试板

图片 下午好,亲爱的哈布罗维特人! 我想向公众介绍我的项目-一个基于STM32的小型调试板,但采用Raspberry Pi尺寸。 它与其他调试板的不同之处在于,其几何结构与Raspberry Pi外壳兼容,并且具有作为无线调制解调器的ESP8266模块。 以及用于micro-SD卡和立体声放大器的连接器形式的不错补充。 为了利用所有这些财富,我开发了一个高级库和一个演示程序(在C ++ 11中)。 在本文中,我想详细描述该项目的硬件和软件部分。


谁可以从这个项目中受益? 可能只适用于那些想自己焊接该板的人,因为即使是小规模生产,我也不会考虑任何选择。 这是纯粹的爱好。 我认为,该委员会涵盖了使用WiFi和声音的小型家用工艺品框架中可能出现的相当广泛的任务。


首先,我将尝试回答为什么这就是所有问题。 该项目的主要动机如下:


  • 选择STM32平台是出于纯粹的美学考虑-我喜欢性价比,再加上广泛的外围设备,以及控制器制造商提供的大型便捷开发生态系统(sw4stm,cubMX,HAL库)。
  • 当然,控制器制造商本身(Discovery,Nucleo)以及第三方制造商(例如Olimex)都有许多调试板。 但是,至少对我来说,要在家中重复使用其中许多产品是有问题的。 在我的版本中,我们有一个简单的两层拓扑结构和一些便于手动焊接的组件。
  • 对于他们的设备,我希望拥有合适的外壳,以掩盖内部电子设备的低质量。 至少有两个流行的平台,其中有大量最多样化的案例:Arduino和Raspberry Pi。 就连接器的切口位置而言,第二个对我来说似乎更方便。 因此,作为木板几何形状的捐助者,我选择了它。
  • 我在板上选择的控制器具有USB,SDIO,I2S,网络。 另一方面,这些相同的接口对于家庭爱好平台也很有用。 因此,除了带有标准线束的控制器之外,我还添加了USB连接器,SD卡,音频路径(数模转换器和放大器)以及基于ESP8266的无线模块。

电路及元件


在我看来,具有以下特征和组件的非常不错的电路板:


  • STM32F405RG控制器:具有数学协处理器的ARM 32位Cortex-M4,频率高达168 MHz,1 Mb闪存,196 Kb RAM。
    二手控制器引脚
    控制器绑定
  • SWD连接器,用于对控制器进行编程(6针)。
  • 重置按钮以重新启动。
  • 三色LED。 一方面,三个控制器引脚丢失。 另一方面,由于GPIO连接器上的触点有限,它们仍然会丢失,并且对于调试此类LED来说,这非常有用。
  • 高频HSE(核心时钟为16 MHz)和低频LSE(实时时钟为32.7680 kHz)石英。
  • 间距为2.54 mm的GPIO引脚与原型板兼容。
  • 我放置了5伏电源连接器,而不是Raspberry Pi的3.5毫米音频插孔。 乍一看,这个决定是有争议的。 但是有优点。 可以选择通过USB连接器供电(下面有详细信息),但这对于调试电路是一个不好的选择,因为在这种情况下,刻录计算机的USB端口之前的时间可能很短。

电源电路


  • 迷你USB端口 一方面,它通过保护芯片STF203-22.TCT连接到控制器的USB-OTG端口。 另一方面,VBUS电源引脚连接到GPIO连接器。 如果将其连接到+ 5V引脚,则将通过USB端口为开发板供电。

USB电路


  • 带有线束的micro-SD存储卡连接器: 47kΩ上拉电阻,电源管理晶体管( P沟道MOSFET BSH205 )和电源线上的绿色小LED。

Micro SD卡概述


晶体管栅极连接到控制器的PA15引脚。 这是JTDI控制器的系统触点,其有趣之处在于,它在初始位置被配置为具有高电平(上拉)电压的输出。 由于使用SWD代替JTAG进行编程,因此该触点保持空闲状态,并且可以用于其他目的,例如控制晶体管。 这很方便-板上供电时,存储卡会断电;要启用该功能,您需要在PA15引脚上施加一个低电平。


  • 基于UDA1334的数模转换器 。 该芯片不需要外部时钟信号,因此便于使用。 数据通过I2S总线传输。 另一方面,数据表建议使用多达47个F的5个极性电容器。 在这种情况下,尺寸很重要。 事实证明,购买的最小的是钽,尺寸为1411,甚至都不便宜。 但是,我将在下面详细介绍价格。 对于模拟电源,使用其自己的线性稳定器,数字部分的电源通过双晶体管打开/关闭。

DAC电路


  • 基于两个31AP2005芯片的两通道放大器。 它们的主要优点是捆扎部件数量少(仅电源滤波器和输入滤波器)。 音频输出-4个平台,间距为2.54毫米。 就我自己而言,我还没有决定最好的选择-这样的临时选择,或者像在覆盆子上那样,使用3.5毫米插头。 通常,耳机与3.5毫米相关,在我们的案例中,我们正在谈论连接扬声器。

放大电路


  • 最后一个模块是带有捆扎带(电源,编程插座)的ESP11披肩,作为WiFi调制解调器。 UART板的结论连接到控制器,并同时输出到外部连接器(用于直接从端子和编程中使用该板)。 有一个电源开关(永久外部或由微控制器控制)。 有一个额外的LED指示电源,还有一个“ FLASH”连接器,用于将板子设置为编程模式。

ESP电路


当然,ESP8266本身是一个不错的控制器,但在性能和外围设备方面仍然不如STM32F4。 是的,这个模块的价格以及它的价格暗示着这是它的哥哥泄漏的调制解调器单元。 该模块由USRT使用文本AT协议控制。


几张照片:


准备ESP11模块


ESP8266是众所周知的东西。 我确信很多人已经熟悉它,因此这里没有多余的详细指南。 由于将ESP11模块连接到开发板上的原理图功能,对于那些想要更改其固件的人,我只给他一个简要指南:


  • 我将使用esptool实用程序与ESP一起使用。 esptool与制造商提供的标准实用程序不同,它与平台无关。
  • 首先,使用ESP-PWR跳线打开外部电源模式(我们关闭触点1和2),然后通过任何USART-USB适配器将模块连接到计算机。 适配器连接到GRD / RX / TD引脚。 我们为电路板供电:
  • 我们确保适配器被操作系统识别。 在我的示例中,我使用基于FT232的适配器,因此在设备列表中,它应该显示为FT232串行(UART)IC:
    > 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本身的闪存数量不同。 在实践中,在同一ESP11模块中,我同时遇到512 KB(4 Mbit)和1 MB(8 Mbit)。 因此,首先要检查的是模块使用的实例中有多少内存。 关闭开发板上的电源,然后将模块置于编程模式,关闭跳线“ FLASH”:


  • 打开电源,使用以下参数运行esptool

 > 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报告说,在这种情况下,我们正在处理具有1 MB内存的模块。
  • 对于1 MB的版本,可以使用最新的固件,例如ESP8266 AT Bin V1.6.1 。 但是它不适用于4 Mbit的版本,您需要使用较旧的版本,例如this 。 固件由几个文件组成,每个文件的起始地址在官方文档ESP8266 AT指令集中进行介绍 。 这些起始地址用作esptool实用程序的参数。 例如,对于具有1 MB的模块,esptool的参数应如下所示(所有必需的文件必须首先从固件档案中提取并收集在工作目录中)
     > 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 
  • 我们为开发板供电,并使用指定的参数运行esptool。
  • 完成脚本后,关闭开发板上的电源,打开“ FLASH”跳线,然后打开微控制器的电源控制。 该模块已准备就绪,可以开始工作了。

软体类


github上有一个测试程序。 她执行以下操作:


  • 以最大频率(168 MHz)显示控制器
  • 激活实时时钟
  • 激活SD卡并从中读取网络配置。 FatFS库用于处理文件系统。
  • 建立与指定WLAN的连接
  • 连接到指定的NTP服务器并从中请求当前时间。 带领时钟。
  • 监视几个指定端口的状态。 如果其状态已更改,则将文本消息发送到指定的TCP服务器。
  • 当您单击外部按钮时,它将从SD卡读取指定的* .wav文件,并以异步模式(使用DMA控制器的I2S)播放该文件。
  • ESP11的工作也以异步模式实现(到目前为止没有DMA,仅在中断时)
  • 通过USART1登录(引脚PB6 / PB7)
  • 当然,LED也会闪烁。

在Habré上,有许多文章专门针对较低级别的STM32进行编程(仅通过寄存器管理或CMSIS)。 例如,从相对较后的位置开始: 。 这些文章当然是非常高质量的,但是我的主观意见是,对于产品的一次性开发,这种方法也许是合理的。 但是对于一个长期的爱好项目,当您希望所有内容都美观且可扩展时,这种方法太底层了。 在我看来,Arduino成为软件平台之所以受欢迎,原因之一是Arduino的作者对面向对象的体系结构没有这么低的要求。 因此,我决定朝同一方向发展,并在HAL库上添加了一个相当高级的面向对象层。


因此,获得了程序的三个级别:


  • 制造商库(HAL,FatFS,以后称为USB-OTG)构成基础
  • 我的StmPlusPlus库基于此基础。 它包括一组基本类(例如System,IOPort,IOPin,Timer,RealTimeClock,Usart,Spi,I2S),一组外部设备的驱动程序类(例如SdCard,Esp11,DcfReceiver,Dac_MCP49x1,AudioDac_UDA1334等),以及服务类,例如WAV异步播放器。
  • 基于StmPlusPlus库,正在构建应用程序本身。

至于语言的方言。 虽然我有些过时,但我仍然使用C ++ 11。 该标准具有一些对开发固件特别有用的功能:枚举类,使用花括号调用构造函数以控制传递的参数的类型以及静态容器(如std :: array)。 顺便说一句,在哈布雷(Habré)上有一篇关于该主题的精彩文章


StmPlusPlus库


完整的库代码可以在github上查看。 在这里,我将仅给出一些小示例,以说明该构想产生的结构,构想和问题。


第一个示例是用于定期轮询引脚(例如按钮)状态并在此状态更改时调用处理程序的类:


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

构造函数定义所有按钮参数:


 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 } 

如果处理此类事件不是优先事项,那么使用中断显然是多余的。 因此,在周期性过程中会实现各种按压场景(例如,一次按压或按住),应从主程序代码中定期调用这些情景。 定期分析状态变化并同步调用onButtonPressed虚拟处理程序,该处理程序应在主程序中实现:


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

这种方法的主要优点是用于从事件处理中检测事件的逻辑和代码的多样性。 不是用于计数时间的HAL_GetTick,由于其类型(uint32_t),每2 ^ 32毫秒(每49天)通过溢出对其进行重置。 我实现了我自己的类RealTimeClock,该类从程序开始算起就是毫秒,或者打开了像uint64_t这样的控制器,这大约需要5 ^ 8年的时间。


第二个示例是使用硬件接口,控制器中有多个硬件接口。 例如,SPI。 从主程序的角度来看,仅选择所需的接口(SPI1 / SPI2 / SPI3)非常方便,并且依赖于此接口的所有其他参数将由类构造函数配置。


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

引脚参数和接口参数存储在本地类中。 不幸的是,当直接根据特定接口实现参数设置时,我选择了一个并非完全成功的实现选项:


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

相同的方案实现了enableClock和disableClock过程,它们的扩展性很差,并且对其他控制器的移植性也很差。 在这种情况下,最好使用模板,其中模板参数为HAL接口名称(SPI1,SPI2,SPI3),引脚参数(GPIO_AF5_SPI1)以及用于控制时钟的开/关的模板。 关于此主题, 一篇有趣的文章,尽管它回顾了AVR控制器,但是这并没有根本的不同。


传输的开始和结束由两种开始/停止方法控制:


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

使用中断使用硬件接口 。 该类使用DMA控制器实现I2S接口。 I2S(IC间声音)是SPI的硬件软件附加组件,例如它本身根据音频协议和比特率执行时钟选择和通道控制。


在这种情况下,I2S类是从“端口”类继承的,即I2S是具有特殊属性的端口。 某些数据存储在HAL结构中(为方便起见加号,为数据量减号)。 一些数据是通过链接从主代码传输的(例如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; }; 

它的构造函数设置所有静态参数:


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

数据传输的开始由启动方法控制,启动方法负责配置端口参数,为接口计时,配置中断,启动DMA,使用指定的传输参数启动接口本身。


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

停止过程执行相反的操作:


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

这里有几个有趣的功能:


  • 在这种情况下,使用的中断被定义为静态常量。 这是对其他控制器的可移植性的减法。
  • 这样的代码组织确保了在无传输时端口引脚始终处于GPIO_MODE_INPUT状态。 这是一个加号。
  • 中断的优先级是从外部传递的,也就是说,有很好的机会在主代码的一个位置设置中断优先级映射。 这也是一个加号。
  • 停止过程将禁用DMA1时钟。 在这种情况下,如果其他人继续使用DMA1,这种简化可能会带来非常不利的后果。 通过创建此类设备使用者的集中寄存器来解决该问题,该寄存器将负责计时。
  • 另一个简化-启动过程不会在出现错误的情况下将接口恢复为原始状态(这是一个负值,但易于修复)。 同时,更详细地记录错误,这是一个加号。
  • 使用此类时,主代码应拦截SPI2_IRQn和DMA1_Stream4_IRQn中断,并确保调用相应的processI2SInterrupt和processDmaTxInterrupt处理程序。

主程序


使用上述库很简单地编写了主程序:


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

在这里,我们初始化HAL库,默认情况下通过输入(GPIO_MODE_INPUT / PULLDOWN)配置所有控制器引脚。 设置控制器的频率,启动时钟(包括来自外部石英的实时时钟)。 然后,以Java的风格,创建一个应用程序实例并调用其run方法,该方法实现了所有应用程序逻辑。


在单独的部分中,我们必须定义所有使用的中断。 由于我们使用C ++编写,并且中断是C语言世界中的东西,因此我们需要相应地屏蔽它们:


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

MyApplication类声明所有使用的设备,为所有这些设备调用构造函数,并实现必要的事件处理程序:


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

也就是说,实际上,所有用过的设备都是静态声明的,这有可能导致用过的内存增加,但大大简化了对数据的访问。 在MyApplication类的构造函数中,有必要调用所有设备的设计器,然后在启动运行过程之前,将初始化所有使用的微控制器设备:


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

例如,用于单击开始/停止播放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()); } } } 

最后,main run方法完成设备的配置(例如,将MyApplication设置为事件处理程序),并启动一个无限循环,在该循环中,它周期性地引用需要定期关注的设备:


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

一点实验


— . — 168 MHz. , , 172 MHz 180 MHz, , , MCO. , USART I2S, , , HAL.



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



. , :


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

该文件


github GPL v3:



感谢您的关注!

Source: https://habr.com/ru/post/zh-CN413101/


All Articles