- PVSM.RU - https://www.pvsm.ru -
Добрый день, уважаемыее! Хочу представить общественности мой проект — небольшая отладочная плата на базе STM32, но в форм-факторе Raspberry Pi. От других отладочных плат она отличается тем, что имеет совместимую с корпусами от Raspberry Pi геометрию и наличие ESP8266-модуля в качестве беспроводного модема. А также приятные дополнения в виде разъёма для micro-SD карты и стерео-усилителя. Для использования всего этого богатства я разработал высокоуровневую библиотеку и демонстрационную программу (на C++11). В статье я хочу подробно описать как аппаратную, так и программную части этого проекта.
Кому этот проект может быть полезен? Наверное, только тем, кто захочет спаять эту плату сам, так как никакие варианты даже мелкосерийного производства я не рассматриваю. Это чисто хобби. На мой взгляд, плата покрывает достаточно широкий спектр задач, которые могут возникнуть в рамках небольших домашних поделок, использующих WiFi и звук.
Для начала, попытаюсь ответить на вопрос, зачем это все. Основные мотиваторы этого проекта выглядят так:
Как мне кажется, получилась достаточно симпатичная плата со следующими характеристиками и компонентами:
ESP8266 — вещь известная. Я уверен, что многие с ней уже знакомы, поэтому подробное руководство будет здесь лишним. В силу схематических особенностей подключения модуля ESP11 к плате, приведу только краткое руководство для тех, кто желает поменять его прошивку:
> 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
...
> 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.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
На github [14] находится тестовая программа. Она делает следующее:
На Хабре было много статей, посвящённых программированию STM32 на достаточно низком уровне (только управлением регистров или CMSIS). Например, из относительно последних: раз [15], два [16], три [17]. Статьи, безусловно, очень качественные, но моё субъективное мнение — для разовой разработки какого-либо продукта этот подход, быть может, себя и оправдывает. Но вот для длительного хобби-проекта, когда хочется, чтобы всё было красиво и расширяемо, этот подход уж слишком низкоуровневый. Одна из причин популярности Ардуино именно как программной платформы, на мой взгляд, заключается в том, что авторы Ардуино ушли с такого низкого уровня на объектно-ориентированную архитектуру. Поэтому я решил пойти в этом же направлении и надстроить над библиотекой HAL достаточно высокоуровневую объектно-ориентированную прослойку.
Таким образом, получается три уровня программы:
Что касается диалекта языка. Пока я несколько старомоден — остаюсь на C++11. Этот стандарт имеет несколько фишек, особенно полезных для разработки встроенного ПО: классы-перечисления (enum class), вызов конструкторов при помощи фигурных скобок для контроля типов передаваемых параметров, статические контейнеры типа std::array. Кстати, на Хабре есть замечательная статья [18] на эту тему.
Полный код библиотеки можно посмотреть на github [19]. Здесь же я приведу только несколько небольших примеров, чтобы показать структуру, идею и проблемы, этой идеей порождённые.
Первый пример — класс для периодического опроса состояния пина (например, кнопки) и вызова обработчика при изменении этого состояния:
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
}
Если обработка таких событий не является приоритетной задачей, то использование прерываний здесь явно лишнее. Поэтому различные сценарии нажатия (например, одиночное нажатие или удержание) реализованы в процедуре periodic, которая должна периодически вызываться из основного кода программы. periodic анализирует изменение состояния и синхронно вызывает виртуальный обработчик 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), и что-то, что управляет включением/выключением тактирования. Здесь [20] есть интересная статья по этой теме, хотя в ней рессматриваются контроллеры AVR, что, впрочем, принципиальной разницы не имеет.
Начало и окончание передачи контролируются двумя методами start/stop:
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;
}
Работа с аппаратным интерфейсом с использованием прерываний. Класс реализует I2S интерфейс с использованием DMA-контроллера. В данном случае, он наследуется от класса «порт», то есть 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;
}
Начало передачи данных контролируются методам start, который отвечают за настройку параметров порта, тактирование интерфейса, настройку прерываний, старт 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;
}
Процедура stop делает всё наоборот:
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);
}
Здесь есть несколько интересных особенностей:
Основная программа пишется с использованием вышеописанной библиотеки достаточно просто:
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 необходимо вызвать конструкторы всех устройств, после чего, к моменту запуска процедуры run, все используемые устройства микроконтроллера будут инициализированы:
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());
}
}
}
Ну и, наконец, основной метод 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 [21] есть список всех компонент платы. Чтобы получить хоть какую-то цифру, я скрупулёзно собрал цены всех компонент из этого списка с сайта Mouser [22] (не сочтите за рекламу). На получившуюся цифру в 37 Евро без слёз смотреть не получается. К ней нужно ещё прибавить стоимость изготовления платы и время на пайку. То есть, по сравнению с массовыми демонстрационными платами от STM или Olimex, моя плата получилась очень уж дорогой.
Наверняка я реализовал многие вещи очень неэффективным способом. В текущей версии я сам вижу проблемы, которые нужно исправлять:
Проект опубликован на github [23] под лицензией GPL v3:
Спасибо за внимание!
Автор: Михаил Кулеш
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/raspberry-pi/281784
Ссылки в тексте:
[1] STM32F405RG: http://www.st.com/en/microcontrollers/stm32f405rg.html
[2] STF203-22.TCT: https://www.mouser.co.uk/datasheet/2/761/stf203-xx-1278105.pdf
[3] P-channel MOSFET BSH205: https://www.mouser.co.uk/datasheet/2/916/BSH205G2-1319564.pdf
[4] UDA1334: https://www.mouser.co.uk/datasheet/2/302/UDA1334BTS-1152159.pdf
[5] 31AP2005: https://www.mouser.co.uk/datasheet/2/198/31AP2005-535750.pdf
[6] AT: https://www.espressif.com/sites/default/files/documentation/4a-esp8266_at_instruction_set_en.pdf
[7] Image: https://habrastorage.org/webt/8v/6r/ed/8v6redrsu8h59-mpxt6ngls3rqc.jpeg
[8] Image: https://habrastorage.org/webt/8s/5i/iw/8s5iiwv5plsuntqckzkw11ba1lu.jpeg
[9] esptool: https://github.com/espressif/esptool
[10] Image: https://habrastorage.org/webt/4j/mf/ov/4jmfovwj6qlrde5t55naikl5sqq.jpeg
[11] Image: https://habrastorage.org/webt/ju/2n/vb/ju2nvbuf7vaqnjrc6idvj1cdoag.jpeg
[12] ESP8266 AT Bin V1.6.1: https://www.espressif.com/en/products/hardware/esp8266ex/resources
[13] эту: https://bbs.espressif.com/viewtopic.php?t=2451
[14] github: https://github.com/mkulesh/stm32DevelopmentBoards/tree/master/src/PI405RG/src
[15] раз: https://habr.com/post/412753
[16] два: https://habr.com/post/407083
[17] три: https://habr.com/post/337622
[18] замечательная статья: https://habr.com/post/347980
[19] github: https://github.com/mkulesh/stm32DevelopmentBoards/tree/master/src/StmPlusPlus
[20] Здесь: https://habr.com/post/357910
[21] github: https://www.pvsm.ruhttp://htmlpreview.github.io/?https://github.com/mkulesh/stm32DevelopmentBoards/blob/master/pcb/stm32_pi_board_bom.html
[22] Mouser: https://www.mouser.de/
[23] github: https://github.com/mkulesh/stm32DevelopmentBoards
[24] Схема: https://docs.google.com/viewer?url=https://github.com/mkulesh/stm32DevelopmentBoards/raw/master/pcb/stm32_pi_board_sch.pdf
[25] Размеры: https://docs.google.com/viewer?url=https://github.com/mkulesh/stm32DevelopmentBoards/raw/master/pcb/stm32_pi_board_mech.pdf
[26] Верхний слой: https://docs.google.com/viewer?url=https://github.com/mkulesh/stm32DevelopmentBoards/raw/master/pcb/stm32_pi_board_top.pdf
[27] Нижний слой: https://docs.google.com/viewer?url=https://github.com/mkulesh/stm32DevelopmentBoards/raw/master/pcb/stm32_pi_board_bottom.pdf
[28] Источник: https://habr.com/post/413101/?utm_source=habrahabr&utm_medium=rss&utm_campaign=413101
Нажмите здесь для печати.